[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Client (please complete the following information):**\n - OS: [e.g. iOS]\n - Browser: [e.g. chrome, safari]\n - Browser version: [e.g. 22]\n\n**Server (please complete the following information):**\n - noVNC version: [e.g. 1.0.0 or git commit id]\n - VNC server: [e.g. QEMU, TigerVNC]\n - WebSocket proxy: [e.g. websockify]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Question or discussion\n    url: https://groups.google.com/forum/?fromgroups#!forum/novnc\n    about: Ask a question or start a discussion\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: Publish\n\non:\n  push:\n  pull_request:\n  release:\n    types: [published]\n\njobs:\n  npm:\n    runs-on: ubuntu-latest\n    permissions:\n      id-token: write\n      contents: read\n    steps:\n      - uses: actions/checkout@v4\n      - run: |\n          GITREV=$(git rev-parse --short HEAD)\n          echo $GITREV\n          sed -i \"s/^\\(.*\\\"version\\\".*\\)\\\"\\([^\\\"]\\+\\)\\\"\\(.*\\)\\$/\\1\\\"\\2-g$GITREV\\\"\\3/\" package.json\n        if: github.event_name != 'release'\n      - uses: actions/setup-node@v4\n        with:\n          # Node 24 is needed to get npm > 11.5.1, which is a requirement for\n          # OIDC auth.\n          node-version: 24\n          # Needs to be explicitly specified for auth to work\n          registry-url: 'https://registry.npmjs.org'\n      - run: npm install\n      - uses: actions/upload-artifact@v4\n        with:\n          name: npm\n          path: lib\n      - run: npm publish --access public\n        if: |\n          github.repository == 'novnc/noVNC' &&\n          github.event_name == 'release' &&\n          !github.event.release.prerelease\n      - run: npm publish --access public --tag beta\n        if: |\n          github.repository == 'novnc/noVNC' &&\n          github.event_name == 'release' &&\n          github.event.release.prerelease\n      - run: npm publish --access public --tag dev\n        if: |\n          github.repository == 'novnc/noVNC' &&\n          github.event_name == 'push' &&\n          github.event.ref == 'refs/heads/master'\n  snap:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - run: |\n          GITREV=$(git rev-parse --short HEAD)\n          echo $GITREV\n          sed -i \"s/^\\(.*\\\"version\\\".*\\)\\\"\\([^\\\"]\\+\\)\\\"\\(.*\\)\\$/\\1\\\"\\2-g$GITREV\\\"\\3/\" package.json\n        if: github.event_name != 'release'\n      - run: |\n          VERSION=$(grep '\"version\"' package.json | cut -d '\"' -f 4)\n          echo $VERSION\n          sed -i \"s/^version:.*/version: '$VERSION'/\" snap/snapcraft.yaml\n      - uses: snapcore/action-build@v1\n        id: snapcraft\n      - uses: actions/upload-artifact@v4\n        with:\n          name: snap\n          path: ${{ steps.snapcraft.outputs.snap }}\n      - uses: snapcore/action-publish@v1\n        with:\n          snap: ${{ steps.snapcraft.outputs.snap }}\n          release: stable\n        env:\n          SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_LOGIN }}\n        if: |\n          github.repository == 'novnc/noVNC' &&\n          github.event_name == 'release' &&\n          !github.event.release.prerelease\n      - uses: snapcore/action-publish@v1\n        with:\n          snap: ${{ steps.snapcraft.outputs.snap }}\n          release: beta\n        env:\n          SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_LOGIN }}\n        if: |\n          github.repository == 'novnc/noVNC' &&\n          github.event_name == 'release' &&\n          github.event.release.prerelease\n      - uses: snapcore/action-publish@v1\n        with:\n          snap: ${{ steps.snapcraft.outputs.snap }}\n          release: edge\n        env:\n          SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_LOGIN }}\n        if: |\n          github.repository == 'novnc/noVNC' &&\n          github.event_name == 'push' &&\n          github.event.ref == 'refs/heads/master'\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Lint\n\non: [push, pull_request]\n\njobs:\n  eslint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n      - run: npm update\n      - run: npm run lint\n  html:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n      - run: npm update\n      - run: git ls-tree --name-only -r HEAD | grep -E \"[.](html|css)$\" | xargs ./utils/validate\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\n\non: [push, pull_request]\n\njobs:\n  test:\n    strategy:\n      matrix:\n        os:\n          - ubuntu-latest\n          - windows-latest\n        browser:\n          - ChromeHeadless\n          - FirefoxHeadless\n        include:\n          - os: macos-latest\n            browser: Safari\n          - os: windows-latest\n            browser: EdgeHeadless\n      fail-fast: false\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n      - run: npm update\n      - run: npm run test\n        env:\n          TEST_BROWSER_NAME: ${{ matrix.browser }}\n"
  },
  {
    "path": ".github/workflows/translate.yml",
    "content": "name: Translate\n\non: [push, pull_request]\n\njobs:\n  translate:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n      - run: npm update\n      - run: sudo apt-get install gettext\n      - run: make -C po update-pot\n      - run: make -C po update-po\n      - run: make -C po update-js\n"
  },
  {
    "path": ".gitignore",
    "content": "*.pyc\n*.o\ntests/data_*.js\nutils/rebind.so\nutils/websockify\n/node_modules\n/build\n/lib\nrecordings\n*.swp\n*~\nnoVNC-*.tgz\n"
  },
  {
    "path": ".gitmodules",
    "content": ""
  },
  {
    "path": "AUTHORS",
    "content": "maintainers:\n- Samuel Mannehed for Cendio AB (@samhed)\n- Pierre Ossman for Cendio AB (@CendioOssman)\nmaintainersEmeritus:\n- Joel Martin (@kanaka)\n- Solly Ross (@directxman12)\n- @astrand \ncontributors:\n# There are a bunch of people that should be here.\n# If you want to be on this list, feel free send a PR\n# to add yourself.\n- jalf <git@jalf.dk>\n- NTT corp.\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "noVNC is Copyright (C) 2022 The noVNC authors\n(./AUTHORS)\n\nThe noVNC core library files are licensed under the MPL 2.0 (Mozilla\nPublic License 2.0). The noVNC core library is composed of the\nJavascript code necessary for full noVNC operation. This includes (but\nis not limited to):\n\n    core/**/*.js\n    app/*.js\n    test/playback.js\n\nThe HTML, CSS, font and images files that included with the noVNC\nsource distibution (or repository) are not considered part of the\nnoVNC core library and are licensed under more permissive licenses.\nThe intent is to allow easy integration of noVNC into existing web\nsites and web applications.\n\nThe HTML, CSS, font and image files are licensed as follows:\n\n    *.html                     : 2-Clause BSD license\n\n    app/styles/*.css           : 2-Clause BSD license\n\n    app/styles/Orbitron*       : SIL Open Font License 1.1\n                                 (Copyright 2009 Matt McInerney)\n\n    app/images/                : Creative Commons Attribution-ShareAlike\n                                 http://creativecommons.org/licenses/by-sa/3.0/\n\nSome portions of noVNC are copyright to their individual authors.\nPlease refer to the individual source files and/or to the noVNC commit\nhistory: https://github.com/novnc/noVNC/commits/master\n\nThe are several files and projects that have been incorporated into\nthe noVNC core library. Here is a list of those files and the original\nlicenses (all MPL 2.0 compatible):\n\n    core/base64.js          : MPL 2.0\n\n    core/des.js             : Various BSD style licenses\n\n    vendor/pako/            : MIT\n\nAny other files not mentioned above are typically marked with\na copyright/license header at the top of the file. The default noVNC\nlicense is MPL-2.0.\n\nThe following license texts are included:\n\n    docs/LICENSE.MPL-2.0\n    docs/LICENSE.OFL-1.1\n    docs/LICENSE.BSD-3-Clause (New BSD)\n    docs/LICENSE.BSD-2-Clause (Simplified BSD / FreeBSD)\n    vendor/pako/LICENSE (MIT)\n\nOr alternatively the license texts may be found here:\n\n    http://www.mozilla.org/MPL/2.0/\n    http://scripts.sil.org/OFL\n    http://en.wikipedia.org/wiki/BSD_licenses\n    https://opensource.org/licenses/MIT\n"
  },
  {
    "path": "README.md",
    "content": "## noVNC: HTML VNC client library and application\n\n[![Test Status](https://github.com/novnc/noVNC/workflows/Test/badge.svg)](https://github.com/novnc/noVNC/actions?query=workflow%3ATest)\n[![Lint Status](https://github.com/novnc/noVNC/workflows/Lint/badge.svg)](https://github.com/novnc/noVNC/actions?query=workflow%3ALint)\n\n### Description\n\nnoVNC is both a HTML VNC client JavaScript library and an application built on\ntop of that library. noVNC runs well in any modern browser including mobile\nbrowsers (iOS and Android).\n\nMany companies, projects and products have integrated noVNC including\n[OpenStack](http://www.openstack.org),\n[OpenNebula](http://opennebula.org/),\n[LibVNCServer](http://libvncserver.sourceforge.net), and\n[ThinLinc](https://cendio.com/thinlinc). See\n[the Projects and companies wiki page](https://github.com/novnc/noVNC/wiki/Projects-and-companies-using-noVNC)\nfor a more complete list with additional info and links.\n\n### Table of contents\n\n- [News/help/contact](#newshelpcontact)\n- [Features](#features)\n- [Screenshots](#screenshots)\n- [Browser requirements](#browser-requirements)\n- [Server requirements](#server-requirements)\n- [Quick start](#quick-start)\n- [Installation from snap package](#installation-from-snap-package)\n- [Integration and deployment](#integration-and-deployment)\n- [Authors/Contributors](#authorscontributors)\n\n### News/help/contact\n\nThe project website is found at [novnc.com](http://novnc.com).\n\nIf you are a noVNC developer/integrator/user (or want to be) please join the\n[noVNC discussion group](https://groups.google.com/forum/?fromgroups#!forum/novnc).\n\nBugs and feature requests can be submitted via\n[github issues](https://github.com/novnc/noVNC/issues). If you have questions\nabout using noVNC then please first use the\n[discussion group](https://groups.google.com/forum/?fromgroups#!forum/novnc).\nWe also have a [wiki](https://github.com/novnc/noVNC/wiki/) with lots of\nhelpful information.\n\nIf you are looking for a place to start contributing to noVNC, a good place to\nstart would be the issues that are marked as\n[\"patchwelcome\"](https://github.com/novnc/noVNC/issues?labels=patchwelcome).\nPlease check our\n[contribution guide](https://github.com/novnc/noVNC/wiki/Contributing) though.\n\nIf you want to show appreciation for noVNC you could donate to a great non-\nprofits such as:\n[Compassion International](http://www.compassion.com/),\n[SIL](http://www.sil.org),\n[Habitat for Humanity](http://www.habitat.org),\n[Electronic Frontier Foundation](https://www.eff.org/),\n[Against Malaria Foundation](http://www.againstmalaria.com/),\n[Nothing But Nets](http://www.nothingbutnets.net/), etc.\n\n\n### Features\n\n* Supports all modern browsers including mobile (iOS, Android)\n* Supported authentication methods: none, classical VNC, RealVNC's\n  RSA-AES, Tight, VeNCrypt Plain, XVP, Apple's Diffie-Hellman,\n  UltraVNC's MSLogonII\n* Supported VNC encodings: raw, copyrect, rre, hextile, tight, tightPNG,\n  ZRLE, JPEG, Zlib, H.264\n* Supports scaling, clipping and resizing the desktop\n* Supports back & forward mouse buttons\n* Local cursor rendering\n* Clipboard copy/paste with full Unicode support\n* Translations\n* Touch gestures for emulating common mouse actions\n* Licensed mainly under the [MPL 2.0](http://www.mozilla.org/MPL/2.0/), see\n  [the license document](LICENSE.txt) for details\n\n### Screenshots\n\nRunning in Firefox before and after connecting:\n\n<img src=\"http://novnc.com/img/noVNC-1-login.png\" width=400>&nbsp;\n<img src=\"http://novnc.com/img/noVNC-3-connected.png\" width=400>\n\nSee more screenshots\n[here](http://novnc.com/screenshots.html).\n\n\n### Browser requirements\n\nnoVNC uses many modern web technologies so a formal requirement list is\nnot available. However these are the minimum versions we are currently\naware of:\n\n* Chrome 89, Firefox 89, Safari 15, Opera 75, Edge 89\n\n\n### Server requirements\n\nnoVNC follows the standard VNC protocol, but unlike other VNC clients it does\nrequire WebSockets support. Many servers include support (e.g.\n[x11vnc/libvncserver](http://libvncserver.sourceforge.net/),\n[QEMU](http://www.qemu.org/), and\n[MobileVNC](http://www.smartlab.at/mobilevnc/)), but for the others you need to\nuse a WebSockets to TCP socket proxy. noVNC has a sister project\n[websockify](https://github.com/novnc/websockify) that provides a simple such\nproxy.\n\n\n### Quick start\n\n* Use the `novnc_proxy` script to automatically download and start websockify, which\n  includes a mini-webserver and the WebSockets proxy. The `--vnc` option is\n  used to specify the location of a running VNC server:\n\n    `./utils/novnc_proxy --vnc localhost:5901`\n    \n* If you don't need to expose the web server to public internet, you can\n  bind to localhost:\n  \n    `./utils/novnc_proxy --vnc localhost:5901 --listen localhost:6081`\n\n* Point your browser to the cut-and-paste URL that is output by the `novnc_proxy`\n  script. Hit the Connect button, enter a password if the VNC server has one\n  configured, and enjoy!\n\n### Installation from snap package\nRunning the command below will install the latest release of noVNC from snap:\n\n`sudo snap install novnc`\n\n#### Running noVNC from snap directly\n\nYou can run the snap package installed novnc directly with, for example:\n\n`novnc --listen 6081 --vnc localhost:5901 # /snap/bin/novnc if /snap/bin is not in your PATH`\n\nIf you want to use certificate files, due to standard snap confinement restrictions you need to have them in the /home/\\<user\\>/snap/novnc/current/ directory. If your username is jsmith an example command would be:\n  \n  `novnc --listen 8443 --cert ~jsmith/snap/novnc/current/self.crt --key ~jsmith/snap/novnc/current/self.key --vnc ubuntu.example.com:5901`\n\n#### Running noVNC from snap as a service (daemon)\nThe snap package also has the capability to run a 'novnc' service which can be\nconfigured to listen on multiple ports connecting to multiple VNC servers \n(effectively a service running multiple instances of novnc).\nInstructions (with example values):\n\nList current services (out-of-box this will be blank):\n\n```\nsudo snap get novnc services\nKey             Value\nservices.n6080  {...}\nservices.n6081  {...}\n```\n\nCreate a new service that listens on port 6082 and connects to the VNC server \nrunning on port 5902 on localhost:\n\n`sudo snap set novnc services.n6082.listen=6082 services.n6082.vnc=localhost:5902`\n\n(Any services you define with 'snap set' will be automatically started)\nNote that the name of the service, 'n6082' in this example, can be anything \nas long as it doesn't start with a number or contain spaces/special characters.\n\nView the configuration of the service just created:\n\n```\nsudo snap get novnc services.n6082\nKey                    Value\nservices.n6082.listen  6082\nservices.n6082.vnc     localhost:5902\n```\n\nDisable a service (note that because of a limitation in snap it's currently not\npossible to unset config variables, setting them to blank values is the way \nto disable a service):\n\n`sudo snap set novnc services.n6082.listen='' services.n6082.vnc=''`\n\n(Any services you set to blank with 'snap set' like this will be automatically stopped)\n\nVerify that the service is disabled (blank values):\n\n```\nsudo snap get novnc services.n6082\nKey                    Value\nservices.n6082.listen  \nservices.n6082.vnc\n```\n\n### Integration and deployment\n\nPlease see our other documents for how to integrate noVNC in your own software,\nor deploying the noVNC application in production environments:\n\n* [Embedding](docs/EMBEDDING.md) - For the noVNC application\n* [Library](docs/LIBRARY.md) - For the noVNC JavaScript library\n\n\n### Authors/Contributors\n\nSee [AUTHORS](AUTHORS) for a (full-ish) list of authors.  If you're not on\nthat list and you think you should be, feel free to send a PR to fix that.\n\n* Core team:\n    * [Samuel Mannehed](https://github.com/samhed) (Cendio)\n    * [Pierre Ossman](https://github.com/CendioOssman) (Cendio)\n\n* Previous core contributors:\n    * [Joel Martin](https://github.com/kanaka) (Project founder)\n    * [Solly Ross](https://github.com/DirectXMan12) (Red Hat / OpenStack)\n\n* Notable contributions:\n    * UI and icons : Pierre Ossman, Chris Gordon\n    * Original logo : Michael Sersen\n    * tight encoding : Michael Tinglof (Mercuri.ca)\n    * RealVNC RSA AES authentication : USTC Vlab Team\n\n* Included libraries:\n    * base64 : Martijn Pieters (Digital Creations 2), Samuel Sieb (sieb.net)\n    * DES : Dave Zimmerman (Widget Workshop), Jef Poskanzer (ACME Labs)\n    * Pako : Vitaly Puzrin (https://github.com/nodeca/pako)\n\nDo you want to be on this list? Check out our\n[contribution guide](https://github.com/novnc/noVNC/wiki/Contributing) and\nstart hacking!\n"
  },
  {
    "path": "app/error-handler.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2019 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n */\n\n// Fallback for all uncaught errors\nfunction handleError(event, err) {\n    try {\n        const msg = document.getElementById('noVNC_fallback_errormsg');\n\n        // Work around Firefox bug:\n        // https://bugzilla.mozilla.org/show_bug.cgi?id=1685038\n        if (event.message === \"ResizeObserver loop completed with undelivered notifications.\") {\n            return false;\n        }\n\n        // Only show the initial error\n        if (msg.hasChildNodes()) {\n            return false;\n        }\n\n        let div = document.createElement(\"div\");\n        div.classList.add('noVNC_message');\n        div.appendChild(document.createTextNode(event.message));\n        msg.appendChild(div);\n\n        if (event.filename) {\n            div = document.createElement(\"div\");\n            div.className = 'noVNC_location';\n            let text = event.filename;\n            if (event.lineno !== undefined) {\n                text += \":\" + event.lineno;\n                if (event.colno !== undefined) {\n                    text += \":\" + event.colno;\n                }\n            }\n            div.appendChild(document.createTextNode(text));\n            msg.appendChild(div);\n        }\n\n        if (err && err.stack) {\n            div = document.createElement(\"div\");\n            div.className = 'noVNC_stack';\n            div.appendChild(document.createTextNode(err.stack));\n            msg.appendChild(div);\n        }\n\n        document.getElementById('noVNC_fallback_error')\n            .classList.add(\"noVNC_open\");\n\n    } catch (exc) {\n        document.write(\"noVNC encountered an error.\");\n    }\n\n    // Try to disable keyboard interaction, best effort\n    try {\n        // Remove focus from the currently focused element in order to\n        // prevent keyboard interaction from continuing\n        if (document.activeElement) { document.activeElement.blur(); }\n\n        // Don't let any element be focusable when showing the error\n        let keyboardFocusable = 'a[href], button, input, textarea, select, details, [tabindex]';\n        document.querySelectorAll(keyboardFocusable).forEach((elem) => {\n            elem.setAttribute(\"tabindex\", \"-1\");\n        });\n    } catch (exc) {\n        // Do nothing\n    }\n\n    // Don't return true since this would prevent the error\n    // from being printed to the browser console.\n    return false;\n}\n\nwindow.addEventListener('error', evt => handleError(evt, evt.error));\nwindow.addEventListener('unhandledrejection', evt => handleError(evt.reason, evt.reason));\n"
  },
  {
    "path": "app/images/icons/Makefile",
    "content": "BROWSER_SIZES := 16 24 32 48 64\n#ANDROID_SIZES := 72 96 144 192\n# FIXME: The ICO is limited to 8 icons due to a Chrome bug:\n#        https://bugs.chromium.org/p/chromium/issues/detail?id=1381393\nANDROID_SIZES := 96 144 192\nWEB_ICON_SIZES := $(BROWSER_SIZES) $(ANDROID_SIZES)\n\n#IOS_1X_SIZES := 20 29 40 76 # No such devices exist anymore\nIOS_2X_SIZES := 40 58 80 120 152 167\nIOS_3X_SIZES := 60 87 120 180\nALL_IOS_SIZES := $(IOS_1X_SIZES) $(IOS_2X_SIZES) $(IOS_3X_SIZES)\n\nALL_ICONS := \\\n\t$(ALL_IOS_SIZES:%=novnc-ios-%.png) \\\n\tnovnc.ico\n\nall: $(ALL_ICONS)\n\n# Our testing shows that the ICO file need to be sorted in largest to\n# smallest to get the apporpriate behviour\nWEB_ICON_SIZES_REVERSE := $(shell echo $(WEB_ICON_SIZES) | tr ' ' '\\n' | sort -nr | tr '\\n' ' ')\nWEB_BASE_ICONS := $(WEB_ICON_SIZES_REVERSE:%=novnc-%.png)\n.INTERMEDIATE: $(WEB_BASE_ICONS)\n\nnovnc.ico: $(WEB_BASE_ICONS)\n\tconvert $(WEB_BASE_ICONS) \"$@\"\n\n# General conversion\nnovnc-%.png: novnc-icon.svg\n\tconvert -depth 8 -background transparent \\\n\t\t-size $*x$* \"$(lastword $^)\" \"$@\"\n\n# iOS icons use their own SVG\nnovnc-ios-%.png: novnc-ios-icon.svg\n\tconvert -depth 8 -background transparent \\\n\t\t-size $*x$* \"$(lastword $^)\" \"$@\"\n\n# The smallest sizes are generated using a different SVG\nnovnc-16.png novnc-24.png novnc-32.png: novnc-icon-sm.svg\n\nclean:\n\trm -f *.png\n"
  },
  {
    "path": "app/locale/README",
    "content": "DO NOT MODIFY THE FILES IN THIS FOLDER, THEY ARE AUTOMATICALLY GENERATED FROM THE PO-FILES.\n"
  },
  {
    "path": "app/locale/cs.json",
    "content": "{\n    \"Connecting...\": \"Připojení...\",\n    \"Disconnecting...\": \"Odpojení...\",\n    \"Reconnecting...\": \"Obnova připojení...\",\n    \"Internal error\": \"Vnitřní chyba\",\n    \"Must set host\": \"Hostitel musí být nastavení\",\n    \"Connected (encrypted) to \": \"Připojení (šifrované) k \",\n    \"Connected (unencrypted) to \": \"Připojení (nešifrované) k \",\n    \"Something went wrong, connection is closed\": \"Něco se pokazilo, odpojeno\",\n    \"Failed to connect to server\": \"Chyba připojení k serveru\",\n    \"Disconnected\": \"Odpojeno\",\n    \"New connection has been rejected with reason: \": \"Nové připojení bylo odmítnuto s odůvodněním: \",\n    \"New connection has been rejected\": \"Nové připojení bylo odmítnuto\",\n    \"Password is required\": \"Je vyžadováno heslo\",\n    \"noVNC encountered an error:\": \"noVNC narazilo na chybu:\",\n    \"Hide/Show the control bar\": \"Skrýt/zobrazit ovládací panel\",\n    \"Move/Drag viewport\": \"Přesunout/přetáhnout výřez\",\n    \"viewport drag\": \"přesun výřezu\",\n    \"Active Mouse Button\": \"Aktivní tlačítka myši\",\n    \"No mousebutton\": \"Žádné\",\n    \"Left mousebutton\": \"Levé tlačítko myši\",\n    \"Middle mousebutton\": \"Prostřední tlačítko myši\",\n    \"Right mousebutton\": \"Pravé tlačítko myši\",\n    \"Keyboard\": \"Klávesnice\",\n    \"Show keyboard\": \"Zobrazit klávesnici\",\n    \"Extra keys\": \"Extra klávesy\",\n    \"Show extra keys\": \"Zobrazit extra klávesy\",\n    \"Ctrl\": \"Ctrl\",\n    \"Toggle Ctrl\": \"Přepnout Ctrl\",\n    \"Alt\": \"Alt\",\n    \"Toggle Alt\": \"Přepnout Alt\",\n    \"Send Tab\": \"Odeslat tabulátor\",\n    \"Tab\": \"Tab\",\n    \"Esc\": \"Esc\",\n    \"Send Escape\": \"Odeslat Esc\",\n    \"Ctrl+Alt+Del\": \"Ctrl+Alt+Del\",\n    \"Send Ctrl-Alt-Del\": \"Poslat Ctrl-Alt-Del\",\n    \"Shutdown/Reboot\": \"Vypnutí/Restart\",\n    \"Shutdown/Reboot...\": \"Vypnutí/Restart...\",\n    \"Power\": \"Napájení\",\n    \"Shutdown\": \"Vypnout\",\n    \"Reboot\": \"Restart\",\n    \"Reset\": \"Reset\",\n    \"Clipboard\": \"Schránka\",\n    \"Clear\": \"Vymazat\",\n    \"Fullscreen\": \"Celá obrazovka\",\n    \"Settings\": \"Nastavení\",\n    \"Shared mode\": \"Sdílený režim\",\n    \"View only\": \"Pouze prohlížení\",\n    \"Clip to window\": \"Přizpůsobit oknu\",\n    \"Scaling mode:\": \"Přizpůsobení velikosti\",\n    \"None\": \"Žádné\",\n    \"Local scaling\": \"Místní\",\n    \"Remote resizing\": \"Vzdálené\",\n    \"Advanced\": \"Pokročilé\",\n    \"Repeater ID:\": \"ID opakovače\",\n    \"WebSocket\": \"WebSocket\",\n    \"Encrypt\": \"Šifrování:\",\n    \"Host:\": \"Hostitel:\",\n    \"Port:\": \"Port:\",\n    \"Path:\": \"Cesta\",\n    \"Automatic reconnect\": \"Automatická obnova připojení\",\n    \"Reconnect delay (ms):\": \"Zpoždění připojení (ms)\",\n    \"Show dot when no cursor\": \"Tečka místo chybějícího kurzoru myši\",\n    \"Logging:\": \"Logování:\",\n    \"Disconnect\": \"Odpojit\",\n    \"Connect\": \"Připojit\",\n    \"Password:\": \"Heslo\",\n    \"Send Password\": \"Odeslat heslo\",\n    \"Cancel\": \"Zrušit\"\n}"
  },
  {
    "path": "app/locale/de.json",
    "content": "{\n    \"Connecting...\": \"Verbinden...\",\n    \"Disconnecting...\": \"Verbindung trennen...\",\n    \"Reconnecting...\": \"Verbindung wiederherstellen...\",\n    \"Internal error\": \"Interner Fehler\",\n    \"Must set host\": \"Richten Sie den Server ein\",\n    \"Connected (encrypted) to \": \"Verbunden mit (verschlüsselt) \",\n    \"Connected (unencrypted) to \": \"Verbunden mit (unverschlüsselt) \",\n    \"Something went wrong, connection is closed\": \"Etwas lief schief, Verbindung wurde getrennt\",\n    \"Disconnected\": \"Verbindung zum Server getrennt\",\n    \"New connection has been rejected with reason: \": \"Verbindung wurde aus folgendem Grund abgelehnt: \",\n    \"New connection has been rejected\": \"Verbindung wurde abgelehnt\",\n    \"Password is required\": \"Passwort ist erforderlich\",\n    \"noVNC encountered an error:\": \"Ein Fehler ist aufgetreten:\",\n    \"Hide/Show the control bar\": \"Kontrollleiste verstecken/anzeigen\",\n    \"Move/Drag viewport\": \"Ansichtsfenster verschieben/ziehen\",\n    \"viewport drag\": \"Ansichtsfenster ziehen\",\n    \"Active Mouse Button\": \"Aktive Maustaste\",\n    \"No mousebutton\": \"Keine Maustaste\",\n    \"Left mousebutton\": \"Linke Maustaste\",\n    \"Middle mousebutton\": \"Mittlere Maustaste\",\n    \"Right mousebutton\": \"Rechte Maustaste\",\n    \"Keyboard\": \"Tastatur\",\n    \"Show keyboard\": \"Tastatur anzeigen\",\n    \"Extra keys\": \"Zusatztasten\",\n    \"Show extra keys\": \"Zusatztasten anzeigen\",\n    \"Ctrl\": \"Strg\",\n    \"Toggle Ctrl\": \"Strg umschalten\",\n    \"Alt\": \"Alt\",\n    \"Toggle Alt\": \"Alt umschalten\",\n    \"Send Tab\": \"Tab senden\",\n    \"Tab\": \"Tab\",\n    \"Esc\": \"Esc\",\n    \"Send Escape\": \"Escape senden\",\n    \"Ctrl+Alt+Del\": \"Strg+Alt+Entf\",\n    \"Send Ctrl-Alt-Del\": \"Strg+Alt+Entf senden\",\n    \"Shutdown/Reboot\": \"Herunterfahren/Neustarten\",\n    \"Shutdown/Reboot...\": \"Herunterfahren/Neustarten...\",\n    \"Power\": \"Energie\",\n    \"Shutdown\": \"Herunterfahren\",\n    \"Reboot\": \"Neustarten\",\n    \"Reset\": \"Zurücksetzen\",\n    \"Clipboard\": \"Zwischenablage\",\n    \"Clear\": \"Löschen\",\n    \"Fullscreen\": \"Vollbild\",\n    \"Settings\": \"Einstellungen\",\n    \"Shared mode\": \"Geteilter Modus\",\n    \"View only\": \"Nur betrachten\",\n    \"Clip to window\": \"Auf Fenster begrenzen\",\n    \"Scaling mode:\": \"Skalierungsmodus:\",\n    \"None\": \"Keiner\",\n    \"Local scaling\": \"Lokales skalieren\",\n    \"Remote resizing\": \"Serverseitiges skalieren\",\n    \"Advanced\": \"Erweitert\",\n    \"Repeater ID:\": \"Repeater ID:\",\n    \"WebSocket\": \"WebSocket\",\n    \"Encrypt\": \"Verschlüsselt\",\n    \"Host:\": \"Server:\",\n    \"Port:\": \"Port:\",\n    \"Path:\": \"Pfad:\",\n    \"Automatic reconnect\": \"Automatisch wiederverbinden\",\n    \"Reconnect delay (ms):\": \"Wiederverbindungsverzögerung (ms):\",\n    \"Logging:\": \"Protokollierung:\",\n    \"Disconnect\": \"Verbindung trennen\",\n    \"Connect\": \"Verbinden\",\n    \"Password:\": \"Passwort:\",\n    \"Cancel\": \"Abbrechen\",\n    \"Canvas not supported.\": \"Canvas nicht unterstützt.\",\n    \"Disconnect timeout\": \"Zeitüberschreitung beim Trennen\",\n    \"Local Downscaling\": \"Lokales herunterskalieren\",\n    \"Local Cursor\": \"Lokaler Mauszeiger\",\n    \"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen\": \"'Clipping-Modus' aktiviert, Scrollbalken in 'IE-Vollbildmodus' werden nicht unterstützt\",\n    \"True Color\": \"True Color\"\n}"
  },
  {
    "path": "app/locale/el.json",
    "content": "{\n    \"HTTPS is required for full functionality\": \"Το HTTPS είναι απαιτούμενο για πλήρη λειτουργικότητα\",\n    \"Connecting...\": \"Συνδέεται...\",\n    \"Disconnecting...\": \"Aποσυνδέεται...\",\n    \"Reconnecting...\": \"Επανασυνδέεται...\",\n    \"Internal error\": \"Εσωτερικό σφάλμα\",\n    \"Must set host\": \"Πρέπει να οριστεί ο διακομιστής\",\n    \"Connected (encrypted) to \": \"Συνδέθηκε (κρυπτογραφημένα) με το \",\n    \"Connected (unencrypted) to \": \"Συνδέθηκε (μη κρυπτογραφημένα) με το \",\n    \"Something went wrong, connection is closed\": \"Κάτι πήγε στραβά, η σύνδεση διακόπηκε\",\n    \"Failed to connect to server\": \"Αποτυχία στη σύνδεση με το διακομιστή\",\n    \"Disconnected\": \"Αποσυνδέθηκε\",\n    \"New connection has been rejected with reason: \": \"Η νέα σύνδεση απορρίφθηκε διότι: \",\n    \"New connection has been rejected\": \"Η νέα σύνδεση απορρίφθηκε \",\n    \"Credentials are required\": \"Απαιτούνται διαπιστευτήρια\",\n    \"noVNC encountered an error:\": \"το noVNC αντιμετώπισε ένα σφάλμα:\",\n    \"Hide/Show the control bar\": \"Απόκρυψη/Εμφάνιση γραμμής ελέγχου\",\n    \"Drag\": \"Σύρσιμο\",\n    \"Move/Drag Viewport\": \"Μετακίνηση/Σύρσιμο Θεατού πεδίου\",\n    \"Keyboard\": \"Πληκτρολόγιο\",\n    \"Show Keyboard\": \"Εμφάνιση Πληκτρολογίου\",\n    \"Extra keys\": \"Επιπλέον πλήκτρα\",\n    \"Show Extra Keys\": \"Εμφάνιση Επιπλέον Πλήκτρων\",\n    \"Ctrl\": \"Ctrl\",\n    \"Toggle Ctrl\": \"Εναλλαγή Ctrl\",\n    \"Alt\": \"Alt\",\n    \"Toggle Alt\": \"Εναλλαγή Alt\",\n    \"Toggle Windows\": \"Εναλλαγή Παράθυρων\",\n    \"Windows\": \"Παράθυρα\",\n    \"Send Tab\": \"Αποστολή Tab\",\n    \"Tab\": \"Tab\",\n    \"Esc\": \"Esc\",\n    \"Send Escape\": \"Αποστολή Escape\",\n    \"Ctrl+Alt+Del\": \"Ctrl+Alt+Del\",\n    \"Send Ctrl-Alt-Del\": \"Αποστολή Ctrl-Alt-Del\",\n    \"Shutdown/Reboot\": \"Κλείσιμο/Επανεκκίνηση\",\n    \"Shutdown/Reboot...\": \"Κλείσιμο/Επανεκκίνηση...\",\n    \"Power\": \"Απενεργοποίηση\",\n    \"Shutdown\": \"Κλείσιμο\",\n    \"Reboot\": \"Επανεκκίνηση\",\n    \"Reset\": \"Επαναφορά\",\n    \"Clipboard\": \"Πρόχειρο\",\n    \"Edit clipboard content in the textarea below.\": \"Επεξεργαστείτε το περιεχόμενο του πρόχειρου στην περιοχή κειμένου παρακάτω.\",\n    \"Full Screen\": \"Πλήρης Οθόνη\",\n    \"Settings\": \"Ρυθμίσεις\",\n    \"Shared Mode\": \"Κοινόχρηστη Λειτουργία\",\n    \"View Only\": \"Μόνο Θέαση\",\n    \"Clip to Window\": \"Αποκοπή στο όριο του Παράθυρου\",\n    \"Scaling Mode:\": \"Λειτουργία Κλιμάκωσης:\",\n    \"None\": \"Καμία\",\n    \"Local Scaling\": \"Τοπική Κλιμάκωση\",\n    \"Remote Resizing\": \"Απομακρυσμένη Αλλαγή μεγέθους\",\n    \"Advanced\": \"Για προχωρημένους\",\n    \"Quality:\": \"Ποιότητα:\",\n    \"Compression level:\": \"Επίπεδο συμπίεσης:\",\n    \"Repeater ID:\": \"Repeater ID:\",\n    \"WebSocket\": \"WebSocket\",\n    \"Encrypt\": \"Κρυπτογράφηση\",\n    \"Host:\": \"Όνομα διακομιστή:\",\n    \"Port:\": \"Πόρτα διακομιστή:\",\n    \"Path:\": \"Διαδρομή:\",\n    \"Automatic Reconnect\": \"Αυτόματη επανασύνδεση\",\n    \"Reconnect Delay (ms):\": \"Καθυστέρηση επανασύνδεσης (ms):\",\n    \"Show Dot when No Cursor\": \"Εμφάνιση Τελείας όταν δεν υπάρχει Δρομέας\",\n    \"Logging:\": \"Καταγραφή:\",\n    \"Version:\": \"Έκδοση:\",\n    \"Disconnect\": \"Αποσύνδεση\",\n    \"Connect\": \"Σύνδεση\",\n    \"Server identity\": \"Ταυτότητα Διακομιστή\",\n    \"The server has provided the following identifying information:\": \"Ο διακομιστής παρείχε την ακόλουθη πληροφορία ταυτοποίησης:\",\n    \"Fingerprint:\": \"Δακτυλικό αποτύπωμα:\",\n    \"Please verify that the information is correct and press \\\"Approve\\\". Otherwise press \\\"Reject\\\".\": \"Παρακαλώ επαληθεύσετε ότι η πληροφορία είναι σωστή και πιέστε \\\"Αποδοχή\\\". Αλλιώς πιέστε \\\"Απόρριψη\\\".\",\n    \"Approve\": \"Αποδοχή\",\n    \"Reject\": \"Απόρριψη\",\n    \"Credentials\": \"Διαπιστευτήρια\",\n    \"Username:\": \"Κωδικός Χρήστη:\",\n    \"Password:\": \"Κωδικός Πρόσβασης:\",\n    \"Send Credentials\": \"Αποστολή Διαπιστευτηρίων\",\n    \"Cancel\": \"Ακύρωση\",\n    \"Password is required\": \"Απαιτείται ο κωδικός πρόσβασης\",\n    \"viewport drag\": \"σύρσιμο θεατού πεδίου\",\n    \"Active Mouse Button\": \"Ενεργό Πλήκτρο Ποντικιού\",\n    \"No mousebutton\": \"Χωρίς Πλήκτρο Ποντικιού\",\n    \"Left mousebutton\": \"Αριστερό Πλήκτρο Ποντικιού\",\n    \"Middle mousebutton\": \"Μεσαίο Πλήκτρο Ποντικιού\",\n    \"Right mousebutton\": \"Δεξί Πλήκτρο Ποντικιού\",\n    \"Clear\": \"Καθάρισμα\",\n    \"Canvas not supported.\": \"Δεν υποστηρίζεται το στοιχείο Canvas\",\n    \"Disconnect timeout\": \"Παρέλευση χρονικού ορίου αποσύνδεσης\",\n    \"Local Downscaling\": \"Τοπική Συρρίκνωση\",\n    \"Local Cursor\": \"Τοπικός Δρομέας\",\n    \"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen\": \"Εφαρμογή λειτουργίας αποκοπής αφού δεν υποστηρίζονται οι λωρίδες κύλισης σε πλήρη οθόνη στον IE\",\n    \"True Color\": \"Πραγματικά Χρώματα\",\n    \"Style:\": \"Στυλ:\",\n    \"default\": \"προεπιλεγμένο\",\n    \"Apply\": \"Εφαρμογή\",\n    \"Connection\": \"Σύνδεση\",\n    \"Token:\": \"Διακριτικό:\",\n    \"Send Password\": \"Αποστολή Κωδικού Πρόσβασης\"\n}"
  },
  {
    "path": "app/locale/es.json",
    "content": "{\n    \"Connecting...\": \"Conectando...\",\n    \"Connected (encrypted) to \": \"Conectado (con encriptación) a\",\n    \"Connected (unencrypted) to \": \"Conectado (sin encriptación) a\",\n    \"Disconnecting...\": \"Desconectando...\",\n    \"Disconnected\": \"Desconectado\",\n    \"Must set host\": \"Se debe configurar el host\",\n    \"Reconnecting...\": \"Reconectando...\",\n    \"Password is required\": \"La contraseña es obligatoria\",\n    \"Disconnect timeout\": \"Tiempo de desconexión agotado\",\n    \"noVNC encountered an error:\": \"noVNC ha encontrado un error:\",\n    \"Hide/Show the control bar\": \"Ocultar/Mostrar la barra de control\",\n    \"Move/Drag viewport\": \"Mover/Arrastrar la ventana\",\n    \"viewport drag\": \"Arrastrar la ventana\",\n    \"Active Mouse Button\": \"Botón activo del ratón\",\n    \"No mousebutton\": \"Ningún botón del ratón\",\n    \"Left mousebutton\": \"Botón izquierdo del ratón\",\n    \"Middle mousebutton\": \"Botón central del ratón\",\n    \"Right mousebutton\": \"Botón derecho del ratón\",\n    \"Keyboard\": \"Teclado\",\n    \"Show keyboard\": \"Mostrar teclado\",\n    \"Extra keys\": \"Teclas adicionales\",\n    \"Show Extra Keys\": \"Mostrar Teclas Adicionales\",\n    \"Ctrl\": \"Ctrl\",\n    \"Toggle Ctrl\": \"Pulsar/Soltar Ctrl\",\n    \"Alt\": \"Alt\",\n    \"Toggle Alt\": \"Pulsar/Soltar Alt\",\n    \"Send Tab\": \"Enviar Tabulación\",\n    \"Tab\": \"Tabulación\",\n    \"Esc\": \"Esc\",\n    \"Send Escape\": \"Enviar Escape\",\n    \"Ctrl+Alt+Del\": \"Ctrl+Alt+Del\",\n    \"Send Ctrl-Alt-Del\": \"Enviar Ctrl+Alt+Del\",\n    \"Shutdown/Reboot\": \"Apagar/Reiniciar\",\n    \"Shutdown/Reboot...\": \"Apagar/Reiniciar...\",\n    \"Power\": \"Encender\",\n    \"Shutdown\": \"Apagar\",\n    \"Reboot\": \"Reiniciar\",\n    \"Reset\": \"Restablecer\",\n    \"Clipboard\": \"Portapapeles\",\n    \"Clear\": \"Vaciar\",\n    \"Fullscreen\": \"Pantalla Completa\",\n    \"Settings\": \"Configuraciones\",\n    \"Encrypt\": \"Encriptar\",\n    \"Shared Mode\": \"Modo Compartido\",\n    \"View only\": \"Solo visualización\",\n    \"Clip to window\": \"Recortar al tamaño de la ventana\",\n    \"Scaling mode:\": \"Modo de escalado:\",\n    \"None\": \"Ninguno\",\n    \"Local Scaling\": \"Escalado Local\",\n    \"Local Downscaling\": \"Reducción de escala local\",\n    \"Remote resizing\": \"Cambio de tamaño remoto\",\n    \"Advanced\": \"Avanzado\",\n    \"Local Cursor\": \"Cursor Local\",\n    \"Repeater ID:\": \"ID del Repetidor:\",\n    \"WebSocket\": \"WebSocket\",\n    \"Host:\": \"Host:\",\n    \"Port:\": \"Puerto:\",\n    \"Path:\": \"Ruta:\",\n    \"Automatic reconnect\": \"Reconexión automática\",\n    \"Reconnect delay (ms):\": \"Retraso en la reconexión (ms):\",\n    \"Logging:\": \"Registrando:\",\n    \"Disconnect\": \"Desconectar\",\n    \"Connect\": \"Conectar\",\n    \"Password:\": \"Contraseña:\",\n    \"Cancel\": \"Cancelar\",\n    \"Canvas not supported.\": \"Canvas no soportado.\"\n}"
  },
  {
    "path": "app/locale/fr.json",
    "content": "{\n    \"Running without HTTPS is not recommended, crashes or other issues are likely.\": \"Lancer sans HTTPS n'est pas recommandé, crashs ou autres problèmes en vue.\",\n    \"Connecting...\": \"En cours de connexion...\",\n    \"Disconnecting...\": \"Déconnexion en cours...\",\n    \"Reconnecting...\": \"Reconnexion en cours...\",\n    \"Internal error\": \"Erreur interne\",\n    \"Failed to connect to server: \": \"Échec de connexion au serveur \",\n    \"Connected (encrypted) to \": \"Connecté (chiffré) à \",\n    \"Connected (unencrypted) to \": \"Connecté (non chiffré) à \",\n    \"Something went wrong, connection is closed\": \"Quelque chose s'est mal passé, la connexion a été fermée\",\n    \"Failed to connect to server\": \"Échec de connexion au serveur\",\n    \"Disconnected\": \"Déconnecté\",\n    \"New connection has been rejected with reason: \": \"Une nouvelle connexion a été rejetée avec motif : \",\n    \"New connection has been rejected\": \"Une nouvelle connexion a été rejetée\",\n    \"Credentials are required\": \"Les identifiants sont requis\",\n    \"noVNC encountered an error:\": \"noVNC a rencontré une erreur :\",\n    \"Hide/Show the control bar\": \"Masquer/Afficher la barre de contrôle\",\n    \"Drag\": \"Faire glisser\",\n    \"Move/Drag viewport\": \"Déplacer la fenêtre de visualisation\",\n    \"Keyboard\": \"Clavier\",\n    \"Show keyboard\": \"Afficher le clavier\",\n    \"Extra keys\": \"Touches supplémentaires\",\n    \"Show extra keys\": \"Afficher les touches supplémentaires\",\n    \"Ctrl\": \"Ctrl\",\n    \"Toggle Ctrl\": \"Basculer Ctrl\",\n    \"Alt\": \"Alt\",\n    \"Toggle Alt\": \"Basculer Alt\",\n    \"Toggle Windows\": \"Basculer Windows\",\n    \"Windows\": \"Fenêtre\",\n    \"Send Tab\": \"Envoyer Tab\",\n    \"Tab\": \"Tabulation\",\n    \"Esc\": \"Esc\",\n    \"Send Escape\": \"Envoyer Escape\",\n    \"Ctrl+Alt+Del\": \"Ctrl+Alt+Del\",\n    \"Send Ctrl-Alt-Del\": \"Envoyer Ctrl-Alt-Del\",\n    \"Shutdown/Reboot\": \"Arrêter/Redémarrer\",\n    \"Shutdown/Reboot...\": \"Arrêter/Redémarrer...\",\n    \"Power\": \"Alimentation\",\n    \"Shutdown\": \"Arrêter\",\n    \"Reboot\": \"Redémarrer\",\n    \"Reset\": \"Réinitialiser\",\n    \"Clipboard\": \"Presse-papiers\",\n    \"Edit clipboard content in the textarea below.\": \"Editer le contenu du presse-papier dans la zone ci-dessous.\",\n    \"Full screen\": \"Plein écran\",\n    \"Settings\": \"Paramètres\",\n    \"Shared mode\": \"Mode partagé\",\n    \"View only\": \"Afficher uniquement\",\n    \"Clip to window\": \"Ajuster à la fenêtre\",\n    \"Scaling mode:\": \"Mode mise à l'échelle :\",\n    \"None\": \"Aucun\",\n    \"Local scaling\": \"Mise à l'échelle locale\",\n    \"Remote resizing\": \"Redimensionnement à distance\",\n    \"Advanced\": \"Avancé\",\n    \"Quality:\": \"Qualité :\",\n    \"Compression level:\": \"Niveau de compression :\",\n    \"Repeater ID:\": \"ID Répéteur :\",\n    \"WebSocket\": \"WebSocket\",\n    \"Encrypt\": \"Chiffrer\",\n    \"Host:\": \"Hôte :\",\n    \"Port:\": \"Port :\",\n    \"Path:\": \"Chemin :\",\n    \"Automatic reconnect\": \"Reconnecter automatiquement\",\n    \"Reconnect delay (ms):\": \"Délai de reconnexion (ms) :\",\n    \"Show dot when no cursor\": \"Afficher le point lorsqu'il n'y a pas de curseur\",\n    \"Logging:\": \"Se connecter :\",\n    \"Version:\": \"Version :\",\n    \"Disconnect\": \"Déconnecter\",\n    \"Connect\": \"Connecter\",\n    \"Server identity\": \"Identité du serveur\",\n    \"The server has provided the following identifying information:\": \"Le serveur a fourni l'identification suivante :\",\n    \"Fingerprint:\": \"Empreinte digitale :\",\n    \"Please verify that the information is correct and press \\\"Approve\\\". Otherwise press \\\"Reject\\\".\": \"SVP, verifiez que l'information est correcte et pressez \\\"Accepter\\\". Sinon pressez \\\"Refuser\\\".\",\n    \"Approve\": \"Accepter\",\n    \"Reject\": \"Refuser\",\n    \"Credentials\": \"Envoyer les identifiants\",\n    \"Username:\": \"Nom d'utilisateur :\",\n    \"Password:\": \"Mot de passe :\",\n    \"Send credentials\": \"Envoyer les identifiants\",\n    \"Cancel\": \"Annuler\",\n    \"Must set host\": \"Doit définir l'hôte\",\n    \"Clear\": \"Effacer\"\n}"
  },
  {
    "path": "app/locale/hu.json",
    "content": "{\n    \"Running without HTTPS is not recommended, crashes or other issues are likely.\": \"HTTPS nélkül futtatni nem ajánlott, összeomlások vagy más problémák várhatók.\",\n    \"Connecting...\": \"Kapcsolódás...\",\n    \"Disconnecting...\": \"Kapcsolat bontása...\",\n    \"Reconnecting...\": \"Újrakapcsolódás...\",\n    \"Internal error\": \"Belső hiba\",\n    \"Failed to connect to server: \": \"Nem sikerült csatlakozni a szerverhez: \",\n    \"Connected (encrypted) to \": \"Kapcsolódva (titkosítva) ehhez: \",\n    \"Connected (unencrypted) to \": \"Kapcsolódva (titkosítatlanul) ehhez: \",\n    \"Something went wrong, connection is closed\": \"Valami hiba történt, a kapcsolat lezárult\",\n    \"Failed to connect to server\": \"Nem sikerült csatlakozni a szerverhez\",\n    \"Disconnected\": \"Kapcsolat bontva\",\n    \"New connection has been rejected with reason: \": \"Az új kapcsolat elutasítva, indok: \",\n    \"New connection has been rejected\": \"Az új kapcsolat elutasítva\",\n    \"Credentials are required\": \"Hitelesítő adatok szükségesek\",\n    \"noVNC encountered an error:\": \"A noVNC hibát észlelt:\",\n    \"Hide/Show the control bar\": \"Vezérlősáv elrejtése/megjelenítése\",\n    \"Drag\": \"Húzás\",\n    \"Move/Drag viewport\": \"Nézet mozgatása/húzása\",\n    \"Keyboard\": \"Billentyűzet\",\n    \"Show keyboard\": \"Billentyűzet megjelenítése\",\n    \"Extra keys\": \"Extra billentyűk\",\n    \"Show extra keys\": \"Extra billentyűk megjelenítése\",\n    \"Ctrl\": \"Ctrl\",\n    \"Toggle Ctrl\": \"Ctrl lenyomása/felengedése\",\n    \"Alt\": \"Alt\",\n    \"Toggle Alt\": \"Alt lenyomása/felengedése\",\n    \"Toggle Windows\": \"Windows lenyomása/felengedése\",\n    \"Windows\": \"Windows\",\n    \"Send Tab\": \"Tab küldése\",\n    \"Tab\": \"Tab\",\n    \"Esc\": \"Esc\",\n    \"Send Escape\": \"Escape küldése\",\n    \"Ctrl+Alt+Del\": \"Ctrl+Alt+Del\",\n    \"Send Ctrl-Alt-Del\": \"Ctrl-Alt-Del küldése\",\n    \"Shutdown/Reboot\": \"Leállítás/Újraindítás\",\n    \"Shutdown/Reboot...\": \"Leállítás/Újraindítás...\",\n    \"Power\": \"Bekapcsolás\",\n    \"Shutdown\": \"Leállítás\",\n    \"Reboot\": \"Újraindítás\",\n    \"Reset\": \"Reset\",\n    \"Clipboard\": \"Vágólap\",\n    \"Edit clipboard content in the textarea below.\": \"Itt tudod módosítani a vágólap tartalmát.\",\n    \"Full screen\": \"Teljes képernyő\",\n    \"Settings\": \"Beállítások\",\n    \"Shared mode\": \"Megosztott mód\",\n    \"View only\": \"Csak megtekintés\",\n    \"Clip to window\": \"Ablakhoz igazítás\",\n    \"Scaling mode:\": \"Méretezési mód:\",\n    \"None\": \"Nincs\",\n    \"Local scaling\": \"Helyi méretezés\",\n    \"Remote resizing\": \"Távoli átméretezés\",\n    \"Advanced\": \"Speciális\",\n    \"Quality:\": \"Minőség:\",\n    \"Compression level:\": \"Tömörítési szint:\",\n    \"Repeater ID:\": \"Ismétlő azonosító:\",\n    \"WebSocket\": \"WebSocket\",\n    \"Encrypt\": \"Titkosítás\",\n    \"Host:\": \"Hoszt:\",\n    \"Port:\": \"Port:\",\n    \"Path:\": \"Útvonal:\",\n    \"Automatic reconnect\": \"Automatikus újracsatlakozás\",\n    \"Reconnect delay (ms):\": \"Újracsatlakozás késleltetése (ms):\",\n    \"Show dot when no cursor\": \"Kurzor hiányában pont mutatása\",\n    \"Logging:\": \"Naplózás:\",\n    \"Version:\": \"Verzió:\",\n    \"Disconnect\": \"Kapcsolat bontása\",\n    \"Connect\": \"Csatlakozás\",\n    \"Server identity\": \"Szerver azonosító\",\n    \"The server has provided the following identifying information:\": \"A szerver a következő azonosító információt adta meg:\",\n    \"Fingerprint:\": \"Ujjlenyomat:\",\n    \"Please verify that the information is correct and press \\\"Approve\\\". Otherwise press \\\"Reject\\\".\": \"Ellenőrizze, hogy az információ helyes-e és nyomja meg a \\\"Jóváhagyás\\\" gombot. Ellenkező esetben nyomja meg az \\\"Elutasítás\\\" gombot.\",\n    \"Approve\": \"Jóváhagyás\",\n    \"Reject\": \"Elutasítás\",\n    \"Credentials\": \"Hitelesítő adatok\",\n    \"Username:\": \"Felhasználónév:\",\n    \"Password:\": \"Jelszó:\",\n    \"Send credentials\": \"Hitelesítő adatok küldése\",\n    \"Cancel\": \"Mégse\"\n}"
  },
  {
    "path": "app/locale/it.json",
    "content": "{\n    \"Connecting...\": \"Connessione in corso...\",\n    \"Disconnecting...\": \"Disconnessione...\",\n    \"Reconnecting...\": \"Riconnessione...\",\n    \"Internal error\": \"Errore interno\",\n    \"Must set host\": \"Devi impostare l'host\",\n    \"Connected (encrypted) to \": \"Connesso (crittografato) a \",\n    \"Connected (unencrypted) to \": \"Connesso (non crittografato) a\",\n    \"Something went wrong, connection is closed\": \"Qualcosa è andato storto, la connessione è stata chiusa\",\n    \"Failed to connect to server\": \"Impossibile connettersi al server\",\n    \"Disconnected\": \"Disconnesso\",\n    \"New connection has been rejected with reason: \": \"La nuova connessione è stata rifiutata con motivo: \",\n    \"New connection has been rejected\": \"La nuova connessione è stata rifiutata\",\n    \"Credentials are required\": \"Le credenziali sono obbligatorie\",\n    \"noVNC encountered an error:\": \"noVNC ha riscontrato un errore:\",\n    \"Hide/Show the control bar\": \"Nascondi/Mostra la barra di controllo\",\n    \"Keyboard\": \"Tastiera\",\n    \"Show keyboard\": \"Mostra tastiera\",\n    \"Extra keys\": \"Tasti Aggiuntivi\",\n    \"Show Extra Keys\": \"Mostra Tasti Aggiuntivi\",\n    \"Ctrl\": \"Ctrl\",\n    \"Toggle Ctrl\": \"Tieni premuto Ctrl\",\n    \"Alt\": \"Alt\",\n    \"Toggle Alt\": \"Tieni premuto Alt\",\n    \"Toggle Windows\": \"Tieni premuto Windows\",\n    \"Windows\": \"Windows\",\n    \"Send Tab\": \"Invia Tab\",\n    \"Tab\": \"Tab\",\n    \"Esc\": \"Esc\",\n    \"Send Escape\": \"Invia Esc\",\n    \"Ctrl+Alt+Del\": \"Ctrl+Alt+Canc\",\n    \"Send Ctrl-Alt-Del\": \"Invia Ctrl-Alt-Canc\",\n    \"Shutdown/Reboot\": \"Spegnimento/Riavvio\",\n    \"Shutdown/Reboot...\": \"Spegnimento/Riavvio...\",\n    \"Power\": \"Alimentazione\",\n    \"Shutdown\": \"Spegnimento\",\n    \"Reboot\": \"Riavvio\",\n    \"Reset\": \"Reset\",\n    \"Clipboard\": \"Clipboard\",\n    \"Clear\": \"Pulisci\",\n    \"Fullscreen\": \"Schermo intero\",\n    \"Settings\": \"Impostazioni\",\n    \"Shared mode\": \"Modalità condivisa\",\n    \"View Only\": \"Sola Visualizzazione\",\n    \"Scaling mode:\": \"Modalità di ridimensionamento:\",\n    \"None\": \"Nessuna\",\n    \"Local Scaling\": \"Ridimensionamento Locale\",\n    \"Remote Resizing\": \"Ridimensionamento Remoto\",\n    \"Advanced\": \"Avanzate\",\n    \"Quality:\": \"Qualità:\",\n    \"Compression level:\": \"Livello Compressione:\",\n    \"Repeater ID:\": \"ID Ripetitore:\",\n    \"WebSocket\": \"WebSocket\",\n    \"Encrypt\": \"Crittografa\",\n    \"Host:\": \"Host:\",\n    \"Port:\": \"Porta:\",\n    \"Path:\": \"Percorso:\",\n    \"Automatic Reconnect\": \"Riconnessione Automatica\",\n    \"Reconnect Delay (ms):\": \"Ritardo Riconnessione (ms):\",\n    \"Show Dot when No Cursor\": \"Mostra Punto quando Nessun Cursore\",\n    \"Version:\": \"Versione:\",\n    \"Disconnect\": \"Disconnetti\",\n    \"Connect\": \"Connetti\",\n    \"Username:\": \"Utente:\",\n    \"Password:\": \"Password:\",\n    \"Send Credentials\": \"Invia Credenziale\",\n    \"Cancel\": \"Annulla\"\n}"
  },
  {
    "path": "app/locale/ja.json",
    "content": "{\n    \"Running without HTTPS is not recommended, crashes or other issues are likely.\": \"HTTPS接続なしで実行することは推奨されません。クラッシュしたりその他の問題が発生したりする可能性があります。\",\n    \"Connecting...\": \"接続しています...\",\n    \"Disconnecting...\": \"切断しています...\",\n    \"Reconnecting...\": \"再接続しています...\",\n    \"Internal error\": \"内部エラー\",\n    \"Must set host\": \"ホストを設定する必要があります\",\n    \"Failed to connect to server: \": \"サーバーへの接続に失敗しました: \",\n    \"Connected (encrypted) to \": \"接続しました (暗号化済み): \",\n    \"Connected (unencrypted) to \": \"接続しました (暗号化されていません): \",\n    \"Something went wrong, connection is closed\": \"問題が発生したため、接続が閉じられました\",\n    \"Failed to connect to server\": \"サーバーへの接続に失敗しました\",\n    \"Disconnected\": \"切断しました\",\n    \"New connection has been rejected with reason: \": \"新規接続は次の理由で拒否されました: \",\n    \"New connection has been rejected\": \"新規接続は拒否されました\",\n    \"Credentials are required\": \"資格情報が必要です\",\n    \"noVNC encountered an error:\": \"noVNC でエラーが発生しました:\",\n    \"Hide/Show the control bar\": \"コントロールバーを隠す/表示する\",\n    \"Drag\": \"ドラッグ\",\n    \"Move/Drag viewport\": \"ビューポートを移動/ドラッグ\",\n    \"Keyboard\": \"キーボード\",\n    \"Show keyboard\": \"キーボードを表示\",\n    \"Extra keys\": \"追加キー\",\n    \"Show extra keys\": \"追加キーを表示\",\n    \"Ctrl\": \"Ctrl\",\n    \"Toggle Ctrl\": \"Ctrl キーをトグル\",\n    \"Alt\": \"Alt\",\n    \"Toggle Alt\": \"Alt キーをトグル\",\n    \"Toggle Windows\": \"Windows キーをトグル\",\n    \"Windows\": \"Windows\",\n    \"Send Tab\": \"Tab キーを送信\",\n    \"Tab\": \"Tab\",\n    \"Esc\": \"Esc\",\n    \"Send Escape\": \"Escape キーを送信\",\n    \"Ctrl+Alt+Del\": \"Ctrl+Alt+Del\",\n    \"Send Ctrl-Alt-Del\": \"Ctrl-Alt-Del を送信\",\n    \"Shutdown/Reboot\": \"シャットダウン/再起動\",\n    \"Shutdown/Reboot...\": \"シャットダウン/再起動...\",\n    \"Power\": \"電源\",\n    \"Shutdown\": \"シャットダウン\",\n    \"Reboot\": \"再起動\",\n    \"Reset\": \"リセット\",\n    \"Clipboard\": \"クリップボード\",\n    \"Edit clipboard content in the textarea below.\": \"以下の入力欄からクリップボードの内容を編集できます。\",\n    \"Full screen\": \"全画面表示\",\n    \"Settings\": \"設定\",\n    \"Shared mode\": \"共有モード\",\n    \"View only\": \"表示専用\",\n    \"Clip to window\": \"ウィンドウにクリップ\",\n    \"Scaling mode:\": \"スケーリングモード:\",\n    \"None\": \"なし\",\n    \"Local scaling\": \"ローカルでスケーリング\",\n    \"Remote resizing\": \"リモートでリサイズ\",\n    \"Advanced\": \"高度\",\n    \"Quality:\": \"品質:\",\n    \"Compression level:\": \"圧縮レベル:\",\n    \"Repeater ID:\": \"リピーター ID:\",\n    \"WebSocket\": \"WebSocket\",\n    \"Encrypt\": \"暗号化\",\n    \"Host:\": \"ホスト:\",\n    \"Port:\": \"ポート:\",\n    \"Path:\": \"パス:\",\n    \"Automatic reconnect\": \"自動再接続\",\n    \"Reconnect delay (ms):\": \"再接続する遅延 (ミリ秒):\",\n    \"Show dot when no cursor\": \"カーソルがないときにドットを表示する\",\n    \"Logging:\": \"ロギング:\",\n    \"Version:\": \"バージョン:\",\n    \"Disconnect\": \"切断\",\n    \"Connect\": \"接続\",\n    \"Server identity\": \"サーバーの識別情報\",\n    \"The server has provided the following identifying information:\": \"サーバーは以下の識別情報を提供しています:\",\n    \"Fingerprint:\": \"フィンガープリント:\",\n    \"Please verify that the information is correct and press \\\"Approve\\\". Otherwise press \\\"Reject\\\".\": \"この情報が正しい場合は「承認」を、そうでない場合は「拒否」を押してください。\",\n    \"Approve\": \"承認\",\n    \"Reject\": \"拒否\",\n    \"Credentials\": \"資格情報\",\n    \"Username:\": \"ユーザー名:\",\n    \"Password:\": \"パスワード:\",\n    \"Send credentials\": \"資格情報を送信\",\n    \"Cancel\": \"キャンセル\"\n}"
  },
  {
    "path": "app/locale/ko.json",
    "content": "{\n    \"Connecting...\": \"연결중...\",\n    \"Disconnecting...\": \"연결 해제중...\",\n    \"Reconnecting...\": \"재연결중...\",\n    \"Internal error\": \"내부 오류\",\n    \"Must set host\": \"호스트는 설정되어야 합니다.\",\n    \"Connected (encrypted) to \": \"다음과 (암호화되어) 연결되었습니다:\",\n    \"Connected (unencrypted) to \": \"다음과 (암호화 없이) 연결되었습니다:\",\n    \"Something went wrong, connection is closed\": \"무언가 잘못되었습니다, 연결이 닫혔습니다.\",\n    \"Failed to connect to server\": \"서버에 연결하지 못했습니다.\",\n    \"Disconnected\": \"연결이 해제되었습니다.\",\n    \"New connection has been rejected with reason: \": \"새 연결이 다음 이유로 거부되었습니다:\",\n    \"New connection has been rejected\": \"새 연결이 거부되었습니다.\",\n    \"Password is required\": \"비밀번호가 필요합니다.\",\n    \"noVNC encountered an error:\": \"noVNC에 오류가 발생했습니다:\",\n    \"Hide/Show the control bar\": \"컨트롤 바 숨기기/보이기\",\n    \"Move/Drag viewport\": \"움직이기/드래그 뷰포트\",\n    \"viewport drag\": \"뷰포트 드래그\",\n    \"Active Mouse Button\": \"마우스 버튼 활성화\",\n    \"No mousebutton\": \"마우스 버튼 없음\",\n    \"Left mousebutton\": \"왼쪽 마우스 버튼\",\n    \"Middle mousebutton\": \"중간 마우스 버튼\",\n    \"Right mousebutton\": \"오른쪽 마우스 버튼\",\n    \"Keyboard\": \"키보드\",\n    \"Show keyboard\": \"키보드 보이기\",\n    \"Extra keys\": \"기타 키들\",\n    \"Show extra keys\": \"기타 키들 보이기\",\n    \"Ctrl\": \"Ctrl\",\n    \"Toggle Ctrl\": \"Ctrl 켜기/끄기\",\n    \"Alt\": \"Alt\",\n    \"Toggle Alt\": \"Alt 켜기/끄기\",\n    \"Send Tab\": \"Tab 보내기\",\n    \"Tab\": \"Tab\",\n    \"Esc\": \"Esc\",\n    \"Send Escape\": \"Esc 보내기\",\n    \"Ctrl+Alt+Del\": \"Ctrl+Alt+Del\",\n    \"Send Ctrl-Alt-Del\": \"Ctrl+Alt+Del 보내기\",\n    \"Shutdown/Reboot\": \"셧다운/리붓\",\n    \"Shutdown/Reboot...\": \"셧다운/리붓...\",\n    \"Power\": \"전원\",\n    \"Shutdown\": \"셧다운\",\n    \"Reboot\": \"리붓\",\n    \"Reset\": \"리셋\",\n    \"Clipboard\": \"클립보드\",\n    \"Clear\": \"지우기\",\n    \"Fullscreen\": \"전체화면\",\n    \"Settings\": \"설정\",\n    \"Shared mode\": \"공유 모드\",\n    \"View only\": \"보기 전용\",\n    \"Clip to window\": \"창에 클립\",\n    \"Scaling mode:\": \"스케일링 모드:\",\n    \"None\": \"없음\",\n    \"Local scaling\": \"로컬 스케일링\",\n    \"Remote resizing\": \"원격 크기 조절\",\n    \"Advanced\": \"고급\",\n    \"Repeater ID:\": \"중계 ID\",\n    \"WebSocket\": \"웹소켓\",\n    \"Encrypt\": \"암호화\",\n    \"Host:\": \"호스트:\",\n    \"Port:\": \"포트:\",\n    \"Path:\": \"위치:\",\n    \"Automatic reconnect\": \"자동 재연결\",\n    \"Reconnect delay (ms):\": \"재연결 지연 시간 (ms)\",\n    \"Logging:\": \"로깅\",\n    \"Disconnect\": \"연결 해제\",\n    \"Connect\": \"연결\",\n    \"Password:\": \"비밀번호:\",\n    \"Send Password\": \"비밀번호 전송\",\n    \"Cancel\": \"취소\"\n}"
  },
  {
    "path": "app/locale/nl.json",
    "content": "{\n    \"Running without HTTPS is not recommended, crashes or other issues are likely.\": \"Het is niet aan te raden om zonder HTTPS te werken, crashes of andere problemen zijn dan waarschijnlijk.\",\n    \"Connecting...\": \"Aan het verbinden…\",\n    \"Disconnecting...\": \"Bezig om verbinding te verbreken...\",\n    \"Reconnecting...\": \"Opnieuw verbinding maken...\",\n    \"Internal error\": \"Interne fout\",\n    \"Failed to connect to server: \": \"Verbinding maken met server is mislukt\",\n    \"Connected (encrypted) to \": \"Verbonden (versleuteld) met \",\n    \"Connected (unencrypted) to \": \"Verbonden (onversleuteld) met \",\n    \"Something went wrong, connection is closed\": \"Er iets fout gelopen, verbinding werd verbroken\",\n    \"Failed to connect to server\": \"Verbinding maken met server is mislukt\",\n    \"Disconnected\": \"Verbinding verbroken\",\n    \"New connection has been rejected with reason: \": \"Nieuwe verbinding is geweigerd met de volgende reden: \",\n    \"New connection has been rejected\": \"Nieuwe verbinding is geweigerd\",\n    \"Credentials are required\": \"Inloggegevens zijn nodig\",\n    \"noVNC encountered an error:\": \"noVNC heeft een fout bemerkt:\",\n    \"Hide/Show the control bar\": \"Verberg/Toon de bedieningsbalk\",\n    \"Drag\": \"Sleep\",\n    \"Move/Drag viewport\": \"Verplaats/Versleep Kijkvenster\",\n    \"Keyboard\": \"Toetsenbord\",\n    \"Show keyboard\": \"Toon Toetsenbord\",\n    \"Extra keys\": \"Extra toetsen\",\n    \"Show extra keys\": \"Toon Extra Toetsen\",\n    \"Ctrl\": \"Ctrl\",\n    \"Toggle Ctrl\": \"Ctrl omschakelen\",\n    \"Alt\": \"Alt\",\n    \"Toggle Alt\": \"Alt omschakelen\",\n    \"Toggle Windows\": \"Vensters omschakelen\",\n    \"Windows\": \"Vensters\",\n    \"Send Tab\": \"Tab Sturen\",\n    \"Tab\": \"Tab\",\n    \"Esc\": \"Esc\",\n    \"Send Escape\": \"Escape Sturen\",\n    \"Ctrl+Alt+Del\": \"Ctrl-Alt-Del\",\n    \"Send Ctrl-Alt-Del\": \"Ctrl-Alt-Del Sturen\",\n    \"Shutdown/Reboot\": \"Uitschakelen/Herstarten\",\n    \"Shutdown/Reboot...\": \"Uitschakelen/Herstarten...\",\n    \"Power\": \"Systeem\",\n    \"Shutdown\": \"Uitschakelen\",\n    \"Reboot\": \"Herstarten\",\n    \"Reset\": \"Resetten\",\n    \"Clipboard\": \"Klembord\",\n    \"Edit clipboard content in the textarea below.\": \"Edit de inhoud van het klembord in het tekstveld hieronder\",\n    \"Full screen\": \"Volledig Scherm\",\n    \"Settings\": \"Instellingen\",\n    \"Shared mode\": \"Gedeelde Modus\",\n    \"View only\": \"Alleen Kijken\",\n    \"Clip to window\": \"Randen buiten venster afsnijden\",\n    \"Scaling mode:\": \"Schaalmodus:\",\n    \"None\": \"Geen\",\n    \"Local scaling\": \"Lokaal Schalen\",\n    \"Remote resizing\": \"Op Afstand Formaat Wijzigen\",\n    \"Advanced\": \"Geavanceerd\",\n    \"Quality:\": \"Kwaliteit:\",\n    \"Compression level:\": \"Compressieniveau:\",\n    \"Repeater ID:\": \"Repeater ID:\",\n    \"WebSocket\": \"WebSocket\",\n    \"Encrypt\": \"Versleutelen\",\n    \"Host:\": \"Host:\",\n    \"Port:\": \"Poort:\",\n    \"Path:\": \"Pad:\",\n    \"Automatic reconnect\": \"Automatisch Opnieuw Verbinden\",\n    \"Reconnect delay (ms):\": \"Vertraging voor Opnieuw Verbinden (ms):\",\n    \"Show dot when no cursor\": \"Geef stip weer indien geen cursor\",\n    \"Logging:\": \"Logmeldingen:\",\n    \"Version:\": \"Versie:\",\n    \"Disconnect\": \"Verbinding verbreken\",\n    \"Connect\": \"Verbinden\",\n    \"Server identity\": \"Serveridentiteit\",\n    \"The server has provided the following identifying information:\": \"De server geeft de volgende identificerende informatie:\",\n    \"Fingerprint:\": \"Vingerafdruk:\",\n    \"Please verify that the information is correct and press \\\"Approve\\\". Otherwise press \\\"Reject\\\".\": \"Verifieer dat de informatie is correct en druk “OK”. Druk anders op “Afwijzen”.\",\n    \"Approve\": \"OK\",\n    \"Reject\": \"Afwijzen\",\n    \"Credentials\": \"Inloggegevens\",\n    \"Username:\": \"Gebruikersnaam:\",\n    \"Password:\": \"Wachtwoord:\",\n    \"Send credentials\": \"Stuur inloggegevens\",\n    \"Cancel\": \"Annuleren\",\n    \"Must set host\": \"Host moeten worden ingesteld\",\n    \"Password is required\": \"Wachtwoord is vereist\",\n    \"viewport drag\": \"kijkvenster slepen\",\n    \"Active Mouse Button\": \"Actieve Muisknop\",\n    \"No mousebutton\": \"Geen muisknop\",\n    \"Left mousebutton\": \"Linker muisknop\",\n    \"Middle mousebutton\": \"Middelste muisknop\",\n    \"Right mousebutton\": \"Rechter muisknop\",\n    \"Clear\": \"Wissen\",\n    \"Send Password\": \"Verzend Wachtwoord:\",\n    \"Disconnect timeout\": \"Timeout tijdens verbreken van verbinding\",\n    \"Local Downscaling\": \"Lokaal Neerschalen\",\n    \"Local Cursor\": \"Lokale Cursor\",\n    \"Canvas not supported.\": \"Canvas wordt niet ondersteund.\",\n    \"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen\": \"''Clipping mode' ingeschakeld, omdat schuifbalken in volledige-scherm-modus in IE niet worden ondersteund\"\n}"
  },
  {
    "path": "app/locale/pl.json",
    "content": "{\n    \"Connecting...\": \"Łączenie...\",\n    \"Disconnecting...\": \"Rozłączanie...\",\n    \"Reconnecting...\": \"Łączenie...\",\n    \"Internal error\": \"Błąd wewnętrzny\",\n    \"Must set host\": \"Host i port są wymagane\",\n    \"Connected (encrypted) to \": \"Połączenie (szyfrowane) z \",\n    \"Connected (unencrypted) to \": \"Połączenie (nieszyfrowane) z \",\n    \"Something went wrong, connection is closed\": \"Coś poszło źle, połączenie zostało zamknięte\",\n    \"Disconnected\": \"Rozłączony\",\n    \"New connection has been rejected with reason: \": \"Nowe połączenie zostało odrzucone z powodu: \",\n    \"New connection has been rejected\": \"Nowe połączenie zostało odrzucone\",\n    \"Password is required\": \"Hasło jest wymagane\",\n    \"noVNC encountered an error:\": \"noVNC napotkało błąd:\",\n    \"Hide/Show the control bar\": \"Pokaż/Ukryj pasek ustawień\",\n    \"Move/Drag Viewport\": \"Ruszaj/Przeciągaj Viewport\",\n    \"viewport drag\": \"przeciągnij viewport\",\n    \"Active Mouse Button\": \"Aktywny Przycisk Myszy\",\n    \"No mousebutton\": \"Brak przycisku myszy\",\n    \"Left mousebutton\": \"Lewy przycisk myszy\",\n    \"Middle mousebutton\": \"Środkowy przycisk myszy\",\n    \"Right mousebutton\": \"Prawy przycisk myszy\",\n    \"Keyboard\": \"Klawiatura\",\n    \"Show keyboard\": \"Pokaż klawiaturę\",\n    \"Extra keys\": \"Przyciski dodatkowe\",\n    \"Show extra keys\": \"Pokaż przyciski dodatkowe\",\n    \"Ctrl\": \"Ctrl\",\n    \"Toggle Ctrl\": \"Przełącz Ctrl\",\n    \"Alt\": \"Alt\",\n    \"Toggle Alt\": \"Przełącz Alt\",\n    \"Send Tab\": \"Wyślij Tab\",\n    \"Tab\": \"Tab\",\n    \"Esc\": \"Esc\",\n    \"Send Escape\": \"Wyślij Escape\",\n    \"Ctrl+Alt+Del\": \"Ctrl+Alt+Del\",\n    \"Send Ctrl-Alt-Del\": \"Wyślij Ctrl-Alt-Del\",\n    \"Shutdown/Reboot\": \"Wyłącz/Uruchom ponownie\",\n    \"Shutdown/Reboot...\": \"Wyłącz/Uruchom ponownie...\",\n    \"Power\": \"Włączony\",\n    \"Shutdown\": \"Wyłącz\",\n    \"Reboot\": \"Uruchom ponownie\",\n    \"Reset\": \"Resetuj\",\n    \"Clipboard\": \"Schowek\",\n    \"Clear\": \"Wyczyść\",\n    \"Fullscreen\": \"Pełny ekran\",\n    \"Settings\": \"Ustawienia\",\n    \"Shared Mode\": \"Tryb Współdzielenia\",\n    \"View Only\": \"Tylko Podgląd\",\n    \"Clip to Window\": \"Przytnij do Okna\",\n    \"Scaling Mode:\": \"Tryb Skalowania:\",\n    \"None\": \"Brak\",\n    \"Local scaling\": \"Skalowanie lokalne\",\n    \"Remote resizing\": \"Skalowanie zdalne\",\n    \"Advanced\": \"Zaawansowane\",\n    \"Repeater ID:\": \"ID Repeatera:\",\n    \"WebSocket\": \"WebSocket\",\n    \"Encrypt\": \"Szyfrowanie\",\n    \"Host:\": \"Host:\",\n    \"Port:\": \"Port:\",\n    \"Path:\": \"Ścieżka:\",\n    \"Automatic reconnect\": \"Automatycznie wznawiaj połączenie\",\n    \"Reconnect delay (ms):\": \"Opóźnienie wznawiania (ms):\",\n    \"Logging:\": \"Poziom logowania:\",\n    \"Disconnect\": \"Rozłącz\",\n    \"Connect\": \"Połącz\",\n    \"Password:\": \"Hasło:\",\n    \"Cancel\": \"Anuluj\",\n    \"Canvas not supported.\": \"Element Canvas nie jest wspierany.\",\n    \"Disconnect timeout\": \"Timeout rozłączenia\",\n    \"Local Downscaling\": \"Downscaling lokalny\",\n    \"Local Cursor\": \"Lokalny kursor\",\n    \"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen\": \"Wymuszam clipping mode ponieważ paski przewijania nie są wspierane przez IE w trybie pełnoekranowym\",\n    \"True Color\": \"True Color\",\n    \"Style:\": \"Styl:\",\n    \"default\": \"domyślny\",\n    \"Apply\": \"Zapisz\",\n    \"Connection\": \"Połączenie\",\n    \"Token:\": \"Token:\",\n    \"Send Password\": \"Wyślij Hasło\"\n}"
  },
  {
    "path": "app/locale/pt_BR.json",
    "content": "{\n    \"Connecting...\": \"Conectando...\",\n    \"Disconnecting...\": \"Desconectando...\",\n    \"Reconnecting...\": \"Reconectando...\",\n    \"Internal error\": \"Erro interno\",\n    \"Must set host\": \"É necessário definir o host\",\n    \"Connected (encrypted) to \": \"Conectado (com criptografia) a \",\n    \"Connected (unencrypted) to \": \"Conectado (sem criptografia) a \",\n    \"Something went wrong, connection is closed\": \"Algo deu errado. A conexão foi encerrada.\",\n    \"Failed to connect to server\": \"Falha ao conectar-se ao servidor\",\n    \"Disconnected\": \"Desconectado\",\n    \"New connection has been rejected with reason: \": \"A nova conexão foi rejeitada pelo motivo: \",\n    \"New connection has been rejected\": \"A nova conexão foi rejeitada\",\n    \"Credentials are required\": \"Credenciais são obrigatórias\",\n    \"noVNC encountered an error:\": \"O noVNC encontrou um erro:\",\n    \"Hide/Show the control bar\": \"Esconder/mostrar a barra de controles\",\n    \"Drag\": \"Arrastar\",\n    \"Move/Drag viewport\": \"Mover/arrastar a janela\",\n    \"Keyboard\": \"Teclado\",\n    \"Show keyboard\": \"Mostrar teclado\",\n    \"Extra keys\": \"Teclas adicionais\",\n    \"Show extra keys\": \"Mostrar teclas adicionais\",\n    \"Ctrl\": \"Ctrl\",\n    \"Toggle Ctrl\": \"Pressionar/soltar Ctrl\",\n    \"Alt\": \"Alt\",\n    \"Toggle Alt\": \"Pressionar/soltar Alt\",\n    \"Toggle Windows\": \"Pressionar/soltar Windows\",\n    \"Windows\": \"Windows\",\n    \"Send Tab\": \"Enviar Tab\",\n    \"Tab\": \"Tab\",\n    \"Esc\": \"Esc\",\n    \"Send Escape\": \"Enviar Esc\",\n    \"Ctrl+Alt+Del\": \"Ctrl+Alt+Del\",\n    \"Send Ctrl-Alt-Del\": \"Enviar Ctrl-Alt-Del\",\n    \"Shutdown/Reboot\": \"Desligar/reiniciar\",\n    \"Shutdown/Reboot...\": \"Desligar/reiniciar...\",\n    \"Power\": \"Ligar\",\n    \"Shutdown\": \"Desligar\",\n    \"Reboot\": \"Reiniciar\",\n    \"Reset\": \"Reiniciar (forçado)\",\n    \"Clipboard\": \"Área de transferência\",\n    \"Clear\": \"Limpar\",\n    \"Fullscreen\": \"Tela cheia\",\n    \"Settings\": \"Configurações\",\n    \"Shared mode\": \"Modo compartilhado\",\n    \"View only\": \"Apenas visualizar\",\n    \"Clip to window\": \"Recortar à janela\",\n    \"Scaling mode:\": \"Modo de dimensionamento:\",\n    \"None\": \"Nenhum\",\n    \"Local scaling\": \"Local\",\n    \"Remote resizing\": \"Remoto\",\n    \"Advanced\": \"Avançado\",\n    \"Quality:\": \"Qualidade:\",\n    \"Compression level:\": \"Nível de compressão:\",\n    \"Repeater ID:\": \"ID do repetidor:\",\n    \"WebSocket\": \"WebSocket\",\n    \"Encrypt\": \"Criptografar\",\n    \"Host:\": \"Host:\",\n    \"Port:\": \"Porta:\",\n    \"Path:\": \"Caminho:\",\n    \"Automatic reconnect\": \"Reconexão automática\",\n    \"Reconnect delay (ms):\": \"Atraso da reconexão (ms)\",\n    \"Show dot when no cursor\": \"Mostrar ponto quando não há cursor\",\n    \"Logging:\": \"Registros:\",\n    \"Version:\": \"Versão:\",\n    \"Disconnect\": \"Desconectar\",\n    \"Connect\": \"Conectar\",\n    \"Username:\": \"Nome de usuário:\",\n    \"Password:\": \"Senha:\",\n    \"Send credentials\": \"Enviar credenciais\",\n    \"Cancel\": \"Cancelar\"\n}"
  },
  {
    "path": "app/locale/ru.json",
    "content": "{\n    \"Connecting...\": \"Подключение...\",\n    \"Disconnecting...\": \"Отключение...\",\n    \"Reconnecting...\": \"Переподключение...\",\n    \"Internal error\": \"Внутренняя ошибка\",\n    \"Must set host\": \"Задайте имя сервера или IP\",\n    \"Connected (encrypted) to \": \"Подключено (с шифрованием) к \",\n    \"Connected (unencrypted) to \": \"Подключено (без шифрования) к \",\n    \"Something went wrong, connection is closed\": \"Что-то пошло не так, подключение разорвано\",\n    \"Failed to connect to server\": \"Ошибка подключения к серверу\",\n    \"Disconnected\": \"Отключено\",\n    \"New connection has been rejected with reason: \": \"Новое соединение отклонено по причине: \",\n    \"New connection has been rejected\": \"Новое соединение отклонено\",\n    \"Credentials are required\": \"Требуются учетные данные\",\n    \"noVNC encountered an error:\": \"Ошибка noVNC: \",\n    \"Hide/Show the control bar\": \"Скрыть/Показать контрольную панель\",\n    \"Drag\": \"Переместить\",\n    \"Move/Drag viewport\": \"Переместить окно\",\n    \"Keyboard\": \"Клавиатура\",\n    \"Show keyboard\": \"Показать клавиатуру\",\n    \"Extra keys\": \"Дополнительные Кнопки\",\n    \"Show Extra Keys\": \"Показать Дополнительные Кнопки\",\n    \"Ctrl\": \"Ctrl\",\n    \"Toggle Ctrl\": \"Зажать Ctrl\",\n    \"Alt\": \"Alt\",\n    \"Toggle Alt\": \"Зажать Alt\",\n    \"Toggle Windows\": \"Зажать Windows\",\n    \"Windows\": \"Вкладка\",\n    \"Send Tab\": \"Передать нажатие Tab\",\n    \"Tab\": \"Tab\",\n    \"Esc\": \"Esc\",\n    \"Send Escape\": \"Передать нажатие Escape\",\n    \"Ctrl+Alt+Del\": \"Ctrl+Alt+Del\",\n    \"Send Ctrl-Alt-Del\": \"Передать нажатие Ctrl-Alt-Del\",\n    \"Shutdown/Reboot\": \"Выключить/Перезагрузить\",\n    \"Shutdown/Reboot...\": \"Выключить/Перезагрузить...\",\n    \"Power\": \"Питание\",\n    \"Shutdown\": \"Выключить\",\n    \"Reboot\": \"Перезагрузить\",\n    \"Reset\": \"Сброс\",\n    \"Clipboard\": \"Буфер обмена\",\n    \"Clear\": \"Очистить\",\n    \"Fullscreen\": \"Во весь экран\",\n    \"Settings\": \"Настройки\",\n    \"Shared mode\": \"Общий режим\",\n    \"View Only\": \"Только Просмотр\",\n    \"Clip to window\": \"В окно\",\n    \"Scaling mode:\": \"Масштаб:\",\n    \"None\": \"Нет\",\n    \"Local scaling\": \"Локальный масштаб\",\n    \"Remote resizing\": \"Удаленная перенастройка размера\",\n    \"Advanced\": \"Дополнительно\",\n    \"Quality:\": \"Качество\",\n    \"Compression level:\": \"Уровень Сжатия\",\n    \"Repeater ID:\": \"Идентификатор ID:\",\n    \"WebSocket\": \"WebSocket\",\n    \"Encrypt\": \"Шифрование\",\n    \"Host:\": \"Сервер:\",\n    \"Port:\": \"Порт:\",\n    \"Path:\": \"Путь:\",\n    \"Automatic reconnect\": \"Автоматическое переподключение\",\n    \"Reconnect delay (ms):\": \"Задержка переподключения (мс):\",\n    \"Show dot when no cursor\": \"Показать точку вместо курсора\",\n    \"Logging:\": \"Лог:\",\n    \"Version:\": \"Версия\",\n    \"Disconnect\": \"Отключение\",\n    \"Connect\": \"Подключение\",\n    \"Username:\": \"Имя Пользователя\",\n    \"Password:\": \"Пароль:\",\n    \"Send Credentials\": \"Передача Учетных Данных\",\n    \"Cancel\": \"Выход\"\n}"
  },
  {
    "path": "app/locale/sv.json",
    "content": "{\n    \"Running without HTTPS is not recommended, crashes or other issues are likely.\": \"Det är ej rekommenderat att köra utan HTTPS, krascher och andra problem är troliga.\",\n    \"Connecting...\": \"Ansluter...\",\n    \"Disconnecting...\": \"Kopplar ner...\",\n    \"Reconnecting...\": \"Återansluter...\",\n    \"Internal error\": \"Internt fel\",\n    \"Failed to connect to server: \": \"Misslyckades att ansluta till servern: \",\n    \"Connected (encrypted) to \": \"Ansluten (krypterat) till \",\n    \"Connected (unencrypted) to \": \"Ansluten (okrypterat) till \",\n    \"Something went wrong, connection is closed\": \"Något gick fel, anslutningen avslutades\",\n    \"Failed to connect to server\": \"Misslyckades att ansluta till servern\",\n    \"Disconnected\": \"Frånkopplad\",\n    \"New connection has been rejected with reason: \": \"Ny anslutning har blivit nekad med följande skäl: \",\n    \"New connection has been rejected\": \"Ny anslutning har blivit nekad\",\n    \"Credentials are required\": \"Användaruppgifter krävs\",\n    \"noVNC encountered an error:\": \"noVNC stötte på ett problem:\",\n    \"Hide/Show the control bar\": \"Göm/Visa kontrollbaren\",\n    \"Drag\": \"Dra\",\n    \"Move/Drag viewport\": \"Flytta/Dra vyn\",\n    \"Keyboard\": \"Tangentbord\",\n    \"Show keyboard\": \"Visa tangentbord\",\n    \"Extra keys\": \"Extraknappar\",\n    \"Show extra keys\": \"Visa extraknappar\",\n    \"Ctrl\": \"Ctrl\",\n    \"Toggle Ctrl\": \"Växla Ctrl\",\n    \"Alt\": \"Alt\",\n    \"Toggle Alt\": \"Växla Alt\",\n    \"Toggle Windows\": \"Växla Windows\",\n    \"Windows\": \"Windows\",\n    \"Send Tab\": \"Skicka Tab\",\n    \"Tab\": \"Tab\",\n    \"Esc\": \"Esc\",\n    \"Send Escape\": \"Skicka Escape\",\n    \"Ctrl+Alt+Del\": \"Ctrl+Alt+Del\",\n    \"Send Ctrl-Alt-Del\": \"Skicka Ctrl-Alt-Del\",\n    \"Shutdown/Reboot\": \"Stäng av/Boota om\",\n    \"Shutdown/Reboot...\": \"Stäng av/Boota om...\",\n    \"Power\": \"Ström\",\n    \"Shutdown\": \"Stäng av\",\n    \"Reboot\": \"Boota om\",\n    \"Reset\": \"Återställ\",\n    \"Clipboard\": \"Urklipp\",\n    \"Edit clipboard content in the textarea below.\": \"Redigera urklippets innehåll i fältet nedan.\",\n    \"Full screen\": \"Fullskärm\",\n    \"Settings\": \"Inställningar\",\n    \"Shared mode\": \"Delat läge\",\n    \"View only\": \"Endast visning\",\n    \"Clip to window\": \"Begränsa till fönster\",\n    \"Scaling mode:\": \"Skalningsläge:\",\n    \"None\": \"Ingen\",\n    \"Local scaling\": \"Lokal skalning\",\n    \"Remote resizing\": \"Ändra storlek\",\n    \"Advanced\": \"Avancerat\",\n    \"Quality:\": \"Kvalitet:\",\n    \"Compression level:\": \"Kompressionsnivå:\",\n    \"Repeater ID:\": \"Repeater-ID:\",\n    \"WebSocket\": \"WebSocket\",\n    \"Encrypt\": \"Kryptera\",\n    \"Host:\": \"Värd:\",\n    \"Port:\": \"Port:\",\n    \"Path:\": \"Sökväg:\",\n    \"Automatic reconnect\": \"Automatisk återanslutning\",\n    \"Reconnect delay (ms):\": \"Fördröjning (ms):\",\n    \"Show dot when no cursor\": \"Visa prick när ingen muspekare finns\",\n    \"Logging:\": \"Loggning:\",\n    \"Version:\": \"Version:\",\n    \"Disconnect\": \"Koppla från\",\n    \"Connect\": \"Anslut\",\n    \"Server identity\": \"Server-identitet\",\n    \"The server has provided the following identifying information:\": \"Servern har gett följande identifierande information:\",\n    \"Fingerprint:\": \"Fingeravtryck:\",\n    \"Please verify that the information is correct and press \\\"Approve\\\". Otherwise press \\\"Reject\\\".\": \"Kontrollera att informationen är korrekt och tryck sedan \\\"Godkänn\\\". Tryck annars \\\"Neka\\\".\",\n    \"Approve\": \"Godkänn\",\n    \"Reject\": \"Neka\",\n    \"Credentials\": \"Användaruppgifter\",\n    \"Username:\": \"Användarnamn:\",\n    \"Password:\": \"Lösenord:\",\n    \"Send credentials\": \"Skicka användaruppgifter\",\n    \"Cancel\": \"Avbryt\",\n    \"Must set host\": \"Du måste specifiera en värd\",\n    \"HTTPS is required for full functionality\": \"HTTPS krävs för full funktionalitet\",\n    \"Clear\": \"Rensa\"\n}"
  },
  {
    "path": "app/locale/tr.json",
    "content": "{\n    \"Connecting...\": \"Bağlanıyor...\",\n    \"Disconnecting...\": \"Bağlantı kesiliyor...\",\n    \"Reconnecting...\": \"Yeniden bağlantı kuruluyor...\",\n    \"Internal error\": \"İç hata\",\n    \"Must set host\": \"Sunucuyu kur\",\n    \"Connected (encrypted) to \": \"Bağlı (şifrelenmiş)\",\n    \"Connected (unencrypted) to \": \"Bağlandı (şifrelenmemiş)\",\n    \"Something went wrong, connection is closed\": \"Bir şeyler ters gitti, bağlantı kesildi\",\n    \"Disconnected\": \"Bağlantı kesildi\",\n    \"New connection has been rejected with reason: \": \"Bağlantı aşağıdaki nedenlerden dolayı reddedildi: \",\n    \"New connection has been rejected\": \"Bağlantı reddedildi\",\n    \"Password is required\": \"Şifre gerekli\",\n    \"noVNC encountered an error:\": \"Bir hata oluştu:\",\n    \"Hide/Show the control bar\": \"Denetim masasını Gizle/Göster\",\n    \"Move/Drag Viewport\": \"Görünümü Taşı/Sürükle\",\n    \"viewport drag\": \"Görüntü penceresini sürükle\",\n    \"Active Mouse Button\": \"Aktif Fare Düğmesi\",\n    \"No mousebutton\": \"Fare düğmesi yok\",\n    \"Left mousebutton\": \"Farenin sol düğmesi\",\n    \"Middle mousebutton\": \"Farenin orta düğmesi\",\n    \"Right mousebutton\": \"Farenin sağ düğmesi\",\n    \"Keyboard\": \"Klavye\",\n    \"Show Keyboard\": \"Klavye Düzenini Göster\",\n    \"Extra keys\": \"Ekstra tuşlar\",\n    \"Show extra keys\": \"Ekstra tuşları göster\",\n    \"Ctrl\": \"Ctrl\",\n    \"Toggle Ctrl\": \"Ctrl Değiştir \",\n    \"Alt\": \"Alt\",\n    \"Toggle Alt\": \"Alt Değiştir\",\n    \"Send Tab\": \"Sekme Gönder\",\n    \"Tab\": \"Sekme\",\n    \"Esc\": \"Esc\",\n    \"Send Escape\": \"Boşluk Gönder\",\n    \"Ctrl+Alt+Del\": \"Ctrl + Alt + Del\",\n    \"Send Ctrl-Alt-Del\": \"Ctrl-Alt-Del Gönder\",\n    \"Shutdown/Reboot\": \"Kapat/Yeniden Başlat\",\n    \"Shutdown/Reboot...\": \"Kapat/Yeniden Başlat...\",\n    \"Power\": \"Güç\",\n    \"Shutdown\": \"Kapat\",\n    \"Reboot\": \"Yeniden Başlat\",\n    \"Reset\": \"Sıfırla\",\n    \"Clipboard\": \"Pano\",\n    \"Clear\": \"Temizle\",\n    \"Fullscreen\": \"Tam Ekran\",\n    \"Settings\": \"Ayarlar\",\n    \"Shared Mode\": \"Paylaşım Modu\",\n    \"View Only\": \"Sadece Görüntüle\",\n    \"Clip to Window\": \"Pencereye Tıkla\",\n    \"Scaling Mode:\": \"Ölçekleme Modu:\",\n    \"None\": \"Bilinmeyen\",\n    \"Local Scaling\": \"Yerel Ölçeklendirme\",\n    \"Remote Resizing\": \"Uzaktan Yeniden Boyutlandırma\",\n    \"Advanced\": \"Gelişmiş\",\n    \"Repeater ID:\": \"Tekralayıcı ID:\",\n    \"WebSocket\": \"WebSocket\",\n    \"Encrypt\": \"Şifrele\",\n    \"Host:\": \"Ana makine:\",\n    \"Port:\": \"Port:\",\n    \"Path:\": \"Yol:\",\n    \"Automatic Reconnect\": \"Otomatik Yeniden Bağlan\",\n    \"Reconnect Delay (ms):\": \"Yeniden Bağlanma Süreci (ms):\",\n    \"Logging:\": \"Giriş yapılıyor:\",\n    \"Disconnect\": \"Bağlantıyı Kes\",\n    \"Connect\": \"Bağlan\",\n    \"Password:\": \"Parola:\",\n    \"Cancel\": \"Vazgeç\",\n    \"Canvas not supported.\": \"Tuval desteklenmiyor.\"\n}"
  },
  {
    "path": "app/locale/uk.json",
    "content": "{\n    \"Running without HTTPS is not recommended, crashes or other issues are likely.\": \"Робота без HTTPS не рекомендується, ймовірні збої чи інші проблеми.\",\n    \"Connecting...\": \"З'єднання...\",\n    \"Disconnecting...\": \"Від'єднання...\",\n    \"Reconnecting...\": \"Перез'єднання...\",\n    \"Internal error\": \"Внутрішня помилка\",\n    \"Failed to connect to server: \": \"Не вдалося з'єднатися з сервером: \",\n    \"Connected (encrypted) to \": \"З'єднано (з шифруванням) з \",\n    \"Connected (unencrypted) to \": \"З'єднано (без шифрування) з \",\n    \"Something went wrong, connection is closed\": \"Щось пішло не так, з'єднання закрито\",\n    \"Failed to connect to server\": \"Не вдалося з'єднатися з сервером\",\n    \"Disconnected\": \"Від'єднано\",\n    \"New connection has been rejected with reason: \": \"Нове з'єднання відхилено. Причина: \",\n    \"New connection has been rejected\": \"Нове з'єднання відхилено\",\n    \"Are you sure you want to disconnect the session?\": \"Точно від'єднати сеанс?\",\n    \"Credentials are required\": \"Треба особові дані\",\n    \"noVNC encountered an error:\": \"Помилка noVNC:\",\n    \"Hide/Show the control bar\": \"Сховати/показати панель керування\",\n    \"Drag\": \"Посунути\",\n    \"Move/Drag viewport\": \"Змістити область огляду\",\n    \"Keyboard\": \"Клавіатура\",\n    \"Show keyboard\": \"Показати клавіатуру\",\n    \"Extra keys\": \"Додаткові клавіші\",\n    \"Show extra keys\": \"Показати додаткові клавіші\",\n    \"Ctrl\": \"Ctrl\",\n    \"Toggle Ctrl\": \"Затиснути Ctrl\",\n    \"Alt\": \"Alt\",\n    \"Toggle Alt\": \"Затиснути Alt\",\n    \"Toggle Windows\": \"Затиснути Windows\",\n    \"Windows\": \"Windows\",\n    \"Send Tab\": \"Натиснути Tab\",\n    \"Tab\": \"Tab\",\n    \"Esc\": \"Esc\",\n    \"Send Escape\": \"Натиснути Escape\",\n    \"Ctrl+Alt+Del\": \"Ctrl+Alt+Del\",\n    \"Send Ctrl-Alt-Del\": \"Натиснути Ctrl+Alt+Del\",\n    \"Shutdown/Reboot\": \"Вимкнути/перезавантажити\",\n    \"Shutdown/Reboot...\": \"Вимкнути/перезавантажити...\",\n    \"Power\": \"Живлення\",\n    \"Shutdown\": \"Вимкнути\",\n    \"Reboot\": \"Перезавантажити\",\n    \"Reset\": \"Скинути\",\n    \"Clipboard\": \"Буфер обміну\",\n    \"Edit clipboard content in the textarea below.\": \"Редагуйте вміст буфера обміну в текстовій зоні внизу.\",\n    \"Full screen\": \"Повний екран\",\n    \"Settings\": \"Параметри\",\n    \"Shared mode\": \"Спільний режим\",\n    \"View only\": \"Лише перегляд\",\n    \"Clip to window\": \"До розмірів вікна\",\n    \"Scaling mode:\": \"Режим масштабування:\",\n    \"None\": \"Вимкнено\",\n    \"Local scaling\": \"Локальне масштабування\",\n    \"Remote resizing\": \"Віддалене масштабування\",\n    \"Advanced\": \"Додатково\",\n    \"Quality:\": \"Якість:\",\n    \"Compression level:\": \"Рівень стиснення:\",\n    \"Repeater ID:\": \"Ідентифікатор репітера:\",\n    \"WebSocket\": \"WebSocket\",\n    \"Encrypt\": \"Шифрування\",\n    \"Host:\": \"Сервер:\",\n    \"Port:\": \"Порт:\",\n    \"Path:\": \"Шлях:\",\n    \"Automatic reconnect\": \"Автоматичне перез'єднання\",\n    \"Reconnect delay (ms):\": \"Затримка перез'єднання (мс):\",\n    \"Show dot when no cursor\": \"Показувати крапку, коли нема курсора\",\n    \"Logging:\": \"Журнал:\",\n    \"Version:\": \"Версія:\",\n    \"Disconnect\": \"Від'єднати\",\n    \"Connect\": \"З'єднати\",\n    \"Server identity\": \"Ідентифікація сервера\",\n    \"The server has provided the following identifying information:\": \"Сервер надає такі ідентифікаційні дані:\",\n    \"Fingerprint:\": \"Відбиток:\",\n    \"Please verify that the information is correct and press \\\"Approve\\\". Otherwise press \\\"Reject\\\".\": \"Перевірте, чи дані коректні, й натисніть «Схвалити». Інакше натисніть «Відхилити».\",\n    \"Approve\": \"Схвалити\",\n    \"Reject\": \"Відхилити\",\n    \"Credentials\": \"Особові дані\",\n    \"Username:\": \"Користувацьке ім'я:\",\n    \"Password:\": \"Пароль:\",\n    \"Send credentials\": \"Надіслати особові дані\",\n    \"Cancel\": \"Скасувати\"\n}"
  },
  {
    "path": "app/locale/zh_CN.json",
    "content": "{\n    \"Running without HTTPS is not recommended, crashes or other issues are likely.\": \"不建议在没有 HTTPS 的情况下运行，可能会出现崩溃或出现其他问题。\",\n    \"Connecting...\": \"连接中...\",\n    \"Disconnecting...\": \"正在断开连接...\",\n    \"Reconnecting...\": \"重新连接中...\",\n    \"Internal error\": \"内部错误\",\n    \"Must set host\": \"必须设置主机\",\n    \"Failed to connect to server: \": \"无法连接到服务器：\",\n    \"Connected (encrypted) to \": \"已连接（已加密）到\",\n    \"Connected (unencrypted) to \": \"已连接（未加密）到\",\n    \"Something went wrong, connection is closed\": \"出了点问题，连接已关闭\",\n    \"Failed to connect to server\": \"无法连接到服务器\",\n    \"Disconnected\": \"已断开连接\",\n    \"New connection has been rejected with reason: \": \"新连接被拒绝，原因如下：\",\n    \"New connection has been rejected\": \"新连接已被拒绝\",\n    \"Credentials are required\": \"需要凭证\",\n    \"noVNC encountered an error:\": \"noVNC 遇到一个错误：\",\n    \"Hide/Show the control bar\": \"显示/隐藏控制栏\",\n    \"Drag\": \"拖动\",\n    \"Move/Drag viewport\": \"移动/拖动窗口\",\n    \"Keyboard\": \"键盘\",\n    \"Show keyboard\": \"显示键盘\",\n    \"Extra keys\": \"额外按键\",\n    \"Show extra keys\": \"显示额外按键\",\n    \"Ctrl\": \"Ctrl\",\n    \"Toggle Ctrl\": \"切换 Ctrl\",\n    \"Alt\": \"Alt\",\n    \"Toggle Alt\": \"切换 Alt\",\n    \"Toggle Windows\": \"切换窗口\",\n    \"Windows\": \"窗口\",\n    \"Send Tab\": \"发送 Tab 键\",\n    \"Tab\": \"Tab\",\n    \"Esc\": \"Esc\",\n    \"Send Escape\": \"发送 Escape 键\",\n    \"Ctrl+Alt+Del\": \"Ctrl+Alt+Del\",\n    \"Send Ctrl-Alt-Del\": \"发送 Ctrl+Alt+Del 键\",\n    \"Shutdown/Reboot\": \"关机/重启\",\n    \"Shutdown/Reboot...\": \"关机/重启...\",\n    \"Power\": \"电源\",\n    \"Shutdown\": \"关机\",\n    \"Reboot\": \"重启\",\n    \"Reset\": \"重置\",\n    \"Clipboard\": \"剪贴板\",\n    \"Edit clipboard content in the textarea below.\": \"在下面的文本区域中编辑剪贴板内容。\",\n    \"Full screen\": \"全屏\",\n    \"Settings\": \"设置\",\n    \"Shared mode\": \"分享模式\",\n    \"View only\": \"仅查看\",\n    \"Clip to window\": \"限制/裁切窗口大小\",\n    \"Scaling mode:\": \"缩放模式：\",\n    \"None\": \"无\",\n    \"Local scaling\": \"本地缩放\",\n    \"Remote resizing\": \"远程调整大小\",\n    \"Advanced\": \"高级\",\n    \"Quality:\": \"品质：\",\n    \"Compression level:\": \"压缩级别：\",\n    \"Repeater ID:\": \"中继站 ID\",\n    \"WebSocket\": \"WebSocket\",\n    \"Encrypt\": \"加密\",\n    \"Host:\": \"主机：\",\n    \"Port:\": \"端口：\",\n    \"Path:\": \"路径：\",\n    \"Automatic reconnect\": \"自动重新连接\",\n    \"Reconnect delay (ms):\": \"重新连接间隔 (ms)：\",\n    \"Show dot when no cursor\": \"无光标时显示点\",\n    \"Logging:\": \"日志级别：\",\n    \"Version:\": \"版本：\",\n    \"Disconnect\": \"断开连接\",\n    \"Connect\": \"连接\",\n    \"Server identity\": \"服务器身份\",\n    \"The server has provided the following identifying information:\": \"服务器提供了以下识别信息：\",\n    \"Fingerprint:\": \"指纹：\",\n    \"Please verify that the information is correct and press \\\"Approve\\\". Otherwise press \\\"Reject\\\".\": \"请核实信息是否正确，并按 “同意”，否则按 “拒绝”。\",\n    \"Approve\": \"同意\",\n    \"Reject\": \"拒绝\",\n    \"Credentials\": \"凭证\",\n    \"Username:\": \"用户名:\",\n    \"Password:\": \"密码：\",\n    \"Send credentials\": \"发送凭证\",\n    \"Cancel\": \"取消\",\n    \"Password is required\": \"请提供密码\",\n    \"Disconnect timeout\": \"超时断开\",\n    \"viewport drag\": \"窗口拖动\",\n    \"Active Mouse Button\": \"启动鼠标按键\",\n    \"No mousebutton\": \"禁用鼠标按键\",\n    \"Left mousebutton\": \"鼠标左键\",\n    \"Middle mousebutton\": \"鼠标中键\",\n    \"Right mousebutton\": \"鼠标右键\",\n    \"Clear\": \"清除\",\n    \"Local Downscaling\": \"降低本地尺寸\",\n    \"Local Cursor\": \"本地光标\",\n    \"Canvas not supported.\": \"不支持 Canvas。\"\n}"
  },
  {
    "path": "app/locale/zh_TW.json",
    "content": "{\n    \"Connecting...\": \"連線中...\",\n    \"Disconnecting...\": \"正在中斷連線...\",\n    \"Reconnecting...\": \"重新連線中...\",\n    \"Internal error\": \"內部錯誤\",\n    \"Must set host\": \"請提供主機資訊\",\n    \"Connected (encrypted) to \": \"已加密連線到\",\n    \"Connected (unencrypted) to \": \"未加密連線到\",\n    \"Something went wrong, connection is closed\": \"發生錯誤，連線已關閉\",\n    \"Failed to connect to server\": \"無法連線到伺服器\",\n    \"Disconnected\": \"連線已中斷\",\n    \"New connection has been rejected with reason: \": \"連線被拒絕，原因：\",\n    \"New connection has been rejected\": \"連線被拒絕\",\n    \"Password is required\": \"請提供密碼\",\n    \"noVNC encountered an error:\": \"noVNC 遇到一個錯誤：\",\n    \"Hide/Show the control bar\": \"顯示/隱藏控制列\",\n    \"Move/Drag viewport\": \"拖放顯示範圍\",\n    \"viewport drag\": \"顯示範圍拖放\",\n    \"Active Mouse Button\": \"啟用滑鼠按鍵\",\n    \"No mousebutton\": \"無滑鼠按鍵\",\n    \"Left mousebutton\": \"滑鼠左鍵\",\n    \"Middle mousebutton\": \"滑鼠中鍵\",\n    \"Right mousebutton\": \"滑鼠右鍵\",\n    \"Keyboard\": \"鍵盤\",\n    \"Show keyboard\": \"顯示鍵盤\",\n    \"Extra keys\": \"額外按鍵\",\n    \"Show extra keys\": \"顯示額外按鍵\",\n    \"Ctrl\": \"Ctrl\",\n    \"Toggle Ctrl\": \"切換 Ctrl\",\n    \"Alt\": \"Alt\",\n    \"Toggle Alt\": \"切換 Alt\",\n    \"Send Tab\": \"送出 Tab 鍵\",\n    \"Tab\": \"Tab\",\n    \"Esc\": \"Esc\",\n    \"Send Escape\": \"送出 Escape 鍵\",\n    \"Ctrl+Alt+Del\": \"Ctrl-Alt-Del\",\n    \"Send Ctrl-Alt-Del\": \"送出 Ctrl-Alt-Del 快捷鍵\",\n    \"Shutdown/Reboot\": \"關機/重新啟動\",\n    \"Shutdown/Reboot...\": \"關機/重新啟動...\",\n    \"Power\": \"電源\",\n    \"Shutdown\": \"關機\",\n    \"Reboot\": \"重新啟動\",\n    \"Reset\": \"重設\",\n    \"Clipboard\": \"剪貼簿\",\n    \"Clear\": \"清除\",\n    \"Fullscreen\": \"全螢幕\",\n    \"Settings\": \"設定\",\n    \"Shared mode\": \"分享模式\",\n    \"View only\": \"僅檢視\",\n    \"Clip to window\": \"限制/裁切視窗大小\",\n    \"Scaling mode:\": \"縮放模式：\",\n    \"None\": \"無\",\n    \"Local scaling\": \"本機縮放\",\n    \"Remote resizing\": \"遠端調整大小\",\n    \"Advanced\": \"進階\",\n    \"Repeater ID:\": \"中繼站 ID\",\n    \"WebSocket\": \"WebSocket\",\n    \"Encrypt\": \"加密\",\n    \"Host:\": \"主機：\",\n    \"Port:\": \"連接埠：\",\n    \"Path:\": \"路徑：\",\n    \"Automatic reconnect\": \"自動重新連線\",\n    \"Reconnect delay (ms):\": \"重新連線間隔 (ms)：\",\n    \"Logging:\": \"日誌級別：\",\n    \"Disconnect\": \"中斷連線\",\n    \"Connect\": \"連線\",\n    \"Password:\": \"密碼：\",\n    \"Cancel\": \"取消\"\n}"
  },
  {
    "path": "app/localization.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2018 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n */\n\n/*\n * Localization utilities\n */\n\nexport class Localizer {\n    constructor() {\n        // Currently configured language\n        this.language = 'en';\n\n        // Current dictionary of translations\n        this._dictionary = undefined;\n    }\n\n    // Configure suitable language based on user preferences\n    async setup(supportedLanguages, baseURL) {\n        this.language = 'en'; // Default: US English\n        this._dictionary = undefined;\n\n        this._setupLanguage(supportedLanguages);\n        await this._setupDictionary(baseURL);\n    }\n\n    _setupLanguage(supportedLanguages) {\n        /*\n         * Navigator.languages only available in Chrome (32+) and FireFox (32+)\n         * Fall back to navigator.language for other browsers\n         */\n        let userLanguages;\n        if (typeof window.navigator.languages == 'object') {\n            userLanguages = window.navigator.languages;\n        } else {\n            userLanguages = [navigator.language || navigator.userLanguage];\n        }\n\n        for (let i = 0;i < userLanguages.length;i++) {\n            const userLang = userLanguages[i]\n                .toLowerCase()\n                .replace(\"_\", \"-\")\n                .split(\"-\");\n\n            // First pass: perfect match\n            for (let j = 0; j < supportedLanguages.length; j++) {\n                const supLang = supportedLanguages[j]\n                    .toLowerCase()\n                    .replace(\"_\", \"-\")\n                    .split(\"-\");\n\n                if (userLang[0] !== supLang[0]) {\n                    continue;\n                }\n                if (userLang[1] !== supLang[1]) {\n                    continue;\n                }\n\n                this.language = supportedLanguages[j];\n                return;\n            }\n\n            // Second pass: English fallback\n            if (userLang[0] === 'en') {\n                return;\n            }\n\n            // Third pass pass: other fallback\n            for (let j = 0;j < supportedLanguages.length;j++) {\n                const supLang = supportedLanguages[j]\n                    .toLowerCase()\n                    .replace(\"_\", \"-\")\n                    .split(\"-\");\n\n                if (userLang[0] !== supLang[0]) {\n                    continue;\n                }\n                if (supLang[1] !== undefined) {\n                    continue;\n                }\n\n                this.language = supportedLanguages[j];\n                return;\n            }\n        }\n    }\n\n    async _setupDictionary(baseURL) {\n        if (baseURL) {\n            if (!baseURL.endsWith(\"/\")) {\n                baseURL = baseURL + \"/\";\n            }\n        } else {\n            baseURL = \"\";\n        }\n\n        if (this.language === \"en\") {\n            return;\n        }\n\n        let response = await fetch(baseURL + this.language + \".json\");\n        if (!response.ok) {\n            throw Error(\"\" + response.status + \" \" + response.statusText);\n        }\n\n        this._dictionary = await response.json();\n    }\n\n    // Retrieve localised text\n    get(id) {\n        if (typeof this._dictionary !== 'undefined' &&\n            this._dictionary[id]) {\n            return this._dictionary[id];\n        } else {\n            return id;\n        }\n    }\n\n    // Traverses the DOM and translates relevant fields\n    // See https://html.spec.whatwg.org/multipage/dom.html#attr-translate\n    translateDOM() {\n        const self = this;\n\n        function process(elem, enabled) {\n            function isAnyOf(searchElement, items) {\n                return items.indexOf(searchElement) !== -1;\n            }\n\n            function translateString(str) {\n                // We assume surrounding whitespace, and whitespace around line\n                // breaks is just for source formatting\n                str = str.split(\"\\n\").map(s => s.trim()).join(\" \").trim();\n                return self.get(str);\n            }\n\n            function translateAttribute(elem, attr) {\n                const str = translateString(elem.getAttribute(attr));\n                elem.setAttribute(attr, str);\n            }\n\n            function translateTextNode(node) {\n                const str = translateString(node.data);\n                node.data = str;\n            }\n\n            if (elem.hasAttribute(\"translate\")) {\n                if (isAnyOf(elem.getAttribute(\"translate\"), [\"\", \"yes\"])) {\n                    enabled = true;\n                } else if (isAnyOf(elem.getAttribute(\"translate\"), [\"no\"])) {\n                    enabled = false;\n                }\n            }\n\n            if (enabled) {\n                if (elem.hasAttribute(\"abbr\") &&\n                    elem.tagName === \"TH\") {\n                    translateAttribute(elem, \"abbr\");\n                }\n                if (elem.hasAttribute(\"alt\") &&\n                    isAnyOf(elem.tagName, [\"AREA\", \"IMG\", \"INPUT\"])) {\n                    translateAttribute(elem, \"alt\");\n                }\n                if (elem.hasAttribute(\"download\") &&\n                    isAnyOf(elem.tagName, [\"A\", \"AREA\"])) {\n                    translateAttribute(elem, \"download\");\n                }\n                if (elem.hasAttribute(\"label\") &&\n                    isAnyOf(elem.tagName, [\"MENUITEM\", \"MENU\", \"OPTGROUP\",\n                                           \"OPTION\", \"TRACK\"])) {\n                    translateAttribute(elem, \"label\");\n                }\n                // FIXME: Should update \"lang\"\n                if (elem.hasAttribute(\"placeholder\") &&\n                    isAnyOf(elem.tagName, [\"INPUT\", \"TEXTAREA\"])) {\n                    translateAttribute(elem, \"placeholder\");\n                }\n                if (elem.hasAttribute(\"title\")) {\n                    translateAttribute(elem, \"title\");\n                }\n                if (elem.hasAttribute(\"value\") &&\n                    elem.tagName === \"INPUT\" &&\n                    isAnyOf(elem.getAttribute(\"type\"), [\"reset\", \"button\", \"submit\"])) {\n                    translateAttribute(elem, \"value\");\n                }\n            }\n\n            for (let i = 0; i < elem.childNodes.length; i++) {\n                const node = elem.childNodes[i];\n                if (node.nodeType === node.ELEMENT_NODE) {\n                    process(node, enabled);\n                } else if (node.nodeType === node.TEXT_NODE && enabled) {\n                    translateTextNode(node);\n                }\n            }\n        }\n\n        process(document.body, true);\n    }\n}\n\nexport const l10n = new Localizer();\nexport default l10n.get.bind(l10n);\n"
  },
  {
    "path": "app/sounds/CREDITS",
    "content": "bell\n        Copyright: Dr. Richard Boulanger et al\n        URL: http://www.archive.org/details/Berklee44v12\n        License: CC-BY Attribution 3.0 Unported\n"
  },
  {
    "path": "app/styles/base.css",
    "content": "/*\n * noVNC base CSS\n * Copyright (C) 2019 The noVNC authors\n * noVNC is licensed under the MPL 2.0 (see LICENSE.txt)\n * This file is licensed under the 2-Clause BSD license (see LICENSE.txt).\n */\n\n/*\n * Z index layers:\n *\n * 0: Main screen\n * 10: Control bar\n * 50: Transition blocker\n * 60: Connection popups\n * 100: Status bar\n * ...\n * 1000: Javascript crash\n * ...\n * 10000: Max (used for polyfills)\n */\n\n/*\n * State variables (set on :root):\n *\n * noVNC_loading: Page is still loading\n * noVNC_connecting: Connecting to server\n * noVNC_reconnecting: Re-establishing a connection\n * noVNC_connected: Connected to server (most common state)\n * noVNC_disconnecting: Disconnecting from server\n */\n\n:root {\n    font-family: sans-serif;\n    line-height: 1.6;\n}\n\nbody {\n    margin:0;\n    padding:0;\n    /*Background image with light grey curve.*/\n    background-color:#494949;\n    background-repeat:no-repeat;\n    background-position:right bottom;\n    height:100%;\n    touch-action: none;\n}\n\nhtml {\n    height:100%;\n}\n\n.noVNC_only_touch.noVNC_hidden {\n    display: none;\n}\n\n.noVNC_disabled {\n    color: var(--novnc-grey);\n}\n\n/* ----------------------------------------\n * Spinner\n * ----------------------------------------\n */\n\n.noVNC_spinner {\n    position: relative;\n}\n.noVNC_spinner, .noVNC_spinner::before, .noVNC_spinner::after {\n    width: 10px;\n    height: 10px;\n    border-radius: 2px;\n    box-shadow: -60px 10px 0 rgba(255, 255, 255, 0);\n    animation: noVNC_spinner 1.0s linear infinite;\n}\n.noVNC_spinner::before {\n    content: \"\";\n    position: absolute;\n    left: 0px;\n    top: 0px;\n    animation-delay: -0.1s;\n}\n.noVNC_spinner::after {\n    content: \"\";\n    position: absolute;\n    top: 0px;\n    left: 0px;\n    animation-delay: 0.1s;\n}\n@keyframes noVNC_spinner {\n    0% { box-shadow: -60px 10px 0 rgba(255, 255, 255, 0); width: 20px; }\n    25% { box-shadow: 20px 10px 0 rgba(255, 255, 255, 1); width: 10px; }\n    50% { box-shadow: 60px 10px 0 rgba(255, 255, 255, 0); width: 10px; }\n}\n\n/* ----------------------------------------\n * WebKit centering hacks\n * ----------------------------------------\n */\n\n.noVNC_center {\n    /*\n     * This is a workaround because webkit misrenders transforms and\n     * uses non-integer coordinates, resulting in blurry content.\n     * Ideally we'd use \"top: 50%; transform: translateY(-50%);\" on\n     * the objects instead.\n     */\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    pointer-events: none;\n}\n.noVNC_center > * {\n    pointer-events: auto;\n}\n.noVNC_vcenter {\n    display: flex !important;\n    flex-direction: column;\n    justify-content: center;\n    position: fixed;\n    top: 0;\n    left: 0;\n    height: 100%;\n    margin: 0 !important;\n    padding: 0 !important;\n    pointer-events: none;\n}\n.noVNC_vcenter > * {\n    pointer-events: auto;\n}\n\n/* ----------------------------------------\n * Layering\n * ----------------------------------------\n */\n\n.noVNC_connect_layer {\n    z-index: 60;\n}\n\n/* ----------------------------------------\n * Fallback error\n * ----------------------------------------\n */\n\n#noVNC_fallback_error {\n    z-index: 1000;\n    visibility: hidden;\n    /* Put a dark background in front of everything but the error,\n       and don't let mouse events pass through */\n    background: rgba(0, 0, 0, 0.8);\n    pointer-events: all;\n}\n#noVNC_fallback_error.noVNC_open {\n    visibility: visible;\n}\n\n#noVNC_fallback_error > div {\n    max-width: calc(100vw - 30px - 30px);\n    max-height: calc(100vh - 30px - 30px);\n    overflow: auto;\n\n    padding: 15px;\n\n    transition: 0.5s ease-in-out;\n\n    transform: translateY(-50px);\n    opacity: 0;\n\n    text-align: center;\n    font-weight: bold;\n    color: #fff;\n\n    border-radius: 12px;\n    box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);\n    background: rgba(200,55,55,0.8);\n}\n#noVNC_fallback_error.noVNC_open > div {\n    transform: translateY(0);\n    opacity: 1;\n}\n\n#noVNC_fallback_errormsg {\n    font-weight: normal;\n}\n\n#noVNC_fallback_errormsg .noVNC_message {\n    display: inline-block;\n    text-align: left;\n    font-family: monospace;\n    white-space: pre-wrap;\n}\n\n#noVNC_fallback_error .noVNC_location {\n    font-style: italic;\n    font-size: 0.8em;\n    color: rgba(255, 255, 255, 0.8);\n}\n\n#noVNC_fallback_error .noVNC_stack {\n    padding: 10px;\n    margin: 10px;\n    font-size: 0.8em;\n    text-align: left;\n    font-family: monospace;\n    white-space: pre;\n    border: 1px solid rgba(0, 0, 0, 0.5);\n    background: rgba(0, 0, 0, 0.2);\n    overflow: auto;\n}\n\n/* ----------------------------------------\n * Control bar\n * ----------------------------------------\n */\n\n#noVNC_control_bar_anchor {\n    /* The anchor is needed to get z-stacking to work */\n    position: fixed;\n    z-index: 10;\n\n    transition: 0.5s ease-in-out;\n\n    /* Edge misrenders animations wihthout this */\n    transform: translateX(0);\n}\n:root.noVNC_connected #noVNC_control_bar_anchor.noVNC_idle {\n    opacity: 0.8;\n}\n#noVNC_control_bar_anchor.noVNC_right {\n    left: auto;\n    right: 0;\n}\n\n#noVNC_control_bar {\n    position: relative;\n    left: -100%;\n\n    transition: 0.5s ease-in-out;\n\n    background-color: var(--novnc-blue);\n    border-radius: 0 12px 12px 0;\n\n    user-select: none;\n    -webkit-user-select: none;\n    -webkit-touch-callout: none; /* Disable iOS image long-press popup */\n}\n#noVNC_control_bar.noVNC_open {\n    box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);\n    left: 0;\n}\n#noVNC_control_bar::before {\n    /* This extra element is to get a proper shadow */\n    content: \"\";\n    position: absolute;\n    z-index: -1;\n    height: 100%;\n    width: 30px;\n    left: -30px;\n    transition: box-shadow 0.5s ease-in-out;\n}\n#noVNC_control_bar.noVNC_open::before {\n    box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);\n}\n.noVNC_right #noVNC_control_bar {\n    left: 100%;\n    border-radius: 12px 0 0 12px;\n}\n.noVNC_right #noVNC_control_bar.noVNC_open {\n    left: 0;\n}\n.noVNC_right #noVNC_control_bar::before {\n    visibility: hidden;\n}\n\n#noVNC_control_bar_handle {\n    position: absolute;\n    left: -15px;\n    top: 0;\n    transform: translateY(35px);\n    width: calc(100% + 30px);\n    height: 50px;\n    z-index: -1;\n    cursor: pointer;\n    border-radius: 6px;\n    background-color: var(--novnc-darkblue);\n    background-image: url(\"../images/handle_bg.svg\");\n    background-repeat: no-repeat;\n    background-position: right;\n    box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.5);\n}\n#noVNC_control_bar_handle:after {\n    content: \"\";\n    transition: transform 0.5s ease-in-out;\n    background: url(\"../images/handle.svg\");\n    position: absolute;\n    top: 22px; /* (50px-6px)/2 */\n    right: 5px;\n    width: 5px;\n    height: 6px;\n}\n#noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {\n    transform: translateX(1px) rotate(180deg);\n}\n:root:not(.noVNC_connected) #noVNC_control_bar_handle {\n    display: none;\n}\n.noVNC_right #noVNC_control_bar_handle {\n    background-position: left;\n}\n.noVNC_right #noVNC_control_bar_handle:after {\n    left: 5px;\n    right: 0;\n    transform: translateX(1px) rotate(180deg);\n}\n.noVNC_right #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {\n    transform: none;\n}\n/* Larger touch area for the handle, used when a touch screen is available */\n#noVNC_control_bar_handle div {\n    position: absolute;\n    right: -35px;\n    top: 0;\n    width: 50px;\n    height: 100%;\n    display: none;\n}\n@media (any-pointer: coarse) {\n    #noVNC_control_bar_handle div {\n        display: initial;\n    }\n}\n.noVNC_right #noVNC_control_bar_handle div {\n    left: -35px;\n    right: auto;\n}\n\n#noVNC_control_bar > .noVNC_scroll {\n    max-height: 100vh; /* Chrome is buggy with 100% */\n    overflow-x: hidden;\n    overflow-y: auto;\n    padding: 0 10px;\n}\n\n#noVNC_control_bar > .noVNC_scroll > * {\n    display: block;\n    margin: 10px auto;\n}\n\n/* Control bar hint */\n#noVNC_hint_anchor {\n    position: fixed;\n    right: -50px;\n    left: auto;\n}\n#noVNC_control_bar_anchor.noVNC_right + #noVNC_hint_anchor {\n    left: -50px;\n    right: auto;\n}\n#noVNC_control_bar_hint {\n    position: relative;\n    transform: scale(0);\n    width: 100px;\n    height: 50%;\n    max-height: 600px;\n\n    visibility: hidden;\n    opacity: 0;\n    transition: 0.2s ease-in-out;\n    background: transparent;\n    box-shadow: 0 0 10px black, inset 0 0 10px 10px var(--novnc-darkblue);\n    border-radius: 12px;\n    transition-delay: 0s;\n}\n#noVNC_control_bar_hint.noVNC_active {\n    visibility: visible;\n    opacity: 1;\n    transition-delay: 0.2s;\n    transform: scale(1);\n}\n#noVNC_control_bar_hint.noVNC_notransition {\n    transition: none !important;\n}\n\n/* Control bar buttons */\n#noVNC_control_bar .noVNC_button {\n    min-width: unset;\n    padding: 4px 4px;\n    vertical-align: middle;\n    border:1px solid rgba(255, 255, 255, 0.2);\n    border-radius: 6px;\n    background-color: transparent;\n}\n#noVNC_control_bar .noVNC_button.noVNC_selected {\n    border-color: rgba(0, 0, 0, 0.8);\n    background-color: rgba(0, 0, 0, 0.5);\n}\n#noVNC_control_bar .noVNC_button.noVNC_hidden {\n    display: none !important;\n}\n\n/* Panels */\n.noVNC_panel {\n    transform: translateX(25px);\n\n    transition: 0.5s ease-in-out;\n\n    box-sizing: border-box; /* so max-width don't have to care about padding */\n    max-width: calc(100vw - 75px - 25px); /* minus left and right margins */\n    max-height: 100vh; /* Chrome is buggy with 100% */\n    overflow-x: hidden;\n    overflow-y: auto;\n\n    visibility: hidden;\n    opacity: 0;\n\n    padding: 15px;\n\n    background: #fff;\n    border-radius: 12px;\n    color: #000;\n    border: 2px solid #E0E0E0;\n    box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);\n}\n.noVNC_panel.noVNC_open {\n    visibility: visible;\n    opacity: 1;\n    transform: translateX(75px);\n}\n.noVNC_right .noVNC_vcenter {\n    left: auto;\n    right: 0;\n}\n.noVNC_right .noVNC_panel {\n    transform: translateX(-25px);\n}\n.noVNC_right .noVNC_panel.noVNC_open {\n    transform: translateX(-75px);\n}\n\n.noVNC_panel > * {\n    display: block;\n    margin: 10px auto;\n}\n.noVNC_panel > *:first-child {\n    margin-top: 0 !important;\n}\n.noVNC_panel > *:last-child {\n    margin-bottom: 0 !important;\n}\n\n.noVNC_panel hr {\n    border: none;\n    border-top: 1px solid var(--novnc-lightgrey);\n    width: 100%; /* <hr> inside a flexbox will otherwise be 0px wide */\n}\n\n.noVNC_panel label {\n    display: block;\n    white-space: nowrap;\n    margin: 5px;\n}\n@media (max-width: 540px) {\n    /* Allow wrapping on small screens */\n    .noVNC_panel label {\n        white-space: unset;\n    }\n}\n\n.noVNC_panel li {\n    margin: 5px;\n}\n\n.noVNC_panel .noVNC_heading {\n    background-color: var(--novnc-blue);\n    border-radius: 6px;\n    padding: 5px 8px;\n    /* Compensate for padding in image */\n    padding-right: 11px;\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    color: white;\n    font-size: 20px;\n    font-weight: bold;\n    white-space: nowrap;\n}\n.noVNC_panel .noVNC_heading img {\n    vertical-align: bottom;\n}\n\n.noVNC_panel form {\n    display: flex;\n    flex-direction: column;\n    gap: 12px\n}\n\n.noVNC_panel .button_row {\n    margin-top: 10px;\n    display: flex;\n    gap: 10px;\n    justify-content: space-between;\n}\n.noVNC_panel .button_row *:only-child {\n    margin-left: auto; /* Align single buttons to the right */\n}\n\n/* Expanders */\n.noVNC_expander {\n    cursor: pointer;\n}\n.noVNC_expander::before {\n    content: url(\"../images/expander.svg\");\n    display: inline-block;\n    margin-right: 5px;\n    transition: 0.2s ease-in-out;\n}\n.noVNC_expander.noVNC_open::before {\n    transform: rotateZ(90deg);\n}\n.noVNC_expander ~ * {\n    margin: 5px;\n    margin-left: 10px;\n    padding: 5px;\n    background: rgba(0, 0, 0, 0.04);\n    border-radius: 6px;\n}\n.noVNC_expander:not(.noVNC_open) ~ * {\n    display: none;\n}\n\n/* Control bar content */\n\n#noVNC_control_bar .noVNC_logo {\n    font-size: 13px;\n}\n\n.noVNC_logo + hr {\n    /* Remove all but top border */\n    border: none;\n    border-top: 1px solid rgba(255, 255, 255, 0.2);\n}\n\n:root:not(.noVNC_connected) #noVNC_view_drag_button {\n    display: none;\n}\n\n/* noVNC Touch Device only buttons */\n:root:not(.noVNC_connected) #noVNC_mobile_buttons {\n    display: none;\n}\n@media not all and (any-pointer: coarse) {\n    /* FIXME: The button for the virtual keyboard is the only button in this\n              group of \"mobile buttons\". It is bad to assume that no touch\n              devices have physical keyboards available. Hopefully we can get\n              a media query for this:\n              https://github.com/w3c/csswg-drafts/issues/3871 */\n    :root.noVNC_connected #noVNC_mobile_buttons {\n        display: none;\n    }\n}\n\n/* Extra manual keys */\n:root:not(.noVNC_connected) #noVNC_toggle_extra_keys_button {\n    display: none;\n}\n\n#noVNC_modifiers {\n    background-color: var(--novnc-darkgrey);\n    border: none;\n    padding: 10px;\n}\n\n/* Shutdown/Reboot */\n:root:not(.noVNC_connected) #noVNC_power_button {\n    display: none;\n}\n#noVNC_power {\n}\n#noVNC_power_buttons {\n    display: none;\n}\n\n#noVNC_power input[type=button] {\n    width: 100%;\n}\n\n/* Clipboard */\n:root:not(.noVNC_connected) #noVNC_clipboard_button {\n    display: none;\n}\n#noVNC_clipboard_text {\n    width: 360px;\n    min-width: 150px;\n    height: 160px;\n    min-height: 70px;\n\n    box-sizing: border-box;\n    max-width: 100%;\n    /* minus approximate height of title, height of subtitle, and margin */\n    max-height: calc(100vh - 10em - 25px);\n}\n\n/* Settings */\n#noVNC_settings {\n}\n#noVNC_settings ul {\n    list-style: none;\n    padding: 0px;\n}\n#noVNC_settings button,\n#noVNC_settings select,\n#noVNC_settings textarea,\n#noVNC_settings input:not([type=checkbox]):not([type=radio]) {\n    margin-left: 6px;\n    /* Prevent inputs in settings from being too wide */\n    max-width: calc(100% - 6px - var(--input-xpadding) * 2);\n}\n\n#noVNC_setting_port {\n    width: 80px;\n}\n#noVNC_setting_path {\n    width: 100px;\n}\n\n/* Version */\n\n.noVNC_version_wrapper {\n    font-size: small;\n}\n\n.noVNC_version {\n    margin-left: 1rem;\n}\n\n/* Connection controls */\n:root:not(.noVNC_connected) #noVNC_disconnect_button {\n    display: none;\n}\n\n/* ----------------------------------------\n * Status dialog\n * ----------------------------------------\n */\n\n#noVNC_status {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    z-index: 100;\n    transform: translateY(-100%);\n\n    cursor: pointer;\n\n    transition: 0.5s ease-in-out;\n\n    visibility: hidden;\n    opacity: 0;\n\n    padding: 5px;\n\n    display: flex;\n    flex-direction: row;\n    justify-content: center;\n    align-content: center;\n\n    line-height: 1.6;\n    word-wrap: break-word;\n    color: #fff;\n\n    border-bottom: 1px solid rgba(0, 0, 0, 0.9);\n}\n#noVNC_status.noVNC_open {\n    transform: translateY(0);\n    visibility: visible;\n    opacity: 1;\n}\n\n#noVNC_status::before {\n    content: \"\";\n    display: inline-block;\n    width: 25px;\n    height: 25px;\n    margin-right: 5px;\n}\n\n#noVNC_status.noVNC_status_normal {\n    background: rgba(128,128,128,0.9);\n}\n#noVNC_status.noVNC_status_normal::before {\n    content: url(\"../images/info.svg\") \" \";\n}\n#noVNC_status.noVNC_status_error {\n    background: rgba(200,55,55,0.9);\n}\n#noVNC_status.noVNC_status_error::before {\n    content: url(\"../images/error.svg\") \" \";\n}\n#noVNC_status.noVNC_status_warn {\n    background: rgba(180,180,30,0.9);\n}\n#noVNC_status.noVNC_status_warn::before {\n    content: url(\"../images/warning.svg\") \" \";\n}\n\n/* ----------------------------------------\n * Connect dialog\n * ----------------------------------------\n */\n\n#noVNC_connect_dlg {\n    transition: 0.5s ease-in-out;\n\n    transform: scale(0, 0);\n    visibility: hidden;\n    opacity: 0;\n}\n#noVNC_connect_dlg.noVNC_open {\n    transform: scale(1, 1);\n    visibility: visible;\n    opacity: 1;\n}\n#noVNC_connect_dlg .noVNC_logo {\n    transition: 0.5s ease-in-out;\n    padding: 10px;\n    margin-bottom: 10px;\n\n    font-size: 80px;\n    text-align: center;\n\n    border-radius: 6px;\n}\n@media (max-width: 440px) {\n    #noVNC_connect_dlg {\n        max-width: calc(100vw - 100px);\n    }\n    #noVNC_connect_dlg .noVNC_logo {\n        font-size: calc(25vw - 30px);\n    }\n}\n#noVNC_connect_dlg div {\n    padding: 18px;\n\n    background-color: var(--novnc-darkgrey);\n    border-radius: 12px;\n    text-align: center;\n    font-size: 20px;\n\n    box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);\n}\n#noVNC_connect_button {\n    width: 100%;\n    padding: 6px 30px;\n    cursor: pointer;\n    border-color: transparent;\n    border-radius: 12px;\n    background-color: var(--novnc-blue);\n    color: white;\n\n    display: flex;\n    justify-content: center;\n    place-items: center;\n    gap: 4px;\n}\n\n#noVNC_connect_button img {\n    vertical-align: bottom;\n    height: 1.3em;\n}\n\n/* ----------------------------------------\n * Server verification dialog\n * ----------------------------------------\n */\n\n#noVNC_verify_server_dlg {\n    position: relative;\n\n    transform: translateY(-50px);\n}\n#noVNC_verify_server_dlg.noVNC_open {\n    transform: translateY(0);\n}\n#noVNC_fingerprint_block {\n    margin: 10px;\n}\n\n/* ----------------------------------------\n * Password dialog\n * ----------------------------------------\n */\n\n#noVNC_credentials_dlg {\n    position: relative;\n\n    transform: translateY(-50px);\n}\n#noVNC_credentials_dlg.noVNC_open {\n    transform: translateY(0);\n}\n#noVNC_username_block.noVNC_hidden,\n#noVNC_password_block.noVNC_hidden {\n    display: none;\n}\n\n\n/* ----------------------------------------\n * Main area\n * ----------------------------------------\n */\n\n/* Transition screen */\n#noVNC_transition {\n    transition: 0.5s ease-in-out;\n\n    display: flex;\n    opacity: 0;\n    visibility: hidden;\n\n    position: fixed;\n    top: 0;\n    left: 0;\n    bottom: 0;\n    right: 0;\n\n    color: white;\n    background: rgba(0, 0, 0, 0.5);\n    z-index: 50;\n\n    /*display: flex;*/\n    align-items: center;\n    justify-content: center;\n    flex-direction: column;\n}\n:root.noVNC_loading #noVNC_transition,\n:root.noVNC_connecting #noVNC_transition,\n:root.noVNC_disconnecting #noVNC_transition,\n:root.noVNC_reconnecting #noVNC_transition {\n    opacity: 1;\n    visibility: visible;\n}\n:root:not(.noVNC_reconnecting) #noVNC_cancel_reconnect_button {\n    display: none;\n}\n#noVNC_transition_text {\n    font-size: 1.5em;\n}\n\n/* Main container */\n#noVNC_container {\n    width: 100%;\n    height: 100%;\n    background-color: #313131;\n    border-bottom-right-radius: 800px 600px;\n    /*border-top-left-radius: 800px 600px;*/\n\n    /* If selection isn't disabled, long-pressing stuff in the sidebar\n       can accidentally select the container or the canvas. This can\n       happen when attempting to move the handle. */\n    user-select: none;\n    -webkit-user-select: none;\n}\n\n#noVNC_keyboardinput {\n    width: 1px;\n    height: 1px;\n    background-color: #fff;\n    color: #fff;\n    border: 0;\n    position: absolute;\n    left: -40px;\n    z-index: -1;\n    ime-mode: disabled;\n}\n\n/*Default noVNC logo.*/\n/* From: http://fonts.googleapis.com/css?family=Orbitron:700 */\n@font-face {\n    font-family: 'Orbitron';\n    font-style: normal;\n    font-weight: 700;\n    src: local('?'), url('Orbitron700.woff') format('woff'),\n                     url('Orbitron700.ttf') format('truetype');\n}\n\n.noVNC_logo {\n    color: var(--novnc-yellow);\n    font-family: 'Orbitron', 'OrbitronTTF', sans-serif;\n    line-height: 0.9;\n    text-shadow: 0.1em 0.1em 0 black;\n}\n.noVNC_logo span{\n    color: var(--novnc-green);\n}\n\n#noVNC_bell {\n    display: none;\n}\n\n/* ----------------------------------------\n * Media sizing\n * ----------------------------------------\n */\n\n@media screen and (max-width: 640px){\n    #noVNC_logo {\n        font-size: 150px;\n    }\n}\n\n@media screen and (min-width: 321px) and (max-width: 480px) {\n    #noVNC_logo {\n        font-size: 110px;\n    }\n}\n\n@media screen and (max-width: 320px) {\n    #noVNC_logo {\n        font-size: 90px;\n    }\n}\n"
  },
  {
    "path": "app/styles/constants.css",
    "content": "/*\n * noVNC general CSS constant variables\n * Copyright (C) 2025 The noVNC authors\n * noVNC is licensed under the MPL 2.0 (see LICENSE.txt)\n * This file is licensed under the 2-Clause BSD license (see LICENSE.txt).\n */\n\n/* ---------- COLORS ----------- */\n\n:root {\n    --novnc-grey: rgb(128, 128, 128);\n    --novnc-lightgrey: rgb(192, 192, 192);\n    --novnc-darkgrey: rgb(92, 92, 92);\n\n    /* Transparent to make button colors adapt to the background */\n    --novnc-buttongrey: rgba(192, 192, 192, 0.5);\n\n    --novnc-blue: rgb(110, 132, 163);\n    --novnc-lightblue: rgb(74, 144, 217);\n    --novnc-darkblue: rgb(83, 99, 122);\n\n    --novnc-green: rgb(0, 128, 0);\n    --novnc-yellow: rgb(255, 255, 0);\n}\n\n/* ------ MISC PROPERTIES ------ */\n\n:root {\n    --input-xpadding: 1em;\n}\n"
  },
  {
    "path": "app/styles/input.css",
    "content": "/*\n * noVNC general input element CSS\n * Copyright (C) 2025 The noVNC authors\n * noVNC is licensed under the MPL 2.0 (see LICENSE.txt)\n * This file is licensed under the 2-Clause BSD license (see LICENSE.txt).\n */\n\n/* ------- SHARED BETWEEN INPUT ELEMENTS -------- */\n\ninput,\ntextarea,\nbutton,\nselect,\ninput::file-selector-button {\n    padding: 0.5em var(--input-xpadding);\n    border-radius: 6px;\n    appearance: none;\n    text-overflow: ellipsis;\n\n    /* Respect standard font settings */\n    font: inherit;\n    line-height: 1.6;\n}\ninput:disabled,\ntextarea:disabled,\nbutton:disabled,\nselect:disabled,\nlabel[disabled] {\n    opacity: 0.4;\n}\n\ninput:focus-visible,\ntextarea:focus-visible,\nbutton:focus-visible,\nselect:focus-visible,\ninput:focus-visible::file-selector-button {\n    outline: 2px solid var(--novnc-lightblue);\n    outline-offset: 1px;\n}\n\n/* ------- TEXT INPUT -------- */\n\ninput:not([type]),\ninput[type=date],\ninput[type=datetime-local],\ninput[type=email],\ninput[type=month],\ninput[type=number],\ninput[type=password],\ninput[type=search],\ninput[type=tel],\ninput[type=text],\ninput[type=time],\ninput[type=url],\ninput[type=week],\ntextarea {\n    border: 1px solid var(--novnc-lightgrey);\n    /* Account for borders on text inputs, buttons dont have borders */\n    padding: calc(0.5em - 1px) var(--input-xpadding);\n}\ninput:not([type]):focus-visible,\ninput[type=date]:focus-visible,\ninput[type=datetime-local]:focus-visible,\ninput[type=email]:focus-visible,\ninput[type=month]:focus-visible,\ninput[type=number]:focus-visible,\ninput[type=password]:focus-visible,\ninput[type=search]:focus-visible,\ninput[type=tel]:focus-visible,\ninput[type=text]:focus-visible,\ninput[type=time]:focus-visible,\ninput[type=url]:focus-visible,\ninput[type=week]:focus-visible,\ntextarea:focus-visible {\n    outline-offset: -1px;\n}\n\ntextarea {\n    margin: unset; /* Remove Firefox's built in margin */\n    /* Prevent layout from shifting when scrollbars show */\n    scrollbar-gutter: stable;\n    /* Make textareas show at minimum one line. This does not work when\n       using box-sizing border-box, in which case, vertical padding and\n       border width needs to be taken into account. */\n    min-height: 1lh;\n    vertical-align: baseline; /* Firefox gives \"text-bottom\" by default */\n}\n\n/* ------- NUMBER PICKERS ------- */\n\n/* We can't style the number spinner buttons:\n   https://github.com/w3c/csswg-drafts/issues/8777 */\ninput[type=number]::-webkit-inner-spin-button,\ninput[type=number]::-webkit-outer-spin-button {\n    /* Get rid of increase/decrease buttons in WebKit */\n    appearance: none;\n}\ninput[type=number] {\n    /* Get rid of increase/decrease buttons in Firefox */\n    appearance: textfield;\n}\n\n/* ------- BUTTON ACTIVATIONS -------- */\n\n/* A color overlay that depends on the activation level. The level can then be\n   set for different states on an element, for example hover and click on a\n   <button>. */\ninput, button, select, option,\ninput::file-selector-button,\n.button-activations {\n    --button-activation-level: 0;\n    /* Note that CSS variables aren't functions, beware when inheriting */\n    --button-activation-alpha: calc(0.08 * var(--button-activation-level));\n    /* FIXME: We want the image() function instead of the linear-gradient()\n              function below. But it's not supported in the browsers yet. */\n    --button-activation-overlay:\n        linear-gradient(rgba(0, 0, 0, var(--button-activation-alpha))\n        100%, transparent);\n    --button-activation-overlay-light:\n        linear-gradient(rgba(255, 255, 255, calc(0.23 * var(--button-activation-level)))\n        100%, transparent);\n}\n.button-activations {\n    background-image: var(--button-activation-overlay);\n\n    /* Disable Chrome's touch tap highlight to avoid conflicts with overlay */\n    -webkit-tap-highlight-color: transparent;\n}\n/* When we want the light overlay on activations instead.\n   This is best used on elements with darker backgrounds. */\n.button-activations.light-overlay {\n    background-image: var(--button-activation-overlay-light);\n    /* Can't use the normal blend mode since that gives washed out colors. */\n    /* FIXME: For elements with these activation overlays we'd like only\n              the luminosity to change. The proprty \"background-blend-mode\" set\n              to \"luminosity\" sounds good, but it doesn't work as intended,\n              see: https://bugzilla.mozilla.org/show_bug.cgi?id=1806417 */\n    background-blend-mode: overlay;\n}\n\ninput:hover, button:hover, select:hover, option:hover,\ninput::file-selector-button:hover,\n.button-activations:hover {\n    --button-activation-level: 1;\n}\n/* Unfortunately we have to disable the :hover effect on touch devices,\n   otherwise the style lingers after tapping the button. */\n@media (any-pointer: coarse) {\n    input:hover, button:hover, select:hover, option:hover,\n    input::file-selector-button:hover,\n    .button-activations:hover {\n        --button-activation-level: 0;\n    }\n}\ninput:active, button:active, select:active, option:active,\ninput::file-selector-button:active,\n.button-activations:active {\n    --button-activation-level: 2;\n}\ninput:disabled, button:disabled, select:disabled, select:disabled option,\ninput:disabled::file-selector-button,\n.button-activations:disabled {\n    --button-activation-level: 0;\n}\n\n/* ------- BUTTONS -------- */\n\ninput[type=button],\ninput[type=color],\ninput[type=image],\ninput[type=reset],\ninput[type=submit],\ninput::file-selector-button,\nbutton,\nselect {\n    min-width: 8em;\n    border: none;\n    color: black;\n    font-weight: bold;\n    background-color: var(--novnc-buttongrey);\n    background-image: var(--button-activation-overlay);\n    cursor: pointer;\n    /* Disable Chrome's touch tap highlight */\n    -webkit-tap-highlight-color: transparent;\n}\ninput[type=button]:disabled,\ninput[type=color]:disabled,\ninput[type=image]:disabled,\ninput[type=reset]:disabled,\ninput[type=submit]:disabled,\ninput:disabled::file-selector-button,\nbutton:disabled,\nselect:disabled {\n    /* See Firefox bug:\n       https://bugzilla.mozilla.org/show_bug.cgi?id=1798304 */\n    cursor: default;\n}\n\ninput[type=button],\ninput[type=color],\ninput[type=reset],\ninput[type=submit] {\n    /* Workaround for text-overflow bugs in Firefox and Chromium:\n        https://bugzilla.mozilla.org/show_bug.cgi?id=1800077\n        https://bugs.chromium.org/p/chromium/issues/detail?id=1383144 */\n    overflow: clip;\n}\n\n/* ------- COLOR PICKERS ------- */\n\ninput[type=color] {\n    min-width: unset;\n    box-sizing: content-box;\n    width: 1.4em;\n    height: 1.4em;\n}\ninput[type=color]::-webkit-color-swatch-wrapper {\n    padding: 0;\n}\n/* -webkit-color-swatch & -moz-color-swatch cant be in a selector list:\n   https://bugs.chromium.org/p/chromium/issues/detail?id=1154623 */\ninput[type=color]::-webkit-color-swatch {\n    border: none;\n    border-radius: 6px;\n}\ninput[type=color]::-moz-color-swatch {\n    border: none;\n    border-radius: 6px;\n}\n\n/* -- SHARED BETWEEN CHECKBOXES, RADIOBUTTONS AND THE TOGGLE CLASS -- */\n\ninput[type=radio],\ninput[type=checkbox] {\n    display: inline-flex;\n    justify-content: center;\n    align-items: center;\n    background-color: var(--novnc-buttongrey);\n    background-image: var(--button-activation-overlay);\n    /* Disable Chrome's touch tap highlight to avoid conflicts with overlay */\n    -webkit-tap-highlight-color: transparent;\n    width: 16px;\n    --checkradio-height: 16px;\n    height: var(--checkradio-height);\n    padding: 0;\n    margin: 0 6px 0 0;\n    /* Don't have transitions for outline in order to be consistent\n       with other elements */\n    transition: all 0.2s, outline-color 0s, outline-offset 0s;\n\n    /* A transparent outline in order to work around a graphical clipping issue\n       in WebKit. See bug: https://bugs.webkit.org/show_bug.cgi?id=256003 */\n    outline: 1px solid transparent;\n    position: relative; /* Since ::before & ::after are absolute positioned */\n\n    /* We want to align with the middle of capital letters, this requires\n       a workaround. The default behavior is to align the bottom of the element\n       on top of the text baseline, this is too far up.\n       We want to push the element down half the difference in height between\n       it and a capital X. In our font, the height of a capital \"X\" is 0.698em.\n     */\n    vertical-align: calc(0px - (var(--checkradio-height) - 0.698em) / 2);\n    /* FIXME: Could write 1cap instead of 0.698em, but it's only supported in\n              Firefox as of 2023 */\n    /* FIXME: We probably want to use round() here, see bug 8148 */\n}\ninput[type=radio]:focus-visible,\ninput[type=checkbox]:focus-visible {\n    outline-color: var(--novnc-lightblue);\n}\ninput[type=checkbox]::before,\ninput[type=checkbox]:not(.toggle)::after,\ninput[type=radio]::before,\ninput[type=radio]::after {\n    content: \"\";\n    display: block; /* width & height doesn't work on inline elements */\n    transition: inherit;\n    /* Let's prevent the pseudo-elements from taking up layout space so that\n       the ::before and ::after pseudo-elements can be in the same place. This\n       is also required for vertical-align: baseline to work like we want it to\n       on radio/checkboxes. If the pseudo-elements take up layout space, the\n       baseline of text inside them will be used instead. */\n    position: absolute;\n}\ninput[type=checkbox]:not(.toggle)::after,\ninput[type=radio]::after {\n    width: 10px;\n    height: 2px;\n    background-color: transparent;\n    border-radius: 2px;\n}\n\n/* ------- CHECKBOXES ------- */\n\ninput[type=checkbox]:not(.toggle) {\n    border-radius: 4px;\n}\ninput[type=checkbox]:not(.toggle):checked,\ninput[type=checkbox]:not(.toggle):indeterminate {\n    background-color: var(--novnc-blue);\n    background-image: var(--button-activation-overlay-light);\n    background-blend-mode: overlay;\n}\ninput[type=checkbox]:not(.toggle)::before {\n    width: 25%;\n    height: 55%;\n    border-style: solid;\n    border-color: transparent;\n    border-width: 0 2px 2px 0;\n    border-radius: 1px;\n    transform: translateY(-1px) rotate(35deg);\n}\ninput[type=checkbox]:not(.toggle):checked::before {\n    border-color: white;\n}\ninput[type=checkbox]:not(.toggle):indeterminate::after {\n    background-color: white;\n}\n\n/* ------- RADIO BUTTONS ------- */\n\ninput[type=radio] {\n    border-radius: 50%;\n    border: 1px solid transparent; /* To ensure a smooth transition */\n}\ninput[type=radio]:checked {\n    border: 4px solid var(--novnc-blue);\n    background-color: white;\n    /* button-activation-overlay should be removed from the radio\n       element to not interfere with button-activation-overlay-light\n       that is set on the ::before element. */\n    background-image: none;\n}\ninput[type=radio]::before {\n    width: inherit;\n    height: inherit;\n    border-radius: inherit;\n    /* We can achieve the highlight overlay effect on border colors by\n       setting button-activation-overlay-light on an element that stays\n       on top (z-axis) of the element with a border. */\n    background-image: var(--button-activation-overlay-light);\n    mix-blend-mode: overlay;\n    opacity: 0;\n}\ninput[type=radio]:checked::before {\n    opacity: 1;\n}\ninput[type=radio]:indeterminate::after {\n    background-color: black;\n}\n\n/* ------- TOGGLE SWITCHES ------- */\n\n/* These are meant to be used instead of checkboxes in some cases. If all of\n   the following critera are true you should use a toggle switch:\n\n    * The choice is a simple ON/OFF or ENABLE/DISABLE\n    * The choice doesn't give the feeling of \"I agree\" or \"I confirm\"\n    * There are not multiple related & grouped options\n */\n\ninput[type=checkbox].toggle {\n    display: inline-block;\n    --checkradio-height: 18px; /* Height value used in calc, see above */\n    width: 31px;\n    cursor: pointer;\n    user-select: none;\n    -webkit-user-select: none;\n    border-radius: 9px;\n}\ninput[type=checkbox].toggle:disabled {\n    cursor: default;\n}\ninput[type=checkbox].toggle:indeterminate {\n    background-color: var(--novnc-buttongrey);\n    background-image: var(--button-activation-overlay);\n}\ninput[type=checkbox].toggle:checked {\n    background-color: var(--novnc-blue);\n    background-image: var(--button-activation-overlay-light);\n    background-blend-mode: overlay;\n}\ninput[type=checkbox].toggle::before {\n    --circle-diameter: 10px;\n    --circle-offset: 4px;\n    width: var(--circle-diameter);\n    height: var(--circle-diameter);\n    top: var(--circle-offset);\n    left: var(--circle-offset);\n    background: white;\n    border-radius: 6px;\n}\ninput[type=checkbox].toggle:checked::before {\n    left: calc(100% - var(--circle-offset) - var(--circle-diameter));\n}\ninput[type=checkbox].toggle:indeterminate::before {\n    left: calc(50% - var(--circle-diameter) / 2);\n}\n\n/* ------- RANGE SLIDERS ------- */\n\ninput[type=range] {\n    border: unset;\n    border-radius: 8px;\n    height: 15px;\n    padding: 0;\n    background: transparent;\n    /* Needed to get properly rounded corners on -moz-range-progress\n       when the thumb is all the way to the right. Without overflow\n       hidden, the pointy edges of the progress track shows to the\n       right of the thumb. */\n    overflow: hidden;\n}\n@supports selector(::-webkit-slider-thumb) {\n    input[type=range] {\n        /* Needs a fixed width to match clip-path */\n        width: 125px;\n        /* overflow: hidden is not ideal for hiding the left part of the box\n           shadow of -webkit-slider-thumb since it doesn't match the smaller\n           border-radius of the progress track. The below clip-path has two\n           circular sides to make the ends of the track have correctly rounded\n           corners. The clip path shape looks something like this:\n\n                  +-------------------------------+\n              /---|                               |---\\\n             |                                         |\n              \\---|                               |---/\n                  +-------------------------------+\n\n           The larger middle part of the clip path is made to have room for the\n           thumb. By using margins on the track, we prevent the thumb from\n           touching the ends of the track.\n         */\n        clip-path: path(' \\\n         M 4.5 3 \\\n         L 4.5 0 \\\n         L 120.5 0 \\\n         L 120.5 3 \\\n         A 1 1 0 0 1 120.5 12 \\\n         L 120.5 15 \\\n         L 4.5 15 \\\n         L 4.5 12 \\\n         A 1 1 0 0 1 4.5 3 \\\n        ');\n    }\n}\ninput[type=range]:hover {\n    cursor: grab;\n}\ninput[type=range]:active {\n    cursor: grabbing;\n}\ninput[type=range]:disabled {\n    cursor: default;\n}\ninput[type=range]:focus-visible {\n    clip-path: none; /* Otherwise it hides the outline */\n}\n/* -webkit-slider.. & -moz-range.. cant be in selector lists:\n   https://bugs.chromium.org/p/chromium/issues/detail?id=1154623 */\ninput[type=range]::-webkit-slider-runnable-track {\n    background-color: var(--novnc-buttongrey);\n    height: 7px;\n    border-radius: 4px;\n    margin: 0 3px;\n}\ninput[type=range]::-moz-range-track {\n    background-color: var(--novnc-buttongrey);\n    height: 7px;\n    border-radius: 4px;\n}\ninput[type=range]::-moz-range-progress {\n    background-color: var(--novnc-blue);\n    height: 9px;\n    /* Needs rounded corners only on the left side. Otherwise the rounding of\n       the progress track starts before the thumb, when the thumb is close to\n       the left edge. */\n    border-radius: 5px 0 0 5px;\n}\ninput[type=range]::-webkit-slider-thumb {\n    appearance: none;\n    width: 15px;\n    height: 15px;\n    border-radius: 50%;\n    background-color: white;\n    background-image: var(--button-activation-overlay);\n    /* Disable Chrome's touch tap highlight to avoid conflicts with overlay */\n    -webkit-tap-highlight-color: transparent;\n    border: 3px solid var(--novnc-blue);\n    margin-top: -4px; /* (track height / 2) - (thumb height /2) */\n\n    /* Since there is no way to style the left part of the range track in\n       webkit, we add a large shadow (1000px wide) to the left of the thumb and\n       then crop it with a clip-path shaped like this:\n                              ___\n        +-------------------/     \\\n        |      progress     |Thumb|\n        +-------------------\\ ___ /\n\n        The large left part of the shadow is clipped by another clip-path on on\n        the main range input element. */\n    /* FIXME: We can remove the box shadow workaround when this is standardized:\n              https://github.com/w3c/csswg-drafts/issues/4410 */\n\n    box-shadow: calc(-100vw - 8px) 0 0 100vw var(--novnc-blue);\n    clip-path: path(' \\\n     M -1000 3 \\\n     L 3 3 \\\n     L 15 7.5 \\\n     A 1 1 0 0 1 0 7.5 \\\n     A 1 1 0 0 1 15 7.5 \\\n     L 3 12 \\\n     L -1000 12 Z \\\n    ');\n}\ninput[type=range]::-moz-range-thumb {\n    appearance: none;\n    width: 15px;\n    height: 15px;\n    border-radius: 50%;\n    box-sizing: border-box;\n    background-color: white;\n    background-image: var(--button-activation-overlay);\n    border: 3px solid var(--novnc-blue);\n    margin-top: -7px;\n}\n\n/* ------- FILE CHOOSERS ------- */\n\ninput[type=file] {\n    background-image: none;\n    border: none;\n}\ninput::file-selector-button {\n    margin-right: 6px;\n}\ninput[type=file]:focus-visible {\n    outline: none; /* We outline the button instead of the entire element */\n}\n\n/* ------- SELECT BUTTONS ------- */\n\nselect {\n    --select-arrow: url('data:image/svg+xml;utf8, \\\n        <svg width=\"11\" height=\"6\" version=\"1.1\" viewBox=\"0 0 11 6\" \\\n             xmlns=\"http://www.w3.org/2000/svg\"> \\\n            <path d=\"m10.5.5-5 5-5-5\" fill=\"none\" \\\n                  stroke=\"black\" stroke-width=\"1.5\" \\\n                  stroke-linecap=\"round\" stroke-linejoin=\"round\"/> \\\n        </svg>');\n\n    /* FIXME: A bug in Firefox, requires a workaround for the background:\n              https://bugzilla.mozilla.org/show_bug.cgi?id=1810958 */\n    /* The dropdown list will show the select element's background above and\n       below the options in Firefox. We want the entire dropdown to be white. */\n    background-color: white;\n    /* However, we don't want the select element to actually show a white\n       background, so let's place a gradient above it with the color we want. */\n    --grey-background: linear-gradient(var(--novnc-buttongrey) 100%,\n                                       transparent);\n    background-image:\n        var(--select-arrow),\n        var(--button-activation-overlay),\n        var(--grey-background);\n    background-position: calc(100% - var(--input-xpadding)), left top, left top;\n    background-repeat: no-repeat;\n    padding-right: calc(2*var(--input-xpadding) + 11px);\n    overflow: auto;\n}\n/* FIXME: :active isn't set when the <select> is opened in Firefox:\n          https://bugzilla.mozilla.org/show_bug.cgi?id=1805406 */\nselect:active {\n    /* Rotated arrow */\n    background-image: url('data:image/svg+xml;utf8, \\\n        <svg width=\"11\" height=\"6\" version=\"1.1\" viewBox=\"0 0 11 6\" \\\n             xmlns=\"http://www.w3.org/2000/svg\" transform=\"rotate(180)\"> \\\n            <path d=\"m10.5.5-5 5-5-5\" fill=\"none\" \\\n                  stroke=\"black\" stroke-width=\"1.5\" \\\n                  stroke-linecap=\"round\" stroke-linejoin=\"round\"/> \\\n        </svg>'),\n        var(--button-activation-overlay),\n        var(--grey-background);\n}\nselect:disabled {\n    background-image:\n        var(--select-arrow),\n        var(--grey-background);\n}\n/* Note that styling for <option> doesn't work in all browsers\n   since its often drawn directly by the OS. We are generally very\n   limited in what we can change here. */\noption {\n    /* Prevent Chrome from inheriting background-color from the <select> */\n    background-color: white;\n    color: black;\n    font-weight: normal;\n    background-image: var(--button-activation-overlay);\n}\noption:checked {\n    background-color: var(--novnc-lightgrey);\n}\n/* Change the look when the <select> isn't used as a dropdown. When \"size\"\n   or \"multiple\" are set, these elements behaves more like lists. */\nselect[size]:not([size=\"1\"]), select[multiple] {\n    background-color: white;\n    background-image: unset; /* Don't show the arrow and other gradients */\n    border: 1px solid var(--novnc-lightgrey);\n    padding: 0;\n    font-weight: normal; /* Without this, options get bold font in WebKit. */\n\n    /* As an exception to the \"list\"-look, multi-selects in Chrome on Android,\n       and Safari on iOS, are unfortunately designed to be shown as a single\n       line. We can mitigate this inconsistency by at least fixing the height\n       here. By setting a min-height that matches other input elements, it\n       doesn't look too much out of place:\n         (1px border * 2) + (6.5px padding * 2) + 24px line-height = 39px */\n    min-height: 39px;\n}\nselect[size]:not([size=\"1\"]):focus-visible,\nselect[multiple]:focus-visible {\n    /* Text input style focus-visible highlight */\n    outline-offset: -1px;\n}\nselect[size]:not([size=\"1\"]) option, select[multiple] option {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    padding: 4px var(--input-xpadding);\n}\n"
  },
  {
    "path": "app/ui.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2019 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n */\n\nimport * as Log from '../core/util/logging.js';\nimport _, { l10n } from './localization.js';\nimport { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari,\n         hasScrollbarGutter, dragThreshold, browserAsyncClipboardSupport }\n    from '../core/util/browser.js';\nimport { setCapture, getPointerEvent } from '../core/util/events.js';\nimport KeyTable from \"../core/input/keysym.js\";\nimport keysyms from \"../core/input/keysymdef.js\";\nimport Keyboard from \"../core/input/keyboard.js\";\nimport RFB from \"../core/rfb.js\";\nimport WakeLockManager from './wakelock.js';\nimport * as WebUtil from \"./webutil.js\";\n\nconst PAGE_TITLE = \"noVNC\";\n\nconst LINGUAS = [\"cs\", \"de\", \"el\", \"es\", \"fr\", \"hr\", \"hu\", \"it\", \"ja\", \"ko\", \"nl\", \"pl\", \"pt_BR\", \"ru\", \"sv\", \"tr\", \"uk\", \"zh_CN\", \"zh_TW\"];\n\nconst UI = {\n\n    customSettings: {},\n\n    connected: false,\n    desktopName: \"\",\n\n    statusTimeout: null,\n    hideKeyboardTimeout: null,\n    idleControlbarTimeout: null,\n    closeControlbarTimeout: null,\n\n    controlbarGrabbed: false,\n    controlbarDrag: false,\n    controlbarMouseDownClientY: 0,\n    controlbarMouseDownOffsetY: 0,\n\n    lastKeyboardinput: null,\n    defaultKeyboardinputLen: 100,\n\n    inhibitReconnect: true,\n    reconnectCallback: null,\n    reconnectPassword: null,\n\n    wakeLockManager: new WakeLockManager(),\n\n    async start(options={}) {\n        UI.customSettings = options.settings || {};\n        if (UI.customSettings.defaults === undefined) {\n            UI.customSettings.defaults = {};\n        }\n        if (UI.customSettings.mandatory === undefined) {\n            UI.customSettings.mandatory = {};\n        }\n\n        // Set up translations\n        try {\n            await l10n.setup(LINGUAS, \"app/locale/\");\n        } catch (err) {\n            Log.Error(\"Failed to load translations: \" + err);\n        }\n\n        // Initialize setting storage\n        await WebUtil.initSettings();\n\n        // Wait for the page to load\n        if (document.readyState !== \"interactive\" && document.readyState !== \"complete\") {\n            await new Promise((resolve, reject) => {\n                document.addEventListener('DOMContentLoaded', resolve);\n            });\n        }\n\n        UI.initSettings();\n\n        // Translate the DOM\n        l10n.translateDOM();\n\n        // We rely on modern APIs which might not be available in an\n        // insecure context\n        if (!window.isSecureContext) {\n            // FIXME: This gets hidden when connecting\n            UI.showStatus(_(\"Running without HTTPS is not recommended, crashes or other issues are likely.\"), 'error');\n        }\n\n        // Try to fetch version number\n        try {\n            let response = await fetch('./package.json');\n            if (!response.ok) {\n                throw Error(\"\" + response.status + \" \" + response.statusText);\n            }\n\n            let packageInfo = await response.json();\n            Array.from(document.getElementsByClassName('noVNC_version')).forEach(el => el.innerText = packageInfo.version);\n        } catch (err) {\n            Log.Error(\"Couldn't fetch package.json: \" + err);\n            Array.from(document.getElementsByClassName('noVNC_version_wrapper'))\n                .concat(Array.from(document.getElementsByClassName('noVNC_version_separator')))\n                .forEach(el => el.style.display = 'none');\n        }\n\n        // Adapt the interface for touch screen devices\n        if (isTouchDevice) {\n            // Remove the address bar\n            setTimeout(() => window.scrollTo(0, 1), 100);\n        }\n\n        // Restore control bar position\n        if (WebUtil.readSetting('controlbar_pos') === 'right') {\n            UI.toggleControlbarSide();\n        }\n\n        UI.initFullscreen();\n\n        // Setup event handlers\n        UI.addControlbarHandlers();\n        UI.addTouchSpecificHandlers();\n        UI.addExtraKeysHandlers();\n        UI.addMachineHandlers();\n        UI.addConnectionControlHandlers();\n        UI.addClipboardHandlers();\n        UI.addSettingsHandlers();\n        document.getElementById(\"noVNC_status\")\n            .addEventListener('click', UI.hideStatus);\n\n        // Bootstrap fallback input handler\n        UI.keyboardinputReset();\n\n        UI.openControlbar();\n\n        UI.updateVisualState('init');\n\n        document.documentElement.classList.remove(\"noVNC_loading\");\n\n        let autoconnect = UI.getSetting('autoconnect');\n        if (autoconnect === 'true' || autoconnect == '1') {\n            UI.connect();\n        } else {\n            // Show the connect panel on first load unless autoconnecting\n            UI.openConnectPanel();\n        }\n    },\n\n    initFullscreen() {\n        // Only show the button if fullscreen is properly supported\n        // * Safari doesn't support alphanumerical input while in fullscreen\n        if (!isSafari() &&\n            (document.documentElement.requestFullscreen ||\n             document.documentElement.mozRequestFullScreen ||\n             document.documentElement.webkitRequestFullscreen ||\n             document.body.msRequestFullscreen)) {\n            document.getElementById('noVNC_fullscreen_button')\n                .classList.remove(\"noVNC_hidden\");\n            UI.addFullscreenHandlers();\n        }\n    },\n\n    initSettings() {\n        // Logging selection dropdown\n        const llevels = ['error', 'warn', 'info', 'debug'];\n        for (let i = 0; i < llevels.length; i += 1) {\n            UI.addOption(document.getElementById('noVNC_setting_logging'), llevels[i], llevels[i]);\n        }\n\n        // Settings with immediate effects\n        UI.initSetting('logging', 'warn');\n        UI.updateLogging();\n\n        UI.setupSettingLabels();\n\n        /* Populate the controls if defaults are provided in the URL */\n        UI.initSetting('host', '');\n        UI.initSetting('port', 0);\n        UI.initSetting('encrypt', (window.location.protocol === \"https:\"));\n        UI.initSetting('password');\n        UI.initSetting('autoconnect', false);\n        UI.initSetting('view_clip', false);\n        UI.initSetting('resize', 'off');\n        UI.initSetting('quality', 6);\n        UI.initSetting('compression', 2);\n        UI.initSetting('shared', true);\n        UI.initSetting('bell', 'on');\n        UI.initSetting('view_only', false);\n        UI.initSetting('show_dot', false);\n        UI.initSetting('path', 'websockify');\n        UI.initSetting('repeaterID', '');\n        UI.initSetting('reconnect', false);\n        UI.initSetting('reconnect_delay', 5000);\n        UI.initSetting('keep_device_awake', false);\n    },\n    // Adds a link to the label elements on the corresponding input elements\n    setupSettingLabels() {\n        const labels = document.getElementsByTagName('LABEL');\n        for (let i = 0; i < labels.length; i++) {\n            const htmlFor = labels[i].htmlFor;\n            if (htmlFor != '') {\n                const elem = document.getElementById(htmlFor);\n                if (elem) elem.label = labels[i];\n            } else {\n                // If 'for' isn't set, use the first input element child\n                const children = labels[i].children;\n                for (let j = 0; j < children.length; j++) {\n                    if (children[j].form !== undefined) {\n                        children[j].label = labels[i];\n                        break;\n                    }\n                }\n            }\n        }\n    },\n\n/* ------^-------\n*     /INIT\n* ==============\n* EVENT HANDLERS\n* ------v------*/\n\n    addControlbarHandlers() {\n        document.getElementById(\"noVNC_control_bar\")\n            .addEventListener('mousemove', UI.activateControlbar);\n        document.getElementById(\"noVNC_control_bar\")\n            .addEventListener('mouseup', UI.activateControlbar);\n        document.getElementById(\"noVNC_control_bar\")\n            .addEventListener('mousedown', UI.activateControlbar);\n        document.getElementById(\"noVNC_control_bar\")\n            .addEventListener('keydown', UI.activateControlbar);\n\n        document.getElementById(\"noVNC_control_bar\")\n            .addEventListener('mousedown', UI.keepControlbar);\n        document.getElementById(\"noVNC_control_bar\")\n            .addEventListener('keydown', UI.keepControlbar);\n\n        document.getElementById(\"noVNC_view_drag_button\")\n            .addEventListener('click', UI.toggleViewDrag);\n\n        document.getElementById(\"noVNC_control_bar_handle\")\n            .addEventListener('mousedown', UI.controlbarHandleMouseDown);\n        document.getElementById(\"noVNC_control_bar_handle\")\n            .addEventListener('mouseup', UI.controlbarHandleMouseUp);\n        document.getElementById(\"noVNC_control_bar_handle\")\n            .addEventListener('mousemove', UI.dragControlbarHandle);\n        // resize events aren't available for elements\n        window.addEventListener('resize', UI.updateControlbarHandle);\n\n        const exps = document.getElementsByClassName(\"noVNC_expander\");\n        for (let i = 0;i < exps.length;i++) {\n            exps[i].addEventListener('click', UI.toggleExpander);\n        }\n    },\n\n    addTouchSpecificHandlers() {\n        document.getElementById(\"noVNC_keyboard_button\")\n            .addEventListener('click', UI.toggleVirtualKeyboard);\n\n        UI.touchKeyboard = new Keyboard(document.getElementById('noVNC_keyboardinput'));\n        UI.touchKeyboard.onkeyevent = UI.keyEvent;\n        UI.touchKeyboard.grab();\n        document.getElementById(\"noVNC_keyboardinput\")\n            .addEventListener('input', UI.keyInput);\n        document.getElementById(\"noVNC_keyboardinput\")\n            .addEventListener('focus', UI.onfocusVirtualKeyboard);\n        document.getElementById(\"noVNC_keyboardinput\")\n            .addEventListener('blur', UI.onblurVirtualKeyboard);\n        document.getElementById(\"noVNC_keyboardinput\")\n            .addEventListener('submit', () => false);\n\n        document.documentElement\n            .addEventListener('mousedown', UI.keepVirtualKeyboard, true);\n\n        document.getElementById(\"noVNC_control_bar\")\n            .addEventListener('touchstart', UI.activateControlbar);\n        document.getElementById(\"noVNC_control_bar\")\n            .addEventListener('touchmove', UI.activateControlbar);\n        document.getElementById(\"noVNC_control_bar\")\n            .addEventListener('touchend', UI.activateControlbar);\n        document.getElementById(\"noVNC_control_bar\")\n            .addEventListener('input', UI.activateControlbar);\n\n        document.getElementById(\"noVNC_control_bar\")\n            .addEventListener('touchstart', UI.keepControlbar);\n        document.getElementById(\"noVNC_control_bar\")\n            .addEventListener('input', UI.keepControlbar);\n\n        document.getElementById(\"noVNC_control_bar_handle\")\n            .addEventListener('touchstart', UI.controlbarHandleMouseDown);\n        document.getElementById(\"noVNC_control_bar_handle\")\n            .addEventListener('touchend', UI.controlbarHandleMouseUp);\n        document.getElementById(\"noVNC_control_bar_handle\")\n            .addEventListener('touchmove', UI.dragControlbarHandle);\n    },\n\n    addExtraKeysHandlers() {\n        document.getElementById(\"noVNC_toggle_extra_keys_button\")\n            .addEventListener('click', UI.toggleExtraKeys);\n        document.getElementById(\"noVNC_toggle_ctrl_button\")\n            .addEventListener('click', UI.toggleCtrl);\n        document.getElementById(\"noVNC_toggle_windows_button\")\n            .addEventListener('click', UI.toggleWindows);\n        document.getElementById(\"noVNC_toggle_alt_button\")\n            .addEventListener('click', UI.toggleAlt);\n        document.getElementById(\"noVNC_send_tab_button\")\n            .addEventListener('click', UI.sendTab);\n        document.getElementById(\"noVNC_send_esc_button\")\n            .addEventListener('click', UI.sendEsc);\n        document.getElementById(\"noVNC_send_ctrl_alt_del_button\")\n            .addEventListener('click', UI.sendCtrlAltDel);\n    },\n\n    addMachineHandlers() {\n        document.getElementById(\"noVNC_shutdown_button\")\n            .addEventListener('click', () => UI.rfb.machineShutdown());\n        document.getElementById(\"noVNC_reboot_button\")\n            .addEventListener('click', () => UI.rfb.machineReboot());\n        document.getElementById(\"noVNC_reset_button\")\n            .addEventListener('click', () => UI.rfb.machineReset());\n        document.getElementById(\"noVNC_power_button\")\n            .addEventListener('click', UI.togglePowerPanel);\n    },\n\n    addConnectionControlHandlers() {\n        document.getElementById(\"noVNC_disconnect_button\")\n            .addEventListener('click', UI.disconnect);\n        document.getElementById(\"noVNC_connect_button\")\n            .addEventListener('click', UI.connect);\n        document.getElementById(\"noVNC_cancel_reconnect_button\")\n            .addEventListener('click', UI.cancelReconnect);\n\n        document.getElementById(\"noVNC_approve_server_button\")\n            .addEventListener('click', UI.approveServer);\n        document.getElementById(\"noVNC_reject_server_button\")\n            .addEventListener('click', UI.rejectServer);\n        document.getElementById(\"noVNC_credentials_button\")\n            .addEventListener('click', UI.setCredentials);\n    },\n\n    addClipboardHandlers() {\n        document.getElementById(\"noVNC_clipboard_button\")\n            .addEventListener('click', UI.toggleClipboardPanel);\n        document.getElementById(\"noVNC_clipboard_text\")\n            .addEventListener('change', UI.clipboardSend);\n    },\n\n    // Add a call to save settings when the element changes,\n    // unless the optional parameter changeFunc is used instead.\n    addSettingChangeHandler(name, changeFunc) {\n        const settingElem = document.getElementById(\"noVNC_setting_\" + name);\n        if (changeFunc === undefined) {\n            changeFunc = () => UI.saveSetting(name);\n        }\n        settingElem.addEventListener('change', changeFunc);\n    },\n\n    addSettingsHandlers() {\n        document.getElementById(\"noVNC_settings_button\")\n            .addEventListener('click', UI.toggleSettingsPanel);\n\n        UI.addSettingChangeHandler('encrypt');\n        UI.addSettingChangeHandler('resize');\n        UI.addSettingChangeHandler('resize', UI.applyResizeMode);\n        UI.addSettingChangeHandler('resize', UI.updateViewClip);\n        UI.addSettingChangeHandler('quality');\n        UI.addSettingChangeHandler('quality', UI.updateQuality);\n        UI.addSettingChangeHandler('compression');\n        UI.addSettingChangeHandler('compression', UI.updateCompression);\n        UI.addSettingChangeHandler('view_clip');\n        UI.addSettingChangeHandler('view_clip', UI.updateViewClip);\n        UI.addSettingChangeHandler('shared');\n        UI.addSettingChangeHandler('view_only');\n        UI.addSettingChangeHandler('view_only', UI.updateViewOnly);\n        UI.addSettingChangeHandler('show_dot');\n        UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor);\n        UI.addSettingChangeHandler('keep_device_awake');\n        UI.addSettingChangeHandler('keep_device_awake', UI.updateRequestWakelock);\n        UI.addSettingChangeHandler('host');\n        UI.addSettingChangeHandler('port');\n        UI.addSettingChangeHandler('path');\n        UI.addSettingChangeHandler('repeaterID');\n        UI.addSettingChangeHandler('logging');\n        UI.addSettingChangeHandler('logging', UI.updateLogging);\n        UI.addSettingChangeHandler('reconnect');\n        UI.addSettingChangeHandler('reconnect_delay');\n    },\n\n    addFullscreenHandlers() {\n        document.getElementById(\"noVNC_fullscreen_button\")\n            .addEventListener('click', UI.toggleFullscreen);\n\n        window.addEventListener('fullscreenchange', UI.updateFullscreenButton);\n        window.addEventListener('mozfullscreenchange', UI.updateFullscreenButton);\n        window.addEventListener('webkitfullscreenchange', UI.updateFullscreenButton);\n        window.addEventListener('msfullscreenchange', UI.updateFullscreenButton);\n    },\n\n/* ------^-------\n * /EVENT HANDLERS\n * ==============\n *     VISUAL\n * ------v------*/\n\n    // Disable/enable controls depending on connection state\n    updateVisualState(state) {\n\n        document.documentElement.classList.remove(\"noVNC_connecting\");\n        document.documentElement.classList.remove(\"noVNC_connected\");\n        document.documentElement.classList.remove(\"noVNC_disconnecting\");\n        document.documentElement.classList.remove(\"noVNC_reconnecting\");\n\n        const transitionElem = document.getElementById(\"noVNC_transition_text\");\n        switch (state) {\n            case 'init':\n                break;\n            case 'connecting':\n                transitionElem.textContent = _(\"Connecting...\");\n                document.documentElement.classList.add(\"noVNC_connecting\");\n                break;\n            case 'connected':\n                document.documentElement.classList.add(\"noVNC_connected\");\n                break;\n            case 'disconnecting':\n                transitionElem.textContent = _(\"Disconnecting...\");\n                document.documentElement.classList.add(\"noVNC_disconnecting\");\n                break;\n            case 'disconnected':\n                break;\n            case 'reconnecting':\n                transitionElem.textContent = _(\"Reconnecting...\");\n                document.documentElement.classList.add(\"noVNC_reconnecting\");\n                break;\n            default:\n                Log.Error(\"Invalid visual state: \" + state);\n                UI.showStatus(_(\"Internal error\"), 'error');\n                return;\n        }\n\n        if (UI.connected) {\n            UI.updateViewClip();\n\n            UI.disableSetting('encrypt');\n            UI.disableSetting('shared');\n            UI.disableSetting('host');\n            UI.disableSetting('port');\n            UI.disableSetting('path');\n            UI.disableSetting('repeaterID');\n\n            // Hide the controlbar after 2 seconds\n            UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000);\n        } else {\n            UI.enableSetting('encrypt');\n            UI.enableSetting('shared');\n            UI.enableSetting('host');\n            UI.enableSetting('port');\n            UI.enableSetting('path');\n            UI.enableSetting('repeaterID');\n            UI.updatePowerButton();\n            UI.keepControlbar();\n        }\n\n        // State change closes dialogs as they may not be relevant\n        // anymore\n        UI.closeAllPanels();\n        document.getElementById('noVNC_verify_server_dlg')\n            .classList.remove('noVNC_open');\n        document.getElementById('noVNC_credentials_dlg')\n            .classList.remove('noVNC_open');\n    },\n\n    showStatus(text, statusType, time) {\n        const statusElem = document.getElementById('noVNC_status');\n\n        if (typeof statusType === 'undefined') {\n            statusType = 'normal';\n        }\n\n        // Don't overwrite more severe visible statuses and never\n        // errors. Only shows the first error.\n        if (statusElem.classList.contains(\"noVNC_open\")) {\n            if (statusElem.classList.contains(\"noVNC_status_error\")) {\n                return;\n            }\n            if (statusElem.classList.contains(\"noVNC_status_warn\") &&\n                statusType === 'normal') {\n                return;\n            }\n        }\n\n        clearTimeout(UI.statusTimeout);\n\n        switch (statusType) {\n            case 'error':\n                statusElem.classList.remove(\"noVNC_status_warn\");\n                statusElem.classList.remove(\"noVNC_status_normal\");\n                statusElem.classList.add(\"noVNC_status_error\");\n                break;\n            case 'warning':\n            case 'warn':\n                statusElem.classList.remove(\"noVNC_status_error\");\n                statusElem.classList.remove(\"noVNC_status_normal\");\n                statusElem.classList.add(\"noVNC_status_warn\");\n                break;\n            case 'normal':\n            case 'info':\n            default:\n                statusElem.classList.remove(\"noVNC_status_error\");\n                statusElem.classList.remove(\"noVNC_status_warn\");\n                statusElem.classList.add(\"noVNC_status_normal\");\n                break;\n        }\n\n        statusElem.textContent = text;\n        statusElem.classList.add(\"noVNC_open\");\n\n        // If no time was specified, show the status for 1.5 seconds\n        if (typeof time === 'undefined') {\n            time = 1500;\n        }\n\n        // Error messages do not timeout\n        if (statusType !== 'error') {\n            UI.statusTimeout = window.setTimeout(UI.hideStatus, time);\n        }\n    },\n\n    hideStatus() {\n        clearTimeout(UI.statusTimeout);\n        document.getElementById('noVNC_status').classList.remove(\"noVNC_open\");\n    },\n\n    activateControlbar(event) {\n        clearTimeout(UI.idleControlbarTimeout);\n        // We manipulate the anchor instead of the actual control\n        // bar in order to avoid creating new a stacking group\n        document.getElementById('noVNC_control_bar_anchor')\n            .classList.remove(\"noVNC_idle\");\n        UI.idleControlbarTimeout = window.setTimeout(UI.idleControlbar, 2000);\n    },\n\n    idleControlbar() {\n        // Don't fade if a child of the control bar has focus\n        if (document.getElementById('noVNC_control_bar')\n            .contains(document.activeElement) && document.hasFocus()) {\n            UI.activateControlbar();\n            return;\n        }\n\n        document.getElementById('noVNC_control_bar_anchor')\n            .classList.add(\"noVNC_idle\");\n    },\n\n    keepControlbar() {\n        clearTimeout(UI.closeControlbarTimeout);\n    },\n\n    openControlbar() {\n        document.getElementById('noVNC_control_bar')\n            .classList.add(\"noVNC_open\");\n    },\n\n    closeControlbar() {\n        UI.closeAllPanels();\n        document.getElementById('noVNC_control_bar')\n            .classList.remove(\"noVNC_open\");\n        UI.rfb.focus();\n    },\n\n    toggleControlbar() {\n        if (document.getElementById('noVNC_control_bar')\n            .classList.contains(\"noVNC_open\")) {\n            UI.closeControlbar();\n        } else {\n            UI.openControlbar();\n        }\n    },\n\n    toggleControlbarSide() {\n        // Temporarily disable animation, if bar is displayed, to avoid weird\n        // movement. The transitionend-event will not fire when display=none.\n        const bar = document.getElementById('noVNC_control_bar');\n        const barDisplayStyle = window.getComputedStyle(bar).display;\n        if (barDisplayStyle !== 'none') {\n            bar.style.transitionDuration = '0s';\n            bar.addEventListener('transitionend', () => bar.style.transitionDuration = '');\n        }\n\n        const anchor = document.getElementById('noVNC_control_bar_anchor');\n        if (anchor.classList.contains(\"noVNC_right\")) {\n            WebUtil.writeSetting('controlbar_pos', 'left');\n            anchor.classList.remove(\"noVNC_right\");\n        } else {\n            WebUtil.writeSetting('controlbar_pos', 'right');\n            anchor.classList.add(\"noVNC_right\");\n        }\n\n        // Consider this a movement of the handle\n        UI.controlbarDrag = true;\n\n        // The user has \"followed\" hint, let's hide it until the next drag\n        UI.showControlbarHint(false, false);\n    },\n\n    showControlbarHint(show, animate=true) {\n        const hint = document.getElementById('noVNC_control_bar_hint');\n\n        if (animate) {\n            hint.classList.remove(\"noVNC_notransition\");\n        } else {\n            hint.classList.add(\"noVNC_notransition\");\n        }\n\n        if (show) {\n            hint.classList.add(\"noVNC_active\");\n        } else {\n            hint.classList.remove(\"noVNC_active\");\n        }\n    },\n\n    dragControlbarHandle(e) {\n        if (!UI.controlbarGrabbed) return;\n\n        const ptr = getPointerEvent(e);\n\n        const anchor = document.getElementById('noVNC_control_bar_anchor');\n        if (ptr.clientX < (window.innerWidth * 0.1)) {\n            if (anchor.classList.contains(\"noVNC_right\")) {\n                UI.toggleControlbarSide();\n            }\n        } else if (ptr.clientX > (window.innerWidth * 0.9)) {\n            if (!anchor.classList.contains(\"noVNC_right\")) {\n                UI.toggleControlbarSide();\n            }\n        }\n\n        if (!UI.controlbarDrag) {\n            const dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY);\n\n            if (dragDistance < dragThreshold) return;\n\n            UI.controlbarDrag = true;\n        }\n\n        const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;\n\n        UI.moveControlbarHandle(eventY);\n\n        e.preventDefault();\n        e.stopPropagation();\n        UI.keepControlbar();\n        UI.activateControlbar();\n    },\n\n    // Move the handle but don't allow any position outside the bounds\n    moveControlbarHandle(viewportRelativeY) {\n        const handle = document.getElementById(\"noVNC_control_bar_handle\");\n        const handleHeight = handle.getBoundingClientRect().height;\n        const controlbarBounds = document.getElementById(\"noVNC_control_bar\")\n            .getBoundingClientRect();\n        const margin = 10;\n\n        // These heights need to be non-zero for the below logic to work\n        if (handleHeight === 0 || controlbarBounds.height === 0) {\n            return;\n        }\n\n        let newY = viewportRelativeY;\n\n        // Check if the coordinates are outside the control bar\n        if (newY < controlbarBounds.top + margin) {\n            // Force coordinates to be below the top of the control bar\n            newY = controlbarBounds.top + margin;\n\n        } else if (newY > controlbarBounds.top +\n                   controlbarBounds.height - handleHeight - margin) {\n            // Force coordinates to be above the bottom of the control bar\n            newY = controlbarBounds.top +\n                controlbarBounds.height - handleHeight - margin;\n        }\n\n        // Corner case: control bar too small for stable position\n        if (controlbarBounds.height < (handleHeight + margin * 2)) {\n            newY = controlbarBounds.top +\n                (controlbarBounds.height - handleHeight) / 2;\n        }\n\n        // The transform needs coordinates that are relative to the parent\n        const parentRelativeY = newY - controlbarBounds.top;\n        handle.style.transform = \"translateY(\" + parentRelativeY + \"px)\";\n    },\n\n    updateControlbarHandle() {\n        // Since the control bar is fixed on the viewport and not the page,\n        // the move function expects coordinates relative the the viewport.\n        const handle = document.getElementById(\"noVNC_control_bar_handle\");\n        const handleBounds = handle.getBoundingClientRect();\n        UI.moveControlbarHandle(handleBounds.top);\n    },\n\n    controlbarHandleMouseUp(e) {\n        if ((e.type == \"mouseup\") && (e.button != 0)) return;\n\n        // mouseup and mousedown on the same place toggles the controlbar\n        if (UI.controlbarGrabbed && !UI.controlbarDrag) {\n            UI.toggleControlbar();\n            e.preventDefault();\n            e.stopPropagation();\n            UI.keepControlbar();\n            UI.activateControlbar();\n        }\n        UI.controlbarGrabbed = false;\n        UI.showControlbarHint(false);\n    },\n\n    controlbarHandleMouseDown(e) {\n        if ((e.type == \"mousedown\") && (e.button != 0)) return;\n\n        const ptr = getPointerEvent(e);\n\n        const handle = document.getElementById(\"noVNC_control_bar_handle\");\n        const bounds = handle.getBoundingClientRect();\n\n        // Touch events have implicit capture\n        if (e.type === \"mousedown\") {\n            setCapture(handle);\n        }\n\n        UI.controlbarGrabbed = true;\n        UI.controlbarDrag = false;\n\n        UI.showControlbarHint(true);\n\n        UI.controlbarMouseDownClientY = ptr.clientY;\n        UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top;\n        e.preventDefault();\n        e.stopPropagation();\n        UI.keepControlbar();\n        UI.activateControlbar();\n    },\n\n    toggleExpander(e) {\n        if (this.classList.contains(\"noVNC_open\")) {\n            this.classList.remove(\"noVNC_open\");\n        } else {\n            this.classList.add(\"noVNC_open\");\n        }\n    },\n\n/* ------^-------\n *    /VISUAL\n * ==============\n *    SETTINGS\n * ------v------*/\n\n    // Initial page load read/initialization of settings\n    initSetting(name, defVal) {\n        // Has the user overridden the default value?\n        if (name in UI.customSettings.defaults) {\n            defVal = UI.customSettings.defaults[name];\n        }\n        // Check Query string followed by cookie\n        let val = WebUtil.getConfigVar(name);\n        if (val === null) {\n            val = WebUtil.readSetting(name, defVal);\n        }\n        WebUtil.setSetting(name, val);\n        UI.updateSetting(name);\n        // Has the user forced a value?\n        if (name in UI.customSettings.mandatory) {\n            val = UI.customSettings.mandatory[name];\n            UI.forceSetting(name, val);\n        }\n        return val;\n    },\n\n    // Set the new value, update and disable form control setting\n    forceSetting(name, val) {\n        WebUtil.setSetting(name, val);\n        UI.updateSetting(name);\n        UI.disableSetting(name);\n    },\n\n    // Update cookie and form control setting. If value is not set, then\n    // updates from control to current cookie setting.\n    updateSetting(name) {\n\n        // Update the settings control\n        let value = UI.getSetting(name);\n\n        const ctrl = document.getElementById('noVNC_setting_' + name);\n        if (ctrl === null) {\n            return;\n        }\n\n        if (ctrl.type === 'checkbox') {\n            ctrl.checked = value;\n        } else if (typeof ctrl.options !== 'undefined') {\n            for (let i = 0; i < ctrl.options.length; i += 1) {\n                if (ctrl.options[i].value === value) {\n                    ctrl.selectedIndex = i;\n                    break;\n                }\n            }\n        } else {\n            ctrl.value = value;\n        }\n    },\n\n    // Save control setting to cookie\n    saveSetting(name) {\n        const ctrl = document.getElementById('noVNC_setting_' + name);\n        let val;\n        if (ctrl.type === 'checkbox') {\n            val = ctrl.checked;\n        } else if (typeof ctrl.options !== 'undefined') {\n            val = ctrl.options[ctrl.selectedIndex].value;\n        } else {\n            val = ctrl.value;\n        }\n        WebUtil.writeSetting(name, val);\n        //Log.Debug(\"Setting saved '\" + name + \"=\" + val + \"'\");\n        return val;\n    },\n\n    // Read form control compatible setting from cookie\n    getSetting(name) {\n        const ctrl = document.getElementById('noVNC_setting_' + name);\n        let val = WebUtil.readSetting(name);\n        if (typeof val !== 'undefined' && val !== null &&\n            ctrl !== null && ctrl.type === 'checkbox') {\n            if (val.toString().toLowerCase() in {'0': 1, 'no': 1, 'false': 1}) {\n                val = false;\n            } else {\n                val = true;\n            }\n        }\n        return val;\n    },\n\n    // These helpers compensate for the lack of parent-selectors and\n    // previous-sibling-selectors in CSS which are needed when we want to\n    // disable the labels that belong to disabled input elements.\n    disableSetting(name) {\n        const ctrl = document.getElementById('noVNC_setting_' + name);\n        if (ctrl !== null) {\n            ctrl.disabled = true;\n            if (ctrl.label !== undefined) {\n                ctrl.label.classList.add('noVNC_disabled');\n            }\n        }\n    },\n\n    enableSetting(name) {\n        const ctrl = document.getElementById('noVNC_setting_' + name);\n        if (ctrl !== null) {\n            ctrl.disabled = false;\n            if (ctrl.label !== undefined) {\n                ctrl.label.classList.remove('noVNC_disabled');\n            }\n        }\n    },\n\n/* ------^-------\n *   /SETTINGS\n * ==============\n *    PANELS\n * ------v------*/\n\n    closeAllPanels() {\n        UI.closeSettingsPanel();\n        UI.closePowerPanel();\n        UI.closeClipboardPanel();\n        UI.closeExtraKeys();\n    },\n\n/* ------^-------\n *   /PANELS\n * ==============\n * SETTINGS (panel)\n * ------v------*/\n\n    openSettingsPanel() {\n        UI.closeAllPanels();\n        UI.openControlbar();\n\n        // Refresh UI elements from saved cookies\n        UI.updateSetting('encrypt');\n        UI.updateSetting('view_clip');\n        UI.updateSetting('resize');\n        UI.updateSetting('quality');\n        UI.updateSetting('compression');\n        UI.updateSetting('shared');\n        UI.updateSetting('view_only');\n        UI.updateSetting('path');\n        UI.updateSetting('repeaterID');\n        UI.updateSetting('logging');\n        UI.updateSetting('reconnect');\n        UI.updateSetting('reconnect_delay');\n\n        document.getElementById('noVNC_settings')\n            .classList.add(\"noVNC_open\");\n        document.getElementById('noVNC_settings_button')\n            .classList.add(\"noVNC_selected\");\n    },\n\n    closeSettingsPanel() {\n        document.getElementById('noVNC_settings')\n            .classList.remove(\"noVNC_open\");\n        document.getElementById('noVNC_settings_button')\n            .classList.remove(\"noVNC_selected\");\n    },\n\n    toggleSettingsPanel() {\n        if (document.getElementById('noVNC_settings')\n            .classList.contains(\"noVNC_open\")) {\n            UI.closeSettingsPanel();\n        } else {\n            UI.openSettingsPanel();\n        }\n    },\n\n/* ------^-------\n *   /SETTINGS\n * ==============\n *     POWER\n * ------v------*/\n\n    openPowerPanel() {\n        UI.closeAllPanels();\n        UI.openControlbar();\n\n        document.getElementById('noVNC_power')\n            .classList.add(\"noVNC_open\");\n        document.getElementById('noVNC_power_button')\n            .classList.add(\"noVNC_selected\");\n    },\n\n    closePowerPanel() {\n        document.getElementById('noVNC_power')\n            .classList.remove(\"noVNC_open\");\n        document.getElementById('noVNC_power_button')\n            .classList.remove(\"noVNC_selected\");\n    },\n\n    togglePowerPanel() {\n        if (document.getElementById('noVNC_power')\n            .classList.contains(\"noVNC_open\")) {\n            UI.closePowerPanel();\n        } else {\n            UI.openPowerPanel();\n        }\n    },\n\n    // Disable/enable power button\n    updatePowerButton() {\n        if (UI.connected &&\n            UI.rfb.capabilities.power &&\n            !UI.rfb.viewOnly) {\n            document.getElementById('noVNC_power_button')\n                .classList.remove(\"noVNC_hidden\");\n        } else {\n            document.getElementById('noVNC_power_button')\n                .classList.add(\"noVNC_hidden\");\n            // Close power panel if open\n            UI.closePowerPanel();\n        }\n    },\n\n/* ------^-------\n *    /POWER\n * ==============\n *   CLIPBOARD\n * ------v------*/\n\n    openClipboardPanel() {\n        UI.closeAllPanels();\n        UI.openControlbar();\n\n        document.getElementById('noVNC_clipboard')\n            .classList.add(\"noVNC_open\");\n        document.getElementById('noVNC_clipboard_button')\n            .classList.add(\"noVNC_selected\");\n    },\n\n    closeClipboardPanel() {\n        document.getElementById('noVNC_clipboard')\n            .classList.remove(\"noVNC_open\");\n        document.getElementById('noVNC_clipboard_button')\n            .classList.remove(\"noVNC_selected\");\n    },\n\n    toggleClipboardPanel() {\n        if (document.getElementById('noVNC_clipboard')\n            .classList.contains(\"noVNC_open\")) {\n            UI.closeClipboardPanel();\n        } else {\n            UI.openClipboardPanel();\n        }\n    },\n\n    clipboardReceive(e) {\n        Log.Debug(\">> UI.clipboardReceive: \" + e.detail.text.substr(0, 40) + \"...\");\n        document.getElementById('noVNC_clipboard_text').value = e.detail.text;\n        Log.Debug(\"<< UI.clipboardReceive\");\n    },\n\n    clipboardSend() {\n        const text = document.getElementById('noVNC_clipboard_text').value;\n        Log.Debug(\">> UI.clipboardSend: \" + text.substr(0, 40) + \"...\");\n        UI.rfb.clipboardPasteFrom(text);\n        Log.Debug(\"<< UI.clipboardSend\");\n    },\n\n/* ------^-------\n *  /CLIPBOARD\n * ==============\n *  CONNECTION\n * ------v------*/\n\n    openConnectPanel() {\n        document.getElementById('noVNC_connect_dlg')\n            .classList.add(\"noVNC_open\");\n    },\n\n    closeConnectPanel() {\n        document.getElementById('noVNC_connect_dlg')\n            .classList.remove(\"noVNC_open\");\n    },\n\n    connect(event, password) {\n\n        // Ignore when rfb already exists\n        if (typeof UI.rfb !== 'undefined') {\n            return;\n        }\n\n        const host = UI.getSetting('host');\n        const port = UI.getSetting('port');\n        const path = UI.getSetting('path');\n\n        if (typeof password === 'undefined') {\n            password = UI.getSetting('password');\n            UI.reconnectPassword = password;\n        }\n\n        if (password === null) {\n            password = undefined;\n        }\n\n        UI.hideStatus();\n\n        UI.closeConnectPanel();\n\n        UI.updateVisualState('connecting');\n\n        let url;\n\n        if (host) {\n            url = new URL(\"https://\" + host);\n\n            url.protocol = UI.getSetting('encrypt') ? 'wss:' : 'ws:';\n            if (port) {\n                url.port = port;\n            }\n\n            // \"./\" is needed to force URL() to interpret the path-variable as\n            // a path and not as an URL. This is relevant if for example path\n            // starts with more than one \"/\", in which case it would be\n            // interpreted as a host name instead.\n            url = new URL(\"./\" + path, url);\n        } else {\n            // Current (May 2024) browsers support relative WebSocket\n            // URLs natively, but we need to support older browsers for\n            // some time.\n            url = new URL(path, location.href);\n            url.protocol = (window.location.protocol === \"https:\") ? 'wss:' : 'ws:';\n        }\n\n        if (UI.getSetting('keep_device_awake')) {\n            UI.wakeLockManager.acquire();\n        }\n\n        try {\n            UI.rfb = new RFB(document.getElementById('noVNC_container'),\n                             url.href,\n                             { shared: UI.getSetting('shared'),\n                               repeaterID: UI.getSetting('repeaterID'),\n                               credentials: { password: password } });\n        } catch (exc) {\n            Log.Error(\"Failed to connect to server: \" + exc);\n            UI.updateVisualState('disconnected');\n            UI.showStatus(_(\"Failed to connect to server: \") + exc, 'error');\n            return;\n        }\n\n        UI.rfb.addEventListener(\"connect\", UI.connectFinished);\n        UI.rfb.addEventListener(\"disconnect\", UI.disconnectFinished);\n        UI.rfb.addEventListener(\"serververification\", UI.serverVerify);\n        UI.rfb.addEventListener(\"credentialsrequired\", UI.credentials);\n        UI.rfb.addEventListener(\"securityfailure\", UI.securityFailed);\n        UI.rfb.addEventListener(\"clippingviewport\", UI.updateViewDrag);\n        UI.rfb.addEventListener(\"capabilities\", UI.updatePowerButton);\n        UI.rfb.addEventListener(\"clipboard\", UI.clipboardReceive);\n        UI.rfb.addEventListener(\"bell\", UI.bell);\n        UI.rfb.addEventListener(\"desktopname\", UI.updateDesktopName);\n        UI.rfb.clipViewport = UI.getSetting('view_clip');\n        UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';\n        UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';\n        UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));\n        UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));\n        UI.rfb.showDotCursor = UI.getSetting('show_dot');\n\n        UI.updateViewOnly(); // requires UI.rfb\n        UI.updateClipboard();\n    },\n\n    disconnect() {\n        UI.rfb.disconnect();\n\n        UI.connected = false;\n\n        // Disable automatic reconnecting\n        UI.inhibitReconnect = true;\n\n        UI.updateVisualState('disconnecting');\n\n        // Don't display the connection settings until we're actually disconnected\n    },\n\n    reconnect() {\n        UI.reconnectCallback = null;\n\n        // if reconnect has been disabled in the meantime, do nothing.\n        if (UI.inhibitReconnect) {\n            return;\n        }\n\n        UI.connect(null, UI.reconnectPassword);\n    },\n\n    cancelReconnect() {\n        if (UI.reconnectCallback !== null) {\n            clearTimeout(UI.reconnectCallback);\n            UI.reconnectCallback = null;\n        }\n\n        UI.updateVisualState('disconnected');\n\n        UI.openControlbar();\n        UI.openConnectPanel();\n    },\n\n    connectFinished(e) {\n        UI.connected = true;\n        UI.inhibitReconnect = false;\n\n        let msg;\n        if (UI.getSetting('encrypt')) {\n            msg = _(\"Connected (encrypted) to \") + UI.desktopName;\n        } else {\n            msg = _(\"Connected (unencrypted) to \") + UI.desktopName;\n        }\n        UI.showStatus(msg);\n        UI.updateVisualState('connected');\n\n        UI.updateBeforeUnload();\n\n        // Do this last because it can only be used on rendered elements\n        UI.rfb.focus();\n    },\n\n    disconnectFinished(e) {\n        const wasConnected = UI.connected;\n\n        // This variable is ideally set when disconnection starts, but\n        // when the disconnection isn't clean or if it is initiated by\n        // the server, we need to do it here as well since\n        // UI.disconnect() won't be used in those cases.\n        UI.connected = false;\n\n        UI.rfb = undefined;\n        UI.wakeLockManager.release();\n\n        if (!e.detail.clean) {\n            UI.updateVisualState('disconnected');\n            if (wasConnected) {\n                UI.showStatus(_(\"Something went wrong, connection is closed\"),\n                              'error');\n            } else {\n                UI.showStatus(_(\"Failed to connect to server\"), 'error');\n            }\n        }\n        // If reconnecting is allowed process it now\n        if (UI.getSetting('reconnect', false) === true && !UI.inhibitReconnect) {\n            UI.updateVisualState('reconnecting');\n\n            const delay = parseInt(UI.getSetting('reconnect_delay'));\n            UI.reconnectCallback = setTimeout(UI.reconnect, delay);\n            return;\n        } else {\n            UI.updateVisualState('disconnected');\n            UI.showStatus(_(\"Disconnected\"), 'normal');\n        }\n\n        UI.updateBeforeUnload();\n\n        document.title = PAGE_TITLE;\n\n        UI.openControlbar();\n        UI.openConnectPanel();\n    },\n\n    securityFailed(e) {\n        let msg;\n        // On security failures we might get a string with a reason\n        // directly from the server. Note that we can't control if\n        // this string is translated or not.\n        if ('reason' in e.detail) {\n            msg = _(\"New connection has been rejected with reason: \") +\n                e.detail.reason;\n        } else {\n            msg = _(\"New connection has been rejected\");\n        }\n        UI.showStatus(msg, 'error');\n    },\n\n    handleBeforeUnload(e) {\n        // Trigger a \"Leave site?\" warning prompt before closing the\n        // page. Modern browsers (Oct 2025) accept either (or both)\n        // preventDefault() or a nonempty returnValue, though the latter is\n        // considered legacy. The custom string is ignored by modern browsers,\n        // which display a native message, but older browsers will show it.\n        e.preventDefault();\n        e.returnValue = _(\"Are you sure you want to disconnect the session?\");\n    },\n\n    updateBeforeUnload() {\n        // Remove first to avoid adding duplicates\n        window.removeEventListener(\"beforeunload\", UI.handleBeforeUnload);\n        if (!UI.rfb?.viewOnly && UI.connected) {\n            window.addEventListener(\"beforeunload\", UI.handleBeforeUnload);\n        }\n    },\n\n/* ------^-------\n *  /CONNECTION\n * ==============\n * SERVER VERIFY\n * ------v------*/\n\n    async serverVerify(e) {\n        const type = e.detail.type;\n        if (type === 'RSA') {\n            const publickey = e.detail.publickey;\n            let fingerprint = await window.crypto.subtle.digest(\"SHA-1\", publickey);\n            // The same fingerprint format as RealVNC\n            fingerprint = Array.from(new Uint8Array(fingerprint).slice(0, 8)).map(\n                x => x.toString(16).padStart(2, '0')).join('-');\n            document.getElementById('noVNC_verify_server_dlg').classList.add('noVNC_open');\n            document.getElementById('noVNC_fingerprint').innerHTML = fingerprint;\n        }\n    },\n\n    approveServer(e) {\n        e.preventDefault();\n        document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open');\n        UI.rfb.approveServer();\n    },\n\n    rejectServer(e) {\n        e.preventDefault();\n        document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open');\n        UI.disconnect();\n    },\n\n/* ------^-------\n * /SERVER VERIFY\n * ==============\n *   PASSWORD\n * ------v------*/\n\n    credentials(e) {\n        // FIXME: handle more types\n\n        document.getElementById(\"noVNC_username_block\").classList.remove(\"noVNC_hidden\");\n        document.getElementById(\"noVNC_password_block\").classList.remove(\"noVNC_hidden\");\n\n        let inputFocus = \"none\";\n        if (e.detail.types.indexOf(\"username\") === -1) {\n            document.getElementById(\"noVNC_username_block\").classList.add(\"noVNC_hidden\");\n        } else {\n            inputFocus = inputFocus === \"none\" ? \"noVNC_username_input\" : inputFocus;\n        }\n        if (e.detail.types.indexOf(\"password\") === -1) {\n            document.getElementById(\"noVNC_password_block\").classList.add(\"noVNC_hidden\");\n        } else {\n            inputFocus = inputFocus === \"none\" ? \"noVNC_password_input\" : inputFocus;\n        }\n        document.getElementById('noVNC_credentials_dlg')\n            .classList.add('noVNC_open');\n\n        setTimeout(() => document\n            .getElementById(inputFocus).focus(), 100);\n\n        Log.Warn(\"Server asked for credentials\");\n        UI.showStatus(_(\"Credentials are required\"), \"warning\");\n    },\n\n    setCredentials(e) {\n        // Prevent actually submitting the form\n        e.preventDefault();\n\n        let inputElemUsername = document.getElementById('noVNC_username_input');\n        const username = inputElemUsername.value;\n\n        let inputElemPassword = document.getElementById('noVNC_password_input');\n        const password = inputElemPassword.value;\n        // Clear the input after reading the password\n        inputElemPassword.value = \"\";\n\n        UI.rfb.sendCredentials({ username: username, password: password });\n        UI.reconnectPassword = password;\n        document.getElementById('noVNC_credentials_dlg')\n            .classList.remove('noVNC_open');\n    },\n\n/* ------^-------\n *  /PASSWORD\n * ==============\n *   FULLSCREEN\n * ------v------*/\n\n    toggleFullscreen() {\n        if (document.fullscreenElement || // alternative standard method\n            document.mozFullScreenElement || // currently working methods\n            document.webkitFullscreenElement ||\n            document.msFullscreenElement) {\n            if (document.exitFullscreen) {\n                document.exitFullscreen();\n            } else if (document.mozCancelFullScreen) {\n                document.mozCancelFullScreen();\n            } else if (document.webkitExitFullscreen) {\n                document.webkitExitFullscreen();\n            } else if (document.msExitFullscreen) {\n                document.msExitFullscreen();\n            }\n        } else {\n            if (document.documentElement.requestFullscreen) {\n                document.documentElement.requestFullscreen();\n            } else if (document.documentElement.mozRequestFullScreen) {\n                document.documentElement.mozRequestFullScreen();\n            } else if (document.documentElement.webkitRequestFullscreen) {\n                document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);\n            } else if (document.body.msRequestFullscreen) {\n                document.body.msRequestFullscreen();\n            }\n        }\n        UI.updateFullscreenButton();\n    },\n\n    updateFullscreenButton() {\n        if (document.fullscreenElement || // alternative standard method\n            document.mozFullScreenElement || // currently working methods\n            document.webkitFullscreenElement ||\n            document.msFullscreenElement ) {\n            document.getElementById('noVNC_fullscreen_button')\n                .classList.add(\"noVNC_selected\");\n        } else {\n            document.getElementById('noVNC_fullscreen_button')\n                .classList.remove(\"noVNC_selected\");\n        }\n    },\n\n/* ------^-------\n *  /FULLSCREEN\n * ==============\n *     RESIZE\n * ------v------*/\n\n    // Apply remote resizing or local scaling\n    applyResizeMode() {\n        if (!UI.rfb) return;\n\n        UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';\n        UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';\n    },\n\n/* ------^-------\n *    /RESIZE\n * ==============\n * VIEW CLIPPING\n * ------v------*/\n\n    // Update viewport clipping property for the connection. The normal\n    // case is to get the value from the setting. There are special cases\n    // for when the viewport is scaled or when a touch device is used.\n    updateViewClip() {\n        if (!UI.rfb) return;\n\n        const scaling = UI.getSetting('resize') === 'scale';\n\n        // Some platforms have overlay scrollbars that are difficult\n        // to use in our case, which means we have to force panning\n        // FIXME: Working scrollbars can still be annoying to use with\n        //        touch, so we should ideally be able to have both\n        //        panning and scrollbars at the same time\n\n        let brokenScrollbars = false;\n\n        if (!hasScrollbarGutter) {\n            if (isIOS() || isAndroid() || isMac() || isChromeOS()) {\n                brokenScrollbars = true;\n            }\n        }\n\n        if (scaling) {\n            // Can't be clipping if viewport is scaled to fit\n            UI.forceSetting('view_clip', false);\n            UI.rfb.clipViewport  = false;\n        } else if (brokenScrollbars) {\n            UI.forceSetting('view_clip', true);\n            UI.rfb.clipViewport = true;\n        } else {\n            UI.enableSetting('view_clip');\n            UI.rfb.clipViewport = UI.getSetting('view_clip');\n        }\n\n        // Changing the viewport may change the state of\n        // the dragging button\n        UI.updateViewDrag();\n    },\n\n/* ------^-------\n * /VIEW CLIPPING\n * ==============\n *    VIEWDRAG\n * ------v------*/\n\n    toggleViewDrag() {\n        if (!UI.rfb) return;\n\n        UI.rfb.dragViewport = !UI.rfb.dragViewport;\n        UI.updateViewDrag();\n    },\n\n    updateViewDrag() {\n        if (!UI.connected) return;\n\n        const viewDragButton = document.getElementById('noVNC_view_drag_button');\n\n        if ((!UI.rfb.clipViewport || !UI.rfb.clippingViewport) &&\n            UI.rfb.dragViewport) {\n            // We are no longer clipping the viewport. Make sure\n            // viewport drag isn't active when it can't be used.\n            UI.rfb.dragViewport = false;\n        }\n\n        if (UI.rfb.dragViewport) {\n            viewDragButton.classList.add(\"noVNC_selected\");\n        } else {\n            viewDragButton.classList.remove(\"noVNC_selected\");\n        }\n\n        if (UI.rfb.clipViewport) {\n            viewDragButton.classList.remove(\"noVNC_hidden\");\n        } else {\n            viewDragButton.classList.add(\"noVNC_hidden\");\n        }\n\n        viewDragButton.disabled = !UI.rfb.clippingViewport;\n    },\n\n/* ------^-------\n *   /VIEWDRAG\n * ==============\n *    QUALITY\n * ------v------*/\n\n    updateQuality() {\n        if (!UI.rfb) return;\n\n        UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));\n    },\n\n/* ------^-------\n *   /QUALITY\n * ==============\n *  COMPRESSION\n * ------v------*/\n\n    updateCompression() {\n        if (!UI.rfb) return;\n\n        UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));\n    },\n\n/* ------^-------\n *  /COMPRESSION\n * ==============\n *    KEYBOARD\n * ------v------*/\n\n    showVirtualKeyboard() {\n        if (!isTouchDevice) return;\n\n        const input = document.getElementById('noVNC_keyboardinput');\n\n        if (document.activeElement == input) return;\n\n        input.focus();\n\n        try {\n            const l = input.value.length;\n            // Move the caret to the end\n            input.setSelectionRange(l, l);\n        } catch (err) {\n            // setSelectionRange is undefined in Google Chrome\n        }\n    },\n\n    hideVirtualKeyboard() {\n        if (!isTouchDevice) return;\n\n        const input = document.getElementById('noVNC_keyboardinput');\n\n        if (document.activeElement != input) return;\n\n        input.blur();\n    },\n\n    toggleVirtualKeyboard() {\n        if (document.getElementById('noVNC_keyboard_button')\n            .classList.contains(\"noVNC_selected\")) {\n            UI.hideVirtualKeyboard();\n        } else {\n            UI.showVirtualKeyboard();\n        }\n    },\n\n    onfocusVirtualKeyboard(event) {\n        document.getElementById('noVNC_keyboard_button')\n            .classList.add(\"noVNC_selected\");\n        if (UI.rfb) {\n            UI.rfb.focusOnClick = false;\n        }\n    },\n\n    onblurVirtualKeyboard(event) {\n        document.getElementById('noVNC_keyboard_button')\n            .classList.remove(\"noVNC_selected\");\n        if (UI.rfb) {\n            UI.rfb.focusOnClick = true;\n        }\n    },\n\n    keepVirtualKeyboard(event) {\n        const input = document.getElementById('noVNC_keyboardinput');\n\n        // Only prevent focus change if the virtual keyboard is active\n        if (document.activeElement != input) {\n            return;\n        }\n\n        // Only allow focus to move to other elements that need\n        // focus to function properly\n        if (event.target.form !== undefined) {\n            switch (event.target.type) {\n                case 'text':\n                case 'email':\n                case 'search':\n                case 'password':\n                case 'tel':\n                case 'url':\n                case 'textarea':\n                case 'select-one':\n                case 'select-multiple':\n                    return;\n            }\n        }\n\n        event.preventDefault();\n    },\n\n    keyboardinputReset() {\n        const kbi = document.getElementById('noVNC_keyboardinput');\n        kbi.value = new Array(UI.defaultKeyboardinputLen).join(\"_\");\n        UI.lastKeyboardinput = kbi.value;\n    },\n\n    keyEvent(keysym, code, down) {\n        if (!UI.rfb) return;\n\n        UI.rfb.sendKey(keysym, code, down);\n    },\n\n    // When normal keyboard events are left uncought, use the input events from\n    // the keyboardinput element instead and generate the corresponding key events.\n    // This code is required since some browsers on Android are inconsistent in\n    // sending keyCodes in the normal keyboard events when using on screen keyboards.\n    keyInput(event) {\n\n        if (!UI.rfb) return;\n\n        const newValue = event.target.value;\n\n        if (!UI.lastKeyboardinput) {\n            UI.keyboardinputReset();\n        }\n        const oldValue = UI.lastKeyboardinput;\n\n        let newLen;\n        try {\n            // Try to check caret position since whitespace at the end\n            // will not be considered by value.length in some browsers\n            newLen = Math.max(event.target.selectionStart, newValue.length);\n        } catch (err) {\n            // selectionStart is undefined in Google Chrome\n            newLen = newValue.length;\n        }\n        const oldLen = oldValue.length;\n\n        let inputs = newLen - oldLen;\n        let backspaces = inputs < 0 ? -inputs : 0;\n\n        // Compare the old string with the new to account for\n        // text-corrections or other input that modify existing text\n        for (let i = 0; i < Math.min(oldLen, newLen); i++) {\n            if (newValue.charAt(i) != oldValue.charAt(i)) {\n                inputs = newLen - i;\n                backspaces = oldLen - i;\n                break;\n            }\n        }\n\n        // Send the key events\n        for (let i = 0; i < backspaces; i++) {\n            UI.rfb.sendKey(KeyTable.XK_BackSpace, \"Backspace\");\n        }\n        for (let i = newLen - inputs; i < newLen; i++) {\n            UI.rfb.sendKey(keysyms.lookup(newValue.charCodeAt(i)));\n        }\n\n        // Control the text content length in the keyboardinput element\n        if (newLen > 2 * UI.defaultKeyboardinputLen) {\n            UI.keyboardinputReset();\n        } else if (newLen < 1) {\n            // There always have to be some text in the keyboardinput\n            // element with which backspace can interact.\n            UI.keyboardinputReset();\n            // This sometimes causes the keyboard to disappear for a second\n            // but it is required for the android keyboard to recognize that\n            // text has been added to the field\n            event.target.blur();\n            // This has to be ran outside of the input handler in order to work\n            setTimeout(event.target.focus.bind(event.target), 0);\n        } else {\n            UI.lastKeyboardinput = newValue;\n        }\n    },\n\n/* ------^-------\n *   /KEYBOARD\n * ==============\n *   EXTRA KEYS\n * ------v------*/\n\n    openExtraKeys() {\n        UI.closeAllPanels();\n        UI.openControlbar();\n\n        document.getElementById('noVNC_modifiers')\n            .classList.add(\"noVNC_open\");\n        document.getElementById('noVNC_toggle_extra_keys_button')\n            .classList.add(\"noVNC_selected\");\n    },\n\n    closeExtraKeys() {\n        document.getElementById('noVNC_modifiers')\n            .classList.remove(\"noVNC_open\");\n        document.getElementById('noVNC_toggle_extra_keys_button')\n            .classList.remove(\"noVNC_selected\");\n    },\n\n    toggleExtraKeys() {\n        if (document.getElementById('noVNC_modifiers')\n            .classList.contains(\"noVNC_open\")) {\n            UI.closeExtraKeys();\n        } else  {\n            UI.openExtraKeys();\n        }\n    },\n\n    sendEsc() {\n        UI.sendKey(KeyTable.XK_Escape, \"Escape\");\n    },\n\n    sendTab() {\n        UI.sendKey(KeyTable.XK_Tab, \"Tab\");\n    },\n\n    toggleCtrl() {\n        const btn = document.getElementById('noVNC_toggle_ctrl_button');\n        if (btn.classList.contains(\"noVNC_selected\")) {\n            UI.sendKey(KeyTable.XK_Control_L, \"ControlLeft\", false);\n            btn.classList.remove(\"noVNC_selected\");\n        } else {\n            UI.sendKey(KeyTable.XK_Control_L, \"ControlLeft\", true);\n            btn.classList.add(\"noVNC_selected\");\n        }\n    },\n\n    toggleWindows() {\n        const btn = document.getElementById('noVNC_toggle_windows_button');\n        if (btn.classList.contains(\"noVNC_selected\")) {\n            UI.sendKey(KeyTable.XK_Super_L, \"MetaLeft\", false);\n            btn.classList.remove(\"noVNC_selected\");\n        } else {\n            UI.sendKey(KeyTable.XK_Super_L, \"MetaLeft\", true);\n            btn.classList.add(\"noVNC_selected\");\n        }\n    },\n\n    toggleAlt() {\n        const btn = document.getElementById('noVNC_toggle_alt_button');\n        if (btn.classList.contains(\"noVNC_selected\")) {\n            UI.sendKey(KeyTable.XK_Alt_L, \"AltLeft\", false);\n            btn.classList.remove(\"noVNC_selected\");\n        } else {\n            UI.sendKey(KeyTable.XK_Alt_L, \"AltLeft\", true);\n            btn.classList.add(\"noVNC_selected\");\n        }\n    },\n\n    sendCtrlAltDel() {\n        UI.rfb.sendCtrlAltDel();\n        // See below\n        UI.rfb.focus();\n        UI.idleControlbar();\n    },\n\n    sendKey(keysym, code, down) {\n        UI.rfb.sendKey(keysym, code, down);\n\n        // Move focus to the screen in order to be able to use the\n        // keyboard right after these extra keys.\n        // The exception is when a virtual keyboard is used, because\n        // if we focus the screen the virtual keyboard would be closed.\n        // In this case we focus our special virtual keyboard input\n        // element instead.\n        if (document.getElementById('noVNC_keyboard_button')\n            .classList.contains(\"noVNC_selected\")) {\n            document.getElementById('noVNC_keyboardinput').focus();\n        } else {\n            UI.rfb.focus();\n        }\n        // fade out the controlbar to highlight that\n        // the focus has been moved to the screen\n        UI.idleControlbar();\n    },\n\n/* ------^-------\n *   /EXTRA KEYS\n * ==============\n *     MISC\n * ------v------*/\n\n    updateViewOnly() {\n        if (!UI.rfb) return;\n        UI.rfb.viewOnly = UI.getSetting('view_only');\n\n        UI.updateBeforeUnload();\n\n        // Hide input related buttons in view only mode\n        if (UI.rfb.viewOnly) {\n            document.getElementById('noVNC_keyboard_button')\n                .classList.add('noVNC_hidden');\n            document.getElementById('noVNC_toggle_extra_keys_button')\n                .classList.add('noVNC_hidden');\n            document.getElementById('noVNC_clipboard_button')\n                .classList.add('noVNC_hidden');\n        } else {\n            document.getElementById('noVNC_keyboard_button')\n                .classList.remove('noVNC_hidden');\n            document.getElementById('noVNC_toggle_extra_keys_button')\n                .classList.remove('noVNC_hidden');\n            document.getElementById('noVNC_clipboard_button')\n                .classList.remove('noVNC_hidden');\n        }\n    },\n\n    updateClipboard() {\n        browserAsyncClipboardSupport()\n            .then((support) => {\n                if (support === 'unsupported') {\n                    // Use fallback clipboard panel\n                    return;\n                }\n                if (support === 'denied' || support === 'available') {\n                    UI.closeClipboardPanel();\n                    document.getElementById('noVNC_clipboard_button')\n                        .classList.add('noVNC_hidden');\n                    document.getElementById('noVNC_clipboard_button')\n                        .removeEventListener('click', UI.toggleClipboardPanel);\n                    document.getElementById('noVNC_clipboard_text')\n                        .removeEventListener('change', UI.clipboardSend);\n                    if (UI.rfb) {\n                        UI.rfb.removeEventListener('clipboard', UI.clipboardReceive);\n                    }\n                }\n            })\n            .catch(() => {\n                // Treat as unsupported\n            });\n    },\n\n    updateShowDotCursor() {\n        if (!UI.rfb) return;\n        UI.rfb.showDotCursor = UI.getSetting('show_dot');\n    },\n\n    updateLogging() {\n        WebUtil.initLogging(UI.getSetting('logging'));\n    },\n\n    updateDesktopName(e) {\n        UI.desktopName = e.detail.name;\n        // Display the desktop name in the document title\n        document.title = e.detail.name + \" - \" + PAGE_TITLE;\n    },\n\n    updateRequestWakelock() {\n        if (!UI.rfb) return;\n        if (UI.getSetting('keep_device_awake')) {\n            UI.wakeLockManager.acquire();\n        } else {\n            UI.wakeLockManager.release();\n        }\n    },\n\n\n    bell(e) {\n        if (UI.getSetting('bell') === 'on') {\n            const promise = document.getElementById('noVNC_bell').play();\n            // The standards disagree on the return value here\n            if (promise) {\n                promise.catch((e) => {\n                    if (e.name === \"NotAllowedError\") {\n                        // Ignore when the browser doesn't let us play audio.\n                        // It is common that the browsers require audio to be\n                        // initiated from a user action.\n                    } else {\n                        Log.Error(\"Unable to play bell: \" + e);\n                    }\n                });\n            }\n        }\n    },\n\n    //Helper to add options to dropdown.\n    addOption(selectbox, text, value) {\n        const optn = document.createElement(\"OPTION\");\n        optn.text = text;\n        optn.value = value;\n        selectbox.options.add(optn);\n    },\n\n/* ------^-------\n *    /MISC\n * ==============\n */\n};\n\nexport default UI;\n"
  },
  {
    "path": "app/wakelock.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2025 The noVNC authors\n * Licensed under MPL 2.0 or any later version (see LICENSE.txt)\n *\n * Wrapper around the `navigator.wakeLock` api that handles reacquiring the\n * lock on visiblility changes.\n *\n * The `acquire` and `release` methods may be called any number of times. The\n * most recent call dictates the desired end-state (if `acquire` was most\n * recently called, then we will try to acquire and hold the wake lock).\n */\n\nimport * as Log from '../core/util/logging.js';\n\nconst _STATES = {\n    /* No wake lock.\n     *\n     * Can transition to:\n     *  - AWAITING_VISIBLE: `acquire` called when document is hidden.\n     *  - ACQUIRING: `acquire` called.\n     *  - ERROR: `acquired` called when the api is not available.\n     */\n    RELEASED: 'released',\n    /* Wake lock requested, waiting for browser.\n     *\n     * Can transition to:\n     *  - ACQUIRED: success\n     *  - ACQUIRING_WANT_RELEASE: `release` called while waiting\n     *  - ERROR\n     */\n    ACQUIRING: 'acquiring',\n    /* Wake lock requested, release called, still waiting for browser.\n     *\n     * Can transition to:\n     *  - ACQUIRING: `acquire` called (but promise has not resolved yet)\n     *  - RELEASED: success\n     */\n    ACQUIRING_WANT_RELEASE: 'releasing',\n    /* Wake lock held.\n     *\n     * Can transition to:\n     *  - AWAITING_VISIBLE: wakelock lost due to visibility change\n     *  - RELEASED: success\n     */\n    ACQUIRED: 'acquired',\n    /* Caller wants wakelock, but we can not get it due to visibility.\n     *\n     * Can transition to:\n     *  - ACQUIRING: document is now visible, attempting to get wakelock.\n     *  - RELEASED: when release is called.\n     */\n    AWAITING_VISIBLE: 'awaiting_visible',\n    /* An error has occurred.\n     *\n     * Can transition to:\n     *  - RELEASED: will happen immediately.\n     */\n    ERROR: 'error',\n};\n\nclass TestOnlyWakeLockManagerStateChangeEvent extends Event {\n    constructor(oldState, newState) {\n        super(\"testOnlyStateChange\");\n        this.oldState = oldState;\n        this.newState = newState;\n    }\n}\n\nexport default class WakeLockManager extends EventTarget {\n    constructor() {\n        super();\n\n        this._state = _STATES.RELEASED;\n        this._wakelock = null;\n\n        this._eventHandlers = {\n            wakelockAcquired: this._wakelockAcquired.bind(this),\n            wakelockReleased: this._wakelockReleased.bind(this),\n            documentVisibilityChange: this._documentVisibilityChange.bind(this),\n        };\n    }\n\n    acquire() {\n        switch (this._state) {\n            case _STATES.ACQUIRING_WANT_RELEASE:\n                // We are currently waiting to acquire the wakelock. While\n                // waiting, `release()` was called. By transitioning back to\n                // ACQUIRING, we will keep the lock after we receive it.\n                this._transitionTo(_STATES.ACQUIRING);\n                break;\n            case _STATES.AWAITING_VISIBLE:\n            case _STATES.ACQUIRING:\n            case _STATES.ACQUIRED:\n                break;\n            case _STATES.ERROR:\n            case _STATES.RELEASED:\n                if (document.hidden) {\n                    // We can not acquire the wakelock while the document is\n                    // hidden (eg, not the active tab). Wait until it is\n                    // visible, then acquire the wakelock.\n                    this._awaitVisible();\n                    break;\n                }\n                this._acquireWakelockNow();\n                break;\n        }\n    }\n\n    release() {\n        switch (this._state) {\n            case _STATES.ERROR:\n            case _STATES.RELEASED:\n            case _STATES.ACQUIRING_WANT_RELEASE:\n                break;\n            case _STATES.ACQUIRING:\n                // We are have requested (but not yet received) the wakelock.\n                // Give it up as soon as we acquire it.\n                this._transitionTo(_STATES.ACQUIRING_WANT_RELEASE);\n                break;\n            case _STATES.ACQUIRED:\n                // We remove the event listener first, as we don't want to be\n                // notified about this release (it is expected).\n                this._wakelock.removeEventListener(\"release\", this._eventHandlers.wakelockReleased);\n                this._wakelock.release();\n                this._wakelock = null;\n                this._transitionTo(_STATES.RELEASED);\n                break;\n            case _STATES.AWAITING_VISIBLE:\n                // We don't currently have the lock, but are waiting for the\n                // document to become visible. By removing the event listener,\n                // we will not attempt to get the wakelock in the future.\n                document.removeEventListener(\"visibilitychange\", this._eventHandlers.documentVisibilityChange);\n                this._transitionTo(_STATES.RELEASED);\n                break;\n        }\n    }\n\n    _transitionTo(newState) {\n        let oldState = this._state;\n        Log.Debug(`WakelockManager transitioning ${oldState} -> ${newState}`);\n        this._state = newState;\n        this.dispatchEvent(new TestOnlyWakeLockManagerStateChangeEvent(oldState, newState));\n    }\n\n    _awaitVisible() {\n        document.addEventListener(\"visibilitychange\", this._eventHandlers.documentVisibilityChange);\n        this._transitionTo(_STATES.AWAITING_VISIBLE);\n    }\n\n    _acquireWakelockNow() {\n        if (!(\"wakeLock\" in navigator)) {\n            Log.Warn(\"Unable to request wakeLock, Browser does not have wakeLock api\");\n            this._transitionTo(_STATES.ERROR);\n            this._transitionTo(_STATES.RELEASED);\n            return;\n        }\n        navigator.wakeLock.request(\"screen\")\n            .then(this._eventHandlers.wakelockAcquired)\n            .catch((err) => {\n                Log.Warn(\"Error occurred while acquiring wakelock: \" + err);\n                this._transitionTo(_STATES.ERROR);\n                this._transitionTo(_STATES.RELEASED);\n            });\n        this._transitionTo(_STATES.ACQUIRING);\n    }\n\n\n    _wakelockAcquired(wakelock) {\n        if (this._state === _STATES.ACQUIRING_WANT_RELEASE) {\n            // We were requested to release the wakelock while we were trying to\n            // acquire it. Now that we have acquired it, immediately release it.\n            wakelock.release();\n            this._transitionTo(_STATES.RELEASED);\n            return;\n        }\n        this._wakelock = wakelock;\n        this._wakelock.addEventListener(\"release\", this._eventHandlers.wakelockReleased);\n        this._transitionTo(_STATES.ACQUIRED);\n    }\n\n    _wakelockReleased(event) {\n        this._wakelock = null;\n        if (document.visibilityState === \"visible\") {\n            Log.Warn(\"Lost wakelock, but document is still visible. Not reacquiring\");\n            this._transitionTo(_STATES.RELEASED);\n            return;\n        }\n        this._awaitVisible();\n    }\n\n    _documentVisibilityChange(event) {\n        if (document.visibilityState !== \"visible\") {\n            return;\n        }\n        document.removeEventListener(\"visibilitychange\", this._eventHandlers.documentVisibilityChange);\n        this._acquireWakelockNow();\n    }\n}\n"
  },
  {
    "path": "app/webutil.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2019 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n */\n\nimport * as Log from '../core/util/logging.js';\n\n// init log level reading the logging HTTP param\nexport function initLogging(level) {\n    \"use strict\";\n    if (typeof level !== \"undefined\") {\n        Log.initLogging(level);\n    } else {\n        const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/);\n        Log.initLogging(param || undefined);\n    }\n}\n\n// Read a query string variable\n// A URL with a query parameter can look like this (But will most probably get logged on the http server):\n// https://www.example.com?myqueryparam=myvalue\n//\n// For privacy (Using a hastag #, the parameters will not be sent to the server)\n// the url can be requested in the following way:\n// https://www.example.com#myqueryparam=myvalue&password=secretvalue\n//\n// Even mixing public and non public parameters will work:\n// https://www.example.com?nonsecretparam=example.com#password=secretvalue\nexport function getQueryVar(name, defVal) {\n    \"use strict\";\n    const re = new RegExp('.*[?&]' + name + '=([^&#]*)'),\n          match = document.location.href.match(re);\n    if (typeof defVal === 'undefined') { defVal = null; }\n\n    if (match) {\n        return decodeURIComponent(match[1]);\n    }\n\n    return defVal;\n}\n\n// Read a hash fragment variable\nexport function getHashVar(name, defVal) {\n    \"use strict\";\n    const re = new RegExp('.*[&#]' + name + '=([^&]*)'),\n          match = document.location.hash.match(re);\n    if (typeof defVal === 'undefined') { defVal = null; }\n\n    if (match) {\n        return decodeURIComponent(match[1]);\n    }\n\n    return defVal;\n}\n\n// Read a variable from the fragment or the query string\n// Fragment takes precedence\nexport function getConfigVar(name, defVal) {\n    \"use strict\";\n    const val = getHashVar(name);\n\n    if (val === null) {\n        return getQueryVar(name, defVal);\n    }\n\n    return val;\n}\n\n/*\n * Cookie handling. Dervied from: http://www.quirksmode.org/js/cookies.html\n */\n\n// No days means only for this browser session\nexport function createCookie(name, value, days) {\n    \"use strict\";\n    let date, expires;\n    if (days) {\n        date = new Date();\n        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));\n        expires = \"; expires=\" + date.toGMTString();\n    } else {\n        expires = \"\";\n    }\n\n    let secure;\n    if (document.location.protocol === \"https:\") {\n        secure = \"; secure\";\n    } else {\n        secure = \"\";\n    }\n    document.cookie = name + \"=\" + value + expires + \"; path=/\" + secure;\n}\n\nexport function readCookie(name, defaultValue) {\n    \"use strict\";\n    const nameEQ = name + \"=\";\n    const ca = document.cookie.split(';');\n\n    for (let i = 0; i < ca.length; i += 1) {\n        let c = ca[i];\n        while (c.charAt(0) === ' ') {\n            c = c.substring(1, c.length);\n        }\n        if (c.indexOf(nameEQ) === 0) {\n            return c.substring(nameEQ.length, c.length);\n        }\n    }\n\n    return (typeof defaultValue !== 'undefined') ? defaultValue : null;\n}\n\nexport function eraseCookie(name) {\n    \"use strict\";\n    createCookie(name, \"\", -1);\n}\n\n/*\n * Setting handling.\n */\n\nlet settings = {};\n\nexport function initSettings() {\n    if (!window.chrome || !window.chrome.storage) {\n        settings = {};\n        return Promise.resolve();\n    }\n\n    return new Promise(resolve => window.chrome.storage.sync.get(resolve))\n        .then((cfg) => { settings = cfg; });\n}\n\n// Update the settings cache, but do not write to permanent storage\nexport function setSetting(name, value) {\n    settings[name] = value;\n}\n\n// No days means only for this browser session\nexport function writeSetting(name, value) {\n    \"use strict\";\n    if (settings[name] === value) return;\n    settings[name] = value;\n    if (window.chrome && window.chrome.storage) {\n        window.chrome.storage.sync.set(settings);\n    } else {\n        localStorageSet(name, value);\n    }\n}\n\nexport function readSetting(name, defaultValue) {\n    \"use strict\";\n    let value;\n    if ((name in settings) || (window.chrome && window.chrome.storage)) {\n        value = settings[name];\n    } else {\n        value = localStorageGet(name);\n        settings[name] = value;\n    }\n    if (typeof value === \"undefined\") {\n        value = null;\n    }\n\n    if (value === null && typeof defaultValue !== \"undefined\") {\n        return defaultValue;\n    }\n\n    return value;\n}\n\nexport function eraseSetting(name) {\n    \"use strict\";\n    // Deleting here means that next time the setting is read when using local\n    // storage, it will be pulled from local storage again.\n    // If the setting in local storage is changed (e.g. in another tab)\n    // between this delete and the next read, it could lead to an unexpected\n    // value change.\n    delete settings[name];\n    if (window.chrome && window.chrome.storage) {\n        window.chrome.storage.sync.remove(name);\n    } else {\n        localStorageRemove(name);\n    }\n}\n\nlet loggedMsgs = [];\nfunction logOnce(msg, level = \"warn\") {\n    if (!loggedMsgs.includes(msg)) {\n        switch (level) {\n            case \"error\":\n                Log.Error(msg);\n                break;\n            case \"warn\":\n                Log.Warn(msg);\n                break;\n            case \"debug\":\n                Log.Debug(msg);\n                break;\n            default:\n                Log.Info(msg);\n        }\n        loggedMsgs.push(msg);\n    }\n}\n\nlet cookiesMsg = \"Couldn't access noVNC settings, are cookies disabled?\";\n\nfunction localStorageGet(name) {\n    let r;\n    try {\n        r = localStorage.getItem(name);\n    } catch (e) {\n        if (e instanceof DOMException) {\n            logOnce(cookiesMsg);\n            logOnce(\"'localStorage.getItem(\" + name + \")' failed: \" + e,\n                    \"debug\");\n        } else {\n            throw e;\n        }\n    }\n    return r;\n}\nfunction localStorageSet(name, value) {\n    try {\n        localStorage.setItem(name, value);\n    } catch (e) {\n        if (e instanceof DOMException) {\n            logOnce(cookiesMsg);\n            logOnce(\"'localStorage.setItem(\" + name + \",\" + value +\n                    \")' failed: \" + e, \"debug\");\n        } else {\n            throw e;\n        }\n    }\n}\nfunction localStorageRemove(name) {\n    try {\n        localStorage.removeItem(name);\n    } catch (e) {\n        if (e instanceof DOMException) {\n            logOnce(cookiesMsg);\n            logOnce(\"'localStorage.removeItem(\" + name + \")' failed: \" + e,\n                    \"debug\");\n        } else {\n            throw e;\n        }\n    }\n}\n"
  },
  {
    "path": "core/base64.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n// From: http://hg.mozilla.org/mozilla-central/raw-file/ec10630b1a54/js/src/devtools/jint/sunspider/string-base64.js\n\nimport * as Log from './util/logging.js';\n\nexport default {\n    /* Convert data (an array of integers) to a Base64 string. */\n    toBase64Table: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''),\n    base64Pad: '=',\n\n    encode(data) {\n        \"use strict\";\n        let result = '';\n        const length = data.length;\n        const lengthpad = (length % 3);\n        // Convert every three bytes to 4 ascii characters.\n\n        for (let i = 0; i < (length - 2); i += 3) {\n            result += this.toBase64Table[data[i] >> 2];\n            result += this.toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)];\n            result += this.toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)];\n            result += this.toBase64Table[data[i + 2] & 0x3f];\n        }\n\n        // Convert the remaining 1 or 2 bytes, pad out to 4 characters.\n        const j = length - lengthpad;\n        if (lengthpad === 2) {\n            result += this.toBase64Table[data[j] >> 2];\n            result += this.toBase64Table[((data[j] & 0x03) << 4) + (data[j + 1] >> 4)];\n            result += this.toBase64Table[(data[j + 1] & 0x0f) << 2];\n            result += this.toBase64Table[64];\n        } else if (lengthpad === 1) {\n            result += this.toBase64Table[data[j] >> 2];\n            result += this.toBase64Table[(data[j] & 0x03) << 4];\n            result += this.toBase64Table[64];\n            result += this.toBase64Table[64];\n        }\n\n        return result;\n    },\n\n    /* Convert Base64 data to a string */\n    /* eslint-disable comma-spacing */\n    toBinaryTable: [\n        -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,\n        -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,\n        -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,\n        52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,\n        -1, 0, 1, 2,  3, 4, 5, 6,  7, 8, 9,10, 11,12,13,14,\n        15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,\n        -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,\n        41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1\n    ],\n    /* eslint-enable comma-spacing */\n\n    decode(data, offset = 0) {\n        let dataLength = data.indexOf('=') - offset;\n        if (dataLength < 0) { dataLength = data.length - offset; }\n\n        /* Every four characters is 3 resulting numbers */\n        const resultLength = (dataLength >> 2) * 3 + Math.floor((dataLength % 4) / 1.5);\n        const result = new Array(resultLength);\n\n        // Convert one by one.\n\n        let leftbits = 0; // number of bits decoded, but yet to be appended\n        let leftdata = 0; // bits decoded, but yet to be appended\n        for (let idx = 0, i = offset; i < data.length; i++) {\n            const c = this.toBinaryTable[data.charCodeAt(i) & 0x7f];\n            const padding = (data.charAt(i) === this.base64Pad);\n            // Skip illegal characters and whitespace\n            if (c === -1) {\n                Log.Error(\"Illegal character code \" + data.charCodeAt(i) + \" at position \" + i);\n                continue;\n            }\n\n            // Collect data into leftdata, update bitcount\n            leftdata = (leftdata << 6) | c;\n            leftbits += 6;\n\n            // If we have 8 or more bits, append 8 bits to the result\n            if (leftbits >= 8) {\n                leftbits -= 8;\n                // Append if not padding.\n                if (!padding) {\n                    result[idx++] = (leftdata >> leftbits) & 0xff;\n                }\n                leftdata &= (1 << leftbits) - 1;\n            }\n        }\n\n        // If there are any bits left, the base64 string was corrupted\n        if (leftbits) {\n            const err = new Error('Corrupted base64 string');\n            err.name = 'Base64-Error';\n            throw err;\n        }\n\n        return result;\n    }\n}; /* End of Base64 namespace */\n"
  },
  {
    "path": "core/clipboard.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (c) 2025 The noVNC authors\n * Licensed under MPL 2.0 or any later version (see LICENSE.txt)\n */\n\nimport * as Log from './util/logging.js';\nimport { browserAsyncClipboardSupport } from './util/browser.js';\n\nexport default class AsyncClipboard {\n    constructor(target) {\n        this._target = target || null;\n\n        this._isAvailable = null;\n\n        this._eventHandlers = {\n            'focus': this._handleFocus.bind(this),\n        };\n\n        // ===== EVENT HANDLERS =====\n\n        this.onpaste = () => {};\n    }\n\n    // ===== PRIVATE METHODS =====\n\n    async _ensureAvailable() {\n        if (this._isAvailable !== null) return this._isAvailable;\n        try {\n            const status = await browserAsyncClipboardSupport();\n            this._isAvailable = (status === 'available');\n        } catch {\n            this._isAvailable = false;\n        }\n        return this._isAvailable;\n    }\n\n    async _handleFocus(event) {\n        if (!(await this._ensureAvailable())) return;\n        try {\n            const text = await navigator.clipboard.readText();\n            this.onpaste(text);\n        } catch (error) {\n            Log.Error(\"Clipboard read failed: \", error);\n        }\n    }\n\n    // ===== PUBLIC METHODS =====\n\n    writeClipboard(text) {\n        // Can lazily check cached availability\n        if (!this._isAvailable) return false;\n        navigator.clipboard.writeText(text)\n            .catch(error => Log.Error(\"Clipboard write failed: \", error));\n        return true;\n    }\n\n    grab() {\n        if (!this._target) return;\n        this._ensureAvailable()\n            .then((isAvailable) => {\n                if (isAvailable) {\n                    this._target.addEventListener('focus', this._eventHandlers.focus);\n                }\n            });\n    }\n\n    ungrab() {\n        if (!this._target) return;\n        this._target.removeEventListener('focus', this._eventHandlers.focus);\n    }\n}\n"
  },
  {
    "path": "core/crypto/aes.js",
    "content": "export class AESECBCipher {\n    constructor() {\n        this._key = null;\n    }\n\n    get algorithm() {\n        return { name: \"AES-ECB\" };\n    }\n\n    static async importKey(key, _algorithm, extractable, keyUsages) {\n        const cipher = new AESECBCipher;\n        await cipher._importKey(key, extractable, keyUsages);\n        return cipher;\n    }\n\n    async _importKey(key, extractable, keyUsages) {\n        this._key = await window.crypto.subtle.importKey(\n            \"raw\", key, {name: \"AES-CBC\"}, extractable, keyUsages);\n    }\n\n    async encrypt(_algorithm, plaintext) {\n        const x = new Uint8Array(plaintext);\n        if (x.length % 16 !== 0 || this._key === null) {\n            return null;\n        }\n        const n = x.length / 16;\n        for (let i = 0; i < n; i++) {\n            const y = new Uint8Array(await window.crypto.subtle.encrypt({\n                name: \"AES-CBC\",\n                iv: new Uint8Array(16),\n            }, this._key, x.slice(i * 16, i * 16 + 16))).slice(0, 16);\n            x.set(y, i * 16);\n        }\n        return x;\n    }\n}\n\nexport class AESEAXCipher {\n    constructor() {\n        this._rawKey = null;\n        this._ctrKey = null;\n        this._cbcKey = null;\n        this._zeroBlock = new Uint8Array(16);\n        this._prefixBlock0 = this._zeroBlock;\n        this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);\n        this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]);\n    }\n\n    get algorithm() {\n        return { name: \"AES-EAX\" };\n    }\n\n    async _encryptBlock(block) {\n        const encrypted = await window.crypto.subtle.encrypt({\n            name: \"AES-CBC\",\n            iv: this._zeroBlock,\n        }, this._cbcKey, block);\n        return new Uint8Array(encrypted).slice(0, 16);\n    }\n\n    async _initCMAC() {\n        const k1 = await this._encryptBlock(this._zeroBlock);\n        const k2 = new Uint8Array(16);\n        const v = k1[0] >>> 6;\n        for (let i = 0; i < 15; i++) {\n            k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2);\n            k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1);\n        }\n        const lut = [0x0, 0x87, 0x0e, 0x89];\n        k2[14] ^= v >>> 1;\n        k2[15] = (k1[15] << 2) ^ lut[v];\n        k1[15] = (k1[15] << 1) ^ lut[v >> 1];\n        this._k1 = k1;\n        this._k2 = k2;\n    }\n\n    async _encryptCTR(data, counter) {\n        const encrypted = await window.crypto.subtle.encrypt({\n            name: \"AES-CTR\",\n            counter: counter,\n            length: 128\n        }, this._ctrKey, data);\n        return new Uint8Array(encrypted);\n    }\n\n    async _decryptCTR(data, counter) {\n        const decrypted = await window.crypto.subtle.decrypt({\n            name: \"AES-CTR\",\n            counter: counter,\n            length: 128\n        }, this._ctrKey, data);\n        return new Uint8Array(decrypted);\n    }\n\n    async _computeCMAC(data, prefixBlock) {\n        if (prefixBlock.length !== 16) {\n            return null;\n        }\n        const n = Math.floor(data.length / 16);\n        const m = Math.ceil(data.length / 16);\n        const r = data.length - n * 16;\n        const cbcData = new Uint8Array((m + 1) * 16);\n        cbcData.set(prefixBlock);\n        cbcData.set(data, 16);\n        if (r === 0) {\n            for (let i = 0; i < 16; i++) {\n                cbcData[n * 16 + i] ^= this._k1[i];\n            }\n        } else {\n            cbcData[(n + 1) * 16 + r] = 0x80;\n            for (let i = 0; i < 16; i++) {\n                cbcData[(n + 1) * 16 + i] ^= this._k2[i];\n            }\n        }\n        let cbcEncrypted = await window.crypto.subtle.encrypt({\n            name: \"AES-CBC\",\n            iv: this._zeroBlock,\n        }, this._cbcKey, cbcData);\n\n        cbcEncrypted = new Uint8Array(cbcEncrypted);\n        const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16);\n        return mac;\n    }\n\n    static async importKey(key, _algorithm, _extractable, _keyUsages) {\n        const cipher = new AESEAXCipher;\n        await cipher._importKey(key);\n        return cipher;\n    }\n\n    async _importKey(key) {\n        this._rawKey = key;\n        this._ctrKey = await window.crypto.subtle.importKey(\n            \"raw\", key, {name: \"AES-CTR\"}, false, [\"encrypt\", \"decrypt\"]);\n        this._cbcKey = await window.crypto.subtle.importKey(\n            \"raw\", key, {name: \"AES-CBC\"}, false, [\"encrypt\"]);\n        await this._initCMAC();\n    }\n\n    async encrypt(algorithm, message) {\n        const ad = algorithm.additionalData;\n        const nonce = algorithm.iv;\n        const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);\n        const encrypted = await this._encryptCTR(message, nCMAC);\n        const adCMAC = await this._computeCMAC(ad, this._prefixBlock1);\n        const mac = await this._computeCMAC(encrypted, this._prefixBlock2);\n        for (let i = 0; i < 16; i++) {\n            mac[i] ^= nCMAC[i] ^ adCMAC[i];\n        }\n        const res = new Uint8Array(16 + encrypted.length);\n        res.set(encrypted);\n        res.set(mac, encrypted.length);\n        return res;\n    }\n\n    async decrypt(algorithm, data) {\n        const encrypted = data.slice(0, data.length - 16);\n        const ad = algorithm.additionalData;\n        const nonce = algorithm.iv;\n        const mac = data.slice(data.length - 16);\n        const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);\n        const adCMAC = await this._computeCMAC(ad, this._prefixBlock1);\n        const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2);\n        for (let i = 0; i < 16; i++) {\n            computedMac[i] ^= nCMAC[i] ^ adCMAC[i];\n        }\n        if (computedMac.length !== mac.length) {\n            return null;\n        }\n        for (let i = 0; i < mac.length; i++) {\n            if (computedMac[i] !== mac[i]) {\n                return null;\n            }\n        }\n        const res = await this._decryptCTR(encrypted, nCMAC);\n        return res;\n    }\n}\n"
  },
  {
    "path": "core/crypto/bigint.js",
    "content": "export function modPow(b, e, m) {\n    let r = 1n;\n    b = b % m;\n    while (e > 0n) {\n        if ((e & 1n) === 1n) {\n            r = (r * b) % m;\n        }\n        e = e >> 1n;\n        b = (b * b) % m;\n    }\n    return r;\n}\n\nexport function bigIntToU8Array(bigint, padLength=0) {\n    let hex = bigint.toString(16);\n    if (padLength === 0) {\n        padLength = Math.ceil(hex.length / 2);\n    }\n    hex = hex.padStart(padLength * 2, '0');\n    const length = hex.length / 2;\n    const arr = new Uint8Array(length);\n    for (let i = 0; i < length; i++) {\n        arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);\n    }\n    return arr;\n}\n\nexport function u8ArrayToBigInt(arr) {\n    let hex = '0x';\n    for (let i = 0; i < arr.length; i++) {\n        hex += arr[i].toString(16).padStart(2, '0');\n    }\n    return BigInt(hex);\n}\n"
  },
  {
    "path": "core/crypto/crypto.js",
    "content": "import { AESECBCipher, AESEAXCipher } from \"./aes.js\";\nimport { DESCBCCipher, DESECBCipher } from \"./des.js\";\nimport { RSACipher } from \"./rsa.js\";\nimport { DHCipher } from \"./dh.js\";\nimport { MD5 } from \"./md5.js\";\n\n// A single interface for the cryptographic algorithms not supported by SubtleCrypto.\n// Both synchronous and asynchronous implmentations are allowed.\nclass LegacyCrypto {\n    constructor() {\n        this._algorithms = {\n            \"AES-ECB\": AESECBCipher,\n            \"AES-EAX\": AESEAXCipher,\n            \"DES-ECB\": DESECBCipher,\n            \"DES-CBC\": DESCBCCipher,\n            \"RSA-PKCS1-v1_5\": RSACipher,\n            \"DH\": DHCipher,\n            \"MD5\": MD5,\n        };\n    }\n\n    encrypt(algorithm, key, data) {\n        if (key.algorithm.name !== algorithm.name) {\n            throw new Error(\"algorithm does not match\");\n        }\n        if (typeof key.encrypt !== \"function\") {\n            throw new Error(\"key does not support encryption\");\n        }\n        return key.encrypt(algorithm, data);\n    }\n\n    decrypt(algorithm, key, data) {\n        if (key.algorithm.name !== algorithm.name) {\n            throw new Error(\"algorithm does not match\");\n        }\n        if (typeof key.decrypt !== \"function\") {\n            throw new Error(\"key does not support encryption\");\n        }\n        return key.decrypt(algorithm, data);\n    }\n\n    importKey(format, keyData, algorithm, extractable, keyUsages) {\n        if (format !== \"raw\") {\n            throw new Error(\"key format is not supported\");\n        }\n        const alg = this._algorithms[algorithm.name];\n        if (typeof alg === \"undefined\" || typeof alg.importKey !== \"function\") {\n            throw new Error(\"algorithm is not supported\");\n        }\n        return alg.importKey(keyData, algorithm, extractable, keyUsages);\n    }\n\n    generateKey(algorithm, extractable, keyUsages) {\n        const alg = this._algorithms[algorithm.name];\n        if (typeof alg === \"undefined\" || typeof alg.generateKey !== \"function\") {\n            throw new Error(\"algorithm is not supported\");\n        }\n        return alg.generateKey(algorithm, extractable, keyUsages);\n    }\n\n    exportKey(format, key) {\n        if (format !== \"raw\") {\n            throw new Error(\"key format is not supported\");\n        }\n        if (typeof key.exportKey !== \"function\") {\n            throw new Error(\"key does not support exportKey\");\n        }\n        return key.exportKey();\n    }\n\n    digest(algorithm, data) {\n        const alg = this._algorithms[algorithm];\n        if (typeof alg !== \"function\") {\n            throw new Error(\"algorithm is not supported\");\n        }\n        return alg(data);\n    }\n\n    deriveBits(algorithm, key, length) {\n        if (key.algorithm.name !== algorithm.name) {\n            throw new Error(\"algorithm does not match\");\n        }\n        if (typeof key.deriveBits !== \"function\") {\n            throw new Error(\"key does not support deriveBits\");\n        }\n        return key.deriveBits(algorithm, length);\n    }\n}\n\nexport default new LegacyCrypto;\n"
  },
  {
    "path": "core/crypto/des.js",
    "content": "/*\n * Ported from Flashlight VNC ActionScript implementation:\n *     http://www.wizhelp.com/flashlight-vnc/\n *\n * Full attribution follows:\n *\n * -------------------------------------------------------------------------\n *\n * This DES class has been extracted from package Acme.Crypto for use in VNC.\n * The unnecessary odd parity code has been removed.\n *\n * These changes are:\n *  Copyright (C) 1999 AT&T Laboratories Cambridge.  All Rights Reserved.\n *\n * This software is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n *\n\n * DesCipher - the DES encryption method\n *\n * The meat of this code is by Dave Zimmerman <dzimm@widget.com>, and is:\n *\n * Copyright (c) 1996 Widget Workshop, Inc. All Rights Reserved.\n *\n * Permission to use, copy, modify, and distribute this software\n * and its documentation for NON-COMMERCIAL or COMMERCIAL purposes and\n * without fee is hereby granted, provided that this copyright notice is kept\n * intact.\n *\n * WIDGET WORKSHOP MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY\n * OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED\n * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\n * PARTICULAR PURPOSE, OR NON-INFRINGEMENT. WIDGET WORKSHOP SHALL NOT BE LIABLE\n * FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR\n * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.\n *\n * THIS SOFTWARE IS NOT DESIGNED OR INTENDED FOR USE OR RESALE AS ON-LINE\n * CONTROL EQUIPMENT IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE\n * PERFORMANCE, SUCH AS IN THE OPERATION OF NUCLEAR FACILITIES, AIRCRAFT\n * NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, DIRECT LIFE\n * SUPPORT MACHINES, OR WEAPONS SYSTEMS, IN WHICH THE FAILURE OF THE\n * SOFTWARE COULD LEAD DIRECTLY TO DEATH, PERSONAL INJURY, OR SEVERE\n * PHYSICAL OR ENVIRONMENTAL DAMAGE (\"HIGH RISK ACTIVITIES\").  WIDGET WORKSHOP\n * SPECIFICALLY DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY OF FITNESS FOR\n * HIGH RISK ACTIVITIES.\n *\n *\n * The rest is:\n *\n * Copyright (C) 1996 by Jef Poskanzer <jef@acme.com>.  All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n *    notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n *    notice, this list of conditions and the following disclaimer in the\n *    documentation and/or other materials provided with the distribution.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND\n * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE\n * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\n * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\n * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY\n * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF\n * SUCH DAMAGE.\n *\n * Visit the ACME Labs Java page for up-to-date versions of this and other\n * fine Java utilities: http://www.acme.com/java/\n */\n\n/* eslint-disable comma-spacing */\n\n// Tables, permutations, S-boxes, etc.\nconst PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3,\n             25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39,\n             50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ],\n      totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28];\n\nconst z = 0x0;\nlet a,b,c,d,e,f;\na=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e;\nconst 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,\n             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,\n             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,\n             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];\na=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e;\nconst 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,\n             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,\n             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,\n             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];\na=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e;\nconst 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,\n             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,\n             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,\n             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];\na=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e;\nconst 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,\n             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,\n             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,\n             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];\na=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e;\nconst 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,\n             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,\n             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,\n             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];\na=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e;\nconst 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,\n             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,\n             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,\n             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];\na=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e;\nconst 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,\n             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,\n             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,\n             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];\na=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e;\nconst 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,\n             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,\n             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,\n             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];\n\n/* eslint-enable comma-spacing */\n\nclass DES {\n    constructor(password) {\n        this.keys = [];\n\n        // Set the key.\n        const pc1m = [], pcr = [], kn = [];\n\n        for (let j = 0, l = 56; j < 56; ++j, l -= 8) {\n            l += l < -5 ? 65 : l < -3 ? 31 : l < -1 ? 63 : l === 27 ? 35 : 0; // PC1\n            const m = l & 0x7;\n            pc1m[j] = ((password[l >>> 3] & (1<<m)) !== 0) ? 1: 0;\n        }\n\n        for (let i = 0; i < 16; ++i) {\n            const m = i << 1;\n            const n = m + 1;\n            kn[m] = kn[n] = 0;\n            for (let o = 28; o < 59; o += 28) {\n                for (let j = o - 28; j < o; ++j) {\n                    const l = j + totrot[i];\n                    pcr[j] = l < o ? pc1m[l] : pc1m[l - 28];\n                }\n            }\n            for (let j = 0; j < 24; ++j) {\n                if (pcr[PC2[j]] !== 0) {\n                    kn[m] |= 1 << (23 - j);\n                }\n                if (pcr[PC2[j + 24]] !== 0) {\n                    kn[n] |= 1 << (23 - j);\n                }\n            }\n        }\n\n        // cookey\n        for (let i = 0, rawi = 0, KnLi = 0; i < 16; ++i) {\n            const raw0 = kn[rawi++];\n            const raw1 = kn[rawi++];\n            this.keys[KnLi] = (raw0 & 0x00fc0000) << 6;\n            this.keys[KnLi] |= (raw0 & 0x00000fc0) << 10;\n            this.keys[KnLi] |= (raw1 & 0x00fc0000) >>> 10;\n            this.keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6;\n            ++KnLi;\n            this.keys[KnLi] = (raw0 & 0x0003f000) << 12;\n            this.keys[KnLi] |= (raw0 & 0x0000003f) << 16;\n            this.keys[KnLi] |= (raw1 & 0x0003f000) >>> 4;\n            this.keys[KnLi] |= (raw1 & 0x0000003f);\n            ++KnLi;\n        }\n    }\n\n    // Encrypt 8 bytes of text\n    enc8(text) {\n        const b = text.slice();\n        let l, r, x; // left, right, accumulator\n\n        // Squash 8 bytes to 2 ints\n        l = b[0]<<24 | b[1]<<16 | b[2]<<8 | b[3];\n        r = b[4]<<24 | b[5]<<16 | b[6]<<8 | b[7];\n\n        x = ((l >>> 4) ^ r) & 0x0f0f0f0f;\n        r ^= x;\n        l ^= (x << 4);\n        x = ((l >>> 16) ^ r) & 0x0000ffff;\n        r ^= x;\n        l ^= (x << 16);\n        x = ((r >>> 2) ^ l) & 0x33333333;\n        l ^= x;\n        r ^= (x << 2);\n        x = ((r >>> 8) ^ l) & 0x00ff00ff;\n        l ^= x;\n        r ^= (x << 8);\n        r = (r << 1) | ((r >>> 31) & 1);\n        x = (l ^ r) & 0xaaaaaaaa;\n        l ^= x;\n        r ^= x;\n        l = (l << 1) | ((l >>> 31) & 1);\n\n        for (let i = 0, keysi = 0; i < 8; ++i) {\n            x = (r << 28) | (r >>> 4);\n            x ^= this.keys[keysi++];\n            let fval =  SP7[x & 0x3f];\n            fval |= SP5[(x >>> 8) & 0x3f];\n            fval |= SP3[(x >>> 16) & 0x3f];\n            fval |= SP1[(x >>> 24) & 0x3f];\n            x = r ^ this.keys[keysi++];\n            fval |= SP8[x & 0x3f];\n            fval |= SP6[(x >>> 8) & 0x3f];\n            fval |= SP4[(x >>> 16) & 0x3f];\n            fval |= SP2[(x >>> 24) & 0x3f];\n            l ^= fval;\n            x = (l << 28) | (l >>> 4);\n            x ^= this.keys[keysi++];\n            fval =  SP7[x & 0x3f];\n            fval |= SP5[(x >>> 8) & 0x3f];\n            fval |= SP3[(x >>> 16) & 0x3f];\n            fval |= SP1[(x >>> 24) & 0x3f];\n            x = l ^ this.keys[keysi++];\n            fval |= SP8[x & 0x0000003f];\n            fval |= SP6[(x >>> 8) & 0x3f];\n            fval |= SP4[(x >>> 16) & 0x3f];\n            fval |= SP2[(x >>> 24) & 0x3f];\n            r ^= fval;\n        }\n\n        r = (r << 31) | (r >>> 1);\n        x = (l ^ r) & 0xaaaaaaaa;\n        l ^= x;\n        r ^= x;\n        l = (l << 31) | (l >>> 1);\n        x = ((l >>> 8) ^ r) & 0x00ff00ff;\n        r ^= x;\n        l ^= (x << 8);\n        x = ((l >>> 2) ^ r) & 0x33333333;\n        r ^= x;\n        l ^= (x << 2);\n        x = ((r >>> 16) ^ l) & 0x0000ffff;\n        l ^= x;\n        r ^= (x << 16);\n        x = ((r >>> 4) ^ l) & 0x0f0f0f0f;\n        l ^= x;\n        r ^= (x << 4);\n\n        // Spread ints to bytes\n        x = [r, l];\n        for (let i = 0; i < 8; i++) {\n            b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256;\n            if (b[i] < 0) { b[i] += 256; } // unsigned\n        }\n        return b;\n    }\n}\n\nexport class DESECBCipher {\n    constructor() {\n        this._cipher = null;\n    }\n\n    get algorithm() {\n        return { name: \"DES-ECB\" };\n    }\n\n    static importKey(key, _algorithm, _extractable, _keyUsages) {\n        const cipher = new DESECBCipher;\n        cipher._importKey(key);\n        return cipher;\n    }\n\n    _importKey(key, _extractable, _keyUsages) {\n        this._cipher = new DES(key);\n    }\n\n    encrypt(_algorithm, plaintext) {\n        const x = new Uint8Array(plaintext);\n        if (x.length % 8 !== 0 || this._cipher === null) {\n            return null;\n        }\n        const n = x.length / 8;\n        for (let i = 0; i < n; i++) {\n            x.set(this._cipher.enc8(x.slice(i * 8, i * 8 + 8)), i * 8);\n        }\n        return x;\n    }\n}\n\nexport class DESCBCCipher {\n    constructor() {\n        this._cipher = null;\n    }\n\n    get algorithm() {\n        return { name: \"DES-CBC\" };\n    }\n\n    static importKey(key, _algorithm, _extractable, _keyUsages) {\n        const cipher = new DESCBCCipher;\n        cipher._importKey(key);\n        return cipher;\n    }\n\n    _importKey(key) {\n        this._cipher = new DES(key);\n    }\n\n    encrypt(algorithm, plaintext) {\n        const x = new Uint8Array(plaintext);\n        let y = new Uint8Array(algorithm.iv);\n        if (x.length % 8 !== 0 || this._cipher === null) {\n            return null;\n        }\n        const n = x.length / 8;\n        for (let i = 0; i < n; i++) {\n            for (let j = 0; j < 8; j++) {\n                y[j] ^= plaintext[i * 8 + j];\n            }\n            y = this._cipher.enc8(y);\n            x.set(y, i * 8);\n        }\n        return x;\n    }\n}\n"
  },
  {
    "path": "core/crypto/dh.js",
    "content": "import { modPow, bigIntToU8Array, u8ArrayToBigInt } from \"./bigint.js\";\n\nclass DHPublicKey {\n    constructor(key) {\n        this._key = key;\n    }\n\n    get algorithm() {\n        return { name: \"DH\" };\n    }\n\n    exportKey() {\n        return this._key;\n    }\n}\n\nexport class DHCipher {\n    constructor() {\n        this._g = null;\n        this._p = null;\n        this._gBigInt = null;\n        this._pBigInt = null;\n        this._privateKey = null;\n    }\n\n    get algorithm() {\n        return { name: \"DH\" };\n    }\n\n    static generateKey(algorithm, _extractable) {\n        const cipher = new DHCipher;\n        cipher._generateKey(algorithm);\n        return { privateKey: cipher, publicKey: new DHPublicKey(cipher._publicKey) };\n    }\n\n    _generateKey(algorithm) {\n        const g = algorithm.g;\n        const p = algorithm.p;\n        this._keyBytes = p.length;\n        this._gBigInt = u8ArrayToBigInt(g);\n        this._pBigInt = u8ArrayToBigInt(p);\n        this._privateKey = window.crypto.getRandomValues(new Uint8Array(this._keyBytes));\n        this._privateKeyBigInt = u8ArrayToBigInt(this._privateKey);\n        this._publicKey = bigIntToU8Array(modPow(\n            this._gBigInt, this._privateKeyBigInt, this._pBigInt), this._keyBytes);\n    }\n\n    deriveBits(algorithm, length) {\n        const bytes = Math.ceil(length / 8);\n        const pkey = new Uint8Array(algorithm.public);\n        const len = bytes > this._keyBytes ? bytes : this._keyBytes;\n        const secret = modPow(u8ArrayToBigInt(pkey), this._privateKeyBigInt, this._pBigInt);\n        return bigIntToU8Array(secret, len).slice(0, len);\n    }\n}\n"
  },
  {
    "path": "core/crypto/md5.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2021 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n */\n\n/*\n * Performs MD5 hashing on an array of bytes, returns an array of bytes\n */\n\nexport async function MD5(d) {\n    let s = \"\";\n    for (let i = 0; i < d.length; i++) {\n        s += String.fromCharCode(d[i]);\n    }\n    return M(V(Y(X(s), 8 * s.length)));\n}\n\nfunction M(d) {\n    let f = new Uint8Array(d.length);\n    for (let i=0;i<d.length;i++) {\n        f[i] = d.charCodeAt(i);\n    }\n    return f;\n}\n\nfunction X(d) {\n    let r = Array(d.length >> 2);\n    for (let m = 0; m < r.length; m++) r[m] = 0;\n    for (let m = 0; m < 8 * d.length; m += 8) r[m >> 5] |= (255 & d.charCodeAt(m / 8)) << m % 32;\n    return r;\n}\n\nfunction V(d) {\n    let r = \"\";\n    for (let m = 0; m < 32 * d.length; m += 8) r += String.fromCharCode(d[m >> 5] >>> m % 32 & 255);\n    return r;\n}\n\nfunction Y(d, g) {\n    d[g >> 5] |= 128 << g % 32, d[14 + (g + 64 >>> 9 << 4)] = g;\n    let m = 1732584193, f = -271733879, r = -1732584194, i = 271733878;\n    for (let n = 0; n < d.length; n += 16) {\n        let h = m,\n            t = f,\n            g = r,\n            e = i;\n        f = ii(f = ii(f = ii(f = ii(f = hh(f = hh(f = hh(f = hh(f = gg(f = gg(f = gg(f = gg(f = ff(f = ff(f = ff(f = ff(f, r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 0], 7, -680876936), f, r, d[n + 1], 12, -389564586), m, f, d[n + 2], 17, 606105819), i, m, d[n + 3], 22, -1044525330), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 4], 7, -176418897), f, r, d[n + 5], 12, 1200080426), m, f, d[n + 6], 17, -1473231341), i, m, d[n + 7], 22, -45705983), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 8], 7, 1770035416), f, r, d[n + 9], 12, -1958414417), m, f, d[n + 10], 17, -42063), i, m, d[n + 11], 22, -1990404162), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 12], 7, 1804603682), f, r, d[n + 13], 12, -40341101), m, f, d[n + 14], 17, -1502002290), i, m, d[n + 15], 22, 1236535329), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 1], 5, -165796510), f, r, d[n + 6], 9, -1069501632), m, f, d[n + 11], 14, 643717713), i, m, d[n + 0], 20, -373897302), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 5], 5, -701558691), f, r, d[n + 10], 9, 38016083), m, f, d[n + 15], 14, -660478335), i, m, d[n + 4], 20, -405537848), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 9], 5, 568446438), f, r, d[n + 14], 9, -1019803690), m, f, d[n + 3], 14, -187363961), i, m, d[n + 8], 20, 1163531501), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 13], 5, -1444681467), f, r, d[n + 2], 9, -51403784), m, f, d[n + 7], 14, 1735328473), i, m, d[n + 12], 20, -1926607734), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 5], 4, -378558), f, r, d[n + 8], 11, -2022574463), m, f, d[n + 11], 16, 1839030562), i, m, d[n + 14], 23, -35309556), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 1], 4, -1530992060), f, r, d[n + 4], 11, 1272893353), m, f, d[n + 7], 16, -155497632), i, m, d[n + 10], 23, -1094730640), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 13], 4, 681279174), f, r, d[n + 0], 11, -358537222), m, f, d[n + 3], 16, -722521979), i, m, d[n + 6], 23, 76029189), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 9], 4, -640364487), f, r, d[n + 12], 11, -421815835), m, f, d[n + 15], 16, 530742520), i, m, d[n + 2], 23, -995338651), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 0], 6, -198630844), f, r, d[n + 7], 10, 1126891415), m, f, d[n + 14], 15, -1416354905), i, m, d[n + 5], 21, -57434055), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 12], 6, 1700485571), f, r, d[n + 3], 10, -1894986606), m, f, d[n + 10], 15, -1051523), i, m, d[n + 1], 21, -2054922799), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 8], 6, 1873313359), f, r, d[n + 15], 10, -30611744), m, f, d[n + 6], 15, -1560198380), i, m, d[n + 13], 21, 1309151649), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 4], 6, -145523070), f, r, d[n + 11], 10, -1120210379), m, f, d[n + 2], 15, 718787259), i, m, d[n + 9], 21, -343485551), m = add(m, h), f = add(f, t), r = add(r, g), i = add(i, e);\n    }\n    return Array(m, f, r, i);\n}\n\nfunction cmn(d, g, m, f, r, i) {\n    return add(rol(add(add(g, d), add(f, i)), r), m);\n}\n\nfunction ff(d, g, m, f, r, i, n) {\n    return cmn(g & m | ~g & f, d, g, r, i, n);\n}\n\nfunction gg(d, g, m, f, r, i, n) {\n    return cmn(g & f | m & ~f, d, g, r, i, n);\n}\n\nfunction hh(d, g, m, f, r, i, n) {\n    return cmn(g ^ m ^ f, d, g, r, i, n);\n}\n\nfunction ii(d, g, m, f, r, i, n) {\n    return cmn(m ^ (g | ~f), d, g, r, i, n);\n}\n\nfunction add(d, g) {\n    let m = (65535 & d) + (65535 & g);\n    return (d >> 16) + (g >> 16) + (m >> 16) << 16 | 65535 & m;\n}\n\nfunction rol(d, g) {\n    return d << g | d >>> 32 - g;\n}\n"
  },
  {
    "path": "core/crypto/rsa.js",
    "content": "import Base64 from \"../base64.js\";\nimport { modPow, bigIntToU8Array, u8ArrayToBigInt } from \"./bigint.js\";\n\nexport class RSACipher {\n    constructor() {\n        this._keyLength = 0;\n        this._keyBytes = 0;\n        this._n = null;\n        this._e = null;\n        this._d = null;\n        this._nBigInt = null;\n        this._eBigInt = null;\n        this._dBigInt = null;\n        this._extractable = false;\n    }\n\n    get algorithm() {\n        return { name: \"RSA-PKCS1-v1_5\" };\n    }\n\n    _base64urlDecode(data) {\n        data = data.replace(/-/g, \"+\").replace(/_/g, \"/\");\n        data = data.padEnd(Math.ceil(data.length / 4) * 4, \"=\");\n        return Base64.decode(data);\n    }\n\n    _padArray(arr, length) {\n        const res = new Uint8Array(length);\n        res.set(arr, length - arr.length);\n        return res;\n    }\n\n    static async generateKey(algorithm, extractable, _keyUsages) {\n        const cipher = new RSACipher;\n        await cipher._generateKey(algorithm, extractable);\n        return { privateKey: cipher };\n    }\n\n    async _generateKey(algorithm, extractable) {\n        this._keyLength = algorithm.modulusLength;\n        this._keyBytes = Math.ceil(this._keyLength / 8);\n        const key = await window.crypto.subtle.generateKey(\n            {\n                name: \"RSA-OAEP\",\n                modulusLength: algorithm.modulusLength,\n                publicExponent: algorithm.publicExponent,\n                hash: {name: \"SHA-256\"},\n            },\n            true, [\"encrypt\", \"decrypt\"]);\n        const privateKey = await window.crypto.subtle.exportKey(\"jwk\", key.privateKey);\n        this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes);\n        this._nBigInt = u8ArrayToBigInt(this._n);\n        this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes);\n        this._eBigInt = u8ArrayToBigInt(this._e);\n        this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes);\n        this._dBigInt = u8ArrayToBigInt(this._d);\n        this._extractable = extractable;\n    }\n\n    static async importKey(key, _algorithm, extractable, keyUsages) {\n        if (keyUsages.length !== 1 || keyUsages[0] !== \"encrypt\") {\n            throw new Error(\"only support importing RSA public key\");\n        }\n        const cipher = new RSACipher;\n        await cipher._importKey(key, extractable);\n        return cipher;\n    }\n\n    async _importKey(key, extractable) {\n        const n = key.n;\n        const e = key.e;\n        if (n.length !== e.length) {\n            throw new Error(\"the sizes of modulus and public exponent do not match\");\n        }\n        this._keyBytes = n.length;\n        this._keyLength = this._keyBytes * 8;\n        this._n = new Uint8Array(this._keyBytes);\n        this._e = new Uint8Array(this._keyBytes);\n        this._n.set(n);\n        this._e.set(e);\n        this._nBigInt = u8ArrayToBigInt(this._n);\n        this._eBigInt = u8ArrayToBigInt(this._e);\n        this._extractable = extractable;\n    }\n\n    async encrypt(_algorithm, message) {\n        if (message.length > this._keyBytes - 11) {\n            return null;\n        }\n        const ps = new Uint8Array(this._keyBytes - message.length - 3);\n        window.crypto.getRandomValues(ps);\n        for (let i = 0; i < ps.length; i++) {\n            ps[i] = Math.floor(ps[i] * 254 / 255 + 1);\n        }\n        const em = new Uint8Array(this._keyBytes);\n        em[1] = 0x02;\n        em.set(ps, 2);\n        em.set(message, ps.length + 3);\n        const emBigInt = u8ArrayToBigInt(em);\n        const c = modPow(emBigInt, this._eBigInt, this._nBigInt);\n        return bigIntToU8Array(c, this._keyBytes);\n    }\n\n    async decrypt(_algorithm, message) {\n        if (message.length !== this._keyBytes) {\n            return null;\n        }\n        const msgBigInt = u8ArrayToBigInt(message);\n        const emBigInt = modPow(msgBigInt, this._dBigInt, this._nBigInt);\n        const em = bigIntToU8Array(emBigInt, this._keyBytes);\n        if (em[0] !== 0x00 || em[1] !== 0x02) {\n            return null;\n        }\n        let i = 2;\n        for (; i < em.length; i++) {\n            if (em[i] === 0x00) {\n                break;\n            }\n        }\n        if (i === em.length) {\n            return null;\n        }\n        return em.slice(i + 1, em.length);\n    }\n\n    async exportKey() {\n        if (!this._extractable) {\n            throw new Error(\"key is not extractable\");\n        }\n        return { n: this._n, e: this._e, d: this._d };\n    }\n}\n"
  },
  {
    "path": "core/decoders/copyrect.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2019 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n *\n */\n\nexport default class CopyRectDecoder {\n    decodeRect(x, y, width, height, sock, display, depth) {\n        if (sock.rQwait(\"COPYRECT\", 4)) {\n            return false;\n        }\n\n        let deltaX = sock.rQshift16();\n        let deltaY = sock.rQshift16();\n\n        if ((width === 0) || (height === 0)) {\n            return true;\n        }\n\n        display.copyImage(deltaX, deltaY, x, y, width, height);\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "core/decoders/h264.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2024 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n *\n */\n\nimport * as Log from '../util/logging.js';\n\nexport class H264Parser {\n    constructor(data) {\n        this._data = data;\n        this._index = 0;\n        this.profileIdc = null;\n        this.constraintSet = null;\n        this.levelIdc = null;\n    }\n\n    _getStartSequenceLen(index) {\n        let data = this._data;\n        if (data[index + 0] == 0 && data[index + 1] == 0 && data[index + 2] == 0 && data[index + 3] == 1) {\n            return 4;\n        }\n        if (data[index + 0] == 0 && data[index + 1] == 0 && data[index + 2] == 1) {\n            return 3;\n        }\n        return 0;\n    }\n\n    _indexOfNextNalUnit(index) {\n        let data = this._data;\n        for (let i = index; i < data.length; ++i) {\n            if (this._getStartSequenceLen(i) != 0) {\n                return i;\n            }\n        }\n        return -1;\n    }\n\n    _parseSps(index) {\n        this.profileIdc = this._data[index];\n        this.constraintSet = this._data[index + 1];\n        this.levelIdc = this._data[index + 2];\n    }\n\n    _parseNalUnit(index) {\n        const firstByte = this._data[index];\n        if (firstByte & 0x80) {\n            throw new Error('H264 parsing sanity check failed, forbidden zero bit is set');\n        }\n        const unitType = firstByte & 0x1f;\n\n        switch (unitType) {\n            case 1: // coded slice, non-idr\n                return { slice: true };\n            case 5: // coded slice, idr\n                return { slice: true, key: true };\n            case 6: // sei\n                return {};\n            case 7: // sps\n                this._parseSps(index + 1);\n                return {};\n            case 8: // pps\n                return {};\n            default:\n                Log.Warn(\"Unhandled unit type: \", unitType);\n                break;\n        }\n        return {};\n    }\n\n    parse() {\n        const startIndex = this._index;\n        let isKey = false;\n\n        while (this._index < this._data.length) {\n            const startSequenceLen = this._getStartSequenceLen(this._index);\n            if (startSequenceLen == 0) {\n                throw new Error('Invalid start sequence in bit stream');\n            }\n\n            const { slice, key } = this._parseNalUnit(this._index + startSequenceLen);\n\n            let nextIndex = this._indexOfNextNalUnit(this._index + startSequenceLen);\n            if (nextIndex == -1) {\n                this._index = this._data.length;\n            } else {\n                this._index = nextIndex;\n            }\n\n            if (key) {\n                isKey = true;\n            }\n            if (slice) {\n                break;\n            }\n        }\n\n        if (startIndex === this._index) {\n            return null;\n        }\n\n        return {\n            frame: this._data.subarray(startIndex, this._index),\n            key: isKey,\n        };\n    }\n}\n\nexport class H264Context {\n    constructor(width, height) {\n        this.lastUsed = 0;\n        this._width = width;\n        this._height = height;\n        this._profileIdc = null;\n        this._constraintSet = null;\n        this._levelIdc = null;\n        this._decoder = null;\n        this._pendingFrames = [];\n    }\n\n    _handleFrame(frame) {\n        let pending = this._pendingFrames.shift();\n        if (pending === undefined) {\n            throw new Error(\"Pending frame queue empty when receiving frame from decoder\");\n        }\n\n        if (pending.timestamp != frame.timestamp) {\n            throw new Error(\"Video frame timestamp mismatch. Expected \" +\n                frame.timestamp + \" but but got \" + pending.timestamp);\n        }\n\n        pending.frame = frame;\n        pending.ready = true;\n        pending.resolve();\n\n        if (!pending.keep) {\n            frame.close();\n        }\n    }\n\n    _handleError(e) {\n        throw new Error(\"Failed to decode frame: \" + e.message);\n    }\n\n    _configureDecoder(profileIdc, constraintSet, levelIdc) {\n        if (this._decoder === null || this._decoder.state === 'closed') {\n            this._decoder = new VideoDecoder({\n                output: frame => this._handleFrame(frame),\n                error: e => this._handleError(e),\n            });\n        }\n        const codec = 'avc1.' +\n            profileIdc.toString(16).padStart(2, '0') +\n            constraintSet.toString(16).padStart(2, '0') +\n            levelIdc.toString(16).padStart(2, '0');\n        this._decoder.configure({\n            codec: codec,\n            codedWidth: this._width,\n            codedHeight: this._height,\n            optimizeForLatency: true,\n        });\n    }\n\n    _preparePendingFrame(timestamp) {\n        let pending = {\n            timestamp: timestamp,\n            promise: null,\n            resolve: null,\n            frame: null,\n            ready: false,\n            keep: false,\n        };\n        pending.promise = new Promise((resolve) => {\n            pending.resolve = resolve;\n        });\n        this._pendingFrames.push(pending);\n\n        return pending;\n    }\n\n    decode(payload) {\n        let parser = new H264Parser(payload);\n        let result = null;\n\n        // Ideally, this timestamp should come from the server, but we'll just\n        // approximate it instead.\n        let timestamp = Math.round(window.performance.now() * 1e3);\n\n        while (true) {\n            let encodedFrame = parser.parse();\n            if (encodedFrame === null) {\n                break;\n            }\n\n            if (parser.profileIdc !== null) {\n                self._profileIdc = parser.profileIdc;\n                self._constraintSet = parser.constraintSet;\n                self._levelIdc = parser.levelIdc;\n            }\n\n            if (this._decoder === null || this._decoder.state !== 'configured') {\n                if (!encodedFrame.key) {\n                    Log.Warn(\"Missing key frame. Can't decode until one arrives\");\n                    continue;\n                }\n                if (self._profileIdc === null) {\n                    Log.Warn('Cannot config decoder. Have not received SPS and PPS yet.');\n                    continue;\n                }\n                this._configureDecoder(self._profileIdc, self._constraintSet,\n                                       self._levelIdc);\n            }\n\n            result = this._preparePendingFrame(timestamp);\n\n            const chunk = new EncodedVideoChunk({\n                timestamp: timestamp,\n                type: encodedFrame.key ? 'key' : 'delta',\n                data: encodedFrame.frame,\n            });\n\n            try {\n                this._decoder.decode(chunk);\n            } catch (e) {\n                Log.Warn(\"Failed to decode:\", e);\n            }\n        }\n\n        // We only keep last frame of each payload\n        if (result !== null) {\n            result.keep = true;\n        }\n\n        return result;\n    }\n}\n\nexport default class H264Decoder {\n    constructor() {\n        this._tick = 0;\n        this._contexts = {};\n    }\n\n    _contextId(x, y, width, height) {\n        return [x, y, width, height].join(',');\n    }\n\n    _findOldestContextId() {\n        let oldestTick = Number.MAX_VALUE;\n        let oldestKey = undefined;\n        for (const [key, value] of Object.entries(this._contexts)) {\n            if (value.lastUsed < oldestTick) {\n                oldestTick = value.lastUsed;\n                oldestKey = key;\n            }\n        }\n        return oldestKey;\n    }\n\n    _createContext(x, y, width, height) {\n        const maxContexts = 64;\n        if (Object.keys(this._contexts).length >= maxContexts) {\n            let oldestContextId = this._findOldestContextId();\n            delete this._contexts[oldestContextId];\n        }\n        let context = new H264Context(width, height);\n        this._contexts[this._contextId(x, y, width, height)] = context;\n        return context;\n    }\n\n    _getContext(x, y, width, height) {\n        let context = this._contexts[this._contextId(x, y, width, height)];\n        return context !== undefined ? context : this._createContext(x, y, width, height);\n    }\n\n    _resetContext(x, y, width, height) {\n        delete this._contexts[this._contextId(x, y, width, height)];\n    }\n\n    _resetAllContexts() {\n        this._contexts = {};\n    }\n\n    decodeRect(x, y, width, height, sock, display, depth) {\n        const resetContextFlag = 1;\n        const resetAllContextsFlag = 2;\n\n        if (sock.rQwait(\"h264 header\", 8)) {\n            return false;\n        }\n\n        const length = sock.rQshift32();\n        const flags = sock.rQshift32();\n\n        if (sock.rQwait(\"h264 payload\", length, 8)) {\n            return false;\n        }\n\n        if (flags & resetAllContextsFlag) {\n            this._resetAllContexts();\n        } else if (flags & resetContextFlag) {\n            this._resetContext(x, y, width, height);\n        }\n\n        let context = this._getContext(x, y, width, height);\n        context.lastUsed = this._tick++;\n\n        if (length !== 0) {\n            let payload = sock.rQshiftBytes(length, false);\n            let frame = context.decode(payload);\n            if (frame !== null) {\n                display.videoFrame(x, y, width, height, frame);\n            }\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "core/decoders/hextile.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2019 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n *\n */\n\nimport * as Log from '../util/logging.js';\n\nexport default class HextileDecoder {\n    constructor() {\n        this._tiles = 0;\n        this._lastsubencoding = 0;\n        this._tileBuffer = new Uint8Array(16 * 16 * 4);\n    }\n\n    decodeRect(x, y, width, height, sock, display, depth) {\n        if (this._tiles === 0) {\n            this._tilesX = Math.ceil(width / 16);\n            this._tilesY = Math.ceil(height / 16);\n            this._totalTiles = this._tilesX * this._tilesY;\n            this._tiles = this._totalTiles;\n        }\n\n        while (this._tiles > 0) {\n            let bytes = 1;\n\n            if (sock.rQwait(\"HEXTILE\", bytes)) {\n                return false;\n            }\n\n            let subencoding = sock.rQpeek8();\n            if (subencoding > 30) {  // Raw\n                throw new Error(\"Illegal hextile subencoding (subencoding: \" +\n                            subencoding + \")\");\n            }\n\n            const currTile = this._totalTiles - this._tiles;\n            const tileX = currTile % this._tilesX;\n            const tileY = Math.floor(currTile / this._tilesX);\n            const tx = x + tileX * 16;\n            const ty = y + tileY * 16;\n            const tw = Math.min(16, (x + width) - tx);\n            const th = Math.min(16, (y + height) - ty);\n\n            // Figure out how much we are expecting\n            if (subencoding & 0x01) {  // Raw\n                bytes += tw * th * 4;\n            } else {\n                if (subencoding & 0x02) {  // Background\n                    bytes += 4;\n                }\n                if (subencoding & 0x04) {  // Foreground\n                    bytes += 4;\n                }\n                if (subencoding & 0x08) {  // AnySubrects\n                    bytes++;  // Since we aren't shifting it off\n\n                    if (sock.rQwait(\"HEXTILE\", bytes)) {\n                        return false;\n                    }\n\n                    let subrects = sock.rQpeekBytes(bytes).at(-1);\n                    if (subencoding & 0x10) {  // SubrectsColoured\n                        bytes += subrects * (4 + 2);\n                    } else {\n                        bytes += subrects * 2;\n                    }\n                }\n            }\n\n            if (sock.rQwait(\"HEXTILE\", bytes)) {\n                return false;\n            }\n\n            // We know the encoding and have a whole tile\n            sock.rQshift8();\n            if (subencoding === 0) {\n                if (this._lastsubencoding & 0x01) {\n                    // Weird: ignore blanks are RAW\n                    Log.Debug(\"     Ignoring blank after RAW\");\n                } else {\n                    display.fillRect(tx, ty, tw, th, this._background);\n                }\n            } else if (subencoding & 0x01) {  // Raw\n                let pixels = tw * th;\n                let data = sock.rQshiftBytes(pixels * 4, false);\n                // Max sure the image is fully opaque\n                for (let i = 0;i <  pixels;i++) {\n                    data[i * 4 + 3] = 255;\n                }\n                display.blitImage(tx, ty, tw, th, data, 0);\n            } else {\n                if (subencoding & 0x02) {  // Background\n                    this._background = new Uint8Array(sock.rQshiftBytes(4));\n                }\n                if (subencoding & 0x04) {  // Foreground\n                    this._foreground = new Uint8Array(sock.rQshiftBytes(4));\n                }\n\n                this._startTile(tx, ty, tw, th, this._background);\n                if (subencoding & 0x08) {  // AnySubrects\n                    let subrects = sock.rQshift8();\n\n                    for (let s = 0; s < subrects; s++) {\n                        let color;\n                        if (subencoding & 0x10) {  // SubrectsColoured\n                            color = sock.rQshiftBytes(4);\n                        } else {\n                            color = this._foreground;\n                        }\n                        const xy = sock.rQshift8();\n                        const sx = (xy >> 4);\n                        const sy = (xy & 0x0f);\n\n                        const wh = sock.rQshift8();\n                        const sw = (wh >> 4) + 1;\n                        const sh = (wh & 0x0f) + 1;\n\n                        this._subTile(sx, sy, sw, sh, color);\n                    }\n                }\n                this._finishTile(display);\n            }\n            this._lastsubencoding = subencoding;\n            this._tiles--;\n        }\n\n        return true;\n    }\n\n    // start updating a tile\n    _startTile(x, y, width, height, color) {\n        this._tileX = x;\n        this._tileY = y;\n        this._tileW = width;\n        this._tileH = height;\n\n        const red = color[0];\n        const green = color[1];\n        const blue = color[2];\n\n        const data = this._tileBuffer;\n        for (let i = 0; i < width * height * 4; i += 4) {\n            data[i]     = red;\n            data[i + 1] = green;\n            data[i + 2] = blue;\n            data[i + 3] = 255;\n        }\n    }\n\n    // update sub-rectangle of the current tile\n    _subTile(x, y, w, h, color) {\n        const red = color[0];\n        const green = color[1];\n        const blue = color[2];\n        const xend = x + w;\n        const yend = y + h;\n\n        const data = this._tileBuffer;\n        const width = this._tileW;\n        for (let j = y; j < yend; j++) {\n            for (let i = x; i < xend; i++) {\n                const p = (i + (j * width)) * 4;\n                data[p]     = red;\n                data[p + 1] = green;\n                data[p + 2] = blue;\n                data[p + 3] = 255;\n            }\n        }\n    }\n\n    // draw the current tile to the screen\n    _finishTile(display) {\n        display.blitImage(this._tileX, this._tileY,\n                          this._tileW, this._tileH,\n                          this._tileBuffer, 0);\n    }\n}\n"
  },
  {
    "path": "core/decoders/jpeg.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2019 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n *\n */\n\nexport default class JPEGDecoder {\n    constructor() {\n        // RealVNC will reuse the quantization tables\n        // and Huffman tables, so we need to cache them.\n        this._cachedQuantTables = [];\n        this._cachedHuffmanTables = [];\n\n        this._segments = [];\n    }\n\n    decodeRect(x, y, width, height, sock, display, depth) {\n        // A rect of JPEG encodings is simply a JPEG file\n        while (true) {\n            let segment = this._readSegment(sock);\n            if (segment === null) {\n                return false;\n            }\n            this._segments.push(segment);\n            // End of image?\n            if (segment[1] === 0xD9) {\n                break;\n            }\n        }\n\n        let huffmanTables = [];\n        let quantTables = [];\n        for (let segment of this._segments) {\n            let type = segment[1];\n            if (type === 0xC4) {\n                // Huffman tables\n                huffmanTables.push(segment);\n            } else if (type === 0xDB) {\n                // Quantization tables\n                quantTables.push(segment);\n            }\n        }\n\n        const sofIndex = this._segments.findIndex(\n            x => x[1] == 0xC0 || x[1] == 0xC2\n        );\n        if (sofIndex == -1) {\n            throw new Error(\"Illegal JPEG image without SOF\");\n        }\n\n        if (quantTables.length === 0) {\n            this._segments.splice(sofIndex+1, 0,\n                                  ...this._cachedQuantTables);\n        }\n        if (huffmanTables.length === 0) {\n            this._segments.splice(sofIndex+1, 0,\n                                  ...this._cachedHuffmanTables);\n        }\n\n        let length = 0;\n        for (let segment of this._segments) {\n            length += segment.length;\n        }\n\n        let data = new Uint8Array(length);\n        length = 0;\n        for (let segment of this._segments) {\n            data.set(segment, length);\n            length += segment.length;\n        }\n\n        display.imageRect(x, y, width, height, \"image/jpeg\", data);\n\n        if (huffmanTables.length !== 0) {\n            this._cachedHuffmanTables = huffmanTables;\n        }\n        if (quantTables.length !== 0) {\n            this._cachedQuantTables = quantTables;\n        }\n\n        this._segments = [];\n\n        return true;\n    }\n\n    _readSegment(sock) {\n        if (sock.rQwait(\"JPEG\", 2)) {\n            return null;\n        }\n\n        let marker = sock.rQshift8();\n        if (marker != 0xFF) {\n            throw new Error(\"Illegal JPEG marker received (byte: \" +\n                               marker + \")\");\n        }\n        let type = sock.rQshift8();\n        if (type >= 0xD0 && type <= 0xD9 || type == 0x01) {\n            // No length after marker\n            return new Uint8Array([marker, type]);\n        }\n\n        if (sock.rQwait(\"JPEG\", 2, 2)) {\n            return null;\n        }\n\n        let length = sock.rQshift16();\n        if (length < 2) {\n            throw new Error(\"Illegal JPEG length received (length: \" +\n                               length + \")\");\n        }\n\n        if (sock.rQwait(\"JPEG\", length-2, 4)) {\n            return null;\n        }\n\n        let extra = 0;\n        if (type === 0xDA) {\n            // start of scan\n            if (sock.rQwait(\"JPEG\", length-2 + 2, 4)) {\n                return null;\n            }\n\n            let len = sock.rQlen();\n            let data = sock.rQpeekBytes(len, false);\n\n            while (true) {\n                let idx = data.indexOf(0xFF, length-2+extra);\n                if (idx === -1) {\n                    sock.rQwait(\"JPEG\", Infinity, 4);\n                    return null;\n                }\n\n                if (idx === len-1) {\n                    sock.rQwait(\"JPEG\", Infinity, 4);\n                    return null;\n                }\n\n                if (data.at(idx+1) === 0x00 ||\n                    (data.at(idx+1) >= 0xD0 && data.at(idx+1) <= 0xD7)) {\n                    extra = idx+2 - (length-2);\n                    continue;\n                }\n\n                extra = idx - (length-2);\n                break;\n            }\n        }\n\n        let segment = new Uint8Array(2 + length + extra);\n        segment[0] = marker;\n        segment[1] = type;\n        segment[2] = length >> 8;\n        segment[3] = length;\n        segment.set(sock.rQshiftBytes(length-2+extra, false), 4);\n\n        return segment;\n    }\n}\n"
  },
  {
    "path": "core/decoders/raw.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2019 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n *\n */\n\nexport default class RawDecoder {\n    constructor() {\n        this._lines = 0;\n    }\n\n    decodeRect(x, y, width, height, sock, display, depth) {\n        if ((width === 0) || (height === 0)) {\n            return true;\n        }\n\n        if (this._lines === 0) {\n            this._lines = height;\n        }\n\n        const pixelSize = depth == 8 ? 1 : 4;\n        const bytesPerLine = width * pixelSize;\n\n        while (this._lines > 0) {\n            if (sock.rQwait(\"RAW\", bytesPerLine)) {\n                return false;\n            }\n\n            const curY = y + (height - this._lines);\n\n            let data = sock.rQshiftBytes(bytesPerLine, false);\n\n            // Convert data if needed\n            if (depth == 8) {\n                const newdata = new Uint8Array(width * 4);\n                for (let i = 0; i < width; i++) {\n                    newdata[i * 4 + 0] = ((data[i] >> 0) & 0x3) * 255 / 3;\n                    newdata[i * 4 + 1] = ((data[i] >> 2) & 0x3) * 255 / 3;\n                    newdata[i * 4 + 2] = ((data[i] >> 4) & 0x3) * 255 / 3;\n                    newdata[i * 4 + 3] = 255;\n                }\n                data = newdata;\n            }\n\n            // Max sure the image is fully opaque\n            for (let i = 0; i < width; i++) {\n                data[i * 4 + 3] = 255;\n            }\n\n            display.blitImage(x, curY, width, 1, data, 0);\n            this._lines--;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "core/decoders/rre.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2019 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n *\n */\n\nexport default class RREDecoder {\n    constructor() {\n        this._subrects = 0;\n    }\n\n    decodeRect(x, y, width, height, sock, display, depth) {\n        if (this._subrects === 0) {\n            if (sock.rQwait(\"RRE\", 4 + 4)) {\n                return false;\n            }\n\n            this._subrects = sock.rQshift32();\n\n            let color = sock.rQshiftBytes(4);  // Background\n            display.fillRect(x, y, width, height, color);\n        }\n\n        while (this._subrects > 0) {\n            if (sock.rQwait(\"RRE\", 4 + 8)) {\n                return false;\n            }\n\n            let color = sock.rQshiftBytes(4);\n            let sx = sock.rQshift16();\n            let sy = sock.rQshift16();\n            let swidth = sock.rQshift16();\n            let sheight = sock.rQshift16();\n            display.fillRect(x + sx, y + sy, swidth, sheight, color);\n\n            this._subrects--;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "core/decoders/tight.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2019 The noVNC authors\n * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca)\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n *\n */\n\nimport * as Log from '../util/logging.js';\nimport Inflator from \"../inflator.js\";\n\nexport default class TightDecoder {\n    constructor() {\n        this._ctl = null;\n        this._filter = null;\n        this._numColors = 0;\n        this._palette = new Uint8Array(1024);  // 256 * 4 (max palette size * max bytes-per-pixel)\n        this._len = 0;\n\n        this._zlibs = [];\n        for (let i = 0; i < 4; i++) {\n            this._zlibs[i] = new Inflator();\n        }\n    }\n\n    decodeRect(x, y, width, height, sock, display, depth) {\n        if (this._ctl === null) {\n            if (sock.rQwait(\"TIGHT compression-control\", 1)) {\n                return false;\n            }\n\n            this._ctl = sock.rQshift8();\n\n            // Reset streams if the server requests it\n            for (let i = 0; i < 4; i++) {\n                if ((this._ctl >> i) & 1) {\n                    this._zlibs[i].reset();\n                    Log.Info(\"Reset zlib stream \" + i);\n                }\n            }\n\n            // Figure out filter\n            this._ctl = this._ctl >> 4;\n        }\n\n        let ret;\n\n        if (this._ctl === 0x08) {\n            ret = this._fillRect(x, y, width, height,\n                                 sock, display, depth);\n        } else if (this._ctl === 0x09) {\n            ret = this._jpegRect(x, y, width, height,\n                                 sock, display, depth);\n        } else if (this._ctl === 0x0A) {\n            ret = this._pngRect(x, y, width, height,\n                                sock, display, depth);\n        } else if ((this._ctl & 0x08) == 0) {\n            ret = this._basicRect(this._ctl, x, y, width, height,\n                                  sock, display, depth);\n        } else {\n            throw new Error(\"Illegal tight compression received (ctl: \" +\n                                   this._ctl + \")\");\n        }\n\n        if (ret) {\n            this._ctl = null;\n        }\n\n        return ret;\n    }\n\n    _fillRect(x, y, width, height, sock, display, depth) {\n        if (sock.rQwait(\"TIGHT\", 3)) {\n            return false;\n        }\n\n        let pixel = sock.rQshiftBytes(3);\n        display.fillRect(x, y, width, height, pixel, false);\n\n        return true;\n    }\n\n    _jpegRect(x, y, width, height, sock, display, depth) {\n        let data = this._readData(sock);\n        if (data === null) {\n            return false;\n        }\n\n        display.imageRect(x, y, width, height, \"image/jpeg\", data);\n\n        return true;\n    }\n\n    _pngRect(x, y, width, height, sock, display, depth) {\n        throw new Error(\"PNG received in standard Tight rect\");\n    }\n\n    _basicRect(ctl, x, y, width, height, sock, display, depth) {\n        if (this._filter === null) {\n            if (ctl & 0x4) {\n                if (sock.rQwait(\"TIGHT\", 1)) {\n                    return false;\n                }\n\n                this._filter = sock.rQshift8();\n            } else {\n                // Implicit CopyFilter\n                this._filter = 0;\n            }\n        }\n\n        let streamId = ctl & 0x3;\n\n        let ret;\n\n        switch (this._filter) {\n            case 0: // CopyFilter\n                ret = this._copyFilter(streamId, x, y, width, height,\n                                       sock, display, depth);\n                break;\n            case 1: // PaletteFilter\n                ret = this._paletteFilter(streamId, x, y, width, height,\n                                          sock, display, depth);\n                break;\n            case 2: // GradientFilter\n                ret = this._gradientFilter(streamId, x, y, width, height,\n                                           sock, display, depth);\n                break;\n            default:\n                throw new Error(\"Illegal tight filter received (ctl: \" +\n                                       this._filter + \")\");\n        }\n\n        if (ret) {\n            this._filter = null;\n        }\n\n        return ret;\n    }\n\n    _copyFilter(streamId, x, y, width, height, sock, display, depth) {\n        const uncompressedSize = width * height * 3;\n        let data;\n\n        if (uncompressedSize === 0) {\n            return true;\n        }\n\n        if (uncompressedSize < 12) {\n            if (sock.rQwait(\"TIGHT\", uncompressedSize)) {\n                return false;\n            }\n\n            data = sock.rQshiftBytes(uncompressedSize);\n        } else {\n            data = this._readData(sock);\n            if (data === null) {\n                return false;\n            }\n\n            this._zlibs[streamId].setInput(data);\n            data = this._zlibs[streamId].inflate(uncompressedSize);\n            this._zlibs[streamId].setInput(null);\n        }\n\n        let rgbx = new Uint8Array(width * height * 4);\n        for (let i = 0, j = 0; i < width * height * 4; i += 4, j += 3) {\n            rgbx[i]     = data[j];\n            rgbx[i + 1] = data[j + 1];\n            rgbx[i + 2] = data[j + 2];\n            rgbx[i + 3] = 255;  // Alpha\n        }\n\n        display.blitImage(x, y, width, height, rgbx, 0, false);\n\n        return true;\n    }\n\n    _paletteFilter(streamId, x, y, width, height, sock, display, depth) {\n        if (this._numColors === 0) {\n            if (sock.rQwait(\"TIGHT palette\", 1)) {\n                return false;\n            }\n\n            const numColors = sock.rQpeek8() + 1;\n            const paletteSize = numColors * 3;\n\n            if (sock.rQwait(\"TIGHT palette\", 1 + paletteSize)) {\n                return false;\n            }\n\n            this._numColors = numColors;\n            sock.rQskipBytes(1);\n\n            sock.rQshiftTo(this._palette, paletteSize);\n        }\n\n        const bpp = (this._numColors <= 2) ? 1 : 8;\n        const rowSize = Math.floor((width * bpp + 7) / 8);\n        const uncompressedSize = rowSize * height;\n\n        let data;\n\n        if (uncompressedSize === 0) {\n            return true;\n        }\n\n        if (uncompressedSize < 12) {\n            if (sock.rQwait(\"TIGHT\", uncompressedSize)) {\n                return false;\n            }\n\n            data = sock.rQshiftBytes(uncompressedSize);\n        } else {\n            data = this._readData(sock);\n            if (data === null) {\n                return false;\n            }\n\n            this._zlibs[streamId].setInput(data);\n            data = this._zlibs[streamId].inflate(uncompressedSize);\n            this._zlibs[streamId].setInput(null);\n        }\n\n        // Convert indexed (palette based) image data to RGB\n        if (this._numColors == 2) {\n            this._monoRect(x, y, width, height, data, this._palette, display);\n        } else {\n            this._paletteRect(x, y, width, height, data, this._palette, display);\n        }\n\n        this._numColors = 0;\n\n        return true;\n    }\n\n    _monoRect(x, y, width, height, data, palette, display) {\n        // Convert indexed (palette based) image data to RGB\n        // TODO: reduce number of calculations inside loop\n        const dest = this._getScratchBuffer(width * height * 4);\n        const w = Math.floor((width + 7) / 8);\n        const w1 = Math.floor(width / 8);\n\n        for (let y = 0; y < height; y++) {\n            let dp, sp, x;\n            for (x = 0; x < w1; x++) {\n                for (let b = 7; b >= 0; b--) {\n                    dp = (y * width + x * 8 + 7 - b) * 4;\n                    sp = (data[y * w + x] >> b & 1) * 3;\n                    dest[dp]     = palette[sp];\n                    dest[dp + 1] = palette[sp + 1];\n                    dest[dp + 2] = palette[sp + 2];\n                    dest[dp + 3] = 255;\n                }\n            }\n\n            for (let b = 7; b >= 8 - width % 8; b--) {\n                dp = (y * width + x * 8 + 7 - b) * 4;\n                sp = (data[y * w + x] >> b & 1) * 3;\n                dest[dp]     = palette[sp];\n                dest[dp + 1] = palette[sp + 1];\n                dest[dp + 2] = palette[sp + 2];\n                dest[dp + 3] = 255;\n            }\n        }\n\n        display.blitImage(x, y, width, height, dest, 0, false);\n    }\n\n    _paletteRect(x, y, width, height, data, palette, display) {\n        // Convert indexed (palette based) image data to RGB\n        const dest = this._getScratchBuffer(width * height * 4);\n        const total = width * height * 4;\n        for (let i = 0, j = 0; i < total; i += 4, j++) {\n            const sp = data[j] * 3;\n            dest[i]     = palette[sp];\n            dest[i + 1] = palette[sp + 1];\n            dest[i + 2] = palette[sp + 2];\n            dest[i + 3] = 255;\n        }\n\n        display.blitImage(x, y, width, height, dest, 0, false);\n    }\n\n    _gradientFilter(streamId, x, y, width, height, sock, display, depth) {\n        // assume the TPIXEL is 3 bytes long\n        const uncompressedSize = width * height * 3;\n        let data;\n\n        if (uncompressedSize === 0) {\n            return true;\n        }\n\n        if (uncompressedSize < 12) {\n            if (sock.rQwait(\"TIGHT\", uncompressedSize)) {\n                return false;\n            }\n\n            data = sock.rQshiftBytes(uncompressedSize);\n        } else {\n            data = this._readData(sock);\n            if (data === null) {\n                return false;\n            }\n\n            this._zlibs[streamId].setInput(data);\n            data = this._zlibs[streamId].inflate(uncompressedSize);\n            this._zlibs[streamId].setInput(null);\n        }\n\n        let rgbx = new Uint8Array(4 * width * height);\n\n        let rgbxIndex = 0, dataIndex = 0;\n        let left = new Uint8Array(3);\n        for (let x = 0; x < width; x++) {\n            for (let c = 0; c < 3; c++) {\n                const prediction = left[c];\n                const value = data[dataIndex++] + prediction;\n                rgbx[rgbxIndex++] = value;\n                left[c] = value;\n            }\n            rgbx[rgbxIndex++] = 255;\n        }\n\n        let upperIndex = 0;\n        let upper = new Uint8Array(3),\n            upperleft = new Uint8Array(3);\n        for (let y = 1; y < height; y++) {\n            left.fill(0);\n            upperleft.fill(0);\n            for (let x = 0; x < width; x++) {\n                for (let c = 0; c < 3; c++) {\n                    upper[c] = rgbx[upperIndex++];\n                    let prediction = left[c] + upper[c] - upperleft[c];\n                    if (prediction < 0) {\n                        prediction = 0;\n                    } else if (prediction > 255) {\n                        prediction = 255;\n                    }\n                    const value = data[dataIndex++] + prediction;\n                    rgbx[rgbxIndex++] = value;\n                    upperleft[c] = upper[c];\n                    left[c] = value;\n                }\n                rgbx[rgbxIndex++] = 255;\n                upperIndex++;\n            }\n        }\n\n        display.blitImage(x, y, width, height, rgbx, 0, false);\n\n        return true;\n    }\n\n    _readData(sock) {\n        if (this._len === 0) {\n            if (sock.rQwait(\"TIGHT\", 3)) {\n                return null;\n            }\n\n            let byte;\n\n            byte = sock.rQshift8();\n            this._len = byte & 0x7f;\n            if (byte & 0x80) {\n                byte = sock.rQshift8();\n                this._len |= (byte & 0x7f) << 7;\n                if (byte & 0x80) {\n                    byte = sock.rQshift8();\n                    this._len |= byte << 14;\n                }\n            }\n        }\n\n        if (sock.rQwait(\"TIGHT\", this._len)) {\n            return null;\n        }\n\n        let data = sock.rQshiftBytes(this._len, false);\n        this._len = 0;\n\n        return data;\n    }\n\n    _getScratchBuffer(size) {\n        if (!this._scratchBuffer || (this._scratchBuffer.length < size)) {\n            this._scratchBuffer = new Uint8Array(size);\n        }\n        return this._scratchBuffer;\n    }\n}\n"
  },
  {
    "path": "core/decoders/tightpng.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2019 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n *\n */\n\nimport TightDecoder from './tight.js';\n\nexport default class TightPNGDecoder extends TightDecoder {\n    _pngRect(x, y, width, height, sock, display, depth) {\n        let data = this._readData(sock);\n        if (data === null) {\n            return false;\n        }\n\n        display.imageRect(x, y, width, height, \"image/png\", data);\n\n        return true;\n    }\n\n    _basicRect(ctl, x, y, width, height, sock, display, depth) {\n        throw new Error(\"BasicCompression received in TightPNG rect\");\n    }\n}\n"
  },
  {
    "path": "core/decoders/zlib.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2024 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n *\n */\n\nimport Inflator from \"../inflator.js\";\n\nexport default class ZlibDecoder {\n    constructor() {\n        this._zlib = new Inflator();\n        this._length = 0;\n    }\n\n    decodeRect(x, y, width, height, sock, display, depth) {\n        if ((width === 0) || (height === 0)) {\n            return true;\n        }\n\n        if (this._length === 0) {\n            if (sock.rQwait(\"ZLIB\", 4)) {\n                return false;\n            }\n\n            this._length = sock.rQshift32();\n        }\n\n        if (sock.rQwait(\"ZLIB\", this._length)) {\n            return false;\n        }\n\n        let data = new Uint8Array(sock.rQshiftBytes(this._length, false));\n        this._length = 0;\n\n        this._zlib.setInput(data);\n        data = this._zlib.inflate(width * height * 4);\n        this._zlib.setInput(null);\n\n        // Max sure the image is fully opaque\n        for (let i = 0; i < width * height; i++) {\n            data[i * 4 + 3] = 255;\n        }\n\n        display.blitImage(x, y, width, height, data, 0);\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "core/decoders/zrle.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2021 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n *\n */\n\nimport Inflate from \"../inflator.js\";\n\nconst ZRLE_TILE_WIDTH = 64;\nconst ZRLE_TILE_HEIGHT = 64;\n\nexport default class ZRLEDecoder {\n    constructor() {\n        this._length = 0;\n        this._inflator = new Inflate();\n\n        this._pixelBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 4);\n        this._tileBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 4);\n    }\n\n    decodeRect(x, y, width, height, sock, display, depth) {\n        if (this._length === 0) {\n            if (sock.rQwait(\"ZLib data length\", 4)) {\n                return false;\n            }\n            this._length = sock.rQshift32();\n        }\n        if (sock.rQwait(\"Zlib data\", this._length)) {\n            return false;\n        }\n\n        const data = sock.rQshiftBytes(this._length, false);\n\n        this._inflator.setInput(data);\n\n        for (let ty = y; ty < y + height; ty += ZRLE_TILE_HEIGHT) {\n            let th = Math.min(ZRLE_TILE_HEIGHT, y + height - ty);\n\n            for (let tx = x; tx < x + width; tx += ZRLE_TILE_WIDTH) {\n                let tw = Math.min(ZRLE_TILE_WIDTH, x + width - tx);\n\n                const tileSize = tw * th;\n                const subencoding = this._inflator.inflate(1)[0];\n                if (subencoding === 0) {\n                    // raw data\n                    const data = this._readPixels(tileSize);\n                    display.blitImage(tx, ty, tw, th, data, 0, false);\n                } else if (subencoding === 1) {\n                    // solid\n                    const background = this._readPixels(1);\n                    display.fillRect(tx, ty, tw, th, [background[0], background[1], background[2]]);\n                } else if (subencoding >= 2 && subencoding <= 16) {\n                    const data = this._decodePaletteTile(subencoding, tileSize, tw, th);\n                    display.blitImage(tx, ty, tw, th, data, 0, false);\n                } else if (subencoding === 128) {\n                    const data = this._decodeRLETile(tileSize);\n                    display.blitImage(tx, ty, tw, th, data, 0, false);\n                } else if (subencoding >= 130 && subencoding <= 255) {\n                    const data = this._decodeRLEPaletteTile(subencoding - 128, tileSize);\n                    display.blitImage(tx, ty, tw, th, data, 0, false);\n                } else {\n                    throw new Error('Unknown subencoding: ' + subencoding);\n                }\n            }\n        }\n        this._length = 0;\n        return true;\n    }\n\n    _getBitsPerPixelInPalette(paletteSize) {\n        if (paletteSize <= 2) {\n            return 1;\n        } else if (paletteSize <= 4) {\n            return 2;\n        } else if (paletteSize <= 16) {\n            return 4;\n        }\n    }\n\n    _readPixels(pixels) {\n        let data = this._pixelBuffer;\n        const buffer = this._inflator.inflate(3*pixels);\n        for (let i = 0, j = 0; i < pixels*4; i += 4, j += 3) {\n            data[i]     = buffer[j];\n            data[i + 1] = buffer[j + 1];\n            data[i + 2] = buffer[j + 2];\n            data[i + 3] = 255;  // Add the Alpha\n        }\n        return data;\n    }\n\n    _decodePaletteTile(paletteSize, tileSize, tilew, tileh) {\n        const data = this._tileBuffer;\n        const palette = this._readPixels(paletteSize);\n        const bitsPerPixel = this._getBitsPerPixelInPalette(paletteSize);\n        const mask = (1 << bitsPerPixel) - 1;\n\n        let offset = 0;\n        let encoded = this._inflator.inflate(1)[0];\n\n        for (let y=0; y<tileh; y++) {\n            let shift = 8-bitsPerPixel;\n            for (let x=0; x<tilew; x++) {\n                if (shift<0) {\n                    shift=8-bitsPerPixel;\n                    encoded = this._inflator.inflate(1)[0];\n                }\n                let indexInPalette = (encoded>>shift) & mask;\n\n                data[offset] = palette[indexInPalette * 4];\n                data[offset + 1] = palette[indexInPalette * 4 + 1];\n                data[offset + 2] = palette[indexInPalette * 4 + 2];\n                data[offset + 3] = palette[indexInPalette * 4 + 3];\n                offset += 4;\n                shift-=bitsPerPixel;\n            }\n            if (shift<8-bitsPerPixel && y<tileh-1) {\n                encoded =  this._inflator.inflate(1)[0];\n            }\n        }\n        return data;\n    }\n\n    _decodeRLETile(tileSize) {\n        const data = this._tileBuffer;\n        let i = 0;\n        while (i < tileSize) {\n            const pixel = this._readPixels(1);\n            const length = this._readRLELength();\n            for (let j = 0; j < length; j++) {\n                data[i * 4] = pixel[0];\n                data[i * 4 + 1] = pixel[1];\n                data[i * 4 + 2] = pixel[2];\n                data[i * 4 + 3] = pixel[3];\n                i++;\n            }\n        }\n        return data;\n    }\n\n    _decodeRLEPaletteTile(paletteSize, tileSize) {\n        const data = this._tileBuffer;\n\n        // palette\n        const palette = this._readPixels(paletteSize);\n\n        let offset = 0;\n        while (offset < tileSize) {\n            let indexInPalette = this._inflator.inflate(1)[0];\n            let length = 1;\n            if (indexInPalette >= 128) {\n                indexInPalette -= 128;\n                length = this._readRLELength();\n            }\n            if (indexInPalette > paletteSize) {\n                throw new Error('Too big index in palette: ' + indexInPalette + ', palette size: ' + paletteSize);\n            }\n            if (offset + length > tileSize) {\n                throw new Error('Too big rle length in palette mode: ' + length + ', allowed length is: ' + (tileSize - offset));\n            }\n\n            for (let j = 0; j < length; j++) {\n                data[offset * 4] = palette[indexInPalette * 4];\n                data[offset * 4 + 1] = palette[indexInPalette * 4 + 1];\n                data[offset * 4 + 2] = palette[indexInPalette * 4 + 2];\n                data[offset * 4 + 3] = palette[indexInPalette * 4 + 3];\n                offset++;\n            }\n        }\n        return data;\n    }\n\n    _readRLELength() {\n        let length = 0;\n        let current;\n        do {\n            current = this._inflator.inflate(1)[0];\n            length += current;\n        } while (current === 255);\n        return length + 1;\n    }\n}\n"
  },
  {
    "path": "core/deflator.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2020 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n */\n\nimport { deflateInit, deflate } from \"../vendor/pako/lib/zlib/deflate.js\";\nimport { Z_FULL_FLUSH, Z_DEFAULT_COMPRESSION } from \"../vendor/pako/lib/zlib/deflate.js\";\nimport ZStream from \"../vendor/pako/lib/zlib/zstream.js\";\n\nexport default class Deflator {\n    constructor() {\n        this.strm = new ZStream();\n        this.chunkSize = 1024 * 10 * 10;\n        this.outputBuffer = new Uint8Array(this.chunkSize);\n\n        deflateInit(this.strm, Z_DEFAULT_COMPRESSION);\n    }\n\n    deflate(inData) {\n        /* eslint-disable camelcase */\n        this.strm.input = inData;\n        this.strm.avail_in = this.strm.input.length;\n        this.strm.next_in = 0;\n        this.strm.output = this.outputBuffer;\n        this.strm.avail_out = this.chunkSize;\n        this.strm.next_out = 0;\n        /* eslint-enable camelcase */\n\n        let lastRet = deflate(this.strm, Z_FULL_FLUSH);\n        let outData = new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);\n\n        if (lastRet < 0) {\n            throw new Error(\"zlib deflate failed\");\n        }\n\n        if (this.strm.avail_in > 0) {\n            // Read chunks until done\n\n            let chunks = [outData];\n            let totalLen = outData.length;\n            do {\n                /* eslint-disable camelcase */\n                this.strm.output = new Uint8Array(this.chunkSize);\n                this.strm.next_out = 0;\n                this.strm.avail_out = this.chunkSize;\n                /* eslint-enable camelcase */\n\n                lastRet = deflate(this.strm, Z_FULL_FLUSH);\n\n                if (lastRet < 0) {\n                    throw new Error(\"zlib deflate failed\");\n                }\n\n                let chunk = new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);\n                totalLen += chunk.length;\n                chunks.push(chunk);\n            } while (this.strm.avail_in > 0);\n\n            // Combine chunks into a single data\n\n            let newData = new Uint8Array(totalLen);\n            let offset = 0;\n\n            for (let i = 0; i < chunks.length; i++) {\n                newData.set(chunks[i], offset);\n                offset += chunks[i].length;\n            }\n\n            outData = newData;\n        }\n\n        /* eslint-disable camelcase */\n        this.strm.input = null;\n        this.strm.avail_in = 0;\n        this.strm.next_in = 0;\n        /* eslint-enable camelcase */\n\n        return outData;\n    }\n\n}\n"
  },
  {
    "path": "core/display.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2019 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n */\n\nimport * as Log from './util/logging.js';\nimport Base64 from \"./base64.js\";\nimport { toSigned32bit } from './util/int.js';\n\nexport default class Display {\n    constructor(target) {\n        this._drawCtx = null;\n\n        this._renderQ = [];  // queue drawing actions for in-order rendering\n        this._flushPromise = null;\n\n        // the full frame buffer (logical canvas) size\n        this._fbWidth = 0;\n        this._fbHeight = 0;\n\n        this._prevDrawStyle = \"\";\n\n        Log.Debug(\">> Display.constructor\");\n\n        // The visible canvas\n        this._target = target;\n\n        if (!this._target) {\n            throw new Error(\"Target must be set\");\n        }\n\n        if (typeof this._target === 'string') {\n            throw new Error('target must be a DOM element');\n        }\n\n        if (!this._target.getContext) {\n            throw new Error(\"no getContext method\");\n        }\n\n        this._targetCtx = this._target.getContext('2d');\n\n        // the visible canvas viewport (i.e. what actually gets seen)\n        this._viewportLoc = { 'x': 0, 'y': 0, 'w': this._target.width, 'h': this._target.height };\n\n        // The hidden canvas, where we do the actual rendering\n        this._backbuffer = document.createElement('canvas');\n        this._drawCtx = this._backbuffer.getContext('2d');\n\n        this._damageBounds = { left: 0, top: 0,\n                               right: this._backbuffer.width,\n                               bottom: this._backbuffer.height };\n\n        Log.Debug(\"User Agent: \" + navigator.userAgent);\n\n        Log.Debug(\"<< Display.constructor\");\n\n        // ===== PROPERTIES =====\n\n        this._scale = 1.0;\n        this._clipViewport = false;\n    }\n\n    // ===== PROPERTIES =====\n\n    get scale() { return this._scale; }\n    set scale(scale) {\n        this._rescale(scale);\n    }\n\n    get clipViewport() { return this._clipViewport; }\n    set clipViewport(viewport) {\n        this._clipViewport = viewport;\n        // May need to readjust the viewport dimensions\n        const vp = this._viewportLoc;\n        this.viewportChangeSize(vp.w, vp.h);\n        this.viewportChangePos(0, 0);\n    }\n\n    get width() {\n        return this._fbWidth;\n    }\n\n    get height() {\n        return this._fbHeight;\n    }\n\n    // ===== PUBLIC METHODS =====\n\n    viewportChangePos(deltaX, deltaY) {\n        const vp = this._viewportLoc;\n        deltaX = Math.floor(deltaX);\n        deltaY = Math.floor(deltaY);\n\n        if (!this._clipViewport) {\n            deltaX = -vp.w;  // clamped later of out of bounds\n            deltaY = -vp.h;\n        }\n\n        const vx2 = vp.x + vp.w - 1;\n        const vy2 = vp.y + vp.h - 1;\n\n        // Position change\n\n        if (deltaX < 0 && vp.x + deltaX < 0) {\n            deltaX = -vp.x;\n        }\n        if (vx2 + deltaX >= this._fbWidth) {\n            deltaX -= vx2 + deltaX - this._fbWidth + 1;\n        }\n\n        if (vp.y + deltaY < 0) {\n            deltaY = -vp.y;\n        }\n        if (vy2 + deltaY >= this._fbHeight) {\n            deltaY -= (vy2 + deltaY - this._fbHeight + 1);\n        }\n\n        if (deltaX === 0 && deltaY === 0) {\n            return;\n        }\n        Log.Debug(\"viewportChange deltaX: \" + deltaX + \", deltaY: \" + deltaY);\n\n        vp.x += deltaX;\n        vp.y += deltaY;\n\n        this._damage(vp.x, vp.y, vp.w, vp.h);\n\n        this.flip();\n    }\n\n    viewportChangeSize(width, height) {\n\n        if (!this._clipViewport ||\n            typeof(width) === \"undefined\" ||\n            typeof(height) === \"undefined\") {\n\n            Log.Debug(\"Setting viewport to full display region\");\n            width = this._fbWidth;\n            height = this._fbHeight;\n        }\n\n        width = Math.floor(width);\n        height = Math.floor(height);\n\n        if (width > this._fbWidth) {\n            width = this._fbWidth;\n        }\n        if (height > this._fbHeight) {\n            height = this._fbHeight;\n        }\n\n        const vp = this._viewportLoc;\n        if (vp.w !== width || vp.h !== height) {\n            vp.w = width;\n            vp.h = height;\n\n            const canvas = this._target;\n            canvas.width = width;\n            canvas.height = height;\n\n            // The position might need to be updated if we've grown\n            this.viewportChangePos(0, 0);\n\n            this._damage(vp.x, vp.y, vp.w, vp.h);\n            this.flip();\n\n            // Update the visible size of the target canvas\n            this._rescale(this._scale);\n        }\n    }\n\n    absX(x) {\n        if (this._scale === 0) {\n            return 0;\n        }\n        return toSigned32bit(x / this._scale + this._viewportLoc.x);\n    }\n\n    absY(y) {\n        if (this._scale === 0) {\n            return 0;\n        }\n        return toSigned32bit(y / this._scale + this._viewportLoc.y);\n    }\n\n    resize(width, height) {\n        this._prevDrawStyle = \"\";\n\n        this._fbWidth = width;\n        this._fbHeight = height;\n\n        const canvas = this._backbuffer;\n        if (canvas.width !== width || canvas.height !== height) {\n\n            // We have to save the canvas data since changing the size will clear it\n            let saveImg = null;\n            if (canvas.width > 0 && canvas.height > 0) {\n                saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height);\n            }\n\n            if (canvas.width !== width) {\n                canvas.width = width;\n            }\n            if (canvas.height !== height) {\n                canvas.height = height;\n            }\n\n            if (saveImg) {\n                this._drawCtx.putImageData(saveImg, 0, 0);\n            }\n        }\n\n        // Readjust the viewport as it may be incorrectly sized\n        // and positioned\n        const vp = this._viewportLoc;\n        this.viewportChangeSize(vp.w, vp.h);\n        this.viewportChangePos(0, 0);\n    }\n\n    getImageData() {\n        return this._drawCtx.getImageData(0, 0, this.width, this.height);\n    }\n\n    toDataURL(type, encoderOptions) {\n        return this._backbuffer.toDataURL(type, encoderOptions);\n    }\n\n    toBlob(callback, type, quality) {\n        return this._backbuffer.toBlob(callback, type, quality);\n    }\n\n    // Track what parts of the visible canvas that need updating\n    _damage(x, y, w, h) {\n        if (x < this._damageBounds.left) {\n            this._damageBounds.left = x;\n        }\n        if (y < this._damageBounds.top) {\n            this._damageBounds.top = y;\n        }\n        if ((x + w) > this._damageBounds.right) {\n            this._damageBounds.right = x + w;\n        }\n        if ((y + h) > this._damageBounds.bottom) {\n            this._damageBounds.bottom = y + h;\n        }\n    }\n\n    // Update the visible canvas with the contents of the\n    // rendering canvas\n    flip(fromQueue) {\n        if (this._renderQ.length !== 0 && !fromQueue) {\n            this._renderQPush({\n                'type': 'flip'\n            });\n        } else {\n            let x = this._damageBounds.left;\n            let y = this._damageBounds.top;\n            let w = this._damageBounds.right - x;\n            let h = this._damageBounds.bottom - y;\n\n            let vx = x - this._viewportLoc.x;\n            let vy = y - this._viewportLoc.y;\n\n            if (vx < 0) {\n                w += vx;\n                x -= vx;\n                vx = 0;\n            }\n            if (vy < 0) {\n                h += vy;\n                y -= vy;\n                vy = 0;\n            }\n\n            if ((vx + w) > this._viewportLoc.w) {\n                w = this._viewportLoc.w - vx;\n            }\n            if ((vy + h) > this._viewportLoc.h) {\n                h = this._viewportLoc.h - vy;\n            }\n\n            if ((w > 0) && (h > 0)) {\n                // FIXME: We may need to disable image smoothing here\n                //        as well (see copyImage()), but we haven't\n                //        noticed any problem yet.\n                this._targetCtx.drawImage(this._backbuffer,\n                                          x, y, w, h,\n                                          vx, vy, w, h);\n            }\n\n            this._damageBounds.left = this._damageBounds.top = 65535;\n            this._damageBounds.right = this._damageBounds.bottom = 0;\n        }\n    }\n\n    pending() {\n        return this._renderQ.length > 0;\n    }\n\n    flush() {\n        if (this._renderQ.length === 0) {\n            return Promise.resolve();\n        } else {\n            if (this._flushPromise === null) {\n                this._flushPromise = new Promise((resolve) => {\n                    this._flushResolve = resolve;\n                });\n            }\n            return this._flushPromise;\n        }\n    }\n\n    fillRect(x, y, width, height, color, fromQueue) {\n        if (this._renderQ.length !== 0 && !fromQueue) {\n            this._renderQPush({\n                'type': 'fill',\n                'x': x,\n                'y': y,\n                'width': width,\n                'height': height,\n                'color': color\n            });\n        } else {\n            this._setFillColor(color);\n            this._drawCtx.fillRect(x, y, width, height);\n            this._damage(x, y, width, height);\n        }\n    }\n\n    copyImage(oldX, oldY, newX, newY, w, h, fromQueue) {\n        if (this._renderQ.length !== 0 && !fromQueue) {\n            this._renderQPush({\n                'type': 'copy',\n                'oldX': oldX,\n                'oldY': oldY,\n                'x': newX,\n                'y': newY,\n                'width': w,\n                'height': h,\n            });\n        } else {\n            // Due to this bug among others [1] we need to disable the image-smoothing to\n            // avoid getting a blur effect when copying data.\n            //\n            // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719\n            //\n            // We need to set these every time since all properties are reset\n            // when the the size is changed\n            this._drawCtx.mozImageSmoothingEnabled = false;\n            this._drawCtx.webkitImageSmoothingEnabled = false;\n            this._drawCtx.msImageSmoothingEnabled = false;\n            this._drawCtx.imageSmoothingEnabled = false;\n\n            this._drawCtx.drawImage(this._backbuffer,\n                                    oldX, oldY, w, h,\n                                    newX, newY, w, h);\n            this._damage(newX, newY, w, h);\n        }\n    }\n\n    imageRect(x, y, width, height, mime, arr) {\n        /* The internal logic cannot handle empty images, so bail early */\n        if ((width === 0) || (height === 0)) {\n            return;\n        }\n\n        const img = new Image();\n        img.src = \"data: \" + mime + \";base64,\" + Base64.encode(arr);\n\n        this._renderQPush({\n            'type': 'img',\n            'img': img,\n            'x': x,\n            'y': y,\n            'width': width,\n            'height': height\n        });\n    }\n\n    videoFrame(x, y, width, height, frame) {\n        this._renderQPush({\n            'type': 'frame',\n            'frame': frame,\n            'x': x,\n            'y': y,\n            'width': width,\n            'height': height\n        });\n    }\n\n    blitImage(x, y, width, height, arr, offset, fromQueue) {\n        if (this._renderQ.length !== 0 && !fromQueue) {\n            // NB(directxman12): it's technically more performant here to use preallocated arrays,\n            // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,\n            // this probably isn't getting called *nearly* as much\n            const newArr = new Uint8Array(width * height * 4);\n            newArr.set(new Uint8Array(arr.buffer, 0, newArr.length));\n            this._renderQPush({\n                'type': 'blit',\n                'data': newArr,\n                'x': x,\n                'y': y,\n                'width': width,\n                'height': height,\n            });\n        } else {\n            // NB(directxman12): arr must be an Type Array view\n            let data = new Uint8ClampedArray(arr.buffer,\n                                             arr.byteOffset + offset,\n                                             width * height * 4);\n            let img = new ImageData(data, width, height);\n            this._drawCtx.putImageData(img, x, y);\n            this._damage(x, y, width, height);\n        }\n    }\n\n    drawImage(img, ...args) {\n        this._drawCtx.drawImage(img, ...args);\n\n        if (args.length <= 4) {\n            const [x, y] = args;\n            this._damage(x, y, img.width, img.height);\n        } else {\n            const [,, sw, sh, dx, dy] = args;\n            this._damage(dx, dy, sw, sh);\n        }\n    }\n\n    autoscale(containerWidth, containerHeight) {\n        let scaleRatio;\n\n        if (containerWidth === 0 || containerHeight === 0) {\n            scaleRatio = 0;\n\n        } else {\n\n            const vp = this._viewportLoc;\n            const targetAspectRatio = containerWidth / containerHeight;\n            const fbAspectRatio = vp.w / vp.h;\n\n            if (fbAspectRatio >= targetAspectRatio) {\n                scaleRatio = containerWidth / vp.w;\n            } else {\n                scaleRatio = containerHeight / vp.h;\n            }\n        }\n\n        this._rescale(scaleRatio);\n    }\n\n    // ===== PRIVATE METHODS =====\n\n    _rescale(factor) {\n        this._scale = factor;\n        const vp = this._viewportLoc;\n\n        // NB(directxman12): If you set the width directly, or set the\n        //                   style width to a number, the canvas is cleared.\n        //                   However, if you set the style width to a string\n        //                   ('NNNpx'), the canvas is scaled without clearing.\n        const width = factor * vp.w + 'px';\n        const height = factor * vp.h + 'px';\n\n        if ((this._target.style.width !== width) ||\n            (this._target.style.height !== height)) {\n            this._target.style.width = width;\n            this._target.style.height = height;\n        }\n    }\n\n    _setFillColor(color) {\n        const newStyle = 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')';\n        if (newStyle !== this._prevDrawStyle) {\n            this._drawCtx.fillStyle = newStyle;\n            this._prevDrawStyle = newStyle;\n        }\n    }\n\n    _renderQPush(action) {\n        this._renderQ.push(action);\n        if (this._renderQ.length === 1) {\n            // If this can be rendered immediately it will be, otherwise\n            // the scanner will wait for the relevant event\n            this._scanRenderQ();\n        }\n    }\n\n    _resumeRenderQ() {\n        // \"this\" is the object that is ready, not the\n        // display object\n        this.removeEventListener('load', this._noVNCDisplay._resumeRenderQ);\n        this._noVNCDisplay._scanRenderQ();\n    }\n\n    _scanRenderQ() {\n        let ready = true;\n        while (ready && this._renderQ.length > 0) {\n            const a = this._renderQ[0];\n            switch (a.type) {\n                case 'flip':\n                    this.flip(true);\n                    break;\n                case 'copy':\n                    this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, true);\n                    break;\n                case 'fill':\n                    this.fillRect(a.x, a.y, a.width, a.height, a.color, true);\n                    break;\n                case 'blit':\n                    this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);\n                    break;\n                case 'img':\n                    if (a.img.complete) {\n                        if (a.img.width !== a.width || a.img.height !== a.height) {\n                            Log.Error(\"Decoded image has incorrect dimensions. Got \" +\n                                      a.img.width + \"x\" + a.img.height + \". Expected \" +\n                                      a.width + \"x\" + a.height + \".\");\n                            return;\n                        }\n                        this.drawImage(a.img, a.x, a.y);\n                        // This helps the browser free the memory right\n                        // away, rather than ballooning\n                        a.img.src = \"\";\n                    } else {\n                        a.img._noVNCDisplay = this;\n                        a.img.addEventListener('load', this._resumeRenderQ);\n                        // We need to wait for this image to 'load'\n                        // to keep things in-order\n                        ready = false;\n                    }\n                    break;\n                case 'frame':\n                    if (a.frame.ready) {\n                        // The encoded frame may be larger than the rect due to\n                        // limitations of the encoder, so we need to crop the\n                        // frame.\n                        let frame = a.frame.frame;\n                        if (frame.codedWidth < a.width || frame.codedHeight < a.height) {\n                            Log.Warn(\"Decoded video frame does not cover its full rectangle area. Expecting at least \" +\n                                      a.width + \"x\" + a.height + \" but got \" +\n                                      frame.codedWidth + \"x\" + frame.codedHeight);\n                        }\n                        const sx = 0;\n                        const sy = 0;\n                        const sw = a.width;\n                        const sh = a.height;\n                        const dx = a.x;\n                        const dy = a.y;\n                        const dw = sw;\n                        const dh = sh;\n                        this.drawImage(frame, sx, sy, sw, sh, dx, dy, dw, dh);\n                        frame.close();\n                    } else {\n                        let display = this;\n                        a.frame.promise.then(() => {\n                            display._scanRenderQ();\n                        });\n                        ready = false;\n                    }\n                    break;\n            }\n\n            if (ready) {\n                this._renderQ.shift();\n            }\n        }\n\n        if (this._renderQ.length === 0 &&\n            this._flushPromise !== null) {\n            this._flushResolve();\n            this._flushPromise = null;\n            this._flushResolve = null;\n        }\n    }\n}\n"
  },
  {
    "path": "core/encodings.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2019 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n */\n\nexport const encodings = {\n    encodingRaw: 0,\n    encodingCopyRect: 1,\n    encodingRRE: 2,\n    encodingHextile: 5,\n    encodingZlib: 6,\n    encodingTight: 7,\n    encodingZRLE: 16,\n    encodingTightPNG: -260,\n    encodingJPEG: 21,\n    encodingH264: 50,\n\n    pseudoEncodingQualityLevel9: -23,\n    pseudoEncodingQualityLevel0: -32,\n    pseudoEncodingDesktopSize: -223,\n    pseudoEncodingLastRect: -224,\n    pseudoEncodingCursor: -239,\n    pseudoEncodingQEMUExtendedKeyEvent: -258,\n    pseudoEncodingQEMULedEvent: -261,\n    pseudoEncodingDesktopName: -307,\n    pseudoEncodingExtendedDesktopSize: -308,\n    pseudoEncodingXvp: -309,\n    pseudoEncodingFence: -312,\n    pseudoEncodingContinuousUpdates: -313,\n    pseudoEncodingExtendedMouseButtons: -316,\n    pseudoEncodingCompressLevel9: -247,\n    pseudoEncodingCompressLevel0: -256,\n    pseudoEncodingVMwareCursor: 0x574d5664,\n    pseudoEncodingExtendedClipboard: 0xc0a1e5ce\n};\n\nexport function encodingName(num) {\n    switch (num) {\n        case encodings.encodingRaw:      return \"Raw\";\n        case encodings.encodingCopyRect: return \"CopyRect\";\n        case encodings.encodingRRE:      return \"RRE\";\n        case encodings.encodingHextile:  return \"Hextile\";\n        case encodings.encodingZlib:     return \"Zlib\";\n        case encodings.encodingTight:    return \"Tight\";\n        case encodings.encodingZRLE:     return \"ZRLE\";\n        case encodings.encodingTightPNG: return \"TightPNG\";\n        case encodings.encodingJPEG:     return \"JPEG\";\n        case encodings.encodingH264:     return \"H.264\";\n        default:                         return \"[unknown encoding \" + num + \"]\";\n    }\n}\n"
  },
  {
    "path": "core/inflator.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2020 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n */\n\nimport { inflateInit, inflate, inflateReset } from \"../vendor/pako/lib/zlib/inflate.js\";\nimport ZStream from \"../vendor/pako/lib/zlib/zstream.js\";\n\nexport default class Inflate {\n    constructor() {\n        this.strm = new ZStream();\n        this.chunkSize = 1024 * 10 * 10;\n        this.strm.output = new Uint8Array(this.chunkSize);\n\n        inflateInit(this.strm);\n    }\n\n    setInput(data) {\n        if (!data) {\n            //FIXME: flush remaining data.\n            /* eslint-disable camelcase */\n            this.strm.input = null;\n            this.strm.avail_in = 0;\n            this.strm.next_in = 0;\n        } else {\n            this.strm.input = data;\n            this.strm.avail_in = this.strm.input.length;\n            this.strm.next_in = 0;\n            /* eslint-enable camelcase */\n        }\n    }\n\n    inflate(expected) {\n        // resize our output buffer if it's too small\n        // (we could just use multiple chunks, but that would cause an extra\n        // allocation each time to flatten the chunks)\n        if (expected > this.chunkSize) {\n            this.chunkSize = expected;\n            this.strm.output = new Uint8Array(this.chunkSize);\n        }\n\n        /* eslint-disable camelcase */\n        this.strm.next_out = 0;\n        this.strm.avail_out = expected;\n        /* eslint-enable camelcase */\n\n        let ret = inflate(this.strm, 0); // Flush argument not used.\n        if (ret < 0) {\n            throw new Error(\"zlib inflate failed\");\n        }\n\n        if (this.strm.next_out != expected) {\n            throw new Error(\"Incomplete zlib block\");\n        }\n\n        return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);\n    }\n\n    reset() {\n        inflateReset(this.strm);\n    }\n}\n"
  },
  {
    "path": "core/input/domkeytable.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2018 The noVNC authors\n * Licensed under MPL 2.0 or any later version (see LICENSE.txt)\n */\n\nimport KeyTable from \"./keysym.js\";\n\n/*\n * Mapping between HTML key values and VNC/X11 keysyms for \"special\"\n * keys that cannot be handled via their Unicode codepoint.\n *\n * See https://www.w3.org/TR/uievents-key/ for possible values.\n */\n\nconst DOMKeyTable = {};\n\nfunction addStandard(key, standard) {\n    if (standard === undefined) throw new Error(\"Undefined keysym for key \\\"\" + key + \"\\\"\");\n    if (key in DOMKeyTable) throw new Error(\"Duplicate entry for key \\\"\" + key + \"\\\"\");\n    DOMKeyTable[key] = [standard, standard, standard, standard];\n}\n\nfunction addLeftRight(key, left, right) {\n    if (left === undefined) throw new Error(\"Undefined keysym for key \\\"\" + key + \"\\\"\");\n    if (right === undefined) throw new Error(\"Undefined keysym for key \\\"\" + key + \"\\\"\");\n    if (key in DOMKeyTable) throw new Error(\"Duplicate entry for key \\\"\" + key + \"\\\"\");\n    DOMKeyTable[key] = [left, left, right, left];\n}\n\nfunction addNumpad(key, standard, numpad) {\n    if (standard === undefined) throw new Error(\"Undefined keysym for key \\\"\" + key + \"\\\"\");\n    if (numpad === undefined) throw new Error(\"Undefined keysym for key \\\"\" + key + \"\\\"\");\n    if (key in DOMKeyTable) throw new Error(\"Duplicate entry for key \\\"\" + key + \"\\\"\");\n    DOMKeyTable[key] = [standard, standard, standard, numpad];\n}\n\n// 3.2. Modifier Keys\n\naddLeftRight(\"Alt\", KeyTable.XK_Alt_L, KeyTable.XK_Alt_R);\naddStandard(\"AltGraph\", KeyTable.XK_ISO_Level3_Shift);\naddStandard(\"CapsLock\", KeyTable.XK_Caps_Lock);\naddLeftRight(\"Control\", KeyTable.XK_Control_L, KeyTable.XK_Control_R);\n// - Fn\n// - FnLock\naddLeftRight(\"Meta\", KeyTable.XK_Super_L, KeyTable.XK_Super_R);\naddStandard(\"NumLock\", KeyTable.XK_Num_Lock);\naddStandard(\"ScrollLock\", KeyTable.XK_Scroll_Lock);\naddLeftRight(\"Shift\", KeyTable.XK_Shift_L, KeyTable.XK_Shift_R);\n// - Symbol\n// - SymbolLock\n// - Hyper\n// - Super\n\n// 3.3. Whitespace Keys\n\naddNumpad(\"Enter\", KeyTable.XK_Return, KeyTable.XK_KP_Enter);\naddStandard(\"Tab\", KeyTable.XK_Tab);\naddNumpad(\" \", KeyTable.XK_space, KeyTable.XK_KP_Space);\n\n// 3.4. Navigation Keys\n\naddNumpad(\"ArrowDown\", KeyTable.XK_Down, KeyTable.XK_KP_Down);\naddNumpad(\"ArrowLeft\", KeyTable.XK_Left, KeyTable.XK_KP_Left);\naddNumpad(\"ArrowRight\", KeyTable.XK_Right, KeyTable.XK_KP_Right);\naddNumpad(\"ArrowUp\", KeyTable.XK_Up, KeyTable.XK_KP_Up);\naddNumpad(\"End\", KeyTable.XK_End, KeyTable.XK_KP_End);\naddNumpad(\"Home\", KeyTable.XK_Home, KeyTable.XK_KP_Home);\naddNumpad(\"PageDown\", KeyTable.XK_Next, KeyTable.XK_KP_Next);\naddNumpad(\"PageUp\", KeyTable.XK_Prior, KeyTable.XK_KP_Prior);\n\n// 3.5. Editing Keys\n\naddStandard(\"Backspace\", KeyTable.XK_BackSpace);\n// Browsers send \"Clear\" for the numpad 5 without NumLock because\n// Windows uses VK_Clear for that key. But Unix expects KP_Begin for\n// that scenario.\naddNumpad(\"Clear\", KeyTable.XK_Clear, KeyTable.XK_KP_Begin);\naddStandard(\"Copy\", KeyTable.XF86XK_Copy);\n// - CrSel\naddStandard(\"Cut\", KeyTable.XF86XK_Cut);\naddNumpad(\"Delete\", KeyTable.XK_Delete, KeyTable.XK_KP_Delete);\n// - EraseEof\n// - ExSel\naddNumpad(\"Insert\", KeyTable.XK_Insert, KeyTable.XK_KP_Insert);\naddStandard(\"Paste\", KeyTable.XF86XK_Paste);\naddStandard(\"Redo\", KeyTable.XK_Redo);\naddStandard(\"Undo\", KeyTable.XK_Undo);\n\n// 3.6. UI Keys\n\n// - Accept\n// - Again (could just be XK_Redo)\n// - Attn\naddStandard(\"Cancel\", KeyTable.XK_Cancel);\naddStandard(\"ContextMenu\", KeyTable.XK_Menu);\naddStandard(\"Escape\", KeyTable.XK_Escape);\naddStandard(\"Execute\", KeyTable.XK_Execute);\naddStandard(\"Find\", KeyTable.XK_Find);\naddStandard(\"Help\", KeyTable.XK_Help);\naddStandard(\"Pause\", KeyTable.XK_Pause);\n// - Play\n// - Props\naddStandard(\"Select\", KeyTable.XK_Select);\naddStandard(\"ZoomIn\", KeyTable.XF86XK_ZoomIn);\naddStandard(\"ZoomOut\", KeyTable.XF86XK_ZoomOut);\n\n// 3.7. Device Keys\n\naddStandard(\"BrightnessDown\", KeyTable.XF86XK_MonBrightnessDown);\naddStandard(\"BrightnessUp\", KeyTable.XF86XK_MonBrightnessUp);\naddStandard(\"Eject\", KeyTable.XF86XK_Eject);\naddStandard(\"LogOff\", KeyTable.XF86XK_LogOff);\naddStandard(\"Power\", KeyTable.XF86XK_PowerOff);\naddStandard(\"PowerOff\", KeyTable.XF86XK_PowerDown);\naddStandard(\"PrintScreen\", KeyTable.XK_Print);\naddStandard(\"Hibernate\", KeyTable.XF86XK_Hibernate);\naddStandard(\"Standby\", KeyTable.XF86XK_Standby);\naddStandard(\"WakeUp\", KeyTable.XF86XK_WakeUp);\n\n// 3.8. IME and Composition Keys\n\naddStandard(\"AllCandidates\", KeyTable.XK_MultipleCandidate);\naddStandard(\"Alphanumeric\", KeyTable.XK_Eisu_toggle);\naddStandard(\"CodeInput\", KeyTable.XK_Codeinput);\naddStandard(\"Compose\", KeyTable.XK_Multi_key);\naddStandard(\"Convert\", KeyTable.XK_Henkan);\n// - Dead\n// - FinalMode\naddStandard(\"GroupFirst\", KeyTable.XK_ISO_First_Group);\naddStandard(\"GroupLast\", KeyTable.XK_ISO_Last_Group);\naddStandard(\"GroupNext\", KeyTable.XK_ISO_Next_Group);\naddStandard(\"GroupPrevious\", KeyTable.XK_ISO_Prev_Group);\n// - ModeChange (XK_Mode_switch is often used for AltGr)\n// - NextCandidate\naddStandard(\"NonConvert\", KeyTable.XK_Muhenkan);\naddStandard(\"PreviousCandidate\", KeyTable.XK_PreviousCandidate);\n// - Process\naddStandard(\"SingleCandidate\", KeyTable.XK_SingleCandidate);\naddStandard(\"HangulMode\", KeyTable.XK_Hangul);\naddStandard(\"HanjaMode\", KeyTable.XK_Hangul_Hanja);\naddStandard(\"JunjaMode\", KeyTable.XK_Hangul_Jeonja);\naddStandard(\"Eisu\", KeyTable.XK_Eisu_toggle);\naddStandard(\"Hankaku\", KeyTable.XK_Hankaku);\naddStandard(\"Hiragana\", KeyTable.XK_Hiragana);\naddStandard(\"HiraganaKatakana\", KeyTable.XK_Hiragana_Katakana);\naddStandard(\"KanaMode\", KeyTable.XK_Kana_Shift); // could also be _Kana_Lock\naddStandard(\"KanjiMode\", KeyTable.XK_Kanji);\naddStandard(\"Katakana\", KeyTable.XK_Katakana);\naddStandard(\"Romaji\", KeyTable.XK_Romaji);\naddStandard(\"Zenkaku\", KeyTable.XK_Zenkaku);\naddStandard(\"ZenkakuHankaku\", KeyTable.XK_Zenkaku_Hankaku);\n\n// 3.9. General-Purpose Function Keys\n\naddStandard(\"F1\", KeyTable.XK_F1);\naddStandard(\"F2\", KeyTable.XK_F2);\naddStandard(\"F3\", KeyTable.XK_F3);\naddStandard(\"F4\", KeyTable.XK_F4);\naddStandard(\"F5\", KeyTable.XK_F5);\naddStandard(\"F6\", KeyTable.XK_F6);\naddStandard(\"F7\", KeyTable.XK_F7);\naddStandard(\"F8\", KeyTable.XK_F8);\naddStandard(\"F9\", KeyTable.XK_F9);\naddStandard(\"F10\", KeyTable.XK_F10);\naddStandard(\"F11\", KeyTable.XK_F11);\naddStandard(\"F12\", KeyTable.XK_F12);\naddStandard(\"F13\", KeyTable.XK_F13);\naddStandard(\"F14\", KeyTable.XK_F14);\naddStandard(\"F15\", KeyTable.XK_F15);\naddStandard(\"F16\", KeyTable.XK_F16);\naddStandard(\"F17\", KeyTable.XK_F17);\naddStandard(\"F18\", KeyTable.XK_F18);\naddStandard(\"F19\", KeyTable.XK_F19);\naddStandard(\"F20\", KeyTable.XK_F20);\naddStandard(\"F21\", KeyTable.XK_F21);\naddStandard(\"F22\", KeyTable.XK_F22);\naddStandard(\"F23\", KeyTable.XK_F23);\naddStandard(\"F24\", KeyTable.XK_F24);\naddStandard(\"F25\", KeyTable.XK_F25);\naddStandard(\"F26\", KeyTable.XK_F26);\naddStandard(\"F27\", KeyTable.XK_F27);\naddStandard(\"F28\", KeyTable.XK_F28);\naddStandard(\"F29\", KeyTable.XK_F29);\naddStandard(\"F30\", KeyTable.XK_F30);\naddStandard(\"F31\", KeyTable.XK_F31);\naddStandard(\"F32\", KeyTable.XK_F32);\naddStandard(\"F33\", KeyTable.XK_F33);\naddStandard(\"F34\", KeyTable.XK_F34);\naddStandard(\"F35\", KeyTable.XK_F35);\n// - Soft1...\n\n// 3.10. Multimedia Keys\n\n// - ChannelDown\n// - ChannelUp\naddStandard(\"Close\", KeyTable.XF86XK_Close);\naddStandard(\"MailForward\", KeyTable.XF86XK_MailForward);\naddStandard(\"MailReply\", KeyTable.XF86XK_Reply);\naddStandard(\"MailSend\", KeyTable.XF86XK_Send);\n// - MediaClose\naddStandard(\"MediaFastForward\", KeyTable.XF86XK_AudioForward);\naddStandard(\"MediaPause\", KeyTable.XF86XK_AudioPause);\naddStandard(\"MediaPlay\", KeyTable.XF86XK_AudioPlay);\n// - MediaPlayPause\naddStandard(\"MediaRecord\", KeyTable.XF86XK_AudioRecord);\naddStandard(\"MediaRewind\", KeyTable.XF86XK_AudioRewind);\naddStandard(\"MediaStop\", KeyTable.XF86XK_AudioStop);\naddStandard(\"MediaTrackNext\", KeyTable.XF86XK_AudioNext);\naddStandard(\"MediaTrackPrevious\", KeyTable.XF86XK_AudioPrev);\naddStandard(\"New\", KeyTable.XF86XK_New);\naddStandard(\"Open\", KeyTable.XF86XK_Open);\naddStandard(\"Print\", KeyTable.XK_Print);\naddStandard(\"Save\", KeyTable.XF86XK_Save);\naddStandard(\"SpellCheck\", KeyTable.XF86XK_Spell);\n\n// 3.11. Multimedia Numpad Keys\n\n// - Key11\n// - Key12\n\n// 3.12. Audio Keys\n\n// - AudioBalanceLeft\n// - AudioBalanceRight\n// - AudioBassBoostDown\n// - AudioBassBoostToggle\n// - AudioBassBoostUp\n// - AudioFaderFront\n// - AudioFaderRear\n// - AudioSurroundModeNext\n// - AudioTrebleDown\n// - AudioTrebleUp\naddStandard(\"AudioVolumeDown\", KeyTable.XF86XK_AudioLowerVolume);\naddStandard(\"AudioVolumeUp\", KeyTable.XF86XK_AudioRaiseVolume);\naddStandard(\"AudioVolumeMute\", KeyTable.XF86XK_AudioMute);\n// - MicrophoneToggle\n// - MicrophoneVolumeDown\n// - MicrophoneVolumeUp\naddStandard(\"MicrophoneVolumeMute\", KeyTable.XF86XK_AudioMicMute);\n\n// 3.13. Speech Keys\n\n// - SpeechCorrectionList\n// - SpeechInputToggle\n\n// 3.14. Application Keys\n\naddStandard(\"LaunchApplication1\", KeyTable.XF86XK_MyComputer);\naddStandard(\"LaunchApplication2\", KeyTable.XF86XK_Calculator);\naddStandard(\"LaunchCalendar\", KeyTable.XF86XK_Calendar);\n// - LaunchContacts\naddStandard(\"LaunchMail\", KeyTable.XF86XK_Mail);\naddStandard(\"LaunchMediaPlayer\", KeyTable.XF86XK_AudioMedia);\naddStandard(\"LaunchMusicPlayer\", KeyTable.XF86XK_Music);\naddStandard(\"LaunchPhone\", KeyTable.XF86XK_Phone);\naddStandard(\"LaunchScreenSaver\", KeyTable.XF86XK_ScreenSaver);\naddStandard(\"LaunchSpreadsheet\", KeyTable.XF86XK_Excel);\naddStandard(\"LaunchWebBrowser\", KeyTable.XF86XK_WWW);\naddStandard(\"LaunchWebCam\", KeyTable.XF86XK_WebCam);\naddStandard(\"LaunchWordProcessor\", KeyTable.XF86XK_Word);\n\n// 3.15. Browser Keys\n\naddStandard(\"BrowserBack\", KeyTable.XF86XK_Back);\naddStandard(\"BrowserFavorites\", KeyTable.XF86XK_Favorites);\naddStandard(\"BrowserForward\", KeyTable.XF86XK_Forward);\naddStandard(\"BrowserHome\", KeyTable.XF86XK_HomePage);\naddStandard(\"BrowserRefresh\", KeyTable.XF86XK_Refresh);\naddStandard(\"BrowserSearch\", KeyTable.XF86XK_Search);\naddStandard(\"BrowserStop\", KeyTable.XF86XK_Stop);\n\n// 3.16. Mobile Phone Keys\n\n// - A whole bunch...\n\n// 3.17. TV Keys\n\n// - A whole bunch...\n\n// 3.18. Media Controller Keys\n\n// - A whole bunch...\naddStandard(\"Dimmer\", KeyTable.XF86XK_BrightnessAdjust);\naddStandard(\"MediaAudioTrack\", KeyTable.XF86XK_AudioCycleTrack);\naddStandard(\"RandomToggle\", KeyTable.XF86XK_AudioRandomPlay);\naddStandard(\"SplitScreenToggle\", KeyTable.XF86XK_SplitScreen);\naddStandard(\"Subtitle\", KeyTable.XF86XK_Subtitle);\naddStandard(\"VideoModeNext\", KeyTable.XF86XK_Next_VMode);\n\n// Extra: Numpad\n\naddNumpad(\"=\", KeyTable.XK_equal, KeyTable.XK_KP_Equal);\naddNumpad(\"+\", KeyTable.XK_plus, KeyTable.XK_KP_Add);\naddNumpad(\"-\", KeyTable.XK_minus, KeyTable.XK_KP_Subtract);\naddNumpad(\"*\", KeyTable.XK_asterisk, KeyTable.XK_KP_Multiply);\naddNumpad(\"/\", KeyTable.XK_slash, KeyTable.XK_KP_Divide);\naddNumpad(\".\", KeyTable.XK_period, KeyTable.XK_KP_Decimal);\naddNumpad(\",\", KeyTable.XK_comma, KeyTable.XK_KP_Separator);\naddNumpad(\"0\", KeyTable.XK_0, KeyTable.XK_KP_0);\naddNumpad(\"1\", KeyTable.XK_1, KeyTable.XK_KP_1);\naddNumpad(\"2\", KeyTable.XK_2, KeyTable.XK_KP_2);\naddNumpad(\"3\", KeyTable.XK_3, KeyTable.XK_KP_3);\naddNumpad(\"4\", KeyTable.XK_4, KeyTable.XK_KP_4);\naddNumpad(\"5\", KeyTable.XK_5, KeyTable.XK_KP_5);\naddNumpad(\"6\", KeyTable.XK_6, KeyTable.XK_KP_6);\naddNumpad(\"7\", KeyTable.XK_7, KeyTable.XK_KP_7);\naddNumpad(\"8\", KeyTable.XK_8, KeyTable.XK_KP_8);\naddNumpad(\"9\", KeyTable.XK_9, KeyTable.XK_KP_9);\n\nexport default DOMKeyTable;\n"
  },
  {
    "path": "core/input/fixedkeys.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2018 The noVNC authors\n * Licensed under MPL 2.0 or any later version (see LICENSE.txt)\n */\n\n/*\n * Fallback mapping between HTML key codes (physical keys) and\n * HTML key values. This only works for keys that don't vary\n * between layouts. We also omit those who manage fine by mapping the\n * Unicode representation.\n *\n * See https://www.w3.org/TR/uievents-code/ for possible codes.\n * See https://www.w3.org/TR/uievents-key/ for possible values.\n */\n\n/* eslint-disable key-spacing */\n\nexport default {\n\n// 3.1.1.1. Writing System Keys\n\n    'Backspace':        'Backspace',\n\n// 3.1.1.2. Functional Keys\n\n    'AltLeft':          'Alt',\n    'AltRight':         'Alt', // This could also be 'AltGraph'\n    'CapsLock':         'CapsLock',\n    'ContextMenu':      'ContextMenu',\n    'ControlLeft':      'Control',\n    'ControlRight':     'Control',\n    'Enter':            'Enter',\n    'MetaLeft':         'Meta',\n    'MetaRight':        'Meta',\n    'ShiftLeft':        'Shift',\n    'ShiftRight':       'Shift',\n    'Tab':              'Tab',\n    // FIXME: Japanese/Korean keys\n\n// 3.1.2. Control Pad Section\n\n    'Delete':           'Delete',\n    'End':              'End',\n    'Help':             'Help',\n    'Home':             'Home',\n    'Insert':           'Insert',\n    'PageDown':         'PageDown',\n    'PageUp':           'PageUp',\n\n// 3.1.3. Arrow Pad Section\n\n    'ArrowDown':        'ArrowDown',\n    'ArrowLeft':        'ArrowLeft',\n    'ArrowRight':       'ArrowRight',\n    'ArrowUp':          'ArrowUp',\n\n// 3.1.4. Numpad Section\n\n    'NumLock':          'NumLock',\n    'NumpadBackspace':  'Backspace',\n    'NumpadClear':      'Clear',\n\n// 3.1.5. Function Section\n\n    'Escape':           'Escape',\n    'F1':               'F1',\n    'F2':               'F2',\n    'F3':               'F3',\n    'F4':               'F4',\n    'F5':               'F5',\n    'F6':               'F6',\n    'F7':               'F7',\n    'F8':               'F8',\n    'F9':               'F9',\n    'F10':              'F10',\n    'F11':              'F11',\n    'F12':              'F12',\n    'F13':              'F13',\n    'F14':              'F14',\n    'F15':              'F15',\n    'F16':              'F16',\n    'F17':              'F17',\n    'F18':              'F18',\n    'F19':              'F19',\n    'F20':              'F20',\n    'F21':              'F21',\n    'F22':              'F22',\n    'F23':              'F23',\n    'F24':              'F24',\n    'F25':              'F25',\n    'F26':              'F26',\n    'F27':              'F27',\n    'F28':              'F28',\n    'F29':              'F29',\n    'F30':              'F30',\n    'F31':              'F31',\n    'F32':              'F32',\n    'F33':              'F33',\n    'F34':              'F34',\n    'F35':              'F35',\n    'PrintScreen':      'PrintScreen',\n    'ScrollLock':       'ScrollLock',\n    'Pause':            'Pause',\n\n// 3.1.6. Media Keys\n\n    'BrowserBack':      'BrowserBack',\n    'BrowserFavorites': 'BrowserFavorites',\n    'BrowserForward':   'BrowserForward',\n    'BrowserHome':      'BrowserHome',\n    'BrowserRefresh':   'BrowserRefresh',\n    'BrowserSearch':    'BrowserSearch',\n    'BrowserStop':      'BrowserStop',\n    'Eject':            'Eject',\n    'LaunchApp1':       'LaunchMyComputer',\n    'LaunchApp2':       'LaunchCalendar',\n    'LaunchMail':       'LaunchMail',\n    'MediaPlayPause':   'MediaPlay',\n    'MediaStop':        'MediaStop',\n    'MediaTrackNext':   'MediaTrackNext',\n    'MediaTrackPrevious': 'MediaTrackPrevious',\n    'Power':            'Power',\n    'Sleep':            'Sleep',\n    'AudioVolumeDown':  'AudioVolumeDown',\n    'AudioVolumeMute':  'AudioVolumeMute',\n    'AudioVolumeUp':    'AudioVolumeUp',\n    'WakeUp':           'WakeUp',\n};\n"
  },
  {
    "path": "core/input/gesturehandler.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2020 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n *\n */\n\nconst GH_NOGESTURE = 0;\nconst GH_ONETAP    = 1;\nconst GH_TWOTAP    = 2;\nconst GH_THREETAP  = 4;\nconst GH_DRAG      = 8;\nconst GH_LONGPRESS = 16;\nconst GH_TWODRAG   = 32;\nconst GH_PINCH     = 64;\n\nconst GH_INITSTATE = 127;\n\nconst GH_MOVE_THRESHOLD = 50;\nconst GH_ANGLE_THRESHOLD = 90; // Degrees\n\n// Timeout when waiting for gestures (ms)\nconst GH_MULTITOUCH_TIMEOUT = 250;\n\n// Maximum time between press and release for a tap (ms)\nconst GH_TAP_TIMEOUT = 1000;\n\n// Timeout when waiting for longpress (ms)\nconst GH_LONGPRESS_TIMEOUT = 1000;\n\n// Timeout when waiting to decide between PINCH and TWODRAG (ms)\nconst GH_TWOTOUCH_TIMEOUT = 50;\n\nexport default class GestureHandler {\n    constructor() {\n        this._target = null;\n\n        this._state = GH_INITSTATE;\n\n        this._tracked = [];\n        this._ignored = [];\n\n        this._waitingRelease = false;\n        this._releaseStart = 0.0;\n\n        this._longpressTimeoutId = null;\n        this._twoTouchTimeoutId = null;\n\n        this._boundEventHandler = this._eventHandler.bind(this);\n    }\n\n    attach(target) {\n        this.detach();\n\n        this._target = target;\n        this._target.addEventListener('touchstart',\n                                      this._boundEventHandler);\n        this._target.addEventListener('touchmove',\n                                      this._boundEventHandler);\n        this._target.addEventListener('touchend',\n                                      this._boundEventHandler);\n        this._target.addEventListener('touchcancel',\n                                      this._boundEventHandler);\n    }\n\n    detach() {\n        if (!this._target) {\n            return;\n        }\n\n        this._stopLongpressTimeout();\n        this._stopTwoTouchTimeout();\n\n        this._target.removeEventListener('touchstart',\n                                         this._boundEventHandler);\n        this._target.removeEventListener('touchmove',\n                                         this._boundEventHandler);\n        this._target.removeEventListener('touchend',\n                                         this._boundEventHandler);\n        this._target.removeEventListener('touchcancel',\n                                         this._boundEventHandler);\n        this._target = null;\n    }\n\n    _eventHandler(e) {\n        let fn;\n\n        e.stopPropagation();\n        e.preventDefault();\n\n        switch (e.type) {\n            case 'touchstart':\n                fn = this._touchStart;\n                break;\n            case 'touchmove':\n                fn = this._touchMove;\n                break;\n            case 'touchend':\n            case 'touchcancel':\n                fn = this._touchEnd;\n                break;\n        }\n\n        for (let i = 0; i < e.changedTouches.length; i++) {\n            let touch = e.changedTouches[i];\n            fn.call(this, touch.identifier, touch.clientX, touch.clientY);\n        }\n    }\n\n    _touchStart(id, x, y) {\n        // Ignore any new touches if there is already an active gesture,\n        // or we're in a cleanup state\n        if (this._hasDetectedGesture() || (this._state === GH_NOGESTURE)) {\n            this._ignored.push(id);\n            return;\n        }\n\n        // Did it take too long between touches that we should no longer\n        // consider this a single gesture?\n        if ((this._tracked.length > 0) &&\n            ((Date.now() - this._tracked[0].started) > GH_MULTITOUCH_TIMEOUT)) {\n            this._state = GH_NOGESTURE;\n            this._ignored.push(id);\n            return;\n        }\n\n        // If we're waiting for fingers to release then we should no longer\n        // recognize new touches\n        if (this._waitingRelease) {\n            this._state = GH_NOGESTURE;\n            this._ignored.push(id);\n            return;\n        }\n\n        this._tracked.push({\n            id: id,\n            started: Date.now(),\n            active: true,\n            firstX: x,\n            firstY: y,\n            lastX: x,\n            lastY: y,\n            angle: 0\n        });\n\n        switch (this._tracked.length) {\n            case 1:\n                this._startLongpressTimeout();\n                break;\n\n            case 2:\n                this._state &= ~(GH_ONETAP | GH_DRAG | GH_LONGPRESS);\n                this._stopLongpressTimeout();\n                break;\n\n            case 3:\n                this._state &= ~(GH_TWOTAP | GH_TWODRAG | GH_PINCH);\n                break;\n\n            default:\n                this._state = GH_NOGESTURE;\n        }\n    }\n\n    _touchMove(id, x, y) {\n        let touch = this._tracked.find(t => t.id === id);\n\n        // If this is an update for a touch we're not tracking, ignore it\n        if (touch === undefined) {\n            return;\n        }\n\n        // Update the touches last position with the event coordinates\n        touch.lastX = x;\n        touch.lastY = y;\n\n        let deltaX = x - touch.firstX;\n        let deltaY = y - touch.firstY;\n\n        // Update angle when the touch has moved\n        if ((touch.firstX !== touch.lastX) ||\n            (touch.firstY !== touch.lastY)) {\n            touch.angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI;\n        }\n\n        if (!this._hasDetectedGesture()) {\n            // Ignore moves smaller than the minimum threshold\n            if (Math.hypot(deltaX, deltaY) < GH_MOVE_THRESHOLD) {\n                return;\n            }\n\n            // Can't be a tap or long press as we've seen movement\n            this._state &= ~(GH_ONETAP | GH_TWOTAP | GH_THREETAP | GH_LONGPRESS);\n            this._stopLongpressTimeout();\n\n            if (this._tracked.length !== 1) {\n                this._state &= ~(GH_DRAG);\n            }\n            if (this._tracked.length !== 2) {\n                this._state &= ~(GH_TWODRAG | GH_PINCH);\n            }\n\n            // We need to figure out which of our different two touch gestures\n            // this might be\n            if (this._tracked.length === 2) {\n\n                // The other touch is the one where the id doesn't match\n                let prevTouch = this._tracked.find(t => t.id !== id);\n\n                // How far the previous touch point has moved since start\n                let prevDeltaMove = Math.hypot(prevTouch.firstX - prevTouch.lastX,\n                                               prevTouch.firstY - prevTouch.lastY);\n\n                // We know that the current touch moved far enough,\n                // but unless both touches moved further than their\n                // threshold we don't want to disqualify any gestures\n                if (prevDeltaMove > GH_MOVE_THRESHOLD) {\n\n                    // The angle difference between the direction of the touch points\n                    let deltaAngle = Math.abs(touch.angle - prevTouch.angle);\n                    deltaAngle = Math.abs(((deltaAngle + 180) % 360) - 180);\n\n                    // PINCH or TWODRAG can be eliminated depending on the angle\n                    if (deltaAngle > GH_ANGLE_THRESHOLD) {\n                        this._state &= ~GH_TWODRAG;\n                    } else {\n                        this._state &= ~GH_PINCH;\n                    }\n\n                    if (this._isTwoTouchTimeoutRunning()) {\n                        this._stopTwoTouchTimeout();\n                    }\n                } else if (!this._isTwoTouchTimeoutRunning()) {\n                    // We can't determine the gesture right now, let's\n                    // wait and see if more events are on their way\n                    this._startTwoTouchTimeout();\n                }\n            }\n\n            if (!this._hasDetectedGesture()) {\n                return;\n            }\n\n            this._pushEvent('gesturestart');\n        }\n\n        this._pushEvent('gesturemove');\n    }\n\n    _touchEnd(id, x, y) {\n        // Check if this is an ignored touch\n        if (this._ignored.indexOf(id) !== -1) {\n            // Remove this touch from ignored\n            this._ignored.splice(this._ignored.indexOf(id), 1);\n\n            // And reset the state if there are no more touches\n            if ((this._ignored.length === 0) &&\n                (this._tracked.length === 0)) {\n                this._state = GH_INITSTATE;\n                this._waitingRelease = false;\n            }\n            return;\n        }\n\n        // We got a touchend before the timer triggered,\n        // this cannot result in a gesture anymore.\n        if (!this._hasDetectedGesture() &&\n            this._isTwoTouchTimeoutRunning()) {\n            this._stopTwoTouchTimeout();\n            this._state = GH_NOGESTURE;\n        }\n\n        // Some gestures don't trigger until a touch is released\n        if (!this._hasDetectedGesture()) {\n            // Can't be a gesture that relies on movement\n            this._state &= ~(GH_DRAG | GH_TWODRAG | GH_PINCH);\n            // Or something that relies on more time\n            this._state &= ~GH_LONGPRESS;\n            this._stopLongpressTimeout();\n\n            if (!this._waitingRelease) {\n                this._releaseStart = Date.now();\n                this._waitingRelease = true;\n\n                // Can't be a tap that requires more touches than we current have\n                switch (this._tracked.length) {\n                    case 1:\n                        this._state &= ~(GH_TWOTAP | GH_THREETAP);\n                        break;\n\n                    case 2:\n                        this._state &= ~(GH_ONETAP | GH_THREETAP);\n                        break;\n                }\n            }\n        }\n\n        // Waiting for all touches to release? (i.e. some tap)\n        if (this._waitingRelease) {\n            // Were all touches released at roughly the same time?\n            if ((Date.now() - this._releaseStart) > GH_MULTITOUCH_TIMEOUT) {\n                this._state = GH_NOGESTURE;\n            }\n\n            // Did too long time pass between press and release?\n            if (this._tracked.some(t => (Date.now() - t.started) > GH_TAP_TIMEOUT)) {\n                this._state = GH_NOGESTURE;\n            }\n\n            let touch = this._tracked.find(t => t.id === id);\n            touch.active = false;\n\n            // Are we still waiting for more releases?\n            if (this._hasDetectedGesture()) {\n                this._pushEvent('gesturestart');\n            } else {\n                // Have we reached a dead end?\n                if (this._state !== GH_NOGESTURE) {\n                    return;\n                }\n            }\n        }\n\n        if (this._hasDetectedGesture()) {\n            this._pushEvent('gestureend');\n        }\n\n        // Ignore any remaining touches until they are ended\n        for (let i = 0; i < this._tracked.length; i++) {\n            if (this._tracked[i].active) {\n                this._ignored.push(this._tracked[i].id);\n            }\n        }\n        this._tracked = [];\n\n        this._state = GH_NOGESTURE;\n\n        // Remove this touch from ignored if it's in there\n        if (this._ignored.indexOf(id) !== -1) {\n            this._ignored.splice(this._ignored.indexOf(id), 1);\n        }\n\n        // We reset the state if ignored is empty\n        if ((this._ignored.length === 0)) {\n            this._state = GH_INITSTATE;\n            this._waitingRelease = false;\n        }\n    }\n\n    _hasDetectedGesture() {\n        if (this._state === GH_NOGESTURE) {\n            return false;\n        }\n        // Check to see if the bitmask value is a power of 2\n        // (i.e. only one bit set). If it is, we have a state.\n        if (this._state & (this._state - 1)) {\n            return false;\n        }\n\n        // For taps we also need to have all touches released\n        // before we've fully detected the gesture\n        if (this._state & (GH_ONETAP | GH_TWOTAP | GH_THREETAP)) {\n            if (this._tracked.some(t => t.active)) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    _startLongpressTimeout() {\n        this._stopLongpressTimeout();\n        this._longpressTimeoutId = setTimeout(() => this._longpressTimeout(),\n                                              GH_LONGPRESS_TIMEOUT);\n    }\n\n    _stopLongpressTimeout() {\n        clearTimeout(this._longpressTimeoutId);\n        this._longpressTimeoutId = null;\n    }\n\n    _longpressTimeout() {\n        if (this._hasDetectedGesture()) {\n            throw new Error(\"A longpress gesture failed, conflict with a different gesture\");\n        }\n\n        this._state = GH_LONGPRESS;\n        this._pushEvent('gesturestart');\n    }\n\n    _startTwoTouchTimeout() {\n        this._stopTwoTouchTimeout();\n        this._twoTouchTimeoutId = setTimeout(() => this._twoTouchTimeout(),\n                                             GH_TWOTOUCH_TIMEOUT);\n    }\n\n    _stopTwoTouchTimeout() {\n        clearTimeout(this._twoTouchTimeoutId);\n        this._twoTouchTimeoutId = null;\n    }\n\n    _isTwoTouchTimeoutRunning() {\n        return this._twoTouchTimeoutId !== null;\n    }\n\n    _twoTouchTimeout() {\n        if (this._tracked.length === 0) {\n            throw new Error(\"A pinch or two drag gesture failed, no tracked touches\");\n        }\n\n        // How far each touch point has moved since start\n        let avgM = this._getAverageMovement();\n        let avgMoveH = Math.abs(avgM.x);\n        let avgMoveV = Math.abs(avgM.y);\n\n        // The difference in the distance between where\n        // the touch points started and where they are now\n        let avgD = this._getAverageDistance();\n        let deltaTouchDistance = Math.abs(Math.hypot(avgD.first.x, avgD.first.y) -\n                                          Math.hypot(avgD.last.x, avgD.last.y));\n\n        if ((avgMoveV < deltaTouchDistance) &&\n            (avgMoveH < deltaTouchDistance)) {\n            this._state = GH_PINCH;\n        } else {\n            this._state = GH_TWODRAG;\n        }\n\n        this._pushEvent('gesturestart');\n        this._pushEvent('gesturemove');\n    }\n\n    _pushEvent(type) {\n        let detail = { type: this._stateToGesture(this._state) };\n\n        // For most gesture events the current (average) position is the\n        // most useful\n        let avg = this._getPosition();\n        let pos = avg.last;\n\n        // However we have a slight distance to detect gestures, so for the\n        // first gesture event we want to use the first positions we saw\n        if (type === 'gesturestart') {\n            pos = avg.first;\n        }\n\n        // For these gestures, we always want the event coordinates\n        // to be where the gesture began, not the current touch location.\n        switch (this._state) {\n            case GH_TWODRAG:\n            case GH_PINCH:\n                pos = avg.first;\n                break;\n        }\n\n        detail['clientX'] = pos.x;\n        detail['clientY'] = pos.y;\n\n        // FIXME: other coordinates?\n\n        // Some gestures also have a magnitude\n        if (this._state === GH_PINCH) {\n            let distance = this._getAverageDistance();\n            if (type === 'gesturestart') {\n                detail['magnitudeX'] = distance.first.x;\n                detail['magnitudeY'] = distance.first.y;\n            } else {\n                detail['magnitudeX'] = distance.last.x;\n                detail['magnitudeY'] = distance.last.y;\n            }\n        } else if (this._state === GH_TWODRAG) {\n            if (type === 'gesturestart') {\n                detail['magnitudeX'] = 0.0;\n                detail['magnitudeY'] = 0.0;\n            } else {\n                let movement = this._getAverageMovement();\n                detail['magnitudeX'] = movement.x;\n                detail['magnitudeY'] = movement.y;\n            }\n        }\n\n        let gev = new CustomEvent(type, { detail: detail });\n        this._target.dispatchEvent(gev);\n    }\n\n    _stateToGesture(state) {\n        switch (state) {\n            case GH_ONETAP:\n                return 'onetap';\n            case GH_TWOTAP:\n                return 'twotap';\n            case GH_THREETAP:\n                return 'threetap';\n            case GH_DRAG:\n                return 'drag';\n            case GH_LONGPRESS:\n                return 'longpress';\n            case GH_TWODRAG:\n                return 'twodrag';\n            case GH_PINCH:\n                return 'pinch';\n        }\n\n        throw new Error(\"Unknown gesture state: \" + state);\n    }\n\n    _getPosition() {\n        if (this._tracked.length === 0) {\n            throw new Error(\"Failed to get gesture position, no tracked touches\");\n        }\n\n        let size = this._tracked.length;\n        let fx = 0, fy = 0, lx = 0, ly = 0;\n\n        for (let i = 0; i < this._tracked.length; i++) {\n            fx += this._tracked[i].firstX;\n            fy += this._tracked[i].firstY;\n            lx += this._tracked[i].lastX;\n            ly += this._tracked[i].lastY;\n        }\n\n        return { first: { x: fx / size,\n                          y: fy / size },\n                 last: { x: lx / size,\n                         y: ly / size } };\n    }\n\n    _getAverageMovement() {\n        if (this._tracked.length === 0) {\n            throw new Error(\"Failed to get gesture movement, no tracked touches\");\n        }\n\n        let totalH, totalV;\n        totalH = totalV = 0;\n        let size = this._tracked.length;\n\n        for (let i = 0; i < this._tracked.length; i++) {\n            totalH += this._tracked[i].lastX - this._tracked[i].firstX;\n            totalV += this._tracked[i].lastY - this._tracked[i].firstY;\n        }\n\n        return { x: totalH / size,\n                 y: totalV / size };\n    }\n\n    _getAverageDistance() {\n        if (this._tracked.length === 0) {\n            throw new Error(\"Failed to get gesture distance, no tracked touches\");\n        }\n\n        // Distance between the first and last tracked touches\n\n        let first = this._tracked[0];\n        let last = this._tracked[this._tracked.length - 1];\n\n        let fdx = Math.abs(last.firstX - first.firstX);\n        let fdy = Math.abs(last.firstY - first.firstY);\n\n        let ldx = Math.abs(last.lastX - first.lastX);\n        let ldy = Math.abs(last.lastY - first.lastY);\n\n        return { first: { x: fdx, y: fdy },\n                 last: { x: ldx, y: ldy } };\n    }\n}\n"
  },
  {
    "path": "core/input/keyboard.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2019 The noVNC authors\n * Licensed under MPL 2.0 or any later version (see LICENSE.txt)\n */\n\nimport * as Log from '../util/logging.js';\nimport { stopEvent } from '../util/events.js';\nimport * as KeyboardUtil from \"./util.js\";\nimport KeyTable from \"./keysym.js\";\nimport * as browser from \"../util/browser.js\";\n\n//\n// Keyboard event handler\n//\n\nexport default class Keyboard {\n    constructor(target) {\n        this._target = target || null;\n\n        this._keyDownList = {};         // List of depressed keys\n                                        // (even if they are happy)\n        this._altGrArmed = false;       // Windows AltGr detection\n\n        // keep these here so we can refer to them later\n        this._eventHandlers = {\n            'keyup': this._handleKeyUp.bind(this),\n            'keydown': this._handleKeyDown.bind(this),\n            'blur': this._allKeysUp.bind(this),\n        };\n\n        // ===== EVENT HANDLERS =====\n\n        this.onkeyevent = () => {}; // Handler for key press/release\n    }\n\n    // ===== PRIVATE METHODS =====\n\n    _sendKeyEvent(keysym, code, down, numlock = null, capslock = null) {\n        if (down) {\n            this._keyDownList[code] = keysym;\n        } else {\n            // Do we really think this key is down?\n            if (!(code in this._keyDownList)) {\n                return;\n            }\n            delete this._keyDownList[code];\n        }\n\n        Log.Debug(\"onkeyevent \" + (down ? \"down\" : \"up\") +\n                  \", keysym: \" + keysym, \", code: \" + code +\n                  \", numlock: \" + numlock + \", capslock: \" + capslock);\n        this.onkeyevent(keysym, code, down, numlock, capslock);\n    }\n\n    _getKeyCode(e) {\n        const code = KeyboardUtil.getKeycode(e);\n        if (code !== 'Unidentified') {\n            return code;\n        }\n\n        // Unstable, but we don't have anything else to go on\n        if (e.keyCode) {\n            // 229 is used for composition events\n            if (e.keyCode !== 229) {\n                return 'Platform' + e.keyCode;\n            }\n        }\n\n        // A precursor to the final DOM3 standard. Unfortunately it\n        // is not layout independent, so it is as bad as using keyCode\n        if (e.keyIdentifier) {\n            // Non-character key?\n            if (e.keyIdentifier.substr(0, 2) !== 'U+') {\n                return e.keyIdentifier;\n            }\n\n            const codepoint = parseInt(e.keyIdentifier.substr(2), 16);\n            const char = String.fromCharCode(codepoint).toUpperCase();\n\n            return 'Platform' + char.charCodeAt();\n        }\n\n        return 'Unidentified';\n    }\n\n    _handleKeyDown(e) {\n        const code = this._getKeyCode(e);\n        let keysym = KeyboardUtil.getKeysym(e);\n        let numlock = e.getModifierState('NumLock');\n        let capslock = e.getModifierState('CapsLock');\n\n        // getModifierState for NumLock is not supported on mac and ios and always returns false.\n        // Set to null to indicate unknown/unsupported instead.\n        if (browser.isMac() || browser.isIOS()) {\n            numlock = null;\n        }\n\n        // Windows doesn't have a proper AltGr, but handles it using\n        // fake Ctrl+Alt. However the remote end might not be Windows,\n        // so we need to merge those in to a single AltGr event. We\n        // detect this case by seeing the two key events directly after\n        // each other with a very short time between them (<50ms).\n        if (this._altGrArmed) {\n            this._altGrArmed = false;\n            clearTimeout(this._altGrTimeout);\n\n            if ((code === \"AltRight\") &&\n                ((e.timeStamp - this._altGrCtrlTime) < 50)) {\n                // FIXME: We fail to detect this if either Ctrl key is\n                //        first manually pressed as Windows then no\n                //        longer sends the fake Ctrl down event. It\n                //        does however happily send real Ctrl events\n                //        even when AltGr is already down. Some\n                //        browsers detect this for us though and set the\n                //        key to \"AltGraph\".\n                keysym = KeyTable.XK_ISO_Level3_Shift;\n            } else {\n                this._sendKeyEvent(KeyTable.XK_Control_L, \"ControlLeft\", true, numlock, capslock);\n            }\n        }\n\n        // We cannot handle keys we cannot track, but we also need\n        // to deal with virtual keyboards which omit key info\n        if (code === 'Unidentified') {\n            if (keysym) {\n                // If it's a virtual keyboard then it should be\n                // sufficient to just send press and release right\n                // after each other\n                this._sendKeyEvent(keysym, code, true, numlock, capslock);\n                this._sendKeyEvent(keysym, code, false, numlock, capslock);\n            }\n\n            stopEvent(e);\n            return;\n        }\n\n        // Alt behaves more like AltGraph on macOS, so shuffle the\n        // keys around a bit to make things more sane for the remote\n        // server. This method is used by RealVNC and TigerVNC (and\n        // possibly others).\n        if (browser.isMac() || browser.isIOS()) {\n            switch (keysym) {\n                case KeyTable.XK_Super_L:\n                    keysym = KeyTable.XK_Alt_L;\n                    break;\n                case KeyTable.XK_Super_R:\n                    keysym = KeyTable.XK_Super_L;\n                    break;\n                case KeyTable.XK_Alt_L:\n                    keysym = KeyTable.XK_Mode_switch;\n                    break;\n                case KeyTable.XK_Alt_R:\n                    keysym = KeyTable.XK_ISO_Level3_Shift;\n                    break;\n            }\n        }\n\n        // Is this key already pressed? If so, then we must use the\n        // same keysym or we'll confuse the server\n        if (code in this._keyDownList) {\n            keysym = this._keyDownList[code];\n        }\n\n        // macOS doesn't send proper key releases if a key is pressed\n        // while meta is held down\n        if ((browser.isMac() || browser.isIOS()) &&\n            (e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight')) {\n            this._sendKeyEvent(keysym, code, true, numlock, capslock);\n            this._sendKeyEvent(keysym, code, false, numlock, capslock);\n            stopEvent(e);\n            return;\n        }\n\n        // macOS doesn't send proper key events for modifiers, only\n        // state change events. That gets extra confusing for CapsLock\n        // which toggles on each press, but not on release. So pretend\n        // it was a quick press and release of the button.\n        if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {\n            this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true, numlock, capslock);\n            this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false, numlock, capslock);\n            stopEvent(e);\n            return;\n        }\n\n        // Windows doesn't send proper key releases for a bunch of\n        // Japanese IM keys so we have to fake the release right away\n        const jpBadKeys = [ KeyTable.XK_Zenkaku_Hankaku,\n                            KeyTable.XK_Eisu_toggle,\n                            KeyTable.XK_Katakana,\n                            KeyTable.XK_Hiragana,\n                            KeyTable.XK_Romaji ];\n        if (browser.isWindows() && jpBadKeys.includes(keysym)) {\n            this._sendKeyEvent(keysym, code, true, numlock, capslock);\n            this._sendKeyEvent(keysym, code, false, numlock, capslock);\n            stopEvent(e);\n            return;\n        }\n\n        stopEvent(e);\n\n        // Possible start of AltGr sequence? (see above)\n        if ((code === \"ControlLeft\") && browser.isWindows() &&\n            !(\"ControlLeft\" in this._keyDownList)) {\n            this._altGrArmed = true;\n            this._altGrTimeout = setTimeout(this._interruptAltGrSequence.bind(this), 100);\n            this._altGrCtrlTime = e.timeStamp;\n            return;\n        }\n\n        this._sendKeyEvent(keysym, code, true, numlock, capslock);\n    }\n\n    _handleKeyUp(e) {\n        stopEvent(e);\n\n        const code = this._getKeyCode(e);\n\n        // We can't get a release in the middle of an AltGr sequence, so\n        // abort that detection\n        this._interruptAltGrSequence();\n\n        // See comment in _handleKeyDown()\n        if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {\n            this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);\n            this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);\n            return;\n        }\n\n        this._sendKeyEvent(this._keyDownList[code], code, false);\n\n        // Windows has a rather nasty bug where it won't send key\n        // release events for a Shift button if the other Shift is still\n        // pressed\n        if (browser.isWindows() && ((code === 'ShiftLeft') ||\n                                    (code === 'ShiftRight'))) {\n            if ('ShiftRight' in this._keyDownList) {\n                this._sendKeyEvent(this._keyDownList['ShiftRight'],\n                                   'ShiftRight', false);\n            }\n            if ('ShiftLeft' in this._keyDownList) {\n                this._sendKeyEvent(this._keyDownList['ShiftLeft'],\n                                   'ShiftLeft', false);\n            }\n        }\n    }\n\n    _interruptAltGrSequence() {\n        if (this._altGrArmed) {\n            this._altGrArmed = false;\n            clearTimeout(this._altGrTimeout);\n            this._sendKeyEvent(KeyTable.XK_Control_L, \"ControlLeft\", true);\n        }\n    }\n\n    _allKeysUp() {\n        Log.Debug(\">> Keyboard.allKeysUp\");\n\n        // Prevent control key being processed after losing focus.\n        this._interruptAltGrSequence();\n\n        for (let code in this._keyDownList) {\n            this._sendKeyEvent(this._keyDownList[code], code, false);\n        }\n        Log.Debug(\"<< Keyboard.allKeysUp\");\n    }\n\n    // ===== PUBLIC METHODS =====\n\n    grab() {\n        //Log.Debug(\">> Keyboard.grab\");\n\n        this._target.addEventListener('keydown', this._eventHandlers.keydown);\n        this._target.addEventListener('keyup', this._eventHandlers.keyup);\n\n        // Release (key up) if window loses focus\n        window.addEventListener('blur', this._eventHandlers.blur);\n\n        //Log.Debug(\"<< Keyboard.grab\");\n    }\n\n    ungrab() {\n        //Log.Debug(\">> Keyboard.ungrab\");\n\n        this._target.removeEventListener('keydown', this._eventHandlers.keydown);\n        this._target.removeEventListener('keyup', this._eventHandlers.keyup);\n        window.removeEventListener('blur', this._eventHandlers.blur);\n\n        // Release (key up) all keys that are in a down state\n        this._allKeysUp();\n\n        //Log.Debug(\">> Keyboard.ungrab\");\n    }\n}\n"
  },
  {
    "path": "core/input/keysym.js",
    "content": "/* eslint-disable key-spacing */\n\nexport default {\n    XK_VoidSymbol:                  0xffffff, /* Void symbol */\n\n    XK_BackSpace:                   0xff08, /* Back space, back char */\n    XK_Tab:                         0xff09,\n    XK_Linefeed:                    0xff0a, /* Linefeed, LF */\n    XK_Clear:                       0xff0b,\n    XK_Return:                      0xff0d, /* Return, enter */\n    XK_Pause:                       0xff13, /* Pause, hold */\n    XK_Scroll_Lock:                 0xff14,\n    XK_Sys_Req:                     0xff15,\n    XK_Escape:                      0xff1b,\n    XK_Delete:                      0xffff, /* Delete, rubout */\n\n    /* International & multi-key character composition */\n\n    XK_Multi_key:                   0xff20, /* Multi-key character compose */\n    XK_Codeinput:                   0xff37,\n    XK_SingleCandidate:             0xff3c,\n    XK_MultipleCandidate:           0xff3d,\n    XK_PreviousCandidate:           0xff3e,\n\n    /* Japanese keyboard support */\n\n    XK_Kanji:                       0xff21, /* Kanji, Kanji convert */\n    XK_Muhenkan:                    0xff22, /* Cancel Conversion */\n    XK_Henkan_Mode:                 0xff23, /* Start/Stop Conversion */\n    XK_Henkan:                      0xff23, /* Alias for Henkan_Mode */\n    XK_Romaji:                      0xff24, /* to Romaji */\n    XK_Hiragana:                    0xff25, /* to Hiragana */\n    XK_Katakana:                    0xff26, /* to Katakana */\n    XK_Hiragana_Katakana:           0xff27, /* Hiragana/Katakana toggle */\n    XK_Zenkaku:                     0xff28, /* to Zenkaku */\n    XK_Hankaku:                     0xff29, /* to Hankaku */\n    XK_Zenkaku_Hankaku:             0xff2a, /* Zenkaku/Hankaku toggle */\n    XK_Touroku:                     0xff2b, /* Add to Dictionary */\n    XK_Massyo:                      0xff2c, /* Delete from Dictionary */\n    XK_Kana_Lock:                   0xff2d, /* Kana Lock */\n    XK_Kana_Shift:                  0xff2e, /* Kana Shift */\n    XK_Eisu_Shift:                  0xff2f, /* Alphanumeric Shift */\n    XK_Eisu_toggle:                 0xff30, /* Alphanumeric toggle */\n    XK_Kanji_Bangou:                0xff37, /* Codeinput */\n    XK_Zen_Koho:                    0xff3d, /* Multiple/All Candidate(s) */\n    XK_Mae_Koho:                    0xff3e, /* Previous Candidate */\n\n    /* Cursor control & motion */\n\n    XK_Home:                        0xff50,\n    XK_Left:                        0xff51, /* Move left, left arrow */\n    XK_Up:                          0xff52, /* Move up, up arrow */\n    XK_Right:                       0xff53, /* Move right, right arrow */\n    XK_Down:                        0xff54, /* Move down, down arrow */\n    XK_Prior:                       0xff55, /* Prior, previous */\n    XK_Page_Up:                     0xff55,\n    XK_Next:                        0xff56, /* Next */\n    XK_Page_Down:                   0xff56,\n    XK_End:                         0xff57, /* EOL */\n    XK_Begin:                       0xff58, /* BOL */\n\n\n    /* Misc functions */\n\n    XK_Select:                      0xff60, /* Select, mark */\n    XK_Print:                       0xff61,\n    XK_Execute:                     0xff62, /* Execute, run, do */\n    XK_Insert:                      0xff63, /* Insert, insert here */\n    XK_Undo:                        0xff65,\n    XK_Redo:                        0xff66, /* Redo, again */\n    XK_Menu:                        0xff67,\n    XK_Find:                        0xff68, /* Find, search */\n    XK_Cancel:                      0xff69, /* Cancel, stop, abort, exit */\n    XK_Help:                        0xff6a, /* Help */\n    XK_Break:                       0xff6b,\n    XK_Mode_switch:                 0xff7e, /* Character set switch */\n    XK_script_switch:               0xff7e, /* Alias for mode_switch */\n    XK_Num_Lock:                    0xff7f,\n\n    /* Keypad functions, keypad numbers cleverly chosen to map to ASCII */\n\n    XK_KP_Space:                    0xff80, /* Space */\n    XK_KP_Tab:                      0xff89,\n    XK_KP_Enter:                    0xff8d, /* Enter */\n    XK_KP_F1:                       0xff91, /* PF1, KP_A, ... */\n    XK_KP_F2:                       0xff92,\n    XK_KP_F3:                       0xff93,\n    XK_KP_F4:                       0xff94,\n    XK_KP_Home:                     0xff95,\n    XK_KP_Left:                     0xff96,\n    XK_KP_Up:                       0xff97,\n    XK_KP_Right:                    0xff98,\n    XK_KP_Down:                     0xff99,\n    XK_KP_Prior:                    0xff9a,\n    XK_KP_Page_Up:                  0xff9a,\n    XK_KP_Next:                     0xff9b,\n    XK_KP_Page_Down:                0xff9b,\n    XK_KP_End:                      0xff9c,\n    XK_KP_Begin:                    0xff9d,\n    XK_KP_Insert:                   0xff9e,\n    XK_KP_Delete:                   0xff9f,\n    XK_KP_Equal:                    0xffbd, /* Equals */\n    XK_KP_Multiply:                 0xffaa,\n    XK_KP_Add:                      0xffab,\n    XK_KP_Separator:                0xffac, /* Separator, often comma */\n    XK_KP_Subtract:                 0xffad,\n    XK_KP_Decimal:                  0xffae,\n    XK_KP_Divide:                   0xffaf,\n\n    XK_KP_0:                        0xffb0,\n    XK_KP_1:                        0xffb1,\n    XK_KP_2:                        0xffb2,\n    XK_KP_3:                        0xffb3,\n    XK_KP_4:                        0xffb4,\n    XK_KP_5:                        0xffb5,\n    XK_KP_6:                        0xffb6,\n    XK_KP_7:                        0xffb7,\n    XK_KP_8:                        0xffb8,\n    XK_KP_9:                        0xffb9,\n\n    /*\n     * Auxiliary functions; note the duplicate definitions for left and right\n     * function keys;  Sun keyboards and a few other manufacturers have such\n     * function key groups on the left and/or right sides of the keyboard.\n     * We've not found a keyboard with more than 35 function keys total.\n     */\n\n    XK_F1:                          0xffbe,\n    XK_F2:                          0xffbf,\n    XK_F3:                          0xffc0,\n    XK_F4:                          0xffc1,\n    XK_F5:                          0xffc2,\n    XK_F6:                          0xffc3,\n    XK_F7:                          0xffc4,\n    XK_F8:                          0xffc5,\n    XK_F9:                          0xffc6,\n    XK_F10:                         0xffc7,\n    XK_F11:                         0xffc8,\n    XK_L1:                          0xffc8,\n    XK_F12:                         0xffc9,\n    XK_L2:                          0xffc9,\n    XK_F13:                         0xffca,\n    XK_L3:                          0xffca,\n    XK_F14:                         0xffcb,\n    XK_L4:                          0xffcb,\n    XK_F15:                         0xffcc,\n    XK_L5:                          0xffcc,\n    XK_F16:                         0xffcd,\n    XK_L6:                          0xffcd,\n    XK_F17:                         0xffce,\n    XK_L7:                          0xffce,\n    XK_F18:                         0xffcf,\n    XK_L8:                          0xffcf,\n    XK_F19:                         0xffd0,\n    XK_L9:                          0xffd0,\n    XK_F20:                         0xffd1,\n    XK_L10:                         0xffd1,\n    XK_F21:                         0xffd2,\n    XK_R1:                          0xffd2,\n    XK_F22:                         0xffd3,\n    XK_R2:                          0xffd3,\n    XK_F23:                         0xffd4,\n    XK_R3:                          0xffd4,\n    XK_F24:                         0xffd5,\n    XK_R4:                          0xffd5,\n    XK_F25:                         0xffd6,\n    XK_R5:                          0xffd6,\n    XK_F26:                         0xffd7,\n    XK_R6:                          0xffd7,\n    XK_F27:                         0xffd8,\n    XK_R7:                          0xffd8,\n    XK_F28:                         0xffd9,\n    XK_R8:                          0xffd9,\n    XK_F29:                         0xffda,\n    XK_R9:                          0xffda,\n    XK_F30:                         0xffdb,\n    XK_R10:                         0xffdb,\n    XK_F31:                         0xffdc,\n    XK_R11:                         0xffdc,\n    XK_F32:                         0xffdd,\n    XK_R12:                         0xffdd,\n    XK_F33:                         0xffde,\n    XK_R13:                         0xffde,\n    XK_F34:                         0xffdf,\n    XK_R14:                         0xffdf,\n    XK_F35:                         0xffe0,\n    XK_R15:                         0xffe0,\n\n    /* Modifiers */\n\n    XK_Shift_L:                     0xffe1, /* Left shift */\n    XK_Shift_R:                     0xffe2, /* Right shift */\n    XK_Control_L:                   0xffe3, /* Left control */\n    XK_Control_R:                   0xffe4, /* Right control */\n    XK_Caps_Lock:                   0xffe5, /* Caps lock */\n    XK_Shift_Lock:                  0xffe6, /* Shift lock */\n\n    XK_Meta_L:                      0xffe7, /* Left meta */\n    XK_Meta_R:                      0xffe8, /* Right meta */\n    XK_Alt_L:                       0xffe9, /* Left alt */\n    XK_Alt_R:                       0xffea, /* Right alt */\n    XK_Super_L:                     0xffeb, /* Left super */\n    XK_Super_R:                     0xffec, /* Right super */\n    XK_Hyper_L:                     0xffed, /* Left hyper */\n    XK_Hyper_R:                     0xffee, /* Right hyper */\n\n    /*\n     * Keyboard (XKB) Extension function and modifier keys\n     * (from Appendix C of \"The X Keyboard Extension: Protocol Specification\")\n     * Byte 3 = 0xfe\n     */\n\n    XK_ISO_Level3_Shift:            0xfe03, /* AltGr */\n    XK_ISO_Next_Group:              0xfe08,\n    XK_ISO_Prev_Group:              0xfe0a,\n    XK_ISO_First_Group:             0xfe0c,\n    XK_ISO_Last_Group:              0xfe0e,\n\n    /*\n     * Latin 1\n     * (ISO/IEC 8859-1: Unicode U+0020..U+00FF)\n     * Byte 3: 0\n     */\n\n    XK_space:                       0x0020, /* U+0020 SPACE */\n    XK_exclam:                      0x0021, /* U+0021 EXCLAMATION MARK */\n    XK_quotedbl:                    0x0022, /* U+0022 QUOTATION MARK */\n    XK_numbersign:                  0x0023, /* U+0023 NUMBER SIGN */\n    XK_dollar:                      0x0024, /* U+0024 DOLLAR SIGN */\n    XK_percent:                     0x0025, /* U+0025 PERCENT SIGN */\n    XK_ampersand:                   0x0026, /* U+0026 AMPERSAND */\n    XK_apostrophe:                  0x0027, /* U+0027 APOSTROPHE */\n    XK_quoteright:                  0x0027, /* deprecated */\n    XK_parenleft:                   0x0028, /* U+0028 LEFT PARENTHESIS */\n    XK_parenright:                  0x0029, /* U+0029 RIGHT PARENTHESIS */\n    XK_asterisk:                    0x002a, /* U+002A ASTERISK */\n    XK_plus:                        0x002b, /* U+002B PLUS SIGN */\n    XK_comma:                       0x002c, /* U+002C COMMA */\n    XK_minus:                       0x002d, /* U+002D HYPHEN-MINUS */\n    XK_period:                      0x002e, /* U+002E FULL STOP */\n    XK_slash:                       0x002f, /* U+002F SOLIDUS */\n    XK_0:                           0x0030, /* U+0030 DIGIT ZERO */\n    XK_1:                           0x0031, /* U+0031 DIGIT ONE */\n    XK_2:                           0x0032, /* U+0032 DIGIT TWO */\n    XK_3:                           0x0033, /* U+0033 DIGIT THREE */\n    XK_4:                           0x0034, /* U+0034 DIGIT FOUR */\n    XK_5:                           0x0035, /* U+0035 DIGIT FIVE */\n    XK_6:                           0x0036, /* U+0036 DIGIT SIX */\n    XK_7:                           0x0037, /* U+0037 DIGIT SEVEN */\n    XK_8:                           0x0038, /* U+0038 DIGIT EIGHT */\n    XK_9:                           0x0039, /* U+0039 DIGIT NINE */\n    XK_colon:                       0x003a, /* U+003A COLON */\n    XK_semicolon:                   0x003b, /* U+003B SEMICOLON */\n    XK_less:                        0x003c, /* U+003C LESS-THAN SIGN */\n    XK_equal:                       0x003d, /* U+003D EQUALS SIGN */\n    XK_greater:                     0x003e, /* U+003E GREATER-THAN SIGN */\n    XK_question:                    0x003f, /* U+003F QUESTION MARK */\n    XK_at:                          0x0040, /* U+0040 COMMERCIAL AT */\n    XK_A:                           0x0041, /* U+0041 LATIN CAPITAL LETTER A */\n    XK_B:                           0x0042, /* U+0042 LATIN CAPITAL LETTER B */\n    XK_C:                           0x0043, /* U+0043 LATIN CAPITAL LETTER C */\n    XK_D:                           0x0044, /* U+0044 LATIN CAPITAL LETTER D */\n    XK_E:                           0x0045, /* U+0045 LATIN CAPITAL LETTER E */\n    XK_F:                           0x0046, /* U+0046 LATIN CAPITAL LETTER F */\n    XK_G:                           0x0047, /* U+0047 LATIN CAPITAL LETTER G */\n    XK_H:                           0x0048, /* U+0048 LATIN CAPITAL LETTER H */\n    XK_I:                           0x0049, /* U+0049 LATIN CAPITAL LETTER I */\n    XK_J:                           0x004a, /* U+004A LATIN CAPITAL LETTER J */\n    XK_K:                           0x004b, /* U+004B LATIN CAPITAL LETTER K */\n    XK_L:                           0x004c, /* U+004C LATIN CAPITAL LETTER L */\n    XK_M:                           0x004d, /* U+004D LATIN CAPITAL LETTER M */\n    XK_N:                           0x004e, /* U+004E LATIN CAPITAL LETTER N */\n    XK_O:                           0x004f, /* U+004F LATIN CAPITAL LETTER O */\n    XK_P:                           0x0050, /* U+0050 LATIN CAPITAL LETTER P */\n    XK_Q:                           0x0051, /* U+0051 LATIN CAPITAL LETTER Q */\n    XK_R:                           0x0052, /* U+0052 LATIN CAPITAL LETTER R */\n    XK_S:                           0x0053, /* U+0053 LATIN CAPITAL LETTER S */\n    XK_T:                           0x0054, /* U+0054 LATIN CAPITAL LETTER T */\n    XK_U:                           0x0055, /* U+0055 LATIN CAPITAL LETTER U */\n    XK_V:                           0x0056, /* U+0056 LATIN CAPITAL LETTER V */\n    XK_W:                           0x0057, /* U+0057 LATIN CAPITAL LETTER W */\n    XK_X:                           0x0058, /* U+0058 LATIN CAPITAL LETTER X */\n    XK_Y:                           0x0059, /* U+0059 LATIN CAPITAL LETTER Y */\n    XK_Z:                           0x005a, /* U+005A LATIN CAPITAL LETTER Z */\n    XK_bracketleft:                 0x005b, /* U+005B LEFT SQUARE BRACKET */\n    XK_backslash:                   0x005c, /* U+005C REVERSE SOLIDUS */\n    XK_bracketright:                0x005d, /* U+005D RIGHT SQUARE BRACKET */\n    XK_asciicircum:                 0x005e, /* U+005E CIRCUMFLEX ACCENT */\n    XK_underscore:                  0x005f, /* U+005F LOW LINE */\n    XK_grave:                       0x0060, /* U+0060 GRAVE ACCENT */\n    XK_quoteleft:                   0x0060, /* deprecated */\n    XK_a:                           0x0061, /* U+0061 LATIN SMALL LETTER A */\n    XK_b:                           0x0062, /* U+0062 LATIN SMALL LETTER B */\n    XK_c:                           0x0063, /* U+0063 LATIN SMALL LETTER C */\n    XK_d:                           0x0064, /* U+0064 LATIN SMALL LETTER D */\n    XK_e:                           0x0065, /* U+0065 LATIN SMALL LETTER E */\n    XK_f:                           0x0066, /* U+0066 LATIN SMALL LETTER F */\n    XK_g:                           0x0067, /* U+0067 LATIN SMALL LETTER G */\n    XK_h:                           0x0068, /* U+0068 LATIN SMALL LETTER H */\n    XK_i:                           0x0069, /* U+0069 LATIN SMALL LETTER I */\n    XK_j:                           0x006a, /* U+006A LATIN SMALL LETTER J */\n    XK_k:                           0x006b, /* U+006B LATIN SMALL LETTER K */\n    XK_l:                           0x006c, /* U+006C LATIN SMALL LETTER L */\n    XK_m:                           0x006d, /* U+006D LATIN SMALL LETTER M */\n    XK_n:                           0x006e, /* U+006E LATIN SMALL LETTER N */\n    XK_o:                           0x006f, /* U+006F LATIN SMALL LETTER O */\n    XK_p:                           0x0070, /* U+0070 LATIN SMALL LETTER P */\n    XK_q:                           0x0071, /* U+0071 LATIN SMALL LETTER Q */\n    XK_r:                           0x0072, /* U+0072 LATIN SMALL LETTER R */\n    XK_s:                           0x0073, /* U+0073 LATIN SMALL LETTER S */\n    XK_t:                           0x0074, /* U+0074 LATIN SMALL LETTER T */\n    XK_u:                           0x0075, /* U+0075 LATIN SMALL LETTER U */\n    XK_v:                           0x0076, /* U+0076 LATIN SMALL LETTER V */\n    XK_w:                           0x0077, /* U+0077 LATIN SMALL LETTER W */\n    XK_x:                           0x0078, /* U+0078 LATIN SMALL LETTER X */\n    XK_y:                           0x0079, /* U+0079 LATIN SMALL LETTER Y */\n    XK_z:                           0x007a, /* U+007A LATIN SMALL LETTER Z */\n    XK_braceleft:                   0x007b, /* U+007B LEFT CURLY BRACKET */\n    XK_bar:                         0x007c, /* U+007C VERTICAL LINE */\n    XK_braceright:                  0x007d, /* U+007D RIGHT CURLY BRACKET */\n    XK_asciitilde:                  0x007e, /* U+007E TILDE */\n\n    XK_nobreakspace:                0x00a0, /* U+00A0 NO-BREAK SPACE */\n    XK_exclamdown:                  0x00a1, /* U+00A1 INVERTED EXCLAMATION MARK */\n    XK_cent:                        0x00a2, /* U+00A2 CENT SIGN */\n    XK_sterling:                    0x00a3, /* U+00A3 POUND SIGN */\n    XK_currency:                    0x00a4, /* U+00A4 CURRENCY SIGN */\n    XK_yen:                         0x00a5, /* U+00A5 YEN SIGN */\n    XK_brokenbar:                   0x00a6, /* U+00A6 BROKEN BAR */\n    XK_section:                     0x00a7, /* U+00A7 SECTION SIGN */\n    XK_diaeresis:                   0x00a8, /* U+00A8 DIAERESIS */\n    XK_copyright:                   0x00a9, /* U+00A9 COPYRIGHT SIGN */\n    XK_ordfeminine:                 0x00aa, /* U+00AA FEMININE ORDINAL INDICATOR */\n    XK_guillemotleft:               0x00ab, /* U+00AB LEFT-POINTING DOUBLE ANGLE QUOTATION MARK */\n    XK_notsign:                     0x00ac, /* U+00AC NOT SIGN */\n    XK_hyphen:                      0x00ad, /* U+00AD SOFT HYPHEN */\n    XK_registered:                  0x00ae, /* U+00AE REGISTERED SIGN */\n    XK_macron:                      0x00af, /* U+00AF MACRON */\n    XK_degree:                      0x00b0, /* U+00B0 DEGREE SIGN */\n    XK_plusminus:                   0x00b1, /* U+00B1 PLUS-MINUS SIGN */\n    XK_twosuperior:                 0x00b2, /* U+00B2 SUPERSCRIPT TWO */\n    XK_threesuperior:               0x00b3, /* U+00B3 SUPERSCRIPT THREE */\n    XK_acute:                       0x00b4, /* U+00B4 ACUTE ACCENT */\n    XK_mu:                          0x00b5, /* U+00B5 MICRO SIGN */\n    XK_paragraph:                   0x00b6, /* U+00B6 PILCROW SIGN */\n    XK_periodcentered:              0x00b7, /* U+00B7 MIDDLE DOT */\n    XK_cedilla:                     0x00b8, /* U+00B8 CEDILLA */\n    XK_onesuperior:                 0x00b9, /* U+00B9 SUPERSCRIPT ONE */\n    XK_masculine:                   0x00ba, /* U+00BA MASCULINE ORDINAL INDICATOR */\n    XK_guillemotright:              0x00bb, /* U+00BB RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK */\n    XK_onequarter:                  0x00bc, /* U+00BC VULGAR FRACTION ONE QUARTER */\n    XK_onehalf:                     0x00bd, /* U+00BD VULGAR FRACTION ONE HALF */\n    XK_threequarters:               0x00be, /* U+00BE VULGAR FRACTION THREE QUARTERS */\n    XK_questiondown:                0x00bf, /* U+00BF INVERTED QUESTION MARK */\n    XK_Agrave:                      0x00c0, /* U+00C0 LATIN CAPITAL LETTER A WITH GRAVE */\n    XK_Aacute:                      0x00c1, /* U+00C1 LATIN CAPITAL LETTER A WITH ACUTE */\n    XK_Acircumflex:                 0x00c2, /* U+00C2 LATIN CAPITAL LETTER A WITH CIRCUMFLEX */\n    XK_Atilde:                      0x00c3, /* U+00C3 LATIN CAPITAL LETTER A WITH TILDE */\n    XK_Adiaeresis:                  0x00c4, /* U+00C4 LATIN CAPITAL LETTER A WITH DIAERESIS */\n    XK_Aring:                       0x00c5, /* U+00C5 LATIN CAPITAL LETTER A WITH RING ABOVE */\n    XK_AE:                          0x00c6, /* U+00C6 LATIN CAPITAL LETTER AE */\n    XK_Ccedilla:                    0x00c7, /* U+00C7 LATIN CAPITAL LETTER C WITH CEDILLA */\n    XK_Egrave:                      0x00c8, /* U+00C8 LATIN CAPITAL LETTER E WITH GRAVE */\n    XK_Eacute:                      0x00c9, /* U+00C9 LATIN CAPITAL LETTER E WITH ACUTE */\n    XK_Ecircumflex:                 0x00ca, /* U+00CA LATIN CAPITAL LETTER E WITH CIRCUMFLEX */\n    XK_Ediaeresis:                  0x00cb, /* U+00CB LATIN CAPITAL LETTER E WITH DIAERESIS */\n    XK_Igrave:                      0x00cc, /* U+00CC LATIN CAPITAL LETTER I WITH GRAVE */\n    XK_Iacute:                      0x00cd, /* U+00CD LATIN CAPITAL LETTER I WITH ACUTE */\n    XK_Icircumflex:                 0x00ce, /* U+00CE LATIN CAPITAL LETTER I WITH CIRCUMFLEX */\n    XK_Idiaeresis:                  0x00cf, /* U+00CF LATIN CAPITAL LETTER I WITH DIAERESIS */\n    XK_ETH:                         0x00d0, /* U+00D0 LATIN CAPITAL LETTER ETH */\n    XK_Eth:                         0x00d0, /* deprecated */\n    XK_Ntilde:                      0x00d1, /* U+00D1 LATIN CAPITAL LETTER N WITH TILDE */\n    XK_Ograve:                      0x00d2, /* U+00D2 LATIN CAPITAL LETTER O WITH GRAVE */\n    XK_Oacute:                      0x00d3, /* U+00D3 LATIN CAPITAL LETTER O WITH ACUTE */\n    XK_Ocircumflex:                 0x00d4, /* U+00D4 LATIN CAPITAL LETTER O WITH CIRCUMFLEX */\n    XK_Otilde:                      0x00d5, /* U+00D5 LATIN CAPITAL LETTER O WITH TILDE */\n    XK_Odiaeresis:                  0x00d6, /* U+00D6 LATIN CAPITAL LETTER O WITH DIAERESIS */\n    XK_multiply:                    0x00d7, /* U+00D7 MULTIPLICATION SIGN */\n    XK_Oslash:                      0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */\n    XK_Ooblique:                    0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */\n    XK_Ugrave:                      0x00d9, /* U+00D9 LATIN CAPITAL LETTER U WITH GRAVE */\n    XK_Uacute:                      0x00da, /* U+00DA LATIN CAPITAL LETTER U WITH ACUTE */\n    XK_Ucircumflex:                 0x00db, /* U+00DB LATIN CAPITAL LETTER U WITH CIRCUMFLEX */\n    XK_Udiaeresis:                  0x00dc, /* U+00DC LATIN CAPITAL LETTER U WITH DIAERESIS */\n    XK_Yacute:                      0x00dd, /* U+00DD LATIN CAPITAL LETTER Y WITH ACUTE */\n    XK_THORN:                       0x00de, /* U+00DE LATIN CAPITAL LETTER THORN */\n    XK_Thorn:                       0x00de, /* deprecated */\n    XK_ssharp:                      0x00df, /* U+00DF LATIN SMALL LETTER SHARP S */\n    XK_agrave:                      0x00e0, /* U+00E0 LATIN SMALL LETTER A WITH GRAVE */\n    XK_aacute:                      0x00e1, /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */\n    XK_acircumflex:                 0x00e2, /* U+00E2 LATIN SMALL LETTER A WITH CIRCUMFLEX */\n    XK_atilde:                      0x00e3, /* U+00E3 LATIN SMALL LETTER A WITH TILDE */\n    XK_adiaeresis:                  0x00e4, /* U+00E4 LATIN SMALL LETTER A WITH DIAERESIS */\n    XK_aring:                       0x00e5, /* U+00E5 LATIN SMALL LETTER A WITH RING ABOVE */\n    XK_ae:                          0x00e6, /* U+00E6 LATIN SMALL LETTER AE */\n    XK_ccedilla:                    0x00e7, /* U+00E7 LATIN SMALL LETTER C WITH CEDILLA */\n    XK_egrave:                      0x00e8, /* U+00E8 LATIN SMALL LETTER E WITH GRAVE */\n    XK_eacute:                      0x00e9, /* U+00E9 LATIN SMALL LETTER E WITH ACUTE */\n    XK_ecircumflex:                 0x00ea, /* U+00EA LATIN SMALL LETTER E WITH CIRCUMFLEX */\n    XK_ediaeresis:                  0x00eb, /* U+00EB LATIN SMALL LETTER E WITH DIAERESIS */\n    XK_igrave:                      0x00ec, /* U+00EC LATIN SMALL LETTER I WITH GRAVE */\n    XK_iacute:                      0x00ed, /* U+00ED LATIN SMALL LETTER I WITH ACUTE */\n    XK_icircumflex:                 0x00ee, /* U+00EE LATIN SMALL LETTER I WITH CIRCUMFLEX */\n    XK_idiaeresis:                  0x00ef, /* U+00EF LATIN SMALL LETTER I WITH DIAERESIS */\n    XK_eth:                         0x00f0, /* U+00F0 LATIN SMALL LETTER ETH */\n    XK_ntilde:                      0x00f1, /* U+00F1 LATIN SMALL LETTER N WITH TILDE */\n    XK_ograve:                      0x00f2, /* U+00F2 LATIN SMALL LETTER O WITH GRAVE */\n    XK_oacute:                      0x00f3, /* U+00F3 LATIN SMALL LETTER O WITH ACUTE */\n    XK_ocircumflex:                 0x00f4, /* U+00F4 LATIN SMALL LETTER O WITH CIRCUMFLEX */\n    XK_otilde:                      0x00f5, /* U+00F5 LATIN SMALL LETTER O WITH TILDE */\n    XK_odiaeresis:                  0x00f6, /* U+00F6 LATIN SMALL LETTER O WITH DIAERESIS */\n    XK_division:                    0x00f7, /* U+00F7 DIVISION SIGN */\n    XK_oslash:                      0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */\n    XK_ooblique:                    0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */\n    XK_ugrave:                      0x00f9, /* U+00F9 LATIN SMALL LETTER U WITH GRAVE */\n    XK_uacute:                      0x00fa, /* U+00FA LATIN SMALL LETTER U WITH ACUTE */\n    XK_ucircumflex:                 0x00fb, /* U+00FB LATIN SMALL LETTER U WITH CIRCUMFLEX */\n    XK_udiaeresis:                  0x00fc, /* U+00FC LATIN SMALL LETTER U WITH DIAERESIS */\n    XK_yacute:                      0x00fd, /* U+00FD LATIN SMALL LETTER Y WITH ACUTE */\n    XK_thorn:                       0x00fe, /* U+00FE LATIN SMALL LETTER THORN */\n    XK_ydiaeresis:                  0x00ff, /* U+00FF LATIN SMALL LETTER Y WITH DIAERESIS */\n\n    /*\n     * Korean\n     * Byte 3 = 0x0e\n     */\n\n    XK_Hangul:                      0xff31, /* Hangul start/stop(toggle) */\n    XK_Hangul_Hanja:                0xff34, /* Start Hangul->Hanja Conversion */\n    XK_Hangul_Jeonja:               0xff38, /* Jeonja mode */\n\n    /*\n     * XFree86 vendor specific keysyms.\n     *\n     * The XFree86 keysym range is 0x10080001 - 0x1008FFFF.\n     */\n\n    XF86XK_ModeLock:                0x1008FF01,\n    XF86XK_MonBrightnessUp:         0x1008FF02,\n    XF86XK_MonBrightnessDown:       0x1008FF03,\n    XF86XK_KbdLightOnOff:           0x1008FF04,\n    XF86XK_KbdBrightnessUp:         0x1008FF05,\n    XF86XK_KbdBrightnessDown:       0x1008FF06,\n    XF86XK_Standby:                 0x1008FF10,\n    XF86XK_AudioLowerVolume:        0x1008FF11,\n    XF86XK_AudioMute:               0x1008FF12,\n    XF86XK_AudioRaiseVolume:        0x1008FF13,\n    XF86XK_AudioPlay:               0x1008FF14,\n    XF86XK_AudioStop:               0x1008FF15,\n    XF86XK_AudioPrev:               0x1008FF16,\n    XF86XK_AudioNext:               0x1008FF17,\n    XF86XK_HomePage:                0x1008FF18,\n    XF86XK_Mail:                    0x1008FF19,\n    XF86XK_Start:                   0x1008FF1A,\n    XF86XK_Search:                  0x1008FF1B,\n    XF86XK_AudioRecord:             0x1008FF1C,\n    XF86XK_Calculator:              0x1008FF1D,\n    XF86XK_Memo:                    0x1008FF1E,\n    XF86XK_ToDoList:                0x1008FF1F,\n    XF86XK_Calendar:                0x1008FF20,\n    XF86XK_PowerDown:               0x1008FF21,\n    XF86XK_ContrastAdjust:          0x1008FF22,\n    XF86XK_RockerUp:                0x1008FF23,\n    XF86XK_RockerDown:              0x1008FF24,\n    XF86XK_RockerEnter:             0x1008FF25,\n    XF86XK_Back:                    0x1008FF26,\n    XF86XK_Forward:                 0x1008FF27,\n    XF86XK_Stop:                    0x1008FF28,\n    XF86XK_Refresh:                 0x1008FF29,\n    XF86XK_PowerOff:                0x1008FF2A,\n    XF86XK_WakeUp:                  0x1008FF2B,\n    XF86XK_Eject:                   0x1008FF2C,\n    XF86XK_ScreenSaver:             0x1008FF2D,\n    XF86XK_WWW:                     0x1008FF2E,\n    XF86XK_Sleep:                   0x1008FF2F,\n    XF86XK_Favorites:               0x1008FF30,\n    XF86XK_AudioPause:              0x1008FF31,\n    XF86XK_AudioMedia:              0x1008FF32,\n    XF86XK_MyComputer:              0x1008FF33,\n    XF86XK_VendorHome:              0x1008FF34,\n    XF86XK_LightBulb:               0x1008FF35,\n    XF86XK_Shop:                    0x1008FF36,\n    XF86XK_History:                 0x1008FF37,\n    XF86XK_OpenURL:                 0x1008FF38,\n    XF86XK_AddFavorite:             0x1008FF39,\n    XF86XK_HotLinks:                0x1008FF3A,\n    XF86XK_BrightnessAdjust:        0x1008FF3B,\n    XF86XK_Finance:                 0x1008FF3C,\n    XF86XK_Community:               0x1008FF3D,\n    XF86XK_AudioRewind:             0x1008FF3E,\n    XF86XK_BackForward:             0x1008FF3F,\n    XF86XK_Launch0:                 0x1008FF40,\n    XF86XK_Launch1:                 0x1008FF41,\n    XF86XK_Launch2:                 0x1008FF42,\n    XF86XK_Launch3:                 0x1008FF43,\n    XF86XK_Launch4:                 0x1008FF44,\n    XF86XK_Launch5:                 0x1008FF45,\n    XF86XK_Launch6:                 0x1008FF46,\n    XF86XK_Launch7:                 0x1008FF47,\n    XF86XK_Launch8:                 0x1008FF48,\n    XF86XK_Launch9:                 0x1008FF49,\n    XF86XK_LaunchA:                 0x1008FF4A,\n    XF86XK_LaunchB:                 0x1008FF4B,\n    XF86XK_LaunchC:                 0x1008FF4C,\n    XF86XK_LaunchD:                 0x1008FF4D,\n    XF86XK_LaunchE:                 0x1008FF4E,\n    XF86XK_LaunchF:                 0x1008FF4F,\n    XF86XK_ApplicationLeft:         0x1008FF50,\n    XF86XK_ApplicationRight:        0x1008FF51,\n    XF86XK_Book:                    0x1008FF52,\n    XF86XK_CD:                      0x1008FF53,\n    XF86XK_Calculater:              0x1008FF54,\n    XF86XK_Clear:                   0x1008FF55,\n    XF86XK_Close:                   0x1008FF56,\n    XF86XK_Copy:                    0x1008FF57,\n    XF86XK_Cut:                     0x1008FF58,\n    XF86XK_Display:                 0x1008FF59,\n    XF86XK_DOS:                     0x1008FF5A,\n    XF86XK_Documents:               0x1008FF5B,\n    XF86XK_Excel:                   0x1008FF5C,\n    XF86XK_Explorer:                0x1008FF5D,\n    XF86XK_Game:                    0x1008FF5E,\n    XF86XK_Go:                      0x1008FF5F,\n    XF86XK_iTouch:                  0x1008FF60,\n    XF86XK_LogOff:                  0x1008FF61,\n    XF86XK_Market:                  0x1008FF62,\n    XF86XK_Meeting:                 0x1008FF63,\n    XF86XK_MenuKB:                  0x1008FF65,\n    XF86XK_MenuPB:                  0x1008FF66,\n    XF86XK_MySites:                 0x1008FF67,\n    XF86XK_New:                     0x1008FF68,\n    XF86XK_News:                    0x1008FF69,\n    XF86XK_OfficeHome:              0x1008FF6A,\n    XF86XK_Open:                    0x1008FF6B,\n    XF86XK_Option:                  0x1008FF6C,\n    XF86XK_Paste:                   0x1008FF6D,\n    XF86XK_Phone:                   0x1008FF6E,\n    XF86XK_Q:                       0x1008FF70,\n    XF86XK_Reply:                   0x1008FF72,\n    XF86XK_Reload:                  0x1008FF73,\n    XF86XK_RotateWindows:           0x1008FF74,\n    XF86XK_RotationPB:              0x1008FF75,\n    XF86XK_RotationKB:              0x1008FF76,\n    XF86XK_Save:                    0x1008FF77,\n    XF86XK_ScrollUp:                0x1008FF78,\n    XF86XK_ScrollDown:              0x1008FF79,\n    XF86XK_ScrollClick:             0x1008FF7A,\n    XF86XK_Send:                    0x1008FF7B,\n    XF86XK_Spell:                   0x1008FF7C,\n    XF86XK_SplitScreen:             0x1008FF7D,\n    XF86XK_Support:                 0x1008FF7E,\n    XF86XK_TaskPane:                0x1008FF7F,\n    XF86XK_Terminal:                0x1008FF80,\n    XF86XK_Tools:                   0x1008FF81,\n    XF86XK_Travel:                  0x1008FF82,\n    XF86XK_UserPB:                  0x1008FF84,\n    XF86XK_User1KB:                 0x1008FF85,\n    XF86XK_User2KB:                 0x1008FF86,\n    XF86XK_Video:                   0x1008FF87,\n    XF86XK_WheelButton:             0x1008FF88,\n    XF86XK_Word:                    0x1008FF89,\n    XF86XK_Xfer:                    0x1008FF8A,\n    XF86XK_ZoomIn:                  0x1008FF8B,\n    XF86XK_ZoomOut:                 0x1008FF8C,\n    XF86XK_Away:                    0x1008FF8D,\n    XF86XK_Messenger:               0x1008FF8E,\n    XF86XK_WebCam:                  0x1008FF8F,\n    XF86XK_MailForward:             0x1008FF90,\n    XF86XK_Pictures:                0x1008FF91,\n    XF86XK_Music:                   0x1008FF92,\n    XF86XK_Battery:                 0x1008FF93,\n    XF86XK_Bluetooth:               0x1008FF94,\n    XF86XK_WLAN:                    0x1008FF95,\n    XF86XK_UWB:                     0x1008FF96,\n    XF86XK_AudioForward:            0x1008FF97,\n    XF86XK_AudioRepeat:             0x1008FF98,\n    XF86XK_AudioRandomPlay:         0x1008FF99,\n    XF86XK_Subtitle:                0x1008FF9A,\n    XF86XK_AudioCycleTrack:         0x1008FF9B,\n    XF86XK_CycleAngle:              0x1008FF9C,\n    XF86XK_FrameBack:               0x1008FF9D,\n    XF86XK_FrameForward:            0x1008FF9E,\n    XF86XK_Time:                    0x1008FF9F,\n    XF86XK_Select:                  0x1008FFA0,\n    XF86XK_View:                    0x1008FFA1,\n    XF86XK_TopMenu:                 0x1008FFA2,\n    XF86XK_Red:                     0x1008FFA3,\n    XF86XK_Green:                   0x1008FFA4,\n    XF86XK_Yellow:                  0x1008FFA5,\n    XF86XK_Blue:                    0x1008FFA6,\n    XF86XK_Suspend:                 0x1008FFA7,\n    XF86XK_Hibernate:               0x1008FFA8,\n    XF86XK_TouchpadToggle:          0x1008FFA9,\n    XF86XK_TouchpadOn:              0x1008FFB0,\n    XF86XK_TouchpadOff:             0x1008FFB1,\n    XF86XK_AudioMicMute:            0x1008FFB2,\n    XF86XK_Switch_VT_1:             0x1008FE01,\n    XF86XK_Switch_VT_2:             0x1008FE02,\n    XF86XK_Switch_VT_3:             0x1008FE03,\n    XF86XK_Switch_VT_4:             0x1008FE04,\n    XF86XK_Switch_VT_5:             0x1008FE05,\n    XF86XK_Switch_VT_6:             0x1008FE06,\n    XF86XK_Switch_VT_7:             0x1008FE07,\n    XF86XK_Switch_VT_8:             0x1008FE08,\n    XF86XK_Switch_VT_9:             0x1008FE09,\n    XF86XK_Switch_VT_10:            0x1008FE0A,\n    XF86XK_Switch_VT_11:            0x1008FE0B,\n    XF86XK_Switch_VT_12:            0x1008FE0C,\n    XF86XK_Ungrab:                  0x1008FE20,\n    XF86XK_ClearGrab:               0x1008FE21,\n    XF86XK_Next_VMode:              0x1008FE22,\n    XF86XK_Prev_VMode:              0x1008FE23,\n    XF86XK_LogWindowTree:           0x1008FE24,\n    XF86XK_LogGrabInfo:             0x1008FE25,\n};\n"
  },
  {
    "path": "core/input/keysymdef.js",
    "content": "/*\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\nconst codepoints = {\n    0x0100: 0x03c0, // XK_Amacron\n    0x0101: 0x03e0, // XK_amacron\n    0x0102: 0x01c3, // XK_Abreve\n    0x0103: 0x01e3, // XK_abreve\n    0x0104: 0x01a1, // XK_Aogonek\n    0x0105: 0x01b1, // XK_aogonek\n    0x0106: 0x01c6, // XK_Cacute\n    0x0107: 0x01e6, // XK_cacute\n    0x0108: 0x02c6, // XK_Ccircumflex\n    0x0109: 0x02e6, // XK_ccircumflex\n    0x010a: 0x02c5, // XK_Cabovedot\n    0x010b: 0x02e5, // XK_cabovedot\n    0x010c: 0x01c8, // XK_Ccaron\n    0x010d: 0x01e8, // XK_ccaron\n    0x010e: 0x01cf, // XK_Dcaron\n    0x010f: 0x01ef, // XK_dcaron\n    0x0110: 0x01d0, // XK_Dstroke\n    0x0111: 0x01f0, // XK_dstroke\n    0x0112: 0x03aa, // XK_Emacron\n    0x0113: 0x03ba, // XK_emacron\n    0x0116: 0x03cc, // XK_Eabovedot\n    0x0117: 0x03ec, // XK_eabovedot\n    0x0118: 0x01ca, // XK_Eogonek\n    0x0119: 0x01ea, // XK_eogonek\n    0x011a: 0x01cc, // XK_Ecaron\n    0x011b: 0x01ec, // XK_ecaron\n    0x011c: 0x02d8, // XK_Gcircumflex\n    0x011d: 0x02f8, // XK_gcircumflex\n    0x011e: 0x02ab, // XK_Gbreve\n    0x011f: 0x02bb, // XK_gbreve\n    0x0120: 0x02d5, // XK_Gabovedot\n    0x0121: 0x02f5, // XK_gabovedot\n    0x0122: 0x03ab, // XK_Gcedilla\n    0x0123: 0x03bb, // XK_gcedilla\n    0x0124: 0x02a6, // XK_Hcircumflex\n    0x0125: 0x02b6, // XK_hcircumflex\n    0x0126: 0x02a1, // XK_Hstroke\n    0x0127: 0x02b1, // XK_hstroke\n    0x0128: 0x03a5, // XK_Itilde\n    0x0129: 0x03b5, // XK_itilde\n    0x012a: 0x03cf, // XK_Imacron\n    0x012b: 0x03ef, // XK_imacron\n    0x012e: 0x03c7, // XK_Iogonek\n    0x012f: 0x03e7, // XK_iogonek\n    0x0130: 0x02a9, // XK_Iabovedot\n    0x0131: 0x02b9, // XK_idotless\n    0x0134: 0x02ac, // XK_Jcircumflex\n    0x0135: 0x02bc, // XK_jcircumflex\n    0x0136: 0x03d3, // XK_Kcedilla\n    0x0137: 0x03f3, // XK_kcedilla\n    0x0138: 0x03a2, // XK_kra\n    0x0139: 0x01c5, // XK_Lacute\n    0x013a: 0x01e5, // XK_lacute\n    0x013b: 0x03a6, // XK_Lcedilla\n    0x013c: 0x03b6, // XK_lcedilla\n    0x013d: 0x01a5, // XK_Lcaron\n    0x013e: 0x01b5, // XK_lcaron\n    0x0141: 0x01a3, // XK_Lstroke\n    0x0142: 0x01b3, // XK_lstroke\n    0x0143: 0x01d1, // XK_Nacute\n    0x0144: 0x01f1, // XK_nacute\n    0x0145: 0x03d1, // XK_Ncedilla\n    0x0146: 0x03f1, // XK_ncedilla\n    0x0147: 0x01d2, // XK_Ncaron\n    0x0148: 0x01f2, // XK_ncaron\n    0x014a: 0x03bd, // XK_ENG\n    0x014b: 0x03bf, // XK_eng\n    0x014c: 0x03d2, // XK_Omacron\n    0x014d: 0x03f2, // XK_omacron\n    0x0150: 0x01d5, // XK_Odoubleacute\n    0x0151: 0x01f5, // XK_odoubleacute\n    0x0152: 0x13bc, // XK_OE\n    0x0153: 0x13bd, // XK_oe\n    0x0154: 0x01c0, // XK_Racute\n    0x0155: 0x01e0, // XK_racute\n    0x0156: 0x03a3, // XK_Rcedilla\n    0x0157: 0x03b3, // XK_rcedilla\n    0x0158: 0x01d8, // XK_Rcaron\n    0x0159: 0x01f8, // XK_rcaron\n    0x015a: 0x01a6, // XK_Sacute\n    0x015b: 0x01b6, // XK_sacute\n    0x015c: 0x02de, // XK_Scircumflex\n    0x015d: 0x02fe, // XK_scircumflex\n    0x015e: 0x01aa, // XK_Scedilla\n    0x015f: 0x01ba, // XK_scedilla\n    0x0160: 0x01a9, // XK_Scaron\n    0x0161: 0x01b9, // XK_scaron\n    0x0162: 0x01de, // XK_Tcedilla\n    0x0163: 0x01fe, // XK_tcedilla\n    0x0164: 0x01ab, // XK_Tcaron\n    0x0165: 0x01bb, // XK_tcaron\n    0x0166: 0x03ac, // XK_Tslash\n    0x0167: 0x03bc, // XK_tslash\n    0x0168: 0x03dd, // XK_Utilde\n    0x0169: 0x03fd, // XK_utilde\n    0x016a: 0x03de, // XK_Umacron\n    0x016b: 0x03fe, // XK_umacron\n    0x016c: 0x02dd, // XK_Ubreve\n    0x016d: 0x02fd, // XK_ubreve\n    0x016e: 0x01d9, // XK_Uring\n    0x016f: 0x01f9, // XK_uring\n    0x0170: 0x01db, // XK_Udoubleacute\n    0x0171: 0x01fb, // XK_udoubleacute\n    0x0172: 0x03d9, // XK_Uogonek\n    0x0173: 0x03f9, // XK_uogonek\n    0x0178: 0x13be, // XK_Ydiaeresis\n    0x0179: 0x01ac, // XK_Zacute\n    0x017a: 0x01bc, // XK_zacute\n    0x017b: 0x01af, // XK_Zabovedot\n    0x017c: 0x01bf, // XK_zabovedot\n    0x017d: 0x01ae, // XK_Zcaron\n    0x017e: 0x01be, // XK_zcaron\n    0x0192: 0x08f6, // XK_function\n    0x01d2: 0x10001d1, // XK_Ocaron\n    0x02c7: 0x01b7, // XK_caron\n    0x02d8: 0x01a2, // XK_breve\n    0x02d9: 0x01ff, // XK_abovedot\n    0x02db: 0x01b2, // XK_ogonek\n    0x02dd: 0x01bd, // XK_doubleacute\n    0x0385: 0x07ae, // XK_Greek_accentdieresis\n    0x0386: 0x07a1, // XK_Greek_ALPHAaccent\n    0x0388: 0x07a2, // XK_Greek_EPSILONaccent\n    0x0389: 0x07a3, // XK_Greek_ETAaccent\n    0x038a: 0x07a4, // XK_Greek_IOTAaccent\n    0x038c: 0x07a7, // XK_Greek_OMICRONaccent\n    0x038e: 0x07a8, // XK_Greek_UPSILONaccent\n    0x038f: 0x07ab, // XK_Greek_OMEGAaccent\n    0x0390: 0x07b6, // XK_Greek_iotaaccentdieresis\n    0x0391: 0x07c1, // XK_Greek_ALPHA\n    0x0392: 0x07c2, // XK_Greek_BETA\n    0x0393: 0x07c3, // XK_Greek_GAMMA\n    0x0394: 0x07c4, // XK_Greek_DELTA\n    0x0395: 0x07c5, // XK_Greek_EPSILON\n    0x0396: 0x07c6, // XK_Greek_ZETA\n    0x0397: 0x07c7, // XK_Greek_ETA\n    0x0398: 0x07c8, // XK_Greek_THETA\n    0x0399: 0x07c9, // XK_Greek_IOTA\n    0x039a: 0x07ca, // XK_Greek_KAPPA\n    0x039b: 0x07cb, // XK_Greek_LAMDA\n    0x039c: 0x07cc, // XK_Greek_MU\n    0x039d: 0x07cd, // XK_Greek_NU\n    0x039e: 0x07ce, // XK_Greek_XI\n    0x039f: 0x07cf, // XK_Greek_OMICRON\n    0x03a0: 0x07d0, // XK_Greek_PI\n    0x03a1: 0x07d1, // XK_Greek_RHO\n    0x03a3: 0x07d2, // XK_Greek_SIGMA\n    0x03a4: 0x07d4, // XK_Greek_TAU\n    0x03a5: 0x07d5, // XK_Greek_UPSILON\n    0x03a6: 0x07d6, // XK_Greek_PHI\n    0x03a7: 0x07d7, // XK_Greek_CHI\n    0x03a8: 0x07d8, // XK_Greek_PSI\n    0x03a9: 0x07d9, // XK_Greek_OMEGA\n    0x03aa: 0x07a5, // XK_Greek_IOTAdieresis\n    0x03ab: 0x07a9, // XK_Greek_UPSILONdieresis\n    0x03ac: 0x07b1, // XK_Greek_alphaaccent\n    0x03ad: 0x07b2, // XK_Greek_epsilonaccent\n    0x03ae: 0x07b3, // XK_Greek_etaaccent\n    0x03af: 0x07b4, // XK_Greek_iotaaccent\n    0x03b0: 0x07ba, // XK_Greek_upsilonaccentdieresis\n    0x03b1: 0x07e1, // XK_Greek_alpha\n    0x03b2: 0x07e2, // XK_Greek_beta\n    0x03b3: 0x07e3, // XK_Greek_gamma\n    0x03b4: 0x07e4, // XK_Greek_delta\n    0x03b5: 0x07e5, // XK_Greek_epsilon\n    0x03b6: 0x07e6, // XK_Greek_zeta\n    0x03b7: 0x07e7, // XK_Greek_eta\n    0x03b8: 0x07e8, // XK_Greek_theta\n    0x03b9: 0x07e9, // XK_Greek_iota\n    0x03ba: 0x07ea, // XK_Greek_kappa\n    0x03bb: 0x07eb, // XK_Greek_lamda\n    0x03bc: 0x07ec, // XK_Greek_mu\n    0x03bd: 0x07ed, // XK_Greek_nu\n    0x03be: 0x07ee, // XK_Greek_xi\n    0x03bf: 0x07ef, // XK_Greek_omicron\n    0x03c0: 0x07f0, // XK_Greek_pi\n    0x03c1: 0x07f1, // XK_Greek_rho\n    0x03c2: 0x07f3, // XK_Greek_finalsmallsigma\n    0x03c3: 0x07f2, // XK_Greek_sigma\n    0x03c4: 0x07f4, // XK_Greek_tau\n    0x03c5: 0x07f5, // XK_Greek_upsilon\n    0x03c6: 0x07f6, // XK_Greek_phi\n    0x03c7: 0x07f7, // XK_Greek_chi\n    0x03c8: 0x07f8, // XK_Greek_psi\n    0x03c9: 0x07f9, // XK_Greek_omega\n    0x03ca: 0x07b5, // XK_Greek_iotadieresis\n    0x03cb: 0x07b9, // XK_Greek_upsilondieresis\n    0x03cc: 0x07b7, // XK_Greek_omicronaccent\n    0x03cd: 0x07b8, // XK_Greek_upsilonaccent\n    0x03ce: 0x07bb, // XK_Greek_omegaaccent\n    0x0401: 0x06b3, // XK_Cyrillic_IO\n    0x0402: 0x06b1, // XK_Serbian_DJE\n    0x0403: 0x06b2, // XK_Macedonia_GJE\n    0x0404: 0x06b4, // XK_Ukrainian_IE\n    0x0405: 0x06b5, // XK_Macedonia_DSE\n    0x0406: 0x06b6, // XK_Ukrainian_I\n    0x0407: 0x06b7, // XK_Ukrainian_YI\n    0x0408: 0x06b8, // XK_Cyrillic_JE\n    0x0409: 0x06b9, // XK_Cyrillic_LJE\n    0x040a: 0x06ba, // XK_Cyrillic_NJE\n    0x040b: 0x06bb, // XK_Serbian_TSHE\n    0x040c: 0x06bc, // XK_Macedonia_KJE\n    0x040e: 0x06be, // XK_Byelorussian_SHORTU\n    0x040f: 0x06bf, // XK_Cyrillic_DZHE\n    0x0410: 0x06e1, // XK_Cyrillic_A\n    0x0411: 0x06e2, // XK_Cyrillic_BE\n    0x0412: 0x06f7, // XK_Cyrillic_VE\n    0x0413: 0x06e7, // XK_Cyrillic_GHE\n    0x0414: 0x06e4, // XK_Cyrillic_DE\n    0x0415: 0x06e5, // XK_Cyrillic_IE\n    0x0416: 0x06f6, // XK_Cyrillic_ZHE\n    0x0417: 0x06fa, // XK_Cyrillic_ZE\n    0x0418: 0x06e9, // XK_Cyrillic_I\n    0x0419: 0x06ea, // XK_Cyrillic_SHORTI\n    0x041a: 0x06eb, // XK_Cyrillic_KA\n    0x041b: 0x06ec, // XK_Cyrillic_EL\n    0x041c: 0x06ed, // XK_Cyrillic_EM\n    0x041d: 0x06ee, // XK_Cyrillic_EN\n    0x041e: 0x06ef, // XK_Cyrillic_O\n    0x041f: 0x06f0, // XK_Cyrillic_PE\n    0x0420: 0x06f2, // XK_Cyrillic_ER\n    0x0421: 0x06f3, // XK_Cyrillic_ES\n    0x0422: 0x06f4, // XK_Cyrillic_TE\n    0x0423: 0x06f5, // XK_Cyrillic_U\n    0x0424: 0x06e6, // XK_Cyrillic_EF\n    0x0425: 0x06e8, // XK_Cyrillic_HA\n    0x0426: 0x06e3, // XK_Cyrillic_TSE\n    0x0427: 0x06fe, // XK_Cyrillic_CHE\n    0x0428: 0x06fb, // XK_Cyrillic_SHA\n    0x0429: 0x06fd, // XK_Cyrillic_SHCHA\n    0x042a: 0x06ff, // XK_Cyrillic_HARDSIGN\n    0x042b: 0x06f9, // XK_Cyrillic_YERU\n    0x042c: 0x06f8, // XK_Cyrillic_SOFTSIGN\n    0x042d: 0x06fc, // XK_Cyrillic_E\n    0x042e: 0x06e0, // XK_Cyrillic_YU\n    0x042f: 0x06f1, // XK_Cyrillic_YA\n    0x0430: 0x06c1, // XK_Cyrillic_a\n    0x0431: 0x06c2, // XK_Cyrillic_be\n    0x0432: 0x06d7, // XK_Cyrillic_ve\n    0x0433: 0x06c7, // XK_Cyrillic_ghe\n    0x0434: 0x06c4, // XK_Cyrillic_de\n    0x0435: 0x06c5, // XK_Cyrillic_ie\n    0x0436: 0x06d6, // XK_Cyrillic_zhe\n    0x0437: 0x06da, // XK_Cyrillic_ze\n    0x0438: 0x06c9, // XK_Cyrillic_i\n    0x0439: 0x06ca, // XK_Cyrillic_shorti\n    0x043a: 0x06cb, // XK_Cyrillic_ka\n    0x043b: 0x06cc, // XK_Cyrillic_el\n    0x043c: 0x06cd, // XK_Cyrillic_em\n    0x043d: 0x06ce, // XK_Cyrillic_en\n    0x043e: 0x06cf, // XK_Cyrillic_o\n    0x043f: 0x06d0, // XK_Cyrillic_pe\n    0x0440: 0x06d2, // XK_Cyrillic_er\n    0x0441: 0x06d3, // XK_Cyrillic_es\n    0x0442: 0x06d4, // XK_Cyrillic_te\n    0x0443: 0x06d5, // XK_Cyrillic_u\n    0x0444: 0x06c6, // XK_Cyrillic_ef\n    0x0445: 0x06c8, // XK_Cyrillic_ha\n    0x0446: 0x06c3, // XK_Cyrillic_tse\n    0x0447: 0x06de, // XK_Cyrillic_che\n    0x0448: 0x06db, // XK_Cyrillic_sha\n    0x0449: 0x06dd, // XK_Cyrillic_shcha\n    0x044a: 0x06df, // XK_Cyrillic_hardsign\n    0x044b: 0x06d9, // XK_Cyrillic_yeru\n    0x044c: 0x06d8, // XK_Cyrillic_softsign\n    0x044d: 0x06dc, // XK_Cyrillic_e\n    0x044e: 0x06c0, // XK_Cyrillic_yu\n    0x044f: 0x06d1, // XK_Cyrillic_ya\n    0x0451: 0x06a3, // XK_Cyrillic_io\n    0x0452: 0x06a1, // XK_Serbian_dje\n    0x0453: 0x06a2, // XK_Macedonia_gje\n    0x0454: 0x06a4, // XK_Ukrainian_ie\n    0x0455: 0x06a5, // XK_Macedonia_dse\n    0x0456: 0x06a6, // XK_Ukrainian_i\n    0x0457: 0x06a7, // XK_Ukrainian_yi\n    0x0458: 0x06a8, // XK_Cyrillic_je\n    0x0459: 0x06a9, // XK_Cyrillic_lje\n    0x045a: 0x06aa, // XK_Cyrillic_nje\n    0x045b: 0x06ab, // XK_Serbian_tshe\n    0x045c: 0x06ac, // XK_Macedonia_kje\n    0x045e: 0x06ae, // XK_Byelorussian_shortu\n    0x045f: 0x06af, // XK_Cyrillic_dzhe\n    0x0490: 0x06bd, // XK_Ukrainian_GHE_WITH_UPTURN\n    0x0491: 0x06ad, // XK_Ukrainian_ghe_with_upturn\n    0x05d0: 0x0ce0, // XK_hebrew_aleph\n    0x05d1: 0x0ce1, // XK_hebrew_bet\n    0x05d2: 0x0ce2, // XK_hebrew_gimel\n    0x05d3: 0x0ce3, // XK_hebrew_dalet\n    0x05d4: 0x0ce4, // XK_hebrew_he\n    0x05d5: 0x0ce5, // XK_hebrew_waw\n    0x05d6: 0x0ce6, // XK_hebrew_zain\n    0x05d7: 0x0ce7, // XK_hebrew_chet\n    0x05d8: 0x0ce8, // XK_hebrew_tet\n    0x05d9: 0x0ce9, // XK_hebrew_yod\n    0x05da: 0x0cea, // XK_hebrew_finalkaph\n    0x05db: 0x0ceb, // XK_hebrew_kaph\n    0x05dc: 0x0cec, // XK_hebrew_lamed\n    0x05dd: 0x0ced, // XK_hebrew_finalmem\n    0x05de: 0x0cee, // XK_hebrew_mem\n    0x05df: 0x0cef, // XK_hebrew_finalnun\n    0x05e0: 0x0cf0, // XK_hebrew_nun\n    0x05e1: 0x0cf1, // XK_hebrew_samech\n    0x05e2: 0x0cf2, // XK_hebrew_ayin\n    0x05e3: 0x0cf3, // XK_hebrew_finalpe\n    0x05e4: 0x0cf4, // XK_hebrew_pe\n    0x05e5: 0x0cf5, // XK_hebrew_finalzade\n    0x05e6: 0x0cf6, // XK_hebrew_zade\n    0x05e7: 0x0cf7, // XK_hebrew_qoph\n    0x05e8: 0x0cf8, // XK_hebrew_resh\n    0x05e9: 0x0cf9, // XK_hebrew_shin\n    0x05ea: 0x0cfa, // XK_hebrew_taw\n    0x060c: 0x05ac, // XK_Arabic_comma\n    0x061b: 0x05bb, // XK_Arabic_semicolon\n    0x061f: 0x05bf, // XK_Arabic_question_mark\n    0x0621: 0x05c1, // XK_Arabic_hamza\n    0x0622: 0x05c2, // XK_Arabic_maddaonalef\n    0x0623: 0x05c3, // XK_Arabic_hamzaonalef\n    0x0624: 0x05c4, // XK_Arabic_hamzaonwaw\n    0x0625: 0x05c5, // XK_Arabic_hamzaunderalef\n    0x0626: 0x05c6, // XK_Arabic_hamzaonyeh\n    0x0627: 0x05c7, // XK_Arabic_alef\n    0x0628: 0x05c8, // XK_Arabic_beh\n    0x0629: 0x05c9, // XK_Arabic_tehmarbuta\n    0x062a: 0x05ca, // XK_Arabic_teh\n    0x062b: 0x05cb, // XK_Arabic_theh\n    0x062c: 0x05cc, // XK_Arabic_jeem\n    0x062d: 0x05cd, // XK_Arabic_hah\n    0x062e: 0x05ce, // XK_Arabic_khah\n    0x062f: 0x05cf, // XK_Arabic_dal\n    0x0630: 0x05d0, // XK_Arabic_thal\n    0x0631: 0x05d1, // XK_Arabic_ra\n    0x0632: 0x05d2, // XK_Arabic_zain\n    0x0633: 0x05d3, // XK_Arabic_seen\n    0x0634: 0x05d4, // XK_Arabic_sheen\n    0x0635: 0x05d5, // XK_Arabic_sad\n    0x0636: 0x05d6, // XK_Arabic_dad\n    0x0637: 0x05d7, // XK_Arabic_tah\n    0x0638: 0x05d8, // XK_Arabic_zah\n    0x0639: 0x05d9, // XK_Arabic_ain\n    0x063a: 0x05da, // XK_Arabic_ghain\n    0x0640: 0x05e0, // XK_Arabic_tatweel\n    0x0641: 0x05e1, // XK_Arabic_feh\n    0x0642: 0x05e2, // XK_Arabic_qaf\n    0x0643: 0x05e3, // XK_Arabic_kaf\n    0x0644: 0x05e4, // XK_Arabic_lam\n    0x0645: 0x05e5, // XK_Arabic_meem\n    0x0646: 0x05e6, // XK_Arabic_noon\n    0x0647: 0x05e7, // XK_Arabic_ha\n    0x0648: 0x05e8, // XK_Arabic_waw\n    0x0649: 0x05e9, // XK_Arabic_alefmaksura\n    0x064a: 0x05ea, // XK_Arabic_yeh\n    0x064b: 0x05eb, // XK_Arabic_fathatan\n    0x064c: 0x05ec, // XK_Arabic_dammatan\n    0x064d: 0x05ed, // XK_Arabic_kasratan\n    0x064e: 0x05ee, // XK_Arabic_fatha\n    0x064f: 0x05ef, // XK_Arabic_damma\n    0x0650: 0x05f0, // XK_Arabic_kasra\n    0x0651: 0x05f1, // XK_Arabic_shadda\n    0x0652: 0x05f2, // XK_Arabic_sukun\n    0x0e01: 0x0da1, // XK_Thai_kokai\n    0x0e02: 0x0da2, // XK_Thai_khokhai\n    0x0e03: 0x0da3, // XK_Thai_khokhuat\n    0x0e04: 0x0da4, // XK_Thai_khokhwai\n    0x0e05: 0x0da5, // XK_Thai_khokhon\n    0x0e06: 0x0da6, // XK_Thai_khorakhang\n    0x0e07: 0x0da7, // XK_Thai_ngongu\n    0x0e08: 0x0da8, // XK_Thai_chochan\n    0x0e09: 0x0da9, // XK_Thai_choching\n    0x0e0a: 0x0daa, // XK_Thai_chochang\n    0x0e0b: 0x0dab, // XK_Thai_soso\n    0x0e0c: 0x0dac, // XK_Thai_chochoe\n    0x0e0d: 0x0dad, // XK_Thai_yoying\n    0x0e0e: 0x0dae, // XK_Thai_dochada\n    0x0e0f: 0x0daf, // XK_Thai_topatak\n    0x0e10: 0x0db0, // XK_Thai_thothan\n    0x0e11: 0x0db1, // XK_Thai_thonangmontho\n    0x0e12: 0x0db2, // XK_Thai_thophuthao\n    0x0e13: 0x0db3, // XK_Thai_nonen\n    0x0e14: 0x0db4, // XK_Thai_dodek\n    0x0e15: 0x0db5, // XK_Thai_totao\n    0x0e16: 0x0db6, // XK_Thai_thothung\n    0x0e17: 0x0db7, // XK_Thai_thothahan\n    0x0e18: 0x0db8, // XK_Thai_thothong\n    0x0e19: 0x0db9, // XK_Thai_nonu\n    0x0e1a: 0x0dba, // XK_Thai_bobaimai\n    0x0e1b: 0x0dbb, // XK_Thai_popla\n    0x0e1c: 0x0dbc, // XK_Thai_phophung\n    0x0e1d: 0x0dbd, // XK_Thai_fofa\n    0x0e1e: 0x0dbe, // XK_Thai_phophan\n    0x0e1f: 0x0dbf, // XK_Thai_fofan\n    0x0e20: 0x0dc0, // XK_Thai_phosamphao\n    0x0e21: 0x0dc1, // XK_Thai_moma\n    0x0e22: 0x0dc2, // XK_Thai_yoyak\n    0x0e23: 0x0dc3, // XK_Thai_rorua\n    0x0e24: 0x0dc4, // XK_Thai_ru\n    0x0e25: 0x0dc5, // XK_Thai_loling\n    0x0e26: 0x0dc6, // XK_Thai_lu\n    0x0e27: 0x0dc7, // XK_Thai_wowaen\n    0x0e28: 0x0dc8, // XK_Thai_sosala\n    0x0e29: 0x0dc9, // XK_Thai_sorusi\n    0x0e2a: 0x0dca, // XK_Thai_sosua\n    0x0e2b: 0x0dcb, // XK_Thai_hohip\n    0x0e2c: 0x0dcc, // XK_Thai_lochula\n    0x0e2d: 0x0dcd, // XK_Thai_oang\n    0x0e2e: 0x0dce, // XK_Thai_honokhuk\n    0x0e2f: 0x0dcf, // XK_Thai_paiyannoi\n    0x0e30: 0x0dd0, // XK_Thai_saraa\n    0x0e31: 0x0dd1, // XK_Thai_maihanakat\n    0x0e32: 0x0dd2, // XK_Thai_saraaa\n    0x0e33: 0x0dd3, // XK_Thai_saraam\n    0x0e34: 0x0dd4, // XK_Thai_sarai\n    0x0e35: 0x0dd5, // XK_Thai_saraii\n    0x0e36: 0x0dd6, // XK_Thai_saraue\n    0x0e37: 0x0dd7, // XK_Thai_sarauee\n    0x0e38: 0x0dd8, // XK_Thai_sarau\n    0x0e39: 0x0dd9, // XK_Thai_sarauu\n    0x0e3a: 0x0dda, // XK_Thai_phinthu\n    0x0e3f: 0x0ddf, // XK_Thai_baht\n    0x0e40: 0x0de0, // XK_Thai_sarae\n    0x0e41: 0x0de1, // XK_Thai_saraae\n    0x0e42: 0x0de2, // XK_Thai_sarao\n    0x0e43: 0x0de3, // XK_Thai_saraaimaimuan\n    0x0e44: 0x0de4, // XK_Thai_saraaimaimalai\n    0x0e45: 0x0de5, // XK_Thai_lakkhangyao\n    0x0e46: 0x0de6, // XK_Thai_maiyamok\n    0x0e47: 0x0de7, // XK_Thai_maitaikhu\n    0x0e48: 0x0de8, // XK_Thai_maiek\n    0x0e49: 0x0de9, // XK_Thai_maitho\n    0x0e4a: 0x0dea, // XK_Thai_maitri\n    0x0e4b: 0x0deb, // XK_Thai_maichattawa\n    0x0e4c: 0x0dec, // XK_Thai_thanthakhat\n    0x0e4d: 0x0ded, // XK_Thai_nikhahit\n    0x0e50: 0x0df0, // XK_Thai_leksun\n    0x0e51: 0x0df1, // XK_Thai_leknung\n    0x0e52: 0x0df2, // XK_Thai_leksong\n    0x0e53: 0x0df3, // XK_Thai_leksam\n    0x0e54: 0x0df4, // XK_Thai_leksi\n    0x0e55: 0x0df5, // XK_Thai_lekha\n    0x0e56: 0x0df6, // XK_Thai_lekhok\n    0x0e57: 0x0df7, // XK_Thai_lekchet\n    0x0e58: 0x0df8, // XK_Thai_lekpaet\n    0x0e59: 0x0df9, // XK_Thai_lekkao\n    0x2002: 0x0aa2, // XK_enspace\n    0x2003: 0x0aa1, // XK_emspace\n    0x2004: 0x0aa3, // XK_em3space\n    0x2005: 0x0aa4, // XK_em4space\n    0x2007: 0x0aa5, // XK_digitspace\n    0x2008: 0x0aa6, // XK_punctspace\n    0x2009: 0x0aa7, // XK_thinspace\n    0x200a: 0x0aa8, // XK_hairspace\n    0x2012: 0x0abb, // XK_figdash\n    0x2013: 0x0aaa, // XK_endash\n    0x2014: 0x0aa9, // XK_emdash\n    0x2015: 0x07af, // XK_Greek_horizbar\n    0x2017: 0x0cdf, // XK_hebrew_doublelowline\n    0x2018: 0x0ad0, // XK_leftsinglequotemark\n    0x2019: 0x0ad1, // XK_rightsinglequotemark\n    0x201a: 0x0afd, // XK_singlelowquotemark\n    0x201c: 0x0ad2, // XK_leftdoublequotemark\n    0x201d: 0x0ad3, // XK_rightdoublequotemark\n    0x201e: 0x0afe, // XK_doublelowquotemark\n    0x2020: 0x0af1, // XK_dagger\n    0x2021: 0x0af2, // XK_doubledagger\n    0x2022: 0x0ae6, // XK_enfilledcircbullet\n    0x2025: 0x0aaf, // XK_doubbaselinedot\n    0x2026: 0x0aae, // XK_ellipsis\n    0x2030: 0x0ad5, // XK_permille\n    0x2032: 0x0ad6, // XK_minutes\n    0x2033: 0x0ad7, // XK_seconds\n    0x2038: 0x0afc, // XK_caret\n    0x203e: 0x047e, // XK_overline\n    0x20a9: 0x0eff, // XK_Korean_Won\n    0x20ac: 0x20ac, // XK_EuroSign\n    0x2105: 0x0ab8, // XK_careof\n    0x2116: 0x06b0, // XK_numerosign\n    0x2117: 0x0afb, // XK_phonographcopyright\n    0x211e: 0x0ad4, // XK_prescription\n    0x2122: 0x0ac9, // XK_trademark\n    0x2153: 0x0ab0, // XK_onethird\n    0x2154: 0x0ab1, // XK_twothirds\n    0x2155: 0x0ab2, // XK_onefifth\n    0x2156: 0x0ab3, // XK_twofifths\n    0x2157: 0x0ab4, // XK_threefifths\n    0x2158: 0x0ab5, // XK_fourfifths\n    0x2159: 0x0ab6, // XK_onesixth\n    0x215a: 0x0ab7, // XK_fivesixths\n    0x215b: 0x0ac3, // XK_oneeighth\n    0x215c: 0x0ac4, // XK_threeeighths\n    0x215d: 0x0ac5, // XK_fiveeighths\n    0x215e: 0x0ac6, // XK_seveneighths\n    0x2190: 0x08fb, // XK_leftarrow\n    0x2191: 0x08fc, // XK_uparrow\n    0x2192: 0x08fd, // XK_rightarrow\n    0x2193: 0x08fe, // XK_downarrow\n    0x21d2: 0x08ce, // XK_implies\n    0x21d4: 0x08cd, // XK_ifonlyif\n    0x2202: 0x08ef, // XK_partialderivative\n    0x2207: 0x08c5, // XK_nabla\n    0x2218: 0x0bca, // XK_jot\n    0x221a: 0x08d6, // XK_radical\n    0x221d: 0x08c1, // XK_variation\n    0x221e: 0x08c2, // XK_infinity\n    0x2227: 0x08de, // XK_logicaland\n    0x2228: 0x08df, // XK_logicalor\n    0x2229: 0x08dc, // XK_intersection\n    0x222a: 0x08dd, // XK_union\n    0x222b: 0x08bf, // XK_integral\n    0x2234: 0x08c0, // XK_therefore\n    0x223c: 0x08c8, // XK_approximate\n    0x2243: 0x08c9, // XK_similarequal\n    0x2245: 0x1002248, // XK_approxeq\n    0x2260: 0x08bd, // XK_notequal\n    0x2261: 0x08cf, // XK_identical\n    0x2264: 0x08bc, // XK_lessthanequal\n    0x2265: 0x08be, // XK_greaterthanequal\n    0x2282: 0x08da, // XK_includedin\n    0x2283: 0x08db, // XK_includes\n    0x22a2: 0x0bfc, // XK_righttack\n    0x22a3: 0x0bdc, // XK_lefttack\n    0x22a4: 0x0bc2, // XK_downtack\n    0x22a5: 0x0bce, // XK_uptack\n    0x2308: 0x0bd3, // XK_upstile\n    0x230a: 0x0bc4, // XK_downstile\n    0x2315: 0x0afa, // XK_telephonerecorder\n    0x2320: 0x08a4, // XK_topintegral\n    0x2321: 0x08a5, // XK_botintegral\n    0x2395: 0x0bcc, // XK_quad\n    0x239b: 0x08ab, // XK_topleftparens\n    0x239d: 0x08ac, // XK_botleftparens\n    0x239e: 0x08ad, // XK_toprightparens\n    0x23a0: 0x08ae, // XK_botrightparens\n    0x23a1: 0x08a7, // XK_topleftsqbracket\n    0x23a3: 0x08a8, // XK_botleftsqbracket\n    0x23a4: 0x08a9, // XK_toprightsqbracket\n    0x23a6: 0x08aa, // XK_botrightsqbracket\n    0x23a8: 0x08af, // XK_leftmiddlecurlybrace\n    0x23ac: 0x08b0, // XK_rightmiddlecurlybrace\n    0x23b7: 0x08a1, // XK_leftradical\n    0x23ba: 0x09ef, // XK_horizlinescan1\n    0x23bb: 0x09f0, // XK_horizlinescan3\n    0x23bc: 0x09f2, // XK_horizlinescan7\n    0x23bd: 0x09f3, // XK_horizlinescan9\n    0x2409: 0x09e2, // XK_ht\n    0x240a: 0x09e5, // XK_lf\n    0x240b: 0x09e9, // XK_vt\n    0x240c: 0x09e3, // XK_ff\n    0x240d: 0x09e4, // XK_cr\n    0x2423: 0x0aac, // XK_signifblank\n    0x2424: 0x09e8, // XK_nl\n    0x2500: 0x08a3, // XK_horizconnector\n    0x2502: 0x08a6, // XK_vertconnector\n    0x250c: 0x08a2, // XK_topleftradical\n    0x2510: 0x09eb, // XK_uprightcorner\n    0x2514: 0x09ed, // XK_lowleftcorner\n    0x2518: 0x09ea, // XK_lowrightcorner\n    0x251c: 0x09f4, // XK_leftt\n    0x2524: 0x09f5, // XK_rightt\n    0x252c: 0x09f7, // XK_topt\n    0x2534: 0x09f6, // XK_bott\n    0x253c: 0x09ee, // XK_crossinglines\n    0x2592: 0x09e1, // XK_checkerboard\n    0x25aa: 0x0ae7, // XK_enfilledsqbullet\n    0x25ab: 0x0ae1, // XK_enopensquarebullet\n    0x25ac: 0x0adb, // XK_filledrectbullet\n    0x25ad: 0x0ae2, // XK_openrectbullet\n    0x25ae: 0x0adf, // XK_emfilledrect\n    0x25af: 0x0acf, // XK_emopenrectangle\n    0x25b2: 0x0ae8, // XK_filledtribulletup\n    0x25b3: 0x0ae3, // XK_opentribulletup\n    0x25b6: 0x0add, // XK_filledrighttribullet\n    0x25b7: 0x0acd, // XK_rightopentriangle\n    0x25bc: 0x0ae9, // XK_filledtribulletdown\n    0x25bd: 0x0ae4, // XK_opentribulletdown\n    0x25c0: 0x0adc, // XK_filledlefttribullet\n    0x25c1: 0x0acc, // XK_leftopentriangle\n    0x25c6: 0x09e0, // XK_soliddiamond\n    0x25cb: 0x0ace, // XK_emopencircle\n    0x25cf: 0x0ade, // XK_emfilledcircle\n    0x25e6: 0x0ae0, // XK_enopencircbullet\n    0x2606: 0x0ae5, // XK_openstar\n    0x260e: 0x0af9, // XK_telephone\n    0x2613: 0x0aca, // XK_signaturemark\n    0x261c: 0x0aea, // XK_leftpointer\n    0x261e: 0x0aeb, // XK_rightpointer\n    0x2640: 0x0af8, // XK_femalesymbol\n    0x2642: 0x0af7, // XK_malesymbol\n    0x2663: 0x0aec, // XK_club\n    0x2665: 0x0aee, // XK_heart\n    0x2666: 0x0aed, // XK_diamond\n    0x266d: 0x0af6, // XK_musicalflat\n    0x266f: 0x0af5, // XK_musicalsharp\n    0x2713: 0x0af3, // XK_checkmark\n    0x2717: 0x0af4, // XK_ballotcross\n    0x271d: 0x0ad9, // XK_latincross\n    0x2720: 0x0af0, // XK_maltesecross\n    0x27e8: 0x0abc, // XK_leftanglebracket\n    0x27e9: 0x0abe, // XK_rightanglebracket\n    0x3001: 0x04a4, // XK_kana_comma\n    0x3002: 0x04a1, // XK_kana_fullstop\n    0x300c: 0x04a2, // XK_kana_openingbracket\n    0x300d: 0x04a3, // XK_kana_closingbracket\n    0x309b: 0x04de, // XK_voicedsound\n    0x309c: 0x04df, // XK_semivoicedsound\n    0x30a1: 0x04a7, // XK_kana_a\n    0x30a2: 0x04b1, // XK_kana_A\n    0x30a3: 0x04a8, // XK_kana_i\n    0x30a4: 0x04b2, // XK_kana_I\n    0x30a5: 0x04a9, // XK_kana_u\n    0x30a6: 0x04b3, // XK_kana_U\n    0x30a7: 0x04aa, // XK_kana_e\n    0x30a8: 0x04b4, // XK_kana_E\n    0x30a9: 0x04ab, // XK_kana_o\n    0x30aa: 0x04b5, // XK_kana_O\n    0x30ab: 0x04b6, // XK_kana_KA\n    0x30ad: 0x04b7, // XK_kana_KI\n    0x30af: 0x04b8, // XK_kana_KU\n    0x30b1: 0x04b9, // XK_kana_KE\n    0x30b3: 0x04ba, // XK_kana_KO\n    0x30b5: 0x04bb, // XK_kana_SA\n    0x30b7: 0x04bc, // XK_kana_SHI\n    0x30b9: 0x04bd, // XK_kana_SU\n    0x30bb: 0x04be, // XK_kana_SE\n    0x30bd: 0x04bf, // XK_kana_SO\n    0x30bf: 0x04c0, // XK_kana_TA\n    0x30c1: 0x04c1, // XK_kana_CHI\n    0x30c3: 0x04af, // XK_kana_tsu\n    0x30c4: 0x04c2, // XK_kana_TSU\n    0x30c6: 0x04c3, // XK_kana_TE\n    0x30c8: 0x04c4, // XK_kana_TO\n    0x30ca: 0x04c5, // XK_kana_NA\n    0x30cb: 0x04c6, // XK_kana_NI\n    0x30cc: 0x04c7, // XK_kana_NU\n    0x30cd: 0x04c8, // XK_kana_NE\n    0x30ce: 0x04c9, // XK_kana_NO\n    0x30cf: 0x04ca, // XK_kana_HA\n    0x30d2: 0x04cb, // XK_kana_HI\n    0x30d5: 0x04cc, // XK_kana_FU\n    0x30d8: 0x04cd, // XK_kana_HE\n    0x30db: 0x04ce, // XK_kana_HO\n    0x30de: 0x04cf, // XK_kana_MA\n    0x30df: 0x04d0, // XK_kana_MI\n    0x30e0: 0x04d1, // XK_kana_MU\n    0x30e1: 0x04d2, // XK_kana_ME\n    0x30e2: 0x04d3, // XK_kana_MO\n    0x30e3: 0x04ac, // XK_kana_ya\n    0x30e4: 0x04d4, // XK_kana_YA\n    0x30e5: 0x04ad, // XK_kana_yu\n    0x30e6: 0x04d5, // XK_kana_YU\n    0x30e7: 0x04ae, // XK_kana_yo\n    0x30e8: 0x04d6, // XK_kana_YO\n    0x30e9: 0x04d7, // XK_kana_RA\n    0x30ea: 0x04d8, // XK_kana_RI\n    0x30eb: 0x04d9, // XK_kana_RU\n    0x30ec: 0x04da, // XK_kana_RE\n    0x30ed: 0x04db, // XK_kana_RO\n    0x30ef: 0x04dc, // XK_kana_WA\n    0x30f2: 0x04a6, // XK_kana_WO\n    0x30f3: 0x04dd, // XK_kana_N\n    0x30fb: 0x04a5, // XK_kana_conjunctive\n    0x30fc: 0x04b0, // XK_prolongedsound\n};\n\nexport 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};\n"
  },
  {
    "path": "core/input/util.js",
    "content": "import KeyTable from \"./keysym.js\";\nimport keysyms from \"./keysymdef.js\";\nimport vkeys from \"./vkeys.js\";\nimport fixedkeys from \"./fixedkeys.js\";\nimport DOMKeyTable from \"./domkeytable.js\";\nimport * as browser from \"../util/browser.js\";\n\n// Get 'KeyboardEvent.code', handling legacy browsers\nexport function getKeycode(evt) {\n    // Are we getting proper key identifiers?\n    // (unfortunately Firefox and Chrome are crappy here and gives\n    // us an empty string on some platforms, rather than leaving it\n    // undefined)\n    if (evt.code) {\n        // Mozilla isn't fully in sync with the spec yet\n        switch (evt.code) {\n            case 'OSLeft': return 'MetaLeft';\n            case 'OSRight': return 'MetaRight';\n        }\n\n        return evt.code;\n    }\n\n    // The de-facto standard is to use Windows Virtual-Key codes\n    // in the 'keyCode' field for non-printable characters\n    if (evt.keyCode in vkeys) {\n        let code = vkeys[evt.keyCode];\n\n        // macOS has messed up this code for some reason\n        if (browser.isMac() && (code === 'ContextMenu')) {\n            code = 'MetaRight';\n        }\n\n        // The keyCode doesn't distinguish between left and right\n        // for the standard modifiers\n        if (evt.location === 2) {\n            switch (code) {\n                case 'ShiftLeft': return 'ShiftRight';\n                case 'ControlLeft': return 'ControlRight';\n                case 'AltLeft': return 'AltRight';\n            }\n        }\n\n        // Nor a bunch of the numpad keys\n        if (evt.location === 3) {\n            switch (code) {\n                case 'Delete': return 'NumpadDecimal';\n                case 'Insert': return 'Numpad0';\n                case 'End': return 'Numpad1';\n                case 'ArrowDown': return 'Numpad2';\n                case 'PageDown': return 'Numpad3';\n                case 'ArrowLeft': return 'Numpad4';\n                case 'ArrowRight': return 'Numpad6';\n                case 'Home': return 'Numpad7';\n                case 'ArrowUp': return 'Numpad8';\n                case 'PageUp': return 'Numpad9';\n                case 'Enter': return 'NumpadEnter';\n            }\n        }\n\n        return code;\n    }\n\n    return 'Unidentified';\n}\n\n// Get 'KeyboardEvent.key', handling legacy browsers\nexport function getKey(evt) {\n    // Are we getting a proper key value?\n    if ((evt.key !== undefined) && (evt.key !== 'Unidentified')) {\n        // Mozilla isn't fully in sync with the spec yet\n        switch (evt.key) {\n            case 'OS': return 'Meta';\n            case 'LaunchMyComputer': return 'LaunchApplication1';\n            case 'LaunchCalculator': return 'LaunchApplication2';\n        }\n\n        // iOS leaks some OS names\n        switch (evt.key) {\n            case 'UIKeyInputUpArrow': return 'ArrowUp';\n            case 'UIKeyInputDownArrow': return 'ArrowDown';\n            case 'UIKeyInputLeftArrow': return 'ArrowLeft';\n            case 'UIKeyInputRightArrow': return 'ArrowRight';\n            case 'UIKeyInputEscape': return 'Escape';\n        }\n\n        // Broken behaviour in Chrome\n        if ((evt.key === '\\x00') && (evt.code === 'NumpadDecimal')) {\n            return 'Delete';\n        }\n\n        return evt.key;\n    }\n\n    // Try to deduce it based on the physical key\n    const code = getKeycode(evt);\n    if (code in fixedkeys) {\n        return fixedkeys[code];\n    }\n\n    // If that failed, then see if we have a printable character\n    if (evt.charCode) {\n        return String.fromCharCode(evt.charCode);\n    }\n\n    // At this point we have nothing left to go on\n    return 'Unidentified';\n}\n\n// Get the most reliable keysym value we can get from a key event\nexport function getKeysym(evt) {\n    const key = getKey(evt);\n\n    if (key === 'Unidentified') {\n        return null;\n    }\n\n    // First look up special keys\n    if (key in DOMKeyTable) {\n        let location = evt.location;\n\n        // Safari screws up location for the right cmd key\n        if ((key === 'Meta') && (location === 0)) {\n            location = 2;\n        }\n\n        // And for Clear\n        if ((key === 'Clear') && (location === 3)) {\n            let code = getKeycode(evt);\n            if (code === 'NumLock') {\n                location = 0;\n            }\n        }\n\n        if ((location === undefined) || (location > 3)) {\n            location = 0;\n        }\n\n        // The original Meta key now gets confused with the Windows key\n        // https://bugs.chromium.org/p/chromium/issues/detail?id=1020141\n        // https://bugzilla.mozilla.org/show_bug.cgi?id=1232918\n        if (key === 'Meta') {\n            let code = getKeycode(evt);\n            if (code === 'AltLeft') {\n                return KeyTable.XK_Meta_L;\n            } else if (code === 'AltRight') {\n                return KeyTable.XK_Meta_R;\n            }\n        }\n\n        // macOS has Clear instead of NumLock, but the remote system is\n        // probably not macOS, so lying here is probably best...\n        if (key === 'Clear') {\n            let code = getKeycode(evt);\n            if (code === 'NumLock') {\n                return KeyTable.XK_Num_Lock;\n            }\n        }\n\n        // Windows sends alternating symbols for some keys when using a\n        // Japanese layout. We have no way of synchronising with the IM\n        // running on the remote system, so we send some combined keysym\n        // instead and hope for the best.\n        if (browser.isWindows()) {\n            switch (key) {\n                case 'Zenkaku':\n                case 'Hankaku':\n                    return KeyTable.XK_Zenkaku_Hankaku;\n                case 'Romaji':\n                case 'KanaMode':\n                    return KeyTable.XK_Romaji;\n            }\n        }\n\n        return DOMKeyTable[key][location];\n    }\n\n    // Now we need to look at the Unicode symbol instead\n\n    // Special key? (FIXME: Should have been caught earlier)\n    if (key.length !== 1) {\n        return null;\n    }\n\n    const codepoint = key.charCodeAt();\n    if (codepoint) {\n        return keysyms.lookup(codepoint);\n    }\n\n    return null;\n}\n"
  },
  {
    "path": "core/input/vkeys.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2018 The noVNC authors\n * Licensed under MPL 2.0 or any later version (see LICENSE.txt)\n */\n\n/*\n * Mapping between Microsoft® Windows® Virtual-Key codes and\n * HTML key codes.\n */\n\nexport default {\n    0x08: 'Backspace',\n    0x09: 'Tab',\n    0x0a: 'NumpadClear',\n    0x0d: 'Enter',\n    0x10: 'ShiftLeft',\n    0x11: 'ControlLeft',\n    0x12: 'AltLeft',\n    0x13: 'Pause',\n    0x14: 'CapsLock',\n    0x15: 'Lang1',\n    0x19: 'Lang2',\n    0x1b: 'Escape',\n    0x1c: 'Convert',\n    0x1d: 'NonConvert',\n    0x20: 'Space',\n    0x21: 'PageUp',\n    0x22: 'PageDown',\n    0x23: 'End',\n    0x24: 'Home',\n    0x25: 'ArrowLeft',\n    0x26: 'ArrowUp',\n    0x27: 'ArrowRight',\n    0x28: 'ArrowDown',\n    0x29: 'Select',\n    0x2c: 'PrintScreen',\n    0x2d: 'Insert',\n    0x2e: 'Delete',\n    0x2f: 'Help',\n    0x30: 'Digit0',\n    0x31: 'Digit1',\n    0x32: 'Digit2',\n    0x33: 'Digit3',\n    0x34: 'Digit4',\n    0x35: 'Digit5',\n    0x36: 'Digit6',\n    0x37: 'Digit7',\n    0x38: 'Digit8',\n    0x39: 'Digit9',\n    0x5b: 'MetaLeft',\n    0x5c: 'MetaRight',\n    0x5d: 'ContextMenu',\n    0x5f: 'Sleep',\n    0x60: 'Numpad0',\n    0x61: 'Numpad1',\n    0x62: 'Numpad2',\n    0x63: 'Numpad3',\n    0x64: 'Numpad4',\n    0x65: 'Numpad5',\n    0x66: 'Numpad6',\n    0x67: 'Numpad7',\n    0x68: 'Numpad8',\n    0x69: 'Numpad9',\n    0x6a: 'NumpadMultiply',\n    0x6b: 'NumpadAdd',\n    0x6c: 'NumpadDecimal',\n    0x6d: 'NumpadSubtract',\n    0x6e: 'NumpadDecimal', // Duplicate, because buggy on Windows\n    0x6f: 'NumpadDivide',\n    0x70: 'F1',\n    0x71: 'F2',\n    0x72: 'F3',\n    0x73: 'F4',\n    0x74: 'F5',\n    0x75: 'F6',\n    0x76: 'F7',\n    0x77: 'F8',\n    0x78: 'F9',\n    0x79: 'F10',\n    0x7a: 'F11',\n    0x7b: 'F12',\n    0x7c: 'F13',\n    0x7d: 'F14',\n    0x7e: 'F15',\n    0x7f: 'F16',\n    0x80: 'F17',\n    0x81: 'F18',\n    0x82: 'F19',\n    0x83: 'F20',\n    0x84: 'F21',\n    0x85: 'F22',\n    0x86: 'F23',\n    0x87: 'F24',\n    0x90: 'NumLock',\n    0x91: 'ScrollLock',\n    0xa6: 'BrowserBack',\n    0xa7: 'BrowserForward',\n    0xa8: 'BrowserRefresh',\n    0xa9: 'BrowserStop',\n    0xaa: 'BrowserSearch',\n    0xab: 'BrowserFavorites',\n    0xac: 'BrowserHome',\n    0xad: 'AudioVolumeMute',\n    0xae: 'AudioVolumeDown',\n    0xaf: 'AudioVolumeUp',\n    0xb0: 'MediaTrackNext',\n    0xb1: 'MediaTrackPrevious',\n    0xb2: 'MediaStop',\n    0xb3: 'MediaPlayPause',\n    0xb4: 'LaunchMail',\n    0xb5: 'MediaSelect',\n    0xb6: 'LaunchApp1',\n    0xb7: 'LaunchApp2',\n    0xe1: 'AltRight', // Only when it is AltGraph\n};\n"
  },
  {
    "path": "core/input/xtscancodes.js",
    "content": "/*\n * This file is auto-generated from keymaps.csv\n * Database checksum sha256(76d68c10e97d37fe2ea459e210125ae41796253fb217e900bf2983ade13a7920)\n * To re-generate, run:\n *   keymap-gen code-map --lang=js keymaps.csv html atset1\n*/\nexport default {\n  \"Again\": 0xe005, /* html:Again (Again) -> linux:129 (KEY_AGAIN) -> atset1:57349 */\n  \"AltLeft\": 0x38, /* html:AltLeft (AltLeft) -> linux:56 (KEY_LEFTALT) -> atset1:56 */\n  \"AltRight\": 0xe038, /* html:AltRight (AltRight) -> linux:100 (KEY_RIGHTALT) -> atset1:57400 */\n  \"ArrowDown\": 0xe050, /* html:ArrowDown (ArrowDown) -> linux:108 (KEY_DOWN) -> atset1:57424 */\n  \"ArrowLeft\": 0xe04b, /* html:ArrowLeft (ArrowLeft) -> linux:105 (KEY_LEFT) -> atset1:57419 */\n  \"ArrowRight\": 0xe04d, /* html:ArrowRight (ArrowRight) -> linux:106 (KEY_RIGHT) -> atset1:57421 */\n  \"ArrowUp\": 0xe048, /* html:ArrowUp (ArrowUp) -> linux:103 (KEY_UP) -> atset1:57416 */\n  \"AudioVolumeDown\": 0xe02e, /* html:AudioVolumeDown (AudioVolumeDown) -> linux:114 (KEY_VOLUMEDOWN) -> atset1:57390 */\n  \"AudioVolumeMute\": 0xe020, /* html:AudioVolumeMute (AudioVolumeMute) -> linux:113 (KEY_MUTE) -> atset1:57376 */\n  \"AudioVolumeUp\": 0xe030, /* html:AudioVolumeUp (AudioVolumeUp) -> linux:115 (KEY_VOLUMEUP) -> atset1:57392 */\n  \"Backquote\": 0x29, /* html:Backquote (Backquote) -> linux:41 (KEY_GRAVE) -> atset1:41 */\n  \"Backslash\": 0x2b, /* html:Backslash (Backslash) -> linux:43 (KEY_BACKSLASH) -> atset1:43 */\n  \"Backspace\": 0xe, /* html:Backspace (Backspace) -> linux:14 (KEY_BACKSPACE) -> atset1:14 */\n  \"BracketLeft\": 0x1a, /* html:BracketLeft (BracketLeft) -> linux:26 (KEY_LEFTBRACE) -> atset1:26 */\n  \"BracketRight\": 0x1b, /* html:BracketRight (BracketRight) -> linux:27 (KEY_RIGHTBRACE) -> atset1:27 */\n  \"BrowserBack\": 0xe06a, /* html:BrowserBack (BrowserBack) -> linux:158 (KEY_BACK) -> atset1:57450 */\n  \"BrowserFavorites\": 0xe066, /* html:BrowserFavorites (BrowserFavorites) -> linux:156 (KEY_BOOKMARKS) -> atset1:57446 */\n  \"BrowserForward\": 0xe069, /* html:BrowserForward (BrowserForward) -> linux:159 (KEY_FORWARD) -> atset1:57449 */\n  \"BrowserHome\": 0xe032, /* html:BrowserHome (BrowserHome) -> linux:172 (KEY_HOMEPAGE) -> atset1:57394 */\n  \"BrowserRefresh\": 0xe067, /* html:BrowserRefresh (BrowserRefresh) -> linux:173 (KEY_REFRESH) -> atset1:57447 */\n  \"BrowserSearch\": 0xe065, /* html:BrowserSearch (BrowserSearch) -> linux:217 (KEY_SEARCH) -> atset1:57445 */\n  \"BrowserStop\": 0xe068, /* html:BrowserStop (BrowserStop) -> linux:128 (KEY_STOP) -> atset1:57448 */\n  \"CapsLock\": 0x3a, /* html:CapsLock (CapsLock) -> linux:58 (KEY_CAPSLOCK) -> atset1:58 */\n  \"Comma\": 0x33, /* html:Comma (Comma) -> linux:51 (KEY_COMMA) -> atset1:51 */\n  \"ContextMenu\": 0xe05d, /* html:ContextMenu (ContextMenu) -> linux:127 (KEY_COMPOSE) -> atset1:57437 */\n  \"ControlLeft\": 0x1d, /* html:ControlLeft (ControlLeft) -> linux:29 (KEY_LEFTCTRL) -> atset1:29 */\n  \"ControlRight\": 0xe01d, /* html:ControlRight (ControlRight) -> linux:97 (KEY_RIGHTCTRL) -> atset1:57373 */\n  \"Convert\": 0x79, /* html:Convert (Convert) -> linux:92 (KEY_HENKAN) -> atset1:121 */\n  \"Copy\": 0xe078, /* html:Copy (Copy) -> linux:133 (KEY_COPY) -> atset1:57464 */\n  \"Cut\": 0xe03c, /* html:Cut (Cut) -> linux:137 (KEY_CUT) -> atset1:57404 */\n  \"Delete\": 0xe053, /* html:Delete (Delete) -> linux:111 (KEY_DELETE) -> atset1:57427 */\n  \"Digit0\": 0xb, /* html:Digit0 (Digit0) -> linux:11 (KEY_0) -> atset1:11 */\n  \"Digit1\": 0x2, /* html:Digit1 (Digit1) -> linux:2 (KEY_1) -> atset1:2 */\n  \"Digit2\": 0x3, /* html:Digit2 (Digit2) -> linux:3 (KEY_2) -> atset1:3 */\n  \"Digit3\": 0x4, /* html:Digit3 (Digit3) -> linux:4 (KEY_3) -> atset1:4 */\n  \"Digit4\": 0x5, /* html:Digit4 (Digit4) -> linux:5 (KEY_4) -> atset1:5 */\n  \"Digit5\": 0x6, /* html:Digit5 (Digit5) -> linux:6 (KEY_5) -> atset1:6 */\n  \"Digit6\": 0x7, /* html:Digit6 (Digit6) -> linux:7 (KEY_6) -> atset1:7 */\n  \"Digit7\": 0x8, /* html:Digit7 (Digit7) -> linux:8 (KEY_7) -> atset1:8 */\n  \"Digit8\": 0x9, /* html:Digit8 (Digit8) -> linux:9 (KEY_8) -> atset1:9 */\n  \"Digit9\": 0xa, /* html:Digit9 (Digit9) -> linux:10 (KEY_9) -> atset1:10 */\n  \"Eject\": 0xe07d, /* html:Eject (Eject) -> linux:162 (KEY_EJECTCLOSECD) -> atset1:57469 */\n  \"End\": 0xe04f, /* html:End (End) -> linux:107 (KEY_END) -> atset1:57423 */\n  \"Enter\": 0x1c, /* html:Enter (Enter) -> linux:28 (KEY_ENTER) -> atset1:28 */\n  \"Equal\": 0xd, /* html:Equal (Equal) -> linux:13 (KEY_EQUAL) -> atset1:13 */\n  \"Escape\": 0x1, /* html:Escape (Escape) -> linux:1 (KEY_ESC) -> atset1:1 */\n  \"F1\": 0x3b, /* html:F1 (F1) -> linux:59 (KEY_F1) -> atset1:59 */\n  \"F10\": 0x44, /* html:F10 (F10) -> linux:68 (KEY_F10) -> atset1:68 */\n  \"F11\": 0x57, /* html:F11 (F11) -> linux:87 (KEY_F11) -> atset1:87 */\n  \"F12\": 0x58, /* html:F12 (F12) -> linux:88 (KEY_F12) -> atset1:88 */\n  \"F13\": 0x5d, /* html:F13 (F13) -> linux:183 (KEY_F13) -> atset1:93 */\n  \"F14\": 0x5e, /* html:F14 (F14) -> linux:184 (KEY_F14) -> atset1:94 */\n  \"F15\": 0x5f, /* html:F15 (F15) -> linux:185 (KEY_F15) -> atset1:95 */\n  \"F16\": 0x55, /* html:F16 (F16) -> linux:186 (KEY_F16) -> atset1:85 */\n  \"F17\": 0xe003, /* html:F17 (F17) -> linux:187 (KEY_F17) -> atset1:57347 */\n  \"F18\": 0xe077, /* html:F18 (F18) -> linux:188 (KEY_F18) -> atset1:57463 */\n  \"F19\": 0xe004, /* html:F19 (F19) -> linux:189 (KEY_F19) -> atset1:57348 */\n  \"F2\": 0x3c, /* html:F2 (F2) -> linux:60 (KEY_F2) -> atset1:60 */\n  \"F20\": 0x5a, /* html:F20 (F20) -> linux:190 (KEY_F20) -> atset1:90 */\n  \"F21\": 0x74, /* html:F21 (F21) -> linux:191 (KEY_F21) -> atset1:116 */\n  \"F22\": 0xe079, /* html:F22 (F22) -> linux:192 (KEY_F22) -> atset1:57465 */\n  \"F23\": 0x6d, /* html:F23 (F23) -> linux:193 (KEY_F23) -> atset1:109 */\n  \"F24\": 0x6f, /* html:F24 (F24) -> linux:194 (KEY_F24) -> atset1:111 */\n  \"F3\": 0x3d, /* html:F3 (F3) -> linux:61 (KEY_F3) -> atset1:61 */\n  \"F4\": 0x3e, /* html:F4 (F4) -> linux:62 (KEY_F4) -> atset1:62 */\n  \"F5\": 0x3f, /* html:F5 (F5) -> linux:63 (KEY_F5) -> atset1:63 */\n  \"F6\": 0x40, /* html:F6 (F6) -> linux:64 (KEY_F6) -> atset1:64 */\n  \"F7\": 0x41, /* html:F7 (F7) -> linux:65 (KEY_F7) -> atset1:65 */\n  \"F8\": 0x42, /* html:F8 (F8) -> linux:66 (KEY_F8) -> atset1:66 */\n  \"F9\": 0x43, /* html:F9 (F9) -> linux:67 (KEY_F9) -> atset1:67 */\n  \"Find\": 0xe041, /* html:Find (Find) -> linux:136 (KEY_FIND) -> atset1:57409 */\n  \"Help\": 0xe075, /* html:Help (Help) -> linux:138 (KEY_HELP) -> atset1:57461 */\n  \"Hiragana\": 0x77, /* html:Hiragana (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */\n  \"Home\": 0xe047, /* html:Home (Home) -> linux:102 (KEY_HOME) -> atset1:57415 */\n  \"Insert\": 0xe052, /* html:Insert (Insert) -> linux:110 (KEY_INSERT) -> atset1:57426 */\n  \"IntlBackslash\": 0x56, /* html:IntlBackslash (IntlBackslash) -> linux:86 (KEY_102ND) -> atset1:86 */\n  \"IntlRo\": 0x73, /* html:IntlRo (IntlRo) -> linux:89 (KEY_RO) -> atset1:115 */\n  \"IntlYen\": 0x7d, /* html:IntlYen (IntlYen) -> linux:124 (KEY_YEN) -> atset1:125 */\n  \"KanaMode\": 0x70, /* html:KanaMode (KanaMode) -> linux:93 (KEY_KATAKANAHIRAGANA) -> atset1:112 */\n  \"Katakana\": 0x78, /* html:Katakana (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */\n  \"KeyA\": 0x1e, /* html:KeyA (KeyA) -> linux:30 (KEY_A) -> atset1:30 */\n  \"KeyB\": 0x30, /* html:KeyB (KeyB) -> linux:48 (KEY_B) -> atset1:48 */\n  \"KeyC\": 0x2e, /* html:KeyC (KeyC) -> linux:46 (KEY_C) -> atset1:46 */\n  \"KeyD\": 0x20, /* html:KeyD (KeyD) -> linux:32 (KEY_D) -> atset1:32 */\n  \"KeyE\": 0x12, /* html:KeyE (KeyE) -> linux:18 (KEY_E) -> atset1:18 */\n  \"KeyF\": 0x21, /* html:KeyF (KeyF) -> linux:33 (KEY_F) -> atset1:33 */\n  \"KeyG\": 0x22, /* html:KeyG (KeyG) -> linux:34 (KEY_G) -> atset1:34 */\n  \"KeyH\": 0x23, /* html:KeyH (KeyH) -> linux:35 (KEY_H) -> atset1:35 */\n  \"KeyI\": 0x17, /* html:KeyI (KeyI) -> linux:23 (KEY_I) -> atset1:23 */\n  \"KeyJ\": 0x24, /* html:KeyJ (KeyJ) -> linux:36 (KEY_J) -> atset1:36 */\n  \"KeyK\": 0x25, /* html:KeyK (KeyK) -> linux:37 (KEY_K) -> atset1:37 */\n  \"KeyL\": 0x26, /* html:KeyL (KeyL) -> linux:38 (KEY_L) -> atset1:38 */\n  \"KeyM\": 0x32, /* html:KeyM (KeyM) -> linux:50 (KEY_M) -> atset1:50 */\n  \"KeyN\": 0x31, /* html:KeyN (KeyN) -> linux:49 (KEY_N) -> atset1:49 */\n  \"KeyO\": 0x18, /* html:KeyO (KeyO) -> linux:24 (KEY_O) -> atset1:24 */\n  \"KeyP\": 0x19, /* html:KeyP (KeyP) -> linux:25 (KEY_P) -> atset1:25 */\n  \"KeyQ\": 0x10, /* html:KeyQ (KeyQ) -> linux:16 (KEY_Q) -> atset1:16 */\n  \"KeyR\": 0x13, /* html:KeyR (KeyR) -> linux:19 (KEY_R) -> atset1:19 */\n  \"KeyS\": 0x1f, /* html:KeyS (KeyS) -> linux:31 (KEY_S) -> atset1:31 */\n  \"KeyT\": 0x14, /* html:KeyT (KeyT) -> linux:20 (KEY_T) -> atset1:20 */\n  \"KeyU\": 0x16, /* html:KeyU (KeyU) -> linux:22 (KEY_U) -> atset1:22 */\n  \"KeyV\": 0x2f, /* html:KeyV (KeyV) -> linux:47 (KEY_V) -> atset1:47 */\n  \"KeyW\": 0x11, /* html:KeyW (KeyW) -> linux:17 (KEY_W) -> atset1:17 */\n  \"KeyX\": 0x2d, /* html:KeyX (KeyX) -> linux:45 (KEY_X) -> atset1:45 */\n  \"KeyY\": 0x15, /* html:KeyY (KeyY) -> linux:21 (KEY_Y) -> atset1:21 */\n  \"KeyZ\": 0x2c, /* html:KeyZ (KeyZ) -> linux:44 (KEY_Z) -> atset1:44 */\n  \"Lang1\": 0x72, /* html:Lang1 (Lang1) -> linux:122 (KEY_HANGEUL) -> atset1:114 */\n  \"Lang2\": 0x71, /* html:Lang2 (Lang2) -> linux:123 (KEY_HANJA) -> atset1:113 */\n  \"Lang3\": 0x78, /* html:Lang3 (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */\n  \"Lang4\": 0x77, /* html:Lang4 (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */\n  \"Lang5\": 0x76, /* html:Lang5 (Lang5) -> linux:85 (KEY_ZENKAKUHANKAKU) -> atset1:118 */\n  \"LaunchApp1\": 0xe06b, /* html:LaunchApp1 (LaunchApp1) -> linux:157 (KEY_COMPUTER) -> atset1:57451 */\n  \"LaunchApp2\": 0xe021, /* html:LaunchApp2 (LaunchApp2) -> linux:140 (KEY_CALC) -> atset1:57377 */\n  \"LaunchMail\": 0xe06c, /* html:LaunchMail (LaunchMail) -> linux:155 (KEY_MAIL) -> atset1:57452 */\n  \"MediaPlayPause\": 0xe022, /* html:MediaPlayPause (MediaPlayPause) -> linux:164 (KEY_PLAYPAUSE) -> atset1:57378 */\n  \"MediaSelect\": 0xe06d, /* html:MediaSelect (MediaSelect) -> linux:226 (KEY_MEDIA) -> atset1:57453 */\n  \"MediaStop\": 0xe024, /* html:MediaStop (MediaStop) -> linux:166 (KEY_STOPCD) -> atset1:57380 */\n  \"MediaTrackNext\": 0xe019, /* html:MediaTrackNext (MediaTrackNext) -> linux:163 (KEY_NEXTSONG) -> atset1:57369 */\n  \"MediaTrackPrevious\": 0xe010, /* html:MediaTrackPrevious (MediaTrackPrevious) -> linux:165 (KEY_PREVIOUSSONG) -> atset1:57360 */\n  \"MetaLeft\": 0xe05b, /* html:MetaLeft (MetaLeft) -> linux:125 (KEY_LEFTMETA) -> atset1:57435 */\n  \"MetaRight\": 0xe05c, /* html:MetaRight (MetaRight) -> linux:126 (KEY_RIGHTMETA) -> atset1:57436 */\n  \"Minus\": 0xc, /* html:Minus (Minus) -> linux:12 (KEY_MINUS) -> atset1:12 */\n  \"NonConvert\": 0x7b, /* html:NonConvert (NonConvert) -> linux:94 (KEY_MUHENKAN) -> atset1:123 */\n  \"NumLock\": 0x45, /* html:NumLock (NumLock) -> linux:69 (KEY_NUMLOCK) -> atset1:69 */\n  \"Numpad0\": 0x52, /* html:Numpad0 (Numpad0) -> linux:82 (KEY_KP0) -> atset1:82 */\n  \"Numpad1\": 0x4f, /* html:Numpad1 (Numpad1) -> linux:79 (KEY_KP1) -> atset1:79 */\n  \"Numpad2\": 0x50, /* html:Numpad2 (Numpad2) -> linux:80 (KEY_KP2) -> atset1:80 */\n  \"Numpad3\": 0x51, /* html:Numpad3 (Numpad3) -> linux:81 (KEY_KP3) -> atset1:81 */\n  \"Numpad4\": 0x4b, /* html:Numpad4 (Numpad4) -> linux:75 (KEY_KP4) -> atset1:75 */\n  \"Numpad5\": 0x4c, /* html:Numpad5 (Numpad5) -> linux:76 (KEY_KP5) -> atset1:76 */\n  \"Numpad6\": 0x4d, /* html:Numpad6 (Numpad6) -> linux:77 (KEY_KP6) -> atset1:77 */\n  \"Numpad7\": 0x47, /* html:Numpad7 (Numpad7) -> linux:71 (KEY_KP7) -> atset1:71 */\n  \"Numpad8\": 0x48, /* html:Numpad8 (Numpad8) -> linux:72 (KEY_KP8) -> atset1:72 */\n  \"Numpad9\": 0x49, /* html:Numpad9 (Numpad9) -> linux:73 (KEY_KP9) -> atset1:73 */\n  \"NumpadAdd\": 0x4e, /* html:NumpadAdd (NumpadAdd) -> linux:78 (KEY_KPPLUS) -> atset1:78 */\n  \"NumpadComma\": 0x7e, /* html:NumpadComma (NumpadComma) -> linux:121 (KEY_KPCOMMA) -> atset1:126 */\n  \"NumpadDecimal\": 0x53, /* html:NumpadDecimal (NumpadDecimal) -> linux:83 (KEY_KPDOT) -> atset1:83 */\n  \"NumpadDivide\": 0xe035, /* html:NumpadDivide (NumpadDivide) -> linux:98 (KEY_KPSLASH) -> atset1:57397 */\n  \"NumpadEnter\": 0xe01c, /* html:NumpadEnter (NumpadEnter) -> linux:96 (KEY_KPENTER) -> atset1:57372 */\n  \"NumpadEqual\": 0x59, /* html:NumpadEqual (NumpadEqual) -> linux:117 (KEY_KPEQUAL) -> atset1:89 */\n  \"NumpadMultiply\": 0x37, /* html:NumpadMultiply (NumpadMultiply) -> linux:55 (KEY_KPASTERISK) -> atset1:55 */\n  \"NumpadParenLeft\": 0xe076, /* html:NumpadParenLeft (NumpadParenLeft) -> linux:179 (KEY_KPLEFTPAREN) -> atset1:57462 */\n  \"NumpadParenRight\": 0xe07b, /* html:NumpadParenRight (NumpadParenRight) -> linux:180 (KEY_KPRIGHTPAREN) -> atset1:57467 */\n  \"NumpadSubtract\": 0x4a, /* html:NumpadSubtract (NumpadSubtract) -> linux:74 (KEY_KPMINUS) -> atset1:74 */\n  \"Open\": 0x64, /* html:Open (Open) -> linux:134 (KEY_OPEN) -> atset1:100 */\n  \"PageDown\": 0xe051, /* html:PageDown (PageDown) -> linux:109 (KEY_PAGEDOWN) -> atset1:57425 */\n  \"PageUp\": 0xe049, /* html:PageUp (PageUp) -> linux:104 (KEY_PAGEUP) -> atset1:57417 */\n  \"Paste\": 0x65, /* html:Paste (Paste) -> linux:135 (KEY_PASTE) -> atset1:101 */\n  \"Pause\": 0xe046, /* html:Pause (Pause) -> linux:119 (KEY_PAUSE) -> atset1:57414 */\n  \"Period\": 0x34, /* html:Period (Period) -> linux:52 (KEY_DOT) -> atset1:52 */\n  \"Power\": 0xe05e, /* html:Power (Power) -> linux:116 (KEY_POWER) -> atset1:57438 */\n  \"PrintScreen\": 0x54, /* html:PrintScreen (PrintScreen) -> linux:99 (KEY_SYSRQ) -> atset1:84 */\n  \"Props\": 0xe006, /* html:Props (Props) -> linux:130 (KEY_PROPS) -> atset1:57350 */\n  \"Quote\": 0x28, /* html:Quote (Quote) -> linux:40 (KEY_APOSTROPHE) -> atset1:40 */\n  \"ScrollLock\": 0x46, /* html:ScrollLock (ScrollLock) -> linux:70 (KEY_SCROLLLOCK) -> atset1:70 */\n  \"Semicolon\": 0x27, /* html:Semicolon (Semicolon) -> linux:39 (KEY_SEMICOLON) -> atset1:39 */\n  \"ShiftLeft\": 0x2a, /* html:ShiftLeft (ShiftLeft) -> linux:42 (KEY_LEFTSHIFT) -> atset1:42 */\n  \"ShiftRight\": 0x36, /* html:ShiftRight (ShiftRight) -> linux:54 (KEY_RIGHTSHIFT) -> atset1:54 */\n  \"Slash\": 0x35, /* html:Slash (Slash) -> linux:53 (KEY_SLASH) -> atset1:53 */\n  \"Sleep\": 0xe05f, /* html:Sleep (Sleep) -> linux:142 (KEY_SLEEP) -> atset1:57439 */\n  \"Space\": 0x39, /* html:Space (Space) -> linux:57 (KEY_SPACE) -> atset1:57 */\n  \"Suspend\": 0xe025, /* html:Suspend (Suspend) -> linux:205 (KEY_SUSPEND) -> atset1:57381 */\n  \"Tab\": 0xf, /* html:Tab (Tab) -> linux:15 (KEY_TAB) -> atset1:15 */\n  \"Undo\": 0xe007, /* html:Undo (Undo) -> linux:131 (KEY_UNDO) -> atset1:57351 */\n  \"WakeUp\": 0xe063, /* html:WakeUp (WakeUp) -> linux:143 (KEY_WAKEUP) -> atset1:57443 */\n};\n"
  },
  {
    "path": "core/ra2.js",
    "content": "import { encodeUTF8 } from './util/strings.js';\nimport EventTargetMixin from './util/eventtarget.js';\nimport legacyCrypto from './crypto/crypto.js';\n\nclass RA2Cipher {\n    constructor() {\n        this._cipher = null;\n        this._counter = new Uint8Array(16);\n    }\n\n    async setKey(key) {\n        this._cipher = await legacyCrypto.importKey(\n            \"raw\", key, { name: \"AES-EAX\" }, false, [\"encrypt, decrypt\"]);\n    }\n\n    async makeMessage(message) {\n        const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]);\n        const encrypted = await legacyCrypto.encrypt({\n            name: \"AES-EAX\",\n            iv: this._counter,\n            additionalData: ad,\n        }, this._cipher, message);\n        for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);\n        const res = new Uint8Array(message.length + 2 + 16);\n        res.set(ad);\n        res.set(encrypted, 2);\n        return res;\n    }\n\n    async receiveMessage(length, encrypted) {\n        const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]);\n        const res = await legacyCrypto.decrypt({\n            name: \"AES-EAX\",\n            iv: this._counter,\n            additionalData: ad,\n        }, this._cipher, encrypted);\n        for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);\n        return res;\n    }\n}\n\nexport default class RSAAESAuthenticationState extends EventTargetMixin {\n    constructor(sock, getCredentials) {\n        super();\n        this._hasStarted = false;\n        this._checkSock = null;\n        this._checkCredentials = null;\n        this._approveServerResolve = null;\n        this._sockReject = null;\n        this._credentialsReject = null;\n        this._approveServerReject = null;\n        this._sock = sock;\n        this._getCredentials = getCredentials;\n    }\n\n    _waitSockAsync(len) {\n        return new Promise((resolve, reject) => {\n            const hasData = () => !this._sock.rQwait('RA2', len);\n            if (hasData()) {\n                resolve();\n            } else {\n                this._checkSock = () => {\n                    if (hasData()) {\n                        resolve();\n                        this._checkSock = null;\n                        this._sockReject = null;\n                    }\n                };\n                this._sockReject = reject;\n            }\n        });\n    }\n\n    _waitApproveKeyAsync() {\n        return new Promise((resolve, reject) => {\n            this._approveServerResolve = resolve;\n            this._approveServerReject = reject;\n        });\n    }\n\n    _waitCredentialsAsync(subtype) {\n        const hasCredentials = () => {\n            if (subtype === 1 && this._getCredentials().username !== undefined &&\n                this._getCredentials().password !== undefined) {\n                return true;\n            } else if (subtype === 2 && this._getCredentials().password !== undefined) {\n                return true;\n            }\n            return false;\n        };\n        return new Promise((resolve, reject) => {\n            if (hasCredentials()) {\n                resolve();\n            } else {\n                this._checkCredentials = () => {\n                    if (hasCredentials()) {\n                        resolve();\n                        this._checkCredentials = null;\n                        this._credentialsReject = null;\n                    }\n                };\n                this._credentialsReject = reject;\n            }\n        });\n    }\n\n    checkInternalEvents() {\n        if (this._checkSock !== null) {\n            this._checkSock();\n        }\n        if (this._checkCredentials !== null) {\n            this._checkCredentials();\n        }\n    }\n\n    approveServer() {\n        if (this._approveServerResolve !== null) {\n            this._approveServerResolve();\n            this._approveServerResolve = null;\n        }\n    }\n\n    disconnect() {\n        if (this._sockReject !== null) {\n            this._sockReject(new Error(\"disconnect normally\"));\n            this._sockReject = null;\n        }\n        if (this._credentialsReject !== null) {\n            this._credentialsReject(new Error(\"disconnect normally\"));\n            this._credentialsReject = null;\n        }\n        if (this._approveServerReject !== null) {\n            this._approveServerReject(new Error(\"disconnect normally\"));\n            this._approveServerReject = null;\n        }\n    }\n\n    async negotiateRA2neAuthAsync() {\n        this._hasStarted = true;\n        // 1: Receive server public key\n        await this._waitSockAsync(4);\n        const serverKeyLengthBuffer = this._sock.rQpeekBytes(4);\n        const serverKeyLength = this._sock.rQshift32();\n        if (serverKeyLength < 1024) {\n            throw new Error(\"RA2: server public key is too short: \" + serverKeyLength);\n        } else if (serverKeyLength > 8192) {\n            throw new Error(\"RA2: server public key is too long: \" + serverKeyLength);\n        }\n        const serverKeyBytes = Math.ceil(serverKeyLength / 8);\n        await this._waitSockAsync(serverKeyBytes * 2);\n        const serverN = this._sock.rQshiftBytes(serverKeyBytes);\n        const serverE = this._sock.rQshiftBytes(serverKeyBytes);\n        const serverRSACipher = await legacyCrypto.importKey(\n            \"raw\", { n: serverN, e: serverE }, { name: \"RSA-PKCS1-v1_5\" }, false, [\"encrypt\"]);\n        const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2);\n        serverPublickey.set(serverKeyLengthBuffer);\n        serverPublickey.set(serverN, 4);\n        serverPublickey.set(serverE, 4 + serverKeyBytes);\n\n        // verify server public key\n        let approveKey = this._waitApproveKeyAsync();\n        this.dispatchEvent(new CustomEvent(\"serververification\", {\n            detail: { type: \"RSA\", publickey: serverPublickey }\n        }));\n        await approveKey;\n\n        // 2: Send client public key\n        const clientKeyLength = 2048;\n        const clientKeyBytes = Math.ceil(clientKeyLength / 8);\n        const clientRSACipher = (await legacyCrypto.generateKey({\n            name: \"RSA-PKCS1-v1_5\",\n            modulusLength: clientKeyLength,\n            publicExponent: new Uint8Array([1, 0, 1]),\n        }, true, [\"encrypt\"])).privateKey;\n        const clientExportedRSAKey = await legacyCrypto.exportKey(\"raw\", clientRSACipher);\n        const clientN = clientExportedRSAKey.n;\n        const clientE = clientExportedRSAKey.e;\n        const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2);\n        clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24;\n        clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16;\n        clientPublicKey[2] = (clientKeyLength & 0xff00) >>> 8;\n        clientPublicKey[3] = clientKeyLength & 0xff;\n        clientPublicKey.set(clientN, 4);\n        clientPublicKey.set(clientE, 4 + clientKeyBytes);\n        this._sock.sQpushBytes(clientPublicKey);\n        this._sock.flush();\n\n        // 3: Send client random\n        const clientRandom = new Uint8Array(16);\n        window.crypto.getRandomValues(clientRandom);\n        const clientEncryptedRandom = await legacyCrypto.encrypt(\n            { name: \"RSA-PKCS1-v1_5\" }, serverRSACipher, clientRandom);\n        const clientRandomMessage = new Uint8Array(2 + serverKeyBytes);\n        clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8;\n        clientRandomMessage[1] = serverKeyBytes & 0xff;\n        clientRandomMessage.set(clientEncryptedRandom, 2);\n        this._sock.sQpushBytes(clientRandomMessage);\n        this._sock.flush();\n\n        // 4: Receive server random\n        await this._waitSockAsync(2);\n        if (this._sock.rQshift16() !== clientKeyBytes) {\n            throw new Error(\"RA2: wrong encrypted message length\");\n        }\n        const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes);\n        const serverRandom = await legacyCrypto.decrypt(\n            { name: \"RSA-PKCS1-v1_5\" }, clientRSACipher, serverEncryptedRandom);\n        if (serverRandom === null || serverRandom.length !== 16) {\n            throw new Error(\"RA2: corrupted server encrypted random\");\n        }\n\n        // 5: Compute session keys and set ciphers\n        let clientSessionKey = new Uint8Array(32);\n        let serverSessionKey = new Uint8Array(32);\n        clientSessionKey.set(serverRandom);\n        clientSessionKey.set(clientRandom, 16);\n        serverSessionKey.set(clientRandom);\n        serverSessionKey.set(serverRandom, 16);\n        clientSessionKey = await window.crypto.subtle.digest(\"SHA-1\", clientSessionKey);\n        clientSessionKey = new Uint8Array(clientSessionKey).slice(0, 16);\n        serverSessionKey = await window.crypto.subtle.digest(\"SHA-1\", serverSessionKey);\n        serverSessionKey = new Uint8Array(serverSessionKey).slice(0, 16);\n        const clientCipher = new RA2Cipher();\n        await clientCipher.setKey(clientSessionKey);\n        const serverCipher = new RA2Cipher();\n        await serverCipher.setKey(serverSessionKey);\n\n        // 6: Compute and exchange hashes\n        let serverHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2);\n        let clientHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2);\n        serverHash.set(serverPublickey);\n        serverHash.set(clientPublicKey, 4 + serverKeyBytes * 2);\n        clientHash.set(clientPublicKey);\n        clientHash.set(serverPublickey, 4 + clientKeyBytes * 2);\n        serverHash = await window.crypto.subtle.digest(\"SHA-1\", serverHash);\n        clientHash = await window.crypto.subtle.digest(\"SHA-1\", clientHash);\n        serverHash = new Uint8Array(serverHash);\n        clientHash = new Uint8Array(clientHash);\n        this._sock.sQpushBytes(await clientCipher.makeMessage(clientHash));\n        this._sock.flush();\n        await this._waitSockAsync(2 + 20 + 16);\n        if (this._sock.rQshift16() !== 20) {\n            throw new Error(\"RA2: wrong server hash\");\n        }\n        const serverHashReceived = await serverCipher.receiveMessage(\n            20, this._sock.rQshiftBytes(20 + 16));\n        if (serverHashReceived === null) {\n            throw new Error(\"RA2: failed to authenticate the message\");\n        }\n        for (let i = 0; i < 20; i++) {\n            if (serverHashReceived[i] !== serverHash[i]) {\n                throw new Error(\"RA2: wrong server hash\");\n            }\n        }\n\n        // 7: Receive subtype\n        await this._waitSockAsync(2 + 1 + 16);\n        if (this._sock.rQshift16() !== 1) {\n            throw new Error(\"RA2: wrong subtype\");\n        }\n        let subtype = (await serverCipher.receiveMessage(\n            1, this._sock.rQshiftBytes(1 + 16)));\n        if (subtype === null) {\n            throw new Error(\"RA2: failed to authenticate the message\");\n        }\n        subtype = subtype[0];\n        let waitCredentials = this._waitCredentialsAsync(subtype);\n        if (subtype === 1) {\n            if (this._getCredentials().username === undefined ||\n                this._getCredentials().password === undefined) {\n                this.dispatchEvent(new CustomEvent(\n                    \"credentialsrequired\",\n                    { detail: { types: [\"username\", \"password\"] } }));\n            }\n        } else if (subtype === 2) {\n            if (this._getCredentials().password === undefined) {\n                this.dispatchEvent(new CustomEvent(\n                    \"credentialsrequired\",\n                    { detail: { types: [\"password\"] } }));\n            }\n        } else {\n            throw new Error(\"RA2: wrong subtype\");\n        }\n        await waitCredentials;\n        let username;\n        if (subtype === 1) {\n            username = encodeUTF8(this._getCredentials().username).slice(0, 255);\n        } else {\n            username = \"\";\n        }\n        const password = encodeUTF8(this._getCredentials().password).slice(0, 255);\n        const credentials = new Uint8Array(username.length + password.length + 2);\n        credentials[0] = username.length;\n        credentials[username.length + 1] = password.length;\n        for (let i = 0; i < username.length; i++) {\n            credentials[i + 1] = username.charCodeAt(i);\n        }\n        for (let i = 0; i < password.length; i++) {\n            credentials[username.length + 2 + i] = password.charCodeAt(i);\n        }\n        this._sock.sQpushBytes(await clientCipher.makeMessage(credentials));\n        this._sock.flush();\n    }\n\n    get hasStarted() {\n        return this._hasStarted;\n    }\n\n    set hasStarted(s) {\n        this._hasStarted = s;\n    }\n}\n"
  },
  {
    "path": "core/rfb.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2020 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n *\n */\n\nimport { toUnsigned32bit, toSigned32bit } from './util/int.js';\nimport * as Log from './util/logging.js';\nimport { encodeUTF8, decodeUTF8 } from './util/strings.js';\nimport { dragThreshold, supportsWebCodecsH264Decode } from './util/browser.js';\nimport { clientToElement } from './util/element.js';\nimport { setCapture } from './util/events.js';\nimport EventTargetMixin from './util/eventtarget.js';\nimport Display from \"./display.js\";\nimport AsyncClipboard from \"./clipboard.js\";\nimport Inflator from \"./inflator.js\";\nimport Deflator from \"./deflator.js\";\nimport Keyboard from \"./input/keyboard.js\";\nimport GestureHandler from \"./input/gesturehandler.js\";\nimport Cursor from \"./util/cursor.js\";\nimport Websock from \"./websock.js\";\nimport KeyTable from \"./input/keysym.js\";\nimport XtScancode from \"./input/xtscancodes.js\";\nimport { encodings } from \"./encodings.js\";\nimport RSAAESAuthenticationState from \"./ra2.js\";\nimport legacyCrypto from \"./crypto/crypto.js\";\n\nimport RawDecoder from \"./decoders/raw.js\";\nimport CopyRectDecoder from \"./decoders/copyrect.js\";\nimport RREDecoder from \"./decoders/rre.js\";\nimport HextileDecoder from \"./decoders/hextile.js\";\nimport ZlibDecoder from './decoders/zlib.js';\nimport TightDecoder from \"./decoders/tight.js\";\nimport TightPNGDecoder from \"./decoders/tightpng.js\";\nimport ZRLEDecoder from \"./decoders/zrle.js\";\nimport JPEGDecoder from \"./decoders/jpeg.js\";\nimport H264Decoder from \"./decoders/h264.js\";\n\n// How many seconds to wait for a disconnect to finish\nconst DISCONNECT_TIMEOUT = 3;\nconst DEFAULT_BACKGROUND = 'rgb(40, 40, 40)';\n\n// Minimum wait (ms) between two mouse moves\nconst MOUSE_MOVE_DELAY = 17;\n\n// Wheel thresholds\nconst WHEEL_STEP = 50; // Pixels needed for one step\nconst WHEEL_LINE_HEIGHT = 19; // Assumed pixels for one line step\n\n// Gesture thresholds\nconst GESTURE_ZOOMSENS = 75;\nconst GESTURE_SCRLSENS = 50;\nconst DOUBLE_TAP_TIMEOUT = 1000;\nconst DOUBLE_TAP_THRESHOLD = 50;\n\n// Security types\nconst securityTypeNone              = 1;\nconst securityTypeVNCAuth           = 2;\nconst securityTypeRA2ne             = 6;\nconst securityTypeTight             = 16;\nconst securityTypeVeNCrypt          = 19;\nconst securityTypeXVP               = 22;\nconst securityTypeARD               = 30;\nconst securityTypeMSLogonII         = 113;\n\n// Special Tight security types\nconst securityTypeUnixLogon         = 129;\n\n// VeNCrypt security types\nconst securityTypePlain             = 256;\n\n// Extended clipboard pseudo-encoding formats\nconst extendedClipboardFormatText   = 1;\n/*eslint-disable no-unused-vars */\nconst extendedClipboardFormatRtf    = 1 << 1;\nconst extendedClipboardFormatHtml   = 1 << 2;\nconst extendedClipboardFormatDib    = 1 << 3;\nconst extendedClipboardFormatFiles  = 1 << 4;\n/*eslint-enable */\n\n// Extended clipboard pseudo-encoding actions\nconst extendedClipboardActionCaps    = 1 << 24;\nconst extendedClipboardActionRequest = 1 << 25;\nconst extendedClipboardActionPeek    = 1 << 26;\nconst extendedClipboardActionNotify  = 1 << 27;\nconst extendedClipboardActionProvide = 1 << 28;\n\nexport default class RFB extends EventTargetMixin {\n    constructor(target, urlOrChannel, options) {\n        if (!target) {\n            throw new Error(\"Must specify target\");\n        }\n        if (!urlOrChannel) {\n            throw new Error(\"Must specify URL, WebSocket or RTCDataChannel\");\n        }\n\n        // We rely on modern APIs which might not be available in an\n        // insecure context\n        if (!window.isSecureContext) {\n            Log.Error(\"noVNC requires a secure context (TLS). Expect crashes!\");\n        }\n\n        super();\n\n        this._target = target;\n\n        if (typeof urlOrChannel === \"string\") {\n            this._url = urlOrChannel;\n        } else {\n            this._url = null;\n            this._rawChannel = urlOrChannel;\n        }\n\n        // Connection details\n        options = options || {};\n        this._rfbCredentials = options.credentials || {};\n        this._shared = 'shared' in options ? !!options.shared : true;\n        this._repeaterID = options.repeaterID || '';\n        this._wsProtocols = options.wsProtocols || [];\n\n        // Internal state\n        this._rfbConnectionState = '';\n        this._rfbInitState = '';\n        this._rfbAuthScheme = -1;\n        this._rfbCleanDisconnect = true;\n        this._rfbRSAAESAuthenticationState = null;\n\n        // Server capabilities\n        this._rfbVersion = 0;\n        this._rfbMaxVersion = 3.8;\n        this._rfbTightVNC = false;\n        this._rfbVeNCryptState = 0;\n        this._rfbXvpVer = 0;\n\n        this._fbWidth = 0;\n        this._fbHeight = 0;\n\n        this._fbName = \"\";\n\n        this._capabilities = { power: false };\n\n        this._supportsFence = false;\n\n        this._supportsContinuousUpdates = false;\n        this._enabledContinuousUpdates = false;\n\n        this._supportsSetDesktopSize = false;\n        this._screenID = 0;\n        this._screenFlags = 0;\n        this._pendingRemoteResize = false;\n        this._lastResize = 0;\n\n        this._qemuExtKeyEventSupported = false;\n\n        this._extendedPointerEventSupported = false;\n\n        this._clipboardText = null;\n        this._clipboardServerCapabilitiesActions = {};\n        this._clipboardServerCapabilitiesFormats = {};\n\n        // Internal objects\n        this._sock = null;              // Websock object\n        this._display = null;           // Display object\n        this._flushing = false;         // Display flushing state\n        this._asyncClipboard = null;    // Async clipboard object\n        this._keyboard = null;          // Keyboard input handler object\n        this._gestures = null;          // Gesture input handler object\n        this._resizeObserver = null;    // Resize observer object\n\n        // Timers\n        this._disconnTimer = null;      // disconnection timer\n        this._resizeTimeout = null;     // resize rate limiting\n        this._mouseMoveTimer = null;\n\n        // Decoder states\n        this._decoders = {};\n\n        this._FBU = {\n            rects: 0,\n            x: 0,\n            y: 0,\n            width: 0,\n            height: 0,\n            encoding: null,\n        };\n\n        // Mouse state\n        this._mousePos = {};\n        this._mouseButtonMask = 0;\n        this._mouseLastMoveTime = 0;\n        this._viewportDragging = false;\n        this._viewportDragPos = {};\n        this._viewportHasMoved = false;\n        this._accumulatedWheelDeltaX = 0;\n        this._accumulatedWheelDeltaY = 0;\n\n        // Gesture state\n        this._gestureLastTapTime = null;\n        this._gestureFirstDoubleTapEv = null;\n        this._gestureLastMagnitudeX = 0;\n        this._gestureLastMagnitudeY = 0;\n\n        // Bound event handlers\n        this._eventHandlers = {\n            focusCanvas: this._focusCanvas.bind(this),\n            handleResize: this._handleResize.bind(this),\n            handleMouse: this._handleMouse.bind(this),\n            handleWheel: this._handleWheel.bind(this),\n            handleGesture: this._handleGesture.bind(this),\n            handleRSAAESCredentialsRequired: this._handleRSAAESCredentialsRequired.bind(this),\n            handleRSAAESServerVerification: this._handleRSAAESServerVerification.bind(this),\n        };\n\n        // main setup\n        Log.Debug(\">> RFB.constructor\");\n\n        // Create DOM elements\n        this._screen = document.createElement('div');\n        this._screen.style.display = 'flex';\n        this._screen.style.width = '100%';\n        this._screen.style.height = '100%';\n        this._screen.style.overflow = 'auto';\n        this._screen.style.background = DEFAULT_BACKGROUND;\n        this._canvas = document.createElement('canvas');\n        this._canvas.style.margin = 'auto';\n        // Some browsers add an outline on focus\n        this._canvas.style.outline = 'none';\n        this._canvas.width = 0;\n        this._canvas.height = 0;\n        this._canvas.tabIndex = -1;\n        this._screen.appendChild(this._canvas);\n\n        // Cursor\n        this._cursor = new Cursor();\n\n        // XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes\n        // it. Result: no cursor at all until a window border or an edit field\n        // is hit blindly. But there are also VNC servers that draw the cursor\n        // in the framebuffer and don't send the empty local cursor. There is\n        // no way to satisfy both sides.\n        //\n        // The spec is unclear on this \"initial cursor\" issue. Many other\n        // viewers (TigerVNC, RealVNC, Remmina) display an arrow as the\n        // initial cursor instead.\n        this._cursorImage = RFB.cursors.none;\n\n        // populate decoder array with objects\n        this._decoders[encodings.encodingRaw] = new RawDecoder();\n        this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder();\n        this._decoders[encodings.encodingRRE] = new RREDecoder();\n        this._decoders[encodings.encodingHextile] = new HextileDecoder();\n        this._decoders[encodings.encodingZlib] = new ZlibDecoder();\n        this._decoders[encodings.encodingTight] = new TightDecoder();\n        this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();\n        this._decoders[encodings.encodingZRLE] = new ZRLEDecoder();\n        this._decoders[encodings.encodingJPEG] = new JPEGDecoder();\n        this._decoders[encodings.encodingH264] = new H264Decoder();\n\n        // NB: nothing that needs explicit teardown should be done\n        // before this point, since this can throw an exception\n        try {\n            this._display = new Display(this._canvas);\n        } catch (exc) {\n            Log.Error(\"Display exception: \" + exc);\n            throw exc;\n        }\n\n        this._asyncClipboard = new AsyncClipboard(this._canvas);\n        this._asyncClipboard.onpaste = this.clipboardPasteFrom.bind(this);\n\n        this._keyboard = new Keyboard(this._canvas);\n        this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);\n        this._remoteCapsLock = null; // Null indicates unknown or irrelevant\n        this._remoteNumLock = null;\n\n        this._gestures = new GestureHandler();\n\n        this._sock = new Websock();\n        this._sock.on('open', this._socketOpen.bind(this));\n        this._sock.on('close', this._socketClose.bind(this));\n        this._sock.on('message', this._handleMessage.bind(this));\n        this._sock.on('error', this._socketError.bind(this));\n\n        this._expectedClientWidth = null;\n        this._expectedClientHeight = null;\n        this._resizeObserver = new ResizeObserver(this._eventHandlers.handleResize);\n\n        // All prepared, kick off the connection\n        this._updateConnectionState('connecting');\n\n        Log.Debug(\"<< RFB.constructor\");\n\n        // ===== PROPERTIES =====\n\n        this.dragViewport = false;\n        this.focusOnClick = true;\n\n        this._viewOnly = false;\n        this._clipViewport = false;\n        this._clippingViewport = false;\n        this._scaleViewport = false;\n        this._resizeSession = false;\n\n        this._showDotCursor = false;\n\n        this._qualityLevel = 6;\n        this._compressionLevel = 2;\n    }\n\n    // ===== PROPERTIES =====\n\n    get viewOnly() { return this._viewOnly; }\n    set viewOnly(viewOnly) {\n        this._viewOnly = viewOnly;\n\n        if (this._rfbConnectionState === \"connecting\" ||\n            this._rfbConnectionState === \"connected\") {\n            if (viewOnly) {\n                this._keyboard.ungrab();\n                this._asyncClipboard.ungrab();\n            } else {\n                this._keyboard.grab();\n                this._asyncClipboard.grab();\n            }\n        }\n    }\n\n    get capabilities() { return this._capabilities; }\n\n    get clippingViewport() { return this._clippingViewport; }\n    _setClippingViewport(on) {\n        if (on === this._clippingViewport) {\n            return;\n        }\n        this._clippingViewport = on;\n        this.dispatchEvent(new CustomEvent(\"clippingviewport\",\n                                           { detail: this._clippingViewport }));\n    }\n\n    get touchButton() { return 0; }\n    set touchButton(button) { Log.Warn(\"Using old API!\"); }\n\n    get clipViewport() { return this._clipViewport; }\n    set clipViewport(viewport) {\n        this._clipViewport = viewport;\n        this._updateClip();\n    }\n\n    get scaleViewport() { return this._scaleViewport; }\n    set scaleViewport(scale) {\n        this._scaleViewport = scale;\n        // Scaling trumps clipping, so we may need to adjust\n        // clipping when enabling or disabling scaling\n        if (scale && this._clipViewport) {\n            this._updateClip();\n        }\n        this._updateScale();\n        if (!scale && this._clipViewport) {\n            this._updateClip();\n        }\n    }\n\n    get resizeSession() { return this._resizeSession; }\n    set resizeSession(resize) {\n        this._resizeSession = resize;\n        if (resize) {\n            this._requestRemoteResize();\n        }\n    }\n\n    get showDotCursor() { return this._showDotCursor; }\n    set showDotCursor(show) {\n        this._showDotCursor = show;\n        this._refreshCursor();\n    }\n\n    get background() { return this._screen.style.background; }\n    set background(cssValue) { this._screen.style.background = cssValue; }\n\n    get qualityLevel() {\n        return this._qualityLevel;\n    }\n    set qualityLevel(qualityLevel) {\n        if (!Number.isInteger(qualityLevel) || qualityLevel < 0 || qualityLevel > 9) {\n            Log.Error(\"qualityLevel must be an integer between 0 and 9\");\n            return;\n        }\n\n        if (this._qualityLevel === qualityLevel) {\n            return;\n        }\n\n        this._qualityLevel = qualityLevel;\n\n        if (this._rfbConnectionState === 'connected') {\n            this._sendEncodings();\n        }\n    }\n\n    get compressionLevel() {\n        return this._compressionLevel;\n    }\n    set compressionLevel(compressionLevel) {\n        if (!Number.isInteger(compressionLevel) || compressionLevel < 0 || compressionLevel > 9) {\n            Log.Error(\"compressionLevel must be an integer between 0 and 9\");\n            return;\n        }\n\n        if (this._compressionLevel === compressionLevel) {\n            return;\n        }\n\n        this._compressionLevel = compressionLevel;\n\n        if (this._rfbConnectionState === 'connected') {\n            this._sendEncodings();\n        }\n    }\n\n    // ===== PUBLIC METHODS =====\n\n    disconnect() {\n        this._updateConnectionState('disconnecting');\n        this._sock.off('error');\n        this._sock.off('message');\n        this._sock.off('open');\n        if (this._rfbRSAAESAuthenticationState !== null) {\n            this._rfbRSAAESAuthenticationState.disconnect();\n        }\n    }\n\n    approveServer() {\n        if (this._rfbRSAAESAuthenticationState !== null) {\n            this._rfbRSAAESAuthenticationState.approveServer();\n        }\n    }\n\n    sendCredentials(creds) {\n        this._rfbCredentials = creds;\n        this._resumeAuthentication();\n    }\n\n    sendCtrlAltDel() {\n        if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }\n        Log.Info(\"Sending Ctrl-Alt-Del\");\n\n        this.sendKey(KeyTable.XK_Control_L, \"ControlLeft\", true);\n        this.sendKey(KeyTable.XK_Alt_L, \"AltLeft\", true);\n        this.sendKey(KeyTable.XK_Delete, \"Delete\", true);\n        this.sendKey(KeyTable.XK_Delete, \"Delete\", false);\n        this.sendKey(KeyTable.XK_Alt_L, \"AltLeft\", false);\n        this.sendKey(KeyTable.XK_Control_L, \"ControlLeft\", false);\n    }\n\n    machineShutdown() {\n        this._xvpOp(1, 2);\n    }\n\n    machineReboot() {\n        this._xvpOp(1, 3);\n    }\n\n    machineReset() {\n        this._xvpOp(1, 4);\n    }\n\n    // Send a key press. If 'down' is not specified then send a down key\n    // followed by an up key.\n    sendKey(keysym, code, down) {\n        if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }\n\n        if (down === undefined) {\n            this.sendKey(keysym, code, true);\n            this.sendKey(keysym, code, false);\n            return;\n        }\n\n        const scancode = XtScancode[code];\n\n        if (this._qemuExtKeyEventSupported && scancode) {\n            // 0 is NoSymbol\n            keysym = keysym || 0;\n\n            Log.Info(\"Sending key (\" + (down ? \"down\" : \"up\") + \"): keysym \" + keysym + \", scancode \" + scancode);\n\n            RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode);\n        } else {\n            if (!keysym) {\n                return;\n            }\n            Log.Info(\"Sending keysym (\" + (down ? \"down\" : \"up\") + \"): \" + keysym);\n            RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);\n        }\n    }\n\n    focus(options) {\n        this._canvas.focus(options);\n    }\n\n    blur() {\n        this._canvas.blur();\n    }\n\n    clipboardPasteFrom(text) {\n        if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }\n\n        if (this._clipboardServerCapabilitiesFormats[extendedClipboardFormatText] &&\n            this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {\n\n            this._clipboardText = text;\n            RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);\n        } else {\n            let length, i;\n            let data;\n\n            length = 0;\n            // eslint-disable-next-line no-unused-vars\n            for (let codePoint of text) {\n                length++;\n            }\n\n            data = new Uint8Array(length);\n\n            i = 0;\n            for (let codePoint of text) {\n                let code = codePoint.codePointAt(0);\n\n                /* Only ISO 8859-1 is supported */\n                if (code > 0xff) {\n                    code = 0x3f; // '?'\n                }\n\n                data[i++] = code;\n            }\n\n            RFB.messages.clientCutText(this._sock, data);\n        }\n    }\n\n    getImageData() {\n        return this._display.getImageData();\n    }\n\n    toDataURL(type, encoderOptions) {\n        return this._display.toDataURL(type, encoderOptions);\n    }\n\n    toBlob(callback, type, quality) {\n        return this._display.toBlob(callback, type, quality);\n    }\n\n    // ===== PRIVATE METHODS =====\n\n    _connect() {\n        Log.Debug(\">> RFB.connect\");\n\n        if (this._url) {\n            Log.Info(`connecting to ${this._url}`);\n            this._sock.open(this._url, this._wsProtocols);\n        } else {\n            Log.Info(`attaching ${this._rawChannel} to Websock`);\n            this._sock.attach(this._rawChannel);\n\n            if (this._sock.readyState === 'closed') {\n                throw Error(\"Cannot use already closed WebSocket/RTCDataChannel\");\n            }\n\n            if (this._sock.readyState === 'open') {\n                // FIXME: _socketOpen() can in theory call _fail(), which\n                //        isn't allowed this early, but I'm not sure that can\n                //        happen without a bug messing up our state variables\n                this._socketOpen();\n            }\n        }\n\n        // Make our elements part of the page\n        this._target.appendChild(this._screen);\n\n        this._gestures.attach(this._canvas);\n\n        this._cursor.attach(this._canvas);\n        this._refreshCursor();\n\n        // Monitor size changes of the screen element\n        this._resizeObserver.observe(this._screen);\n\n        // Always grab focus on some kind of click event\n        this._canvas.addEventListener(\"mousedown\", this._eventHandlers.focusCanvas);\n        this._canvas.addEventListener(\"touchstart\", this._eventHandlers.focusCanvas);\n\n        // Mouse events\n        this._canvas.addEventListener('mousedown', this._eventHandlers.handleMouse);\n        this._canvas.addEventListener('mouseup', this._eventHandlers.handleMouse);\n        this._canvas.addEventListener('mousemove', this._eventHandlers.handleMouse);\n        // Prevent middle-click pasting (see handler for why we bind to document)\n        this._canvas.addEventListener('click', this._eventHandlers.handleMouse);\n        // preventDefault() on mousedown doesn't stop this event for some\n        // reason so we have to explicitly block it\n        this._canvas.addEventListener('contextmenu', this._eventHandlers.handleMouse);\n\n        // Wheel events\n        this._canvas.addEventListener(\"wheel\", this._eventHandlers.handleWheel);\n\n        // Gesture events\n        this._canvas.addEventListener(\"gesturestart\", this._eventHandlers.handleGesture);\n        this._canvas.addEventListener(\"gesturemove\", this._eventHandlers.handleGesture);\n        this._canvas.addEventListener(\"gestureend\", this._eventHandlers.handleGesture);\n\n        Log.Debug(\"<< RFB.connect\");\n    }\n\n    _disconnect() {\n        Log.Debug(\">> RFB.disconnect\");\n        this._cursor.detach();\n        this._canvas.removeEventListener(\"gesturestart\", this._eventHandlers.handleGesture);\n        this._canvas.removeEventListener(\"gesturemove\", this._eventHandlers.handleGesture);\n        this._canvas.removeEventListener(\"gestureend\", this._eventHandlers.handleGesture);\n        this._canvas.removeEventListener(\"wheel\", this._eventHandlers.handleWheel);\n        this._canvas.removeEventListener('mousedown', this._eventHandlers.handleMouse);\n        this._canvas.removeEventListener('mouseup', this._eventHandlers.handleMouse);\n        this._canvas.removeEventListener('mousemove', this._eventHandlers.handleMouse);\n        this._canvas.removeEventListener('click', this._eventHandlers.handleMouse);\n        this._canvas.removeEventListener('contextmenu', this._eventHandlers.handleMouse);\n        this._canvas.removeEventListener(\"mousedown\", this._eventHandlers.focusCanvas);\n        this._canvas.removeEventListener(\"touchstart\", this._eventHandlers.focusCanvas);\n        this._resizeObserver.disconnect();\n        this._keyboard.ungrab();\n        this._gestures.detach();\n        this._sock.close();\n        try {\n            this._target.removeChild(this._screen);\n        } catch (e) {\n            if (e.name === 'NotFoundError') {\n                // Some cases where the initial connection fails\n                // can disconnect before the _screen is created\n            } else {\n                throw e;\n            }\n        }\n        clearTimeout(this._resizeTimeout);\n        clearTimeout(this._mouseMoveTimer);\n        Log.Debug(\"<< RFB.disconnect\");\n    }\n\n    _socketOpen() {\n        if ((this._rfbConnectionState === 'connecting') &&\n            (this._rfbInitState === '')) {\n            this._rfbInitState = 'ProtocolVersion';\n            Log.Debug(\"Starting VNC handshake\");\n        } else {\n            this._fail(\"Unexpected server connection while \" +\n                       this._rfbConnectionState);\n        }\n    }\n\n    _socketClose(e) {\n        Log.Debug(\"WebSocket on-close event\");\n        let msg = \"\";\n        if (e.code) {\n            msg = \"(code: \" + e.code;\n            if (e.reason) {\n                msg += \", reason: \" + e.reason;\n            }\n            msg += \")\";\n        }\n        switch (this._rfbConnectionState) {\n            case 'connecting':\n                this._fail(\"Connection closed \" + msg);\n                break;\n            case 'connected':\n                // Handle disconnects that were initiated server-side\n                this._updateConnectionState('disconnecting');\n                this._updateConnectionState('disconnected');\n                break;\n            case 'disconnecting':\n                // Normal disconnection path\n                this._updateConnectionState('disconnected');\n                break;\n            case 'disconnected':\n                this._fail(\"Unexpected server disconnect \" +\n                           \"when already disconnected \" + msg);\n                break;\n            default:\n                this._fail(\"Unexpected server disconnect before connecting \" +\n                           msg);\n                break;\n        }\n        this._sock.off('close');\n        // Delete reference to raw channel to allow cleanup.\n        this._rawChannel = null;\n    }\n\n    _socketError(e) {\n        Log.Warn(\"WebSocket on-error event\");\n    }\n\n    _focusCanvas(event) {\n        if (!this.focusOnClick) {\n            return;\n        }\n\n        this.focus({ preventScroll: true });\n    }\n\n    _setDesktopName(name) {\n        this._fbName = name;\n        this.dispatchEvent(new CustomEvent(\n            \"desktopname\",\n            { detail: { name: this._fbName } }));\n    }\n\n    _saveExpectedClientSize() {\n        this._expectedClientWidth = this._screen.clientWidth;\n        this._expectedClientHeight = this._screen.clientHeight;\n    }\n\n    _currentClientSize() {\n        return [this._screen.clientWidth, this._screen.clientHeight];\n    }\n\n    _clientHasExpectedSize() {\n        const [currentWidth, currentHeight] = this._currentClientSize();\n        return currentWidth == this._expectedClientWidth &&\n            currentHeight == this._expectedClientHeight;\n    }\n\n    // Handle browser window resizes\n    _handleResize() {\n        // Don't change anything if the client size is already as expected\n        if (this._clientHasExpectedSize()) {\n            return;\n        }\n        // If the window resized then our screen element might have\n        // as well. Update the viewport dimensions.\n        window.requestAnimationFrame(() => {\n            this._updateClip();\n            this._updateScale();\n            this._saveExpectedClientSize();\n        });\n\n        // Request changing the resolution of the remote display to\n        // the size of the local browser viewport.\n        this._requestRemoteResize();\n    }\n\n    // Update state of clipping in Display object, and make sure the\n    // configured viewport matches the current screen size\n    _updateClip() {\n        const curClip = this._display.clipViewport;\n        let newClip = this._clipViewport;\n\n        if (this._scaleViewport) {\n            // Disable viewport clipping if we are scaling\n            newClip = false;\n        }\n\n        if (curClip !== newClip) {\n            this._display.clipViewport = newClip;\n        }\n\n        if (newClip) {\n            // When clipping is enabled, the screen is limited to\n            // the size of the container.\n            const size = this._screenSize();\n            this._display.viewportChangeSize(size.w, size.h);\n            this._fixScrollbars();\n            this._setClippingViewport(size.w < this._display.width ||\n                                      size.h < this._display.height);\n        } else {\n            this._setClippingViewport(false);\n        }\n\n        // When changing clipping we might show or hide scrollbars.\n        // This causes the expected client dimensions to change.\n        if (curClip !== newClip) {\n            this._saveExpectedClientSize();\n        }\n    }\n\n    _updateScale() {\n        if (!this._scaleViewport) {\n            this._display.scale = 1.0;\n        } else {\n            const size = this._screenSize();\n            this._display.autoscale(size.w, size.h);\n        }\n        this._fixScrollbars();\n    }\n\n    // Requests a change of remote desktop size. This message is an extension\n    // and may only be sent if we have received an ExtendedDesktopSize message\n    _requestRemoteResize() {\n        if (!this._resizeSession) {\n            return;\n        }\n        if (this._viewOnly) {\n            return;\n        }\n        if (!this._supportsSetDesktopSize) {\n            return;\n        }\n\n        // Rate limit to one pending resize at a time\n        if (this._pendingRemoteResize) {\n            return;\n        }\n\n        // And no more than once every 100ms\n        if ((Date.now() - this._lastResize) < 100) {\n            clearTimeout(this._resizeTimeout);\n            this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this),\n                                             100 - (Date.now() - this._lastResize));\n            return;\n        }\n        this._resizeTimeout = null;\n\n        const size = this._screenSize();\n\n        // Do we actually change anything?\n        if (size.w === this._fbWidth && size.h === this._fbHeight) {\n            return;\n        }\n\n        this._pendingRemoteResize = true;\n        this._lastResize = Date.now();\n        RFB.messages.setDesktopSize(this._sock,\n                                    Math.floor(size.w), Math.floor(size.h),\n                                    this._screenID, this._screenFlags);\n\n        Log.Debug('Requested new desktop size: ' +\n                   size.w + 'x' + size.h);\n    }\n\n    // Gets the the size of the available screen\n    _screenSize() {\n        let r = this._screen.getBoundingClientRect();\n        return { w: r.width, h: r.height };\n    }\n\n    _fixScrollbars() {\n        // This is a hack because Safari on macOS screws up the calculation\n        // for when scrollbars are needed. We get scrollbars when making the\n        // browser smaller, despite remote resize being enabled. So to fix it\n        // we temporarily toggle them off and on.\n        const orig = this._screen.style.overflow;\n        this._screen.style.overflow = 'hidden';\n        // Force Safari to recalculate the layout by asking for\n        // an element's dimensions\n        this._screen.getBoundingClientRect();\n        this._screen.style.overflow = orig;\n    }\n\n    /*\n     * Connection states:\n     *   connecting\n     *   connected\n     *   disconnecting\n     *   disconnected - permanent state\n     */\n    _updateConnectionState(state) {\n        const oldstate = this._rfbConnectionState;\n\n        if (state === oldstate) {\n            Log.Debug(\"Already in state '\" + state + \"', ignoring\");\n            return;\n        }\n\n        // The 'disconnected' state is permanent for each RFB object\n        if (oldstate === 'disconnected') {\n            Log.Error(\"Tried changing state of a disconnected RFB object\");\n            return;\n        }\n\n        // Ensure proper transitions before doing anything\n        switch (state) {\n            case 'connected':\n                if (oldstate !== 'connecting') {\n                    Log.Error(\"Bad transition to connected state, \" +\n                               \"previous connection state: \" + oldstate);\n                    return;\n                }\n                break;\n\n            case 'disconnected':\n                if (oldstate !== 'disconnecting') {\n                    Log.Error(\"Bad transition to disconnected state, \" +\n                               \"previous connection state: \" + oldstate);\n                    return;\n                }\n                break;\n\n            case 'connecting':\n                if (oldstate !== '') {\n                    Log.Error(\"Bad transition to connecting state, \" +\n                               \"previous connection state: \" + oldstate);\n                    return;\n                }\n                break;\n\n            case 'disconnecting':\n                if (oldstate !== 'connected' && oldstate !== 'connecting') {\n                    Log.Error(\"Bad transition to disconnecting state, \" +\n                               \"previous connection state: \" + oldstate);\n                    return;\n                }\n                break;\n\n            default:\n                Log.Error(\"Unknown connection state: \" + state);\n                return;\n        }\n\n        // State change actions\n\n        this._rfbConnectionState = state;\n\n        Log.Debug(\"New state '\" + state + \"', was '\" + oldstate + \"'.\");\n\n        if (this._disconnTimer && state !== 'disconnecting') {\n            Log.Debug(\"Clearing disconnect timer\");\n            clearTimeout(this._disconnTimer);\n            this._disconnTimer = null;\n\n            // make sure we don't get a double event\n            this._sock.off('close');\n        }\n\n        switch (state) {\n            case 'connecting':\n                this._connect();\n                break;\n\n            case 'connected':\n                this.dispatchEvent(new CustomEvent(\"connect\", { detail: {} }));\n                break;\n\n            case 'disconnecting':\n                this._disconnect();\n\n                this._disconnTimer = setTimeout(() => {\n                    Log.Error(\"Disconnection timed out.\");\n                    this._updateConnectionState('disconnected');\n                }, DISCONNECT_TIMEOUT * 1000);\n                break;\n\n            case 'disconnected':\n                this.dispatchEvent(new CustomEvent(\n                    \"disconnect\", { detail:\n                                    { clean: this._rfbCleanDisconnect } }));\n                break;\n        }\n    }\n\n    /* Print errors and disconnect\n     *\n     * The parameter 'details' is used for information that\n     * should be logged but not sent to the user interface.\n     */\n    _fail(details) {\n        switch (this._rfbConnectionState) {\n            case 'disconnecting':\n                Log.Error(\"Failed when disconnecting: \" + details);\n                break;\n            case 'connected':\n                Log.Error(\"Failed while connected: \" + details);\n                break;\n            case 'connecting':\n                Log.Error(\"Failed when connecting: \" + details);\n                break;\n            default:\n                Log.Error(\"RFB failure: \" + details);\n                break;\n        }\n        this._rfbCleanDisconnect = false; //This is sent to the UI\n\n        // Transition to disconnected without waiting for socket to close\n        this._updateConnectionState('disconnecting');\n        this._updateConnectionState('disconnected');\n\n        return false;\n    }\n\n    _setCapability(cap, val) {\n        this._capabilities[cap] = val;\n        this.dispatchEvent(new CustomEvent(\"capabilities\",\n                                           { detail: { capabilities: this._capabilities } }));\n    }\n\n    _handleMessage() {\n        if (this._sock.rQwait(\"message\", 1)) {\n            Log.Warn(\"handleMessage called on an empty receive queue\");\n            return;\n        }\n\n        switch (this._rfbConnectionState) {\n            case 'disconnected':\n                Log.Error(\"Got data while disconnected\");\n                break;\n            case 'connected':\n                while (true) {\n                    if (this._flushing) {\n                        break;\n                    }\n                    if (!this._normalMsg()) {\n                        break;\n                    }\n                    if (this._sock.rQwait(\"message\", 1)) {\n                        break;\n                    }\n                }\n                break;\n            case 'connecting':\n                while (this._rfbConnectionState === 'connecting') {\n                    if (!this._initMsg()) {\n                        break;\n                    }\n                }\n                break;\n            default:\n                Log.Error(\"Got data while in an invalid state\");\n                break;\n        }\n    }\n\n    _handleKeyEvent(keysym, code, down, numlock, capslock) {\n        // If remote state of capslock is known, and it doesn't match the local led state of\n        // the keyboard, we send a capslock keypress first to bring it into sync.\n        // If we just pressed CapsLock, or we toggled it remotely due to it being out of sync\n        // we clear the remote state so that we don't send duplicate or spurious fixes,\n        // since it may take some time to receive the new remote CapsLock state.\n        if (code == 'CapsLock' && down) {\n            this._remoteCapsLock = null;\n        }\n        if (this._remoteCapsLock !== null && capslock !== null && this._remoteCapsLock !== capslock && down) {\n            Log.Debug(\"Fixing remote caps lock\");\n\n            this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', true);\n            this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', false);\n            // We clear the remote capsLock state when we do this to prevent issues with doing this twice\n            // before we receive an update of the the remote state.\n            this._remoteCapsLock = null;\n        }\n\n        // Logic for numlock is exactly the same.\n        if (code == 'NumLock' && down) {\n            this._remoteNumLock = null;\n        }\n        if (this._remoteNumLock !== null && numlock !== null && this._remoteNumLock !== numlock && down) {\n            Log.Debug(\"Fixing remote num lock\");\n            this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', true);\n            this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', false);\n            this._remoteNumLock = null;\n        }\n        this.sendKey(keysym, code, down);\n    }\n\n    static _convertButtonMask(buttons) {\n        /* The bits in MouseEvent.buttons property correspond\n         * to the following mouse buttons:\n         *     0: Left\n         *     1: Right\n         *     2: Middle\n         *     3: Back\n         *     4: Forward\n         *\n         * These bits needs to be converted to what they are defined as\n         * in the RFB protocol.\n         */\n\n        const buttonMaskMap = {\n            0: 1 << 0, // Left\n            1: 1 << 2, // Right\n            2: 1 << 1, // Middle\n            3: 1 << 7, // Back\n            4: 1 << 8, // Forward\n        };\n\n        let bmask = 0;\n        for (let i = 0; i < 5; i++) {\n            if (buttons & (1 << i)) {\n                bmask |= buttonMaskMap[i];\n            }\n        }\n        return bmask;\n    }\n\n    _handleMouse(ev) {\n        /*\n         * We don't check connection status or viewOnly here as the\n         * mouse events might be used to control the viewport\n         */\n\n        if (ev.type === 'click') {\n            /*\n             * Note: This is only needed for the 'click' event as it fails\n             *       to fire properly for the target element so we have\n             *       to listen on the document element instead.\n             */\n            if (ev.target !== this._canvas) {\n                return;\n            }\n        }\n\n        // FIXME: if we're in view-only and not dragging,\n        //        should we stop events?\n        ev.stopPropagation();\n        ev.preventDefault();\n\n        if ((ev.type === 'click') || (ev.type === 'contextmenu')) {\n            return;\n        }\n\n        let pos = clientToElement(ev.clientX, ev.clientY,\n                                  this._canvas);\n\n        let bmask = RFB._convertButtonMask(ev.buttons);\n\n        let down = ev.type == 'mousedown';\n        switch (ev.type) {\n            case 'mousedown':\n            case 'mouseup':\n                if (this.dragViewport) {\n                    if (down && !this._viewportDragging) {\n                        this._viewportDragging = true;\n                        this._viewportDragPos = {'x': pos.x, 'y': pos.y};\n                        this._viewportHasMoved = false;\n\n                        this._flushMouseMoveTimer(pos.x, pos.y);\n\n                        // Skip sending mouse events, instead save the current\n                        // mouse mask so we can send it later.\n                        this._mouseButtonMask = bmask;\n                        break;\n                    } else {\n                        this._viewportDragging = false;\n\n                        // If we actually performed a drag then we are done\n                        // here and should not send any mouse events\n                        if (this._viewportHasMoved) {\n                            this._mouseButtonMask = bmask;\n                            break;\n                        }\n                        // Otherwise we treat this as a mouse click event.\n                        // Send the previously saved button mask, followed\n                        // by the current button mask at the end of this\n                        // function.\n                        this._sendMouse(pos.x, pos.y,  this._mouseButtonMask);\n                    }\n                }\n                if (down) {\n                    setCapture(this._canvas);\n                }\n                this._handleMouseButton(pos.x, pos.y, bmask);\n                break;\n            case 'mousemove':\n                if (this._viewportDragging) {\n                    const deltaX = this._viewportDragPos.x - pos.x;\n                    const deltaY = this._viewportDragPos.y - pos.y;\n\n                    if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||\n                                                   Math.abs(deltaY) > dragThreshold)) {\n                        this._viewportHasMoved = true;\n\n                        this._viewportDragPos = {'x': pos.x, 'y': pos.y};\n                        this._display.viewportChangePos(deltaX, deltaY);\n                    }\n\n                    // Skip sending mouse events\n                    break;\n                }\n                this._handleMouseMove(pos.x, pos.y);\n                break;\n        }\n    }\n\n    _handleMouseButton(x, y, bmask) {\n        // Flush waiting move event first\n        this._flushMouseMoveTimer(x, y);\n\n        this._mouseButtonMask = bmask;\n        this._sendMouse(x, y, this._mouseButtonMask);\n    }\n\n    _handleMouseMove(x, y) {\n        this._mousePos = { 'x': x, 'y': y };\n\n        // Limit many mouse move events to one every MOUSE_MOVE_DELAY ms\n        if (this._mouseMoveTimer == null) {\n\n            const timeSinceLastMove = Date.now() - this._mouseLastMoveTime;\n            if (timeSinceLastMove > MOUSE_MOVE_DELAY) {\n                this._sendMouse(x, y, this._mouseButtonMask);\n                this._mouseLastMoveTime = Date.now();\n            } else {\n                // Too soon since the latest move, wait the remaining time\n                this._mouseMoveTimer = setTimeout(() => {\n                    this._handleDelayedMouseMove();\n                }, MOUSE_MOVE_DELAY - timeSinceLastMove);\n            }\n        }\n    }\n\n    _handleDelayedMouseMove() {\n        this._mouseMoveTimer = null;\n        this._sendMouse(this._mousePos.x, this._mousePos.y,\n                        this._mouseButtonMask);\n        this._mouseLastMoveTime = Date.now();\n    }\n\n    _sendMouse(x, y, mask) {\n        if (this._rfbConnectionState !== 'connected') { return; }\n        if (this._viewOnly) { return; } // View only, skip mouse events\n\n        // Highest bit in mask is never sent to the server\n        if (mask & 0x8000) {\n            throw new Error(\"Illegal mouse button mask (mask: \" + mask + \")\");\n        }\n\n        let extendedMouseButtons = mask & 0x7f80;\n\n        if (this._extendedPointerEventSupported && extendedMouseButtons) {\n            RFB.messages.extendedPointerEvent(this._sock, this._display.absX(x),\n                                              this._display.absY(y), mask);\n        } else {\n            RFB.messages.pointerEvent(this._sock, this._display.absX(x),\n                                      this._display.absY(y), mask);\n        }\n    }\n\n    _handleWheel(ev) {\n        if (this._rfbConnectionState !== 'connected') { return; }\n        if (this._viewOnly) { return; } // View only, skip mouse events\n\n        ev.stopPropagation();\n        ev.preventDefault();\n\n        let pos = clientToElement(ev.clientX, ev.clientY,\n                                  this._canvas);\n\n        let bmask = RFB._convertButtonMask(ev.buttons);\n        let dX = ev.deltaX;\n        let dY = ev.deltaY;\n\n        // Pixel units unless it's non-zero.\n        // Note that if deltamode is line or page won't matter since we aren't\n        // sending the mouse wheel delta to the server anyway.\n        // The difference between pixel and line can be important however since\n        // we have a threshold that can be smaller than the line height.\n        if (ev.deltaMode !== 0) {\n            dX *= WHEEL_LINE_HEIGHT;\n            dY *= WHEEL_LINE_HEIGHT;\n        }\n\n        // Mouse wheel events are sent in steps over VNC. This means that the VNC\n        // protocol can't handle a wheel event with specific distance or speed.\n        // Therefor, if we get a lot of small mouse wheel events we combine them.\n        this._accumulatedWheelDeltaX += dX;\n        this._accumulatedWheelDeltaY += dY;\n\n\n        // Generate a mouse wheel step event when the accumulated delta\n        // for one of the axes is large enough.\n        if (Math.abs(this._accumulatedWheelDeltaX) >= WHEEL_STEP) {\n            if (this._accumulatedWheelDeltaX < 0) {\n                this._handleMouseButton(pos.x, pos.y, bmask | 1 << 5);\n                this._handleMouseButton(pos.x, pos.y, bmask);\n            } else if (this._accumulatedWheelDeltaX > 0) {\n                this._handleMouseButton(pos.x, pos.y, bmask | 1 << 6);\n                this._handleMouseButton(pos.x, pos.y, bmask);\n            }\n\n            this._accumulatedWheelDeltaX = 0;\n        }\n        if (Math.abs(this._accumulatedWheelDeltaY) >= WHEEL_STEP) {\n            if (this._accumulatedWheelDeltaY < 0) {\n                this._handleMouseButton(pos.x, pos.y, bmask | 1 << 3);\n                this._handleMouseButton(pos.x, pos.y, bmask);\n            } else if (this._accumulatedWheelDeltaY > 0) {\n                this._handleMouseButton(pos.x, pos.y, bmask | 1 << 4);\n                this._handleMouseButton(pos.x, pos.y, bmask);\n            }\n\n            this._accumulatedWheelDeltaY = 0;\n        }\n    }\n\n    _fakeMouseMove(ev, elementX, elementY) {\n        this._handleMouseMove(elementX, elementY);\n        this._cursor.move(ev.detail.clientX, ev.detail.clientY);\n    }\n\n    _handleTapEvent(ev, bmask) {\n        let pos = clientToElement(ev.detail.clientX, ev.detail.clientY,\n                                  this._canvas);\n\n        // If the user quickly taps multiple times we assume they meant to\n        // hit the same spot, so slightly adjust coordinates\n\n        if ((this._gestureLastTapTime !== null) &&\n            ((Date.now() - this._gestureLastTapTime) < DOUBLE_TAP_TIMEOUT) &&\n            (this._gestureFirstDoubleTapEv.detail.type === ev.detail.type)) {\n            let dx = this._gestureFirstDoubleTapEv.detail.clientX - ev.detail.clientX;\n            let dy = this._gestureFirstDoubleTapEv.detail.clientY - ev.detail.clientY;\n            let distance = Math.hypot(dx, dy);\n\n            if (distance < DOUBLE_TAP_THRESHOLD) {\n                pos = clientToElement(this._gestureFirstDoubleTapEv.detail.clientX,\n                                      this._gestureFirstDoubleTapEv.detail.clientY,\n                                      this._canvas);\n            } else {\n                this._gestureFirstDoubleTapEv = ev;\n            }\n        } else {\n            this._gestureFirstDoubleTapEv = ev;\n        }\n        this._gestureLastTapTime = Date.now();\n\n        this._fakeMouseMove(this._gestureFirstDoubleTapEv, pos.x, pos.y);\n        this._handleMouseButton(pos.x, pos.y, bmask);\n        this._handleMouseButton(pos.x, pos.y, 0x0);\n    }\n\n    _handleGesture(ev) {\n        let magnitude;\n\n        let pos = clientToElement(ev.detail.clientX, ev.detail.clientY,\n                                  this._canvas);\n        switch (ev.type) {\n            case 'gesturestart':\n                switch (ev.detail.type) {\n                    case 'onetap':\n                        this._handleTapEvent(ev, 0x1);\n                        break;\n                    case 'twotap':\n                        this._handleTapEvent(ev, 0x4);\n                        break;\n                    case 'threetap':\n                        this._handleTapEvent(ev, 0x2);\n                        break;\n                    case 'drag':\n                        if (this.dragViewport) {\n                            this._viewportHasMoved = false;\n                            this._viewportDragging = true;\n                            this._viewportDragPos = {'x': pos.x, 'y': pos.y};\n                        } else {\n                            this._fakeMouseMove(ev, pos.x, pos.y);\n                            this._handleMouseButton(pos.x, pos.y, 0x1);\n                        }\n                        break;\n                    case 'longpress':\n                        if (this.dragViewport) {\n                            // If dragViewport is true, we need to wait to see\n                            // if we have dragged outside the threshold before\n                            // sending any events to the server.\n                            this._viewportHasMoved = false;\n                            this._viewportDragPos = {'x': pos.x, 'y': pos.y};\n                        } else {\n                            this._fakeMouseMove(ev, pos.x, pos.y);\n                            this._handleMouseButton(pos.x, pos.y, 0x4);\n                        }\n                        break;\n                    case 'twodrag':\n                        this._gestureLastMagnitudeX = ev.detail.magnitudeX;\n                        this._gestureLastMagnitudeY = ev.detail.magnitudeY;\n                        this._fakeMouseMove(ev, pos.x, pos.y);\n                        break;\n                    case 'pinch':\n                        this._gestureLastMagnitudeX = Math.hypot(ev.detail.magnitudeX,\n                                                                 ev.detail.magnitudeY);\n                        this._fakeMouseMove(ev, pos.x, pos.y);\n                        break;\n                }\n                break;\n\n            case 'gesturemove':\n                switch (ev.detail.type) {\n                    case 'onetap':\n                    case 'twotap':\n                    case 'threetap':\n                        break;\n                    case 'drag':\n                    case 'longpress':\n                        if (this.dragViewport) {\n                            this._viewportDragging = true;\n                            const deltaX = this._viewportDragPos.x - pos.x;\n                            const deltaY = this._viewportDragPos.y - pos.y;\n\n                            if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||\n                                                           Math.abs(deltaY) > dragThreshold)) {\n                                this._viewportHasMoved = true;\n\n                                this._viewportDragPos = {'x': pos.x, 'y': pos.y};\n                                this._display.viewportChangePos(deltaX, deltaY);\n                            }\n                        } else {\n                            this._fakeMouseMove(ev, pos.x, pos.y);\n                        }\n                        break;\n                    case 'twodrag':\n                        // Always scroll in the same position.\n                        // We don't know if the mouse was moved so we need to move it\n                        // every update.\n                        this._fakeMouseMove(ev, pos.x, pos.y);\n                        while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) > GESTURE_SCRLSENS) {\n                            this._handleMouseButton(pos.x, pos.y, 0x8);\n                            this._handleMouseButton(pos.x, pos.y, 0x0);\n                            this._gestureLastMagnitudeY += GESTURE_SCRLSENS;\n                        }\n                        while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) < -GESTURE_SCRLSENS) {\n                            this._handleMouseButton(pos.x, pos.y, 0x10);\n                            this._handleMouseButton(pos.x, pos.y, 0x0);\n                            this._gestureLastMagnitudeY -= GESTURE_SCRLSENS;\n                        }\n                        while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) > GESTURE_SCRLSENS) {\n                            this._handleMouseButton(pos.x, pos.y, 0x20);\n                            this._handleMouseButton(pos.x, pos.y, 0x0);\n                            this._gestureLastMagnitudeX += GESTURE_SCRLSENS;\n                        }\n                        while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) < -GESTURE_SCRLSENS) {\n                            this._handleMouseButton(pos.x, pos.y, 0x40);\n                            this._handleMouseButton(pos.x, pos.y, 0x0);\n                            this._gestureLastMagnitudeX -= GESTURE_SCRLSENS;\n                        }\n                        break;\n                    case 'pinch':\n                        // Always scroll in the same position.\n                        // We don't know if the mouse was moved so we need to move it\n                        // every update.\n                        this._fakeMouseMove(ev, pos.x, pos.y);\n                        magnitude = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY);\n                        if (Math.abs(magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {\n                            this._handleKeyEvent(KeyTable.XK_Control_L, \"ControlLeft\", true);\n                            while ((magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {\n                                this._handleMouseButton(pos.x, pos.y, 0x8);\n                                this._handleMouseButton(pos.x, pos.y, 0x0);\n                                this._gestureLastMagnitudeX += GESTURE_ZOOMSENS;\n                            }\n                            while ((magnitude -  this._gestureLastMagnitudeX) < -GESTURE_ZOOMSENS) {\n                                this._handleMouseButton(pos.x, pos.y, 0x10);\n                                this._handleMouseButton(pos.x, pos.y, 0x0);\n                                this._gestureLastMagnitudeX -= GESTURE_ZOOMSENS;\n                            }\n                        }\n                        this._handleKeyEvent(KeyTable.XK_Control_L, \"ControlLeft\", false);\n                        break;\n                }\n                break;\n\n            case 'gestureend':\n                switch (ev.detail.type) {\n                    case 'onetap':\n                    case 'twotap':\n                    case 'threetap':\n                    case 'pinch':\n                    case 'twodrag':\n                        break;\n                    case 'drag':\n                        if (this.dragViewport) {\n                            this._viewportDragging = false;\n                        } else {\n                            this._fakeMouseMove(ev, pos.x, pos.y);\n                            this._handleMouseButton(pos.x, pos.y, 0x0);\n                        }\n                        break;\n                    case 'longpress':\n                        if (this._viewportHasMoved) {\n                            // We don't want to send any events if we have moved\n                            // our viewport\n                            break;\n                        }\n\n                        if (this.dragViewport && !this._viewportHasMoved) {\n                            this._fakeMouseMove(ev, pos.x, pos.y);\n                            // If dragViewport is true, we need to wait to see\n                            // if we have dragged outside the threshold before\n                            // sending any events to the server.\n                            this._handleMouseButton(pos.x, pos.y, 0x4);\n                            this._handleMouseButton(pos.x, pos.y, 0x0);\n                            this._viewportDragging = false;\n                        } else {\n                            this._fakeMouseMove(ev, pos.x, pos.y);\n                            this._handleMouseButton(pos.x, pos.y, 0x0);\n                        }\n                        break;\n                }\n                break;\n        }\n    }\n\n    _flushMouseMoveTimer(x, y) {\n        if (this._mouseMoveTimer !== null) {\n            clearTimeout(this._mouseMoveTimer);\n            this._mouseMoveTimer = null;\n            this._sendMouse(x, y, this._mouseButtonMask);\n        }\n    }\n\n    // Message handlers\n\n    _negotiateProtocolVersion() {\n        if (this._sock.rQwait(\"version\", 12)) {\n            return false;\n        }\n\n        const sversion = this._sock.rQshiftStr(12).substr(4, 7);\n        Log.Info(\"Server ProtocolVersion: \" + sversion);\n        let isRepeater = 0;\n        switch (sversion) {\n            case \"000.000\":  // UltraVNC repeater\n                isRepeater = 1;\n                break;\n            case \"003.003\":\n            case \"003.006\":  // UltraVNC\n                this._rfbVersion = 3.3;\n                break;\n            case \"003.007\":\n                this._rfbVersion = 3.7;\n                break;\n            case \"003.008\":\n            case \"003.889\":  // Apple Remote Desktop\n            case \"004.000\":  // Intel AMT KVM\n            case \"004.001\":  // RealVNC 4.6\n            case \"005.000\":  // RealVNC 5.3\n                this._rfbVersion = 3.8;\n                break;\n            default:\n                return this._fail(\"Invalid server version \" + sversion);\n        }\n\n        if (isRepeater) {\n            let repeaterID = \"ID:\" + this._repeaterID;\n            while (repeaterID.length < 250) {\n                repeaterID += \"\\0\";\n            }\n            this._sock.sQpushString(repeaterID);\n            this._sock.flush();\n            return true;\n        }\n\n        if (this._rfbVersion > this._rfbMaxVersion) {\n            this._rfbVersion = this._rfbMaxVersion;\n        }\n\n        const cversion = \"00\" + parseInt(this._rfbVersion, 10) +\n                       \".00\" + ((this._rfbVersion * 10) % 10);\n        this._sock.sQpushString(\"RFB \" + cversion + \"\\n\");\n        this._sock.flush();\n        Log.Debug('Sent ProtocolVersion: ' + cversion);\n\n        this._rfbInitState = 'Security';\n    }\n\n    _isSupportedSecurityType(type) {\n        const clientTypes = [\n            securityTypeNone,\n            securityTypeVNCAuth,\n            securityTypeRA2ne,\n            securityTypeTight,\n            securityTypeVeNCrypt,\n            securityTypeXVP,\n            securityTypeARD,\n            securityTypeMSLogonII,\n            securityTypePlain,\n        ];\n\n        return clientTypes.includes(type);\n    }\n\n    _negotiateSecurity() {\n        if (this._rfbVersion >= 3.7) {\n            // Server sends supported list, client decides\n            const numTypes = this._sock.rQshift8();\n            if (this._sock.rQwait(\"security type\", numTypes, 1)) { return false; }\n\n            if (numTypes === 0) {\n                this._rfbInitState = \"SecurityReason\";\n                this._securityContext = \"no security types\";\n                this._securityStatus = 1;\n                return true;\n            }\n\n            const types = this._sock.rQshiftBytes(numTypes);\n            Log.Debug(\"Server security types: \" + types);\n\n            // Look for a matching security type in the order that the\n            // server prefers\n            this._rfbAuthScheme = -1;\n            for (let type of types) {\n                if (this._isSupportedSecurityType(type)) {\n                    this._rfbAuthScheme = type;\n                    break;\n                }\n            }\n\n            if (this._rfbAuthScheme === -1) {\n                return this._fail(\"Unsupported security types (types: \" + types + \")\");\n            }\n\n            this._sock.sQpush8(this._rfbAuthScheme);\n            this._sock.flush();\n        } else {\n            // Server decides\n            if (this._sock.rQwait(\"security scheme\", 4)) { return false; }\n            this._rfbAuthScheme = this._sock.rQshift32();\n\n            if (this._rfbAuthScheme == 0) {\n                this._rfbInitState = \"SecurityReason\";\n                this._securityContext = \"authentication scheme\";\n                this._securityStatus = 1;\n                return true;\n            }\n        }\n\n        this._rfbInitState = 'Authentication';\n        Log.Debug('Authenticating using scheme: ' + this._rfbAuthScheme);\n\n        return true;\n    }\n\n    _handleSecurityReason() {\n        if (this._sock.rQwait(\"reason length\", 4)) {\n            return false;\n        }\n        const strlen = this._sock.rQshift32();\n        let reason = \"\";\n\n        if (strlen > 0) {\n            if (this._sock.rQwait(\"reason\", strlen, 4)) { return false; }\n            reason = this._sock.rQshiftStr(strlen);\n        }\n\n        if (reason !== \"\") {\n            this.dispatchEvent(new CustomEvent(\n                \"securityfailure\",\n                { detail: { status: this._securityStatus,\n                            reason: reason } }));\n\n            return this._fail(\"Security negotiation failed on \" +\n                              this._securityContext +\n                              \" (reason: \" + reason + \")\");\n        } else {\n            this.dispatchEvent(new CustomEvent(\n                \"securityfailure\",\n                { detail: { status: this._securityStatus } }));\n\n            return this._fail(\"Security negotiation failed on \" +\n                              this._securityContext);\n        }\n    }\n\n    // authentication\n    _negotiateXvpAuth() {\n        if (this._rfbCredentials.username === undefined ||\n            this._rfbCredentials.password === undefined ||\n            this._rfbCredentials.target === undefined) {\n            this.dispatchEvent(new CustomEvent(\n                \"credentialsrequired\",\n                { detail: { types: [\"username\", \"password\", \"target\"] } }));\n            return false;\n        }\n\n        this._sock.sQpush8(this._rfbCredentials.username.length);\n        this._sock.sQpush8(this._rfbCredentials.target.length);\n        this._sock.sQpushString(this._rfbCredentials.username);\n        this._sock.sQpushString(this._rfbCredentials.target);\n\n        this._sock.flush();\n\n        this._rfbAuthScheme = securityTypeVNCAuth;\n\n        return this._negotiateAuthentication();\n    }\n\n    // VeNCrypt authentication, currently only supports version 0.2 and only Plain subtype\n    _negotiateVeNCryptAuth() {\n\n        // waiting for VeNCrypt version\n        if (this._rfbVeNCryptState == 0) {\n            if (this._sock.rQwait(\"vencrypt version\", 2)) { return false; }\n\n            const major = this._sock.rQshift8();\n            const minor = this._sock.rQshift8();\n\n            if (!(major == 0 && minor == 2)) {\n                return this._fail(\"Unsupported VeNCrypt version \" + major + \".\" + minor);\n            }\n\n            this._sock.sQpush8(0);\n            this._sock.sQpush8(2);\n            this._sock.flush();\n            this._rfbVeNCryptState = 1;\n        }\n\n        // waiting for ACK\n        if (this._rfbVeNCryptState == 1) {\n            if (this._sock.rQwait(\"vencrypt ack\", 1)) { return false; }\n\n            const res = this._sock.rQshift8();\n\n            if (res != 0) {\n                return this._fail(\"VeNCrypt failure \" + res);\n            }\n\n            this._rfbVeNCryptState = 2;\n        }\n        // must fall through here (i.e. no \"else if\"), beacause we may have already received\n        // the subtypes length and won't be called again\n\n        if (this._rfbVeNCryptState == 2) { // waiting for subtypes length\n            if (this._sock.rQwait(\"vencrypt subtypes length\", 1)) { return false; }\n\n            const subtypesLength = this._sock.rQshift8();\n            if (subtypesLength < 1) {\n                return this._fail(\"VeNCrypt subtypes empty\");\n            }\n\n            this._rfbVeNCryptSubtypesLength = subtypesLength;\n            this._rfbVeNCryptState = 3;\n        }\n\n        // waiting for subtypes list\n        if (this._rfbVeNCryptState == 3) {\n            if (this._sock.rQwait(\"vencrypt subtypes\", 4 * this._rfbVeNCryptSubtypesLength)) { return false; }\n\n            const subtypes = [];\n            for (let i = 0; i < this._rfbVeNCryptSubtypesLength; i++) {\n                subtypes.push(this._sock.rQshift32());\n            }\n\n            // Look for a matching security type in the order that the\n            // server prefers\n            this._rfbAuthScheme = -1;\n            for (let type of subtypes) {\n                // Avoid getting in to a loop\n                if (type === securityTypeVeNCrypt) {\n                    continue;\n                }\n\n                if (this._isSupportedSecurityType(type)) {\n                    this._rfbAuthScheme = type;\n                    break;\n                }\n            }\n\n            if (this._rfbAuthScheme === -1) {\n                return this._fail(\"Unsupported security types (types: \" + subtypes + \")\");\n            }\n\n            this._sock.sQpush32(this._rfbAuthScheme);\n            this._sock.flush();\n\n            this._rfbVeNCryptState = 4;\n            return true;\n        }\n    }\n\n    _negotiatePlainAuth() {\n        if (this._rfbCredentials.username === undefined ||\n            this._rfbCredentials.password === undefined) {\n            this.dispatchEvent(new CustomEvent(\n                \"credentialsrequired\",\n                { detail: { types: [\"username\", \"password\"] } }));\n            return false;\n        }\n\n        const user = encodeUTF8(this._rfbCredentials.username);\n        const pass = encodeUTF8(this._rfbCredentials.password);\n\n        this._sock.sQpush32(user.length);\n        this._sock.sQpush32(pass.length);\n        this._sock.sQpushString(user);\n        this._sock.sQpushString(pass);\n        this._sock.flush();\n\n        this._rfbInitState = \"SecurityResult\";\n        return true;\n    }\n\n    _negotiateStdVNCAuth() {\n        if (this._sock.rQwait(\"auth challenge\", 16)) { return false; }\n\n        if (this._rfbCredentials.password === undefined) {\n            this.dispatchEvent(new CustomEvent(\n                \"credentialsrequired\",\n                { detail: { types: [\"password\"] } }));\n            return false;\n        }\n\n        // TODO(directxman12): make genDES not require an Array\n        const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));\n        const response = RFB.genDES(this._rfbCredentials.password, challenge);\n        this._sock.sQpushBytes(response);\n        this._sock.flush();\n        this._rfbInitState = \"SecurityResult\";\n        return true;\n    }\n\n    _negotiateARDAuth() {\n\n        if (this._rfbCredentials.username === undefined ||\n            this._rfbCredentials.password === undefined) {\n            this.dispatchEvent(new CustomEvent(\n                \"credentialsrequired\",\n                { detail: { types: [\"username\", \"password\"] } }));\n            return false;\n        }\n\n        if (this._rfbCredentials.ardPublicKey != undefined &&\n            this._rfbCredentials.ardCredentials != undefined) {\n            // if the async web crypto is done return the results\n            this._sock.sQpushBytes(this._rfbCredentials.ardCredentials);\n            this._sock.sQpushBytes(this._rfbCredentials.ardPublicKey);\n            this._sock.flush();\n            this._rfbCredentials.ardCredentials = null;\n            this._rfbCredentials.ardPublicKey = null;\n            this._rfbInitState = \"SecurityResult\";\n            return true;\n        }\n\n        if (this._sock.rQwait(\"read ard\", 4)) { return false; }\n\n        let generator = this._sock.rQshiftBytes(2);   // DH base generator value\n\n        let keyLength = this._sock.rQshift16();\n\n        if (this._sock.rQwait(\"read ard keylength\", keyLength*2, 4)) { return false; }\n\n        // read the server values\n        let prime = this._sock.rQshiftBytes(keyLength);  // predetermined prime modulus\n        let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key\n\n        let clientKey = legacyCrypto.generateKey(\n            { name: \"DH\", g: generator, p: prime }, false, [\"deriveBits\"]);\n        this._negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey);\n\n        return false;\n    }\n\n    async _negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey) {\n        const clientPublicKey = legacyCrypto.exportKey(\"raw\", clientKey.publicKey);\n        const sharedKey = legacyCrypto.deriveBits(\n            { name: \"DH\", public: serverPublicKey }, clientKey.privateKey, keyLength * 8);\n\n        const username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);\n        const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);\n\n        const credentials = window.crypto.getRandomValues(new Uint8Array(128));\n        for (let i = 0; i < username.length; i++) {\n            credentials[i] = username.charCodeAt(i);\n        }\n        credentials[username.length] = 0;\n        for (let i = 0; i < password.length; i++) {\n            credentials[64 + i] = password.charCodeAt(i);\n        }\n        credentials[64 + password.length] = 0;\n\n        const key = await legacyCrypto.digest(\"MD5\", sharedKey);\n        const cipher = await legacyCrypto.importKey(\n            \"raw\", key, { name: \"AES-ECB\" }, false, [\"encrypt\"]);\n        const encrypted = await legacyCrypto.encrypt({ name: \"AES-ECB\" }, cipher, credentials);\n\n        this._rfbCredentials.ardCredentials = encrypted;\n        this._rfbCredentials.ardPublicKey = clientPublicKey;\n\n        this._resumeAuthentication();\n    }\n\n    _negotiateTightUnixAuth() {\n        if (this._rfbCredentials.username === undefined ||\n            this._rfbCredentials.password === undefined) {\n            this.dispatchEvent(new CustomEvent(\n                \"credentialsrequired\",\n                { detail: { types: [\"username\", \"password\"] } }));\n            return false;\n        }\n\n        this._sock.sQpush32(this._rfbCredentials.username.length);\n        this._sock.sQpush32(this._rfbCredentials.password.length);\n        this._sock.sQpushString(this._rfbCredentials.username);\n        this._sock.sQpushString(this._rfbCredentials.password);\n        this._sock.flush();\n\n        this._rfbInitState = \"SecurityResult\";\n        return true;\n    }\n\n    _negotiateTightTunnels(numTunnels) {\n        const clientSupportedTunnelTypes = {\n            0: { vendor: 'TGHT', signature: 'NOTUNNEL' }\n        };\n        const serverSupportedTunnelTypes = {};\n        // receive tunnel capabilities\n        for (let i = 0; i < numTunnels; i++) {\n            const capCode = this._sock.rQshift32();\n            const capVendor = this._sock.rQshiftStr(4);\n            const capSignature = this._sock.rQshiftStr(8);\n            serverSupportedTunnelTypes[capCode] = { vendor: capVendor, signature: capSignature };\n        }\n\n        Log.Debug(\"Server Tight tunnel types: \" + serverSupportedTunnelTypes);\n\n        // Siemens touch panels have a VNC server that supports NOTUNNEL,\n        // but forgets to advertise it. Try to detect such servers by\n        // looking for their custom tunnel type.\n        if (serverSupportedTunnelTypes[1] &&\n            (serverSupportedTunnelTypes[1].vendor === \"SICR\") &&\n            (serverSupportedTunnelTypes[1].signature === \"SCHANNEL\")) {\n            Log.Debug(\"Detected Siemens server. Assuming NOTUNNEL support.\");\n            serverSupportedTunnelTypes[0] = { vendor: 'TGHT', signature: 'NOTUNNEL' };\n        }\n\n        // choose the notunnel type\n        if (serverSupportedTunnelTypes[0]) {\n            if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor ||\n                serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) {\n                return this._fail(\"Client's tunnel type had the incorrect \" +\n                                  \"vendor or signature\");\n            }\n            Log.Debug(\"Selected tunnel type: \" + clientSupportedTunnelTypes[0]);\n            this._sock.sQpush32(0); // use NOTUNNEL\n            this._sock.flush();\n            return false; // wait until we receive the sub auth count to continue\n        } else {\n            return this._fail(\"Server wanted tunnels, but doesn't support \" +\n                              \"the notunnel type\");\n        }\n    }\n\n    _negotiateTightAuth() {\n        if (!this._rfbTightVNC) {  // first pass, do the tunnel negotiation\n            if (this._sock.rQwait(\"num tunnels\", 4)) { return false; }\n            const numTunnels = this._sock.rQshift32();\n            if (numTunnels > 0 && this._sock.rQwait(\"tunnel capabilities\", 16 * numTunnels, 4)) { return false; }\n\n            this._rfbTightVNC = true;\n\n            if (numTunnels > 0) {\n                this._negotiateTightTunnels(numTunnels);\n                return false;  // wait until we receive the sub auth to continue\n            }\n        }\n\n        // second pass, do the sub-auth negotiation\n        if (this._sock.rQwait(\"sub auth count\", 4)) { return false; }\n        const subAuthCount = this._sock.rQshift32();\n        if (subAuthCount === 0) {  // empty sub-auth list received means 'no auth' subtype selected\n            this._rfbInitState = 'SecurityResult';\n            return true;\n        }\n\n        if (this._sock.rQwait(\"sub auth capabilities\", 16 * subAuthCount, 4)) { return false; }\n\n        const clientSupportedTypes = {\n            'STDVNOAUTH__': 1,\n            'STDVVNCAUTH_': 2,\n            'TGHTULGNAUTH': 129\n        };\n\n        const serverSupportedTypes = [];\n\n        for (let i = 0; i < subAuthCount; i++) {\n            this._sock.rQshift32(); // capNum\n            const capabilities = this._sock.rQshiftStr(12);\n            serverSupportedTypes.push(capabilities);\n        }\n\n        Log.Debug(\"Server Tight authentication types: \" + serverSupportedTypes);\n\n        for (let authType in clientSupportedTypes) {\n            if (serverSupportedTypes.indexOf(authType) != -1) {\n                this._sock.sQpush32(clientSupportedTypes[authType]);\n                this._sock.flush();\n                Log.Debug(\"Selected authentication type: \" + authType);\n\n                switch (authType) {\n                    case 'STDVNOAUTH__':  // no auth\n                        this._rfbInitState = 'SecurityResult';\n                        return true;\n                    case 'STDVVNCAUTH_':\n                        this._rfbAuthScheme = securityTypeVNCAuth;\n                        return true;\n                    case 'TGHTULGNAUTH':\n                        this._rfbAuthScheme = securityTypeUnixLogon;\n                        return true;\n                    default:\n                        return this._fail(\"Unsupported tiny auth scheme \" +\n                                          \"(scheme: \" + authType + \")\");\n                }\n            }\n        }\n\n        return this._fail(\"No supported sub-auth types!\");\n    }\n\n    _handleRSAAESCredentialsRequired(event) {\n        this.dispatchEvent(event);\n    }\n\n    _handleRSAAESServerVerification(event) {\n        this.dispatchEvent(event);\n    }\n\n    _negotiateRA2neAuth() {\n        if (this._rfbRSAAESAuthenticationState === null) {\n            this._rfbRSAAESAuthenticationState = new RSAAESAuthenticationState(this._sock, () => this._rfbCredentials);\n            this._rfbRSAAESAuthenticationState.addEventListener(\n                \"serververification\", this._eventHandlers.handleRSAAESServerVerification);\n            this._rfbRSAAESAuthenticationState.addEventListener(\n                \"credentialsrequired\", this._eventHandlers.handleRSAAESCredentialsRequired);\n        }\n        this._rfbRSAAESAuthenticationState.checkInternalEvents();\n        if (!this._rfbRSAAESAuthenticationState.hasStarted) {\n            this._rfbRSAAESAuthenticationState.negotiateRA2neAuthAsync()\n                .catch((e) => {\n                    if (e.message !== \"disconnect normally\") {\n                        this._fail(e.message);\n                    }\n                })\n                .then(() => {\n                    this._rfbInitState = \"SecurityResult\";\n                    return true;\n                }).finally(() => {\n                    this._rfbRSAAESAuthenticationState.removeEventListener(\n                        \"serververification\", this._eventHandlers.handleRSAAESServerVerification);\n                    this._rfbRSAAESAuthenticationState.removeEventListener(\n                        \"credentialsrequired\", this._eventHandlers.handleRSAAESCredentialsRequired);\n                    this._rfbRSAAESAuthenticationState = null;\n                });\n        }\n        return false;\n    }\n\n    _negotiateMSLogonIIAuth() {\n        if (this._sock.rQwait(\"mslogonii dh param\", 24)) { return false; }\n\n        if (this._rfbCredentials.username === undefined ||\n            this._rfbCredentials.password === undefined) {\n            this.dispatchEvent(new CustomEvent(\n                \"credentialsrequired\",\n                { detail: { types: [\"username\", \"password\"] } }));\n            return false;\n        }\n\n        const g = this._sock.rQshiftBytes(8);\n        const p = this._sock.rQshiftBytes(8);\n        const A = this._sock.rQshiftBytes(8);\n        const dhKey = legacyCrypto.generateKey({ name: \"DH\", g: g, p: p }, true, [\"deriveBits\"]);\n        const B = legacyCrypto.exportKey(\"raw\", dhKey.publicKey);\n        const secret = legacyCrypto.deriveBits({ name: \"DH\", public: A }, dhKey.privateKey, 64);\n\n        const key = legacyCrypto.importKey(\"raw\", secret, { name: \"DES-CBC\" }, false, [\"encrypt\"]);\n        const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255);\n        const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);\n        let usernameBytes = new Uint8Array(256);\n        let passwordBytes = new Uint8Array(64);\n        window.crypto.getRandomValues(usernameBytes);\n        window.crypto.getRandomValues(passwordBytes);\n        for (let i = 0; i < username.length; i++) {\n            usernameBytes[i] = username.charCodeAt(i);\n        }\n        usernameBytes[username.length] = 0;\n        for (let i = 0; i < password.length; i++) {\n            passwordBytes[i] = password.charCodeAt(i);\n        }\n        passwordBytes[password.length] = 0;\n        usernameBytes = legacyCrypto.encrypt({ name: \"DES-CBC\", iv: secret }, key, usernameBytes);\n        passwordBytes = legacyCrypto.encrypt({ name: \"DES-CBC\", iv: secret }, key, passwordBytes);\n        this._sock.sQpushBytes(B);\n        this._sock.sQpushBytes(usernameBytes);\n        this._sock.sQpushBytes(passwordBytes);\n        this._sock.flush();\n        this._rfbInitState = \"SecurityResult\";\n        return true;\n    }\n\n    _negotiateAuthentication() {\n        switch (this._rfbAuthScheme) {\n            case securityTypeNone:\n                if (this._rfbVersion >= 3.8) {\n                    this._rfbInitState = 'SecurityResult';\n                } else {\n                    this._rfbInitState = 'ClientInitialisation';\n                }\n                return true;\n\n            case securityTypeXVP:\n                return this._negotiateXvpAuth();\n\n            case securityTypeARD:\n                return this._negotiateARDAuth();\n\n            case securityTypeVNCAuth:\n                return this._negotiateStdVNCAuth();\n\n            case securityTypeTight:\n                return this._negotiateTightAuth();\n\n            case securityTypeVeNCrypt:\n                return this._negotiateVeNCryptAuth();\n\n            case securityTypePlain:\n                return this._negotiatePlainAuth();\n\n            case securityTypeUnixLogon:\n                return this._negotiateTightUnixAuth();\n\n            case securityTypeRA2ne:\n                return this._negotiateRA2neAuth();\n\n            case securityTypeMSLogonII:\n                return this._negotiateMSLogonIIAuth();\n\n            default:\n                return this._fail(\"Unsupported auth scheme (scheme: \" +\n                                  this._rfbAuthScheme + \")\");\n        }\n    }\n\n    _handleSecurityResult() {\n        if (this._sock.rQwait('VNC auth response ', 4)) { return false; }\n\n        const status = this._sock.rQshift32();\n\n        if (status === 0) { // OK\n            this._rfbInitState = 'ClientInitialisation';\n            Log.Debug('Authentication OK');\n            return true;\n        } else {\n            if (this._rfbVersion >= 3.8) {\n                this._rfbInitState = \"SecurityReason\";\n                this._securityContext = \"security result\";\n                this._securityStatus = status;\n                return true;\n            } else {\n                this.dispatchEvent(new CustomEvent(\n                    \"securityfailure\",\n                    { detail: { status: status } }));\n\n                return this._fail(\"Security handshake failed\");\n            }\n        }\n    }\n\n    _negotiateServerInit() {\n        if (this._sock.rQwait(\"server initialization\", 24)) { return false; }\n\n        /* Screen size */\n        const width = this._sock.rQshift16();\n        const height = this._sock.rQshift16();\n\n        /* PIXEL_FORMAT */\n        const bpp         = this._sock.rQshift8();\n        const depth       = this._sock.rQshift8();\n        const bigEndian  = this._sock.rQshift8();\n        const trueColor  = this._sock.rQshift8();\n\n        const redMax     = this._sock.rQshift16();\n        const greenMax   = this._sock.rQshift16();\n        const blueMax    = this._sock.rQshift16();\n        const redShift   = this._sock.rQshift8();\n        const greenShift = this._sock.rQshift8();\n        const blueShift  = this._sock.rQshift8();\n        this._sock.rQskipBytes(3);  // padding\n\n        // NB(directxman12): we don't want to call any callbacks or print messages until\n        //                   *after* we're past the point where we could backtrack\n\n        /* Connection name/title */\n        const nameLength = this._sock.rQshift32();\n        if (this._sock.rQwait('server init name', nameLength, 24)) { return false; }\n        let name = this._sock.rQshiftStr(nameLength);\n        name = decodeUTF8(name, true);\n\n        if (this._rfbTightVNC) {\n            if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + nameLength)) { return false; }\n            // In TightVNC mode, ServerInit message is extended\n            const numServerMessages = this._sock.rQshift16();\n            const numClientMessages = this._sock.rQshift16();\n            const numEncodings = this._sock.rQshift16();\n            this._sock.rQskipBytes(2);  // padding\n\n            const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;\n            if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + nameLength)) { return false; }\n\n            // we don't actually do anything with the capability information that TIGHT sends,\n            // so we just skip the all of this.\n\n            // TIGHT server message capabilities\n            this._sock.rQskipBytes(16 * numServerMessages);\n\n            // TIGHT client message capabilities\n            this._sock.rQskipBytes(16 * numClientMessages);\n\n            // TIGHT encoding capabilities\n            this._sock.rQskipBytes(16 * numEncodings);\n        }\n\n        // NB(directxman12): these are down here so that we don't run them multiple times\n        //                   if we backtrack\n        Log.Info(\"Screen: \" + width + \"x\" + height +\n                  \", bpp: \" + bpp + \", depth: \" + depth +\n                  \", bigEndian: \" + bigEndian +\n                  \", trueColor: \" + trueColor +\n                  \", redMax: \" + redMax +\n                  \", greenMax: \" + greenMax +\n                  \", blueMax: \" + blueMax +\n                  \", redShift: \" + redShift +\n                  \", greenShift: \" + greenShift +\n                  \", blueShift: \" + blueShift);\n\n        // we're past the point where we could backtrack, so it's safe to call this\n        this._setDesktopName(name);\n        this._resize(width, height);\n\n        if (!this._viewOnly) {\n            this._keyboard.grab();\n            this._asyncClipboard.grab();\n        }\n\n        this._fbDepth = 24;\n\n        if (this._fbName === \"Intel(r) AMT KVM\") {\n            Log.Warn(\"Intel AMT KVM only supports 8/16 bit depths. Using low color mode.\");\n            this._fbDepth = 8;\n        }\n\n        RFB.messages.pixelFormat(this._sock, this._fbDepth, true);\n        this._sendEncodings();\n        RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fbWidth, this._fbHeight);\n\n        this._updateConnectionState('connected');\n        return true;\n    }\n\n    _sendEncodings() {\n        const encs = [];\n\n        // In preference order\n        encs.push(encodings.encodingCopyRect);\n        // Only supported with full depth support\n        if (this._fbDepth == 24) {\n            if (supportsWebCodecsH264Decode) {\n                encs.push(encodings.encodingH264);\n            }\n            encs.push(encodings.encodingTight);\n            encs.push(encodings.encodingTightPNG);\n            encs.push(encodings.encodingZRLE);\n            encs.push(encodings.encodingJPEG);\n            encs.push(encodings.encodingHextile);\n            encs.push(encodings.encodingRRE);\n            encs.push(encodings.encodingZlib);\n        }\n        encs.push(encodings.encodingRaw);\n\n        // Psuedo-encoding settings\n        encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel);\n        encs.push(encodings.pseudoEncodingCompressLevel0 + this._compressionLevel);\n\n        encs.push(encodings.pseudoEncodingDesktopSize);\n        encs.push(encodings.pseudoEncodingLastRect);\n        encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);\n        encs.push(encodings.pseudoEncodingQEMULedEvent);\n        encs.push(encodings.pseudoEncodingExtendedDesktopSize);\n        encs.push(encodings.pseudoEncodingXvp);\n        encs.push(encodings.pseudoEncodingFence);\n        encs.push(encodings.pseudoEncodingContinuousUpdates);\n        encs.push(encodings.pseudoEncodingDesktopName);\n        encs.push(encodings.pseudoEncodingExtendedClipboard);\n        encs.push(encodings.pseudoEncodingExtendedMouseButtons);\n\n        if (this._fbDepth == 24) {\n            encs.push(encodings.pseudoEncodingVMwareCursor);\n            encs.push(encodings.pseudoEncodingCursor);\n        }\n\n        RFB.messages.clientEncodings(this._sock, encs);\n    }\n\n    /* RFB protocol initialization states:\n     *   ProtocolVersion\n     *   Security\n     *   Authentication\n     *   SecurityResult\n     *   ClientInitialization - not triggered by server message\n     *   ServerInitialization\n     */\n    _initMsg() {\n        switch (this._rfbInitState) {\n            case 'ProtocolVersion':\n                return this._negotiateProtocolVersion();\n\n            case 'Security':\n                return this._negotiateSecurity();\n\n            case 'Authentication':\n                return this._negotiateAuthentication();\n\n            case 'SecurityResult':\n                return this._handleSecurityResult();\n\n            case 'SecurityReason':\n                return this._handleSecurityReason();\n\n            case 'ClientInitialisation':\n                this._sock.sQpush8(this._shared ? 1 : 0); // ClientInitialisation\n                this._sock.flush();\n                this._rfbInitState = 'ServerInitialisation';\n                return true;\n\n            case 'ServerInitialisation':\n                return this._negotiateServerInit();\n\n            default:\n                return this._fail(\"Unknown init state (state: \" +\n                                  this._rfbInitState + \")\");\n        }\n    }\n\n    // Resume authentication handshake after it was paused for some\n    // reason, e.g. waiting for a password from the user\n    _resumeAuthentication() {\n        // We use setTimeout() so it's run in its own context, just like\n        // it originally did via the WebSocket's event handler\n        setTimeout(this._initMsg.bind(this), 0);\n    }\n\n    _handleSetColourMapMsg() {\n        Log.Debug(\"SetColorMapEntries\");\n\n        return this._fail(\"Unexpected SetColorMapEntries message\");\n    }\n\n    _writeClipboard(text) {\n        if (this._viewOnly) return;\n        if (this._asyncClipboard.writeClipboard(text)) return;\n        // Fallback clipboard\n        this.dispatchEvent(\n            new CustomEvent(\"clipboard\", {detail: {text: text}})\n        );\n    }\n\n    _handleServerCutText() {\n        Log.Debug(\"ServerCutText\");\n\n        if (this._sock.rQwait(\"ServerCutText header\", 7, 1)) { return false; }\n\n        this._sock.rQskipBytes(3);  // Padding\n\n        let length = this._sock.rQshift32();\n        length = toSigned32bit(length);\n\n        if (this._sock.rQwait(\"ServerCutText content\", Math.abs(length), 8)) { return false; }\n\n        if (length >= 0) {\n            //Standard msg\n            const text = this._sock.rQshiftStr(length);\n            if (this._viewOnly) {\n                return true;\n            }\n\n            this._writeClipboard(text);\n\n        } else {\n            //Extended msg.\n            length = Math.abs(length);\n            const flags = this._sock.rQshift32();\n            let formats = flags & 0x0000FFFF;\n            let actions = flags & 0xFF000000;\n\n            let isCaps = (!!(actions & extendedClipboardActionCaps));\n            if (isCaps) {\n                this._clipboardServerCapabilitiesFormats = {};\n                this._clipboardServerCapabilitiesActions = {};\n\n                // Update our server capabilities for Formats\n                for (let i = 0; i <= 15; i++) {\n                    let index = 1 << i;\n\n                    // Check if format flag is set.\n                    if ((formats & index)) {\n                        this._clipboardServerCapabilitiesFormats[index] = true;\n                        // We don't send unsolicited clipboard, so we\n                        // ignore the size\n                        this._sock.rQshift32();\n                    }\n                }\n\n                // Update our server capabilities for Actions\n                for (let i = 24; i <= 31; i++) {\n                    let index = 1 << i;\n                    this._clipboardServerCapabilitiesActions[index] = !!(actions & index);\n                }\n\n                /*  Caps handling done, send caps with the clients\n                    capabilities set as a response */\n                let clientActions = [\n                    extendedClipboardActionCaps,\n                    extendedClipboardActionRequest,\n                    extendedClipboardActionPeek,\n                    extendedClipboardActionNotify,\n                    extendedClipboardActionProvide\n                ];\n                RFB.messages.extendedClipboardCaps(this._sock, clientActions, {extendedClipboardFormatText: 0});\n\n            } else if (actions === extendedClipboardActionRequest) {\n                if (this._viewOnly) {\n                    return true;\n                }\n\n                // Check if server has told us it can handle Provide and there is clipboard data to send.\n                if (this._clipboardText != null &&\n                    this._clipboardServerCapabilitiesActions[extendedClipboardActionProvide]) {\n\n                    if (formats & extendedClipboardFormatText) {\n                        RFB.messages.extendedClipboardProvide(this._sock, [extendedClipboardFormatText], [this._clipboardText]);\n                    }\n                }\n\n            } else if (actions === extendedClipboardActionPeek) {\n                if (this._viewOnly) {\n                    return true;\n                }\n\n                if (this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {\n\n                    if (this._clipboardText != null) {\n                        RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);\n                    } else {\n                        RFB.messages.extendedClipboardNotify(this._sock, []);\n                    }\n                }\n\n            } else if (actions === extendedClipboardActionNotify) {\n                if (this._viewOnly) {\n                    return true;\n                }\n\n                if (this._clipboardServerCapabilitiesActions[extendedClipboardActionRequest]) {\n\n                    if (formats & extendedClipboardFormatText) {\n                        RFB.messages.extendedClipboardRequest(this._sock, [extendedClipboardFormatText]);\n                    }\n                }\n\n            } else if (actions === extendedClipboardActionProvide) {\n                if (this._viewOnly) {\n                    return true;\n                }\n\n                if (!(formats & extendedClipboardFormatText)) {\n                    return true;\n                }\n                // Ignore what we had in our clipboard client side.\n                this._clipboardText = null;\n\n                // FIXME: Should probably verify that this data was actually requested\n                let zlibStream = this._sock.rQshiftBytes(length - 4);\n                let streamInflator = new Inflator();\n                let textData = null;\n\n                streamInflator.setInput(zlibStream);\n                for (let i = 0; i <= 15; i++) {\n                    let format = 1 << i;\n\n                    if (formats & format) {\n\n                        let size = 0x00;\n                        let sizeArray = streamInflator.inflate(4);\n\n                        size |= (sizeArray[0] << 24);\n                        size |= (sizeArray[1] << 16);\n                        size |= (sizeArray[2] << 8);\n                        size |= (sizeArray[3]);\n                        let chunk = streamInflator.inflate(size);\n\n                        if (format === extendedClipboardFormatText) {\n                            textData = chunk;\n                        }\n                    }\n                }\n                streamInflator.setInput(null);\n\n                if (textData !== null) {\n                    let tmpText = \"\";\n                    for (let i = 0; i < textData.length; i++) {\n                        tmpText += String.fromCharCode(textData[i]);\n                    }\n                    textData = tmpText;\n\n                    textData = decodeUTF8(textData);\n                    if ((textData.length > 0) && \"\\0\" === textData.charAt(textData.length - 1)) {\n                        textData = textData.slice(0, -1);\n                    }\n\n                    textData = textData.replaceAll(\"\\r\\n\", \"\\n\");\n\n                    this._writeClipboard(textData);\n                }\n            } else {\n                return this._fail(\"Unexpected action in extended clipboard message: \" + actions);\n            }\n        }\n        return true;\n    }\n\n    _handleServerFenceMsg() {\n        if (this._sock.rQwait(\"ServerFence header\", 8, 1)) { return false; }\n        this._sock.rQskipBytes(3); // Padding\n        let flags = this._sock.rQshift32();\n        let length = this._sock.rQshift8();\n\n        if (this._sock.rQwait(\"ServerFence payload\", length, 9)) { return false; }\n\n        if (length > 64) {\n            Log.Warn(\"Bad payload length (\" + length + \") in fence response\");\n            length = 64;\n        }\n\n        const payload = this._sock.rQshiftStr(length);\n\n        this._supportsFence = true;\n\n        /*\n         * Fence flags\n         *\n         *  (1<<0)  - BlockBefore\n         *  (1<<1)  - BlockAfter\n         *  (1<<2)  - SyncNext\n         *  (1<<31) - Request\n         */\n\n        if (!(flags & (1<<31))) {\n            return this._fail(\"Unexpected fence response\");\n        }\n\n        // Filter out unsupported flags\n        // FIXME: support syncNext\n        flags &= (1<<0) | (1<<1);\n\n        // BlockBefore and BlockAfter are automatically handled by\n        // the fact that we process each incoming message\n        // synchronuosly.\n        RFB.messages.clientFence(this._sock, flags, payload);\n\n        return true;\n    }\n\n    _handleXvpMsg() {\n        if (this._sock.rQwait(\"XVP version and message\", 3, 1)) { return false; }\n        this._sock.rQskipBytes(1);  // Padding\n        const xvpVer = this._sock.rQshift8();\n        const xvpMsg = this._sock.rQshift8();\n\n        switch (xvpMsg) {\n            case 0:  // XVP_FAIL\n                Log.Error(\"XVP operation failed\");\n                break;\n            case 1:  // XVP_INIT\n                this._rfbXvpVer = xvpVer;\n                Log.Info(\"XVP extensions enabled (version \" + this._rfbXvpVer + \")\");\n                this._setCapability(\"power\", true);\n                break;\n            default:\n                this._fail(\"Illegal server XVP message (msg: \" + xvpMsg + \")\");\n                break;\n        }\n\n        return true;\n    }\n\n    _normalMsg() {\n        let msgType;\n        if (this._FBU.rects > 0) {\n            msgType = 0;\n        } else {\n            msgType = this._sock.rQshift8();\n        }\n\n        let first, ret;\n        switch (msgType) {\n            case 0:  // FramebufferUpdate\n                ret = this._framebufferUpdate();\n                if (ret && !this._enabledContinuousUpdates) {\n                    RFB.messages.fbUpdateRequest(this._sock, true, 0, 0,\n                                                 this._fbWidth, this._fbHeight);\n                }\n                return ret;\n\n            case 1:  // SetColorMapEntries\n                return this._handleSetColourMapMsg();\n\n            case 2:  // Bell\n                Log.Debug(\"Bell\");\n                this.dispatchEvent(new CustomEvent(\n                    \"bell\",\n                    { detail: {} }));\n                return true;\n\n            case 3:  // ServerCutText\n                return this._handleServerCutText();\n\n            case 150: // EndOfContinuousUpdates\n                first = !this._supportsContinuousUpdates;\n                this._supportsContinuousUpdates = true;\n                this._enabledContinuousUpdates = false;\n                if (first) {\n                    this._enabledContinuousUpdates = true;\n                    this._updateContinuousUpdates();\n                    Log.Info(\"Enabling continuous updates.\");\n                } else {\n                    // FIXME: We need to send a framebufferupdaterequest here\n                    // if we add support for turning off continuous updates\n                }\n                return true;\n\n            case 248: // ServerFence\n                return this._handleServerFenceMsg();\n\n            case 250:  // XVP\n                return this._handleXvpMsg();\n\n            default:\n                this._fail(\"Unexpected server message (type \" + msgType + \")\");\n                Log.Debug(\"sock.rQpeekBytes(30): \" + this._sock.rQpeekBytes(30));\n                return true;\n        }\n    }\n\n    _framebufferUpdate() {\n        if (this._FBU.rects === 0) {\n            if (this._sock.rQwait(\"FBU header\", 3, 1)) { return false; }\n            this._sock.rQskipBytes(1);  // Padding\n            this._FBU.rects = this._sock.rQshift16();\n\n            // Make sure the previous frame is fully rendered first\n            // to avoid building up an excessive queue\n            if (this._display.pending()) {\n                this._flushing = true;\n                this._display.flush()\n                    .then(() => {\n                        this._flushing = false;\n                        // Resume processing\n                        if (!this._sock.rQwait(\"message\", 1)) {\n                            this._handleMessage();\n                        }\n                    });\n                return false;\n            }\n        }\n\n        while (this._FBU.rects > 0) {\n            if (this._FBU.encoding === null) {\n                if (this._sock.rQwait(\"rect header\", 12)) { return false; }\n                /* New FramebufferUpdate */\n\n                this._FBU.x = this._sock.rQshift16();\n                this._FBU.y = this._sock.rQshift16();\n                this._FBU.width = this._sock.rQshift16();\n                this._FBU.height = this._sock.rQshift16();\n                this._FBU.encoding = this._sock.rQshift32();\n                /* Encodings are signed */\n                this._FBU.encoding >>= 0;\n            }\n\n            if (!this._handleRect()) {\n                return false;\n            }\n\n            this._FBU.rects--;\n            this._FBU.encoding = null;\n        }\n\n        this._display.flip();\n\n        return true;  // We finished this FBU\n    }\n\n    _handleRect() {\n        switch (this._FBU.encoding) {\n            case encodings.pseudoEncodingLastRect:\n                this._FBU.rects = 1; // Will be decreased when we return\n                return true;\n\n            case encodings.pseudoEncodingVMwareCursor:\n                return this._handleVMwareCursor();\n\n            case encodings.pseudoEncodingCursor:\n                return this._handleCursor();\n\n            case encodings.pseudoEncodingQEMUExtendedKeyEvent:\n                this._qemuExtKeyEventSupported = true;\n                return true;\n\n            case encodings.pseudoEncodingDesktopName:\n                return this._handleDesktopName();\n\n            case encodings.pseudoEncodingDesktopSize:\n                this._resize(this._FBU.width, this._FBU.height);\n                return true;\n\n            case encodings.pseudoEncodingExtendedDesktopSize:\n                return this._handleExtendedDesktopSize();\n\n            case encodings.pseudoEncodingExtendedMouseButtons:\n                this._extendedPointerEventSupported = true;\n                return true;\n\n            case encodings.pseudoEncodingQEMULedEvent:\n                return this._handleLedEvent();\n\n            default:\n                return this._handleDataRect();\n        }\n    }\n\n    _handleVMwareCursor() {\n        const hotx = this._FBU.x;  // hotspot-x\n        const hoty = this._FBU.y;  // hotspot-y\n        const w = this._FBU.width;\n        const h = this._FBU.height;\n        if (this._sock.rQwait(\"VMware cursor encoding\", 1)) {\n            return false;\n        }\n\n        const cursorType = this._sock.rQshift8();\n\n        this._sock.rQshift8(); //Padding\n\n        let rgba;\n        const bytesPerPixel = 4;\n\n        //Classic cursor\n        if (cursorType == 0) {\n            //Used to filter away unimportant bits.\n            //OR is used for correct conversion in js.\n            const PIXEL_MASK = 0xffffff00 | 0;\n            rgba = new Array(w * h * bytesPerPixel);\n\n            if (this._sock.rQwait(\"VMware cursor classic encoding\",\n                                  (w * h * bytesPerPixel) * 2, 2)) {\n                return false;\n            }\n\n            let andMask = new Array(w * h);\n            for (let pixel = 0; pixel < (w * h); pixel++) {\n                andMask[pixel] = this._sock.rQshift32();\n            }\n\n            let xorMask = new Array(w * h);\n            for (let pixel = 0; pixel < (w * h); pixel++) {\n                xorMask[pixel] = this._sock.rQshift32();\n            }\n\n            for (let pixel = 0; pixel < (w * h); pixel++) {\n                if (andMask[pixel] == 0) {\n                    //Fully opaque pixel\n                    let bgr = xorMask[pixel];\n                    let r   = bgr >> 8  & 0xff;\n                    let g   = bgr >> 16 & 0xff;\n                    let b   = bgr >> 24 & 0xff;\n\n                    rgba[(pixel * bytesPerPixel)     ] = r;    //r\n                    rgba[(pixel * bytesPerPixel) + 1 ] = g;    //g\n                    rgba[(pixel * bytesPerPixel) + 2 ] = b;    //b\n                    rgba[(pixel * bytesPerPixel) + 3 ] = 0xff; //a\n\n                } else if ((andMask[pixel] & PIXEL_MASK) ==\n                           PIXEL_MASK) {\n                    //Only screen value matters, no mouse colouring\n                    if (xorMask[pixel] == 0) {\n                        //Transparent pixel\n                        rgba[(pixel * bytesPerPixel)     ] = 0x00;\n                        rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;\n                        rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;\n                        rgba[(pixel * bytesPerPixel) + 3 ] = 0x00;\n\n                    } else if ((xorMask[pixel] & PIXEL_MASK) ==\n                               PIXEL_MASK) {\n                        //Inverted pixel, not supported in browsers.\n                        //Fully opaque instead.\n                        rgba[(pixel * bytesPerPixel)     ] = 0x00;\n                        rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;\n                        rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;\n                        rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;\n\n                    } else {\n                        //Unhandled xorMask\n                        rgba[(pixel * bytesPerPixel)     ] = 0x00;\n                        rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;\n                        rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;\n                        rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;\n                    }\n\n                } else {\n                    //Unhandled andMask\n                    rgba[(pixel * bytesPerPixel)     ] = 0x00;\n                    rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;\n                    rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;\n                    rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;\n                }\n            }\n\n        //Alpha cursor.\n        } else if (cursorType == 1) {\n            if (this._sock.rQwait(\"VMware cursor alpha encoding\",\n                                  (w * h * 4), 2)) {\n                return false;\n            }\n\n            rgba = new Array(w * h * bytesPerPixel);\n\n            for (let pixel = 0; pixel < (w * h); pixel++) {\n                let data = this._sock.rQshift32();\n\n                rgba[(pixel * 4)     ] = data >> 24 & 0xff; //r\n                rgba[(pixel * 4) + 1 ] = data >> 16 & 0xff; //g\n                rgba[(pixel * 4) + 2 ] = data >> 8 & 0xff;  //b\n                rgba[(pixel * 4) + 3 ] = data & 0xff;       //a\n            }\n\n        } else {\n            Log.Warn(\"The given cursor type is not supported: \"\n                      + cursorType + \" given.\");\n            return false;\n        }\n\n        this._updateCursor(rgba, hotx, hoty, w, h);\n\n        return true;\n    }\n\n    _handleCursor() {\n        const hotx = this._FBU.x;  // hotspot-x\n        const hoty = this._FBU.y;  // hotspot-y\n        const w = this._FBU.width;\n        const h = this._FBU.height;\n\n        const pixelslength = w * h * 4;\n        const masklength = Math.ceil(w / 8) * h;\n\n        let bytes = pixelslength + masklength;\n        if (this._sock.rQwait(\"cursor encoding\", bytes)) {\n            return false;\n        }\n\n        // Decode from BGRX pixels + bit mask to RGBA\n        const pixels = this._sock.rQshiftBytes(pixelslength);\n        const mask = this._sock.rQshiftBytes(masklength);\n        let rgba = new Uint8Array(w * h * 4);\n\n        let pixIdx = 0;\n        for (let y = 0; y < h; y++) {\n            for (let x = 0; x < w; x++) {\n                let maskIdx = y * Math.ceil(w / 8) + Math.floor(x / 8);\n                let alpha = (mask[maskIdx] << (x % 8)) & 0x80 ? 255 : 0;\n                rgba[pixIdx    ] = pixels[pixIdx + 2];\n                rgba[pixIdx + 1] = pixels[pixIdx + 1];\n                rgba[pixIdx + 2] = pixels[pixIdx];\n                rgba[pixIdx + 3] = alpha;\n                pixIdx += 4;\n            }\n        }\n\n        this._updateCursor(rgba, hotx, hoty, w, h);\n\n        return true;\n    }\n\n    _handleDesktopName() {\n        if (this._sock.rQwait(\"DesktopName\", 4)) {\n            return false;\n        }\n\n        let length = this._sock.rQshift32();\n\n        if (this._sock.rQwait(\"DesktopName\", length, 4)) {\n            return false;\n        }\n\n        let name = this._sock.rQshiftStr(length);\n        name = decodeUTF8(name, true);\n\n        this._setDesktopName(name);\n\n        return true;\n    }\n\n    _handleLedEvent() {\n        if (this._sock.rQwait(\"LED status\", 1)) {\n            return false;\n        }\n\n        let data = this._sock.rQshift8();\n        // ScrollLock state can be retrieved with data & 1. This is currently not needed.\n        let numLock = data & 2 ? true : false;\n        let capsLock = data & 4 ? true : false;\n        this._remoteCapsLock = capsLock;\n        this._remoteNumLock = numLock;\n\n        return true;\n    }\n\n    _handleExtendedDesktopSize() {\n        if (this._sock.rQwait(\"ExtendedDesktopSize\", 4)) {\n            return false;\n        }\n\n        const numberOfScreens = this._sock.rQpeek8();\n\n        let bytes = 4 + (numberOfScreens * 16);\n        if (this._sock.rQwait(\"ExtendedDesktopSize\", bytes)) {\n            return false;\n        }\n\n        const firstUpdate = !this._supportsSetDesktopSize;\n        this._supportsSetDesktopSize = true;\n\n        this._sock.rQskipBytes(1);  // number-of-screens\n        this._sock.rQskipBytes(3);  // padding\n\n        for (let i = 0; i < numberOfScreens; i += 1) {\n            // Save the id and flags of the first screen\n            if (i === 0) {\n                this._screenID = this._sock.rQshift32();    // id\n                this._sock.rQskipBytes(2);                  // x-position\n                this._sock.rQskipBytes(2);                  // y-position\n                this._sock.rQskipBytes(2);                  // width\n                this._sock.rQskipBytes(2);                  // height\n                this._screenFlags = this._sock.rQshift32(); // flags\n            } else {\n                this._sock.rQskipBytes(16);\n            }\n        }\n\n        /*\n         * The x-position indicates the reason for the change:\n         *\n         *  0 - server resized on its own\n         *  1 - this client requested the resize\n         *  2 - another client requested the resize\n         */\n\n        if (this._FBU.x === 1) {\n            this._pendingRemoteResize = false;\n        }\n\n        // We need to handle errors when we requested the resize.\n        if (this._FBU.x === 1 && this._FBU.y !== 0) {\n            let msg;\n            // The y-position indicates the status code from the server\n            switch (this._FBU.y) {\n                case 1:\n                    msg = \"Resize is administratively prohibited\";\n                    break;\n                case 2:\n                    msg = \"Out of resources\";\n                    break;\n                case 3:\n                    msg = \"Invalid screen layout\";\n                    break;\n                default:\n                    msg = \"Unknown reason\";\n                    break;\n            }\n            Log.Warn(\"Server did not accept the resize request: \"\n                     + msg);\n        } else {\n            this._resize(this._FBU.width, this._FBU.height);\n        }\n\n        // Normally we only apply the current resize mode after a\n        // window resize event. However there is no such trigger on the\n        // initial connect. And we don't know if the server supports\n        // resizing until we've gotten here.\n        if (firstUpdate) {\n            this._requestRemoteResize();\n        }\n\n        if (this._FBU.x === 1 && this._FBU.y === 0) {\n            // We might have resized again whilst waiting for the\n            // previous request, so check if we are in sync\n            this._requestRemoteResize();\n        }\n\n        return true;\n    }\n\n    _handleDataRect() {\n        let decoder = this._decoders[this._FBU.encoding];\n        if (!decoder) {\n            this._fail(\"Unsupported encoding (encoding: \" +\n                       this._FBU.encoding + \")\");\n            return false;\n        }\n\n        try {\n            return decoder.decodeRect(this._FBU.x, this._FBU.y,\n                                      this._FBU.width, this._FBU.height,\n                                      this._sock, this._display,\n                                      this._fbDepth);\n        } catch (err) {\n            this._fail(\"Error decoding rect: \" + err);\n            return false;\n        }\n    }\n\n    _updateContinuousUpdates() {\n        if (!this._enabledContinuousUpdates) { return; }\n\n        RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0,\n                                             this._fbWidth, this._fbHeight);\n    }\n\n    // Handle resize-messages from the server\n    _resize(width, height) {\n        this._fbWidth = width;\n        this._fbHeight = height;\n\n        this._display.resize(this._fbWidth, this._fbHeight);\n\n        // Adjust the visible viewport based on the new dimensions\n        this._updateClip();\n        this._updateScale();\n\n        this._updateContinuousUpdates();\n\n        // Keep this size until browser client size changes\n        this._saveExpectedClientSize();\n    }\n\n    _xvpOp(ver, op) {\n        if (this._rfbXvpVer < ver) { return; }\n        Log.Info(\"Sending XVP operation \" + op + \" (version \" + ver + \")\");\n        RFB.messages.xvpOp(this._sock, ver, op);\n    }\n\n    _updateCursor(rgba, hotx, hoty, w, h) {\n        this._cursorImage = {\n            rgbaPixels: rgba,\n            hotx: hotx, hoty: hoty, w: w, h: h,\n        };\n        this._refreshCursor();\n    }\n\n    _shouldShowDotCursor() {\n        // Called when this._cursorImage is updated\n        if (!this._showDotCursor) {\n            // User does not want to see the dot, so...\n            return false;\n        }\n\n        // The dot should not be shown if the cursor is already visible,\n        // i.e. contains at least one not-fully-transparent pixel.\n        // So iterate through all alpha bytes in rgba and stop at the\n        // first non-zero.\n        for (let i = 3; i < this._cursorImage.rgbaPixels.length; i += 4) {\n            if (this._cursorImage.rgbaPixels[i]) {\n                return false;\n            }\n        }\n\n        // At this point, we know that the cursor is fully transparent, and\n        // the user wants to see the dot instead of this.\n        return true;\n    }\n\n    _refreshCursor() {\n        if (this._rfbConnectionState !== \"connecting\" &&\n            this._rfbConnectionState !== \"connected\") {\n            return;\n        }\n        const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage;\n        this._cursor.change(image.rgbaPixels,\n                            image.hotx, image.hoty,\n                            image.w, image.h\n        );\n    }\n\n    static genDES(password, challenge) {\n        const passwordChars = password.split('').map(c => c.charCodeAt(0));\n        const key = legacyCrypto.importKey(\n            \"raw\", passwordChars, { name: \"DES-ECB\" }, false, [\"encrypt\"]);\n        return legacyCrypto.encrypt({ name: \"DES-ECB\" }, key, challenge);\n    }\n}\n\n// Class Methods\nRFB.messages = {\n    keyEvent(sock, keysym, down) {\n        sock.sQpush8(4); // msg-type\n        sock.sQpush8(down);\n\n        sock.sQpush16(0);\n\n        sock.sQpush32(keysym);\n\n        sock.flush();\n    },\n\n    QEMUExtendedKeyEvent(sock, keysym, down, keycode) {\n        function getRFBkeycode(xtScanCode) {\n            const upperByte = (keycode >> 8);\n            const lowerByte = (keycode & 0x00ff);\n            if (upperByte === 0xe0 && lowerByte < 0x7f) {\n                return lowerByte | 0x80;\n            }\n            return xtScanCode;\n        }\n\n        sock.sQpush8(255); // msg-type\n        sock.sQpush8(0); // sub msg-type\n\n        sock.sQpush16(down);\n\n        sock.sQpush32(keysym);\n\n        const RFBkeycode = getRFBkeycode(keycode);\n\n        sock.sQpush32(RFBkeycode);\n\n        sock.flush();\n    },\n\n    pointerEvent(sock, x, y, mask) {\n        sock.sQpush8(5); // msg-type\n\n        // Marker bit must be set to 0, otherwise the server might\n        // confuse the marker bit with the highest bit in a normal\n        // PointerEvent message.\n        mask = mask & 0x7f;\n        sock.sQpush8(mask);\n\n        sock.sQpush16(x);\n        sock.sQpush16(y);\n\n        sock.flush();\n    },\n\n    extendedPointerEvent(sock, x, y, mask) {\n        sock.sQpush8(5); // msg-type\n\n        let higherBits = (mask >> 7) & 0xff;\n\n        // Bits 2-7 are reserved\n        if (higherBits & 0xfc) {\n            throw new Error(\"Invalid mouse button mask: \" + mask);\n        }\n\n        let lowerBits = mask & 0x7f;\n        lowerBits |= 0x80; // Set marker bit to 1\n\n        sock.sQpush8(lowerBits);\n        sock.sQpush16(x);\n        sock.sQpush16(y);\n        sock.sQpush8(higherBits);\n\n        sock.flush();\n    },\n\n    // Used to build Notify and Request data.\n    _buildExtendedClipboardFlags(actions, formats) {\n        let data = new Uint8Array(4);\n        let formatFlag = 0x00000000;\n        let actionFlag = 0x00000000;\n\n        for (let i = 0; i < actions.length; i++) {\n            actionFlag |= actions[i];\n        }\n\n        for (let i = 0; i < formats.length; i++) {\n            formatFlag |= formats[i];\n        }\n\n        data[0] = actionFlag >> 24; // Actions\n        data[1] = 0x00;             // Reserved\n        data[2] = 0x00;             // Reserved\n        data[3] = formatFlag;       // Formats\n\n        return data;\n    },\n\n    extendedClipboardProvide(sock, formats, inData) {\n        // Deflate incomming data and their sizes\n        let deflator = new Deflator();\n        let dataToDeflate = [];\n\n        for (let i = 0; i < formats.length; i++) {\n            // We only support the format Text at this time\n            if (formats[i] != extendedClipboardFormatText) {\n                throw new Error(\"Unsupported extended clipboard format for Provide message.\");\n            }\n\n            // Change lone \\r or \\n into \\r\\n as defined in rfbproto\n            inData[i] = inData[i].replace(/\\r\\n|\\r|\\n/gm, \"\\r\\n\");\n\n            // Check if it already has \\0\n            let text = encodeUTF8(inData[i] + \"\\0\");\n\n            dataToDeflate.push( (text.length >> 24) & 0xFF,\n                                (text.length >> 16) & 0xFF,\n                                (text.length >>  8) & 0xFF,\n                                (text.length & 0xFF));\n\n            for (let j = 0; j < text.length; j++) {\n                dataToDeflate.push(text.charCodeAt(j));\n            }\n        }\n\n        let deflatedData = deflator.deflate(new Uint8Array(dataToDeflate));\n\n        // Build data  to send\n        let data = new Uint8Array(4 + deflatedData.length);\n        data.set(RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionProvide],\n                                                           formats));\n        data.set(deflatedData, 4);\n\n        RFB.messages.clientCutText(sock, data, true);\n    },\n\n    extendedClipboardNotify(sock, formats) {\n        let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionNotify],\n                                                              formats);\n        RFB.messages.clientCutText(sock, flags, true);\n    },\n\n    extendedClipboardRequest(sock, formats) {\n        let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionRequest],\n                                                              formats);\n        RFB.messages.clientCutText(sock, flags, true);\n    },\n\n    extendedClipboardCaps(sock, actions, formats) {\n        let formatKeys = Object.keys(formats);\n        let data  = new Uint8Array(4 + (4 * formatKeys.length));\n\n        formatKeys.map(x => parseInt(x));\n        formatKeys.sort((a, b) =>  a - b);\n\n        data.set(RFB.messages._buildExtendedClipboardFlags(actions, []));\n\n        let loopOffset = 4;\n        for (let i = 0; i < formatKeys.length; i++) {\n            data[loopOffset]     = formats[formatKeys[i]] >> 24;\n            data[loopOffset + 1] = formats[formatKeys[i]] >> 16;\n            data[loopOffset + 2] = formats[formatKeys[i]] >> 8;\n            data[loopOffset + 3] = formats[formatKeys[i]] >> 0;\n\n            loopOffset += 4;\n            data[3] |= (1 << formatKeys[i]); // Update our format flags\n        }\n\n        RFB.messages.clientCutText(sock, data, true);\n    },\n\n    clientCutText(sock, data, extended = false) {\n        sock.sQpush8(6); // msg-type\n\n        sock.sQpush8(0); // padding\n        sock.sQpush8(0); // padding\n        sock.sQpush8(0); // padding\n\n        let length;\n        if (extended) {\n            length = toUnsigned32bit(-data.length);\n        } else {\n            length = data.length;\n        }\n\n        sock.sQpush32(length);\n        sock.sQpushBytes(data);\n        sock.flush();\n    },\n\n    setDesktopSize(sock, width, height, id, flags) {\n        sock.sQpush8(251); // msg-type\n\n        sock.sQpush8(0); // padding\n\n        sock.sQpush16(width);\n        sock.sQpush16(height);\n\n        sock.sQpush8(1); // number-of-screens\n\n        sock.sQpush8(0); // padding\n\n        // screen array\n        sock.sQpush32(id);\n        sock.sQpush16(0); // x-position\n        sock.sQpush16(0); // y-position\n        sock.sQpush16(width);\n        sock.sQpush16(height);\n        sock.sQpush32(flags);\n\n        sock.flush();\n    },\n\n    clientFence(sock, flags, payload) {\n        sock.sQpush8(248); // msg-type\n\n        sock.sQpush8(0); // padding\n        sock.sQpush8(0); // padding\n        sock.sQpush8(0); // padding\n\n        sock.sQpush32(flags);\n\n        sock.sQpush8(payload.length);\n        sock.sQpushString(payload);\n\n        sock.flush();\n    },\n\n    enableContinuousUpdates(sock, enable, x, y, width, height) {\n        sock.sQpush8(150); // msg-type\n\n        sock.sQpush8(enable);\n\n        sock.sQpush16(x);\n        sock.sQpush16(y);\n        sock.sQpush16(width);\n        sock.sQpush16(height);\n\n        sock.flush();\n    },\n\n    pixelFormat(sock, depth, trueColor) {\n        let bpp;\n\n        if (depth > 16) {\n            bpp = 32;\n        } else if (depth > 8) {\n            bpp = 16;\n        } else {\n            bpp = 8;\n        }\n\n        const bits = Math.floor(depth/3);\n\n        sock.sQpush8(0); // msg-type\n\n        sock.sQpush8(0); // padding\n        sock.sQpush8(0); // padding\n        sock.sQpush8(0); // padding\n\n        sock.sQpush8(bpp);\n        sock.sQpush8(depth);\n        sock.sQpush8(0); // little-endian\n        sock.sQpush8(trueColor ? 1 : 0);\n\n        sock.sQpush16((1 << bits) - 1); // red-max\n        sock.sQpush16((1 << bits) - 1); // green-max\n        sock.sQpush16((1 << bits) - 1); // blue-max\n\n        sock.sQpush8(bits * 0); // red-shift\n        sock.sQpush8(bits * 1); // green-shift\n        sock.sQpush8(bits * 2); // blue-shift\n\n        sock.sQpush8(0); // padding\n        sock.sQpush8(0); // padding\n        sock.sQpush8(0); // padding\n\n        sock.flush();\n    },\n\n    clientEncodings(sock, encodings) {\n        sock.sQpush8(2); // msg-type\n\n        sock.sQpush8(0); // padding\n\n        sock.sQpush16(encodings.length);\n        for (let i = 0; i < encodings.length; i++) {\n            sock.sQpush32(encodings[i]);\n        }\n\n        sock.flush();\n    },\n\n    fbUpdateRequest(sock, incremental, x, y, w, h) {\n        if (typeof(x) === \"undefined\") { x = 0; }\n        if (typeof(y) === \"undefined\") { y = 0; }\n\n        sock.sQpush8(3); // msg-type\n\n        sock.sQpush8(incremental ? 1 : 0);\n\n        sock.sQpush16(x);\n        sock.sQpush16(y);\n        sock.sQpush16(w);\n        sock.sQpush16(h);\n\n        sock.flush();\n    },\n\n    xvpOp(sock, ver, op) {\n        sock.sQpush8(250); // msg-type\n\n        sock.sQpush8(0); // padding\n\n        sock.sQpush8(ver);\n        sock.sQpush8(op);\n\n        sock.flush();\n    }\n};\n\nRFB.cursors = {\n    none: {\n        rgbaPixels: new Uint8Array(),\n        w: 0, h: 0,\n        hotx: 0, hoty: 0,\n    },\n\n    dot: {\n        /* eslint-disable indent */\n        rgbaPixels: new Uint8Array([\n            255, 255, 255, 255,   0,   0,   0, 255, 255, 255, 255, 255,\n              0,   0,   0, 255,   0,   0,   0,   0,   0,   0,  0,  255,\n            255, 255, 255, 255,   0,   0,   0, 255, 255, 255, 255, 255,\n        ]),\n        /* eslint-enable indent */\n        w: 3, h: 3,\n        hotx: 1, hoty: 1,\n    }\n};\n"
  },
  {
    "path": "core/util/browser.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2019 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n *\n * Browser feature support detection\n */\n\nimport * as Log from './logging.js';\nimport Base64 from '../base64.js';\n\n// Async clipboard detection\n\n/* Evaluates if there is browser support for the async clipboard API and\n * relevant clipboard permissions. Returns 'unsupported' if permission states\n * cannot be resolved. On the other hand, detecting 'granted' or 'prompt'\n * permission states for both read and write indicates full API support with no\n * imposed native browser paste prompt. Conversely, detecting 'denied' indicates\n * the user elected to disable clipboard.\n */\nexport async function browserAsyncClipboardSupport() {\n    if (!(navigator?.permissions?.query &&\n          navigator?.clipboard?.writeText &&\n          navigator?.clipboard?.readText)) {\n        return 'unsupported';\n    }\n    try {\n        const writePerm = await navigator.permissions.query(\n            {name: \"clipboard-write\", allowWithoutGesture: true});\n        const readPerm = await navigator.permissions.query(\n            {name: \"clipboard-read\",  allowWithoutGesture: false});\n        if (writePerm.state === \"denied\" || readPerm.state  === \"denied\") {\n            return 'denied';\n        }\n        if ((writePerm.state === \"granted\" || writePerm.state === \"prompt\") &&\n            (readPerm.state  === \"granted\" || readPerm.state  === \"prompt\")) {\n            return 'available';\n        }\n    } catch {\n        return 'unsupported';\n    }\n    return 'unsupported';\n}\n\n// Touch detection\nexport let isTouchDevice = ('ontouchstart' in document.documentElement) ||\n                                 // required for Chrome debugger\n                                 (document.ontouchstart !== undefined) ||\n                                 // required for MS Surface\n                                 (navigator.maxTouchPoints > 0) ||\n                                 (navigator.msMaxTouchPoints > 0);\nwindow.addEventListener('touchstart', function onFirstTouch() {\n    isTouchDevice = true;\n    window.removeEventListener('touchstart', onFirstTouch, false);\n}, false);\n\n\n// The goal is to find a certain physical width, the devicePixelRatio\n// brings us a bit closer but is not optimal.\nexport let dragThreshold = 10 * (window.devicePixelRatio || 1);\n\nlet _supportsCursorURIs = false;\n\ntry {\n    const target = document.createElement('canvas');\n    target.style.cursor = 'url(\"data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==\") 2 2, default';\n\n    if (target.style.cursor.indexOf(\"url\") === 0) {\n        Log.Info(\"Data URI scheme cursor supported\");\n        _supportsCursorURIs = true;\n    } else {\n        Log.Warn(\"Data URI scheme cursor not supported\");\n    }\n} catch (exc) {\n    Log.Error(\"Data URI scheme cursor test exception: \" + exc);\n}\n\nexport const supportsCursorURIs = _supportsCursorURIs;\n\nlet _hasScrollbarGutter = true;\ntry {\n    // Create invisible container\n    const container = document.createElement('div');\n    container.style.visibility = 'hidden';\n    container.style.overflow = 'scroll'; // forcing scrollbars\n    document.body.appendChild(container);\n\n    // Create a div and place it in the container\n    const child = document.createElement('div');\n    container.appendChild(child);\n\n    // Calculate the difference between the container's full width\n    // and the child's width - the difference is the scrollbars\n    const scrollbarWidth = (container.offsetWidth - child.offsetWidth);\n\n    // Clean up\n    container.parentNode.removeChild(container);\n\n    _hasScrollbarGutter = scrollbarWidth != 0;\n} catch (exc) {\n    Log.Error(\"Scrollbar test exception: \" + exc);\n}\nexport const hasScrollbarGutter = _hasScrollbarGutter;\n\nexport let supportsWebCodecsH264Decode = false;\n\nasync function _checkWebCodecsH264DecodeSupport() {\n    if (!('VideoDecoder' in window)) {\n        return false;\n    }\n\n    // We'll need to make do with some placeholders here\n    const config = {\n        codec: 'avc1.42401f',\n        codedWidth: 1920,\n        codedHeight: 1080,\n        optimizeForLatency: true,\n    };\n\n    let support = await VideoDecoder.isConfigSupported(config);\n    if (!support.supported) {\n        return false;\n    }\n\n    // Firefox incorrectly reports supports for H.264 under some\n    // circumstances, so we need to actually test a real frame\n    // https://bugzilla.mozilla.org/show_bug.cgi?id=1932392\n\n    const data = new Uint8Array(Base64.decode(\n        'AAAAAWdCwBTZnpuAgICgAAADACAAAAZB4oVNAAAAAWjJYyyAAAABBgX//4Hc' +\n        'Rem95tlIt5Ys2CDZI+7veDI2NCAtIGNvcmUgMTY0IHIzMTA4IDMxZTE5Zjkg' +\n        'LSBILjI2NC9NUEVHLTQgQVZDIGNvZGVjIC0gQ29weWxlZnQgMjAwMy0yMDIz' +\n        'IC0gaHR0cDovL3d3dy52aWRlb2xhbi5vcmcveDI2NC5odG1sIC0gb3B0aW9u' +\n        'czogY2FiYWM9MCByZWY9NSBkZWJsb2NrPTE6MDowIGFuYWx5c2U9MHgxOjB4' +\n        'MTExIG1lPWhleCBzdWJtZT04IHBzeT0xIHBzeV9yZD0xLjAwOjAuMDAgbWl4' +\n        'ZWRfcmVmPTEgbWVfcmFuZ2U9MTYgY2hyb21hX21lPTEgdHJlbGxpcz0yIDh4' +\n        'OGRjdD0wIGNxbT0wIGRlYWR6b25lPTIxLDExIGZhc3RfcHNraXA9MSBjaHJv' +\n        'bWFfcXBfb2Zmc2V0PS0yIHRocmVhZHM9MSBsb29rYWhlYWRfdGhyZWFkcz0x' +\n        'IHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0ZT0xIGludGVybGFjZWQ9' +\n        'MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVz' +\n        'PTAgd2VpZ2h0cD0wIGtleWludD1pbmZpbml0ZSBrZXlpbnRfbWluPTI1IHNj' +\n        'ZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NTAgcmM9' +\n        'YWJyIG1idHJlZT0xIGJpdHJhdGU9NDAwIHJhdGV0b2w9MS4wIHFjb21wPTAu' +\n        'NjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFx' +\n        'PTE6MS4wMACAAAABZYiEBrxmKAAPVccAAS044AA5DRJMnkycJk4TPw=='));\n\n    let gotframe = false;\n    let error = null;\n\n    let decoder = new VideoDecoder({\n        output: (frame) => { gotframe = true; frame.close(); },\n        error: (e) => { error = e; },\n    });\n    let chunk = new EncodedVideoChunk({\n        timestamp: 0,\n        type: 'key',\n        data: data,\n    });\n\n    decoder.configure(config);\n    decoder.decode(chunk);\n    try {\n        await decoder.flush();\n    } catch (e) {\n        // Firefox incorrectly throws an exception here\n        // https://bugzilla.mozilla.org/show_bug.cgi?id=1932566\n        error = e;\n    }\n\n    // Firefox fails to deliver the error on Windows, so we need to\n    // check if we got a frame instead\n    // https://bugzilla.mozilla.org/show_bug.cgi?id=1932579\n    if (!gotframe) {\n        return false;\n    }\n\n    if (error !== null) {\n        return false;\n    }\n\n    return true;\n}\nsupportsWebCodecsH264Decode = await _checkWebCodecsH264DecodeSupport();\n\n/*\n * The functions for detection of platforms and browsers below are exported\n * but the use of these should be minimized as much as possible.\n *\n * It's better to use feature detection than platform detection.\n */\n\n/* OS */\n\nexport function isMac() {\n    return !!(/mac/i).exec(navigator.platform);\n}\n\nexport function isWindows() {\n    return !!(/win/i).exec(navigator.platform);\n}\n\nexport function isIOS() {\n    return (!!(/ipad/i).exec(navigator.platform) ||\n            !!(/iphone/i).exec(navigator.platform) ||\n            !!(/ipod/i).exec(navigator.platform));\n}\n\nexport function isAndroid() {\n    /* Android sets navigator.platform to Linux :/ */\n    return !!navigator.userAgent.match('Android ');\n}\n\nexport function isChromeOS() {\n    /* ChromeOS sets navigator.platform to Linux :/ */\n    return !!navigator.userAgent.match(' CrOS ');\n}\n\n/* Browser */\n\nexport function isSafari() {\n    return !!navigator.userAgent.match('Safari/...') &&\n           !navigator.userAgent.match('Chrome/...') &&\n           !navigator.userAgent.match('Chromium/...') &&\n           !navigator.userAgent.match('Epiphany/...');\n}\n\nexport function isFirefox() {\n    return !!navigator.userAgent.match('Firefox/...') &&\n           !navigator.userAgent.match('Seamonkey/...');\n}\n\nexport function isChrome() {\n    return !!navigator.userAgent.match('Chrome/...') &&\n           !navigator.userAgent.match('Chromium/...') &&\n           !navigator.userAgent.match('Edg/...') &&\n           !navigator.userAgent.match('OPR/...');\n}\n\nexport function isChromium() {\n    return !!navigator.userAgent.match('Chromium/...');\n}\n\nexport function isOpera() {\n    return !!navigator.userAgent.match('OPR/...');\n}\n\nexport function isEdge() {\n    return !!navigator.userAgent.match('Edg/...');\n}\n\n/* Engine */\n\nexport function isGecko() {\n    return !!navigator.userAgent.match('Gecko/...');\n}\n\nexport function isWebKit() {\n    return !!navigator.userAgent.match('AppleWebKit/...') &&\n           !navigator.userAgent.match('Chrome/...');\n}\n\nexport function isBlink() {\n    return !!navigator.userAgent.match('Chrome/...');\n}\n"
  },
  {
    "path": "core/util/cursor.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2019 The noVNC authors\n * Licensed under MPL 2.0 or any later version (see LICENSE.txt)\n */\n\nimport { supportsCursorURIs, isTouchDevice } from './browser.js';\n\nconst useFallback = !supportsCursorURIs || isTouchDevice;\n\nexport default class Cursor {\n    constructor() {\n        this._target = null;\n\n        this._canvas = document.createElement('canvas');\n\n        if (useFallback) {\n            this._canvas.style.position = 'fixed';\n            this._canvas.style.zIndex = '65535';\n            this._canvas.style.pointerEvents = 'none';\n            // Safari on iOS can select the cursor image\n            // https://bugs.webkit.org/show_bug.cgi?id=249223\n            this._canvas.style.userSelect = 'none';\n            this._canvas.style.WebkitUserSelect = 'none';\n            // Can't use \"display\" because of Firefox bug #1445997\n            this._canvas.style.visibility = 'hidden';\n        }\n\n        this._position = { x: 0, y: 0 };\n        this._hotSpot = { x: 0, y: 0 };\n\n        this._eventHandlers = {\n            'mouseover': this._handleMouseOver.bind(this),\n            'mouseleave': this._handleMouseLeave.bind(this),\n            'mousemove': this._handleMouseMove.bind(this),\n            'mouseup': this._handleMouseUp.bind(this),\n        };\n    }\n\n    attach(target) {\n        if (this._target) {\n            this.detach();\n        }\n\n        this._target = target;\n\n        if (useFallback) {\n            document.body.appendChild(this._canvas);\n\n            const options = { capture: true, passive: true };\n            this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options);\n            this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options);\n            this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options);\n            this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options);\n        }\n\n        this.clear();\n    }\n\n    detach() {\n        if (!this._target) {\n            return;\n        }\n\n        if (useFallback) {\n            const options = { capture: true, passive: true };\n            this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options);\n            this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options);\n            this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);\n            this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);\n\n            if (document.contains(this._canvas)) {\n                document.body.removeChild(this._canvas);\n            }\n        }\n\n        this._target = null;\n    }\n\n    change(rgba, hotx, hoty, w, h) {\n        if ((w === 0) || (h === 0)) {\n            this.clear();\n            return;\n        }\n\n        this._position.x = this._position.x + this._hotSpot.x - hotx;\n        this._position.y = this._position.y + this._hotSpot.y - hoty;\n        this._hotSpot.x = hotx;\n        this._hotSpot.y = hoty;\n\n        let ctx = this._canvas.getContext('2d');\n\n        this._canvas.width = w;\n        this._canvas.height = h;\n\n        let img = new ImageData(new Uint8ClampedArray(rgba), w, h);\n        ctx.clearRect(0, 0, w, h);\n        ctx.putImageData(img, 0, 0);\n\n        if (useFallback) {\n            this._updatePosition();\n        } else {\n            let url = this._canvas.toDataURL();\n            this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';\n        }\n    }\n\n    clear() {\n        this._target.style.cursor = 'none';\n        this._canvas.width = 0;\n        this._canvas.height = 0;\n        this._position.x = this._position.x + this._hotSpot.x;\n        this._position.y = this._position.y + this._hotSpot.y;\n        this._hotSpot.x = 0;\n        this._hotSpot.y = 0;\n    }\n\n    // Mouse events might be emulated, this allows\n    // moving the cursor in such cases\n    move(clientX, clientY) {\n        if (!useFallback) {\n            return;\n        }\n        // clientX/clientY are relative the _visual viewport_,\n        // but our position is relative the _layout viewport_,\n        // so try to compensate when we can\n        if (window.visualViewport) {\n            this._position.x = clientX + window.visualViewport.offsetLeft;\n            this._position.y = clientY + window.visualViewport.offsetTop;\n        } else {\n            this._position.x = clientX;\n            this._position.y = clientY;\n        }\n        this._updatePosition();\n        let target = document.elementFromPoint(clientX, clientY);\n        this._updateVisibility(target);\n    }\n\n    _handleMouseOver(event) {\n        // This event could be because we're entering the target, or\n        // moving around amongst its sub elements. Let the move handler\n        // sort things out.\n        this._handleMouseMove(event);\n    }\n\n    _handleMouseLeave(event) {\n        // Check if we should show the cursor on the element we are leaving to\n        this._updateVisibility(event.relatedTarget);\n    }\n\n    _handleMouseMove(event) {\n        this._updateVisibility(event.target);\n\n        this._position.x = event.clientX - this._hotSpot.x;\n        this._position.y = event.clientY - this._hotSpot.y;\n\n        this._updatePosition();\n    }\n\n    _handleMouseUp(event) {\n        // We might get this event because of a drag operation that\n        // moved outside of the target. Check what's under the cursor\n        // now and adjust visibility based on that.\n        let target = document.elementFromPoint(event.clientX, event.clientY);\n        this._updateVisibility(target);\n\n        // Captures end with a mouseup but we can't know the event order of\n        // mouseup vs releaseCapture.\n        //\n        // In the cases when releaseCapture comes first, the code above is\n        // enough.\n        //\n        // In the cases when the mouseup comes first, we need wait for the\n        // browser to flush all events and then check again if the cursor\n        // should be visible.\n        if (this._captureIsActive()) {\n            window.setTimeout(() => {\n                // We might have detached at this point\n                if (!this._target) {\n                    return;\n                }\n                // Refresh the target from elementFromPoint since queued events\n                // might have altered the DOM\n                target = document.elementFromPoint(event.clientX,\n                                                   event.clientY);\n                this._updateVisibility(target);\n            }, 0);\n        }\n    }\n\n    _showCursor() {\n        if (this._canvas.style.visibility === 'hidden') {\n            this._canvas.style.visibility = '';\n        }\n    }\n\n    _hideCursor() {\n        if (this._canvas.style.visibility !== 'hidden') {\n            this._canvas.style.visibility = 'hidden';\n        }\n    }\n\n    // Should we currently display the cursor?\n    // (i.e. are we over the target, or a child of the target without a\n    // different cursor set)\n    _shouldShowCursor(target) {\n        if (!target) {\n            return false;\n        }\n        // Easy case\n        if (target === this._target) {\n            return true;\n        }\n        // Other part of the DOM?\n        if (!this._target.contains(target)) {\n            return false;\n        }\n        // Has the child its own cursor?\n        // FIXME: How can we tell that a sub element has an\n        //        explicit \"cursor: none;\"?\n        if (window.getComputedStyle(target).cursor !== 'none') {\n            return false;\n        }\n        return true;\n    }\n\n    _updateVisibility(target) {\n        // When the cursor target has capture we want to show the cursor.\n        // So, if a capture is active - look at the captured element instead.\n        if (this._captureIsActive()) {\n            target = document.captureElement;\n        }\n        if (this._shouldShowCursor(target)) {\n            this._showCursor();\n        } else {\n            this._hideCursor();\n        }\n    }\n\n    _updatePosition() {\n        this._canvas.style.left = this._position.x + \"px\";\n        this._canvas.style.top = this._position.y + \"px\";\n    }\n\n    _captureIsActive() {\n        return document.captureElement &&\n            document.documentElement.contains(document.captureElement);\n    }\n}\n"
  },
  {
    "path": "core/util/element.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2020 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n */\n\n/*\n * HTML element utility functions\n */\n\nexport function clientToElement(x, y, elem) {\n    const bounds = elem.getBoundingClientRect();\n    let pos = { x: 0, y: 0 };\n    // Clip to target bounds\n    if (x < bounds.left) {\n        pos.x = 0;\n    } else if (x >= bounds.right) {\n        pos.x = bounds.width - 1;\n    } else {\n        pos.x = x - bounds.left;\n    }\n    if (y < bounds.top) {\n        pos.y = 0;\n    } else if (y >= bounds.bottom) {\n        pos.y = bounds.height - 1;\n    } else {\n        pos.y = y - bounds.top;\n    }\n    return pos;\n}\n"
  },
  {
    "path": "core/util/events.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2018 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n */\n\n/*\n * Cross-browser event and position routines\n */\n\nexport function getPointerEvent(e) {\n    return e.changedTouches ? e.changedTouches[0] : e.touches ? e.touches[0] : e;\n}\n\nexport function stopEvent(e) {\n    e.stopPropagation();\n    e.preventDefault();\n}\n\n// Emulate Element.setCapture() when not supported\nlet _captureRecursion = false;\nlet _elementForUnflushedEvents = null;\ndocument.captureElement = null;\nfunction _captureProxy(e) {\n    // Recursion protection as we'll see our own event\n    if (_captureRecursion) return;\n\n    // Clone the event as we cannot dispatch an already dispatched event\n    const newEv = new e.constructor(e.type, e);\n\n    _captureRecursion = true;\n    if (document.captureElement) {\n        document.captureElement.dispatchEvent(newEv);\n    } else {\n        _elementForUnflushedEvents.dispatchEvent(newEv);\n    }\n    _captureRecursion = false;\n\n    // Avoid double events\n    e.stopPropagation();\n\n    // Respect the wishes of the redirected event handlers\n    if (newEv.defaultPrevented) {\n        e.preventDefault();\n    }\n\n    // Implicitly release the capture on button release\n    if (e.type === \"mouseup\") {\n        releaseCapture();\n    }\n}\n\n// Follow cursor style of target element\nfunction _capturedElemChanged() {\n    const proxyElem = document.getElementById(\"noVNC_mouse_capture_elem\");\n    proxyElem.style.cursor = window.getComputedStyle(document.captureElement).cursor;\n}\n\nconst _captureObserver = new MutationObserver(_capturedElemChanged);\n\nexport function setCapture(target) {\n    if (target.setCapture) {\n\n        target.setCapture();\n        document.captureElement = target;\n    } else {\n        // Release any existing capture in case this method is\n        // called multiple times without coordination\n        releaseCapture();\n\n        let proxyElem = document.getElementById(\"noVNC_mouse_capture_elem\");\n\n        if (proxyElem === null) {\n            proxyElem = document.createElement(\"div\");\n            proxyElem.id = \"noVNC_mouse_capture_elem\";\n            proxyElem.style.position = \"fixed\";\n            proxyElem.style.top = \"0px\";\n            proxyElem.style.left = \"0px\";\n            proxyElem.style.width = \"100%\";\n            proxyElem.style.height = \"100%\";\n            proxyElem.style.zIndex = 10000;\n            proxyElem.style.display = \"none\";\n            document.body.appendChild(proxyElem);\n\n            // This is to make sure callers don't get confused by having\n            // our blocking element as the target\n            proxyElem.addEventListener('contextmenu', _captureProxy);\n\n            proxyElem.addEventListener('mousemove', _captureProxy);\n            proxyElem.addEventListener('mouseup', _captureProxy);\n        }\n\n        document.captureElement = target;\n\n        // Track cursor and get initial cursor\n        _captureObserver.observe(target, {attributes: true});\n        _capturedElemChanged();\n\n        proxyElem.style.display = \"\";\n\n        // We listen to events on window in order to keep tracking if it\n        // happens to leave the viewport\n        window.addEventListener('mousemove', _captureProxy);\n        window.addEventListener('mouseup', _captureProxy);\n    }\n}\n\nexport function releaseCapture() {\n    if (document.releaseCapture) {\n\n        document.releaseCapture();\n        document.captureElement = null;\n\n    } else {\n        if (!document.captureElement) {\n            return;\n        }\n\n        // There might be events already queued. The event proxy needs\n        // access to the captured element for these queued events.\n        // E.g. contextmenu (right-click) in Microsoft Edge\n        //\n        // Before removing the capturedElem pointer we save it to a\n        // temporary variable that the unflushed events can use.\n        _elementForUnflushedEvents = document.captureElement;\n        document.captureElement = null;\n\n        _captureObserver.disconnect();\n\n        const proxyElem = document.getElementById(\"noVNC_mouse_capture_elem\");\n        proxyElem.style.display = \"none\";\n\n        window.removeEventListener('mousemove', _captureProxy);\n        window.removeEventListener('mouseup', _captureProxy);\n    }\n}\n"
  },
  {
    "path": "core/util/eventtarget.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2019 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n */\n\nexport default class EventTargetMixin {\n    constructor() {\n        this._listeners = new Map();\n    }\n\n    addEventListener(type, callback) {\n        if (!this._listeners.has(type)) {\n            this._listeners.set(type, new Set());\n        }\n        this._listeners.get(type).add(callback);\n    }\n\n    removeEventListener(type, callback) {\n        if (this._listeners.has(type)) {\n            this._listeners.get(type).delete(callback);\n        }\n    }\n\n    dispatchEvent(event) {\n        if (!this._listeners.has(event.type)) {\n            return true;\n        }\n        this._listeners.get(event.type)\n            .forEach(callback => callback.call(this, event));\n        return !event.defaultPrevented;\n    }\n}\n"
  },
  {
    "path": "core/util/int.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2020 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n */\n\nexport function toUnsigned32bit(toConvert) {\n    return toConvert >>> 0;\n}\n\nexport function toSigned32bit(toConvert) {\n    return toConvert | 0;\n}\n"
  },
  {
    "path": "core/util/logging.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2019 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n */\n\n/*\n * Logging/debug routines\n */\n\nlet _logLevel = 'warn';\n\nlet Debug = () => {};\nlet Info = () => {};\nlet Warn = () => {};\nlet Error = () => {};\n\nexport function initLogging(level) {\n    if (typeof level === 'undefined') {\n        level = _logLevel;\n    } else {\n        _logLevel = level;\n    }\n\n    Debug = Info = Warn = Error = () => {};\n\n    if (typeof window.console !== \"undefined\") {\n        /* eslint-disable no-console, no-fallthrough */\n        switch (level) {\n            case 'debug':\n                Debug = console.debug.bind(window.console);\n            case 'info':\n                Info  = console.info.bind(window.console);\n            case 'warn':\n                Warn  = console.warn.bind(window.console);\n            case 'error':\n                Error = console.error.bind(window.console);\n            case 'none':\n                break;\n            default:\n                throw new window.Error(\"invalid logging type '\" + level + \"'\");\n        }\n        /* eslint-enable no-console, no-fallthrough */\n    }\n}\n\nexport function getLogging() {\n    return _logLevel;\n}\n\nexport { Debug, Info, Warn, Error };\n\n// Initialize logging level\ninitLogging();\n"
  },
  {
    "path": "core/util/strings.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2019 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * See README.md for usage and integration instructions.\n */\n\n// Decode from UTF-8\nexport function decodeUTF8(utf8string, allowLatin1=false) {\n    try {\n        return decodeURIComponent(escape(utf8string));\n    } catch (e) {\n        if (e instanceof URIError) {\n            if (allowLatin1) {\n                // If we allow Latin1 we can ignore any decoding fails\n                // and in these cases return the original string\n                return utf8string;\n            }\n        }\n        throw e;\n    }\n}\n\n// Encode to UTF-8\nexport function encodeUTF8(DOMString) {\n    return unescape(encodeURIComponent(DOMString));\n}\n"
  },
  {
    "path": "core/websock.js",
    "content": "/*\n * Websock: high-performance buffering wrapper\n * Copyright (C) 2019 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n *\n * Websock is similar to the standard WebSocket / RTCDataChannel object\n * but with extra buffer handling.\n *\n * Websock has built-in receive queue buffering; the message event\n * does not contain actual data but is simply a notification that\n * there is new data available. Several rQ* methods are available to\n * read binary data off of the receive queue.\n */\n\nimport * as Log from './util/logging.js';\n\n// this has performance issues in some versions Chromium, and\n// doesn't gain a tremendous amount of performance increase in Firefox\n// at the moment.  It may be valuable to turn it on in the future.\nconst MAX_RQ_GROW_SIZE = 40 * 1024 * 1024;  // 40 MiB\n\n// Constants pulled from RTCDataChannelState enum\n// https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/readyState#RTCDataChannelState_enum\nconst DataChannel = {\n    CONNECTING: \"connecting\",\n    OPEN: \"open\",\n    CLOSING: \"closing\",\n    CLOSED: \"closed\"\n};\n\nconst ReadyStates = {\n    CONNECTING: [WebSocket.CONNECTING, DataChannel.CONNECTING],\n    OPEN: [WebSocket.OPEN, DataChannel.OPEN],\n    CLOSING: [WebSocket.CLOSING, DataChannel.CLOSING],\n    CLOSED: [WebSocket.CLOSED, DataChannel.CLOSED],\n};\n\n// Properties a raw channel must have, WebSocket and RTCDataChannel are two examples\nconst rawChannelProps = [\n    \"send\",\n    \"close\",\n    \"binaryType\",\n    \"onerror\",\n    \"onmessage\",\n    \"onopen\",\n    \"protocol\",\n    \"readyState\",\n];\n\nexport default class Websock {\n    constructor() {\n        this._websocket = null;  // WebSocket or RTCDataChannel object\n\n        this._rQi = 0;           // Receive queue index\n        this._rQlen = 0;         // Next write position in the receive queue\n        this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB)\n        // called in init: this._rQ = new Uint8Array(this._rQbufferSize);\n        this._rQ = null; // Receive queue\n\n        this._sQbufferSize = 1024 * 10;  // 10 KiB\n        // called in init: this._sQ = new Uint8Array(this._sQbufferSize);\n        this._sQlen = 0;\n        this._sQ = null;  // Send queue\n\n        this._eventHandlers = {\n            message: () => {},\n            open: () => {},\n            close: () => {},\n            error: () => {}\n        };\n    }\n\n    // Getters and setters\n\n    get readyState() {\n        let subState;\n\n        if (this._websocket === null) {\n            return \"unused\";\n        }\n\n        subState = this._websocket.readyState;\n\n        if (ReadyStates.CONNECTING.includes(subState)) {\n            return \"connecting\";\n        } else if (ReadyStates.OPEN.includes(subState)) {\n            return \"open\";\n        } else if (ReadyStates.CLOSING.includes(subState)) {\n            return \"closing\";\n        } else if (ReadyStates.CLOSED.includes(subState)) {\n            return \"closed\";\n        }\n\n        return \"unknown\";\n    }\n\n    // Receive queue\n    rQpeek8() {\n        return this._rQ[this._rQi];\n    }\n\n    rQskipBytes(bytes) {\n        this._rQi += bytes;\n    }\n\n    rQshift8() {\n        return this._rQshift(1);\n    }\n\n    rQshift16() {\n        return this._rQshift(2);\n    }\n\n    rQshift32() {\n        return this._rQshift(4);\n    }\n\n    // TODO(directxman12): test performance with these vs a DataView\n    _rQshift(bytes) {\n        let res = 0;\n        for (let byte = bytes - 1; byte >= 0; byte--) {\n            res += this._rQ[this._rQi++] << (byte * 8);\n        }\n        return res >>> 0;\n    }\n\n    rQlen() {\n        return this._rQlen - this._rQi;\n    }\n\n    rQshiftStr(len) {\n        let str = \"\";\n        // Handle large arrays in steps to avoid long strings on the stack\n        for (let i = 0; i < len; i += 4096) {\n            let part = this.rQshiftBytes(Math.min(4096, len - i), false);\n            str += String.fromCharCode.apply(null, part);\n        }\n        return str;\n    }\n\n    rQshiftBytes(len, copy=true) {\n        this._rQi += len;\n        if (copy) {\n            return this._rQ.slice(this._rQi - len, this._rQi);\n        } else {\n            return this._rQ.subarray(this._rQi - len, this._rQi);\n        }\n    }\n\n    rQshiftTo(target, len) {\n        // TODO: make this just use set with views when using a ArrayBuffer to store the rQ\n        target.set(new Uint8Array(this._rQ.buffer, this._rQi, len));\n        this._rQi += len;\n    }\n\n    rQpeekBytes(len, copy=true) {\n        if (copy) {\n            return this._rQ.slice(this._rQi, this._rQi + len);\n        } else {\n            return this._rQ.subarray(this._rQi, this._rQi + len);\n        }\n    }\n\n    // Check to see if we must wait for 'num' bytes (default to FBU.bytes)\n    // to be available in the receive queue. Return true if we need to\n    // wait (and possibly print a debug message), otherwise false.\n    rQwait(msg, num, goback) {\n        if (this._rQlen - this._rQi < num) {\n            if (goback) {\n                if (this._rQi < goback) {\n                    throw new Error(\"rQwait cannot backup \" + goback + \" bytes\");\n                }\n                this._rQi -= goback;\n            }\n            return true; // true means need more data\n        }\n        return false;\n    }\n\n    // Send queue\n\n    sQpush8(num) {\n        this._sQensureSpace(1);\n        this._sQ[this._sQlen++] = num;\n    }\n\n    sQpush16(num) {\n        this._sQensureSpace(2);\n        this._sQ[this._sQlen++] = (num >> 8) & 0xff;\n        this._sQ[this._sQlen++] = (num >> 0) & 0xff;\n    }\n\n    sQpush32(num) {\n        this._sQensureSpace(4);\n        this._sQ[this._sQlen++] = (num >> 24) & 0xff;\n        this._sQ[this._sQlen++] = (num >> 16) & 0xff;\n        this._sQ[this._sQlen++] = (num >>  8) & 0xff;\n        this._sQ[this._sQlen++] = (num >>  0) & 0xff;\n    }\n\n    sQpushString(str) {\n        let bytes = str.split('').map(chr => chr.charCodeAt(0));\n        this.sQpushBytes(new Uint8Array(bytes));\n    }\n\n    sQpushBytes(bytes) {\n        for (let offset = 0;offset < bytes.length;) {\n            this._sQensureSpace(1);\n\n            let chunkSize = this._sQbufferSize - this._sQlen;\n            if (chunkSize > bytes.length - offset) {\n                chunkSize = bytes.length - offset;\n            }\n\n            this._sQ.set(bytes.subarray(offset, offset + chunkSize), this._sQlen);\n            this._sQlen += chunkSize;\n            offset += chunkSize;\n        }\n    }\n\n    flush() {\n        if (this._sQlen > 0 && this.readyState === 'open') {\n            this._websocket.send(new Uint8Array(this._sQ.buffer, 0, this._sQlen));\n            this._sQlen = 0;\n        }\n    }\n\n    _sQensureSpace(bytes) {\n        if (this._sQbufferSize - this._sQlen < bytes) {\n            this.flush();\n        }\n    }\n\n    // Event handlers\n    off(evt) {\n        this._eventHandlers[evt] = () => {};\n    }\n\n    on(evt, handler) {\n        this._eventHandlers[evt] = handler;\n    }\n\n    _allocateBuffers() {\n        this._rQ = new Uint8Array(this._rQbufferSize);\n        this._sQ = new Uint8Array(this._sQbufferSize);\n    }\n\n    init() {\n        this._allocateBuffers();\n        this._rQi = 0;\n        this._websocket = null;\n    }\n\n    open(uri, protocols) {\n        this.attach(new WebSocket(uri, protocols));\n    }\n\n    attach(rawChannel) {\n        this.init();\n\n        // Must get object and class methods to be compatible with the tests.\n        const channelProps = [...Object.keys(rawChannel), ...Object.getOwnPropertyNames(Object.getPrototypeOf(rawChannel))];\n        for (let i = 0; i < rawChannelProps.length; i++) {\n            const prop = rawChannelProps[i];\n            if (channelProps.indexOf(prop) < 0) {\n                throw new Error('Raw channel missing property: ' + prop);\n            }\n        }\n\n        this._websocket = rawChannel;\n        this._websocket.binaryType = \"arraybuffer\";\n        this._websocket.onmessage = this._recvMessage.bind(this);\n\n        this._websocket.onopen = () => {\n            Log.Debug('>> WebSock.onopen');\n            if (this._websocket.protocol) {\n                Log.Info(\"Server choose sub-protocol: \" + this._websocket.protocol);\n            }\n\n            this._eventHandlers.open();\n            Log.Debug(\"<< WebSock.onopen\");\n        };\n\n        this._websocket.onclose = (e) => {\n            Log.Debug(\">> WebSock.onclose\");\n            this._eventHandlers.close(e);\n            Log.Debug(\"<< WebSock.onclose\");\n        };\n\n        this._websocket.onerror = (e) => {\n            Log.Debug(\">> WebSock.onerror: \" + e);\n            this._eventHandlers.error(e);\n            Log.Debug(\"<< WebSock.onerror: \" + e);\n        };\n    }\n\n    close() {\n        if (this._websocket) {\n            if (this.readyState === 'connecting' ||\n                this.readyState === 'open') {\n                Log.Info(\"Closing WebSocket connection\");\n                this._websocket.close();\n            }\n\n            this._websocket.onmessage = () => {};\n        }\n    }\n\n    // private methods\n\n    // We want to move all the unread data to the start of the queue,\n    // e.g. compacting.\n    // The function also expands the receive que if needed, and for\n    // performance reasons we combine these two actions to avoid\n    // unnecessary copying.\n    _expandCompactRQ(minFit) {\n        // if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place\n        // instead of resizing\n        const requiredBufferSize =  (this._rQlen - this._rQi + minFit) * 8;\n        const resizeNeeded = this._rQbufferSize < requiredBufferSize;\n\n        if (resizeNeeded) {\n            // Make sure we always *at least* double the buffer size, and have at least space for 8x\n            // the current amount of data\n            this._rQbufferSize = Math.max(this._rQbufferSize * 2, requiredBufferSize);\n        }\n\n        // we don't want to grow unboundedly\n        if (this._rQbufferSize > MAX_RQ_GROW_SIZE) {\n            this._rQbufferSize = MAX_RQ_GROW_SIZE;\n            if (this._rQbufferSize - (this._rQlen - this._rQi) < minFit) {\n                throw new Error(\"Receive queue buffer exceeded \" + MAX_RQ_GROW_SIZE + \" bytes, and the new message could not fit\");\n            }\n        }\n\n        if (resizeNeeded) {\n            const oldRQbuffer = this._rQ.buffer;\n            this._rQ = new Uint8Array(this._rQbufferSize);\n            this._rQ.set(new Uint8Array(oldRQbuffer, this._rQi, this._rQlen - this._rQi));\n        } else {\n            this._rQ.copyWithin(0, this._rQi, this._rQlen);\n        }\n\n        this._rQlen = this._rQlen - this._rQi;\n        this._rQi = 0;\n    }\n\n    // push arraybuffer values onto the end of the receive que\n    _recvMessage(e) {\n        if (this._rQlen == this._rQi) {\n            // All data has now been processed, this means we\n            // can reset the receive queue.\n            this._rQlen = 0;\n            this._rQi = 0;\n        }\n        const u8 = new Uint8Array(e.data);\n        if (u8.length > this._rQbufferSize - this._rQlen) {\n            this._expandCompactRQ(u8.length);\n        }\n        this._rQ.set(u8, this._rQlen);\n        this._rQlen += u8.length;\n\n        if (this._rQlen - this._rQi > 0) {\n            this._eventHandlers.message();\n        } else {\n            Log.Debug(\"Ignoring empty message\");\n        }\n    }\n}\n"
  },
  {
    "path": "defaults.json",
    "content": "{}\n"
  },
  {
    "path": "docs/API-internal.md",
    "content": "# 1. Internal modules\n\nThe noVNC client is composed of several internal modules that handle\nrendering, input, networking, etc. Each of the modules is designed to\nbe cross-browser and independent from each other.\n\nNote however that the API of these modules is not guaranteed to be\nstable, and this documentation is not maintained as well as the\nofficial external API.\n\n\n## 1.1 Module list\n\n* __Keyboard__ (core/input/keyboard.js): Keyboard input event handler with\nnon-US keyboard support. Translates keyDown and keyUp events to X11\nkeysym values.\n\n* __Display__ (core/display.js): Efficient 2D rendering abstraction\nlayered on the HTML5 canvas element.\n\n* __Clipboard__ (core/clipboard.js): Clipboard event handler.\n\n* __Websock__ (core/websock.js): Websock client from websockify\nwith transparent binary data support.\n[Websock API](https://github.com/novnc/websockify-js/wiki/websock.js) wiki page.\n\n\n## 1.2 Callbacks\n\nFor the Mouse, Keyboard, Display, and Clipboard objects, the callback\nfunctions are assigned to configuration attributes, just as for the RFB\nobject. The WebSock module has a method named 'on' that takes two\nparameters: the callback event name, and the callback function.\n\n## 2. Modules\n\n## 2.1 Keyboard module\n\n### 2.1.1 Configuration attributes\n\nNone\n\n### 2.1.2 Methods\n\n| name   | parameters | description\n| ------ | ---------- | ------------\n| grab   | ()         | Begin capturing keyboard events\n| ungrab | ()         | Stop capturing keyboard events\n\n### 2.1.3 Callbacks\n\n| name       | parameters           | description\n| ---------- | -------------------- | ------------\n| onkeypress | (keysym, code, down) | Handler for key press/release\n\n\n## 2.2 Display module\n\n### 2.2.1 Configuration attributes\n\n| name         | type  | mode | default | description\n| ------------ | ----- | ---- | ------- | ------------\n| scale        | float | RW   | 1.0     | Display area scale factor 0.0 - 1.0\n| clipViewport | bool  | RW   | false   | Use viewport clipping\n| width        | int   | RO   |         | Display area width\n| height       | int   | RO   |         | Display area height\n\n### 2.2.2 Methods\n\n| name               | parameters                                              | description\n| ------------------ | ------------------------------------------------------- | ------------\n| viewportChangePos  | (deltaX, deltaY)                                        | Move the viewport relative to the current location\n| viewportChangeSize | (width, height)                                         | Change size of the viewport\n| absX               | (x)                                                     | Return X relative to the remote display\n| absY               | (y)                                                     | Return Y relative to the remote display\n| resize             | (width, height)                                         | Set width and height\n| flip               | (from_queue)                                            | Update the visible canvas with the contents of the rendering canvas\n| pending            | ()                                                      | Check if there are waiting items in the render queue\n| flush              | ()                                                      | Resume processing the render queue unless it's empty\n| fillRect           | (x, y, width, height, color, from_queue)                | Draw a filled in rectangle\n| copyImage          | (old_x, old_y, new_x, new_y, width, height, from_queue) | Copy a rectangular area\n| imageRect          | (x, y, width, height, mime, arr)                        | Draw a rectangle with an image\n| blitImage          | (x, y, width, height, arr, offset, from_queue)          | Blit pixels (of R,G,B,A) to the display\n| drawImage          | (img, x, y)                                             | Draw image and track damage\n| autoscale          | (containerWidth, containerHeight)                       | Scale the display\n\n## 2.3 Clipboard module\n\n### 2.3.1 Configuration attributes\n\nNone\n\n### 2.3.2 Methods\n\n| name               | parameters        | description\n| ------------------ | ----------------- | ------------\n| writeClipboard     | (text)            | An async write text to clipboard\n| grab               | ()                | Begin capturing clipboard events\n| ungrab             | ()                | Stop capturing clipboard events\n\n### 2.3.3 Callbacks\n\n| name    | parameters | description\n| ------- | ---------- | ------------\n| onpaste | (text)     | Called following a target focus event and an async clipboard read\n"
  },
  {
    "path": "docs/API.md",
    "content": "# noVNC API\n\nThe interface of the noVNC client consists of a single RFB object that\nis instantiated once per connection.\n\n## RFB\n\nThe `RFB` object represents a single connection to a VNC server. It\ncommunicates using a WebSocket that must provide a standard RFB\nprotocol stream.\n\n### Constructor\n\n[`RFB()`](#rfb-1)\n  - Creates and returns a new `RFB` object.\n\n### Properties\n\n`background`\n  - Is a valid CSS [background][mdn-bg] style value indicating which\n    background style should be applied to the element containing the\n    remote session screen. The default value is `rgb(40, 40, 40)` (solid\n    gray color).\n\n[mdn-bg]: https://developer.mozilla.org/en-US/docs/Web/CSS/background\n\n`capabilities` *Read only*\n  - Is an `Object` indicating which optional extensions are available\n    on the server. Some methods may only be called if the corresponding\n    capability is set. The following capabilities are defined:\n\n    | name     | type      | description\n    | -------- | --------- | -----------\n    | `power`  | `boolean` | Machine power control is available\n\n`clippingViewport` *Read only*\n  - Is a `boolean` indicating if the remote session is currently being\n    clipped to its container. Only relevant if `clipViewport` is\n    enabled.\n\n`clipViewport`\n  - Is a `boolean` indicating if the remote session should be clipped\n    to its container. When disabled scrollbars will be shown to handle\n    the resulting overflow. Disabled by default.\n\n`compressionLevel`\n  - Is an `int` in range `[0-9]` controlling the desired compression\n    level. Value `0` means no compression. Level 1 uses a minimum of CPU\n    resources and achieves weak compression ratios, while level 9 offers\n    best compression but is slow in terms of CPU consumption on the server\n    side. Use high levels with very slow network connections.\n    Default value is `2`.\n\n`dragViewport`\n  - Is a `boolean` indicating if mouse events should control the\n    relative position of a clipped remote session. Only relevant if\n    `clipViewport` is enabled. Disabled by default.\n\n`focusOnClick`\n  - Is a `boolean` indicating if keyboard focus should automatically be\n    moved to the remote session when a `mousedown` or `touchstart`\n    event is received. Enabled by default.\n\n`qualityLevel`\n  - Is an `int` in range `[0-9]` controlling the desired JPEG quality.\n    Value `0` implies low quality and `9` implies high quality.\n    Default value is `6`.\n\n`resizeSession`\n  - Is a `boolean` indicating if a request to resize the remote session\n    should be sent whenever the container changes dimensions. Disabled\n    by default.\n\n`scaleViewport`\n  - Is a `boolean` indicating if the remote session should be scaled\n    locally so it fits its container. When disabled it will be centered\n    if the remote session is smaller than its container, or handled\n    according to `clipViewport` if it is larger. Disabled by default.\n\n`viewOnly`\n  - Is a `boolean` indicating if any events (e.g. key presses or mouse\n    movement) should be prevented from being sent to the server.\n    Disabled by default.\n\n### Events\n\n[`bell`](#bell)\n  - The `bell` event is fired when a audible bell request is received\n    from the server.\n\n[`capabilities`](#capabilities)\n  - The `capabilities` event is fired when `RFB.capabilities` is\n    updated.\n\n[`clipboard`](#clipboard)\n  - The `clipboard` event is fired when clipboard data is received from\n    the server.\n\n[`clippingviewport`](#clippingviewport)\n  - The `clippingviewport` event is fired when `RFB.clippingViewport` is\n    updated.\n\n[`connect`](#connect)\n  - The `connect` event is fired when the `RFB` object has completed\n    the connection and handshaking with the server.\n\n[`credentialsrequired`](#credentialsrequired)\n  - The `credentialsrequired` event is fired when more credentials must\n    be given to continue.\n\n[`desktopname`](#desktopname)\n  - The `desktopname` event is fired when the remote desktop name\n    changes.\n\n[`disconnect`](#disconnect)\n  - The `disconnect` event is fired when the `RFB` object disconnects.\n\n[`securityfailure`](#securityfailure)\n  - The `securityfailure` event is fired when the security negotiation\n    with the server fails.\n\n[`serververification`](#serververification)\n  - The `serververification` event is fired when the server identity\n    must be confirmed by the user.\n\n### Methods\n\n[`RFB.approveServer()`](#rfbapproveserver)\n  - Proceed connecting to the server. Should be called after the\n    [`serververification`](#serververification) event has fired and the\n    user has verified the identity of the server.\n\n[`RFB.blur()`](#rfbblur)\n  - Remove keyboard focus from the remote session.\n\n[`RFB.clipboardPasteFrom()`](#rfbclipboardpastefrom)\n  - Send clipboard contents to server.\n\n[`RFB.disconnect()`](#rfbdisconnect)\n  - Disconnect from the server.\n\n[`RFB.focus()`](#rfbfocus)\n  - Move keyboard focus to the remote session.\n\n[`RFB.getImageData()`](#rfbgetimagedata)\n  - Return the current content of the screen as an ImageData array.\n\n[`RFB.machineReboot()`](#rfbmachinereboot)\n  - Request a reboot of the remote machine.\n\n[`RFB.machineReset()`](#rfbmachinereset)\n  - Request a reset of the remote machine.\n\n[`RFB.machineShutdown()`](#rfbmachineshutdown)\n  - Request a shutdown of the remote machine.\n\n[`RFB.sendCredentials()`](#rfbsendcredentials)\n  - Send credentials to server. Should be called after the\n    [`credentialsrequired`](#credentialsrequired) event has fired.\n\n[`RFB.sendCtrlAltDel()`](#rfbsendctrlaltdel)\n  - Send Ctrl-Alt-Del key sequence.\n\n[`RFB.sendKey()`](#rfbsendkey)\n  - Send a key event.\n\n[`RFB.toBlob()`](#rfbtoblob)\n  - Return the current content of the screen as Blob encoded image file.\n\n[`RFB.toDataURL()`](#rfbtodataurl)\n  - Return the current content of the screen as data-url encoded image file.\n\n### Details\n\n#### RFB()\n\nThe `RFB()` constructor returns a new `RFB` object and initiates a new\nconnection to a specified VNC server.\n\n##### Syntax\n\n```js\nnew RFB(target, urlOrChannel);\nnew RFB(target, urlOrChannel, options);\n```\n\n###### Parameters\n\n**`target`**\n  - A block [`HTMLElement`][mdn-elem] that specifies where the `RFB`\n    object should attach itself. The existing contents of the\n    `HTMLElement` will be untouched, but new elements will be added\n    during the lifetime of the `RFB` object.\n\n[mdn-elem]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement\n\n**`urlOrChannel`**\n  - A `DOMString` specifying the VNC server to connect to. This must be\n    a valid WebSocket URL. This can also be a `WebSocket` or `RTCDataChannel`.\n\n**`options`** *Optional*\n  - An `Object` specifying extra details about how the connection\n    should be made.\n\n    Possible options:\n\n    `shared`\n      - A `boolean` indicating if the remote server should be shared or\n        if any other connected clients should be disconnected. Enabled\n        by default.\n\n    `credentials`\n      - An `Object` specifying the credentials to provide to the server\n        when authenticating. The following credentials are possible:\n\n        | name         | type        | description\n        | ------------ | ----------- | -----------\n        | `\"username\"` | `DOMString` | The user that authenticates\n        | `\"password\"` | `DOMString` | Password for the user\n        | `\"target\"`   | `DOMString` | Target machine or session\n\n    `repeaterID`\n      - A `DOMString` specifying the ID to provide to any VNC repeater\n        encountered.\n\n    `wsProtocols`\n      - An `Array` of `DOMString`s specifying the sub-protocols to use\n        in the WebSocket connection. Empty by default.\n\n#### bell\n\nThe `bell` event is fired when the server has requested an audible\nbell.\n\n#### capabilities\n\nThe `capabilities` event is fired whenever an entry is added or removed\nfrom `RFB.capabilities`. The `detail` property is an `Object` with the\nproperty `capabilities` containing the new value of `RFB.capabilities`.\n\n#### clippingviewport\n\nThe `clippingviewport` event is fired whenever `RFB.clippingViewport`\nchanges between `true` and `false`. The `detail` property is a `boolean`\nwith the new value of `RFB.clippingViewport`.\n\n#### clipboard\n\nThe `clipboard` event is fired when the server has sent clipboard data.\nThe `detail` property is an `Object` containing the property `text`\nwhich is a `DOMString` with the clipboard data.\n\n#### credentialsrequired\n\nThe `credentialsrequired` event is fired when the server requests more\ncredentials than were specified to [`RFB()`](#rfb-1). The `detail`\nproperty is an `Object` containing the property `types` which is an\n`Array` of `DOMString` listing the credentials that are required.\n\n#### connect\n\nThe `connect` event is fired after all the handshaking with the server\nis completed and the connection is fully established. After this event\nthe `RFB` object is ready to recieve graphics updates and to send input.\n\n#### desktopname\n\nThe `desktopname` event is fired when the name of the remote desktop\nchanges. The `detail` property is an `Object` with the property `name`\nwhich is a `DOMString` specifying the new name.\n\n#### disconnect\n\nThe `disconnect` event is fired when the connection has been\nterminated. The `detail` property is an `Object` that contains the\nproperty `clean`. `clean` is a `boolean` indicating if the termination\nwas clean or not. In the event of an unexpected termination or an error\n`clean` will be set to false.\n\n#### securityfailure\n\nThe `securityfailure` event is fired when the handshaking process with\nthe server fails during the security negotiation step. The `detail`\nproperty is an `Object` containing the following properties:\n\n| Property | Type        | Description\n| -------- | ----------- | -----------\n| `status` | `long`      | The failure status code\n| `reason` | `DOMString` | The **optional** reason for the failure\n\nThe property `status` corresponds to the [SecurityResult][rfb-secresult]\nstatus code in cases of failure. A status of zero will not be sent in\nthis event since that indicates a successful security handshaking\nprocess. The optional property `reason` is provided by the server and\nthus the language of the string is not known. However most servers will\nprobably send English strings. The server can choose to not send a\nreason and in these cases the `reason` property will be omitted.\n\n[rfb-secresult]: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#securityresult\n\n#### serververification\n\nThe `serververification` event is fired when the server provides\ninformation that allows the user to verify that it is the correct server\nand protect against a man-in-the-middle attack. The `detail` property is\nan `Object` containing the property `type` which is a `DOMString`\nspecifying which type of information the server has provided. Other\nproperties are also available, depending on the value of `type`:\n\n`\"RSA\"`\n - The server identity is verified using just a RSA key. The property\n   `publickey` is a `Uint8Array` containing the public key in a unsigned\n   big endian representation.\n\n#### RFB.approveServer()\n\nThe `RFB.approveServer()` method is used to signal that the user has\nverified the server identity provided in a `serververification` event\nand that the connection can continue.\n\n##### Syntax\n\n```js\nRFB.approveServer();\n```\n\n#### RFB.blur()\n\nThe `RFB.blur()` method remove keyboard focus on the remote session.\nKeyboard events will no longer be sent to the remote server after this\npoint.\n\n##### Syntax\n\n```js\nRFB.blur();\n```\n\n#### RFB.clipboardPasteFrom()\n\nThe `RFB.clipboardPasteFrom()` method is used to send clipboard data\nto the remote server.\n\n##### Syntax\n\n```js\nRFB.clipboardPasteFrom(text);\n```\n\n###### Parameters\n\n**`text`**\n  - A `DOMString` specifying the clipboard data to send.\n\n#### RFB.disconnect()\n\nThe `RFB.disconnect()` method is used to disconnect from the currently\nconnected server.\n\n##### Syntax\n\n```js\nRFB.disconnect();\n```\n\n#### RFB.focus()\n\nThe `RFB.focus()` method sets the keyboard focus on the remote session.\nKeyboard events will be sent to the remote server after this point.\n\n##### Syntax\n\n```js\nRFB.focus();\nRFB.focus(options);\n```\n\n###### Parameters\n\n**`options`** *Optional*\n  - A `object` providing options to control how the focus will be\n    performed. Please see [`HTMLElement.focus()`][mdn-focus] for\n    available options.\n\n[mdn-focus]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus\n\n#### RFB.getImageData()\n\nThe `RFB.getImageData()` method is used to return the current content of\nthe screen encoded as [`ImageData`][mdn-imagedata].\n\n[mdn-imagedata]: https://developer.mozilla.org/en-US/docs/Web/API/ImageData\n\n##### Syntax\n\n```js\nRFB.getImageData();\n```\n\n#### RFB.machineReboot()\n\nThe `RFB.machineReboot()` method is used to request a clean reboot of\nthe remote machine. The capability `power` must be set for this method\nto have any effect.\n\n##### Syntax\n\n```js\nRFB.machineReboot();\n```\n\n#### RFB.machineReset()\n\nThe `RFB.machineReset()` method is used to request a forced reset of\nthe remote machine. The capability `power` must be set for this method\nto have any effect.\n\n##### Syntax\n\n```js\nRFB.machineReset();\n```\n\n#### RFB.machineShutdown()\n\nThe `RFB.machineShutdown()` method is used to request to shut down the\nremote machine. The capability `power` must be set for this method to\nhave any effect.\n\n##### Syntax\n\n```js\nRFB.machineShutdown();\n```\n\n#### RFB.sendCredentials()\n\nThe `RFB.sendCredentials()` method is used to provide the missing\ncredentials after a `credentialsrequired` event has been fired.\n\n##### Syntax\n\n```js\nRFB.sendCredentials(credentials);\n```\n\n###### Parameters\n\n**`credentials`**\n  - An `Object` specifying the credentials to provide to the server\n    when authenticating. See [`RFB()`](#rfb-1) for details.\n\n#### RFB.sendCtrlAltDel()\n\nThe `RFB.sendCtrlAltDel()` method is used to send the key sequence\n*left Control*, *left Alt*, *Delete*. This is a convenience wrapper\naround [`RFB.sendKey()`](#rfbsendkey).\n\n##### Syntax\n\n```js\nRFB.sendCtrlAltDel();\n```\n\n#### RFB.sendKey()\n\nThe `RFB.sendKey()` method is used to send a key event to the server.\n\n##### Syntax\n\n```js\nRFB.sendKey(keysym, code);\nRFB.sendKey(keysym, code, down);\n```\n\n###### Parameters\n\n**`keysym`**\n  - A `long` specifying the RFB keysym to send. Can be `0` if a valid\n    **`code`** is specified.\n\n**`code`**\n  - A `DOMString` specifying the physical key to send. Valid values are\n    those that can be specified to [`KeyboardEvent.code`][mdn-keycode].\n    If the physical key cannot be determined then `null` shall be\n    specified.\n\n[mdn-keycode]: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code\n\n**`down`** *Optional*\n  - A `boolean` specifying if a press or a release event should be\n    sent. If omitted then both a press and release event are sent.\n\n#### RFB.toBlob()\n\nThe `RFB.toBlob()` method is used to return the current content of the\nscreen encoded as [`Blob`][mdn-blob].\n\n[mdn-blob]: https://developer.mozilla.org/en-US/docs/Web/API/Blob\n\n##### Syntax\n\n```js\nRFB.toBlob(callback);\nRFB.toBlob(callback, type);\nRFB.toBlob(callback, type, quality);\n```\n\n###### Parameters\n\n**`callback`**\n  - A callback function which will receive the resulting\n    [`Blob`][mdn-blob] as the single argument\n\n**`type`** *Optional*\n  - A string indicating the requested MIME type of the image\n\n**`quality`** *Optional*\n  - A number between 0 and 1 indicating the image quality.\n\n#### RFB.toDataURL()\n\nThe `RFB.toDataURL()` method is used to return the current content of the\nscreen encoded as a data URL that could for example be put in the `src` attribute\nof an `img` tag.\n\n##### Syntax\n\n```js\nRFB.toDataURL();\nRFB.toDataURL(type);\nRFB.toDataURL(type, encoderOptions);\n```\n\n###### Parameters\n\n**`type`** *Optional*\n  - A string indicating the requested MIME type of the image\n\n**`encoderOptions`** *Optional*\n  - A number between 0 and 1 indicating the image quality.\n"
  },
  {
    "path": "docs/EMBEDDING.md",
    "content": "# Embedding and deploying noVNC application\n\nThis document describes how to embed and deploy the noVNC application, which\nincludes settings and a full user interface. If you are looking for\ndocumentation on how to use the core noVNC library in your own application,\nthen please see our [library documentation](LIBRARY.md).\n\n## Files\n\nThe noVNC application consists of the following files and directories:\n\n* `vnc.html` - The main page for the application and where users should go. It\n  is possible to rename this file.\n\n* `app/` - Support files for the application. Contains code, images, styles and\n  translations.\n\n* `core/` - The core noVNC library.\n\n* `vendor/` - Third party support libraries used by the application and the\n  core library.\n\nThe most basic deployment consists of simply serving these files from a web\nserver and setting up a WebSocket proxy to the VNC server.\n\n## Parameters\n\nThe noVNC application can be controlled via a number of settings. All of\nthem are available in the UI for the user to change, but they can also\nbe set via other means:\n\n* Via the URL, either as a query parameter:\n\n  ```\n  https://www.example.com/vnc.html?reconnect=0&shared=1\n  ```\n\n  or as a fragment:\n\n  ```\n  https://www.example.com/vnc.html#reconnect=0&shared=1\n  ```\n\n  The latter might be preferred as it is not sent to the server.\n\n* Via the files `defaults.json` and `mandatory.json`\n\nCurrently, the following options are available:\n\n* `autoconnect` - Automatically connect as soon as the page has finished\n  loading.\n\n* `reconnect` - If noVNC should automatically reconnect if the connection is\n  dropped.\n\n* `reconnect_delay` - How long to wait in milliseconds before attempting to\n  reconnect.\n\n* `host` - The WebSocket host to connect to. This setting is deprecated\n  in favor of specifying a URL in `path`.\n\n* `port` - The WebSocket port to connect to. This setting is deprecated\n  in favor of specifying a URL in `path`.\n\n* `encrypt` - If TLS should be used for the WebSocket connection. This\n  setting is deprecated in favor of specifying a URL in `path`.\n\n* `path` - The WebSocket URL to use. It can be either an absolute URL,\n  or a URL relative vnc.html. If `host` is specified, then `path` will\n  be interpreted as the path component in the URL instead.\n\n* `password` - The password sent to the server, if required.\n\n* `repeaterID` - The repeater ID to use if a VNC repeater is detected.\n\n* `shared` - If other VNC clients should be disconnected when noVNC connects.\n\n* `bell` - If the keyboard bell should be enabled or not.\n\n* `view_only` - If the remote session should be in non-interactive mode.\n\n* `view_clip` - If the remote session should be clipped or use scrollbars if\n  it cannot fit in the browser.\n\n* `resize` - How to resize the remote session if it is not the same size as\n  the browser window. Can be one of `off`, `scale` and `remote`.\n\n* `quality` - The session JPEG quality level. Can be `0` to `9`.\n\n* `compression` - The session compression level. Can be `0` to `9`.\n\n* `logging` - The console log level. Can be one of `error`, `warn`, `info` or\n  `debug`.\n\n* `keep_device_awake` - Should we prevent the (local) display from going into\n  sleep mode while a connection is active? Useful for view-only sessions where\n  there unlikely to be any keyboard/mouse activity to keep the device active.\n\n## HTTP serving considerations\n### Browser cache issue\n\nIf you serve noVNC files using a web server that provides an ETag header, and\ninclude any options in the query string, a nasty browser cache issue can bite\nyou on upgrade, resulting in a red error box. The issue is caused by a mismatch\nbetween the new vnc.html (which is reloaded because the user has used it with\nnew query string after the upgrade) and the old javascript files (that the\nbrowser reuses from its cache). To avoid this issue, the browser must be told\nto always revalidate cached files using conditional requests. The correct\nsemantics are achieved via the (confusingly named) `Cache-Control: no-cache`\nheader that needs to be provided in the web server responses.\n\n### Example server configurations\n\nApache:\n\n```\n    # In the main configuration file\n    # (Debian/Ubuntu users: use \"a2enmod headers\" instead)\n    LoadModule headers_module modules/mod_headers.so\n\n    # In the <Directory> or <Location> block related to noVNC\n    Header set Cache-Control \"no-cache\"\n```\n\nNginx:\n\n```\n    # In the location block related to noVNC\n    add_header Cache-Control no-cache;\n```\n"
  },
  {
    "path": "docs/LIBRARY.md",
    "content": "# Using the noVNC JavaScript library\n\nThis document describes how to make use of the noVNC JavaScript library for\nintegration in your own VNC client application. If you wish to embed the more\ncomplete noVNC application with its included user interface then please see\nour [embedding documentation](EMBEDDING.md).\n\n## API\n\nThe API of noVNC consists of a single object called `RFB`. The formal\ndocumentation for that object can be found in our [API documentation](API.md).\n\n## Example\n\nnoVNC includes a small example application called `vnc_lite.html`. This does\nnot make use of all the features of noVNC, but is a good start to see how to\ndo things.\n\n## Conversion of modules\n\nnoVNC is written using ECMAScript 6 modules. This is not supported by older\nversions of Node.js. To use noVNC with those older versions of Node.js the\nlibrary must first be converted.\n\nFortunately noVNC includes a script to handle this conversion. Please follow\nthe following steps:\n\n 1. Install Node.js\n 2. Run `npm install` in the noVNC directory\n\nThe result of the conversion is available in the `lib/` directory.\n"
  },
  {
    "path": "docs/LICENSE.BSD-2-Clause",
    "content": "Copyright (c) <year>, <copyright holder>\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright\n      notice, this list of conditions and the following disclaimer in the\n      documentation and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY\nDIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "docs/LICENSE.BSD-3-Clause",
    "content": "Copyright (c) <year>, <copyright holder>\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright\n      notice, this list of conditions and the following disclaimer in the\n      documentation and/or other materials provided with the distribution.\n    * Neither the name of the <organization> nor the\n      names of its contributors may be used to endorse or promote products\n      derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY\nDIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "docs/LICENSE.MPL-2.0",
    "content": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in \n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "docs/LICENSE.OFL-1.1",
    "content": "This Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttp://scripts.sil.org/OFL\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "docs/flash_policy.txt",
    "content": "Manual setup:\n\nDATA=\"echo \\'<cross-domain-policy><allow-access-from domain=\\\\\\\"*\\\\\\\" to-ports=\\\\\\\"*\\\\\\\" /></cross-domain-policy>\\'\"\n/usr/bin/socat -T 1 TCP-L:843,reuseaddr,fork,crlf SYSTEM:\"$DATA\"\n"
  },
  {
    "path": "docs/links",
    "content": "New tight PNG protocol:\n    http://wiki.qemu.org/VNC_Tight_PNG\n    http://xf.iksaif.net/blog/index.php?post/2010/06/14/QEMU:-Tight-PNG-and-some-profiling\n\nRFB protocol and extensions:\n    http://tigervnc.org/cgi-bin/rfbproto\n\nCanvas Browser Compatibility:\n    http://philip.html5.org/tests/canvas/suite/tests/results.html\n\nWebSockets API standard:\n    http://www.whatwg.org/specs/web-apps/current-work/complete.html#websocket\n    http://dev.w3.org/html5/websockets/\n    http://www.ietf.org/id/draft-ietf-hybi-thewebsocketprotocol-00.txt\n\nBrowser Keyboard Events detailed:\n    http://unixpapa.com/js/key.html\n\nActionScript (Flash) WebSocket implementation:\n    http://github.com/gimite/web-socket-js\n\nActionScript (Flash) crypto/TLS library:\n    http://code.google.com/p/as3crypto\n    http://github.com/lyokato/as3crypto_patched\n\nTLS Protocol:\n    http://en.wikipedia.org/wiki/Transport_Layer_Security\n\nGenerate self-signed certificate:\n    http://docs.python.org/dev/library/ssl.html#certificates\n\nCursor appearance/style (for Cursor pseudo-encoding):\n    http://en.wikipedia.org/wiki/ICO_(file_format)\n    http://www.daubnet.com/en/file-format-cur\n    https://developer.mozilla.org/en/Using_URL_values_for_the_cursor_property\n    http://www.fileformat.info/format/bmp/egff.htm\n\nIcon/Cursor file format:\n    http://msdn.microsoft.com/en-us/library/ms997538\n    http://msdn.microsoft.com/en-us/library/aa921550.aspx\n    http://msdn.microsoft.com/en-us/library/aa930622.aspx\n\n\nRDP Protocol specification:\n    http://msdn.microsoft.com/en-us/library/cc240445(v=PROT.10).aspx\n\n\nRelated projects:\n    \n    guacamole: http://guacamole.sourceforge.net/\n\n        - Web client, but Java servlet does pre-processing\n\n    jsvnc: http://code.google.com/p/jsvnc/\n\n        - No releases\n\n    webvnc: http://code.google.com/p/webvnc/\n\n        - Jetty web server gateway, no updates since April 2008.\n\n    RealVNC Java applet: http://www.realvnc.com/support/javavncviewer.html\n\n        - Java applet\n\n    Flashlight-VNC: http://www.wizhelp.com/flashlight-vnc/\n\n        - Adobe Flash implementation\n\n    FVNC: http://osflash.org/fvnc\n\n        - Adbove Flash implementation\n\n    CanVNC: http://canvnc.sourceforge.net/\n\n        - HTML client with REST to VNC python proxy. Mostly vapor.\n"
  },
  {
    "path": "docs/notes",
    "content": "Rebuilding inflator.js\n\n- Download pako from npm\n- Install browserify using npm\n- browserify core/inflator.mod.js -o core/inflator.js -s Inflator\n"
  },
  {
    "path": "docs/novnc_proxy.1",
    "content": ".TH novnc_proxy 1  \"June 25, 2020\" \"version 1.2.0\" \"USER COMMANDS\"\n\n.SH NAME\nnovnc_proxy - noVNC proxy server\n.SH SYNOPSIS\n.B novnc_proxy [--listen [HOST:]PORT] [--vnc VNC_HOST:PORT] [--cert CERT] [--ssl-only]\n\nStarts the WebSockets proxy and a mini-webserver and\nprovides a cut-and-paste URL to go to.\n\n    --listen [HOST:]PORT  Port for proxy/webserver to listen on\n                          Default: 6080 (on all interfaces)\n    --vnc VNC_HOST:PORT   VNC server host:port proxy target\n                          Default: localhost:5900\n    --cert CERT           Path to combined cert/key file, or just\n                          the cert file if used with --key\n                          Default: self.pem\n    --key KEY             Path to key file, when not combined with cert\n    --web WEB             Path to web files (e.g. vnc.html)\n                          Default: ./\n    --ssl-only            Disable non-https connections.\n\n    --record FILE         Record traffic to FILE.session.js\n\n    --syslog SERVER       Can be local socket such as /dev/log, or a UDP host:port pair.\n\n    --heartbeat SEC       send a ping to the client every SEC seconds\n    --timeout SEC         after SEC seconds exit when not connected\n    --idle-timeout SEC    server exits after SEC seconds if there are no\n                          active connections\n\n.SH AUTHOR\nThe noVNC authors\nhttps://github.com/novnc/noVNC\n\n.SH SEE ALSO\nwebsockify(1), nova-novncproxy(1)\n"
  },
  {
    "path": "docs/rfb_notes",
    "content": "5.1.1 ProtocolVersion: 12, 12 bytes\n\n    - Sent by server, max supported\n        12 ascii - \"RFB 003.008\\n\"\n    - Response by client, version to use\n        12 ascii - \"RFB 003.003\\n\"\n\n5.1.2 Authentication: >=4, [16, 4] bytes\n\n    - Sent by server\n        CARD32 - authentication-scheme\n                0 - connection failed\n                    CARD32 - length\n                    length - reason\n                1 - no authentication\n\n                2 - VNC authentication\n                    16 CARD8 - challenge (random bytes)\n\n    - Response by client (if VNC authentication)\n        16 CARD8 - client encrypts the challenge with DES, using user\n                   password as key, sends resulting 16 byte response\n\n    - Response by server (if VNC authentication) \n        CARD32 - 0 - OK\n                 1 - failed\n                 2 - too-many\n\n5.1.3 ClientInitialisation: 1 byte\n    - Sent by client\n        CARD8 - shared-flag, 0 exclusive, non-zero shared\n\n5.1.4 ServerInitialisation: >=24 bytes\n    - Sent by server\n        CARD16 - framebuffer-width\n        CARD16 - framebuffer-height\n        16 byte PIXEL_FORMAT - server-pixel-format\n            CARD8 - bits-per-pixel\n            CARD8 - depth\n            CARD8 - big-endian-flag, non-zero is big endian\n            CARD8 - true-color-flag, non-zero then next 6 apply\n            CARD16 - red-max\n            CARD16 - green-max\n            CARD16 - blue-max\n            CARD8 - red-shift\n            CARD8 - green-shift\n            CARD8 - blue-shift\n            3 bytes - padding\n        CARD32 - name-length\n\n        CARD8[length] - name-string\n\n\n\nClient to Server Messages:\n\n5.2.1 SetPixelFormat: 20 bytes\n    CARD8: 0 - message-type\n    ...\n\n5.2.2 FixColourMapEntries: >=6 bytes\n    CARD8: 1 - message-type\n    ...\n\n5.2.3 SetEncodings: >=8 bytes\n    CARD8: 2 - message-type\n    CARD8    - padding\n    CARD16   - numer-of-encodings\n\n    CARD32   - encoding-type in preference order\n        0 - raw\n        1 - copy-rectangle\n        2 - RRE\n        4 - CoRRE\n        5 - hextile\n\n5.2.4 FramebufferUpdateRequest (10 bytes)\n    CARD8: 3 - message-type\n    CARD8    - incremental (0 for full-update, non-zero for incremental)\n    CARD16   - x-position\n    CARD16   - y-position\n    CARD16   - width\n    CARD16   - height\n\n\n5.2.5 KeyEvent: 8 bytes\n    CARD8: 4 - message-type\n    CARD8    - down-flag\n    2 bytes  - padding\n    CARD32   - key (X-Windows keysym values)\n\n5.2.6 PointerEvent: 6 bytes\n    CARD8: 5 - message-type\n    CARD8    - button-mask\n    CARD16   - x-position\n    CARD16   - y-position\n\n5.2.7 ClientCutText: >=9 bytes\n    CARD8: 6 - message-type\n    ...\n\n\nServer to Client Messages:\n\n5.3.1 FramebufferUpdate\n    CARD8: 0 - message-type\n    1 byte   - padding\n    CARD16   - number-of-rectangles\n\n    CARD16   - x-position\n    CARD16   - y-position\n    CARD16   - width\n    CARD16   - height\n    CARD16   - encoding-type:\n        0 - raw\n        1 - copy rectangle\n        2 - RRE\n        4 - CoRRE\n        5 - hextile\n\n        raw:\n            - width x height pixel values\n\n        copy rectangle: \n            CARD16 - src-x-position\n            CARD16 - src-y-position\n\n        RRE:\n            CARD32  - N number-of-subrectangles\n            Nxd bytes - background-pixel-value (d bits-per-pixel)\n\n        ...\n\n5.3.2 SetColourMapEntries (no support)\n    CARD8: 1 - message-type\n    ...\n\n5.3.3 Bell\n    CARD8: 2 - message-type\n\n5.3.4 ServerCutText\n    CARD8: 3 - message-type\n\n\n\n\n    \n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import globals from \"globals\";\nimport { defineConfig } from \"eslint/config\";\nimport js from \"@eslint/js\";\n\nexport default defineConfig([\n    js.configs.recommended,\n    {\n        languageOptions: {\n            ecmaVersion: 2022,\n            sourceType: \"module\",\n            globals: {\n                ...globals.browser,\n                ...globals.es2022,\n            }\n        },\n        ignores: [\"**/xtscancodes.js\"],\n        rules: {\n            // Unsafe or confusing stuff that we forbid\n\n            \"no-unused-vars\": [\"error\", { \"vars\": \"all\",\n                                          \"args\": \"none\",\n                                          \"ignoreRestSiblings\": true,\n                                          \"caughtErrors\": \"none\" }],\n            \"no-constant-condition\": [\"error\", { \"checkLoops\": false }],\n            \"no-var\": \"error\",\n            \"no-useless-constructor\": \"error\",\n            \"object-shorthand\": [\"error\", \"methods\", { \"avoidQuotes\": true }],\n            \"prefer-arrow-callback\": \"error\",\n            \"arrow-body-style\": [\"error\", \"as-needed\", { \"requireReturnForObjectLiteral\": false } ],\n            \"arrow-parens\": [\"error\", \"as-needed\", { \"requireForBlockBody\": true }],\n            \"arrow-spacing\": [\"error\"],\n            \"no-confusing-arrow\": [\"error\", { \"allowParens\": true }],\n\n            // Enforced coding style\n\n            \"brace-style\": [\"error\", \"1tbs\", { \"allowSingleLine\": true }],\n            \"indent\": [\"error\", 4, { \"SwitchCase\": 1,\n                                     \"VariableDeclarator\": \"first\",\n                                     \"FunctionDeclaration\": { \"parameters\": \"first\" },\n                                     \"FunctionExpression\": { \"parameters\": \"first\" },\n                                     \"CallExpression\": { \"arguments\": \"first\" },\n                                     \"ArrayExpression\": \"first\",\n                                     \"ObjectExpression\": \"first\",\n                                     \"ImportDeclaration\": \"first\",\n                                     \"ignoreComments\": true }],\n            \"comma-spacing\": [\"error\"],\n            \"comma-style\": [\"error\"],\n            \"curly\": [\"error\", \"multi-line\"],\n            \"func-call-spacing\": [\"error\"],\n            \"func-names\": [\"error\"],\n            \"func-style\": [\"error\", \"declaration\", { \"allowArrowFunctions\": true }],\n            \"key-spacing\": [\"error\"],\n            \"keyword-spacing\": [\"error\"],\n            \"no-trailing-spaces\": [\"error\"],\n            \"semi\": [\"error\"],\n            \"space-before-blocks\": [\"error\"],\n            \"space-before-function-paren\": [\"error\", { \"anonymous\": \"always\",\n                                                       \"named\": \"never\",\n                                                       \"asyncArrow\": \"always\" }],\n            \"switch-colon-spacing\": [\"error\"],\n            \"camelcase\": [\"error\", { \"allow\": [\"^XK_\", \"^XF86XK_\"] }],\n            \"no-console\": [\"error\"],\n        }\n    },\n    {\n        files: [\"po/po2js\", \"po/xgettext-html\"],\n        languageOptions: {\n            globals: {\n                ...globals.node,\n            }\n        },\n        rules: {\n            \"no-console\": 0,\n        },\n    },\n    {\n        files: [\"tests/*\"],\n        languageOptions: {\n            globals: {\n                ...globals.node,\n                ...globals.mocha,\n                sinon: false,\n                expect: false,\n            }\n        },\n        rules: {\n            \"prefer-arrow-callback\": 0,\n            // Too many anonymous callbacks\n            \"func-names\": \"off\",\n        },\n    },\n    {\n        files: [\"utils/*\"],\n        languageOptions: {\n            globals: {\n                ...globals.node,\n            }\n        },\n        rules: {\n            \"no-console\": 0,\n        },\n    },\n]);\n"
  },
  {
    "path": "karma.conf.cjs",
    "content": "// Karma configuration\n\n// The Safari launcher is broken, so construct our own\nfunction SafariBrowser(id, baseBrowserDecorator, args) {\n  baseBrowserDecorator(this);\n\n  this._start = function(url) {\n    this._execCommand('/usr/bin/open', ['-W', '-n', '-a', 'Safari', url]);\n  }\n}\n\nSafariBrowser.prototype = {\n  name: 'Safari'\n}\n\nmodule.exports = (config) => {\n  let browsers = [];\n\n  if (process.env.TEST_BROWSER_NAME) {\n    browsers = process.env.TEST_BROWSER_NAME.split(',');\n  }\n\n  const my_conf = {\n\n    // base path that will be used to resolve all patterns (eg. files, exclude)\n    basePath: '',\n\n    // frameworks to use\n    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter\n    frameworks: ['mocha'],\n\n    // list of files / patterns to load in the browser\n    files: [\n      // node modules\n      { pattern: 'node_modules/chai/**', included: false },\n      { pattern: 'node_modules/sinon/**', included: false },\n      { pattern: 'node_modules/sinon-chai/**', included: false },\n      // modules to test\n      { pattern: 'app/localization.js', included: false, type: 'module' },\n      { pattern: 'app/wakelock.js', included: false, type: 'module' },\n      { pattern: 'app/webutil.js', included: false, type: 'module' },\n      { pattern: 'core/**/*.js', included: false, type: 'module' },\n      { pattern: 'vendor/pako/**/*.js', included: false, type: 'module' },\n      // tests\n      { pattern: 'tests/test.*.js', type: 'module' },\n      // test support files\n      { pattern: 'tests/fake.*.js', included: false, type: 'module' },\n      { pattern: 'tests/assertions.js', type: 'module' },\n    ],\n\n    client: {\n      mocha: {\n        // replace Karma debug page with mocha display\n        'reporter': 'html',\n        'ui': 'bdd'\n      }\n    },\n\n    // list of files to exclude\n    exclude: [\n    ],\n\n    plugins: [\n      'karma-*',\n      '@chiragrupani/karma-chromium-edge-launcher',\n      { 'launcher:Safari': [ 'type', SafariBrowser ] },\n    ],\n\n    // start these browsers\n    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher\n    browsers: browsers,\n\n    // test results reporter to use\n    // possible values: 'dots', 'progress'\n    // available reporters: https://npmjs.org/browse/keyword/karma-reporter\n    reporters: ['mocha'],\n\n\n    // level of logging\n    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG\n    logLevel: config.LOG_INFO,\n\n\n    // enable / disable watching file and executing tests whenever any file changes\n    autoWatch: false,\n\n    // Continuous Integration mode\n    // if true, Karma captures browsers, runs the tests and exits\n    singleRun: true,\n  };\n\n  config.set(my_conf);\n};\n"
  },
  {
    "path": "mandatory.json",
    "content": "{}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@novnc/novnc\",\n  \"version\": \"1.6.0\",\n  \"description\": \"An HTML5 VNC client\",\n  \"type\": \"module\",\n  \"files\": [\n    \"core\",\n    \"vendor\",\n    \"AUTHORS\",\n    \"VERSION\",\n    \"docs/API.md\",\n    \"docs/LIBRARY.md\",\n    \"docs/LICENSE*\"\n  ],\n  \"exports\": \"./core/rfb.js\",\n  \"scripts\": {\n    \"lint\": \"eslint app core po/po2js po/xgettext-html tests utils\",\n    \"test\": \"karma start karma.conf.cjs\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/novnc/noVNC.git\"\n  },\n  \"author\": \"Joel Martin <github@martintribe.org> (https://github.com/kanaka)\",\n  \"contributors\": [\n    \"Samuel Mannehed <samuel@cendio.se> (https://github.com/samhed)\",\n    \"Pierre Ossman <ossman@cendio.se> (https://github.com/CendioOssman)\"\n  ],\n  \"license\": \"MPL-2.0\",\n  \"bugs\": {\n    \"url\": \"https://github.com/novnc/noVNC/issues\"\n  },\n  \"homepage\": \"https://github.com/novnc/noVNC\",\n  \"devDependencies\": {\n    \"@babel/core\": \"latest\",\n    \"@babel/preset-env\": \"latest\",\n    \"babel-plugin-import-redirect\": \"latest\",\n    \"browserify\": \"latest\",\n    \"chai\": \"latest\",\n    \"commander\": \"latest\",\n    \"eslint\": \"latest\",\n    \"@eslint/js\": \"latest\",\n    \"fs-extra\": \"latest\",\n    \"globals\": \"latest\",\n    \"jsdom\": \"latest\",\n    \"karma\": \"latest\",\n    \"karma-mocha\": \"latest\",\n    \"karma-chrome-launcher\": \"latest\",\n    \"@chiragrupani/karma-chromium-edge-launcher\": \"latest\",\n    \"karma-firefox-launcher\": \"latest\",\n    \"karma-ie-launcher\": \"latest\",\n    \"karma-mocha-reporter\": \"latest\",\n    \"karma-safari-launcher\": \"latest\",\n    \"karma-script-launcher\": \"latest\",\n    \"mocha\": \"latest\",\n    \"pofile\": \"latest\",\n    \"sinon\": \"latest\",\n    \"sinon-chai\": \"latest\"\n  },\n  \"dependencies\": {},\n  \"keywords\": [\n    \"vnc\",\n    \"rfb\",\n    \"novnc\",\n    \"websockify\"\n  ]\n}\n"
  },
  {
    "path": "po/Makefile",
    "content": "all:\n.PHONY: update-po update-js update-pot\n.PHONY: FORCE\n\nLINGUAS := cs de el es fr hr hu it ja ko nl pl pt_BR ru sv tr uk zh_CN zh_TW\n\nVERSION := $(shell grep '\"version\"' ../package.json | cut -d '\"' -f 4)\n\nPOFILES := $(addsuffix .po,$(LINGUAS))\nJSONFILES := $(addprefix ../app/locale/,$(addsuffix .json,$(LINGUAS)))\n\nupdate-po: $(POFILES)\nupdate-js: $(JSONFILES)\n\n%.po: FORCE\n\tmsgmerge --update --lang=$* $@ noVNC.pot\n../app/locale/%.json: FORCE\n\t./po2js $*.po $@\n\nupdate-pot:\n\txgettext --output=noVNC.js.pot \\\n\t\t--copyright-holder=\"The noVNC authors\" \\\n\t\t--package-name=\"noVNC\" \\\n\t\t--package-version=\"$(VERSION)\" \\\n\t\t--msgid-bugs-address=\"novnc@googlegroups.com\" \\\n\t\t--add-comments=TRANSLATORS: \\\n\t\t--from-code=UTF-8 \\\n\t\t--sort-by-file \\\n\t\t../app/*.js \\\n\t\t../core/*.js \\\n\t\t../core/input/*.js\n\t./xgettext-html --output=noVNC.html.pot \\\n\t\t../vnc.html\n\tmsgcat --output-file=noVNC.pot \\\n\t\t--sort-by-file noVNC.js.pot noVNC.html.pot\n\trm -f noVNC.js.pot noVNC.html.pot\n"
  },
  {
    "path": "po/cs.po",
    "content": "# Czech translations for noVNC package.\n# Copyright (C) 2018 The noVNC authors\n# This file is distributed under the same license as the noVNC package.\n# Petr <petr@kle.cz>, 2018.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 1.0.0-testing.2\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2018-10-19 12:00+0200\\n\"\n\"PO-Revision-Date: 2018-10-19 12:00+0200\\n\"\n\"Last-Translator: Petr <petr@kle.cz>\\n\"\n\"Language-Team: Czech\\n\"\n\"Language: cs\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\\n\"\n\n#: ../app/ui.js:389\nmsgid \"Connecting...\"\nmsgstr \"Připojení...\"\n\n#: ../app/ui.js:396\nmsgid \"Disconnecting...\"\nmsgstr \"Odpojení...\"\n\n#: ../app/ui.js:402\nmsgid \"Reconnecting...\"\nmsgstr \"Obnova připojení...\"\n\n#: ../app/ui.js:407\nmsgid \"Internal error\"\nmsgstr \"Vnitřní chyba\"\n\n#: ../app/ui.js:997\nmsgid \"Must set host\"\nmsgstr \"Hostitel musí být nastavení\"\n\n#: ../app/ui.js:1079\nmsgid \"Connected (encrypted) to \"\nmsgstr \"Připojení (šifrované) k \"\n\n#: ../app/ui.js:1081\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"Připojení (nešifrované) k \"\n\n#: ../app/ui.js:1104\nmsgid \"Something went wrong, connection is closed\"\nmsgstr \"Něco se pokazilo, odpojeno\"\n\n#: ../app/ui.js:1107\nmsgid \"Failed to connect to server\"\nmsgstr \"Chyba připojení k serveru\"\n\n#: ../app/ui.js:1117\nmsgid \"Disconnected\"\nmsgstr \"Odpojeno\"\n\n#: ../app/ui.js:1130\nmsgid \"New connection has been rejected with reason: \"\nmsgstr \"Nové připojení bylo odmítnuto s odůvodněním: \"\n\n#: ../app/ui.js:1133\nmsgid \"New connection has been rejected\"\nmsgstr \"Nové připojení bylo odmítnuto\"\n\n#: ../app/ui.js:1153\nmsgid \"Password is required\"\nmsgstr \"Je vyžadováno heslo\"\n\n#: ../vnc.html:84\nmsgid \"noVNC encountered an error:\"\nmsgstr \"noVNC narazilo na chybu:\"\n\n#: ../vnc.html:94\nmsgid \"Hide/Show the control bar\"\nmsgstr \"Skrýt/zobrazit ovládací panel\"\n\n#: ../vnc.html:101\nmsgid \"Move/Drag viewport\"\nmsgstr \"Přesunout/přetáhnout výřez\"\n\n#: ../vnc.html:101\nmsgid \"viewport drag\"\nmsgstr \"přesun výřezu\"\n\n#: ../vnc.html:107 ../vnc.html:110 ../vnc.html:113 ../vnc.html:116\nmsgid \"Active Mouse Button\"\nmsgstr \"Aktivní tlačítka myši\"\n\n#: ../vnc.html:107\nmsgid \"No mousebutton\"\nmsgstr \"Žádné\"\n\n#: ../vnc.html:110\nmsgid \"Left mousebutton\"\nmsgstr \"Levé tlačítko myši\"\n\n#: ../vnc.html:113\nmsgid \"Middle mousebutton\"\nmsgstr \"Prostřední tlačítko myši\"\n\n#: ../vnc.html:116\nmsgid \"Right mousebutton\"\nmsgstr \"Pravé tlačítko myši\"\n\n#: ../vnc.html:119\nmsgid \"Keyboard\"\nmsgstr \"Klávesnice\"\n\n#: ../vnc.html:119\nmsgid \"Show keyboard\"\nmsgstr \"Zobrazit klávesnici\"\n\n#: ../vnc.html:126\nmsgid \"Extra keys\"\nmsgstr \"Extra klávesy\"\n\n#: ../vnc.html:126\nmsgid \"Show extra keys\"\nmsgstr \"Zobrazit extra klávesy\"\n\n#: ../vnc.html:131\nmsgid \"Ctrl\"\nmsgstr \"Ctrl\"\n\n#: ../vnc.html:131\nmsgid \"Toggle Ctrl\"\nmsgstr \"Přepnout Ctrl\"\n\n#: ../vnc.html:134\nmsgid \"Alt\"\nmsgstr \"Alt\"\n\n#: ../vnc.html:134\nmsgid \"Toggle Alt\"\nmsgstr \"Přepnout Alt\"\n\n#: ../vnc.html:137\nmsgid \"Send Tab\"\nmsgstr \"Odeslat tabulátor\"\n\n#: ../vnc.html:137\nmsgid \"Tab\"\nmsgstr \"Tab\"\n\n#: ../vnc.html:140\nmsgid \"Esc\"\nmsgstr \"Esc\"\n\n#: ../vnc.html:140\nmsgid \"Send Escape\"\nmsgstr \"Odeslat Esc\"\n\n#: ../vnc.html:143\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"Ctrl+Alt+Del\"\n\n#: ../vnc.html:143\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"Poslat Ctrl-Alt-Del\"\n\n#: ../vnc.html:151\nmsgid \"Shutdown/Reboot\"\nmsgstr \"Vypnutí/Restart\"\n\n#: ../vnc.html:151\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"Vypnutí/Restart...\"\n\n#: ../vnc.html:157\nmsgid \"Power\"\nmsgstr \"Napájení\"\n\n#: ../vnc.html:159\nmsgid \"Shutdown\"\nmsgstr \"Vypnout\"\n\n#: ../vnc.html:160\nmsgid \"Reboot\"\nmsgstr \"Restart\"\n\n#: ../vnc.html:161\nmsgid \"Reset\"\nmsgstr \"Reset\"\n\n#: ../vnc.html:166 ../vnc.html:172\nmsgid \"Clipboard\"\nmsgstr \"Schránka\"\n\n#: ../vnc.html:176\nmsgid \"Clear\"\nmsgstr \"Vymazat\"\n\n#: ../vnc.html:182\nmsgid \"Fullscreen\"\nmsgstr \"Celá obrazovka\"\n\n#: ../vnc.html:187 ../vnc.html:194\nmsgid \"Settings\"\nmsgstr \"Nastavení\"\n\n#: ../vnc.html:197\nmsgid \"Shared mode\"\nmsgstr \"Sdílený režim\"\n\n#: ../vnc.html:200\nmsgid \"View only\"\nmsgstr \"Pouze prohlížení\"\n\n#: ../vnc.html:204\nmsgid \"Clip to window\"\nmsgstr \"Přizpůsobit oknu\"\n\n#: ../vnc.html:207\nmsgid \"Scaling mode:\"\nmsgstr \"Přizpůsobení velikosti\"\n\n#: ../vnc.html:209\nmsgid \"None\"\nmsgstr \"Žádné\"\n\n#: ../vnc.html:210\nmsgid \"Local scaling\"\nmsgstr \"Místní\"\n\n#: ../vnc.html:211\nmsgid \"Remote resizing\"\nmsgstr \"Vzdálené\"\n\n#: ../vnc.html:216\nmsgid \"Advanced\"\nmsgstr \"Pokročilé\"\n\n#: ../vnc.html:219\nmsgid \"Repeater ID:\"\nmsgstr \"ID opakovače\"\n\n#: ../vnc.html:223\nmsgid \"WebSocket\"\nmsgstr \"WebSocket\"\n\n#: ../vnc.html:226\nmsgid \"Encrypt\"\nmsgstr \"Šifrování:\"\n\n#: ../vnc.html:229\nmsgid \"Host:\"\nmsgstr \"Hostitel:\"\n\n#: ../vnc.html:233\nmsgid \"Port:\"\nmsgstr \"Port:\"\n\n#: ../vnc.html:237\nmsgid \"Path:\"\nmsgstr \"Cesta\"\n\n#: ../vnc.html:244\nmsgid \"Automatic reconnect\"\nmsgstr \"Automatická obnova připojení\"\n\n#: ../vnc.html:247\nmsgid \"Reconnect delay (ms):\"\nmsgstr \"Zpoždění připojení (ms)\"\n\n#: ../vnc.html:252\nmsgid \"Show dot when no cursor\"\nmsgstr \"Tečka místo chybějícího kurzoru myši\"\n\n#: ../vnc.html:257\nmsgid \"Logging:\"\nmsgstr \"Logování:\"\n\n#: ../vnc.html:269\nmsgid \"Disconnect\"\nmsgstr \"Odpojit\"\n\n#: ../vnc.html:288\nmsgid \"Connect\"\nmsgstr \"Připojit\"\n\n#: ../vnc.html:298\nmsgid \"Password:\"\nmsgstr \"Heslo\"\n\n#: ../vnc.html:302\nmsgid \"Send Password\"\nmsgstr \"Odeslat heslo\"\n\n#: ../vnc.html:312\nmsgid \"Cancel\"\nmsgstr \"Zrušit\"\n"
  },
  {
    "path": "po/de.po",
    "content": "# German translations for noVNC package\n# German translation for noVNC.\n# Copyright (C) 2018 The noVNC authors\n# This file is distributed under the same license as the noVNC package.\n# Loek Janssen <loekjanssen@gmail.com>, 2016.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 0.6.1\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2017-11-24 07:16+0000\\n\"\n\"PO-Revision-Date: 2017-11-24 08:20+0100\\n\"\n\"Last-Translator: Dominik Csapak <d.csapak@proxmox.com>\\n\"\n\"Language-Team: none\\n\"\n\"Language: de\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Generator: Poedit 1.8.11\\n\"\n\n#: ../app/ui.js:404\nmsgid \"Connecting...\"\nmsgstr \"Verbinden...\"\n\n#: ../app/ui.js:411\nmsgid \"Disconnecting...\"\nmsgstr \"Verbindung trennen...\"\n\n#: ../app/ui.js:417\nmsgid \"Reconnecting...\"\nmsgstr \"Verbindung wiederherstellen...\"\n\n#: ../app/ui.js:422\nmsgid \"Internal error\"\nmsgstr \"Interner Fehler\"\n\n#: ../app/ui.js:1019\nmsgid \"Must set host\"\nmsgstr \"Richten Sie den Server ein\"\n\n#: ../app/ui.js:1099\nmsgid \"Connected (encrypted) to \"\nmsgstr \"Verbunden mit (verschlüsselt) \"\n\n#: ../app/ui.js:1101\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"Verbunden mit (unverschlüsselt) \"\n\n#: ../app/ui.js:1119\nmsgid \"Something went wrong, connection is closed\"\nmsgstr \"Etwas lief schief, Verbindung wurde getrennt\"\n\n#: ../app/ui.js:1129\nmsgid \"Disconnected\"\nmsgstr \"Verbindung zum Server getrennt\"\n\n#: ../app/ui.js:1142\nmsgid \"New connection has been rejected with reason: \"\nmsgstr \"Verbindung wurde aus folgendem Grund abgelehnt: \"\n\n#: ../app/ui.js:1145\nmsgid \"New connection has been rejected\"\nmsgstr \"Verbindung wurde abgelehnt\"\n\n#: ../app/ui.js:1166\nmsgid \"Password is required\"\nmsgstr \"Passwort ist erforderlich\"\n\n#: ../vnc.html:89\nmsgid \"noVNC encountered an error:\"\nmsgstr \"Ein Fehler ist aufgetreten:\"\n\n#: ../vnc.html:99\nmsgid \"Hide/Show the control bar\"\nmsgstr \"Kontrollleiste verstecken/anzeigen\"\n\n#: ../vnc.html:106\nmsgid \"Move/Drag viewport\"\nmsgstr \"Ansichtsfenster verschieben/ziehen\"\n\n#: ../vnc.html:106\nmsgid \"viewport drag\"\nmsgstr \"Ansichtsfenster ziehen\"\n\n#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121\nmsgid \"Active Mouse Button\"\nmsgstr \"Aktive Maustaste\"\n\n#: ../vnc.html:112\nmsgid \"No mousebutton\"\nmsgstr \"Keine Maustaste\"\n\n#: ../vnc.html:115\nmsgid \"Left mousebutton\"\nmsgstr \"Linke Maustaste\"\n\n#: ../vnc.html:118\nmsgid \"Middle mousebutton\"\nmsgstr \"Mittlere Maustaste\"\n\n#: ../vnc.html:121\nmsgid \"Right mousebutton\"\nmsgstr \"Rechte Maustaste\"\n\n#: ../vnc.html:124\nmsgid \"Keyboard\"\nmsgstr \"Tastatur\"\n\n#: ../vnc.html:124\nmsgid \"Show keyboard\"\nmsgstr \"Tastatur anzeigen\"\n\n#: ../vnc.html:131\nmsgid \"Extra keys\"\nmsgstr \"Zusatztasten\"\n\n#: ../vnc.html:131\nmsgid \"Show extra keys\"\nmsgstr \"Zusatztasten anzeigen\"\n\n#: ../vnc.html:136\nmsgid \"Ctrl\"\nmsgstr \"Strg\"\n\n#: ../vnc.html:136\nmsgid \"Toggle Ctrl\"\nmsgstr \"Strg umschalten\"\n\n#: ../vnc.html:139\nmsgid \"Alt\"\nmsgstr \"Alt\"\n\n#: ../vnc.html:139\nmsgid \"Toggle Alt\"\nmsgstr \"Alt umschalten\"\n\n#: ../vnc.html:142\nmsgid \"Send Tab\"\nmsgstr \"Tab senden\"\n\n#: ../vnc.html:142\nmsgid \"Tab\"\nmsgstr \"Tab\"\n\n#: ../vnc.html:145\nmsgid \"Esc\"\nmsgstr \"Esc\"\n\n#: ../vnc.html:145\nmsgid \"Send Escape\"\nmsgstr \"Escape senden\"\n\n#: ../vnc.html:148\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"Strg+Alt+Entf\"\n\n#: ../vnc.html:148\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"Strg+Alt+Entf senden\"\n\n#: ../vnc.html:156\nmsgid \"Shutdown/Reboot\"\nmsgstr \"Herunterfahren/Neustarten\"\n\n#: ../vnc.html:156\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"Herunterfahren/Neustarten...\"\n\n#: ../vnc.html:162\nmsgid \"Power\"\nmsgstr \"Energie\"\n\n#: ../vnc.html:164\nmsgid \"Shutdown\"\nmsgstr \"Herunterfahren\"\n\n#: ../vnc.html:165\nmsgid \"Reboot\"\nmsgstr \"Neustarten\"\n\n#: ../vnc.html:166\nmsgid \"Reset\"\nmsgstr \"Zurücksetzen\"\n\n#: ../vnc.html:171 ../vnc.html:177\nmsgid \"Clipboard\"\nmsgstr \"Zwischenablage\"\n\n#: ../vnc.html:181\nmsgid \"Clear\"\nmsgstr \"Löschen\"\n\n#: ../vnc.html:187\nmsgid \"Fullscreen\"\nmsgstr \"Vollbild\"\n\n#: ../vnc.html:192 ../vnc.html:199\nmsgid \"Settings\"\nmsgstr \"Einstellungen\"\n\n#: ../vnc.html:202\nmsgid \"Shared mode\"\nmsgstr \"Geteilter Modus\"\n\n#: ../vnc.html:205\nmsgid \"View only\"\nmsgstr \"Nur betrachten\"\n\n#: ../vnc.html:209\nmsgid \"Clip to window\"\nmsgstr \"Auf Fenster begrenzen\"\n\n#: ../vnc.html:212\nmsgid \"Scaling mode:\"\nmsgstr \"Skalierungsmodus:\"\n\n#: ../vnc.html:214\nmsgid \"None\"\nmsgstr \"Keiner\"\n\n#: ../vnc.html:215\nmsgid \"Local scaling\"\nmsgstr \"Lokales skalieren\"\n\n#: ../vnc.html:216\nmsgid \"Remote resizing\"\nmsgstr \"Serverseitiges skalieren\"\n\n#: ../vnc.html:221\nmsgid \"Advanced\"\nmsgstr \"Erweitert\"\n\n#: ../vnc.html:224\nmsgid \"Repeater ID:\"\nmsgstr \"Repeater ID:\"\n\n#: ../vnc.html:228\nmsgid \"WebSocket\"\nmsgstr \"WebSocket\"\n\n#: ../vnc.html:231\nmsgid \"Encrypt\"\nmsgstr \"Verschlüsselt\"\n\n#: ../vnc.html:234\nmsgid \"Host:\"\nmsgstr \"Server:\"\n\n#: ../vnc.html:238\nmsgid \"Port:\"\nmsgstr \"Port:\"\n\n#: ../vnc.html:242\nmsgid \"Path:\"\nmsgstr \"Pfad:\"\n\n#: ../vnc.html:249\nmsgid \"Automatic reconnect\"\nmsgstr \"Automatisch wiederverbinden\"\n\n#: ../vnc.html:252\nmsgid \"Reconnect delay (ms):\"\nmsgstr \"Wiederverbindungsverzögerung (ms):\"\n\n#: ../vnc.html:258\nmsgid \"Logging:\"\nmsgstr \"Protokollierung:\"\n\n#: ../vnc.html:270\nmsgid \"Disconnect\"\nmsgstr \"Verbindung trennen\"\n\n#: ../vnc.html:289\nmsgid \"Connect\"\nmsgstr \"Verbinden\"\n\n#: ../vnc.html:299\nmsgid \"Password:\"\nmsgstr \"Passwort:\"\n\n#: ../vnc.html:313\nmsgid \"Cancel\"\nmsgstr \"Abbrechen\"\n\n#: ../vnc.html:329\nmsgid \"Canvas not supported.\"\nmsgstr \"Canvas nicht unterstützt.\"\n\n#~ msgid \"Disconnect timeout\"\n#~ msgstr \"Zeitüberschreitung beim Trennen\"\n\n#~ msgid \"Local Downscaling\"\n#~ msgstr \"Lokales herunterskalieren\"\n\n#~ msgid \"Local Cursor\"\n#~ msgstr \"Lokaler Mauszeiger\"\n\n#~ msgid \"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen\"\n#~ msgstr \"'Clipping-Modus' aktiviert, Scrollbalken in 'IE-Vollbildmodus' werden nicht unterstützt\"\n\n#~ msgid \"True Color\"\n#~ msgstr \"True Color\"\n"
  },
  {
    "path": "po/el.po",
    "content": "# Greek translations for noVNC package.\n# Copyright (C) 2018 The noVNC authors\n# This file is distributed under the same license as the noVNC package.\n# Giannis Kosmas <kosmasgiannis@gmail.com>, 2016.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 0.6.1\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2022-12-27 15:24+0100\\n\"\n\"PO-Revision-Date: 2017-10-11 16:16+0200\\n\"\n\"Last-Translator: Giannis Kosmas <kosmasgiannis@gmail.com>\\n\"\n\"Language-Team: none\\n\"\n\"Language: el\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\n#: ../app/ui.js:69\nmsgid \"HTTPS is required for full functionality\"\nmsgstr \"Το HTTPS είναι απαιτούμενο για πλήρη λειτουργικότητα\"\n\n#: ../app/ui.js:410\nmsgid \"Connecting...\"\nmsgstr \"Συνδέεται...\"\n\n#: ../app/ui.js:417\nmsgid \"Disconnecting...\"\nmsgstr \"Aποσυνδέεται...\"\n\n#: ../app/ui.js:423\nmsgid \"Reconnecting...\"\nmsgstr \"Επανασυνδέεται...\"\n\n#: ../app/ui.js:428\nmsgid \"Internal error\"\nmsgstr \"Εσωτερικό σφάλμα\"\n\n#: ../app/ui.js:1026\nmsgid \"Must set host\"\nmsgstr \"Πρέπει να οριστεί ο διακομιστής\"\n\n#: ../app/ui.js:1110\nmsgid \"Connected (encrypted) to \"\nmsgstr \"Συνδέθηκε (κρυπτογραφημένα) με το \"\n\n#: ../app/ui.js:1112\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"Συνδέθηκε (μη κρυπτογραφημένα) με το \"\n\n#: ../app/ui.js:1135\nmsgid \"Something went wrong, connection is closed\"\nmsgstr \"Κάτι πήγε στραβά, η σύνδεση διακόπηκε\"\n\n#: ../app/ui.js:1138\nmsgid \"Failed to connect to server\"\nmsgstr \"Αποτυχία στη σύνδεση με το διακομιστή\"\n\n#: ../app/ui.js:1150\nmsgid \"Disconnected\"\nmsgstr \"Αποσυνδέθηκε\"\n\n#: ../app/ui.js:1165\nmsgid \"New connection has been rejected with reason: \"\nmsgstr \"Η νέα σύνδεση απορρίφθηκε διότι: \"\n\n#: ../app/ui.js:1168\nmsgid \"New connection has been rejected\"\nmsgstr \"Η νέα σύνδεση απορρίφθηκε \"\n\n#: ../app/ui.js:1234\nmsgid \"Credentials are required\"\nmsgstr \"Απαιτούνται διαπιστευτήρια\"\n\n#: ../vnc.html:57\nmsgid \"noVNC encountered an error:\"\nmsgstr \"το noVNC αντιμετώπισε ένα σφάλμα:\"\n\n#: ../vnc.html:67\nmsgid \"Hide/Show the control bar\"\nmsgstr \"Απόκρυψη/Εμφάνιση γραμμής ελέγχου\"\n\n#: ../vnc.html:76\nmsgid \"Drag\"\nmsgstr \"Σύρσιμο\"\n\n#: ../vnc.html:76\nmsgid \"Move/Drag Viewport\"\nmsgstr \"Μετακίνηση/Σύρσιμο Θεατού πεδίου\"\n\n#: ../vnc.html:82\nmsgid \"Keyboard\"\nmsgstr \"Πληκτρολόγιο\"\n\n#: ../vnc.html:82\nmsgid \"Show Keyboard\"\nmsgstr \"Εμφάνιση Πληκτρολογίου\"\n\n#: ../vnc.html:87\nmsgid \"Extra keys\"\nmsgstr \"Επιπλέον πλήκτρα\"\n\n#: ../vnc.html:87\nmsgid \"Show Extra Keys\"\nmsgstr \"Εμφάνιση Επιπλέον Πλήκτρων\"\n\n#: ../vnc.html:92\nmsgid \"Ctrl\"\nmsgstr \"Ctrl\"\n\n#: ../vnc.html:92\nmsgid \"Toggle Ctrl\"\nmsgstr \"Εναλλαγή Ctrl\"\n\n#: ../vnc.html:95\nmsgid \"Alt\"\nmsgstr \"Alt\"\n\n#: ../vnc.html:95\nmsgid \"Toggle Alt\"\nmsgstr \"Εναλλαγή Alt\"\n\n#: ../vnc.html:98\nmsgid \"Toggle Windows\"\nmsgstr \"Εναλλαγή Παράθυρων\"\n\n#: ../vnc.html:98\nmsgid \"Windows\"\nmsgstr \"Παράθυρα\"\n\n#: ../vnc.html:101\nmsgid \"Send Tab\"\nmsgstr \"Αποστολή Tab\"\n\n#: ../vnc.html:101\nmsgid \"Tab\"\nmsgstr \"Tab\"\n\n#: ../vnc.html:104\nmsgid \"Esc\"\nmsgstr \"Esc\"\n\n#: ../vnc.html:104\nmsgid \"Send Escape\"\nmsgstr \"Αποστολή Escape\"\n\n#: ../vnc.html:107\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"Ctrl+Alt+Del\"\n\n#: ../vnc.html:107\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"Αποστολή Ctrl-Alt-Del\"\n\n#: ../vnc.html:114\nmsgid \"Shutdown/Reboot\"\nmsgstr \"Κλείσιμο/Επανεκκίνηση\"\n\n#: ../vnc.html:114\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"Κλείσιμο/Επανεκκίνηση...\"\n\n#: ../vnc.html:120\nmsgid \"Power\"\nmsgstr \"Απενεργοποίηση\"\n\n#: ../vnc.html:122\nmsgid \"Shutdown\"\nmsgstr \"Κλείσιμο\"\n\n#: ../vnc.html:123\nmsgid \"Reboot\"\nmsgstr \"Επανεκκίνηση\"\n\n#: ../vnc.html:124\nmsgid \"Reset\"\nmsgstr \"Επαναφορά\"\n\n#: ../vnc.html:129 ../vnc.html:135\nmsgid \"Clipboard\"\nmsgstr \"Πρόχειρο\"\n\n#: ../vnc.html:137\nmsgid \"Edit clipboard content in the textarea below.\"\nmsgstr \"Επεξεργαστείτε το περιεχόμενο του πρόχειρου στην περιοχή κειμένου παρακάτω.\"\n\n#: ../vnc.html:145\n#, fuzzy\nmsgid \"Full Screen\"\nmsgstr \"Πλήρης Οθόνη\"\n\n#: ../vnc.html:150 ../vnc.html:156\nmsgid \"Settings\"\nmsgstr \"Ρυθμίσεις\"\n\n#: ../vnc.html:160\nmsgid \"Shared Mode\"\nmsgstr \"Κοινόχρηστη Λειτουργία\"\n\n#: ../vnc.html:163\nmsgid \"View Only\"\nmsgstr \"Μόνο Θέαση\"\n\n#: ../vnc.html:167\nmsgid \"Clip to Window\"\nmsgstr \"Αποκοπή στο όριο του Παράθυρου\"\n\n#: ../vnc.html:170\nmsgid \"Scaling Mode:\"\nmsgstr \"Λειτουργία Κλιμάκωσης:\"\n\n#: ../vnc.html:172\nmsgid \"None\"\nmsgstr \"Καμία\"\n\n#: ../vnc.html:173\nmsgid \"Local Scaling\"\nmsgstr \"Τοπική Κλιμάκωση\"\n\n#: ../vnc.html:174\nmsgid \"Remote Resizing\"\nmsgstr \"Απομακρυσμένη Αλλαγή μεγέθους\"\n\n#: ../vnc.html:179\nmsgid \"Advanced\"\nmsgstr \"Για προχωρημένους\"\n\n#: ../vnc.html:182\nmsgid \"Quality:\"\nmsgstr \"Ποιότητα:\"\n\n#: ../vnc.html:186\nmsgid \"Compression level:\"\nmsgstr \"Επίπεδο συμπίεσης:\"\n\n#: ../vnc.html:191\nmsgid \"Repeater ID:\"\nmsgstr \"Repeater ID:\"\n\n#: ../vnc.html:195\nmsgid \"WebSocket\"\nmsgstr \"WebSocket\"\n\n#: ../vnc.html:198\nmsgid \"Encrypt\"\nmsgstr \"Κρυπτογράφηση\"\n\n#: ../vnc.html:201\nmsgid \"Host:\"\nmsgstr \"Όνομα διακομιστή:\"\n\n#: ../vnc.html:205\nmsgid \"Port:\"\nmsgstr \"Πόρτα διακομιστή:\"\n\n#: ../vnc.html:209\nmsgid \"Path:\"\nmsgstr \"Διαδρομή:\"\n\n#: ../vnc.html:216\nmsgid \"Automatic Reconnect\"\nmsgstr \"Αυτόματη επανασύνδεση\"\n\n#: ../vnc.html:219\nmsgid \"Reconnect Delay (ms):\"\nmsgstr \"Καθυστέρηση επανασύνδεσης (ms):\"\n\n#: ../vnc.html:224\nmsgid \"Show Dot when No Cursor\"\nmsgstr \"Εμφάνιση Τελείας όταν δεν υπάρχει Δρομέας\"\n\n#: ../vnc.html:229\nmsgid \"Logging:\"\nmsgstr \"Καταγραφή:\"\n\n#: ../vnc.html:238\nmsgid \"Version:\"\nmsgstr \"Έκδοση:\"\n\n#: ../vnc.html:246\nmsgid \"Disconnect\"\nmsgstr \"Αποσύνδεση\"\n\n#: ../vnc.html:269\nmsgid \"Connect\"\nmsgstr \"Σύνδεση\"\n\n#: ../vnc.html:278\nmsgid \"Server identity\"\nmsgstr \"Ταυτότητα Διακομιστή\"\n\n#: ../vnc.html:281\nmsgid \"The server has provided the following identifying information:\"\nmsgstr \"Ο διακομιστής παρείχε την ακόλουθη πληροφορία ταυτοποίησης:\"\n\n#: ../vnc.html:285\nmsgid \"Fingerprint:\"\nmsgstr \"Δακτυλικό αποτύπωμα:\"\n\n#: ../vnc.html:288\nmsgid \"\"\n\"Please verify that the information is correct and press \\\"Approve\\\". \"\n\"Otherwise press \\\"Reject\\\".\"\nmsgstr \"\"\n\"Παρακαλώ επαληθεύσετε ότι η πληροφορία είναι σωστή και πιέστε \\\"Αποδοχή\\\". \"\n\"Αλλιώς πιέστε \\\"Απόρριψη\\\".\"\n\n#: ../vnc.html:293\nmsgid \"Approve\"\nmsgstr \"Αποδοχή\"\n\n#: ../vnc.html:294\nmsgid \"Reject\"\nmsgstr \"Απόρριψη\"\n\n#: ../vnc.html:302\nmsgid \"Credentials\"\nmsgstr \"Διαπιστευτήρια\"\n\n#: ../vnc.html:306\nmsgid \"Username:\"\nmsgstr \"Κωδικός Χρήστη:\"\n\n#: ../vnc.html:310\nmsgid \"Password:\"\nmsgstr \"Κωδικός Πρόσβασης:\"\n\n#: ../vnc.html:314\nmsgid \"Send Credentials\"\nmsgstr \"Αποστολή Διαπιστευτηρίων\"\n\n#: ../vnc.html:323\nmsgid \"Cancel\"\nmsgstr \"Ακύρωση\"\n\n#~ msgid \"Password is required\"\n#~ msgstr \"Απαιτείται ο κωδικός πρόσβασης\"\n\n#~ msgid \"viewport drag\"\n#~ msgstr \"σύρσιμο θεατού πεδίου\"\n\n#~ msgid \"Active Mouse Button\"\n#~ msgstr \"Ενεργό Πλήκτρο Ποντικιού\"\n\n#~ msgid \"No mousebutton\"\n#~ msgstr \"Χωρίς Πλήκτρο Ποντικιού\"\n\n#~ msgid \"Left mousebutton\"\n#~ msgstr \"Αριστερό Πλήκτρο Ποντικιού\"\n\n#~ msgid \"Middle mousebutton\"\n#~ msgstr \"Μεσαίο Πλήκτρο Ποντικιού\"\n\n#~ msgid \"Right mousebutton\"\n#~ msgstr \"Δεξί Πλήκτρο Ποντικιού\"\n\n#~ msgid \"Clear\"\n#~ msgstr \"Καθάρισμα\"\n\n#~ msgid \"Canvas not supported.\"\n#~ msgstr \"Δεν υποστηρίζεται το στοιχείο Canvas\"\n\n#~ msgid \"Disconnect timeout\"\n#~ msgstr \"Παρέλευση χρονικού ορίου αποσύνδεσης\"\n\n#~ msgid \"Local Downscaling\"\n#~ msgstr \"Τοπική Συρρίκνωση\"\n\n#~ msgid \"Local Cursor\"\n#~ msgstr \"Τοπικός Δρομέας\"\n\n#~ msgid \"\"\n#~ \"Forcing clipping mode since scrollbars aren't supported by IE in \"\n#~ \"fullscreen\"\n#~ msgstr \"\"\n#~ \"Εφαρμογή λειτουργίας αποκοπής αφού δεν υποστηρίζονται οι λωρίδες κύλισης \"\n#~ \"σε πλήρη οθόνη στον IE\"\n\n#~ msgid \"True Color\"\n#~ msgstr \"Πραγματικά Χρώματα\"\n\n#~ msgid \"Style:\"\n#~ msgstr \"Στυλ:\"\n\n#~ msgid \"default\"\n#~ msgstr \"προεπιλεγμένο\"\n\n#~ msgid \"Apply\"\n#~ msgstr \"Εφαρμογή\"\n\n#~ msgid \"Connection\"\n#~ msgstr \"Σύνδεση\"\n\n#~ msgid \"Token:\"\n#~ msgstr \"Διακριτικό:\"\n\n#~ msgid \"Send Password\"\n#~ msgstr \"Αποστολή Κωδικού Πρόσβασης\"\n"
  },
  {
    "path": "po/es.po",
    "content": "# Spanish translations for noVNC package\n# Traducciones al español para el paquete noVNC.\n# Copyright (C) 2018 The noVNC authors\n# This file is distributed under the same license as the noVNC package.\n# Juanjo Diaz <juanjo.diazmo@gmail.com>, 2018.\n# Adrian Scillato <ascillato@gmail.com>, 2021.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 1.0.0-testing.2\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2017-10-06 10:07+0200\\n\"\n\"PO-Revision-Date: 2021-04-23 12:00-0300\\n\"\n\"Last-Translator: Adrian Scillato <ascillato@gmail.com>\\n\"\n\"Language-Team: Spanish\\n\"\n\"Language: es\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\n#: ../app/ui.js:430\nmsgid \"Connecting...\"\nmsgstr \"Conectando...\"\n\n#: ../app/ui.js:438\nmsgid \"Connected (encrypted) to \"\nmsgstr \"Conectado (con encriptación) a\"\n\n#: ../app/ui.js:440\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"Conectado (sin encriptación) a\"\n\n#: ../app/ui.js:446\nmsgid \"Disconnecting...\"\nmsgstr \"Desconectando...\"\n\n#: ../app/ui.js:450\nmsgid \"Disconnected\"\nmsgstr \"Desconectado\"\n\n#: ../app/ui.js:1052 ../core/rfb.js:248\nmsgid \"Must set host\"\nmsgstr \"Se debe configurar el host\"\n\n#: ../app/ui.js:1101\nmsgid \"Reconnecting...\"\nmsgstr \"Reconectando...\"\n\n#: ../app/ui.js:1140\nmsgid \"Password is required\"\nmsgstr \"La contraseña es obligatoria\"\n\n#: ../core/rfb.js:548\nmsgid \"Disconnect timeout\"\nmsgstr \"Tiempo de desconexión agotado\"\n\n#: ../vnc.html:89\nmsgid \"noVNC encountered an error:\"\nmsgstr \"noVNC ha encontrado un error:\"\n\n#: ../vnc.html:99\nmsgid \"Hide/Show the control bar\"\nmsgstr \"Ocultar/Mostrar la barra de control\"\n\n#: ../vnc.html:106\nmsgid \"Move/Drag viewport\"\nmsgstr \"Mover/Arrastrar la ventana\"\n\n#: ../vnc.html:106\nmsgid \"viewport drag\"\nmsgstr \"Arrastrar la ventana\"\n\n#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121\nmsgid \"Active Mouse Button\"\nmsgstr \"Botón activo del ratón\"\n\n#: ../vnc.html:112\nmsgid \"No mousebutton\"\nmsgstr \"Ningún botón del ratón\"\n\n#: ../vnc.html:115\nmsgid \"Left mousebutton\"\nmsgstr \"Botón izquierdo del ratón\"\n\n#: ../vnc.html:118\nmsgid \"Middle mousebutton\"\nmsgstr \"Botón central del ratón\"\n\n#: ../vnc.html:121\nmsgid \"Right mousebutton\"\nmsgstr \"Botón derecho del ratón\"\n\n#: ../vnc.html:124\nmsgid \"Keyboard\"\nmsgstr \"Teclado\"\n\n#: ../vnc.html:124\nmsgid \"Show keyboard\"\nmsgstr \"Mostrar teclado\"\n\n#: ../vnc.html:131\nmsgid \"Extra keys\"\nmsgstr \"Teclas adicionales\"\n\n#: ../vnc.html:131\nmsgid \"Show Extra Keys\"\nmsgstr \"Mostrar Teclas Adicionales\"\n\n#: ../vnc.html:136\nmsgid \"Ctrl\"\nmsgstr \"Ctrl\"\n\n#: ../vnc.html:136\nmsgid \"Toggle Ctrl\"\nmsgstr \"Pulsar/Soltar Ctrl\"\n\n#: ../vnc.html:139\nmsgid \"Alt\"\nmsgstr \"Alt\"\n\n#: ../vnc.html:139\nmsgid \"Toggle Alt\"\nmsgstr \"Pulsar/Soltar Alt\"\n\n#: ../vnc.html:142\nmsgid \"Send Tab\"\nmsgstr \"Enviar Tabulación\"\n\n#: ../vnc.html:142\nmsgid \"Tab\"\nmsgstr \"Tabulación\"\n\n#: ../vnc.html:145\nmsgid \"Esc\"\nmsgstr \"Esc\"\n\n#: ../vnc.html:145\nmsgid \"Send Escape\"\nmsgstr \"Enviar Escape\"\n\n#: ../vnc.html:148\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"Ctrl+Alt+Del\"\n\n#: ../vnc.html:148\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"Enviar Ctrl+Alt+Del\"\n\n#: ../vnc.html:156\nmsgid \"Shutdown/Reboot\"\nmsgstr \"Apagar/Reiniciar\"\n\n#: ../vnc.html:156\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"Apagar/Reiniciar...\"\n\n#: ../vnc.html:162\nmsgid \"Power\"\nmsgstr \"Encender\"\n\n#: ../vnc.html:164\nmsgid \"Shutdown\"\nmsgstr \"Apagar\"\n\n#: ../vnc.html:165\nmsgid \"Reboot\"\nmsgstr \"Reiniciar\"\n\n#: ../vnc.html:166\nmsgid \"Reset\"\nmsgstr \"Restablecer\"\n\n#: ../vnc.html:171 ../vnc.html:177\nmsgid \"Clipboard\"\nmsgstr \"Portapapeles\"\n\n#: ../vnc.html:181\nmsgid \"Clear\"\nmsgstr \"Vaciar\"\n\n#: ../vnc.html:187\nmsgid \"Fullscreen\"\nmsgstr \"Pantalla Completa\"\n\n#: ../vnc.html:192 ../vnc.html:199\nmsgid \"Settings\"\nmsgstr \"Configuraciones\"\n\n#: ../vnc.html:200\nmsgid \"Encrypt\"\nmsgstr \"Encriptar\"\n\n#: ../vnc.html:202\nmsgid \"Shared Mode\"\nmsgstr \"Modo Compartido\"\n\n#: ../vnc.html:205\nmsgid \"View only\"\nmsgstr \"Solo visualización\"\n\n#: ../vnc.html:209\nmsgid \"Clip to window\"\nmsgstr \"Recortar al tamaño de la ventana\"\n\n#: ../vnc.html:212\nmsgid \"Scaling mode:\"\nmsgstr \"Modo de escalado:\"\n\n#: ../vnc.html:214\nmsgid \"None\"\nmsgstr \"Ninguno\"\n\n#: ../vnc.html:215\nmsgid \"Local Scaling\"\nmsgstr \"Escalado Local\"\n\n#: ../vnc.html:216\nmsgid \"Local Downscaling\"\nmsgstr \"Reducción de escala local\"\n\n#: ../vnc.html:217\nmsgid \"Remote resizing\"\nmsgstr \"Cambio de tamaño remoto\"\n\n#: ../vnc.html:222\nmsgid \"Advanced\"\nmsgstr \"Avanzado\"\n\n#: ../vnc.html:225\nmsgid \"Local Cursor\"\nmsgstr \"Cursor Local\"\n\n#: ../vnc.html:229\nmsgid \"Repeater ID:\"\nmsgstr \"ID del Repetidor:\"\n\n#: ../vnc.html:233\nmsgid \"WebSocket\"\nmsgstr \"WebSocket\"\n\n#: ../vnc.html:239\nmsgid \"Host:\"\nmsgstr \"Host:\"\n\n#: ../vnc.html:243\nmsgid \"Port:\"\nmsgstr \"Puerto:\"\n\n#: ../vnc.html:247\nmsgid \"Path:\"\nmsgstr \"Ruta:\"\n\n#: ../vnc.html:254\nmsgid \"Automatic reconnect\"\nmsgstr \"Reconexión automática\"\n\n#: ../vnc.html:257\nmsgid \"Reconnect delay (ms):\"\nmsgstr \"Retraso en la reconexión (ms):\"\n\n#: ../vnc.html:263\nmsgid \"Logging:\"\nmsgstr \"Registrando:\"\n\n#: ../vnc.html:275\nmsgid \"Disconnect\"\nmsgstr \"Desconectar\"\n\n#: ../vnc.html:294\nmsgid \"Connect\"\nmsgstr \"Conectar\"\n\n#: ../vnc.html:304\nmsgid \"Password:\"\nmsgstr \"Contraseña:\"\n\n#: ../vnc.html:318\nmsgid \"Cancel\"\nmsgstr \"Cancelar\"\n\n#: ../vnc.html:334\nmsgid \"Canvas not supported.\"\nmsgstr \"Canvas no soportado.\"\n"
  },
  {
    "path": "po/fr.po",
    "content": "# French translations for noVNC package\n# Traductions françaises du paquet noVNC.\n# Copyright (C) 2021 The noVNC authors\n# This file is distributed under the same license as the noVNC package.\n# Jose <jose.matsuda@canada.ca>, 2021.\n# Lowxorx <lowxorx@lahan.fr>, 2022.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 1.6.0\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2025-02-14 10:14+0100\\n\"\n\"PO-Revision-Date: 2025-02-17 10:04+0100\\n\"\n\"Last-Translator: Martine & Philippe <martineke.breizh@gmail.com>\\n\"\n\"Language-Team: French\\n\"\n\"Language: fr\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n > 1);\\n\"\n\"X-Generator: Poedit 3.5\\n\"\n\n#: ../app/ui.js:84\nmsgid \"\"\n\"Running without HTTPS is not recommended, crashes or other issues are likely.\"\nmsgstr \"\"\n\"Lancer sans HTTPS n'est pas recommandé, crashs ou autres problèmes en vue.\"\n\n#: ../app/ui.js:413\nmsgid \"Connecting...\"\nmsgstr \"En cours de connexion...\"\n\n#: ../app/ui.js:420\nmsgid \"Disconnecting...\"\nmsgstr \"Déconnexion en cours...\"\n\n#: ../app/ui.js:426\nmsgid \"Reconnecting...\"\nmsgstr \"Reconnexion en cours...\"\n\n#: ../app/ui.js:431\nmsgid \"Internal error\"\nmsgstr \"Erreur interne\"\n\n#: ../app/ui.js:1079\nmsgid \"Failed to connect to server: \"\nmsgstr \"Échec de connexion au serveur \"\n\n#: ../app/ui.js:1145\nmsgid \"Connected (encrypted) to \"\nmsgstr \"Connecté (chiffré) à \"\n\n#: ../app/ui.js:1147\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"Connecté (non chiffré) à \"\n\n#: ../app/ui.js:1170\nmsgid \"Something went wrong, connection is closed\"\nmsgstr \"Quelque chose s'est mal passé, la connexion a été fermée\"\n\n#: ../app/ui.js:1173\nmsgid \"Failed to connect to server\"\nmsgstr \"Échec de connexion au serveur\"\n\n#: ../app/ui.js:1185\nmsgid \"Disconnected\"\nmsgstr \"Déconnecté\"\n\n#: ../app/ui.js:1200\nmsgid \"New connection has been rejected with reason: \"\nmsgstr \"Une nouvelle connexion a été rejetée avec motif : \"\n\n#: ../app/ui.js:1203\nmsgid \"New connection has been rejected\"\nmsgstr \"Une nouvelle connexion a été rejetée\"\n\n#: ../app/ui.js:1269\nmsgid \"Credentials are required\"\nmsgstr \"Les identifiants sont requis\"\n\n#: ../vnc.html:106\nmsgid \"noVNC encountered an error:\"\nmsgstr \"noVNC a rencontré une erreur :\"\n\n#: ../vnc.html:116\nmsgid \"Hide/Show the control bar\"\nmsgstr \"Masquer/Afficher la barre de contrôle\"\n\n#: ../vnc.html:125\nmsgid \"Drag\"\nmsgstr \"Faire glisser\"\n\n#: ../vnc.html:125\nmsgid \"Move/Drag viewport\"\nmsgstr \"Déplacer la fenêtre de visualisation\"\n\n#: ../vnc.html:131\nmsgid \"Keyboard\"\nmsgstr \"Clavier\"\n\n#: ../vnc.html:131\nmsgid \"Show keyboard\"\nmsgstr \"Afficher le clavier\"\n\n#: ../vnc.html:136\nmsgid \"Extra keys\"\nmsgstr \"Touches supplémentaires\"\n\n#: ../vnc.html:136\nmsgid \"Show extra keys\"\nmsgstr \"Afficher les touches supplémentaires\"\n\n#: ../vnc.html:141\nmsgid \"Ctrl\"\nmsgstr \"Ctrl\"\n\n#: ../vnc.html:141\nmsgid \"Toggle Ctrl\"\nmsgstr \"Basculer Ctrl\"\n\n#: ../vnc.html:144\nmsgid \"Alt\"\nmsgstr \"Alt\"\n\n#: ../vnc.html:144\nmsgid \"Toggle Alt\"\nmsgstr \"Basculer Alt\"\n\n#: ../vnc.html:147\nmsgid \"Toggle Windows\"\nmsgstr \"Basculer Windows\"\n\n#: ../vnc.html:147\nmsgid \"Windows\"\nmsgstr \"Fenêtre\"\n\n#: ../vnc.html:150\nmsgid \"Send Tab\"\nmsgstr \"Envoyer Tab\"\n\n#: ../vnc.html:150\nmsgid \"Tab\"\nmsgstr \"Tabulation\"\n\n#: ../vnc.html:153\nmsgid \"Esc\"\nmsgstr \"Esc\"\n\n#: ../vnc.html:153\nmsgid \"Send Escape\"\nmsgstr \"Envoyer Escape\"\n\n#: ../vnc.html:156\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"Ctrl+Alt+Del\"\n\n#: ../vnc.html:156\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"Envoyer Ctrl-Alt-Del\"\n\n#: ../vnc.html:163\nmsgid \"Shutdown/Reboot\"\nmsgstr \"Arrêter/Redémarrer\"\n\n#: ../vnc.html:163\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"Arrêter/Redémarrer...\"\n\n#: ../vnc.html:169\nmsgid \"Power\"\nmsgstr \"Alimentation\"\n\n#: ../vnc.html:171\nmsgid \"Shutdown\"\nmsgstr \"Arrêter\"\n\n#: ../vnc.html:172\nmsgid \"Reboot\"\nmsgstr \"Redémarrer\"\n\n#: ../vnc.html:173\nmsgid \"Reset\"\nmsgstr \"Réinitialiser\"\n\n#: ../vnc.html:178 ../vnc.html:184\nmsgid \"Clipboard\"\nmsgstr \"Presse-papiers\"\n\n#: ../vnc.html:186\nmsgid \"Edit clipboard content in the textarea below.\"\nmsgstr \"Editer le contenu du presse-papier dans la zone ci-dessous.\"\n\n#: ../vnc.html:194\nmsgid \"Full screen\"\nmsgstr \"Plein écran\"\n\n#: ../vnc.html:199 ../vnc.html:205\nmsgid \"Settings\"\nmsgstr \"Paramètres\"\n\n#: ../vnc.html:211\nmsgid \"Shared mode\"\nmsgstr \"Mode partagé\"\n\n#: ../vnc.html:218\nmsgid \"View only\"\nmsgstr \"Afficher uniquement\"\n\n#: ../vnc.html:226\nmsgid \"Clip to window\"\nmsgstr \"Ajuster à la fenêtre\"\n\n#: ../vnc.html:231\nmsgid \"Scaling mode:\"\nmsgstr \"Mode mise à l'échelle :\"\n\n#: ../vnc.html:233\nmsgid \"None\"\nmsgstr \"Aucun\"\n\n#: ../vnc.html:234\nmsgid \"Local scaling\"\nmsgstr \"Mise à l'échelle locale\"\n\n#: ../vnc.html:235\nmsgid \"Remote resizing\"\nmsgstr \"Redimensionnement à distance\"\n\n#: ../vnc.html:240\nmsgid \"Advanced\"\nmsgstr \"Avancé\"\n\n#: ../vnc.html:243\nmsgid \"Quality:\"\nmsgstr \"Qualité :\"\n\n#: ../vnc.html:247\nmsgid \"Compression level:\"\nmsgstr \"Niveau de compression :\"\n\n#: ../vnc.html:252\nmsgid \"Repeater ID:\"\nmsgstr \"ID Répéteur :\"\n\n#: ../vnc.html:256\nmsgid \"WebSocket\"\nmsgstr \"WebSocket\"\n\n#: ../vnc.html:261\nmsgid \"Encrypt\"\nmsgstr \"Chiffrer\"\n\n#: ../vnc.html:266\nmsgid \"Host:\"\nmsgstr \"Hôte :\"\n\n#: ../vnc.html:270\nmsgid \"Port:\"\nmsgstr \"Port :\"\n\n#: ../vnc.html:274\nmsgid \"Path:\"\nmsgstr \"Chemin :\"\n\n#: ../vnc.html:283\nmsgid \"Automatic reconnect\"\nmsgstr \"Reconnecter automatiquement\"\n\n#: ../vnc.html:288\nmsgid \"Reconnect delay (ms):\"\nmsgstr \"Délai de reconnexion (ms) :\"\n\n#: ../vnc.html:295\nmsgid \"Show dot when no cursor\"\nmsgstr \"Afficher le point lorsqu'il n'y a pas de curseur\"\n\n#: ../vnc.html:302\nmsgid \"Logging:\"\nmsgstr \"Se connecter :\"\n\n#: ../vnc.html:311\nmsgid \"Version:\"\nmsgstr \"Version :\"\n\n#: ../vnc.html:319\nmsgid \"Disconnect\"\nmsgstr \"Déconnecter\"\n\n#: ../vnc.html:342\nmsgid \"Connect\"\nmsgstr \"Connecter\"\n\n#: ../vnc.html:351\nmsgid \"Server identity\"\nmsgstr \"Identité du serveur\"\n\n#: ../vnc.html:354\nmsgid \"The server has provided the following identifying information:\"\nmsgstr \"Le serveur a fourni l'identification suivante :\"\n\n#: ../vnc.html:357\nmsgid \"Fingerprint:\"\nmsgstr \"Empreinte digitale :\"\n\n#: ../vnc.html:361\nmsgid \"\"\n\"Please verify that the information is correct and press \\\"Approve\\\". \"\n\"Otherwise press \\\"Reject\\\".\"\nmsgstr \"\"\n\"SVP, verifiez que l'information est correcte et pressez \\\"Accepter\\\". Sinon \"\n\"pressez \\\"Refuser\\\".\"\n\n#: ../vnc.html:366\nmsgid \"Approve\"\nmsgstr \"Accepter\"\n\n#: ../vnc.html:367\nmsgid \"Reject\"\nmsgstr \"Refuser\"\n\n#: ../vnc.html:375\nmsgid \"Credentials\"\nmsgstr \"Envoyer les identifiants\"\n\n#: ../vnc.html:379\nmsgid \"Username:\"\nmsgstr \"Nom d'utilisateur :\"\n\n#: ../vnc.html:383\nmsgid \"Password:\"\nmsgstr \"Mot de passe :\"\n\n#: ../vnc.html:387\nmsgid \"Send credentials\"\nmsgstr \"Envoyer les identifiants\"\n\n#: ../vnc.html:396\nmsgid \"Cancel\"\nmsgstr \"Annuler\"\n\n#~ msgid \"Must set host\"\n#~ msgstr \"Doit définir l'hôte\"\n\n#~ msgid \"Clear\"\n#~ msgstr \"Effacer\"\n"
  },
  {
    "path": "po/hr.po",
    "content": "# Croatian translations for noVNC package\n# Hrvatski prijevod za noVNC paket\n# Copyright (C) 2025 The noVNC authors\n# This file is distributed under the same license as the noVNC package.\n# Milo Ivir <mail@milotype.de>, 2025.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 1.6.0\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2025-02-14 10:14+0100\\n\"\n\"PO-Revision-Date: 2025-08-25 18:24+0200\\n\"\n\"Last-Translator: Milo Ivir <mail@mivirtype.de>\\n\"\n\"Language-Team: \\n\"\n\"Language: hr\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: Poedit 3.7\\n\"\n\n#: ../app/ui.js:84\nmsgid \"\"\n\"Running without HTTPS is not recommended, crashes or other issues are likely.\"\nmsgstr \"\"\n\"Pokretanje bez HTTPS-a se ne preporučuje, vjerojatno će se dogoditi prekidi \"\n\"rada ili drugi problemi.\"\n\n#: ../app/ui.js:413\nmsgid \"Connecting...\"\nmsgstr \"Povezivanje …\"\n\n#: ../app/ui.js:420\nmsgid \"Disconnecting...\"\nmsgstr \"Odspajanje …\"\n\n#: ../app/ui.js:426\nmsgid \"Reconnecting...\"\nmsgstr \"Ponovno povezivanje …\"\n\n#: ../app/ui.js:431\nmsgid \"Internal error\"\nmsgstr \"Interna greška\"\n\n#: ../app/ui.js:1079\nmsgid \"Failed to connect to server: \"\nmsgstr \"Povezivanje sa serverom nije uspjelo: \"\n\n#: ../app/ui.js:1145\nmsgid \"Connected (encrypted) to \"\nmsgstr \"Povezano (šifrirano) na \"\n\n#: ../app/ui.js:1147\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"Povezano (nešifrirano) na \"\n\n#: ../app/ui.js:1170\nmsgid \"Something went wrong, connection is closed\"\nmsgstr \"Nešto nije u redu, veza je zatvorena\"\n\n#: ../app/ui.js:1173\nmsgid \"Failed to connect to server\"\nmsgstr \"Povezivanje sa serverom nije uspjelo\"\n\n#: ../app/ui.js:1185\nmsgid \"Disconnected\"\nmsgstr \"Odspojeno\"\n\n#: ../app/ui.js:1200\nmsgid \"New connection has been rejected with reason: \"\nmsgstr \"Nova veza je odbijena s razlogom: \"\n\n#: ../app/ui.js:1203\nmsgid \"New connection has been rejected\"\nmsgstr \"Nova veza je odbijena\"\n\n#: ../app/ui.js:1269\nmsgid \"Credentials are required\"\nmsgstr \"Podaci za prijavu su obavezni\"\n\n#: ../vnc.html:106\nmsgid \"noVNC encountered an error:\"\nmsgstr \"noVNC je naišao na grešku:\"\n\n#: ../vnc.html:116\nmsgid \"Hide/Show the control bar\"\nmsgstr \"Sakrij/Prikaži traku kontrola\"\n\n#: ../vnc.html:125\nmsgid \"Drag\"\nmsgstr \"Povuci\"\n\n#: ../vnc.html:125\nmsgid \"Move/Drag viewport\"\nmsgstr \"Pomakni/Povuci vidljivo područje\"\n\n#: ../vnc.html:131\nmsgid \"Keyboard\"\nmsgstr \"Tipkovnica\"\n\n#: ../vnc.html:131\nmsgid \"Show keyboard\"\nmsgstr \"Prikaži tipkovnicu\"\n\n#: ../vnc.html:136\nmsgid \"Extra keys\"\nmsgstr \"Dodatne tipke\"\n\n#: ../vnc.html:136\nmsgid \"Show extra keys\"\nmsgstr \"Prikaži dodatne tipke\"\n\n#: ../vnc.html:141\nmsgid \"Ctrl\"\nmsgstr \"Ctrl\"\n\n#: ../vnc.html:141\nmsgid \"Toggle Ctrl\"\nmsgstr \"Uključi/Isključi Ctrl\"\n\n#: ../vnc.html:144\nmsgid \"Alt\"\nmsgstr \"Alt\"\n\n#: ../vnc.html:144\nmsgid \"Toggle Alt\"\nmsgstr \"Uključi/Isključi Alt\"\n\n#: ../vnc.html:147\nmsgid \"Toggle Windows\"\nmsgstr \"Uključi/Isključi Windows\"\n\n#: ../vnc.html:147\nmsgid \"Windows\"\nmsgstr \"Windows\"\n\n#: ../vnc.html:150\nmsgid \"Send Tab\"\nmsgstr \"Pošalji tabulator\"\n\n#: ../vnc.html:150\nmsgid \"Tab\"\nmsgstr \"Tabulator\"\n\n#: ../vnc.html:153\nmsgid \"Esc\"\nmsgstr \"Esc\"\n\n#: ../vnc.html:153\nmsgid \"Send Escape\"\nmsgstr \"Pošalji Escape\"\n\n#: ../vnc.html:156\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"Ctrl + Alt + Del\"\n\n#: ../vnc.html:156\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"Pošalji Ctrl+Alt+Del\"\n\n#: ../vnc.html:163\nmsgid \"Shutdown/Reboot\"\nmsgstr \"Isključi/Ponovo pokreni\"\n\n#: ../vnc.html:163\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"Isključi/Ponovo pokreni …\"\n\n#: ../vnc.html:169\nmsgid \"Power\"\nmsgstr \"Napajanje\"\n\n#: ../vnc.html:171\nmsgid \"Shutdown\"\nmsgstr \"Isključi\"\n\n#: ../vnc.html:172\nmsgid \"Reboot\"\nmsgstr \"Ponovo pokreni\"\n\n#: ../vnc.html:173\nmsgid \"Reset\"\nmsgstr \"Resetiraj\"\n\n#: ../vnc.html:178 ../vnc.html:184\nmsgid \"Clipboard\"\nmsgstr \"Međuspremnik\"\n\n#: ../vnc.html:186\nmsgid \"Edit clipboard content in the textarea below.\"\nmsgstr \"Uredi sadržaj međuspremnika u donjem području teksta.\"\n\n#: ../vnc.html:194\nmsgid \"Full screen\"\nmsgstr \"Cjeloekranski prikaz\"\n\n#: ../vnc.html:199 ../vnc.html:205\nmsgid \"Settings\"\nmsgstr \"Postavke\"\n\n#: ../vnc.html:211\nmsgid \"Shared mode\"\nmsgstr \"Dijeljeni modus\"\n\n#: ../vnc.html:218\nmsgid \"View only\"\nmsgstr \"Samo prikaz\"\n\n#: ../vnc.html:226\nmsgid \"Clip to window\"\nmsgstr \"Isijeci na veličinu prozora\"\n\n#: ../vnc.html:231\nmsgid \"Scaling mode:\"\nmsgstr \"Modus skaliranja:\"\n\n#: ../vnc.html:233\nmsgid \"None\"\nmsgstr \"Bez\"\n\n#: ../vnc.html:234\nmsgid \"Local scaling\"\nmsgstr \"Lokalno skaliranje\"\n\n#: ../vnc.html:235\nmsgid \"Remote resizing\"\nmsgstr \"Daljinsko mijenjanje veličine\"\n\n#: ../vnc.html:240\nmsgid \"Advanced\"\nmsgstr \"Napredno\"\n\n#: ../vnc.html:243\nmsgid \"Quality:\"\nmsgstr \"Kvaliteta:\"\n\n#: ../vnc.html:247\nmsgid \"Compression level:\"\nmsgstr \"Razina kompresije:\"\n\n#: ../vnc.html:252\nmsgid \"Repeater ID:\"\nmsgstr \"ID repetitora:\"\n\n#: ../vnc.html:256\nmsgid \"WebSocket\"\nmsgstr \"WebSocket\"\n\n#: ../vnc.html:261\nmsgid \"Encrypt\"\nmsgstr \"Šifriraj\"\n\n#: ../vnc.html:266\nmsgid \"Host:\"\nmsgstr \"Host:\"\n\n#: ../vnc.html:270\nmsgid \"Port:\"\nmsgstr \"Priključak:\"\n\n#: ../vnc.html:274\nmsgid \"Path:\"\nmsgstr \"Putanja:\"\n\n#: ../vnc.html:283\nmsgid \"Automatic reconnect\"\nmsgstr \"Automatsko ponovno povezivanje\"\n\n#: ../vnc.html:288\nmsgid \"Reconnect delay (ms):\"\nmsgstr \"Kašnjenje ponovnog povezivanja (ms):\"\n\n#: ../vnc.html:295\nmsgid \"Show dot when no cursor\"\nmsgstr \"Prikaži točku kada nema pokazivača\"\n\n#: ../vnc.html:302\nmsgid \"Logging:\"\nmsgstr \"Zapisivanje:\"\n\n#: ../vnc.html:311\nmsgid \"Version:\"\nmsgstr \"Verzija:\"\n\n#: ../vnc.html:319\nmsgid \"Disconnect\"\nmsgstr \"Odspoji\"\n\n#: ../vnc.html:342\nmsgid \"Connect\"\nmsgstr \"Poveži\"\n\n#: ../vnc.html:351\nmsgid \"Server identity\"\nmsgstr \"Identitet servera\"\n\n#: ../vnc.html:354\nmsgid \"The server has provided the following identifying information:\"\nmsgstr \"Server je pružio sljedeće identifikacijske podatke:\"\n\n#: ../vnc.html:357\nmsgid \"Fingerprint:\"\nmsgstr \"Otisak:\"\n\n#: ../vnc.html:361\nmsgid \"\"\n\"Please verify that the information is correct and press \\\"Approve\\\". \"\n\"Otherwise press \\\"Reject\\\".\"\nmsgstr \"\"\n\"Provjeri jesu li podaci točni i pritisni „Odobri“. U suprotnom pritisni \"\n\"„Odbaci“.\"\n\n#: ../vnc.html:366\nmsgid \"Approve\"\nmsgstr \"Odobri\"\n\n#: ../vnc.html:367\nmsgid \"Reject\"\nmsgstr \"Odbij\"\n\n#: ../vnc.html:375\nmsgid \"Credentials\"\nmsgstr \"Podaci za prijavu\"\n\n#: ../vnc.html:379\nmsgid \"Username:\"\nmsgstr \"Korisničko ime:\"\n\n#: ../vnc.html:383\nmsgid \"Password:\"\nmsgstr \"Lozinka:\"\n\n#: ../vnc.html:387\nmsgid \"Send credentials\"\nmsgstr \"Pošalji podatke za prijavu\"\n\n#: ../vnc.html:396\nmsgid \"Cancel\"\nmsgstr \"Odustani\"\n"
  },
  {
    "path": "po/hu.po",
    "content": "# Hungarian translations for noVNC package.\n# Copyright (C) 2025 The noVNC authors\n# This file is distributed under the same license as the noVNC package.\n# Daniel Felso <danielfelso@protonmail.com>, 2025.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 1.6.0\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2025-02-14 10:14+0100\\n\"\n\"PO-Revision-Date: 2025-10-06 14:38+0200\\n\"\n\"Last-Translator: Daniel Felso <danielfelso@protonmail.com>\\n\"\n\"Language-Team: \\n\"\n\"Language: hu\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\n#: ../app/ui.js:84\nmsgid \"\"\n\"Running without HTTPS is not recommended, crashes or other issues are likely.\"\nmsgstr \"HTTPS nélkül futtatni nem ajánlott, összeomlások vagy más problémák várhatók.\"\n\n#: ../app/ui.js:413\nmsgid \"Connecting...\"\nmsgstr \"Kapcsolódás...\"\n\n#: ../app/ui.js:420\nmsgid \"Disconnecting...\"\nmsgstr \"Kapcsolat bontása...\"\n\n#: ../app/ui.js:426\nmsgid \"Reconnecting...\"\nmsgstr \"Újrakapcsolódás...\"\n\n#: ../app/ui.js:431\nmsgid \"Internal error\"\nmsgstr \"Belső hiba\"\n\n#: ../app/ui.js:1079\nmsgid \"Failed to connect to server: \"\nmsgstr \"Nem sikerült csatlakozni a szerverhez: \"\n\n#: ../app/ui.js:1145\nmsgid \"Connected (encrypted) to \"\nmsgstr \"Kapcsolódva (titkosítva) ehhez: \"\n\n#: ../app/ui.js:1147\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"Kapcsolódva (titkosítatlanul) ehhez: \"\n\n#: ../app/ui.js:1170\nmsgid \"Something went wrong, connection is closed\"\nmsgstr \"Valami hiba történt, a kapcsolat lezárult\"\n\n#: ../app/ui.js:1173\nmsgid \"Failed to connect to server\"\nmsgstr \"Nem sikerült csatlakozni a szerverhez\"\n\n#: ../app/ui.js:1185\nmsgid \"Disconnected\"\nmsgstr \"Kapcsolat bontva\"\n\n#: ../app/ui.js:1200\nmsgid \"New connection has been rejected with reason: \"\nmsgstr \"Az új kapcsolat elutasítva, indok: \"\n\n#: ../app/ui.js:1203\nmsgid \"New connection has been rejected\"\nmsgstr \"Az új kapcsolat elutasítva\"\n\n#: ../app/ui.js:1269\nmsgid \"Credentials are required\"\nmsgstr \"Hitelesítő adatok szükségesek\"\n\n#: ../vnc.html:106\nmsgid \"noVNC encountered an error:\"\nmsgstr \"A noVNC hibát észlelt:\"\n\n#: ../vnc.html:116\nmsgid \"Hide/Show the control bar\"\nmsgstr \"Vezérlősáv elrejtése/megjelenítése\"\n\n#: ../vnc.html:125\nmsgid \"Drag\"\nmsgstr \"Húzás\"\n\n#: ../vnc.html:125\nmsgid \"Move/Drag viewport\"\nmsgstr \"Nézet mozgatása/húzása\"\n\n#: ../vnc.html:131\nmsgid \"Keyboard\"\nmsgstr \"Billentyűzet\"\n\n#: ../vnc.html:131\nmsgid \"Show keyboard\"\nmsgstr \"Billentyűzet megjelenítése\"\n\n#: ../vnc.html:136\nmsgid \"Extra keys\"\nmsgstr \"Extra billentyűk\"\n\n#: ../vnc.html:136\nmsgid \"Show extra keys\"\nmsgstr \"Extra billentyűk megjelenítése\"\n\n#: ../vnc.html:141\nmsgid \"Ctrl\"\nmsgstr \"Ctrl\"\n\n#: ../vnc.html:141\nmsgid \"Toggle Ctrl\"\nmsgstr \"Ctrl lenyomása/felengedése\"\n\n#: ../vnc.html:144\nmsgid \"Alt\"\nmsgstr \"Alt\"\n\n#: ../vnc.html:144\nmsgid \"Toggle Alt\"\nmsgstr \"Alt lenyomása/felengedése\"\n\n#: ../vnc.html:147\nmsgid \"Toggle Windows\"\nmsgstr \"Windows lenyomása/felengedése\"\n\n#: ../vnc.html:147\nmsgid \"Windows\"\nmsgstr \"Windows\"\n\n#: ../vnc.html:150\nmsgid \"Send Tab\"\nmsgstr \"Tab küldése\"\n\n#: ../vnc.html:150\nmsgid \"Tab\"\nmsgstr \"Tab\"\n\n#: ../vnc.html:153\nmsgid \"Esc\"\nmsgstr \"Esc\"\n\n#: ../vnc.html:153\nmsgid \"Send Escape\"\nmsgstr \"Escape küldése\"\n\n#: ../vnc.html:156\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"Ctrl+Alt+Del\"\n\n#: ../vnc.html:156\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"Ctrl-Alt-Del küldése\"\n\n#: ../vnc.html:163\nmsgid \"Shutdown/Reboot\"\nmsgstr \"Leállítás/Újraindítás\"\n\n#: ../vnc.html:163\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"Leállítás/Újraindítás...\"\n\n#: ../vnc.html:169\nmsgid \"Power\"\nmsgstr \"Bekapcsolás\"\n\n#: ../vnc.html:171\nmsgid \"Shutdown\"\nmsgstr \"Leállítás\"\n\n#: ../vnc.html:172\nmsgid \"Reboot\"\nmsgstr \"Újraindítás\"\n\n#: ../vnc.html:173\nmsgid \"Reset\"\nmsgstr \"Reset\"\n\n#: ../vnc.html:178 ../vnc.html:184\nmsgid \"Clipboard\"\nmsgstr \"Vágólap\"\n\n#: ../vnc.html:186\nmsgid \"Edit clipboard content in the textarea below.\"\nmsgstr \"Itt tudod módosítani a vágólap tartalmát.\"\n\n#: ../vnc.html:194\nmsgid \"Full screen\"\nmsgstr \"Teljes képernyő\"\n\n#: ../vnc.html:199 ../vnc.html:205\nmsgid \"Settings\"\nmsgstr \"Beállítások\"\n\n#: ../vnc.html:211\nmsgid \"Shared mode\"\nmsgstr \"Megosztott mód\"\n\n#: ../vnc.html:218\nmsgid \"View only\"\nmsgstr \"Csak megtekintés\"\n\n#: ../vnc.html:226\nmsgid \"Clip to window\"\nmsgstr \"Ablakhoz igazítás\"\n\n#: ../vnc.html:231\nmsgid \"Scaling mode:\"\nmsgstr \"Méretezési mód:\"\n\n#: ../vnc.html:233\nmsgid \"None\"\nmsgstr \"Nincs\"\n\n#: ../vnc.html:234\nmsgid \"Local scaling\"\nmsgstr \"Helyi méretezés\"\n\n#: ../vnc.html:235\nmsgid \"Remote resizing\"\nmsgstr \"Távoli átméretezés\"\n\n#: ../vnc.html:240\nmsgid \"Advanced\"\nmsgstr \"Speciális\"\n\n#: ../vnc.html:243\nmsgid \"Quality:\"\nmsgstr \"Minőség:\"\n\n#: ../vnc.html:247\nmsgid \"Compression level:\"\nmsgstr \"Tömörítési szint:\"\n\n#: ../vnc.html:252\nmsgid \"Repeater ID:\"\nmsgstr \"Ismétlő azonosító:\"\n\n#: ../vnc.html:256\nmsgid \"WebSocket\"\nmsgstr \"WebSocket\"\n\n#: ../vnc.html:261\nmsgid \"Encrypt\"\nmsgstr \"Titkosítás\"\n\n#: ../vnc.html:266\nmsgid \"Host:\"\nmsgstr \"Hoszt:\"\n\n#: ../vnc.html:270\nmsgid \"Port:\"\nmsgstr \"Port:\"\n\n#: ../vnc.html:274\nmsgid \"Path:\"\nmsgstr \"Útvonal:\"\n\n#: ../vnc.html:283\nmsgid \"Automatic reconnect\"\nmsgstr \"Automatikus újracsatlakozás\"\n\n#: ../vnc.html:288\nmsgid \"Reconnect delay (ms):\"\nmsgstr \"Újracsatlakozás késleltetése (ms):\"\n\n#: ../vnc.html:295\nmsgid \"Show dot when no cursor\"\nmsgstr \"Kurzor hiányában pont mutatása\"\n\n#: ../vnc.html:302\nmsgid \"Logging:\"\nmsgstr \"Naplózás:\"\n\n#: ../vnc.html:311\nmsgid \"Version:\"\nmsgstr \"Verzió:\"\n\n#: ../vnc.html:319\nmsgid \"Disconnect\"\nmsgstr \"Kapcsolat bontása\"\n\n#: ../vnc.html:342\nmsgid \"Connect\"\nmsgstr \"Csatlakozás\"\n\n#: ../vnc.html:351\nmsgid \"Server identity\"\nmsgstr \"Szerver azonosító\"\n\n#: ../vnc.html:354\nmsgid \"The server has provided the following identifying information:\"\nmsgstr \"A szerver a következő azonosító információt adta meg:\"\n\n#: ../vnc.html:357\nmsgid \"Fingerprint:\"\nmsgstr \"Ujjlenyomat:\"\n\n#: ../vnc.html:361\nmsgid \"\"\n\"Please verify that the information is correct and press \\\"Approve\\\". \"\n\"Otherwise press \\\"Reject\\\".\"\nmsgstr \"Ellenőrizze, hogy az információ helyes-e és nyomja meg a \\\"Jóváhagyás\\\" gombot. Ellenkező esetben nyomja meg az \\\"Elutasítás\\\" gombot.\"\n\n#: ../vnc.html:366\nmsgid \"Approve\"\nmsgstr \"Jóváhagyás\"\n\n#: ../vnc.html:367\nmsgid \"Reject\"\nmsgstr \"Elutasítás\"\n\n#: ../vnc.html:375\nmsgid \"Credentials\"\nmsgstr \"Hitelesítő adatok\"\n\n#: ../vnc.html:379\nmsgid \"Username:\"\nmsgstr \"Felhasználónév:\"\n\n#: ../vnc.html:383\nmsgid \"Password:\"\nmsgstr \"Jelszó:\"\n\n#: ../vnc.html:387\nmsgid \"Send credentials\"\nmsgstr \"Hitelesítő adatok küldése\"\n\n#: ../vnc.html:396\nmsgid \"Cancel\"\nmsgstr \"Mégse\"\n"
  },
  {
    "path": "po/it.po",
    "content": "# Italian translations for noVNC\n# Traduzione italiana di noVNC\n# Copyright (C) 2022 The noVNC authors\n# This file is distributed under the same license as the noVNC package.\n# Fabio Fantoni <fabio.fantoni@m2r.biz>, 2022.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 1.3.0\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2021-08-27 16:03+0200\\n\"\n\"PO-Revision-Date: 2022-09-08 13:27+0200\\n\"\n\"Last-Translator: Fabio Fantoni <fabio.fantoni@m2r.biz>\\n\"\n\"Language-Team: Italian\\n\"\n\"Language: it\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Generator: Poedit 3.1.1\\n\"\n\n#: ../app/ui.js:400\nmsgid \"Connecting...\"\nmsgstr \"Connessione in corso...\"\n\n#: ../app/ui.js:407\nmsgid \"Disconnecting...\"\nmsgstr \"Disconnessione...\"\n\n#: ../app/ui.js:413\nmsgid \"Reconnecting...\"\nmsgstr \"Riconnessione...\"\n\n#: ../app/ui.js:418\nmsgid \"Internal error\"\nmsgstr \"Errore interno\"\n\n#: ../app/ui.js:1009\nmsgid \"Must set host\"\nmsgstr \"Devi impostare l'host\"\n\n#: ../app/ui.js:1091\nmsgid \"Connected (encrypted) to \"\nmsgstr \"Connesso (crittografato) a \"\n\n#: ../app/ui.js:1093\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"Connesso (non crittografato) a\"\n\n#: ../app/ui.js:1116\nmsgid \"Something went wrong, connection is closed\"\nmsgstr \"Qualcosa è andato storto, la connessione è stata chiusa\"\n\n#: ../app/ui.js:1119\nmsgid \"Failed to connect to server\"\nmsgstr \"Impossibile connettersi al server\"\n\n#: ../app/ui.js:1129\nmsgid \"Disconnected\"\nmsgstr \"Disconnesso\"\n\n#: ../app/ui.js:1144\nmsgid \"New connection has been rejected with reason: \"\nmsgstr \"La nuova connessione è stata rifiutata con motivo: \"\n\n#: ../app/ui.js:1147\nmsgid \"New connection has been rejected\"\nmsgstr \"La nuova connessione è stata rifiutata\"\n\n#: ../app/ui.js:1182\nmsgid \"Credentials are required\"\nmsgstr \"Le credenziali sono obbligatorie\"\n\n#: ../vnc.html:61\nmsgid \"noVNC encountered an error:\"\nmsgstr \"noVNC ha riscontrato un errore:\"\n\n#: ../vnc.html:71\nmsgid \"Hide/Show the control bar\"\nmsgstr \"Nascondi/Mostra la barra di controllo\"\n\n#: ../vnc.html:78\nmsgid \"Drag\"\nmsgstr \"\"\n\n#: ../vnc.html:78\nmsgid \"Move/Drag viewport\"\nmsgstr \"\"\n\n#: ../vnc.html:84\nmsgid \"Keyboard\"\nmsgstr \"Tastiera\"\n\n#: ../vnc.html:84\nmsgid \"Show keyboard\"\nmsgstr \"Mostra tastiera\"\n\n#: ../vnc.html:89\nmsgid \"Extra keys\"\nmsgstr \"Tasti Aggiuntivi\"\n\n#: ../vnc.html:89\nmsgid \"Show Extra Keys\"\nmsgstr \"Mostra Tasti Aggiuntivi\"\n\n#: ../vnc.html:94\nmsgid \"Ctrl\"\nmsgstr \"Ctrl\"\n\n#: ../vnc.html:94\nmsgid \"Toggle Ctrl\"\nmsgstr \"Tieni premuto Ctrl\"\n\n#: ../vnc.html:97\nmsgid \"Alt\"\nmsgstr \"Alt\"\n\n#: ../vnc.html:97\nmsgid \"Toggle Alt\"\nmsgstr \"Tieni premuto Alt\"\n\n#: ../vnc.html:100\nmsgid \"Toggle Windows\"\nmsgstr \"Tieni premuto Windows\"\n\n#: ../vnc.html:100\nmsgid \"Windows\"\nmsgstr \"Windows\"\n\n#: ../vnc.html:103\nmsgid \"Send Tab\"\nmsgstr \"Invia Tab\"\n\n#: ../vnc.html:103\nmsgid \"Tab\"\nmsgstr \"Tab\"\n\n#: ../vnc.html:106\nmsgid \"Esc\"\nmsgstr \"Esc\"\n\n#: ../vnc.html:106\nmsgid \"Send Escape\"\nmsgstr \"Invia Esc\"\n\n#: ../vnc.html:109\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"Ctrl+Alt+Canc\"\n\n#: ../vnc.html:109\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"Invia Ctrl-Alt-Canc\"\n\n#: ../vnc.html:116\nmsgid \"Shutdown/Reboot\"\nmsgstr \"Spegnimento/Riavvio\"\n\n#: ../vnc.html:116\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"Spegnimento/Riavvio...\"\n\n#: ../vnc.html:122\nmsgid \"Power\"\nmsgstr \"Alimentazione\"\n\n#: ../vnc.html:124\nmsgid \"Shutdown\"\nmsgstr \"Spegnimento\"\n\n#: ../vnc.html:125\nmsgid \"Reboot\"\nmsgstr \"Riavvio\"\n\n#: ../vnc.html:126\nmsgid \"Reset\"\nmsgstr \"Reset\"\n\n#: ../vnc.html:131 ../vnc.html:137\nmsgid \"Clipboard\"\nmsgstr \"Clipboard\"\n\n#: ../vnc.html:141\nmsgid \"Clear\"\nmsgstr \"Pulisci\"\n\n#: ../vnc.html:147\nmsgid \"Fullscreen\"\nmsgstr \"Schermo intero\"\n\n#: ../vnc.html:152 ../vnc.html:159\nmsgid \"Settings\"\nmsgstr \"Impostazioni\"\n\n#: ../vnc.html:162\nmsgid \"Shared mode\"\nmsgstr \"Modalità condivisa\"\n\n#: ../vnc.html:165\nmsgid \"View Only\"\nmsgstr \"Sola Visualizzazione\"\n\n#: ../vnc.html:169\nmsgid \"Clip to window\"\nmsgstr \"\"\n\n#: ../vnc.html:172\nmsgid \"Scaling mode:\"\nmsgstr \"Modalità di ridimensionamento:\"\n\n#: ../vnc.html:174\nmsgid \"None\"\nmsgstr \"Nessuna\"\n\n#: ../vnc.html:175\nmsgid \"Local Scaling\"\nmsgstr \"Ridimensionamento Locale\"\n\n#: ../vnc.html:176\nmsgid \"Remote Resizing\"\nmsgstr \"Ridimensionamento Remoto\"\n\n#: ../vnc.html:181\nmsgid \"Advanced\"\nmsgstr \"Avanzate\"\n\n#: ../vnc.html:184\nmsgid \"Quality:\"\nmsgstr \"Qualità:\"\n\n#: ../vnc.html:188\nmsgid \"Compression level:\"\nmsgstr \"Livello Compressione:\"\n\n#: ../vnc.html:193\nmsgid \"Repeater ID:\"\nmsgstr \"ID Ripetitore:\"\n\n#: ../vnc.html:197\nmsgid \"WebSocket\"\nmsgstr \"WebSocket\"\n\n#: ../vnc.html:200\nmsgid \"Encrypt\"\nmsgstr \"Crittografa\"\n\n#: ../vnc.html:203\nmsgid \"Host:\"\nmsgstr \"Host:\"\n\n#: ../vnc.html:207\nmsgid \"Port:\"\nmsgstr \"Porta:\"\n\n#: ../vnc.html:211\nmsgid \"Path:\"\nmsgstr \"Percorso:\"\n\n#: ../vnc.html:218\nmsgid \"Automatic Reconnect\"\nmsgstr \"Riconnessione Automatica\"\n\n#: ../vnc.html:221\nmsgid \"Reconnect Delay (ms):\"\nmsgstr \"Ritardo Riconnessione (ms):\"\n\n#: ../vnc.html:226\nmsgid \"Show Dot when No Cursor\"\nmsgstr \"Mostra Punto quando Nessun Cursore\"\n\n#: ../vnc.html:231\nmsgid \"Logging:\"\nmsgstr \"\"\n\n#: ../vnc.html:240\nmsgid \"Version:\"\nmsgstr \"Versione:\"\n\n#: ../vnc.html:248\nmsgid \"Disconnect\"\nmsgstr \"Disconnetti\"\n\n#: ../vnc.html:267\nmsgid \"Connect\"\nmsgstr \"Connetti\"\n\n#: ../vnc.html:277\nmsgid \"Username:\"\nmsgstr \"Utente:\"\n\n#: ../vnc.html:281\nmsgid \"Password:\"\nmsgstr \"Password:\"\n\n#: ../vnc.html:285\nmsgid \"Send Credentials\"\nmsgstr \"Invia Credenziale\"\n\n#: ../vnc.html:295\nmsgid \"Cancel\"\nmsgstr \"Annulla\"\n"
  },
  {
    "path": "po/ja.po",
    "content": "# Japanese translations for noVNC package\n# noVNC パッケージに対する日訳\n# Copyright (C) 2019-2024 The noVNC authors\n# This file is distributed under the same license as the noVNC package.\n# nnn1590 <nnn1590@nnn1590.org>, 2019-2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 1.5.0\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2024-06-03 14:10+0200\\n\"\n\"PO-Revision-Date: 2024-12-14 15:22+0900\\n\"\n\"Last-Translator: nnn1590 <nnn1590@nnn1590.org>\\n\"\n\"Language-Team: Japanese\\n\"\n\"Language: ja\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\"X-Generator: Poedit 2.3\\n\"\n\n#: ../app/ui.js:69\nmsgid \"\"\n\"Running without HTTPS is not recommended, crashes or other issues are likely.\"\nmsgstr \"\"\n\"HTTPS接続なしで実行することは推奨されません。クラッシュしたりその他の問題が発\"\n\"生したりする可能性があります。\"\n\n#: ../app/ui.js:410\nmsgid \"Connecting...\"\nmsgstr \"接続しています...\"\n\n#: ../app/ui.js:417\nmsgid \"Disconnecting...\"\nmsgstr \"切断しています...\"\n\n#: ../app/ui.js:423\nmsgid \"Reconnecting...\"\nmsgstr \"再接続しています...\"\n\n#: ../app/ui.js:428\nmsgid \"Internal error\"\nmsgstr \"内部エラー\"\n\n#: ../app/ui.js:1026\nmsgid \"Must set host\"\nmsgstr \"ホストを設定する必要があります\"\n\n#: ../app/ui.js:1052\nmsgid \"Failed to connect to server: \"\nmsgstr \"サーバーへの接続に失敗しました: \"\n\n#: ../app/ui.js:1118\nmsgid \"Connected (encrypted) to \"\nmsgstr \"接続しました (暗号化済み): \"\n\n#: ../app/ui.js:1120\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"接続しました (暗号化されていません): \"\n\n#: ../app/ui.js:1143\nmsgid \"Something went wrong, connection is closed\"\nmsgstr \"問題が発生したため、接続が閉じられました\"\n\n#: ../app/ui.js:1146\nmsgid \"Failed to connect to server\"\nmsgstr \"サーバーへの接続に失敗しました\"\n\n#: ../app/ui.js:1158\nmsgid \"Disconnected\"\nmsgstr \"切断しました\"\n\n#: ../app/ui.js:1173\nmsgid \"New connection has been rejected with reason: \"\nmsgstr \"新規接続は次の理由で拒否されました: \"\n\n#: ../app/ui.js:1176\nmsgid \"New connection has been rejected\"\nmsgstr \"新規接続は拒否されました\"\n\n#: ../app/ui.js:1242\nmsgid \"Credentials are required\"\nmsgstr \"資格情報が必要です\"\n\n#: ../vnc.html:55\nmsgid \"noVNC encountered an error:\"\nmsgstr \"noVNC でエラーが発生しました:\"\n\n#: ../vnc.html:65\nmsgid \"Hide/Show the control bar\"\nmsgstr \"コントロールバーを隠す/表示する\"\n\n#: ../vnc.html:74\nmsgid \"Drag\"\nmsgstr \"ドラッグ\"\n\n#: ../vnc.html:74\nmsgid \"Move/Drag viewport\"\nmsgstr \"ビューポートを移動/ドラッグ\"\n\n#: ../vnc.html:80\nmsgid \"Keyboard\"\nmsgstr \"キーボード\"\n\n#: ../vnc.html:80\nmsgid \"Show keyboard\"\nmsgstr \"キーボードを表示\"\n\n#: ../vnc.html:85\nmsgid \"Extra keys\"\nmsgstr \"追加キー\"\n\n#: ../vnc.html:85\nmsgid \"Show extra keys\"\nmsgstr \"追加キーを表示\"\n\n#: ../vnc.html:90\nmsgid \"Ctrl\"\nmsgstr \"Ctrl\"\n\n#: ../vnc.html:90\nmsgid \"Toggle Ctrl\"\nmsgstr \"Ctrl キーをトグル\"\n\n#: ../vnc.html:93\nmsgid \"Alt\"\nmsgstr \"Alt\"\n\n#: ../vnc.html:93\nmsgid \"Toggle Alt\"\nmsgstr \"Alt キーをトグル\"\n\n#: ../vnc.html:96\nmsgid \"Toggle Windows\"\nmsgstr \"Windows キーをトグル\"\n\n#: ../vnc.html:96\nmsgid \"Windows\"\nmsgstr \"Windows\"\n\n#: ../vnc.html:99\nmsgid \"Send Tab\"\nmsgstr \"Tab キーを送信\"\n\n#: ../vnc.html:99\nmsgid \"Tab\"\nmsgstr \"Tab\"\n\n#: ../vnc.html:102\nmsgid \"Esc\"\nmsgstr \"Esc\"\n\n#: ../vnc.html:102\nmsgid \"Send Escape\"\nmsgstr \"Escape キーを送信\"\n\n#: ../vnc.html:105\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"Ctrl+Alt+Del\"\n\n#: ../vnc.html:105\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"Ctrl-Alt-Del を送信\"\n\n#: ../vnc.html:112\nmsgid \"Shutdown/Reboot\"\nmsgstr \"シャットダウン/再起動\"\n\n#: ../vnc.html:112\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"シャットダウン/再起動...\"\n\n#: ../vnc.html:118\nmsgid \"Power\"\nmsgstr \"電源\"\n\n#: ../vnc.html:120\nmsgid \"Shutdown\"\nmsgstr \"シャットダウン\"\n\n#: ../vnc.html:121\nmsgid \"Reboot\"\nmsgstr \"再起動\"\n\n#: ../vnc.html:122\nmsgid \"Reset\"\nmsgstr \"リセット\"\n\n#: ../vnc.html:127 ../vnc.html:133\nmsgid \"Clipboard\"\nmsgstr \"クリップボード\"\n\n#: ../vnc.html:135\nmsgid \"Edit clipboard content in the textarea below.\"\nmsgstr \"以下の入力欄からクリップボードの内容を編集できます。\"\n\n#: ../vnc.html:143\nmsgid \"Full screen\"\nmsgstr \"全画面表示\"\n\n#: ../vnc.html:148 ../vnc.html:154\nmsgid \"Settings\"\nmsgstr \"設定\"\n\n#: ../vnc.html:158\nmsgid \"Shared mode\"\nmsgstr \"共有モード\"\n\n#: ../vnc.html:161\nmsgid \"View only\"\nmsgstr \"表示専用\"\n\n#: ../vnc.html:165\nmsgid \"Clip to window\"\nmsgstr \"ウィンドウにクリップ\"\n\n#: ../vnc.html:168\nmsgid \"Scaling mode:\"\nmsgstr \"スケーリングモード:\"\n\n#: ../vnc.html:170\nmsgid \"None\"\nmsgstr \"なし\"\n\n#: ../vnc.html:171\nmsgid \"Local scaling\"\nmsgstr \"ローカルでスケーリング\"\n\n#: ../vnc.html:172\nmsgid \"Remote resizing\"\nmsgstr \"リモートでリサイズ\"\n\n#: ../vnc.html:177\nmsgid \"Advanced\"\nmsgstr \"高度\"\n\n#: ../vnc.html:180\nmsgid \"Quality:\"\nmsgstr \"品質:\"\n\n#: ../vnc.html:184\nmsgid \"Compression level:\"\nmsgstr \"圧縮レベル:\"\n\n#: ../vnc.html:189\nmsgid \"Repeater ID:\"\nmsgstr \"リピーター ID:\"\n\n#: ../vnc.html:193\nmsgid \"WebSocket\"\nmsgstr \"WebSocket\"\n\n#: ../vnc.html:196\nmsgid \"Encrypt\"\nmsgstr \"暗号化\"\n\n#: ../vnc.html:199\nmsgid \"Host:\"\nmsgstr \"ホスト:\"\n\n#: ../vnc.html:203\nmsgid \"Port:\"\nmsgstr \"ポート:\"\n\n#: ../vnc.html:207\nmsgid \"Path:\"\nmsgstr \"パス:\"\n\n#: ../vnc.html:214\nmsgid \"Automatic reconnect\"\nmsgstr \"自動再接続\"\n\n#: ../vnc.html:217\nmsgid \"Reconnect delay (ms):\"\nmsgstr \"再接続する遅延 (ミリ秒):\"\n\n#: ../vnc.html:222\nmsgid \"Show dot when no cursor\"\nmsgstr \"カーソルがないときにドットを表示する\"\n\n#: ../vnc.html:227\nmsgid \"Logging:\"\nmsgstr \"ロギング:\"\n\n#: ../vnc.html:236\nmsgid \"Version:\"\nmsgstr \"バージョン:\"\n\n#: ../vnc.html:244\nmsgid \"Disconnect\"\nmsgstr \"切断\"\n\n#: ../vnc.html:267\nmsgid \"Connect\"\nmsgstr \"接続\"\n\n#: ../vnc.html:276\nmsgid \"Server identity\"\nmsgstr \"サーバーの識別情報\"\n\n#: ../vnc.html:279\nmsgid \"The server has provided the following identifying information:\"\nmsgstr \"サーバーは以下の識別情報を提供しています:\"\n\n#: ../vnc.html:283\nmsgid \"Fingerprint:\"\nmsgstr \"フィンガープリント:\"\n\n#: ../vnc.html:286\nmsgid \"\"\n\"Please verify that the information is correct and press \\\"Approve\\\". \"\n\"Otherwise press \\\"Reject\\\".\"\nmsgstr \"\"\n\"この情報が正しい場合は「承認」を、そうでない場合は「拒否」を押してください。\"\n\n#: ../vnc.html:291\nmsgid \"Approve\"\nmsgstr \"承認\"\n\n#: ../vnc.html:292\nmsgid \"Reject\"\nmsgstr \"拒否\"\n\n#: ../vnc.html:300\nmsgid \"Credentials\"\nmsgstr \"資格情報\"\n\n#: ../vnc.html:304\nmsgid \"Username:\"\nmsgstr \"ユーザー名:\"\n\n#: ../vnc.html:308\nmsgid \"Password:\"\nmsgstr \"パスワード:\"\n\n#: ../vnc.html:312\nmsgid \"Send credentials\"\nmsgstr \"資格情報を送信\"\n\n#: ../vnc.html:321\nmsgid \"Cancel\"\nmsgstr \"キャンセル\"\n"
  },
  {
    "path": "po/ko.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2018 The noVNC authors\n# This file is distributed under the same license as the noVNC package.\n# Baw Appie <pp121324@gmail.com>, 2018.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 1.0.0-testing.2\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2018-01-31 16:29+0100\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: Baw Appie <pp121324@gmail.com>\\n\"\n\"Language-Team: Korean\\n\"\n\"Language: ko\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\n#: ../app/ui.js:395\nmsgid \"Connecting...\"\nmsgstr \"연결중...\"\n\n#: ../app/ui.js:402\nmsgid \"Disconnecting...\"\nmsgstr \"연결 해제중...\"\n\n#: ../app/ui.js:408\nmsgid \"Reconnecting...\"\nmsgstr \"재연결중...\"\n\n#: ../app/ui.js:413\nmsgid \"Internal error\"\nmsgstr \"내부 오류\"\n\n#: ../app/ui.js:1002\nmsgid \"Must set host\"\nmsgstr \"호스트는 설정되어야 합니다.\"\n\n#: ../app/ui.js:1083\nmsgid \"Connected (encrypted) to \"\nmsgstr \"다음과 (암호화되어) 연결되었습니다:\"\n\n#: ../app/ui.js:1085\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"다음과 (암호화 없이) 연결되었습니다:\"\n\n#: ../app/ui.js:1108\nmsgid \"Something went wrong, connection is closed\"\nmsgstr \"무언가 잘못되었습니다, 연결이 닫혔습니다.\"\n\n#: ../app/ui.js:1111\nmsgid \"Failed to connect to server\"\nmsgstr \"서버에 연결하지 못했습니다.\"\n\n#: ../app/ui.js:1121\nmsgid \"Disconnected\"\nmsgstr \"연결이 해제되었습니다.\"\n\n#: ../app/ui.js:1134\nmsgid \"New connection has been rejected with reason: \"\nmsgstr \"새 연결이 다음 이유로 거부되었습니다:\"\n\n#: ../app/ui.js:1137\nmsgid \"New connection has been rejected\"\nmsgstr \"새 연결이 거부되었습니다.\"\n\n#: ../app/ui.js:1158\nmsgid \"Password is required\"\nmsgstr \"비밀번호가 필요합니다.\"\n\n#: ../vnc.html:91\nmsgid \"noVNC encountered an error:\"\nmsgstr \"noVNC에 오류가 발생했습니다:\"\n\n#: ../vnc.html:101\nmsgid \"Hide/Show the control bar\"\nmsgstr \"컨트롤 바 숨기기/보이기\"\n\n#: ../vnc.html:108\nmsgid \"Move/Drag viewport\"\nmsgstr \"움직이기/드래그 뷰포트\"\n\n#: ../vnc.html:108\nmsgid \"viewport drag\"\nmsgstr \"뷰포트 드래그\"\n\n#: ../vnc.html:114 ../vnc.html:117 ../vnc.html:120 ../vnc.html:123\nmsgid \"Active Mouse Button\"\nmsgstr \"마우스 버튼 활성화\"\n\n#: ../vnc.html:114\nmsgid \"No mousebutton\"\nmsgstr \"마우스 버튼 없음\"\n\n#: ../vnc.html:117\nmsgid \"Left mousebutton\"\nmsgstr \"왼쪽 마우스 버튼\"\n\n#: ../vnc.html:120\nmsgid \"Middle mousebutton\"\nmsgstr \"중간 마우스 버튼\"\n\n#: ../vnc.html:123\nmsgid \"Right mousebutton\"\nmsgstr \"오른쪽 마우스 버튼\"\n\n#: ../vnc.html:126\nmsgid \"Keyboard\"\nmsgstr \"키보드\"\n\n#: ../vnc.html:126\nmsgid \"Show keyboard\"\nmsgstr \"키보드 보이기\"\n\n#: ../vnc.html:133\nmsgid \"Extra keys\"\nmsgstr \"기타 키들\"\n\n#: ../vnc.html:133\nmsgid \"Show extra keys\"\nmsgstr \"기타 키들 보이기\"\n\n#: ../vnc.html:138\nmsgid \"Ctrl\"\nmsgstr \"Ctrl\"\n\n#: ../vnc.html:138\nmsgid \"Toggle Ctrl\"\nmsgstr \"Ctrl 켜기/끄기\"\n\n#: ../vnc.html:141\nmsgid \"Alt\"\nmsgstr \"Alt\"\n\n#: ../vnc.html:141\nmsgid \"Toggle Alt\"\nmsgstr \"Alt 켜기/끄기\"\n\n#: ../vnc.html:144\nmsgid \"Send Tab\"\nmsgstr \"Tab 보내기\"\n\n#: ../vnc.html:144\nmsgid \"Tab\"\nmsgstr \"Tab\"\n\n#: ../vnc.html:147\nmsgid \"Esc\"\nmsgstr \"Esc\"\n\n#: ../vnc.html:147\nmsgid \"Send Escape\"\nmsgstr \"Esc 보내기\"\n\n#: ../vnc.html:150\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"Ctrl+Alt+Del\"\n\n#: ../vnc.html:150\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"Ctrl+Alt+Del 보내기\"\n\n#: ../vnc.html:158\nmsgid \"Shutdown/Reboot\"\nmsgstr \"셧다운/리붓\"\n\n#: ../vnc.html:158\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"셧다운/리붓...\"\n\n#: ../vnc.html:164\nmsgid \"Power\"\nmsgstr \"전원\"\n\n#: ../vnc.html:166\nmsgid \"Shutdown\"\nmsgstr \"셧다운\"\n\n#: ../vnc.html:167\nmsgid \"Reboot\"\nmsgstr \"리붓\"\n\n#: ../vnc.html:168\nmsgid \"Reset\"\nmsgstr \"리셋\"\n\n#: ../vnc.html:173 ../vnc.html:179\nmsgid \"Clipboard\"\nmsgstr \"클립보드\"\n\n#: ../vnc.html:183\nmsgid \"Clear\"\nmsgstr \"지우기\"\n\n#: ../vnc.html:189\nmsgid \"Fullscreen\"\nmsgstr \"전체화면\"\n\n#: ../vnc.html:194 ../vnc.html:201\nmsgid \"Settings\"\nmsgstr \"설정\"\n\n#: ../vnc.html:204\nmsgid \"Shared mode\"\nmsgstr \"공유 모드\"\n\n#: ../vnc.html:207\nmsgid \"View only\"\nmsgstr \"보기 전용\"\n\n#: ../vnc.html:211\nmsgid \"Clip to window\"\nmsgstr \"창에 클립\"\n\n#: ../vnc.html:214\nmsgid \"Scaling mode:\"\nmsgstr \"스케일링 모드:\"\n\n#: ../vnc.html:216\nmsgid \"None\"\nmsgstr \"없음\"\n\n#: ../vnc.html:217\nmsgid \"Local scaling\"\nmsgstr \"로컬 스케일링\"\n\n#: ../vnc.html:218\nmsgid \"Remote resizing\"\nmsgstr \"원격 크기 조절\"\n\n#: ../vnc.html:223\nmsgid \"Advanced\"\nmsgstr \"고급\"\n\n#: ../vnc.html:226\nmsgid \"Repeater ID:\"\nmsgstr \"중계 ID\"\n\n#: ../vnc.html:230\nmsgid \"WebSocket\"\nmsgstr \"웹소켓\"\n\n#: ../vnc.html:233\nmsgid \"Encrypt\"\nmsgstr \"암호화\"\n\n#: ../vnc.html:236\nmsgid \"Host:\"\nmsgstr \"호스트:\"\n\n#: ../vnc.html:240\nmsgid \"Port:\"\nmsgstr \"포트:\"\n\n#: ../vnc.html:244\nmsgid \"Path:\"\nmsgstr \"위치:\"\n\n#: ../vnc.html:251\nmsgid \"Automatic reconnect\"\nmsgstr \"자동 재연결\"\n\n#: ../vnc.html:254\nmsgid \"Reconnect delay (ms):\"\nmsgstr \"재연결 지연 시간 (ms)\"\n\n#: ../vnc.html:260\nmsgid \"Logging:\"\nmsgstr \"로깅\"\n\n#: ../vnc.html:272\nmsgid \"Disconnect\"\nmsgstr \"연결 해제\"\n\n#: ../vnc.html:291\nmsgid \"Connect\"\nmsgstr \"연결\"\n\n#: ../vnc.html:301\nmsgid \"Password:\"\nmsgstr \"비밀번호:\"\n\n#: ../vnc.html:305\nmsgid \"Send Password\"\nmsgstr \"비밀번호 전송\"\n\n#: ../vnc.html:315\nmsgid \"Cancel\"\nmsgstr \"취소\"\n"
  },
  {
    "path": "po/nl.po",
    "content": "# Dutch translations for noVNC package\n# Nederlandse vertalingen voor het pakket noVNC.\n# Copyright (C) 2018 The noVNC authors\n# This file is distributed under the same license as the noVNC package.\n# Loek Janssen <loekjanssen@gmail.com>, 2016.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 1.6.0\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2025-02-14 10:14+0100\\n\"\n\"PO-Revision-Date: 2025-03-03 18:20+0100\\n\"\n\"Last-Translator: Harold Horsman <haroldhorsman@gmail.com>\\n\"\n\"Language-Team: none\\n\"\n\"Language: nl\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Generator: Poedit 3.5\\n\"\n\n#: ../app/ui.js:84\nmsgid \"\"\n\"Running without HTTPS is not recommended, crashes or other issues are likely.\"\nmsgstr \"\"\n\"Het is niet aan te raden om zonder HTTPS te werken, crashes of andere \"\n\"problemen zijn dan waarschijnlijk.\"\n\n#: ../app/ui.js:413\nmsgid \"Connecting...\"\nmsgstr \"Aan het verbinden…\"\n\n#: ../app/ui.js:420\nmsgid \"Disconnecting...\"\nmsgstr \"Bezig om verbinding te verbreken...\"\n\n#: ../app/ui.js:426\nmsgid \"Reconnecting...\"\nmsgstr \"Opnieuw verbinding maken...\"\n\n#: ../app/ui.js:431\nmsgid \"Internal error\"\nmsgstr \"Interne fout\"\n\n#: ../app/ui.js:1079\n#, fuzzy\nmsgid \"Failed to connect to server: \"\nmsgstr \"Verbinding maken met server is mislukt\"\n\n#: ../app/ui.js:1145\nmsgid \"Connected (encrypted) to \"\nmsgstr \"Verbonden (versleuteld) met \"\n\n#: ../app/ui.js:1147\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"Verbonden (onversleuteld) met \"\n\n#: ../app/ui.js:1170\nmsgid \"Something went wrong, connection is closed\"\nmsgstr \"Er iets fout gelopen, verbinding werd verbroken\"\n\n#: ../app/ui.js:1173\nmsgid \"Failed to connect to server\"\nmsgstr \"Verbinding maken met server is mislukt\"\n\n#: ../app/ui.js:1185\nmsgid \"Disconnected\"\nmsgstr \"Verbinding verbroken\"\n\n#: ../app/ui.js:1200\nmsgid \"New connection has been rejected with reason: \"\nmsgstr \"Nieuwe verbinding is geweigerd met de volgende reden: \"\n\n#: ../app/ui.js:1203\nmsgid \"New connection has been rejected\"\nmsgstr \"Nieuwe verbinding is geweigerd\"\n\n#: ../app/ui.js:1269\nmsgid \"Credentials are required\"\nmsgstr \"Inloggegevens zijn nodig\"\n\n#: ../vnc.html:106\nmsgid \"noVNC encountered an error:\"\nmsgstr \"noVNC heeft een fout bemerkt:\"\n\n#: ../vnc.html:116\nmsgid \"Hide/Show the control bar\"\nmsgstr \"Verberg/Toon de bedieningsbalk\"\n\n#: ../vnc.html:125\nmsgid \"Drag\"\nmsgstr \"Sleep\"\n\n#: ../vnc.html:125\n#, fuzzy\nmsgid \"Move/Drag viewport\"\nmsgstr \"Verplaats/Versleep Kijkvenster\"\n\n#: ../vnc.html:131\nmsgid \"Keyboard\"\nmsgstr \"Toetsenbord\"\n\n#: ../vnc.html:131\n#, fuzzy\nmsgid \"Show keyboard\"\nmsgstr \"Toon Toetsenbord\"\n\n#: ../vnc.html:136\nmsgid \"Extra keys\"\nmsgstr \"Extra toetsen\"\n\n#: ../vnc.html:136\n#, fuzzy\nmsgid \"Show extra keys\"\nmsgstr \"Toon Extra Toetsen\"\n\n#: ../vnc.html:141\nmsgid \"Ctrl\"\nmsgstr \"Ctrl\"\n\n#: ../vnc.html:141\nmsgid \"Toggle Ctrl\"\nmsgstr \"Ctrl omschakelen\"\n\n#: ../vnc.html:144\nmsgid \"Alt\"\nmsgstr \"Alt\"\n\n#: ../vnc.html:144\nmsgid \"Toggle Alt\"\nmsgstr \"Alt omschakelen\"\n\n#: ../vnc.html:147\nmsgid \"Toggle Windows\"\nmsgstr \"Vensters omschakelen\"\n\n#: ../vnc.html:147\nmsgid \"Windows\"\nmsgstr \"Vensters\"\n\n#: ../vnc.html:150\nmsgid \"Send Tab\"\nmsgstr \"Tab Sturen\"\n\n#: ../vnc.html:150\nmsgid \"Tab\"\nmsgstr \"Tab\"\n\n#: ../vnc.html:153\nmsgid \"Esc\"\nmsgstr \"Esc\"\n\n#: ../vnc.html:153\nmsgid \"Send Escape\"\nmsgstr \"Escape Sturen\"\n\n#: ../vnc.html:156\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"Ctrl-Alt-Del\"\n\n#: ../vnc.html:156\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"Ctrl-Alt-Del Sturen\"\n\n#: ../vnc.html:163\nmsgid \"Shutdown/Reboot\"\nmsgstr \"Uitschakelen/Herstarten\"\n\n#: ../vnc.html:163\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"Uitschakelen/Herstarten...\"\n\n#: ../vnc.html:169\nmsgid \"Power\"\nmsgstr \"Systeem\"\n\n#: ../vnc.html:171\nmsgid \"Shutdown\"\nmsgstr \"Uitschakelen\"\n\n#: ../vnc.html:172\nmsgid \"Reboot\"\nmsgstr \"Herstarten\"\n\n#: ../vnc.html:173\nmsgid \"Reset\"\nmsgstr \"Resetten\"\n\n#: ../vnc.html:178 ../vnc.html:184\nmsgid \"Clipboard\"\nmsgstr \"Klembord\"\n\n#: ../vnc.html:186\nmsgid \"Edit clipboard content in the textarea below.\"\nmsgstr \"Edit de inhoud van het klembord in het tekstveld hieronder\"\n\n#: ../vnc.html:194\n#, fuzzy\nmsgid \"Full screen\"\nmsgstr \"Volledig Scherm\"\n\n#: ../vnc.html:199 ../vnc.html:205\nmsgid \"Settings\"\nmsgstr \"Instellingen\"\n\n#: ../vnc.html:211\n#, fuzzy\nmsgid \"Shared mode\"\nmsgstr \"Gedeelde Modus\"\n\n#: ../vnc.html:218\n#, fuzzy\nmsgid \"View only\"\nmsgstr \"Alleen Kijken\"\n\n#: ../vnc.html:226\nmsgid \"Clip to window\"\nmsgstr \"Randen buiten venster afsnijden\"\n\n#: ../vnc.html:231\nmsgid \"Scaling mode:\"\nmsgstr \"Schaalmodus:\"\n\n#: ../vnc.html:233\nmsgid \"None\"\nmsgstr \"Geen\"\n\n#: ../vnc.html:234\n#, fuzzy\nmsgid \"Local scaling\"\nmsgstr \"Lokaal Schalen\"\n\n#: ../vnc.html:235\n#, fuzzy\nmsgid \"Remote resizing\"\nmsgstr \"Op Afstand Formaat Wijzigen\"\n\n#: ../vnc.html:240\nmsgid \"Advanced\"\nmsgstr \"Geavanceerd\"\n\n#: ../vnc.html:243\nmsgid \"Quality:\"\nmsgstr \"Kwaliteit:\"\n\n#: ../vnc.html:247\nmsgid \"Compression level:\"\nmsgstr \"Compressieniveau:\"\n\n#: ../vnc.html:252\nmsgid \"Repeater ID:\"\nmsgstr \"Repeater ID:\"\n\n#: ../vnc.html:256\nmsgid \"WebSocket\"\nmsgstr \"WebSocket\"\n\n#: ../vnc.html:261\nmsgid \"Encrypt\"\nmsgstr \"Versleutelen\"\n\n#: ../vnc.html:266\nmsgid \"Host:\"\nmsgstr \"Host:\"\n\n#: ../vnc.html:270\nmsgid \"Port:\"\nmsgstr \"Poort:\"\n\n#: ../vnc.html:274\nmsgid \"Path:\"\nmsgstr \"Pad:\"\n\n#: ../vnc.html:283\n#, fuzzy\nmsgid \"Automatic reconnect\"\nmsgstr \"Automatisch Opnieuw Verbinden\"\n\n#: ../vnc.html:288\n#, fuzzy\nmsgid \"Reconnect delay (ms):\"\nmsgstr \"Vertraging voor Opnieuw Verbinden (ms):\"\n\n#: ../vnc.html:295\nmsgid \"Show dot when no cursor\"\nmsgstr \"Geef stip weer indien geen cursor\"\n\n#: ../vnc.html:302\nmsgid \"Logging:\"\nmsgstr \"Logmeldingen:\"\n\n#: ../vnc.html:311\nmsgid \"Version:\"\nmsgstr \"Versie:\"\n\n#: ../vnc.html:319\nmsgid \"Disconnect\"\nmsgstr \"Verbinding verbreken\"\n\n#: ../vnc.html:342\nmsgid \"Connect\"\nmsgstr \"Verbinden\"\n\n#: ../vnc.html:351\nmsgid \"Server identity\"\nmsgstr \"Serveridentiteit\"\n\n#: ../vnc.html:354\nmsgid \"The server has provided the following identifying information:\"\nmsgstr \"De server geeft de volgende identificerende informatie:\"\n\n#: ../vnc.html:357\nmsgid \"Fingerprint:\"\nmsgstr \"Vingerafdruk:\"\n\n#: ../vnc.html:361\nmsgid \"\"\n\"Please verify that the information is correct and press \\\"Approve\\\". \"\n\"Otherwise press \\\"Reject\\\".\"\nmsgstr \"\"\n\"Verifieer dat de informatie is correct en druk “OK”. Druk anders op \"\n\"“Afwijzen”.\"\n\n#: ../vnc.html:366\nmsgid \"Approve\"\nmsgstr \"OK\"\n\n#: ../vnc.html:367\nmsgid \"Reject\"\nmsgstr \"Afwijzen\"\n\n#: ../vnc.html:375\nmsgid \"Credentials\"\nmsgstr \"Inloggegevens\"\n\n#: ../vnc.html:379\nmsgid \"Username:\"\nmsgstr \"Gebruikersnaam:\"\n\n#: ../vnc.html:383\nmsgid \"Password:\"\nmsgstr \"Wachtwoord:\"\n\n# Translated by Harold Horsman\n#: ../vnc.html:387\nmsgid \"Send credentials\"\nmsgstr \"Stuur inloggegevens\"\n\n#: ../vnc.html:396\nmsgid \"Cancel\"\nmsgstr \"Annuleren\"\n\n#~ msgid \"Must set host\"\n#~ msgstr \"Host moeten worden ingesteld\"\n\n#~ msgid \"Password is required\"\n#~ msgstr \"Wachtwoord is vereist\"\n\n#~ msgid \"viewport drag\"\n#~ msgstr \"kijkvenster slepen\"\n\n#~ msgid \"Active Mouse Button\"\n#~ msgstr \"Actieve Muisknop\"\n\n#~ msgid \"No mousebutton\"\n#~ msgstr \"Geen muisknop\"\n\n#~ msgid \"Left mousebutton\"\n#~ msgstr \"Linker muisknop\"\n\n#~ msgid \"Middle mousebutton\"\n#~ msgstr \"Middelste muisknop\"\n\n#~ msgid \"Right mousebutton\"\n#~ msgstr \"Rechter muisknop\"\n\n#~ msgid \"Clear\"\n#~ msgstr \"Wissen\"\n\n#~ msgid \"Send Password\"\n#~ msgstr \"Verzend Wachtwoord:\"\n\n#~ msgid \"Disconnect timeout\"\n#~ msgstr \"Timeout tijdens verbreken van verbinding\"\n\n#~ msgid \"Local Downscaling\"\n#~ msgstr \"Lokaal Neerschalen\"\n\n#~ msgid \"Local Cursor\"\n#~ msgstr \"Lokale Cursor\"\n\n#~ msgid \"Canvas not supported.\"\n#~ msgstr \"Canvas wordt niet ondersteund.\"\n\n#~ msgid \"\"\n#~ \"Forcing clipping mode since scrollbars aren't supported by IE in \"\n#~ \"fullscreen\"\n#~ msgstr \"\"\n#~ \"''Clipping mode' ingeschakeld, omdat schuifbalken in volledige-scherm-\"\n#~ \"modus in IE niet worden ondersteund\"\n"
  },
  {
    "path": "po/noVNC.pot",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR The noVNC authors\n# This file is distributed under the same license as the noVNC package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 1.6.0\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2025-10-31 09:17+0100\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language-Team: LANGUAGE <LL@li.org>\\n\"\n\"Language: \\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=CHARSET\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\n#: ../app/ui.js:84\nmsgid \"\"\n\"Running without HTTPS is not recommended, crashes or other issues are likely.\"\nmsgstr \"\"\n\n#: ../app/ui.js:413\nmsgid \"Connecting...\"\nmsgstr \"\"\n\n#: ../app/ui.js:420\nmsgid \"Disconnecting...\"\nmsgstr \"\"\n\n#: ../app/ui.js:426\nmsgid \"Reconnecting...\"\nmsgstr \"\"\n\n#: ../app/ui.js:431\nmsgid \"Internal error\"\nmsgstr \"\"\n\n#: ../app/ui.js:1084\nmsgid \"Failed to connect to server: \"\nmsgstr \"\"\n\n#: ../app/ui.js:1151\nmsgid \"Connected (encrypted) to \"\nmsgstr \"\"\n\n#: ../app/ui.js:1153\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"\"\n\n#: ../app/ui.js:1178\nmsgid \"Something went wrong, connection is closed\"\nmsgstr \"\"\n\n#: ../app/ui.js:1181\nmsgid \"Failed to connect to server\"\nmsgstr \"\"\n\n#: ../app/ui.js:1193\nmsgid \"Disconnected\"\nmsgstr \"\"\n\n#: ../app/ui.js:1210\nmsgid \"New connection has been rejected with reason: \"\nmsgstr \"\"\n\n#: ../app/ui.js:1213\nmsgid \"New connection has been rejected\"\nmsgstr \"\"\n\n#: ../app/ui.js:1225\nmsgid \"Are you sure you want to disconnect the session?\"\nmsgstr \"\"\n\n#: ../app/ui.js:1297\nmsgid \"Credentials are required\"\nmsgstr \"\"\n\n#: ../vnc.html:106\nmsgid \"noVNC encountered an error:\"\nmsgstr \"\"\n\n#: ../vnc.html:116\nmsgid \"Hide/Show the control bar\"\nmsgstr \"\"\n\n#: ../vnc.html:125\nmsgid \"Drag\"\nmsgstr \"\"\n\n#: ../vnc.html:125\nmsgid \"Move/Drag viewport\"\nmsgstr \"\"\n\n#: ../vnc.html:131\nmsgid \"Keyboard\"\nmsgstr \"\"\n\n#: ../vnc.html:131\nmsgid \"Show keyboard\"\nmsgstr \"\"\n\n#: ../vnc.html:136\nmsgid \"Extra keys\"\nmsgstr \"\"\n\n#: ../vnc.html:136\nmsgid \"Show extra keys\"\nmsgstr \"\"\n\n#: ../vnc.html:141\nmsgid \"Ctrl\"\nmsgstr \"\"\n\n#: ../vnc.html:141\nmsgid \"Toggle Ctrl\"\nmsgstr \"\"\n\n#: ../vnc.html:144\nmsgid \"Alt\"\nmsgstr \"\"\n\n#: ../vnc.html:144\nmsgid \"Toggle Alt\"\nmsgstr \"\"\n\n#: ../vnc.html:147\nmsgid \"Toggle Windows\"\nmsgstr \"\"\n\n#: ../vnc.html:147\nmsgid \"Windows\"\nmsgstr \"\"\n\n#: ../vnc.html:150\nmsgid \"Send Tab\"\nmsgstr \"\"\n\n#: ../vnc.html:150\nmsgid \"Tab\"\nmsgstr \"\"\n\n#: ../vnc.html:153\nmsgid \"Esc\"\nmsgstr \"\"\n\n#: ../vnc.html:153\nmsgid \"Send Escape\"\nmsgstr \"\"\n\n#: ../vnc.html:156\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"\"\n\n#: ../vnc.html:156\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"\"\n\n#: ../vnc.html:163\nmsgid \"Shutdown/Reboot\"\nmsgstr \"\"\n\n#: ../vnc.html:163\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"\"\n\n#: ../vnc.html:169\nmsgid \"Power\"\nmsgstr \"\"\n\n#: ../vnc.html:171\nmsgid \"Shutdown\"\nmsgstr \"\"\n\n#: ../vnc.html:172\nmsgid \"Reboot\"\nmsgstr \"\"\n\n#: ../vnc.html:173\nmsgid \"Reset\"\nmsgstr \"\"\n\n#: ../vnc.html:178 ../vnc.html:184\nmsgid \"Clipboard\"\nmsgstr \"\"\n\n#: ../vnc.html:186\nmsgid \"Edit clipboard content in the textarea below.\"\nmsgstr \"\"\n\n#: ../vnc.html:194\nmsgid \"Full screen\"\nmsgstr \"\"\n\n#: ../vnc.html:199 ../vnc.html:205\nmsgid \"Settings\"\nmsgstr \"\"\n\n#: ../vnc.html:211\nmsgid \"Shared mode\"\nmsgstr \"\"\n\n#: ../vnc.html:218\nmsgid \"View only\"\nmsgstr \"\"\n\n#: ../vnc.html:226\nmsgid \"Clip to window\"\nmsgstr \"\"\n\n#: ../vnc.html:231\nmsgid \"Scaling mode:\"\nmsgstr \"\"\n\n#: ../vnc.html:233\nmsgid \"None\"\nmsgstr \"\"\n\n#: ../vnc.html:234\nmsgid \"Local scaling\"\nmsgstr \"\"\n\n#: ../vnc.html:235\nmsgid \"Remote resizing\"\nmsgstr \"\"\n\n#: ../vnc.html:240\nmsgid \"Advanced\"\nmsgstr \"\"\n\n#: ../vnc.html:243\nmsgid \"Quality:\"\nmsgstr \"\"\n\n#: ../vnc.html:247\nmsgid \"Compression level:\"\nmsgstr \"\"\n\n#: ../vnc.html:252\nmsgid \"Repeater ID:\"\nmsgstr \"\"\n\n#: ../vnc.html:256\nmsgid \"WebSocket\"\nmsgstr \"\"\n\n#: ../vnc.html:261\nmsgid \"Encrypt\"\nmsgstr \"\"\n\n#: ../vnc.html:266\nmsgid \"Host:\"\nmsgstr \"\"\n\n#: ../vnc.html:270\nmsgid \"Port:\"\nmsgstr \"\"\n\n#: ../vnc.html:274\nmsgid \"Path:\"\nmsgstr \"\"\n\n#: ../vnc.html:283\nmsgid \"Automatic reconnect\"\nmsgstr \"\"\n\n#: ../vnc.html:288\nmsgid \"Reconnect delay (ms):\"\nmsgstr \"\"\n\n#: ../vnc.html:295\nmsgid \"Show dot when no cursor\"\nmsgstr \"\"\n\n#: ../vnc.html:302\nmsgid \"Logging:\"\nmsgstr \"\"\n\n#: ../vnc.html:311\nmsgid \"Version:\"\nmsgstr \"\"\n\n#: ../vnc.html:319\nmsgid \"Disconnect\"\nmsgstr \"\"\n\n#: ../vnc.html:342\nmsgid \"Connect\"\nmsgstr \"\"\n\n#: ../vnc.html:351\nmsgid \"Server identity\"\nmsgstr \"\"\n\n#: ../vnc.html:354\nmsgid \"The server has provided the following identifying information:\"\nmsgstr \"\"\n\n#: ../vnc.html:357\nmsgid \"Fingerprint:\"\nmsgstr \"\"\n\n#: ../vnc.html:361\nmsgid \"\"\n\"Please verify that the information is correct and press \\\"Approve\\\". \"\n\"Otherwise press \\\"Reject\\\".\"\nmsgstr \"\"\n\n#: ../vnc.html:366\nmsgid \"Approve\"\nmsgstr \"\"\n\n#: ../vnc.html:367\nmsgid \"Reject\"\nmsgstr \"\"\n\n#: ../vnc.html:375\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: ../vnc.html:379\nmsgid \"Username:\"\nmsgstr \"\"\n\n#: ../vnc.html:383\nmsgid \"Password:\"\nmsgstr \"\"\n\n#: ../vnc.html:387\nmsgid \"Send credentials\"\nmsgstr \"\"\n\n#: ../vnc.html:396\nmsgid \"Cancel\"\nmsgstr \"\"\n"
  },
  {
    "path": "po/pl.po",
    "content": "# Polish translations for noVNC package.\n# Copyright (C) 2018 The noVNC authors\n# This file is distributed under the same license as the noVNC package.\n# Mariusz Jamro <mariusz.jamro@gmail.com>, 2017.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 0.6.1\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2017-11-21 19:53+0100\\n\"\n\"PO-Revision-Date: 2017-11-21 19:54+0100\\n\"\n\"Last-Translator: Mariusz Jamro <mariusz.jamro@gmail.com>\\n\"\n\"Language-Team: Polish\\n\"\n\"Language: pl\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 \"\n\"|| n%100>=20) ? 1 : 2);\\n\"\n\"X-Generator: Poedit 2.0.1\\n\"\n\n#: ../app/ui.js:404\nmsgid \"Connecting...\"\nmsgstr \"Łączenie...\"\n\n#: ../app/ui.js:411\nmsgid \"Disconnecting...\"\nmsgstr \"Rozłączanie...\"\n\n#: ../app/ui.js:417\nmsgid \"Reconnecting...\"\nmsgstr \"Łączenie...\"\n\n#: ../app/ui.js:422\nmsgid \"Internal error\"\nmsgstr \"Błąd wewnętrzny\"\n\n#: ../app/ui.js:1019\nmsgid \"Must set host\"\nmsgstr \"Host i port są wymagane\"\n\n#: ../app/ui.js:1099\nmsgid \"Connected (encrypted) to \"\nmsgstr \"Połączenie (szyfrowane) z \"\n\n#: ../app/ui.js:1101\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"Połączenie (nieszyfrowane) z \"\n\n#: ../app/ui.js:1119\nmsgid \"Something went wrong, connection is closed\"\nmsgstr \"Coś poszło źle, połączenie zostało zamknięte\"\n\n#: ../app/ui.js:1129\nmsgid \"Disconnected\"\nmsgstr \"Rozłączony\"\n\n#: ../app/ui.js:1142\nmsgid \"New connection has been rejected with reason: \"\nmsgstr \"Nowe połączenie zostało odrzucone z powodu: \"\n\n#: ../app/ui.js:1145\nmsgid \"New connection has been rejected\"\nmsgstr \"Nowe połączenie zostało odrzucone\"\n\n#: ../app/ui.js:1166\nmsgid \"Password is required\"\nmsgstr \"Hasło jest wymagane\"\n\n#: ../vnc.html:89\nmsgid \"noVNC encountered an error:\"\nmsgstr \"noVNC napotkało błąd:\"\n\n#: ../vnc.html:99\nmsgid \"Hide/Show the control bar\"\nmsgstr \"Pokaż/Ukryj pasek ustawień\"\n\n#: ../vnc.html:106\nmsgid \"Move/Drag Viewport\"\nmsgstr \"Ruszaj/Przeciągaj Viewport\"\n\n#: ../vnc.html:106\nmsgid \"viewport drag\"\nmsgstr \"przeciągnij viewport\"\n\n#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121\nmsgid \"Active Mouse Button\"\nmsgstr \"Aktywny Przycisk Myszy\"\n\n#: ../vnc.html:112\nmsgid \"No mousebutton\"\nmsgstr \"Brak przycisku myszy\"\n\n#: ../vnc.html:115\nmsgid \"Left mousebutton\"\nmsgstr \"Lewy przycisk myszy\"\n\n#: ../vnc.html:118\nmsgid \"Middle mousebutton\"\nmsgstr \"Środkowy przycisk myszy\"\n\n#: ../vnc.html:121\nmsgid \"Right mousebutton\"\nmsgstr \"Prawy przycisk myszy\"\n\n#: ../vnc.html:124\nmsgid \"Keyboard\"\nmsgstr \"Klawiatura\"\n\n#: ../vnc.html:124\nmsgid \"Show keyboard\"\nmsgstr \"Pokaż klawiaturę\"\n\n#: ../vnc.html:131\nmsgid \"Extra keys\"\nmsgstr \"Przyciski dodatkowe\"\n\n#: ../vnc.html:131\nmsgid \"Show extra keys\"\nmsgstr \"Pokaż przyciski dodatkowe\"\n\n#: ../vnc.html:136\nmsgid \"Ctrl\"\nmsgstr \"Ctrl\"\n\n#: ../vnc.html:136\nmsgid \"Toggle Ctrl\"\nmsgstr \"Przełącz Ctrl\"\n\n#: ../vnc.html:139\nmsgid \"Alt\"\nmsgstr \"Alt\"\n\n#: ../vnc.html:139\nmsgid \"Toggle Alt\"\nmsgstr \"Przełącz Alt\"\n\n#: ../vnc.html:142\nmsgid \"Send Tab\"\nmsgstr \"Wyślij Tab\"\n\n#: ../vnc.html:142\nmsgid \"Tab\"\nmsgstr \"Tab\"\n\n#: ../vnc.html:145\nmsgid \"Esc\"\nmsgstr \"Esc\"\n\n#: ../vnc.html:145\nmsgid \"Send Escape\"\nmsgstr \"Wyślij Escape\"\n\n#: ../vnc.html:148\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"Ctrl+Alt+Del\"\n\n#: ../vnc.html:148\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"Wyślij Ctrl-Alt-Del\"\n\n#: ../vnc.html:156\nmsgid \"Shutdown/Reboot\"\nmsgstr \"Wyłącz/Uruchom ponownie\"\n\n#: ../vnc.html:156\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"Wyłącz/Uruchom ponownie...\"\n\n#: ../vnc.html:162\nmsgid \"Power\"\nmsgstr \"Włączony\"\n\n#: ../vnc.html:164\nmsgid \"Shutdown\"\nmsgstr \"Wyłącz\"\n\n#: ../vnc.html:165\nmsgid \"Reboot\"\nmsgstr \"Uruchom ponownie\"\n\n#: ../vnc.html:166\nmsgid \"Reset\"\nmsgstr \"Resetuj\"\n\n#: ../vnc.html:171 ../vnc.html:177\nmsgid \"Clipboard\"\nmsgstr \"Schowek\"\n\n#: ../vnc.html:181\nmsgid \"Clear\"\nmsgstr \"Wyczyść\"\n\n#: ../vnc.html:187\nmsgid \"Fullscreen\"\nmsgstr \"Pełny ekran\"\n\n#: ../vnc.html:192 ../vnc.html:199\nmsgid \"Settings\"\nmsgstr \"Ustawienia\"\n\n#: ../vnc.html:202\nmsgid \"Shared Mode\"\nmsgstr \"Tryb Współdzielenia\"\n\n#: ../vnc.html:205\nmsgid \"View Only\"\nmsgstr \"Tylko Podgląd\"\n\n#: ../vnc.html:209\nmsgid \"Clip to Window\"\nmsgstr \"Przytnij do Okna\"\n\n#: ../vnc.html:212\nmsgid \"Scaling Mode:\"\nmsgstr \"Tryb Skalowania:\"\n\n#: ../vnc.html:214\nmsgid \"None\"\nmsgstr \"Brak\"\n\n#: ../vnc.html:215\nmsgid \"Local scaling\"\nmsgstr \"Skalowanie lokalne\"\n\n#: ../vnc.html:216\nmsgid \"Remote resizing\"\nmsgstr \"Skalowanie zdalne\"\n\n#: ../vnc.html:221\nmsgid \"Advanced\"\nmsgstr \"Zaawansowane\"\n\n#: ../vnc.html:224\nmsgid \"Repeater ID:\"\nmsgstr \"ID Repeatera:\"\n\n#: ../vnc.html:228\nmsgid \"WebSocket\"\nmsgstr \"WebSocket\"\n\n#: ../vnc.html:231\nmsgid \"Encrypt\"\nmsgstr \"Szyfrowanie\"\n\n#: ../vnc.html:234\nmsgid \"Host:\"\nmsgstr \"Host:\"\n\n#: ../vnc.html:238\nmsgid \"Port:\"\nmsgstr \"Port:\"\n\n#: ../vnc.html:242\nmsgid \"Path:\"\nmsgstr \"Ścieżka:\"\n\n#: ../vnc.html:249\nmsgid \"Automatic reconnect\"\nmsgstr \"Automatycznie wznawiaj połączenie\"\n\n#: ../vnc.html:252\nmsgid \"Reconnect delay (ms):\"\nmsgstr \"Opóźnienie wznawiania (ms):\"\n\n#: ../vnc.html:258\nmsgid \"Logging:\"\nmsgstr \"Poziom logowania:\"\n\n#: ../vnc.html:270\nmsgid \"Disconnect\"\nmsgstr \"Rozłącz\"\n\n#: ../vnc.html:289\nmsgid \"Connect\"\nmsgstr \"Połącz\"\n\n#: ../vnc.html:299\nmsgid \"Password:\"\nmsgstr \"Hasło:\"\n\n#: ../vnc.html:313\nmsgid \"Cancel\"\nmsgstr \"Anuluj\"\n\n#: ../vnc.html:329\nmsgid \"Canvas not supported.\"\nmsgstr \"Element Canvas nie jest wspierany.\"\n\n#~ msgid \"Disconnect timeout\"\n#~ msgstr \"Timeout rozłączenia\"\n\n#~ msgid \"Local Downscaling\"\n#~ msgstr \"Downscaling lokalny\"\n\n#~ msgid \"Local Cursor\"\n#~ msgstr \"Lokalny kursor\"\n\n#~ msgid \"\"\n#~ \"Forcing clipping mode since scrollbars aren't supported by IE in \"\n#~ \"fullscreen\"\n#~ msgstr \"\"\n#~ \"Wymuszam clipping mode ponieważ paski przewijania nie są wspierane przez \"\n#~ \"IE w trybie pełnoekranowym\"\n\n#~ msgid \"True Color\"\n#~ msgstr \"True Color\"\n\n#~ msgid \"Style:\"\n#~ msgstr \"Styl:\"\n\n#~ msgid \"default\"\n#~ msgstr \"domyślny\"\n\n#~ msgid \"Apply\"\n#~ msgstr \"Zapisz\"\n\n#~ msgid \"Connection\"\n#~ msgstr \"Połączenie\"\n\n#~ msgid \"Token:\"\n#~ msgstr \"Token:\"\n\n#~ msgid \"Send Password\"\n#~ msgstr \"Wyślij Hasło\"\n"
  },
  {
    "path": "po/po2js",
    "content": "#!/usr/bin/env node\n/*\n * ps2js: gettext .po to noVNC .js converter\n * Copyright (C) 2018 The noVNC authors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <http://www.gnu.org/licenses/>.\n */\n\nimport { program } from 'commander';\nimport fs from 'fs';\nimport pofile from \"pofile\";\n\nprogram\n    .argument('<input>')\n    .argument('<output>')\n    .parse(process.argv);\n\nlet data = fs.readFileSync(program.args[0], \"utf8\");\nlet po = pofile.parse(data);\n\nconst bodyPart = po.items\n    .filter(item => item.msgid !== \"\")\n    .filter(item => item.msgstr[0] !== \"\")\n    .filter(item => !item.flags.fuzzy)\n    .filter(item => !item.obsolete)\n    .map(item => \"    \" + JSON.stringify(item.msgid) + \": \" + JSON.stringify(item.msgstr[0]))\n    .join(\",\\n\");\n\nconst output = \"{\\n\" + bodyPart + \"\\n}\";\n\nfs.writeFileSync(program.args[1], output);\n"
  },
  {
    "path": "po/pt_BR.po",
    "content": "# Portuguese translations for noVNC package.\n# Copyright (C) 2021 The noVNC authors\n# This file is distributed under the same license as the noVNC package.\n#  <liddack@outlook.com>, 2021.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 1.2.0\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2021-03-15 21:55-0300\\n\"\n\"PO-Revision-Date: 2021-03-15 22:09-0300\\n\"\n\"Last-Translator: <liddack@outlook.com>\\n\"\n\"Language-Team: Brazilian Portuguese\\n\"\n\"Language: pt_BR\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n > 1);\\n\"\n\"X-Generator: Poedit 2.4.1\\n\"\n\n#: ../app/ui.js:400\nmsgid \"Connecting...\"\nmsgstr \"Conectando...\"\n\n#: ../app/ui.js:407\nmsgid \"Disconnecting...\"\nmsgstr \"Desconectando...\"\n\n#: ../app/ui.js:413\nmsgid \"Reconnecting...\"\nmsgstr \"Reconectando...\"\n\n#: ../app/ui.js:418\nmsgid \"Internal error\"\nmsgstr \"Erro interno\"\n\n#: ../app/ui.js:1009\nmsgid \"Must set host\"\nmsgstr \"É necessário definir o host\"\n\n#: ../app/ui.js:1091\nmsgid \"Connected (encrypted) to \"\nmsgstr \"Conectado (com criptografia) a \"\n\n#: ../app/ui.js:1093\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"Conectado (sem criptografia) a \"\n\n#: ../app/ui.js:1116\nmsgid \"Something went wrong, connection is closed\"\nmsgstr \"Algo deu errado. A conexão foi encerrada.\"\n\n#: ../app/ui.js:1119\nmsgid \"Failed to connect to server\"\nmsgstr \"Falha ao conectar-se ao servidor\"\n\n#: ../app/ui.js:1129\nmsgid \"Disconnected\"\nmsgstr \"Desconectado\"\n\n#: ../app/ui.js:1144\nmsgid \"New connection has been rejected with reason: \"\nmsgstr \"A nova conexão foi rejeitada pelo motivo: \"\n\n#: ../app/ui.js:1147\nmsgid \"New connection has been rejected\"\nmsgstr \"A nova conexão foi rejeitada\"\n\n#: ../app/ui.js:1182\nmsgid \"Credentials are required\"\nmsgstr \"Credenciais são obrigatórias\"\n\n#: ../vnc.html:61\nmsgid \"noVNC encountered an error:\"\nmsgstr \"O noVNC encontrou um erro:\"\n\n#: ../vnc.html:71\nmsgid \"Hide/Show the control bar\"\nmsgstr \"Esconder/mostrar a barra de controles\"\n\n#: ../vnc.html:78\nmsgid \"Drag\"\nmsgstr \"Arrastar\"\n\n#: ../vnc.html:78\nmsgid \"Move/Drag viewport\"\nmsgstr \"Mover/arrastar a janela\"\n\n#: ../vnc.html:84\nmsgid \"Keyboard\"\nmsgstr \"Teclado\"\n\n#: ../vnc.html:84\nmsgid \"Show keyboard\"\nmsgstr \"Mostrar teclado\"\n\n#: ../vnc.html:89\nmsgid \"Extra keys\"\nmsgstr \"Teclas adicionais\"\n\n#: ../vnc.html:89\nmsgid \"Show extra keys\"\nmsgstr \"Mostrar teclas adicionais\"\n\n#: ../vnc.html:94\nmsgid \"Ctrl\"\nmsgstr \"Ctrl\"\n\n#: ../vnc.html:94\nmsgid \"Toggle Ctrl\"\nmsgstr \"Pressionar/soltar Ctrl\"\n\n#: ../vnc.html:97\nmsgid \"Alt\"\nmsgstr \"Alt\"\n\n#: ../vnc.html:97\nmsgid \"Toggle Alt\"\nmsgstr \"Pressionar/soltar Alt\"\n\n#: ../vnc.html:100\nmsgid \"Toggle Windows\"\nmsgstr \"Pressionar/soltar Windows\"\n\n#: ../vnc.html:100\nmsgid \"Windows\"\nmsgstr \"Windows\"\n\n#: ../vnc.html:103\nmsgid \"Send Tab\"\nmsgstr \"Enviar Tab\"\n\n#: ../vnc.html:103\nmsgid \"Tab\"\nmsgstr \"Tab\"\n\n#: ../vnc.html:106\nmsgid \"Esc\"\nmsgstr \"Esc\"\n\n#: ../vnc.html:106\nmsgid \"Send Escape\"\nmsgstr \"Enviar Esc\"\n\n#: ../vnc.html:109\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"Ctrl+Alt+Del\"\n\n#: ../vnc.html:109\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"Enviar Ctrl-Alt-Del\"\n\n#: ../vnc.html:116\nmsgid \"Shutdown/Reboot\"\nmsgstr \"Desligar/reiniciar\"\n\n#: ../vnc.html:116\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"Desligar/reiniciar...\"\n\n#: ../vnc.html:122\nmsgid \"Power\"\nmsgstr \"Ligar\"\n\n#: ../vnc.html:124\nmsgid \"Shutdown\"\nmsgstr \"Desligar\"\n\n#: ../vnc.html:125\nmsgid \"Reboot\"\nmsgstr \"Reiniciar\"\n\n#: ../vnc.html:126\nmsgid \"Reset\"\nmsgstr \"Reiniciar (forçado)\"\n\n#: ../vnc.html:131 ../vnc.html:137\nmsgid \"Clipboard\"\nmsgstr \"Área de transferência\"\n\n#: ../vnc.html:141\nmsgid \"Clear\"\nmsgstr \"Limpar\"\n\n#: ../vnc.html:147\nmsgid \"Fullscreen\"\nmsgstr \"Tela cheia\"\n\n#: ../vnc.html:152 ../vnc.html:159\nmsgid \"Settings\"\nmsgstr \"Configurações\"\n\n#: ../vnc.html:162\nmsgid \"Shared mode\"\nmsgstr \"Modo compartilhado\"\n\n#: ../vnc.html:165\nmsgid \"View only\"\nmsgstr \"Apenas visualizar\"\n\n#: ../vnc.html:169\nmsgid \"Clip to window\"\nmsgstr \"Recortar à janela\"\n\n#: ../vnc.html:172\nmsgid \"Scaling mode:\"\nmsgstr \"Modo de dimensionamento:\"\n\n#: ../vnc.html:174\nmsgid \"None\"\nmsgstr \"Nenhum\"\n\n#: ../vnc.html:175\nmsgid \"Local scaling\"\nmsgstr \"Local\"\n\n#: ../vnc.html:176\nmsgid \"Remote resizing\"\nmsgstr \"Remoto\"\n\n#: ../vnc.html:181\nmsgid \"Advanced\"\nmsgstr \"Avançado\"\n\n#: ../vnc.html:184\nmsgid \"Quality:\"\nmsgstr \"Qualidade:\"\n\n#: ../vnc.html:188\nmsgid \"Compression level:\"\nmsgstr \"Nível de compressão:\"\n\n#: ../vnc.html:193\nmsgid \"Repeater ID:\"\nmsgstr \"ID do repetidor:\"\n\n#: ../vnc.html:197\nmsgid \"WebSocket\"\nmsgstr \"WebSocket\"\n\n#: ../vnc.html:200\nmsgid \"Encrypt\"\nmsgstr \"Criptografar\"\n\n#: ../vnc.html:203\nmsgid \"Host:\"\nmsgstr \"Host:\"\n\n#: ../vnc.html:207\nmsgid \"Port:\"\nmsgstr \"Porta:\"\n\n#: ../vnc.html:211\nmsgid \"Path:\"\nmsgstr \"Caminho:\"\n\n#: ../vnc.html:218\nmsgid \"Automatic reconnect\"\nmsgstr \"Reconexão automática\"\n\n#: ../vnc.html:221\nmsgid \"Reconnect delay (ms):\"\nmsgstr \"Atraso da reconexão (ms)\"\n\n#: ../vnc.html:226\nmsgid \"Show dot when no cursor\"\nmsgstr \"Mostrar ponto quando não há cursor\"\n\n#: ../vnc.html:231\nmsgid \"Logging:\"\nmsgstr \"Registros:\"\n\n#: ../vnc.html:240\nmsgid \"Version:\"\nmsgstr \"Versão:\"\n\n#: ../vnc.html:248\nmsgid \"Disconnect\"\nmsgstr \"Desconectar\"\n\n#: ../vnc.html:267\nmsgid \"Connect\"\nmsgstr \"Conectar\"\n\n#: ../vnc.html:277\nmsgid \"Username:\"\nmsgstr \"Nome de usuário:\"\n\n#: ../vnc.html:281\nmsgid \"Password:\"\nmsgstr \"Senha:\"\n\n#: ../vnc.html:285\nmsgid \"Send credentials\"\nmsgstr \"Enviar credenciais\"\n\n#: ../vnc.html:295\nmsgid \"Cancel\"\nmsgstr \"Cancelar\"\n"
  },
  {
    "path": "po/ru.po",
    "content": "# Russian translations for noVNC package\n# Русский перевод для пакета noVNC.\n# Copyright (C) 2019 Dmitriy Shweew\n# This file is distributed under the same license as the noVNC package.\n# Dmitriy Shweew <shweew@it-advisor.ru>, 2019.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 1.5.0\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2021-08-27 16:03+0200\\n\"\n\"PO-Revision-Date: 2024-02-11 03:58+0300\\n\"\n\"Last-Translator: Dim5x <dim5x@yahoo.com>\\n\"\n\"Language-Team: Russian\\n\"\n\"Language: ru\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n\"\n\"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\\n\"\n\"X-Generator: Poedit 2.2.1\\n\"\n\"X-Poedit-Flags-xgettext: --add-comments\\n\"\n\n#: ../app/ui.js:400\nmsgid \"Connecting...\"\nmsgstr \"Подключение...\"\n\n#: ../app/ui.js:407\nmsgid \"Disconnecting...\"\nmsgstr \"Отключение...\"\n\n#: ../app/ui.js:413\nmsgid \"Reconnecting...\"\nmsgstr \"Переподключение...\"\n\n#: ../app/ui.js:418\nmsgid \"Internal error\"\nmsgstr \"Внутренняя ошибка\"\n\n#: ../app/ui.js:1009\nmsgid \"Must set host\"\nmsgstr \"Задайте имя сервера или IP\"\n\n#: ../app/ui.js:1091\nmsgid \"Connected (encrypted) to \"\nmsgstr \"Подключено (с шифрованием) к \"\n\n#: ../app/ui.js:1093\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"Подключено (без шифрования) к \"\n\n#: ../app/ui.js:1116\nmsgid \"Something went wrong, connection is closed\"\nmsgstr \"Что-то пошло не так, подключение разорвано\"\n\n#: ../app/ui.js:1119\nmsgid \"Failed to connect to server\"\nmsgstr \"Ошибка подключения к серверу\"\n\n#: ../app/ui.js:1129\nmsgid \"Disconnected\"\nmsgstr \"Отключено\"\n\n#: ../app/ui.js:1144\nmsgid \"New connection has been rejected with reason: \"\nmsgstr \"Новое соединение отклонено по причине: \"\n\n#: ../app/ui.js:1147\nmsgid \"New connection has been rejected\"\nmsgstr \"Новое соединение отклонено\"\n\n#: ../app/ui.js:1182\nmsgid \"Credentials are required\"\nmsgstr \"Требуются учетные данные\"\n\n#: ../vnc.html:61\nmsgid \"noVNC encountered an error:\"\nmsgstr \"Ошибка noVNC: \"\n\n#: ../vnc.html:71\nmsgid \"Hide/Show the control bar\"\nmsgstr \"Скрыть/Показать контрольную панель\"\n\n#: ../vnc.html:78\nmsgid \"Drag\"\nmsgstr \"Переместить\"\n\n#: ../vnc.html:78\nmsgid \"Move/Drag viewport\"\nmsgstr \"Переместить окно\"\n\n#: ../vnc.html:84\nmsgid \"Keyboard\"\nmsgstr \"Клавиатура\"\n\n#: ../vnc.html:84\nmsgid \"Show keyboard\"\nmsgstr \"Показать клавиатуру\"\n\n#: ../vnc.html:89\nmsgid \"Extra keys\"\nmsgstr \"Дополнительные Кнопки\"\n\n#: ../vnc.html:89\nmsgid \"Show Extra Keys\"\nmsgstr \"Показать Дополнительные Кнопки\"\n\n#: ../vnc.html:94\nmsgid \"Ctrl\"\nmsgstr \"Ctrl\"\n\n#: ../vnc.html:94\nmsgid \"Toggle Ctrl\"\nmsgstr \"Зажать Ctrl\"\n\n#: ../vnc.html:97\nmsgid \"Alt\"\nmsgstr \"Alt\"\n\n#: ../vnc.html:97\nmsgid \"Toggle Alt\"\nmsgstr \"Зажать Alt\"\n\n#: ../vnc.html:100\nmsgid \"Toggle Windows\"\nmsgstr \"Зажать Windows\"\n\n#: ../vnc.html:100\nmsgid \"Windows\"\nmsgstr \"Вкладка\"\n\n#: ../vnc.html:103\nmsgid \"Send Tab\"\nmsgstr \"Передать нажатие Tab\"\n\n#: ../vnc.html:103\nmsgid \"Tab\"\nmsgstr \"Tab\"\n\n#: ../vnc.html:106\nmsgid \"Esc\"\nmsgstr \"Esc\"\n\n#: ../vnc.html:106\nmsgid \"Send Escape\"\nmsgstr \"Передать нажатие Escape\"\n\n#: ../vnc.html:109\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"Ctrl+Alt+Del\"\n\n#: ../vnc.html:109\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"Передать нажатие Ctrl-Alt-Del\"\n\n#: ../vnc.html:116\nmsgid \"Shutdown/Reboot\"\nmsgstr \"Выключить/Перезагрузить\"\n\n#: ../vnc.html:116\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"Выключить/Перезагрузить...\"\n\n#: ../vnc.html:122\nmsgid \"Power\"\nmsgstr \"Питание\"\n\n#: ../vnc.html:124\nmsgid \"Shutdown\"\nmsgstr \"Выключить\"\n\n#: ../vnc.html:125\nmsgid \"Reboot\"\nmsgstr \"Перезагрузить\"\n\n#: ../vnc.html:126\nmsgid \"Reset\"\nmsgstr \"Сброс\"\n\n#: ../vnc.html:131 ../vnc.html:137\nmsgid \"Clipboard\"\nmsgstr \"Буфер обмена\"\n\n#: ../vnc.html:141\nmsgid \"Clear\"\nmsgstr \"Очистить\"\n\n#: ../vnc.html:147\nmsgid \"Fullscreen\"\nmsgstr \"Во весь экран\"\n\n#: ../vnc.html:152 ../vnc.html:159\nmsgid \"Settings\"\nmsgstr \"Настройки\"\n\n#: ../vnc.html:162\nmsgid \"Shared mode\"\nmsgstr \"Общий режим\"\n\n#: ../vnc.html:165\nmsgid \"View Only\"\nmsgstr \"Только Просмотр\"\n\n#: ../vnc.html:169\nmsgid \"Clip to window\"\nmsgstr \"В окно\"\n\n#: ../vnc.html:172\nmsgid \"Scaling mode:\"\nmsgstr \"Масштаб:\"\n\n#: ../vnc.html:174\nmsgid \"None\"\nmsgstr \"Нет\"\n\n#: ../vnc.html:175\nmsgid \"Local scaling\"\nmsgstr \"Локальный масштаб\"\n\n#: ../vnc.html:176\nmsgid \"Remote resizing\"\nmsgstr \"Удаленная перенастройка размера\"\n\n#: ../vnc.html:181\nmsgid \"Advanced\"\nmsgstr \"Дополнительно\"\n\n#: ../vnc.html:184\nmsgid \"Quality:\"\nmsgstr \"Качество\"\n\n#: ../vnc.html:188\nmsgid \"Compression level:\"\nmsgstr \"Уровень Сжатия\"\n\n#: ../vnc.html:193\nmsgid \"Repeater ID:\"\nmsgstr \"Идентификатор ID:\"\n\n#: ../vnc.html:197\nmsgid \"WebSocket\"\nmsgstr \"WebSocket\"\n\n#: ../vnc.html:200\nmsgid \"Encrypt\"\nmsgstr \"Шифрование\"\n\n#: ../vnc.html:203\nmsgid \"Host:\"\nmsgstr \"Сервер:\"\n\n#: ../vnc.html:207\nmsgid \"Port:\"\nmsgstr \"Порт:\"\n\n#: ../vnc.html:211\nmsgid \"Path:\"\nmsgstr \"Путь:\"\n\n#: ../vnc.html:218\nmsgid \"Automatic reconnect\"\nmsgstr \"Автоматическое переподключение\"\n\n#: ../vnc.html:221\nmsgid \"Reconnect delay (ms):\"\nmsgstr \"Задержка переподключения (мс):\"\n\n#: ../vnc.html:226\nmsgid \"Show dot when no cursor\"\nmsgstr \"Показать точку вместо курсора\"\n\n#: ../vnc.html:231\nmsgid \"Logging:\"\nmsgstr \"Лог:\"\n\n#: ../vnc.html:240\nmsgid \"Version:\"\nmsgstr \"Версия\"\n\n#: ../vnc.html:248\nmsgid \"Disconnect\"\nmsgstr \"Отключение\"\n\n#: ../vnc.html:267\nmsgid \"Connect\"\nmsgstr \"Подключение\"\n\n#: ../vnc.html:277\nmsgid \"Username:\"\nmsgstr \"Имя Пользователя\"\n\n#: ../vnc.html:281\nmsgid \"Password:\"\nmsgstr \"Пароль:\"\n\n#: ../vnc.html:285\nmsgid \"Send Credentials\"\nmsgstr \"Передача Учетных Данных\"\n\n#: ../vnc.html:295\nmsgid \"Cancel\"\nmsgstr \"Выход\"\n"
  },
  {
    "path": "po/sv.po",
    "content": "# Swedish translations for noVNC package\r\n# Svenska översättningar för paketet noVNC.\r\n# Copyright (C) 2025 The noVNC authors\r\n# This file is distributed under the same license as the noVNC package.\r\n# Samuel Mannehed <samuel@cendio.se>, 2020.\r\n#\r\nmsgid \"\"\r\nmsgstr \"\"\r\n\"Project-Id-Version: noVNC 1.6.0\\n\"\r\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\r\n\"POT-Creation-Date: 2025-10-31 09:17+0100\\n\"\r\n\"PO-Revision-Date: 2025-10-31 10:48+0100\\n\"\r\n\"Last-Translator: Alexander Zeijlon <aleze@cendio.com>\\n\"\r\n\"Language-Team: none\\n\"\r\n\"Language: sv\\n\"\r\n\"MIME-Version: 1.0\\n\"\r\n\"Content-Type: text/plain; charset=UTF-8\\n\"\r\n\"Content-Transfer-Encoding: 8bit\\n\"\r\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\r\n\"X-Generator: Poedit 3.7\\n\"\r\n\r\n#: ../app/ui.js:84\r\nmsgid \"\"\r\n\"Running without HTTPS is not recommended, crashes or other issues are likely.\"\r\nmsgstr \"\"\r\n\"Det är ej rekommenderat att köra utan HTTPS, krascher och andra problem är \"\r\n\"troliga.\"\r\n\r\n#: ../app/ui.js:413\r\nmsgid \"Connecting...\"\r\nmsgstr \"Ansluter...\"\r\n\r\n#: ../app/ui.js:420\r\nmsgid \"Disconnecting...\"\r\nmsgstr \"Kopplar ifrån...\"\r\n\r\n#: ../app/ui.js:426\r\nmsgid \"Reconnecting...\"\r\nmsgstr \"Återansluter...\"\r\n\r\n#: ../app/ui.js:431\r\nmsgid \"Internal error\"\r\nmsgstr \"Internt fel\"\r\n\r\n#: ../app/ui.js:1084\r\nmsgid \"Failed to connect to server: \"\r\nmsgstr \"Misslyckades att ansluta till servern: \"\r\n\r\n#: ../app/ui.js:1151\r\nmsgid \"Connected (encrypted) to \"\r\nmsgstr \"Ansluten (krypterat) till \"\r\n\r\n#: ../app/ui.js:1153\r\nmsgid \"Connected (unencrypted) to \"\r\nmsgstr \"Ansluten (okrypterat) till \"\r\n\r\n#: ../app/ui.js:1178\r\nmsgid \"Something went wrong, connection is closed\"\r\nmsgstr \"Något gick fel, anslutningen avslutades\"\r\n\r\n#: ../app/ui.js:1181\r\nmsgid \"Failed to connect to server\"\r\nmsgstr \"Misslyckades att ansluta till servern\"\r\n\r\n#: ../app/ui.js:1193\r\nmsgid \"Disconnected\"\r\nmsgstr \"Frånkopplad\"\r\n\r\n#: ../app/ui.js:1210\r\nmsgid \"New connection has been rejected with reason: \"\r\nmsgstr \"Ny anslutning har blivit nekad med följande skäl: \"\r\n\r\n#: ../app/ui.js:1213\r\nmsgid \"New connection has been rejected\"\r\nmsgstr \"Ny anslutning har blivit nekad\"\r\n\r\n#: ../app/ui.js:1225\r\nmsgid \"Are you sure you want to disconnect the session?\"\r\nmsgstr \"Är du säker på att du vill koppla ifrån sessionen?\"\r\n\r\n#: ../app/ui.js:1297\r\nmsgid \"Credentials are required\"\r\nmsgstr \"Användaruppgifter krävs\"\r\n\r\n#: ../vnc.html:106\r\nmsgid \"noVNC encountered an error:\"\r\nmsgstr \"noVNC stötte på ett problem:\"\r\n\r\n#: ../vnc.html:116\r\nmsgid \"Hide/Show the control bar\"\r\nmsgstr \"Göm/Visa kontrollbaren\"\r\n\r\n#: ../vnc.html:125\r\nmsgid \"Drag\"\r\nmsgstr \"Dra\"\r\n\r\n#: ../vnc.html:125\r\nmsgid \"Move/Drag viewport\"\r\nmsgstr \"Flytta/Dra vyn\"\r\n\r\n#: ../vnc.html:131\r\nmsgid \"Keyboard\"\r\nmsgstr \"Tangentbord\"\r\n\r\n#: ../vnc.html:131\r\nmsgid \"Show keyboard\"\r\nmsgstr \"Visa tangentbord\"\r\n\r\n#: ../vnc.html:136\r\nmsgid \"Extra keys\"\r\nmsgstr \"Extraknappar\"\r\n\r\n#: ../vnc.html:136\r\nmsgid \"Show extra keys\"\r\nmsgstr \"Visa extraknappar\"\r\n\r\n#: ../vnc.html:141\r\nmsgid \"Ctrl\"\r\nmsgstr \"Ctrl\"\r\n\r\n#: ../vnc.html:141\r\nmsgid \"Toggle Ctrl\"\r\nmsgstr \"Växla Ctrl\"\r\n\r\n#: ../vnc.html:144\r\nmsgid \"Alt\"\r\nmsgstr \"Alt\"\r\n\r\n#: ../vnc.html:144\r\nmsgid \"Toggle Alt\"\r\nmsgstr \"Växla Alt\"\r\n\r\n#: ../vnc.html:147\r\nmsgid \"Toggle Windows\"\r\nmsgstr \"Växla Windows\"\r\n\r\n#: ../vnc.html:147\r\nmsgid \"Windows\"\r\nmsgstr \"Windows\"\r\n\r\n#: ../vnc.html:150\r\nmsgid \"Send Tab\"\r\nmsgstr \"Skicka Tab\"\r\n\r\n#: ../vnc.html:150\r\nmsgid \"Tab\"\r\nmsgstr \"Tab\"\r\n\r\n#: ../vnc.html:153\r\nmsgid \"Esc\"\r\nmsgstr \"Esc\"\r\n\r\n#: ../vnc.html:153\r\nmsgid \"Send Escape\"\r\nmsgstr \"Skicka Escape\"\r\n\r\n#: ../vnc.html:156\r\nmsgid \"Ctrl+Alt+Del\"\r\nmsgstr \"Ctrl+Alt+Del\"\r\n\r\n#: ../vnc.html:156\r\nmsgid \"Send Ctrl-Alt-Del\"\r\nmsgstr \"Skicka Ctrl-Alt-Del\"\r\n\r\n#: ../vnc.html:163\r\nmsgid \"Shutdown/Reboot\"\r\nmsgstr \"Stäng av/Starta om\"\r\n\r\n#: ../vnc.html:163\r\nmsgid \"Shutdown/Reboot...\"\r\nmsgstr \"Stäng av/Starta om...\"\r\n\r\n#: ../vnc.html:169\r\nmsgid \"Power\"\r\nmsgstr \"Ström\"\r\n\r\n#: ../vnc.html:171\r\nmsgid \"Shutdown\"\r\nmsgstr \"Stäng av\"\r\n\r\n#: ../vnc.html:172\r\nmsgid \"Reboot\"\r\nmsgstr \"Starta om\"\r\n\r\n#: ../vnc.html:173\r\nmsgid \"Reset\"\r\nmsgstr \"Återställ\"\r\n\r\n#: ../vnc.html:178 ../vnc.html:184\r\nmsgid \"Clipboard\"\r\nmsgstr \"Urklipp\"\r\n\r\n#: ../vnc.html:186\r\nmsgid \"Edit clipboard content in the textarea below.\"\r\nmsgstr \"Redigera urklippets innehåll i fältet nedan.\"\r\n\r\n#: ../vnc.html:194\r\nmsgid \"Full screen\"\r\nmsgstr \"Fullskärm\"\r\n\r\n#: ../vnc.html:199 ../vnc.html:205\r\nmsgid \"Settings\"\r\nmsgstr \"Inställningar\"\r\n\r\n#: ../vnc.html:211\r\nmsgid \"Shared mode\"\r\nmsgstr \"Delat läge\"\r\n\r\n#: ../vnc.html:218\r\nmsgid \"View only\"\r\nmsgstr \"Endast visning\"\r\n\r\n#: ../vnc.html:226\r\nmsgid \"Clip to window\"\r\nmsgstr \"Begränsa till fönster\"\r\n\r\n#: ../vnc.html:231\r\nmsgid \"Scaling mode:\"\r\nmsgstr \"Skalningsläge:\"\r\n\r\n#: ../vnc.html:233\r\nmsgid \"None\"\r\nmsgstr \"Ingen\"\r\n\r\n#: ../vnc.html:234\r\nmsgid \"Local scaling\"\r\nmsgstr \"Lokal skalning\"\r\n\r\n#: ../vnc.html:235\r\nmsgid \"Remote resizing\"\r\nmsgstr \"Ändra storlek\"\r\n\r\n#: ../vnc.html:240\r\nmsgid \"Advanced\"\r\nmsgstr \"Avancerat\"\r\n\r\n#: ../vnc.html:243\r\nmsgid \"Quality:\"\r\nmsgstr \"Kvalitet:\"\r\n\r\n#: ../vnc.html:247\r\nmsgid \"Compression level:\"\r\nmsgstr \"Kompressionsnivå:\"\r\n\r\n#: ../vnc.html:252\r\nmsgid \"Repeater ID:\"\r\nmsgstr \"Repeater-ID:\"\r\n\r\n#: ../vnc.html:256\r\nmsgid \"WebSocket\"\r\nmsgstr \"WebSocket\"\r\n\r\n#: ../vnc.html:261\r\nmsgid \"Encrypt\"\r\nmsgstr \"Kryptera\"\r\n\r\n#: ../vnc.html:266\r\nmsgid \"Host:\"\r\nmsgstr \"Värd:\"\r\n\r\n#: ../vnc.html:270\r\nmsgid \"Port:\"\r\nmsgstr \"Port:\"\r\n\r\n#: ../vnc.html:274\r\nmsgid \"Path:\"\r\nmsgstr \"Sökväg:\"\r\n\r\n#: ../vnc.html:283\r\nmsgid \"Automatic reconnect\"\r\nmsgstr \"Automatisk återanslutning\"\r\n\r\n#: ../vnc.html:288\r\nmsgid \"Reconnect delay (ms):\"\r\nmsgstr \"Fördröjning (ms):\"\r\n\r\n#: ../vnc.html:295\r\nmsgid \"Show dot when no cursor\"\r\nmsgstr \"Visa prick när ingen muspekare finns\"\r\n\r\n#: ../vnc.html:302\r\nmsgid \"Logging:\"\r\nmsgstr \"Loggning:\"\r\n\r\n#: ../vnc.html:311\r\nmsgid \"Version:\"\r\nmsgstr \"Version:\"\r\n\r\n#: ../vnc.html:319\r\nmsgid \"Disconnect\"\r\nmsgstr \"Koppla ifrån\"\r\n\r\n#: ../vnc.html:342\r\nmsgid \"Connect\"\r\nmsgstr \"Anslut\"\r\n\r\n#: ../vnc.html:351\r\nmsgid \"Server identity\"\r\nmsgstr \"Serveridentitet\"\r\n\r\n#: ../vnc.html:354\r\nmsgid \"The server has provided the following identifying information:\"\r\nmsgstr \"Servern har gett följande identifierande information:\"\r\n\r\n#: ../vnc.html:357\r\nmsgid \"Fingerprint:\"\r\nmsgstr \"Fingeravtryck:\"\r\n\r\n#: ../vnc.html:361\r\nmsgid \"\"\r\n\"Please verify that the information is correct and press \\\"Approve\\\". \"\r\n\"Otherwise press \\\"Reject\\\".\"\r\nmsgstr \"\"\r\n\"Kontrollera att informationen är korrekt och tryck sedan \\\"Godkänn\\\". Tryck \"\r\n\"annars \\\"Avvisa\\\".\"\r\n\r\n#: ../vnc.html:366\r\nmsgid \"Approve\"\r\nmsgstr \"Godkänn\"\r\n\r\n#: ../vnc.html:367\r\nmsgid \"Reject\"\r\nmsgstr \"Avvisa\"\r\n\r\n#: ../vnc.html:375\r\nmsgid \"Credentials\"\r\nmsgstr \"Användaruppgifter\"\r\n\r\n#: ../vnc.html:379\r\nmsgid \"Username:\"\r\nmsgstr \"Användarnamn:\"\r\n\r\n#: ../vnc.html:383\r\nmsgid \"Password:\"\r\nmsgstr \"Lösenord:\"\r\n\r\n#: ../vnc.html:387\r\nmsgid \"Send credentials\"\r\nmsgstr \"Skicka användaruppgifter\"\r\n\r\n#: ../vnc.html:396\r\nmsgid \"Cancel\"\r\nmsgstr \"Avbryt\"\r\n\r\n#~ msgid \"Must set host\"\r\n#~ msgstr \"Du måste specifiera en värd\"\r\n\r\n#~ msgid \"HTTPS is required for full functionality\"\r\n#~ msgstr \"HTTPS krävs för full funktionalitet\"\r\n\r\n#~ msgid \"Clear\"\r\n#~ msgstr \"Rensa\"\r\n"
  },
  {
    "path": "po/tr.po",
    "content": "# Turkish translations for noVNC package\n# Turkish translation for noVNC.\n# Copyright (C) 2018 The noVNC authors\n# This file is distributed under the same license as the noVNC package.\n# Ömer ÇAKMAK <farukomercakmak@gmail.com>, 2018.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 0.6.1\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2017-11-24 07:16+0000\\n\"\n\"PO-Revision-Date: 2018-01-05 19:07+0300\\n\"\n\"Last-Translator: Ömer ÇAKMAK <farukomercakmak@gmail.com>\\n\"\n\"Language-Team: Türkçe <gnome-turk@gnome.org>\\n\"\n\"Language: tr\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\"X-Generator: Gtranslator 2.91.7\\n\"\n\n#: ../app/ui.js:404\nmsgid \"Connecting...\"\nmsgstr \"Bağlanıyor...\"\n\n#: ../app/ui.js:411\nmsgid \"Disconnecting...\"\nmsgstr \"Bağlantı kesiliyor...\"\n\n#: ../app/ui.js:417\nmsgid \"Reconnecting...\"\nmsgstr \"Yeniden bağlantı kuruluyor...\"\n\n#: ../app/ui.js:422\nmsgid \"Internal error\"\nmsgstr \"İç hata\"\n\n#: ../app/ui.js:1019\nmsgid \"Must set host\"\nmsgstr \"Sunucuyu kur\"\n\n#: ../app/ui.js:1099\nmsgid \"Connected (encrypted) to \"\nmsgstr \"Bağlı (şifrelenmiş)\"\n\n#: ../app/ui.js:1101\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"Bağlandı (şifrelenmemiş)\"\n\n#: ../app/ui.js:1119\nmsgid \"Something went wrong, connection is closed\"\nmsgstr \"Bir şeyler ters gitti, bağlantı kesildi\"\n\n#: ../app/ui.js:1129\nmsgid \"Disconnected\"\nmsgstr \"Bağlantı kesildi\"\n\n#: ../app/ui.js:1142\nmsgid \"New connection has been rejected with reason: \"\nmsgstr \"Bağlantı aşağıdaki nedenlerden dolayı reddedildi: \"\n\n#: ../app/ui.js:1145\nmsgid \"New connection has been rejected\"\nmsgstr \"Bağlantı reddedildi\"\n\n#: ../app/ui.js:1166\nmsgid \"Password is required\"\nmsgstr \"Şifre gerekli\"\n\n#: ../vnc.html:89\nmsgid \"noVNC encountered an error:\"\nmsgstr \"Bir hata oluştu:\"\n\n#: ../vnc.html:99\nmsgid \"Hide/Show the control bar\"\nmsgstr \"Denetim masasını Gizle/Göster\"\n\n#: ../vnc.html:106\nmsgid \"Move/Drag Viewport\"\nmsgstr \"Görünümü Taşı/Sürükle\"\n\n#: ../vnc.html:106\nmsgid \"viewport drag\"\nmsgstr \"Görüntü penceresini sürükle\"\n\n#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121\nmsgid \"Active Mouse Button\"\nmsgstr \"Aktif Fare Düğmesi\"\n\n#: ../vnc.html:112\nmsgid \"No mousebutton\"\nmsgstr \"Fare düğmesi yok\"\n\n#: ../vnc.html:115\nmsgid \"Left mousebutton\"\nmsgstr \"Farenin sol düğmesi\"\n\n#: ../vnc.html:118\nmsgid \"Middle mousebutton\"\nmsgstr \"Farenin orta düğmesi\"\n\n#: ../vnc.html:121\nmsgid \"Right mousebutton\"\nmsgstr \"Farenin sağ düğmesi\"\n\n#: ../vnc.html:124\nmsgid \"Keyboard\"\nmsgstr \"Klavye\"\n\n#: ../vnc.html:124\nmsgid \"Show Keyboard\"\nmsgstr \"Klavye Düzenini Göster\"\n\n#: ../vnc.html:131\nmsgid \"Extra keys\"\nmsgstr \"Ekstra tuşlar\"\n\n#: ../vnc.html:131\nmsgid \"Show extra keys\"\nmsgstr \"Ekstra tuşları göster\"\n\n#: ../vnc.html:136\nmsgid \"Ctrl\"\nmsgstr \"Ctrl\"\n\n#: ../vnc.html:136\nmsgid \"Toggle Ctrl\"\nmsgstr \"Ctrl Değiştir \"\n\n#: ../vnc.html:139\nmsgid \"Alt\"\nmsgstr \"Alt\"\n\n#: ../vnc.html:139\nmsgid \"Toggle Alt\"\nmsgstr \"Alt Değiştir\"\n\n#: ../vnc.html:142\nmsgid \"Send Tab\"\nmsgstr \"Sekme Gönder\"\n\n#: ../vnc.html:142\nmsgid \"Tab\"\nmsgstr \"Sekme\"\n\n#: ../vnc.html:145\nmsgid \"Esc\"\nmsgstr \"Esc\"\n\n#: ../vnc.html:145\nmsgid \"Send Escape\"\nmsgstr \"Boşluk Gönder\"\n\n#: ../vnc.html:148\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"Ctrl + Alt + Del\"\n\n#: ../vnc.html:148\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"Ctrl-Alt-Del Gönder\"\n\n#: ../vnc.html:156\nmsgid \"Shutdown/Reboot\"\nmsgstr \"Kapat/Yeniden Başlat\"\n\n#: ../vnc.html:156\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"Kapat/Yeniden Başlat...\"\n\n#: ../vnc.html:162\nmsgid \"Power\"\nmsgstr \"Güç\"\n\n#: ../vnc.html:164\nmsgid \"Shutdown\"\nmsgstr \"Kapat\"\n\n#: ../vnc.html:165\nmsgid \"Reboot\"\nmsgstr \"Yeniden Başlat\"\n\n#: ../vnc.html:166\nmsgid \"Reset\"\nmsgstr \"Sıfırla\"\n\n#: ../vnc.html:171 ../vnc.html:177\nmsgid \"Clipboard\"\nmsgstr \"Pano\"\n\n#: ../vnc.html:181\nmsgid \"Clear\"\nmsgstr \"Temizle\"\n\n#: ../vnc.html:187\nmsgid \"Fullscreen\"\nmsgstr \"Tam Ekran\"\n\n#: ../vnc.html:192 ../vnc.html:199\nmsgid \"Settings\"\nmsgstr \"Ayarlar\"\n\n#: ../vnc.html:202\nmsgid \"Shared Mode\"\nmsgstr \"Paylaşım Modu\"\n\n#: ../vnc.html:205\nmsgid \"View Only\"\nmsgstr \"Sadece Görüntüle\"\n\n#: ../vnc.html:209\nmsgid \"Clip to Window\"\nmsgstr \"Pencereye Tıkla\"\n\n#: ../vnc.html:212\nmsgid \"Scaling Mode:\"\nmsgstr \"Ölçekleme Modu:\"\n\n#: ../vnc.html:214\nmsgid \"None\"\nmsgstr \"Bilinmeyen\"\n\n#: ../vnc.html:215\nmsgid \"Local Scaling\"\nmsgstr \"Yerel Ölçeklendirme\"\n\n#: ../vnc.html:216\nmsgid \"Remote Resizing\"\nmsgstr \"Uzaktan Yeniden Boyutlandırma\"\n\n#: ../vnc.html:221\nmsgid \"Advanced\"\nmsgstr \"Gelişmiş\"\n\n#: ../vnc.html:224\nmsgid \"Repeater ID:\"\nmsgstr \"Tekralayıcı ID:\"\n\n#: ../vnc.html:228\nmsgid \"WebSocket\"\nmsgstr \"WebSocket\"\n\n#: ../vnc.html:231\nmsgid \"Encrypt\"\nmsgstr \"Şifrele\"\n\n#: ../vnc.html:234\nmsgid \"Host:\"\nmsgstr \"Ana makine:\"\n\n#: ../vnc.html:238\nmsgid \"Port:\"\nmsgstr \"Port:\"\n\n#: ../vnc.html:242\nmsgid \"Path:\"\nmsgstr \"Yol:\"\n\n#: ../vnc.html:249\nmsgid \"Automatic Reconnect\"\nmsgstr \"Otomatik Yeniden Bağlan\"\n\n#: ../vnc.html:252\nmsgid \"Reconnect Delay (ms):\"\nmsgstr \"Yeniden Bağlanma Süreci (ms):\"\n\n#: ../vnc.html:258\nmsgid \"Logging:\"\nmsgstr \"Giriş yapılıyor:\"\n\n#: ../vnc.html:270\nmsgid \"Disconnect\"\nmsgstr \"Bağlantıyı Kes\"\n\n#: ../vnc.html:289\nmsgid \"Connect\"\nmsgstr \"Bağlan\"\n\n#: ../vnc.html:299\nmsgid \"Password:\"\nmsgstr \"Parola:\"\n\n#: ../vnc.html:313\nmsgid \"Cancel\"\nmsgstr \"Vazgeç\"\n\n#: ../vnc.html:329\nmsgid \"Canvas not supported.\"\nmsgstr \"Tuval desteklenmiyor.\"\n"
  },
  {
    "path": "po/uk.po",
    "content": "# Ukrainian translation of noVNC.\n# Copyright (C) 2025 The noVNC authors\n# This file is distributed under the same license as the noVNC package.\n# Denys Nykula <nykula@ukr.net>, 2025.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 1.6.0\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2025-10-31 09:17+0100\\n\"\n\"PO-Revision-Date: 2025-11-22 20:21+0200\\n\"\n\"Last-Translator: Denys Nykula <nykula@ukr.net>\\n\"\n\"Language-Team: Ukrainian <uk@li.org>\\n\"\n\"Language: uk\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n\"\n\"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\\n\"\n\n#: ../app/ui.js:84\nmsgid \"\"\n\"Running without HTTPS is not recommended, crashes or other issues are likely.\"\nmsgstr \"\"\n\"Робота без HTTPS не рекомендується, ймовірні збої чи інші проблеми.\"\n\n#: ../app/ui.js:413\nmsgid \"Connecting...\"\nmsgstr \"З'єднання...\"\n\n#: ../app/ui.js:420\nmsgid \"Disconnecting...\"\nmsgstr \"Від'єднання...\"\n\n#: ../app/ui.js:426\nmsgid \"Reconnecting...\"\nmsgstr \"Перез'єднання...\"\n\n#: ../app/ui.js:431\nmsgid \"Internal error\"\nmsgstr \"Внутрішня помилка\"\n\n#: ../app/ui.js:1084\nmsgid \"Failed to connect to server: \"\nmsgstr \"Не вдалося з'єднатися з сервером: \"\n\n#: ../app/ui.js:1151\nmsgid \"Connected (encrypted) to \"\nmsgstr \"З'єднано (з шифруванням) з \"\n\n#: ../app/ui.js:1153\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"З'єднано (без шифрування) з \"\n\n#: ../app/ui.js:1178\nmsgid \"Something went wrong, connection is closed\"\nmsgstr \"Щось пішло не так, з'єднання закрито\"\n\n#: ../app/ui.js:1181\nmsgid \"Failed to connect to server\"\nmsgstr \"Не вдалося з'єднатися з сервером\"\n\n#: ../app/ui.js:1193\nmsgid \"Disconnected\"\nmsgstr \"Від'єднано\"\n\n#: ../app/ui.js:1210\nmsgid \"New connection has been rejected with reason: \"\nmsgstr \"Нове з'єднання відхилено. Причина: \"\n\n#: ../app/ui.js:1213\nmsgid \"New connection has been rejected\"\nmsgstr \"Нове з'єднання відхилено\"\n\n#: ../app/ui.js:1225\nmsgid \"Are you sure you want to disconnect the session?\"\nmsgstr \"Точно від'єднати сеанс?\"\n\n#: ../app/ui.js:1297\nmsgid \"Credentials are required\"\nmsgstr \"Треба особові дані\"\n\n#: ../vnc.html:106\nmsgid \"noVNC encountered an error:\"\nmsgstr \"Помилка noVNC:\"\n\n#: ../vnc.html:116\nmsgid \"Hide/Show the control bar\"\nmsgstr \"Сховати/показати панель керування\"\n\n#: ../vnc.html:125\nmsgid \"Drag\"\nmsgstr \"Посунути\"\n\n#: ../vnc.html:125\nmsgid \"Move/Drag viewport\"\nmsgstr \"Змістити область огляду\"\n\n#: ../vnc.html:131\nmsgid \"Keyboard\"\nmsgstr \"Клавіатура\"\n\n#: ../vnc.html:131\nmsgid \"Show keyboard\"\nmsgstr \"Показати клавіатуру\"\n\n#: ../vnc.html:136\nmsgid \"Extra keys\"\nmsgstr \"Додаткові клавіші\"\n\n#: ../vnc.html:136\nmsgid \"Show extra keys\"\nmsgstr \"Показати додаткові клавіші\"\n\n#: ../vnc.html:141\nmsgid \"Ctrl\"\nmsgstr \"Ctrl\"\n\n#: ../vnc.html:141\nmsgid \"Toggle Ctrl\"\nmsgstr \"Затиснути Ctrl\"\n\n#: ../vnc.html:144\nmsgid \"Alt\"\nmsgstr \"Alt\"\n\n#: ../vnc.html:144\nmsgid \"Toggle Alt\"\nmsgstr \"Затиснути Alt\"\n\n#: ../vnc.html:147\nmsgid \"Toggle Windows\"\nmsgstr \"Затиснути Windows\"\n\n#: ../vnc.html:147\nmsgid \"Windows\"\nmsgstr \"Windows\"\n\n#: ../vnc.html:150\nmsgid \"Send Tab\"\nmsgstr \"Натиснути Tab\"\n\n#: ../vnc.html:150\nmsgid \"Tab\"\nmsgstr \"Tab\"\n\n#: ../vnc.html:153\nmsgid \"Esc\"\nmsgstr \"Esc\"\n\n#: ../vnc.html:153\nmsgid \"Send Escape\"\nmsgstr \"Натиснути Escape\"\n\n#: ../vnc.html:156\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"Ctrl+Alt+Del\"\n\n#: ../vnc.html:156\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"Натиснути Ctrl+Alt+Del\"\n\n#: ../vnc.html:163\nmsgid \"Shutdown/Reboot\"\nmsgstr \"Вимкнути/перезавантажити\"\n\n#: ../vnc.html:163\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"Вимкнути/перезавантажити...\"\n\n#: ../vnc.html:169\nmsgid \"Power\"\nmsgstr \"Живлення\"\n\n#: ../vnc.html:171\nmsgid \"Shutdown\"\nmsgstr \"Вимкнути\"\n\n#: ../vnc.html:172\nmsgid \"Reboot\"\nmsgstr \"Перезавантажити\"\n\n#: ../vnc.html:173\nmsgid \"Reset\"\nmsgstr \"Скинути\"\n\n#: ../vnc.html:178 ../vnc.html:184\nmsgid \"Clipboard\"\nmsgstr \"Буфер обміну\"\n\n#: ../vnc.html:186\nmsgid \"Edit clipboard content in the textarea below.\"\nmsgstr \"Редагуйте вміст буфера обміну в текстовій зоні внизу.\"\n\n#: ../vnc.html:194\nmsgid \"Full screen\"\nmsgstr \"Повний екран\"\n\n#: ../vnc.html:199 ../vnc.html:205\nmsgid \"Settings\"\nmsgstr \"Параметри\"\n\n#: ../vnc.html:211\nmsgid \"Shared mode\"\nmsgstr \"Спільний режим\"\n\n#: ../vnc.html:218\nmsgid \"View only\"\nmsgstr \"Лише перегляд\"\n\n#: ../vnc.html:226\nmsgid \"Clip to window\"\nmsgstr \"До розмірів вікна\"\n\n#: ../vnc.html:231\nmsgid \"Scaling mode:\"\nmsgstr \"Режим масштабування:\"\n\n#: ../vnc.html:233\nmsgid \"None\"\nmsgstr \"Вимкнено\"\n\n#: ../vnc.html:234\nmsgid \"Local scaling\"\nmsgstr \"Локальне масштабування\"\n\n#: ../vnc.html:235\nmsgid \"Remote resizing\"\nmsgstr \"Віддалене масштабування\"\n\n#: ../vnc.html:240\nmsgid \"Advanced\"\nmsgstr \"Додатково\"\n\n#: ../vnc.html:243\nmsgid \"Quality:\"\nmsgstr \"Якість:\"\n\n#: ../vnc.html:247\nmsgid \"Compression level:\"\nmsgstr \"Рівень стиснення:\"\n\n#: ../vnc.html:252\nmsgid \"Repeater ID:\"\nmsgstr \"Ідентифікатор репітера:\"\n\n#: ../vnc.html:256\nmsgid \"WebSocket\"\nmsgstr \"WebSocket\"\n\n#: ../vnc.html:261\nmsgid \"Encrypt\"\nmsgstr \"Шифрування\"\n\n#: ../vnc.html:266\nmsgid \"Host:\"\nmsgstr \"Сервер:\"\n\n#: ../vnc.html:270\nmsgid \"Port:\"\nmsgstr \"Порт:\"\n\n#: ../vnc.html:274\nmsgid \"Path:\"\nmsgstr \"Шлях:\"\n\n#: ../vnc.html:283\nmsgid \"Automatic reconnect\"\nmsgstr \"Автоматичне перез'єднання\"\n\n#: ../vnc.html:288\nmsgid \"Reconnect delay (ms):\"\nmsgstr \"Затримка перез'єднання (мс):\"\n\n#: ../vnc.html:295\nmsgid \"Show dot when no cursor\"\nmsgstr \"Показувати крапку, коли нема курсора\"\n\n#: ../vnc.html:302\nmsgid \"Logging:\"\nmsgstr \"Журнал:\"\n\n#: ../vnc.html:311\nmsgid \"Version:\"\nmsgstr \"Версія:\"\n\n#: ../vnc.html:319\nmsgid \"Disconnect\"\nmsgstr \"Від'єднати\"\n\n#: ../vnc.html:342\nmsgid \"Connect\"\nmsgstr \"З'єднати\"\n\n#: ../vnc.html:351\nmsgid \"Server identity\"\nmsgstr \"Ідентифікація сервера\"\n\n#: ../vnc.html:354\nmsgid \"The server has provided the following identifying information:\"\nmsgstr \"Сервер надає такі ідентифікаційні дані:\"\n\n#: ../vnc.html:357\nmsgid \"Fingerprint:\"\nmsgstr \"Відбиток:\"\n\n#: ../vnc.html:361\nmsgid \"\"\n\"Please verify that the information is correct and press \\\"Approve\\\". \"\n\"Otherwise press \\\"Reject\\\".\"\nmsgstr \"\"\n\"Перевірте, чи дані коректні, й натисніть «Схвалити». \"\n\"Інакше натисніть «Відхилити».\"\n\n#: ../vnc.html:366\nmsgid \"Approve\"\nmsgstr \"Схвалити\"\n\n#: ../vnc.html:367\nmsgid \"Reject\"\nmsgstr \"Відхилити\"\n\n#: ../vnc.html:375\nmsgid \"Credentials\"\nmsgstr \"Особові дані\"\n\n#: ../vnc.html:379\nmsgid \"Username:\"\nmsgstr \"Користувацьке ім'я:\"\n\n#: ../vnc.html:383\nmsgid \"Password:\"\nmsgstr \"Пароль:\"\n\n#: ../vnc.html:387\nmsgid \"Send credentials\"\nmsgstr \"Надіслати особові дані\"\n\n#: ../vnc.html:396\nmsgid \"Cancel\"\nmsgstr \"Скасувати\"\n"
  },
  {
    "path": "po/xgettext-html",
    "content": "#!/usr/bin/env node\n/*\n * xgettext-html: HTML gettext parser\n * Copyright (C) 2018 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n */\n\nimport { program } from 'commander';\nimport jsdom from 'jsdom';\nimport fs from 'fs';\n\nprogram\n    .argument('<INPUT...>')\n    .requiredOption('-o, --output <FILE>', 'write output to specified file')\n    .parse(process.argv);\n\nconst strings = {};\n\nfunction addString(str, location) {\n    // We assume surrounding whitespace, and whitespace around line\n    // breaks, is just for source formatting\n    str = str.split(\"\\n\").map(s => s.trim()).join(\" \").trim();\n\n    if (str.length == 0) {\n        return;\n    }\n\n    if (strings[str] === undefined) {\n        strings[str] = {};\n    }\n    strings[str][location] = null;\n}\n\n// See https://html.spec.whatwg.org/multipage/dom.html#attr-translate\nfunction process(elem, locator, enabled) {\n    function isAnyOf(searchElement, items) {\n        return items.indexOf(searchElement) !== -1;\n    }\n\n    if (elem.hasAttribute(\"translate\")) {\n        if (isAnyOf(elem.getAttribute(\"translate\"), [\"\", \"yes\"])) {\n            enabled = true;\n        } else if (isAnyOf(elem.getAttribute(\"translate\"), [\"no\"])) {\n            enabled = false;\n        }\n    }\n\n    if (enabled) {\n        if (elem.hasAttribute(\"abbr\") &&\n            elem.tagName === \"TH\") {\n            addString(elem.getAttribute(\"abbr\"), locator(elem));\n        }\n        if (elem.hasAttribute(\"alt\") &&\n            isAnyOf(elem.tagName, [\"AREA\", \"IMG\", \"INPUT\"])) {\n            addString(elem.getAttribute(\"alt\"), locator(elem));\n        }\n        if (elem.hasAttribute(\"download\") &&\n            isAnyOf(elem.tagName, [\"A\", \"AREA\"])) {\n            addString(elem.getAttribute(\"download\"), locator(elem));\n        }\n        if (elem.hasAttribute(\"label\") &&\n            isAnyOf(elem.tagName, [\"MENUITEM\", \"MENU\", \"OPTGROUP\",\n                                   \"OPTION\", \"TRACK\"])) {\n            addString(elem.getAttribute(\"label\"), locator(elem));\n        }\n        if (elem.hasAttribute(\"placeholder\") &&\n            isAnyOf(elem.tagName in [\"INPUT\", \"TEXTAREA\"])) {\n            addString(elem.getAttribute(\"placeholder\"), locator(elem));\n        }\n        if (elem.hasAttribute(\"title\")) {\n            addString(elem.getAttribute(\"title\"), locator(elem));\n        }\n        if (elem.hasAttribute(\"value\") &&\n            elem.tagName === \"INPUT\" &&\n            isAnyOf(elem.getAttribute(\"type\"), [\"reset\", \"button\", \"submit\"])) {\n            addString(elem.getAttribute(\"value\"), locator(elem));\n        }\n    }\n\n    for (let i = 0; i < elem.childNodes.length; i++) {\n        let node = elem.childNodes[i];\n        if (node.nodeType === node.ELEMENT_NODE) {\n            process(node, locator, enabled);\n        } else if (node.nodeType === node.TEXT_NODE && enabled) {\n            addString(node.data, locator(node));\n        }\n    }\n}\n\nfor (let i = 0; i < program.args.length; i++) {\n    const fn = program.args[i];\n    const file = fs.readFileSync(fn, \"utf8\");\n    const dom = new jsdom.JSDOM(file, { includeNodeLocations: true });\n    const body = dom.window.document.body;\n\n    let locator = (elem) => {\n        const offset = dom.nodeLocation(elem).startOffset;\n        const line = file.slice(0, offset).split(\"\\n\").length;\n        return fn + \":\" + line;\n    };\n\n    process(body, locator, true);\n}\n\nlet output = \"\";\n\nfor (let str in strings) {\n    output += \"#:\";\n    for (let location in strings[str]) {\n        output += \" \" + location;\n    }\n    output += \"\\n\";\n\n    output += \"msgid \" + JSON.stringify(str) + \"\\n\";\n    output += \"msgstr \\\"\\\"\\n\";\n    output += \"\\n\";\n}\n\nfs.writeFileSync(program.opts().output, output);\n"
  },
  {
    "path": "po/zh_CN.po",
    "content": "# Simplified Chinese translations for noVNC package.\n# Copyright (C) 2020 The noVNC authors\n# This file is distributed under the same license as the noVNC package.\n# Peter Dave Hello <hsu@peterdavehello.org>, 2018.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 1.1.0\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2024-06-03 14:10+0200\\n\"\n\"PO-Revision-Date: 2024-11-23 15:29+0800\\n\"\n\"Last-Translator: wxtewx <wxtewx@qq.com>\\n\"\n\"Language-Team: \\n\"\n\"Language: zh_CN\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: Poedit 3.5\\n\"\n\n#: ../app/ui.js:69\nmsgid \"\"\n\"Running without HTTPS is not recommended, crashes or other issues are likely.\"\nmsgstr \"不建议在没有 HTTPS 的情况下运行，可能会出现崩溃或出现其他问题。\"\n\n#: ../app/ui.js:410\nmsgid \"Connecting...\"\nmsgstr \"连接中...\"\n\n#: ../app/ui.js:417\nmsgid \"Disconnecting...\"\nmsgstr \"正在断开连接...\"\n\n#: ../app/ui.js:423\nmsgid \"Reconnecting...\"\nmsgstr \"重新连接中...\"\n\n#: ../app/ui.js:428\nmsgid \"Internal error\"\nmsgstr \"内部错误\"\n\n#: ../app/ui.js:1026\nmsgid \"Must set host\"\nmsgstr \"必须设置主机\"\n\n#: ../app/ui.js:1052\nmsgid \"Failed to connect to server: \"\nmsgstr \"无法连接到服务器：\"\n\n#: ../app/ui.js:1118\nmsgid \"Connected (encrypted) to \"\nmsgstr \"已连接（已加密）到\"\n\n#: ../app/ui.js:1120\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"已连接（未加密）到\"\n\n#: ../app/ui.js:1143\nmsgid \"Something went wrong, connection is closed\"\nmsgstr \"出了点问题，连接已关闭\"\n\n#: ../app/ui.js:1146\nmsgid \"Failed to connect to server\"\nmsgstr \"无法连接到服务器\"\n\n#: ../app/ui.js:1158\nmsgid \"Disconnected\"\nmsgstr \"已断开连接\"\n\n#: ../app/ui.js:1173\nmsgid \"New connection has been rejected with reason: \"\nmsgstr \"新连接被拒绝，原因如下：\"\n\n#: ../app/ui.js:1176\nmsgid \"New connection has been rejected\"\nmsgstr \"新连接已被拒绝\"\n\n#: ../app/ui.js:1242\nmsgid \"Credentials are required\"\nmsgstr \"需要凭证\"\n\n#: ../vnc.html:55\nmsgid \"noVNC encountered an error:\"\nmsgstr \"noVNC 遇到一个错误：\"\n\n#: ../vnc.html:65\nmsgid \"Hide/Show the control bar\"\nmsgstr \"显示/隐藏控制栏\"\n\n#: ../vnc.html:74\nmsgid \"Drag\"\nmsgstr \"拖动\"\n\n#: ../vnc.html:74\nmsgid \"Move/Drag viewport\"\nmsgstr \"移动/拖动窗口\"\n\n#: ../vnc.html:80\nmsgid \"Keyboard\"\nmsgstr \"键盘\"\n\n#: ../vnc.html:80\nmsgid \"Show keyboard\"\nmsgstr \"显示键盘\"\n\n#: ../vnc.html:85\nmsgid \"Extra keys\"\nmsgstr \"额外按键\"\n\n#: ../vnc.html:85\nmsgid \"Show extra keys\"\nmsgstr \"显示额外按键\"\n\n#: ../vnc.html:90\nmsgid \"Ctrl\"\nmsgstr \"Ctrl\"\n\n#: ../vnc.html:90\nmsgid \"Toggle Ctrl\"\nmsgstr \"切换 Ctrl\"\n\n#: ../vnc.html:93\nmsgid \"Alt\"\nmsgstr \"Alt\"\n\n#: ../vnc.html:93\nmsgid \"Toggle Alt\"\nmsgstr \"切换 Alt\"\n\n#: ../vnc.html:96\nmsgid \"Toggle Windows\"\nmsgstr \"切换窗口\"\n\n#: ../vnc.html:96\nmsgid \"Windows\"\nmsgstr \"窗口\"\n\n#: ../vnc.html:99\nmsgid \"Send Tab\"\nmsgstr \"发送 Tab 键\"\n\n#: ../vnc.html:99\nmsgid \"Tab\"\nmsgstr \"Tab\"\n\n#: ../vnc.html:102\nmsgid \"Esc\"\nmsgstr \"Esc\"\n\n#: ../vnc.html:102\nmsgid \"Send Escape\"\nmsgstr \"发送 Escape 键\"\n\n#: ../vnc.html:105\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"Ctrl+Alt+Del\"\n\n#: ../vnc.html:105\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"发送 Ctrl+Alt+Del 键\"\n\n#: ../vnc.html:112\nmsgid \"Shutdown/Reboot\"\nmsgstr \"关机/重启\"\n\n#: ../vnc.html:112\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"关机/重启...\"\n\n#: ../vnc.html:118\nmsgid \"Power\"\nmsgstr \"电源\"\n\n#: ../vnc.html:120\nmsgid \"Shutdown\"\nmsgstr \"关机\"\n\n#: ../vnc.html:121\nmsgid \"Reboot\"\nmsgstr \"重启\"\n\n#: ../vnc.html:122\nmsgid \"Reset\"\nmsgstr \"重置\"\n\n#: ../vnc.html:127 ../vnc.html:133\nmsgid \"Clipboard\"\nmsgstr \"剪贴板\"\n\n#: ../vnc.html:135\nmsgid \"Edit clipboard content in the textarea below.\"\nmsgstr \"在下面的文本区域中编辑剪贴板内容。\"\n\n#: ../vnc.html:143\nmsgid \"Full screen\"\nmsgstr \"全屏\"\n\n#: ../vnc.html:148 ../vnc.html:154\nmsgid \"Settings\"\nmsgstr \"设置\"\n\n#: ../vnc.html:158\nmsgid \"Shared mode\"\nmsgstr \"分享模式\"\n\n#: ../vnc.html:161\nmsgid \"View only\"\nmsgstr \"仅查看\"\n\n#: ../vnc.html:165\nmsgid \"Clip to window\"\nmsgstr \"限制/裁切窗口大小\"\n\n#: ../vnc.html:168\nmsgid \"Scaling mode:\"\nmsgstr \"缩放模式：\"\n\n#: ../vnc.html:170\nmsgid \"None\"\nmsgstr \"无\"\n\n#: ../vnc.html:171\nmsgid \"Local scaling\"\nmsgstr \"本地缩放\"\n\n#: ../vnc.html:172\nmsgid \"Remote resizing\"\nmsgstr \"远程调整大小\"\n\n#: ../vnc.html:177\nmsgid \"Advanced\"\nmsgstr \"高级\"\n\n#: ../vnc.html:180\nmsgid \"Quality:\"\nmsgstr \"品质：\"\n\n#: ../vnc.html:184\nmsgid \"Compression level:\"\nmsgstr \"压缩级别：\"\n\n#: ../vnc.html:189\nmsgid \"Repeater ID:\"\nmsgstr \"中继站 ID\"\n\n#: ../vnc.html:193\nmsgid \"WebSocket\"\nmsgstr \"WebSocket\"\n\n#: ../vnc.html:196\nmsgid \"Encrypt\"\nmsgstr \"加密\"\n\n#: ../vnc.html:199\nmsgid \"Host:\"\nmsgstr \"主机：\"\n\n#: ../vnc.html:203\nmsgid \"Port:\"\nmsgstr \"端口：\"\n\n#: ../vnc.html:207\nmsgid \"Path:\"\nmsgstr \"路径：\"\n\n#: ../vnc.html:214\nmsgid \"Automatic reconnect\"\nmsgstr \"自动重新连接\"\n\n#: ../vnc.html:217\nmsgid \"Reconnect delay (ms):\"\nmsgstr \"重新连接间隔 (ms)：\"\n\n#: ../vnc.html:222\nmsgid \"Show dot when no cursor\"\nmsgstr \"无光标时显示点\"\n\n#: ../vnc.html:227\nmsgid \"Logging:\"\nmsgstr \"日志级别：\"\n\n#: ../vnc.html:236\nmsgid \"Version:\"\nmsgstr \"版本：\"\n\n#: ../vnc.html:244\nmsgid \"Disconnect\"\nmsgstr \"断开连接\"\n\n#: ../vnc.html:267\nmsgid \"Connect\"\nmsgstr \"连接\"\n\n#: ../vnc.html:276\nmsgid \"Server identity\"\nmsgstr \"服务器身份\"\n\n#: ../vnc.html:279\nmsgid \"The server has provided the following identifying information:\"\nmsgstr \"服务器提供了以下识别信息：\"\n\n#: ../vnc.html:283\nmsgid \"Fingerprint:\"\nmsgstr \"指纹：\"\n\n#: ../vnc.html:286\nmsgid \"\"\n\"Please verify that the information is correct and press \\\"Approve\\\". \"\n\"Otherwise press \\\"Reject\\\".\"\nmsgstr \"请核实信息是否正确，并按 “同意”，否则按 “拒绝”。\"\n\n#: ../vnc.html:291\nmsgid \"Approve\"\nmsgstr \"同意\"\n\n#: ../vnc.html:292\nmsgid \"Reject\"\nmsgstr \"拒绝\"\n\n#: ../vnc.html:300\nmsgid \"Credentials\"\nmsgstr \"凭证\"\n\n#: ../vnc.html:304\nmsgid \"Username:\"\nmsgstr \"用户名:\"\n\n#: ../vnc.html:308\nmsgid \"Password:\"\nmsgstr \"密码：\"\n\n#: ../vnc.html:312\nmsgid \"Send credentials\"\nmsgstr \"发送凭证\"\n\n#: ../vnc.html:321\nmsgid \"Cancel\"\nmsgstr \"取消\"\n\n#~ msgid \"Password is required\"\n#~ msgstr \"请提供密码\"\n\n#~ msgid \"Disconnect timeout\"\n#~ msgstr \"超时断开\"\n\n#~ msgid \"viewport drag\"\n#~ msgstr \"窗口拖动\"\n\n#~ msgid \"Active Mouse Button\"\n#~ msgstr \"启动鼠标按键\"\n\n#~ msgid \"No mousebutton\"\n#~ msgstr \"禁用鼠标按键\"\n\n#~ msgid \"Left mousebutton\"\n#~ msgstr \"鼠标左键\"\n\n#~ msgid \"Middle mousebutton\"\n#~ msgstr \"鼠标中键\"\n\n#~ msgid \"Right mousebutton\"\n#~ msgstr \"鼠标右键\"\n\n#~ msgid \"Clear\"\n#~ msgstr \"清除\"\n\n#~ msgid \"Local Downscaling\"\n#~ msgstr \"降低本地尺寸\"\n\n#~ msgid \"Local Cursor\"\n#~ msgstr \"本地光标\"\n\n#~ msgid \"Canvas not supported.\"\n#~ msgstr \"不支持 Canvas。\"\n"
  },
  {
    "path": "po/zh_TW.po",
    "content": "# Traditional Chinese translations for noVNC package.\n# Copyright (C) 2018 The noVNC authors\n# This file is distributed under the same license as the noVNC package.\n# Peter Dave Hello <hsu@peterdavehello.org>, 2018.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: noVNC 1.0.0-testing.2\\n\"\n\"Report-Msgid-Bugs-To: novnc@googlegroups.com\\n\"\n\"POT-Creation-Date: 2018-01-10 00:53+0800\\n\"\n\"PO-Revision-Date: 2018-01-10 01:33+0800\\n\"\n\"Last-Translator: Peter Dave Hello <hsu@peterdavehello.org>\\n\"\n\"Language-Team: Peter Dave Hello <hsu@peterdavehello.org>\\n\"\n\"Language: zh\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\n#: ../app/ui.js:395\nmsgid \"Connecting...\"\nmsgstr \"連線中...\"\n\n#: ../app/ui.js:402\nmsgid \"Disconnecting...\"\nmsgstr \"正在中斷連線...\"\n\n#: ../app/ui.js:408\nmsgid \"Reconnecting...\"\nmsgstr \"重新連線中...\"\n\n#: ../app/ui.js:413\nmsgid \"Internal error\"\nmsgstr \"內部錯誤\"\n\n#: ../app/ui.js:1015\nmsgid \"Must set host\"\nmsgstr \"請提供主機資訊\"\n\n#: ../app/ui.js:1097\nmsgid \"Connected (encrypted) to \"\nmsgstr \"已加密連線到\"\n\n#: ../app/ui.js:1099\nmsgid \"Connected (unencrypted) to \"\nmsgstr \"未加密連線到\"\n\n#: ../app/ui.js:1120\nmsgid \"Something went wrong, connection is closed\"\nmsgstr \"發生錯誤，連線已關閉\"\n\n#: ../app/ui.js:1123\nmsgid \"Failed to connect to server\"\nmsgstr \"無法連線到伺服器\"\n\n#: ../app/ui.js:1133\nmsgid \"Disconnected\"\nmsgstr \"連線已中斷\"\n\n#: ../app/ui.js:1146\nmsgid \"New connection has been rejected with reason: \"\nmsgstr \"連線被拒絕，原因：\"\n\n#: ../app/ui.js:1149\nmsgid \"New connection has been rejected\"\nmsgstr \"連線被拒絕\"\n\n#: ../app/ui.js:1170\nmsgid \"Password is required\"\nmsgstr \"請提供密碼\"\n\n#: ../vnc.html:89\nmsgid \"noVNC encountered an error:\"\nmsgstr \"noVNC 遇到一個錯誤：\"\n\n#: ../vnc.html:99\nmsgid \"Hide/Show the control bar\"\nmsgstr \"顯示/隱藏控制列\"\n\n#: ../vnc.html:106\nmsgid \"Move/Drag viewport\"\nmsgstr \"拖放顯示範圍\"\n\n#: ../vnc.html:106\nmsgid \"viewport drag\"\nmsgstr \"顯示範圍拖放\"\n\n#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121\nmsgid \"Active Mouse Button\"\nmsgstr \"啟用滑鼠按鍵\"\n\n#: ../vnc.html:112\nmsgid \"No mousebutton\"\nmsgstr \"無滑鼠按鍵\"\n\n#: ../vnc.html:115\nmsgid \"Left mousebutton\"\nmsgstr \"滑鼠左鍵\"\n\n#: ../vnc.html:118\nmsgid \"Middle mousebutton\"\nmsgstr \"滑鼠中鍵\"\n\n#: ../vnc.html:121\nmsgid \"Right mousebutton\"\nmsgstr \"滑鼠右鍵\"\n\n#: ../vnc.html:124\nmsgid \"Keyboard\"\nmsgstr \"鍵盤\"\n\n#: ../vnc.html:124\nmsgid \"Show keyboard\"\nmsgstr \"顯示鍵盤\"\n\n#: ../vnc.html:131\nmsgid \"Extra keys\"\nmsgstr \"額外按鍵\"\n\n#: ../vnc.html:131\nmsgid \"Show extra keys\"\nmsgstr \"顯示額外按鍵\"\n\n#: ../vnc.html:136\nmsgid \"Ctrl\"\nmsgstr \"Ctrl\"\n\n#: ../vnc.html:136\nmsgid \"Toggle Ctrl\"\nmsgstr \"切換 Ctrl\"\n\n#: ../vnc.html:139\nmsgid \"Alt\"\nmsgstr \"Alt\"\n\n#: ../vnc.html:139\nmsgid \"Toggle Alt\"\nmsgstr \"切換 Alt\"\n\n#: ../vnc.html:142\nmsgid \"Send Tab\"\nmsgstr \"送出 Tab 鍵\"\n\n#: ../vnc.html:142\nmsgid \"Tab\"\nmsgstr \"Tab\"\n\n#: ../vnc.html:145\nmsgid \"Esc\"\nmsgstr \"Esc\"\n\n#: ../vnc.html:145\nmsgid \"Send Escape\"\nmsgstr \"送出 Escape 鍵\"\n\n#: ../vnc.html:148\nmsgid \"Ctrl+Alt+Del\"\nmsgstr \"Ctrl-Alt-Del\"\n\n#: ../vnc.html:148\nmsgid \"Send Ctrl-Alt-Del\"\nmsgstr \"送出 Ctrl-Alt-Del 快捷鍵\"\n\n#: ../vnc.html:156\nmsgid \"Shutdown/Reboot\"\nmsgstr \"關機/重新啟動\"\n\n#: ../vnc.html:156\nmsgid \"Shutdown/Reboot...\"\nmsgstr \"關機/重新啟動...\"\n\n#: ../vnc.html:162\nmsgid \"Power\"\nmsgstr \"電源\"\n\n#: ../vnc.html:164\nmsgid \"Shutdown\"\nmsgstr \"關機\"\n\n#: ../vnc.html:165\nmsgid \"Reboot\"\nmsgstr \"重新啟動\"\n\n#: ../vnc.html:166\nmsgid \"Reset\"\nmsgstr \"重設\"\n\n#: ../vnc.html:171 ../vnc.html:177\nmsgid \"Clipboard\"\nmsgstr \"剪貼簿\"\n\n#: ../vnc.html:181\nmsgid \"Clear\"\nmsgstr \"清除\"\n\n#: ../vnc.html:187\nmsgid \"Fullscreen\"\nmsgstr \"全螢幕\"\n\n#: ../vnc.html:192 ../vnc.html:199\nmsgid \"Settings\"\nmsgstr \"設定\"\n\n#: ../vnc.html:202\nmsgid \"Shared mode\"\nmsgstr \"分享模式\"\n\n#: ../vnc.html:205\nmsgid \"View only\"\nmsgstr \"僅檢視\"\n\n#: ../vnc.html:209\nmsgid \"Clip to window\"\nmsgstr \"限制/裁切視窗大小\"\n\n#: ../vnc.html:212\nmsgid \"Scaling mode:\"\nmsgstr \"縮放模式：\"\n\n#: ../vnc.html:214\nmsgid \"None\"\nmsgstr \"無\"\n\n#: ../vnc.html:215\nmsgid \"Local scaling\"\nmsgstr \"本機縮放\"\n\n#: ../vnc.html:216\nmsgid \"Remote resizing\"\nmsgstr \"遠端調整大小\"\n\n#: ../vnc.html:221\nmsgid \"Advanced\"\nmsgstr \"進階\"\n\n#: ../vnc.html:224\nmsgid \"Repeater ID:\"\nmsgstr \"中繼站 ID\"\n\n#: ../vnc.html:228\nmsgid \"WebSocket\"\nmsgstr \"WebSocket\"\n\n#: ../vnc.html:231\nmsgid \"Encrypt\"\nmsgstr \"加密\"\n\n#: ../vnc.html:234\nmsgid \"Host:\"\nmsgstr \"主機：\"\n\n#: ../vnc.html:238\nmsgid \"Port:\"\nmsgstr \"連接埠：\"\n\n#: ../vnc.html:242\nmsgid \"Path:\"\nmsgstr \"路徑：\"\n\n#: ../vnc.html:249\nmsgid \"Automatic reconnect\"\nmsgstr \"自動重新連線\"\n\n#: ../vnc.html:252\nmsgid \"Reconnect delay (ms):\"\nmsgstr \"重新連線間隔 (ms)：\"\n\n#: ../vnc.html:258\nmsgid \"Logging:\"\nmsgstr \"日誌級別：\"\n\n#: ../vnc.html:270\nmsgid \"Disconnect\"\nmsgstr \"中斷連線\"\n\n#: ../vnc.html:289\nmsgid \"Connect\"\nmsgstr \"連線\"\n\n#: ../vnc.html:299\nmsgid \"Password:\"\nmsgstr \"密碼：\"\n\n#: ../vnc.html:313\nmsgid \"Cancel\"\nmsgstr \"取消\"\n"
  },
  {
    "path": "snap/hooks/configure",
    "content": "#!/bin/sh -e\n\nsnapctl restart novnc.novncsvc\n"
  },
  {
    "path": "snap/local/svc_wrapper.sh",
    "content": "#!/bin/bash\n\n# `snapctl get services` returns a JSON array, example:\n#{\n#\"n6801\": {\n#   \"listen\": 6801,\n#   \"vnc\": \"localhost:5901\"\n#},\n#\"n6802\": {\n#    \"listen\": 6802,\n#   \"vnc\": \"localhost:5902\"\n#}\n#}\nsnapctl get services | jq -c '.[]' | while read service; do # for each service the user specified..\n    # get the important data for the service (listen port, VNC host:port)\n    listen_port=\"$(echo $service | jq --raw-output '.listen')\"\n    vnc_host_port=\"$(echo $service | jq --raw-output '.vnc')\" # --raw-output removes any quotation marks from the output\n    \n    # check whether those values are valid\n    expr \"$listen_port\" : '^[0-9]\\+$' > /dev/null\n    listen_port_valid=$?\n    if [ ! $listen_port_valid ] || [ -z \"$vnc_host_port\" ]; then\n        # invalid values mean the service is disabled, do nothing except for printing a message (logged in /var/log/system or systemd journal)\n        echo \"novnc: not starting service ${service} with listen_port ${listen_port} and vnc_host_port ${vnc_host_port}\"\n    else\n        # start (and fork with '&') the service using the specified listen port and VNC host:port\n        $SNAP/novnc_proxy --listen $listen_port --vnc $vnc_host_port &\n    fi\ndone\n"
  },
  {
    "path": "snap/snapcraft.yaml",
    "content": "name: novnc\nbase: core22 # the base snap is the execution environment for this snap\nversion: git\nsummary: Open Source VNC client using HTML5 (WebSockets, Canvas)\ndescription: |\n  Open Source VNC client using HTML5 (WebSockets, Canvas).\n  noVNC is both a VNC client JavaScript library as well as an\n  application built on top of that library. noVNC runs well in any\n  modern browser including mobile browsers (iOS and Android).\n\ngrade: stable\nconfinement: strict\n\nparts:\n    novnc:\n        source: .\n        plugin: dump\n        organize:\n            utils/novnc_proxy: /\n        stage:\n            - vnc.html\n            - app\n            - core/**/*.js\n            - vendor/**/*.js\n            - novnc_proxy\n\n    novnc-deps:\n        plugin: nil\n        stage-packages:\n            - bash\n\n    svc-script:\n        source: snap/local\n        plugin: dump\n        stage:\n            - svc_wrapper.sh\n\n    svc-script-deps:\n        plugin: nil\n        stage-packages:\n            - bash\n            - jq\n\n    websockify:\n        source: https://github.com/novnc/websockify/archive/v0.13.0.tar.gz\n        plugin: python\n        stage-packages:\n            - python3-numpy\n\nhooks:\n    configure:\n        plugs: [network, network-bind]\n\napps:\n    novnc:\n        command: ./novnc_proxy\n        plugs: [network, network-bind]\n    novncsvc:\n        command: ./svc_wrapper.sh\n        daemon: forking\n        plugs: [network, network-bind]\n"
  },
  {
    "path": "tests/assertions.js",
    "content": "import * as chai from '../node_modules/chai/index.js';\nimport sinon from '../node_modules/sinon/pkg/sinon-esm.js';\nimport sinonChai from '../node_modules/sinon-chai/lib/sinon-chai.js';\n\nwindow.expect = chai.expect;\n\nwindow.sinon = sinon;\nchai.use(sinonChai);\n\n// noVNC specific assertions\nchai.use(function (_chai, utils) {\n    function _equal(a, b) {\n        return a === b;\n    }\n    _chai.Assertion.addMethod('displayed', function (targetData, cmp=_equal) {\n        const obj = this._obj;\n        const ctx = obj._target.getContext('2d');\n        const data = ctx.getImageData(0, 0, obj._target.width, obj._target.height).data;\n        const len = data.length;\n        new chai.Assertion(len).to.be.equal(targetData.length, \"unexpected display size\");\n        let same = true;\n        for (let i = 0; i < len; i++) {\n            if (!cmp(data[i], targetData[i])) {\n                same = false;\n                break;\n            }\n        }\n        if (!same) {\n            // eslint-disable-next-line no-console\n            console.log(\"expected data: %o, actual data: %o\", targetData, data);\n        }\n        this.assert(same,\n                    \"expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}\",\n                    \"expected #{this} not to have displayed the image #{act}\",\n                    targetData,\n                    data);\n    });\n\n    _chai.Assertion.addMethod('sent', function (targetData) {\n        const obj = this._obj;\n        const data = obj._websocket._getSentData();\n        let same = true;\n        if (data.length != targetData.length) {\n            same = false;\n        } else {\n            for (let i = 0; i < data.length; i++) {\n                if (data[i] != targetData[i]) {\n                    same = false;\n                    break;\n                }\n            }\n        }\n        if (!same) {\n            // eslint-disable-next-line no-console\n            console.log(\"expected data: %o, actual data: %o\", targetData, data);\n        }\n        this.assert(same,\n                    \"expected #{this} to have sent the data #{exp}, but it actually sent #{act}\",\n                    \"expected #{this} not to have sent the data #{act}\",\n                    Array.prototype.slice.call(targetData),\n                    Array.prototype.slice.call(data));\n    });\n\n    _chai.Assertion.addProperty('array', function () {\n        utils.flag(this, 'array', true);\n    });\n\n    _chai.Assertion.overwriteMethod('equal', function (_super) {\n        return function assertArrayEqual(target) {\n            if (utils.flag(this, 'array')) {\n                const obj = this._obj;\n\n                let same = true;\n\n                if (utils.flag(this, 'deep')) {\n                    for (let i = 0; i < obj.length; i++) {\n                        if (!utils.eql(obj[i], target[i])) {\n                            same = false;\n                            break;\n                        }\n                    }\n\n                    this.assert(same,\n                                \"expected #{this} to have elements deeply equal to #{exp}\",\n                                \"expected #{this} not to have elements deeply equal to #{exp}\",\n                                Array.prototype.slice.call(target));\n                } else {\n                    for (let i = 0; i < obj.length; i++) {\n                        if (obj[i] != target[i]) {\n                            same = false;\n                            break;\n                        }\n                    }\n\n                    this.assert(same,\n                                \"expected #{this} to have elements equal to #{exp}\",\n                                \"expected #{this} not to have elements equal to #{exp}\",\n                                Array.prototype.slice.call(target));\n                }\n            } else {\n                _super.apply(this, arguments);\n            }\n        };\n    });\n});\n"
  },
  {
    "path": "tests/fake.websocket.js",
    "content": "import Base64 from '../core/base64.js';\n\nexport default class FakeWebSocket {\n    constructor(uri, protocols) {\n        this.url = uri;\n        this.binaryType = \"arraybuffer\";\n        this.extensions = \"\";\n\n        this.onerror = null;\n        this.onmessage = null;\n        this.onopen = null;\n\n        if (!protocols || typeof protocols === 'string') {\n            this.protocol = protocols;\n        } else {\n            this.protocol = protocols[0];\n        }\n\n        this._sendQueue = new Uint8Array(20000);\n\n        this.readyState = FakeWebSocket.CONNECTING;\n        this.bufferedAmount = 0;\n\n        this._isFake = true;\n    }\n\n    close(code, reason) {\n        this.readyState = FakeWebSocket.CLOSED;\n        if (this.onclose) {\n            this.onclose(new CloseEvent(\"close\", { 'code': code, 'reason': reason, 'wasClean': true }));\n        }\n    }\n\n    send(data) {\n        if (this.protocol == 'base64') {\n            data = Base64.decode(data);\n        } else {\n            data = new Uint8Array(data);\n        }\n        if (this.bufferedAmount + data.length > this._sendQueue.length) {\n            let newlen = this._sendQueue.length;\n            while (this.bufferedAmount + data.length > newlen) {\n                newlen *= 2;\n            }\n            let newbuf = new Uint8Array(newlen);\n            newbuf.set(this._sendQueue);\n            this._sendQueue = newbuf;\n        }\n        this._sendQueue.set(data, this.bufferedAmount);\n        this.bufferedAmount += data.length;\n    }\n\n    _getSentData() {\n        const res = this._sendQueue.slice(0, this.bufferedAmount);\n        this.bufferedAmount = 0;\n        return res;\n    }\n\n    _open() {\n        this.readyState = FakeWebSocket.OPEN;\n        if (this.onopen) {\n            this.onopen(new Event('open'));\n        }\n    }\n\n    _receiveData(data) {\n        if (data.length < 4096) {\n            // Break apart the data to expose bugs where we assume data is\n            // neatly packaged\n            for (let i = 0;i < data.length;i++) {\n                let buf = data.slice(i, i+1);\n                this.onmessage(new MessageEvent(\"message\", { 'data': buf.buffer }));\n            }\n        } else {\n            this.onmessage(new MessageEvent(\"message\", { 'data': data.buffer }));\n        }\n    }\n}\n\nFakeWebSocket.OPEN = WebSocket.OPEN;\nFakeWebSocket.CONNECTING = WebSocket.CONNECTING;\nFakeWebSocket.CLOSING = WebSocket.CLOSING;\nFakeWebSocket.CLOSED = WebSocket.CLOSED;\n\nFakeWebSocket._isFake = true;\n\nFakeWebSocket.replace = () => {\n    if (!WebSocket._isFake) {\n        const realVersion = WebSocket;\n        // eslint-disable-next-line no-global-assign\n        WebSocket = FakeWebSocket;\n        FakeWebSocket._realVersion = realVersion;\n    }\n};\n\nFakeWebSocket.restore = () => {\n    if (WebSocket._isFake) {\n        // eslint-disable-next-line no-global-assign\n        WebSocket = WebSocket._realVersion;\n    }\n};\n"
  },
  {
    "path": "tests/playback-ui.js",
    "content": "/* global VNC_frame_data, VNC_frame_encoding */\n\nimport * as WebUtil from '../app/webutil.js';\nimport RecordingPlayer from './playback.js';\nimport Base64 from '../core/base64.js';\n\nlet frames = null;\n\nfunction message(str) {\n    const cell = document.getElementById('messages');\n    cell.textContent += str + \"\\n\";\n    cell.scrollTop = cell.scrollHeight;\n}\n\nfunction loadFile() {\n    const fname = WebUtil.getQueryVar('data', null);\n\n    if (!fname) {\n        return Promise.reject(\"Must specify data=FOO in query string.\");\n    }\n\n    message(\"Loading \" + fname + \"...\");\n\n    return new Promise((resolve, reject) => {\n        const script = document.createElement(\"script\");\n        script.onload = resolve;\n        script.onerror = () => { reject(\"Failed to load \" + fname); };\n        document.body.appendChild(script);\n        script.src = \"../recordings/\" + fname;\n    });\n}\n\nfunction enableUI() {\n    const iterations = WebUtil.getQueryVar('iterations', 3);\n    document.getElementById('iterations').value = iterations;\n\n    const mode = WebUtil.getQueryVar('mode', 3);\n    if (mode === 'realtime') {\n        document.getElementById('mode2').checked = true;\n    } else {\n        document.getElementById('mode1').checked = true;\n    }\n\n    /* eslint-disable-next-line camelcase */\n    message(\"Loaded \" + VNC_frame_data.length + \" frames\");\n\n    const startButton = document.getElementById('startButton');\n    startButton.disabled = false;\n    startButton.addEventListener('click', start);\n\n    message(\"Converting...\");\n\n    /* eslint-disable-next-line camelcase */\n    frames = VNC_frame_data;\n\n    let encoding;\n\n    /* eslint-disable camelcase */\n    if (window.VNC_frame_encoding) {\n        // Only present in older recordings\n        encoding = VNC_frame_encoding;\n    /* eslint-enable camelcase */\n    } else {\n        let frame = frames[0];\n        let start = frame.indexOf('{', 1) + 1;\n        if (frame.slice(start, start+4) === 'UkZC') {\n            encoding = 'base64';\n        } else {\n            encoding = 'binary';\n        }\n    }\n\n    for (let i = 0;i < frames.length;i++) {\n        let frame = frames[i];\n\n        if (frame === \"EOF\") {\n            frames.splice(i);\n            break;\n        }\n\n        let dataIdx = frame.indexOf('{', 1) + 1;\n\n        let time = parseInt(frame.slice(1, dataIdx - 1));\n\n        let u8;\n        if (encoding === 'base64') {\n            u8 = Base64.decode(frame.slice(dataIdx));\n        } else {\n            u8 = new Uint8Array(frame.length - dataIdx);\n            for (let j = 0; j < frame.length - dataIdx; j++) {\n                u8[j] = frame.charCodeAt(dataIdx + j);\n            }\n        }\n\n        frames[i] = { fromClient: frame[0] === '}',\n                      timestamp: time,\n                      data: u8 };\n    }\n\n    message(\"Ready\");\n}\n\nclass IterationPlayer {\n    constructor(iterations, frames) {\n        this._iterations = iterations;\n\n        this._iteration = undefined;\n        this._player = undefined;\n\n        this._startTime = undefined;\n\n        this._frames = frames;\n\n        this._state = 'running';\n\n        this.onfinish = () => {};\n        this.oniterationfinish = () => {};\n        this.rfbdisconnected = () => {};\n    }\n\n    start(realtime) {\n        this._iteration = 0;\n        this._startTime = (new Date()).getTime();\n\n        this._realtime = realtime;\n\n        this._nextIteration();\n    }\n\n    _nextIteration() {\n        const player = new RecordingPlayer(this._frames, this._disconnected.bind(this));\n        player.onfinish = this._iterationFinish.bind(this);\n\n        if (this._state !== 'running') { return; }\n\n        this._iteration++;\n        if (this._iteration > this._iterations) {\n            this._finish();\n            return;\n        }\n\n        player.run(this._realtime, false);\n    }\n\n    _finish() {\n        const endTime = (new Date()).getTime();\n        const totalDuration = endTime - this._startTime;\n\n        const evt = new CustomEvent('finish',\n                                    { detail:\n                                      { duration: totalDuration,\n                                        iterations: this._iterations } } );\n        this.onfinish(evt);\n    }\n\n    _iterationFinish(duration) {\n        const evt = new CustomEvent('iterationfinish',\n                                    { detail:\n                                      { duration: duration,\n                                        number: this._iteration } } );\n        this.oniterationfinish(evt);\n\n        this._nextIteration();\n    }\n\n    _disconnected(clean, frame) {\n        if (!clean) {\n            this._state = 'failed';\n        }\n\n        const evt = new CustomEvent('rfbdisconnected',\n                                    { detail:\n                                      { clean: clean,\n                                        frame: frame,\n                                        iteration: this._iteration } } );\n        this.onrfbdisconnected(evt);\n    }\n}\n\nfunction start() {\n    document.getElementById('startButton').value = \"Running\";\n    document.getElementById('startButton').disabled = true;\n\n    const iterations = document.getElementById('iterations').value;\n\n    let realtime;\n\n    if (document.getElementById('mode1').checked) {\n        message(`Starting performance playback (fullspeed) [${iterations} iteration(s)]`);\n        realtime = false;\n    } else {\n        message(`Starting realtime playback [${iterations} iteration(s)]`);\n        realtime = true;\n    }\n\n    const player = new IterationPlayer(iterations, frames);\n    player.oniterationfinish = (evt) => {\n        message(`Iteration ${evt.detail.number} took ${evt.detail.duration}ms`);\n    };\n    player.onrfbdisconnected = (evt) => {\n        if (!evt.detail.clean) {\n            message(`noVNC sent disconnected during iteration ${evt.detail.iteration} frame ${evt.detail.frame}`);\n\n            document.getElementById('startButton').disabled = false;\n            document.getElementById('startButton').value = \"Start\";\n        }\n    };\n    player.onfinish = (evt) => {\n        const iterTime = parseInt(evt.detail.duration / evt.detail.iterations, 10);\n        message(`${evt.detail.iterations} iterations took ${evt.detail.duration}ms (average ${iterTime}ms / iteration)`);\n\n        document.getElementById('startButton').disabled = false;\n        document.getElementById('startButton').value = \"Start\";\n    };\n    player.start(realtime);\n}\n\nloadFile().then(enableUI).catch(e => message(\"Error loading recording: \" + e));\n"
  },
  {
    "path": "tests/playback.js",
    "content": "/*\n * noVNC: HTML5 VNC client\n * Copyright (C) 2018 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n */\n\nimport RFB from '../core/rfb.js';\nimport * as Log from '../core/util/logging.js';\n\n// Immediate polyfill\nif (window.setImmediate === undefined) {\n    let _immediateIdCounter = 1;\n    const _immediateFuncs = {};\n\n    window.setImmediate = (func) => {\n        const index = _immediateIdCounter++;\n        _immediateFuncs[index] = func;\n        window.postMessage(\"noVNC immediate trigger:\" + index, \"*\");\n        return index;\n    };\n\n    window.clearImmediate = (id) => {\n        _immediateFuncs[id];\n    };\n\n    window.addEventListener(\"message\", (event) => {\n        if ((typeof event.data !== \"string\") ||\n            (event.data.indexOf(\"noVNC immediate trigger:\") !== 0)) {\n            return;\n        }\n\n        const index = event.data.slice(\"noVNC immediate trigger:\".length);\n\n        const callback = _immediateFuncs[index];\n        if (callback === undefined) {\n            return;\n        }\n\n        delete _immediateFuncs[index];\n\n        callback();\n    });\n}\n\nclass FakeWebSocket {\n    constructor() {\n        this.binaryType = \"arraybuffer\";\n        this.protocol = \"\";\n        this.readyState = \"open\";\n\n        this.onerror = () => {};\n        this.onmessage = () => {};\n        this.onopen = () => {};\n    }\n\n    send() {\n    }\n\n    close() {\n    }\n}\n\nexport default class RecordingPlayer {\n    constructor(frames, disconnected) {\n        this._frames = frames;\n\n        this._disconnected = disconnected;\n\n        this._rfb = undefined;\n        this._frameLength = this._frames.length;\n\n        this._frameIndex = 0;\n        this._startTime = undefined;\n        this._realtime = true;\n        this._trafficManagement = true;\n\n        this._running = false;\n\n        this.onfinish = () => {};\n    }\n\n    run(realtime, trafficManagement) {\n        // initialize a new RFB\n        this._ws = new FakeWebSocket();\n        this._rfb = new RFB(document.getElementById('VNC_screen'), this._ws);\n        this._rfb.viewOnly = true;\n        this._rfb.addEventListener(\"disconnect\",\n                                   this._handleDisconnect.bind(this));\n        this._rfb.addEventListener(\"credentialsrequired\",\n                                   this._handleCredentials.bind(this));\n\n        // reset the frame index and timer\n        this._frameIndex = 0;\n        this._startTime = (new Date()).getTime();\n\n        this._realtime = realtime;\n        this._trafficManagement = (trafficManagement === undefined) ? !realtime : trafficManagement;\n\n        this._running = true;\n        this._queueNextPacket();\n    }\n\n    _queueNextPacket() {\n        if (!this._running) { return; }\n\n        let frame = this._frames[this._frameIndex];\n\n        // skip send frames\n        while (this._frameIndex < this._frameLength && frame.fromClient) {\n            this._frameIndex++;\n            frame = this._frames[this._frameIndex];\n        }\n\n        if (this._frameIndex >= this._frameLength) {\n            Log.Debug('Finished, no more frames');\n            this._finish();\n            return;\n        }\n\n        if (this._realtime) {\n            const toffset = (new Date()).getTime() - this._startTime;\n            let delay = frame.timestamp - toffset;\n            if (delay < 1) delay = 1;\n\n            setTimeout(this._doPacket.bind(this), delay);\n        } else {\n            setImmediate(this._doPacket.bind(this));\n        }\n    }\n\n    _doPacket() {\n        // Avoid having excessive queue buildup in non-realtime mode\n        if (this._trafficManagement && this._rfb._flushing) {\n            this._rfb.flush()\n                .then(() => {\n                    this._doPacket();\n                });\n            return;\n        }\n\n        const frame = this._frames[this._frameIndex];\n\n        this._ws.onmessage({'data': frame.data});\n        this._frameIndex++;\n\n        this._queueNextPacket();\n    }\n\n    _finish() {\n        if (this._rfb._display.pending()) {\n            this._rfb._display.flush()\n                .then(() => { this._finish(); });\n        } else {\n            this._running = false;\n            this._ws.onclose({code: 1000, reason: \"\"});\n            delete this._rfb;\n            this.onfinish((new Date()).getTime() - this._startTime);\n        }\n    }\n\n    _handleDisconnect(evt) {\n        this._running = false;\n        this._disconnected(evt.detail.clean, this._frameIndex);\n    }\n\n    _handleCredentials(evt) {\n        this._rfb.sendCredentials({\"username\": \"Foo\",\n                                   \"password\": \"Bar\",\n                                   \"target\": \"Baz\"});\n    }\n}\n"
  },
  {
    "path": "tests/test.base64.js",
    "content": "import Base64 from '../core/base64.js';\n\ndescribe('Base64 tools', function () {\n    \"use strict\";\n\n    const BIN_ARR = new Array(256);\n    for (let i = 0; i < 256; i++) {\n        BIN_ARR[i] = i;\n    }\n\n    const B64_STR = \"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==\";\n\n\n    describe('encode', function () {\n        it('should encode a binary string into Base64', function () {\n            const encoded = Base64.encode(BIN_ARR);\n            expect(encoded).to.equal(B64_STR);\n        });\n    });\n\n    describe('decode', function () {\n        it('should decode a Base64 string into a normal string', function () {\n            const decoded = Base64.decode(B64_STR);\n            expect(decoded).to.deep.equal(BIN_ARR);\n        });\n\n        it('should throw an error if we have extra characters at the end of the string', function () {\n            expect(() => Base64.decode(B64_STR+'abcdef')).to.throw(Error);\n        });\n    });\n});\n"
  },
  {
    "path": "tests/test.browser.js",
    "content": "import { isMac, isWindows, isIOS, isAndroid, isChromeOS,\n         isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge,\n         isGecko, isWebKit, isBlink,\n         browserAsyncClipboardSupport } from '../core/util/browser.js';\n\ndescribe('Async clipboard', function () {\n    \"use strict\";\n\n    beforeEach(function () {\n        sinon.stub(navigator, \"clipboard\").value({\n            writeText: sinon.stub(),\n            readText: sinon.stub(),\n        });\n        sinon.stub(navigator, \"permissions\").value({\n            query: sinon.stub().resolves({ state: \"granted\" })\n        });\n    });\n\n    afterEach(function () {\n        sinon.restore();\n    });\n\n    it(\"queries permissions with correct parameters\", async function () {\n        const queryStub = navigator.permissions.query;\n        await browserAsyncClipboardSupport();\n        expect(queryStub.firstCall).to.have.been.calledWithExactly({\n            name: \"clipboard-write\",\n            allowWithoutGesture: true\n        });\n        expect(queryStub.secondCall).to.have.been.calledWithExactly({\n            name: \"clipboard-read\",\n            allowWithoutGesture: false\n        });\n    });\n\n    it(\"is available when API present and permissions granted\", async function () {\n        navigator.permissions.query.resolves({ state: \"granted\" });\n        const result = await browserAsyncClipboardSupport();\n        expect(result).to.equal('available');\n    });\n\n    it(\"is available when API present and permissions yield 'prompt'\", async function () {\n        navigator.permissions.query.resolves({ state: \"prompt\" });\n        const result = await browserAsyncClipboardSupport();\n        expect(result).to.equal('available');\n    });\n\n    it(\"is unavailable when permissions denied\", async function () {\n        navigator.permissions.query.resolves({ state: \"denied\" });\n        const result = await browserAsyncClipboardSupport();\n        expect(result).to.equal('denied');\n    });\n\n    it(\"is unavailable when permissions API fails\", async function () {\n        navigator.permissions.query.rejects(new Error(\"fail\"));\n        const result = await browserAsyncClipboardSupport();\n        expect(result).to.equal('unsupported');\n    });\n\n    it(\"is unavailable when write text API missing\", async function () {\n        navigator.clipboard.writeText = undefined;\n        const result = await browserAsyncClipboardSupport();\n        expect(result).to.equal('unsupported');\n    });\n\n    it(\"is unavailable when read text API missing\", async function () {\n        navigator.clipboard.readText = undefined;\n        const result = await browserAsyncClipboardSupport();\n        expect(result).to.equal('unsupported');\n    });\n});\n\ndescribe('OS detection', function () {\n    let origNavigator;\n    beforeEach(function () {\n        // window.navigator is a protected read-only property in many\n        // environments, so we need to redefine it whilst running these\n        // tests.\n        origNavigator = Object.getOwnPropertyDescriptor(window, \"navigator\");\n\n        Object.defineProperty(window, \"navigator\", {value: {}});\n    });\n\n    afterEach(function () {\n        Object.defineProperty(window, \"navigator\", origNavigator);\n    });\n\n    it('should handle macOS', function () {\n        const platforms = [\n            \"MacIntel\",\n            \"MacPPC\",\n        ];\n\n        navigator.userAgent = \"Mozilla/5.0 (Macintosh; Intel Mac OS X 12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15\";\n        platforms.forEach((platform) => {\n            navigator.platform = platform;\n            expect(isMac()).to.be.true;\n            expect(isWindows()).to.be.false;\n            expect(isIOS()).to.be.false;\n            expect(isAndroid()).to.be.false;\n            expect(isChromeOS()).to.be.false;\n        });\n    });\n\n    it('should handle Windows', function () {\n        const platforms = [\n            \"Win32\",\n            \"Win64\",\n        ];\n\n        navigator.userAgent = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36\";\n        platforms.forEach((platform) => {\n            navigator.platform = platform;\n            expect(isMac()).to.be.false;\n            expect(isWindows()).to.be.true;\n            expect(isIOS()).to.be.false;\n            expect(isAndroid()).to.be.false;\n            expect(isChromeOS()).to.be.false;\n        });\n    });\n\n    it('should handle iOS', function () {\n        const platforms = [\n            \"iPhone\",\n            \"iPod\",\n            \"iPad\",\n        ];\n\n        navigator.userAgent = \"Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Mobile/15E148 Safari/604.1\";\n        platforms.forEach((platform) => {\n            navigator.platform = platform;\n            expect(isMac()).to.be.false;\n            expect(isWindows()).to.be.false;\n            expect(isIOS()).to.be.true;\n            expect(isAndroid()).to.be.false;\n            expect(isChromeOS()).to.be.false;\n        });\n    });\n\n    it('should handle Android', function () {\n        let userAgents = [\n            \"Mozilla/5.0 (Linux; Android 13; SM-G960U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.128 Mobile Safari/537.36\",\n            \"Mozilla/5.0 (Android 13; Mobile; LG-M255; rv:108.0) Gecko/108.0 Firefox/108.0\",\n        ];\n\n        navigator.platform = \"Linux x86_64\";\n        userAgents.forEach((ua) => {\n            navigator.userAgent = ua;\n            expect(isMac()).to.be.false;\n            expect(isWindows()).to.be.false;\n            expect(isIOS()).to.be.false;\n            expect(isAndroid()).to.be.true;\n            expect(isChromeOS()).to.be.false;\n        });\n    });\n\n    it('should handle ChromeOS', function () {\n        let userAgents = [\n            \"Mozilla/5.0 (X11; CrOS x86_64 15183.59.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.75 Safari/537.36\",\n            \"Mozilla/5.0 (X11; CrOS aarch64 15183.59.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.75 Safari/537.36\",\n        ];\n\n        navigator.platform = \"Linux x86_64\";\n        userAgents.forEach((ua) => {\n            navigator.userAgent = ua;\n            expect(isMac()).to.be.false;\n            expect(isWindows()).to.be.false;\n            expect(isIOS()).to.be.false;\n            expect(isAndroid()).to.be.false;\n            expect(isChromeOS()).to.be.true;\n        });\n    });\n});\n\ndescribe('Browser detection', function () {\n    let origNavigator;\n    beforeEach(function () {\n        // window.navigator is a protected read-only property in many\n        // environments, so we need to redefine it whilst running these\n        // tests.\n        origNavigator = Object.getOwnPropertyDescriptor(window, \"navigator\");\n\n        Object.defineProperty(window, \"navigator\", {value: {}});\n    });\n\n    afterEach(function () {\n        Object.defineProperty(window, \"navigator\", origNavigator);\n    });\n\n    it('should handle Chrome', function () {\n        navigator.userAgent = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36\";\n\n        expect(isSafari()).to.be.false;\n        expect(isFirefox()).to.be.false;\n        expect(isChrome()).to.be.true;\n        expect(isChromium()).to.be.false;\n        expect(isOpera()).to.be.false;\n        expect(isEdge()).to.be.false;\n\n        expect(isGecko()).to.be.false;\n        expect(isWebKit()).to.be.false;\n        expect(isBlink()).to.be.true;\n    });\n\n    it('should handle Chromium', function () {\n        navigator.userAgent = \"Mozilla/5.0 (X11; Linux armv7l) AppleWebKit/537.36 (KHTML, like Gecko) Raspbian Chromium/74.0.3729.157 Chrome/74.0.3729.157 Safari/537.36\";\n\n        expect(isSafari()).to.be.false;\n        expect(isFirefox()).to.be.false;\n        expect(isChrome()).to.be.false;\n        expect(isChromium()).to.be.true;\n        expect(isOpera()).to.be.false;\n        expect(isEdge()).to.be.false;\n\n        expect(isGecko()).to.be.false;\n        expect(isWebKit()).to.be.false;\n        expect(isBlink()).to.be.true;\n    });\n\n    it('should handle Firefox', function () {\n        navigator.userAgent = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0\";\n\n        expect(isSafari()).to.be.false;\n        expect(isFirefox()).to.be.true;\n        expect(isChrome()).to.be.false;\n        expect(isChromium()).to.be.false;\n        expect(isOpera()).to.be.false;\n        expect(isEdge()).to.be.false;\n\n        expect(isGecko()).to.be.true;\n        expect(isWebKit()).to.be.false;\n        expect(isBlink()).to.be.false;\n    });\n\n    it('should handle Seamonkey', function () {\n        navigator.userAgent = \"Mozilla/5.0 (Windows NT 6.1; rv:36.0) Gecko/20100101 Firefox/36.0 Seamonkey/2.33.1\";\n\n        expect(isSafari()).to.be.false;\n        expect(isFirefox()).to.be.false;\n        expect(isChrome()).to.be.false;\n        expect(isChromium()).to.be.false;\n        expect(isOpera()).to.be.false;\n        expect(isEdge()).to.be.false;\n\n        expect(isGecko()).to.be.true;\n        expect(isWebKit()).to.be.false;\n        expect(isBlink()).to.be.false;\n    });\n\n    it('should handle Safari', function () {\n        navigator.userAgent = \"Mozilla/5.0 (Macintosh; Intel Mac OS X 12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15\";\n\n        expect(isSafari()).to.be.true;\n        expect(isFirefox()).to.be.false;\n        expect(isChrome()).to.be.false;\n        expect(isChromium()).to.be.false;\n        expect(isOpera()).to.be.false;\n        expect(isEdge()).to.be.false;\n\n        expect(isGecko()).to.be.false;\n        expect(isWebKit()).to.be.true;\n        expect(isBlink()).to.be.false;\n    });\n\n    it('should handle Edge', function () {\n        navigator.userAgent = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 Edg/106.0.1370.34\";\n\n        expect(isSafari()).to.be.false;\n        expect(isFirefox()).to.be.false;\n        expect(isChrome()).to.be.false;\n        expect(isChromium()).to.be.false;\n        expect(isOpera()).to.be.false;\n        expect(isEdge()).to.be.true;\n\n        expect(isGecko()).to.be.false;\n        expect(isWebKit()).to.be.false;\n        expect(isBlink()).to.be.true;\n    });\n\n    it('should handle Opera', function () {\n        navigator.userAgent = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 OPR/91.0.4516.20\";\n\n        expect(isSafari()).to.be.false;\n        expect(isFirefox()).to.be.false;\n        expect(isChrome()).to.be.false;\n        expect(isChromium()).to.be.false;\n        expect(isOpera()).to.be.true;\n        expect(isEdge()).to.be.false;\n\n        expect(isGecko()).to.be.false;\n        expect(isWebKit()).to.be.false;\n        expect(isBlink()).to.be.true;\n    });\n\n    it('should handle Epiphany', function () {\n        navigator.userAgent = \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Safari/605.1.15 Epiphany/605.1.15\";\n\n        expect(isSafari()).to.be.false;\n        expect(isFirefox()).to.be.false;\n        expect(isChrome()).to.be.false;\n        expect(isChromium()).to.be.false;\n        expect(isOpera()).to.be.false;\n        expect(isEdge()).to.be.false;\n\n        expect(isGecko()).to.be.false;\n        expect(isWebKit()).to.be.true;\n        expect(isBlink()).to.be.false;\n    });\n});\n"
  },
  {
    "path": "tests/test.clipboard.js",
    "content": "import AsyncClipboard from '../core/clipboard.js';\n\ndescribe('Async Clipboard', function () {\n    \"use strict\";\n\n    let targetMock;\n    let clipboard;\n\n    beforeEach(function () {\n        sinon.stub(navigator, \"clipboard\").value({\n            writeText: sinon.stub().resolves(),\n            readText: sinon.stub().resolves(),\n        });\n\n        sinon.stub(navigator, \"permissions\").value({\n            query: sinon.stub(),\n        });\n\n        targetMock = document.createElement(\"canvas\");\n        clipboard = new AsyncClipboard(targetMock);\n    });\n\n    afterEach(function () {\n        sinon.restore();\n        targetMock = null;\n        clipboard = null;\n    });\n\n    function stubClipboardPermissions(state) {\n        navigator.permissions.query\n            .withArgs({ name: 'clipboard-write', allowWithoutGesture: true })\n            .resolves({ state: state });\n        navigator.permissions.query\n            .withArgs({ name: 'clipboard-read', allowWithoutGesture: false })\n            .resolves({ state: state });\n    }\n\n    function nextTick() {\n        return new Promise(resolve => setTimeout(resolve, 0));\n    }\n\n    it('grab() adds listener if permissions granted', async function () {\n        stubClipboardPermissions('granted');\n\n        const addListenerSpy = sinon.spy(targetMock, 'addEventListener');\n        clipboard.grab();\n\n        await nextTick();\n\n        expect(addListenerSpy.calledWith('focus')).to.be.true;\n    });\n\n    it('grab() does not add listener if permissions denied', async function () {\n        stubClipboardPermissions('denied');\n\n        const addListenerSpy = sinon.spy(targetMock, 'addEventListener');\n        clipboard.grab();\n\n        await nextTick();\n\n        expect(addListenerSpy.calledWith('focus')).to.be.false;\n    });\n\n    it('focus event triggers onpaste() if permissions granted', async function () {\n        stubClipboardPermissions('granted');\n\n        const text = 'hello clipboard world';\n        navigator.clipboard.readText.resolves(text);\n\n        const spyPromise = new Promise(resolve => clipboard.onpaste = resolve);\n\n        clipboard.grab();\n\n        await nextTick();\n\n        targetMock.dispatchEvent(new Event('focus'));\n\n        const res = await spyPromise;\n        expect(res).to.equal(text);\n    });\n\n    it('focus event does not trigger onpaste() if permissions denied', async function () {\n        stubClipboardPermissions('denied');\n\n        const text = 'should not read';\n        navigator.clipboard.readText.resolves(text);\n\n        clipboard.onpaste = sinon.spy();\n\n        clipboard.grab();\n\n        await nextTick();\n\n        targetMock.dispatchEvent(new Event('focus'));\n\n        expect(clipboard.onpaste.called).to.be.false;\n    });\n\n    it('writeClipboard() calls navigator.clipboard.writeText() if permissions granted', async function () {\n        stubClipboardPermissions('granted');\n        clipboard._isAvailable = true;\n\n        const text = 'writing to clipboard';\n        const result = clipboard.writeClipboard(text);\n\n        expect(navigator.clipboard.writeText.calledWith(text)).to.be.true;\n        expect(result).to.be.true;\n    });\n\n    it('writeClipboard() does not call navigator.clipboard.writeText() if permissions denied', async function () {\n        stubClipboardPermissions('denied');\n        clipboard._isAvailable = false;\n\n        const text = 'should not write';\n        const result = clipboard.writeClipboard(text);\n\n        expect(navigator.clipboard.writeText.called).to.be.false;\n        expect(result).to.be.false;\n    });\n\n});\n"
  },
  {
    "path": "tests/test.copyrect.js",
    "content": "import Websock from '../core/websock.js';\nimport Display from '../core/display.js';\n\nimport CopyRectDecoder from '../core/decoders/copyrect.js';\n\nimport FakeWebSocket from './fake.websocket.js';\n\nfunction testDecodeRect(decoder, x, y, width, height, data, display, depth) {\n    let sock;\n    let done = false;\n\n    sock = new Websock;\n    sock.open(\"ws://example.com\");\n\n    sock.on('message', () => {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    });\n\n    // Empty messages are filtered at multiple layers, so we need to\n    // do a direct call\n    if (data.length === 0) {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    } else {\n        sock._websocket._receiveData(new Uint8Array(data));\n    }\n\n    display.flip();\n\n    return done;\n}\n\ndescribe('CopyRect decoder', function () {\n    let decoder;\n    let display;\n\n    before(FakeWebSocket.replace);\n    after(FakeWebSocket.restore);\n\n    beforeEach(function () {\n        decoder = new CopyRectDecoder();\n        display = new Display(document.createElement('canvas'));\n        display.resize(4, 4);\n    });\n\n    it('should handle the CopyRect encoding', function () {\n        // seed some initial data to copy\n        display.fillRect(0, 0, 4, 4, [ 0x11, 0x22, 0x33 ]);\n        display.fillRect(0, 0, 2, 2, [ 0x00, 0x00, 0xff ]);\n        display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);\n\n        let done;\n        done = testDecodeRect(decoder, 0, 2, 2, 2,\n                              [0x00, 0x02, 0x00, 0x00],\n                              display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 2, 2, 2, 2,\n                              [0x00, 0x00, 0x00, 0x00],\n                              display, 24);\n        expect(done).to.be.true;\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle empty rects', function () {\n        display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);\n        display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);\n        display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);\n\n        let done = testDecodeRect(decoder, 1, 2, 0, 0,\n                                  [0x00, 0x00, 0x00, 0x00],\n                                  display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n});\n"
  },
  {
    "path": "tests/test.deflator.js",
    "content": "import { inflateInit, inflate } from \"../vendor/pako/lib/zlib/inflate.js\";\nimport ZStream from \"../vendor/pako/lib/zlib/zstream.js\";\nimport Deflator from \"../core/deflator.js\";\n\nfunction _inflator(compText, expected) {\n    let strm = new ZStream();\n    let chunkSize = 1024 * 10 * 10;\n    strm.output = new Uint8Array(chunkSize);\n\n    inflateInit(strm, 5);\n\n    if (expected > chunkSize) {\n        chunkSize = expected;\n        strm.output = new Uint8Array(chunkSize);\n    }\n\n    /* eslint-disable camelcase */\n    strm.input = compText;\n    strm.avail_in = strm.input.length;\n    strm.next_in = 0;\n\n    strm.next_out = 0;\n    strm.avail_out = expected.length;\n    /* eslint-enable camelcase */\n\n    let ret = inflate(strm, 0);\n\n    // Check that return code is not an error\n    expect(ret).to.be.greaterThan(-1);\n\n    return new Uint8Array(strm.output.buffer, 0, strm.next_out);\n}\n\ndescribe('Deflate data', function () {\n\n    it('should be able to deflate messages', function () {\n        let deflator = new Deflator();\n\n        let text = \"123asdf\";\n        let preText = new Uint8Array(text.length);\n        for (let i = 0; i < preText.length; i++) {\n            preText[i] = text.charCodeAt(i);\n        }\n\n        let compText = deflator.deflate(preText);\n\n        let inflatedText = _inflator(compText, text.length);\n        expect(inflatedText).to.array.equal(preText);\n\n    });\n\n    it('should be able to deflate large messages', function () {\n        let deflator = new Deflator();\n\n        /* Generate a big string with random characters. Used because\n           repetition of letters might be deflated more effectively than\n           random ones. */\n        let text = \"\";\n        let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n        for (let i = 0; i < 300000; i++) {\n            text += characters.charAt(Math.floor(Math.random() * characters.length));\n        }\n\n        let preText = new Uint8Array(text.length);\n        for (let i = 0; i < preText.length; i++) {\n            preText[i] = text.charCodeAt(i);\n        }\n\n        let compText = deflator.deflate(preText);\n\n        //Check that the compressed size is expected size\n        expect(compText.length).to.be.greaterThan((1024 * 10 * 10) * 2);\n\n        let inflatedText = _inflator(compText, text.length);\n\n        expect(inflatedText).to.array.equal(preText);\n\n    });\n});\n"
  },
  {
    "path": "tests/test.display.js",
    "content": "import Base64 from '../core/base64.js';\nimport Display from '../core/display.js';\n\ndescribe('Display/Canvas helper', function () {\n    const checkedData = new Uint8ClampedArray([\n        0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n        0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n        0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n        0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n    ]);\n\n    const basicData = new Uint8ClampedArray([0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0xff, 0xff, 0xff, 255]);\n\n    function makeImageCanvas(inputData, width, height) {\n        const canvas = document.createElement('canvas');\n        canvas.width = width;\n        canvas.height = height;\n        const ctx = canvas.getContext('2d');\n        const data = new ImageData(inputData, width, height);\n        ctx.putImageData(data, 0, 0);\n        return canvas;\n    }\n\n    function makeImagePng(inputData, width, height) {\n        const canvas = makeImageCanvas(inputData, width, height);\n        const url = canvas.toDataURL();\n        const data = url.split(\",\")[1];\n        return Base64.decode(data);\n    }\n\n    describe('viewport handling', function () {\n        let display;\n        beforeEach(function () {\n            display = new Display(document.createElement('canvas'));\n            display.clipViewport = true;\n            display.resize(5, 5);\n            display.viewportChangeSize(3, 3);\n            display.viewportChangePos(1, 1);\n        });\n\n        it('should take viewport location into consideration when drawing images', function () {\n            display.resize(4, 4);\n            display.viewportChangeSize(2, 2);\n            display.drawImage(makeImageCanvas(basicData, 4, 1), 1, 1);\n            display.flip();\n\n            const expected = new Uint8Array(16);\n            for (let i = 0; i < 8; i++) { expected[i] = basicData[i]; }\n            for (let i = 8; i < 16; i++) { expected[i] = 0; }\n            expect(display).to.have.displayed(expected);\n        });\n\n        it('should resize the target canvas when resizing the viewport', function () {\n            display.viewportChangeSize(2, 2);\n            expect(display._target.width).to.equal(2);\n            expect(display._target.height).to.equal(2);\n        });\n\n        it('should move the viewport if necessary', function () {\n            display.viewportChangeSize(5, 5);\n            expect(display.absX(0)).to.equal(0);\n            expect(display.absY(0)).to.equal(0);\n            expect(display._target.width).to.equal(5);\n            expect(display._target.height).to.equal(5);\n        });\n\n        it('should limit the viewport to the framebuffer size', function () {\n            display.viewportChangeSize(6, 6);\n            expect(display._target.width).to.equal(5);\n            expect(display._target.height).to.equal(5);\n        });\n\n        it('should redraw when moving the viewport', function () {\n            display.flip = sinon.spy();\n            display.viewportChangePos(-1, 1);\n            expect(display.flip).to.have.been.calledOnce;\n        });\n\n        it('should redraw when resizing the viewport', function () {\n            display.flip = sinon.spy();\n            display.viewportChangeSize(2, 2);\n            expect(display.flip).to.have.been.calledOnce;\n        });\n\n        it('should show the entire framebuffer when disabling the viewport', function () {\n            display.clipViewport = false;\n            expect(display.absX(0)).to.equal(0);\n            expect(display.absY(0)).to.equal(0);\n            expect(display._target.width).to.equal(5);\n            expect(display._target.height).to.equal(5);\n        });\n\n        it('should ignore viewport changes when the viewport is disabled', function () {\n            display.clipViewport = false;\n            display.viewportChangeSize(2, 2);\n            display.viewportChangePos(1, 1);\n            expect(display.absX(0)).to.equal(0);\n            expect(display.absY(0)).to.equal(0);\n            expect(display._target.width).to.equal(5);\n            expect(display._target.height).to.equal(5);\n        });\n\n        it('should show the entire framebuffer just after enabling the viewport', function () {\n            display.clipViewport = false;\n            display.clipViewport = true;\n            expect(display.absX(0)).to.equal(0);\n            expect(display.absY(0)).to.equal(0);\n            expect(display._target.width).to.equal(5);\n            expect(display._target.height).to.equal(5);\n        });\n    });\n\n    describe('resizing', function () {\n        let display;\n        beforeEach(function () {\n            display = new Display(document.createElement('canvas'));\n            display.clipViewport = false;\n            display.resize(4, 4);\n        });\n\n        it('should change the size of the logical canvas', function () {\n            display.resize(5, 7);\n            expect(display._fbWidth).to.equal(5);\n            expect(display._fbHeight).to.equal(7);\n        });\n\n        it('should keep the framebuffer data', function () {\n            display.fillRect(0, 0, 4, 4, [0xff, 0, 0]);\n            display.resize(2, 2);\n            display.flip();\n            const expected = [];\n            for (let i = 0; i < 4 * 2*2; i += 4) {\n                expected[i] = 0xff;\n                expected[i+1] = expected[i+2] = 0;\n                expected[i+3] = 0xff;\n            }\n            expect(display).to.have.displayed(new Uint8Array(expected));\n        });\n\n        describe('viewport', function () {\n            beforeEach(function () {\n                display.clipViewport = true;\n                display.viewportChangeSize(3, 3);\n                display.viewportChangePos(1, 1);\n            });\n\n            it('should keep the viewport position and size if possible', function () {\n                display.resize(6, 6);\n                expect(display.absX(0)).to.equal(1);\n                expect(display.absY(0)).to.equal(1);\n                expect(display._target.width).to.equal(3);\n                expect(display._target.height).to.equal(3);\n            });\n\n            it('should move the viewport if necessary', function () {\n                display.resize(3, 3);\n                expect(display.absX(0)).to.equal(0);\n                expect(display.absY(0)).to.equal(0);\n                expect(display._target.width).to.equal(3);\n                expect(display._target.height).to.equal(3);\n            });\n\n            it('should shrink the viewport if necessary', function () {\n                display.resize(2, 2);\n                expect(display.absX(0)).to.equal(0);\n                expect(display.absY(0)).to.equal(0);\n                expect(display._target.width).to.equal(2);\n                expect(display._target.height).to.equal(2);\n            });\n        });\n    });\n\n    describe('rescaling', function () {\n        let display;\n        let canvas;\n\n        beforeEach(function () {\n            canvas = document.createElement('canvas');\n            display = new Display(canvas);\n            display.clipViewport = true;\n            display.resize(4, 4);\n            display.viewportChangeSize(3, 3);\n            display.viewportChangePos(1, 1);\n            document.body.appendChild(canvas);\n        });\n\n        afterEach(function () {\n            document.body.removeChild(canvas);\n        });\n\n        it('should not change the bitmap size of the canvas', function () {\n            display.scale = 2.0;\n            expect(canvas.width).to.equal(3);\n            expect(canvas.height).to.equal(3);\n        });\n\n        it('should change the effective rendered size of the canvas', function () {\n            display.scale = 2.0;\n            expect(canvas.clientWidth).to.equal(6);\n            expect(canvas.clientHeight).to.equal(6);\n        });\n\n        it('should not change when resizing', function () {\n            display.scale = 2.0;\n            display.resize(5, 5);\n            expect(display.scale).to.equal(2.0);\n            expect(canvas.width).to.equal(3);\n            expect(canvas.height).to.equal(3);\n            expect(canvas.clientWidth).to.equal(6);\n            expect(canvas.clientHeight).to.equal(6);\n        });\n    });\n\n    describe('autoscaling', function () {\n        let display;\n        let canvas;\n\n        beforeEach(function () {\n            canvas = document.createElement('canvas');\n            display = new Display(canvas);\n            display.clipViewport = true;\n            display.resize(4, 3);\n            display.viewportChangeSize(4, 3);\n            document.body.appendChild(canvas);\n        });\n\n        afterEach(function () {\n            document.body.removeChild(canvas);\n        });\n\n        it('should preserve aspect ratio while autoscaling', function () {\n            display.autoscale(16, 9);\n            expect(canvas.clientWidth / canvas.clientHeight).to.equal(4 / 3);\n        });\n\n        it('should use width to determine scale when the current aspect ratio is wider than the target', function () {\n            display.autoscale(9, 16);\n            expect(display.absX(9)).to.equal(4);\n            expect(display.absY(18)).to.equal(8);\n            expect(canvas.clientWidth).to.equal(9);\n            expect(canvas.clientHeight).to.equal(7); // round 9 / (4 / 3)\n        });\n\n        it('should use height to determine scale when the current aspect ratio is taller than the target', function () {\n            display.autoscale(16, 9);\n            expect(display.absX(9)).to.equal(3);\n            expect(display.absY(18)).to.equal(6);\n            expect(canvas.clientWidth).to.equal(12);  // 16 * (4 / 3)\n            expect(canvas.clientHeight).to.equal(9);\n\n        });\n\n        it('should not change the bitmap size of the canvas', function () {\n            display.autoscale(16, 9);\n            expect(canvas.width).to.equal(4);\n            expect(canvas.height).to.equal(3);\n        });\n    });\n\n    describe('drawing', function () {\n\n        // TODO(directxman12): improve the tests for each of the drawing functions to cover more than just the\n        //                     basic cases\n        let display;\n        beforeEach(function () {\n            display = new Display(document.createElement('canvas'));\n            display.resize(4, 4);\n        });\n\n        it('should not draw directly on the target canvas', function () {\n            display.fillRect(0, 0, 4, 4, [0xff, 0, 0]);\n            display.flip();\n            display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);\n            const expected = [];\n            for (let i = 0; i < 4 * display._fbWidth * display._fbHeight; i += 4) {\n                expected[i] = 0xff;\n                expected[i+1] = expected[i+2] = 0;\n                expected[i+3] = 0xff;\n            }\n            expect(display).to.have.displayed(new Uint8Array(expected));\n        });\n\n        it('should support filling a rectangle with particular color via #fillRect', function () {\n            display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);\n            display.fillRect(0, 0, 2, 2, [0, 0, 0xff]);\n            display.fillRect(2, 2, 2, 2, [0, 0, 0xff]);\n            display.flip();\n            expect(display).to.have.displayed(checkedData);\n        });\n\n        it('should support copying an portion of the canvas via #copyImage', function () {\n            display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);\n            display.fillRect(0, 0, 2, 2, [0, 0, 0xff]);\n            display.copyImage(0, 0, 2, 2, 2, 2);\n            display.flip();\n            expect(display).to.have.displayed(checkedData);\n        });\n\n        it('should support drawing images via #imageRect', async function () {\n            display.imageRect(0, 0, 4, 4, \"image/png\", makeImagePng(checkedData, 4, 4));\n            display.flip();\n            await display.flush();\n            expect(display).to.have.displayed(checkedData);\n        });\n\n        it('should support blit images with true color via #blitImage', function () {\n            display.blitImage(0, 0, 4, 4, checkedData, 0);\n            display.flip();\n            expect(display).to.have.displayed(checkedData);\n        });\n\n        it('should support drawing an image object via #drawImage', function () {\n            const img = makeImageCanvas(checkedData, 4, 4);\n            display.drawImage(img, 0, 0);\n            display.flip();\n            expect(display).to.have.displayed(checkedData);\n        });\n    });\n\n    describe('the render queue processor', function () {\n        let display;\n        beforeEach(function () {\n            display = new Display(document.createElement('canvas'));\n            display.resize(4, 4);\n            sinon.spy(display, '_scanRenderQ');\n        });\n\n        it('should try to process an item when it is pushed on, if nothing else is on the queue', function () {\n            display._renderQPush({ type: 'noop' });  // does nothing\n            expect(display._scanRenderQ).to.have.been.calledOnce;\n        });\n\n        it('should not try to process an item when it is pushed on if we are waiting for other items', function () {\n            display._renderQ.length = 2;\n            display._renderQPush({ type: 'noop' });\n            expect(display._scanRenderQ).to.not.have.been.called;\n        });\n\n        it('should wait until an image is loaded to attempt to draw it and the rest of the queue', function () {\n            const img = { complete: false, width: 4, height: 4, addEventListener: sinon.spy() };\n            display._renderQ = [{ type: 'img', x: 3, y: 4, width: 4, height: 4, img: img },\n                                { type: 'fill', x: 1, y: 2, width: 3, height: 4, color: 5 }];\n            display.drawImage = sinon.spy();\n            display.fillRect = sinon.spy();\n\n            display._scanRenderQ();\n            expect(display.drawImage).to.not.have.been.called;\n            expect(display.fillRect).to.not.have.been.called;\n            expect(img.addEventListener).to.have.been.calledOnce;\n\n            display._renderQ[0].img.complete = true;\n            display._scanRenderQ();\n            expect(display.drawImage).to.have.been.calledOnce;\n            expect(display.fillRect).to.have.been.calledOnce;\n            expect(img.addEventListener).to.have.been.calledOnce;\n        });\n\n        it('should resolve promise when queue is flushed', async function () {\n            display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);\n            let promise = display.flush();\n            expect(promise).to.be.an.instanceOf(Promise);\n            await promise;\n        });\n\n        it('should draw a blit image on type \"blit\"', function () {\n            display.blitImage = sinon.spy();\n            display._renderQPush({ type: 'blit', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] });\n            expect(display.blitImage).to.have.been.calledOnce;\n            expect(display.blitImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0);\n        });\n\n        it('should copy a region on type \"copy\"', function () {\n            display.copyImage = sinon.spy();\n            display._renderQPush({ type: 'copy', x: 3, y: 4, width: 5, height: 6, oldX: 7, oldY: 8 });\n            expect(display.copyImage).to.have.been.calledOnce;\n            expect(display.copyImage).to.have.been.calledWith(7, 8, 3, 4, 5, 6);\n        });\n\n        it('should fill a rect with a given color on type \"fill\"', function () {\n            display.fillRect = sinon.spy();\n            display._renderQPush({ type: 'fill', x: 3, y: 4, width: 5, height: 6, color: [7, 8, 9]});\n            expect(display.fillRect).to.have.been.calledOnce;\n            expect(display.fillRect).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9]);\n        });\n\n        it('should draw an image from an image object on type \"img\" (if complete)', function () {\n            const img = { complete: true };\n            display.drawImage = sinon.spy();\n            display._renderQPush({ type: 'img', x: 3, y: 4, img: img });\n            expect(display.drawImage).to.have.been.calledOnce;\n            expect(display.drawImage).to.have.been.calledWith(img, 3, 4);\n        });\n    });\n});\n"
  },
  {
    "path": "tests/test.gesturehandler.js",
    "content": "import EventTargetMixin from '../core/util/eventtarget.js';\n\nimport GestureHandler from '../core/input/gesturehandler.js';\n\nclass DummyTarget extends EventTargetMixin {\n}\n\ndescribe('Gesture handler', function () {\n    let target, handler;\n    let gestures;\n    let clock;\n    let touches;\n\n    before(function () {\n        clock = sinon.useFakeTimers();\n    });\n\n    after(function () {\n        clock.restore();\n    });\n\n    beforeEach(function () {\n        target = new DummyTarget();\n        gestures = sinon.spy();\n        target.addEventListener('gesturestart', gestures);\n        target.addEventListener('gesturemove', gestures);\n        target.addEventListener('gestureend', gestures);\n        touches = [];\n        handler = new GestureHandler();\n        handler.attach(target);\n    });\n\n    afterEach(function () {\n        if (handler) {\n            handler.detach();\n        }\n        target = null;\n        gestures = null;\n    });\n\n    function touchStart(id, x, y) {\n        let touch = { identifier: id,\n                      clientX: x, clientY: y };\n        touches.push(touch);\n        let ev = { type: 'touchstart',\n                   touches: touches,\n                   targetTouches: touches,\n                   changedTouches: [ touch ],\n                   stopPropagation: sinon.spy(),\n                   preventDefault: sinon.spy() };\n        target.dispatchEvent(ev);\n    }\n\n    function touchMove(id, x, y) {\n        let touch = touches.find(t => t.identifier === id);\n        touch.clientX = x;\n        touch.clientY = y;\n        let ev = { type: 'touchmove',\n                   touches: touches,\n                   targetTouches: touches,\n                   changedTouches: [ touch ],\n                   stopPropagation: sinon.spy(),\n                   preventDefault: sinon.spy() };\n        target.dispatchEvent(ev);\n    }\n\n    function touchEnd(id) {\n        let idx = touches.findIndex(t => t.identifier === id);\n        let touch = touches.splice(idx, 1)[0];\n        let ev = { type: 'touchend',\n                   touches: touches,\n                   targetTouches: touches,\n                   changedTouches: [ touch ],\n                   stopPropagation: sinon.spy(),\n                   preventDefault: sinon.spy() };\n        target.dispatchEvent(ev);\n    }\n\n    describe('Single finger tap', function () {\n        it('should handle single finger tap', function () {\n            touchStart(1, 20.0, 30.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchEnd(1);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'onetap',\n                                        clientX: 20.0,\n                                        clientY: 30.0 } }));\n\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'onetap',\n                                        clientX: 20.0,\n                                        clientY: 30.0 } }));\n        });\n    });\n\n    describe('Two finger tap', function () {\n        it('should handle two finger tap', function () {\n            touchStart(1, 20.0, 30.0);\n            touchStart(2, 30.0, 50.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchEnd(1);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchEnd(2);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'twotap',\n                                        clientX: 25.0,\n                                        clientY: 40.0 } }));\n\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'twotap',\n                                        clientX: 25.0,\n                                        clientY: 40.0 } }));\n        });\n\n        it('should ignore slow starting two finger tap', function () {\n            touchStart(1, 20.0, 30.0);\n\n            clock.tick(500);\n\n            touchStart(2, 30.0, 50.0);\n            touchEnd(1);\n            touchEnd(2);\n\n            expect(gestures).to.not.have.been.called;\n        });\n\n        it('should ignore slow ending two finger tap', function () {\n            touchStart(1, 20.0, 30.0);\n            touchStart(2, 30.0, 50.0);\n            touchEnd(1);\n\n            clock.tick(500);\n\n            touchEnd(2);\n\n            expect(gestures).to.not.have.been.called;\n        });\n\n        it('should ignore slow two finger tap', function () {\n            touchStart(1, 20.0, 30.0);\n            touchStart(2, 30.0, 50.0);\n\n            clock.tick(1500);\n\n            touchEnd(1);\n            touchEnd(2);\n\n            expect(gestures).to.not.have.been.called;\n        });\n    });\n\n    describe('Three finger tap', function () {\n        it('should handle three finger tap', function () {\n            touchStart(1, 20.0, 30.0);\n            touchStart(2, 30.0, 50.0);\n            touchStart(3, 40.0, 40.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchEnd(1);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchEnd(2);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchEnd(3);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'threetap',\n                                        clientX: 30.0,\n                                        clientY: 40.0 } }));\n\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'threetap',\n                                        clientX: 30.0,\n                                        clientY: 40.0 } }));\n        });\n\n        it('should ignore slow starting three finger tap', function () {\n            touchStart(1, 20.0, 30.0);\n            touchStart(2, 30.0, 50.0);\n\n            clock.tick(500);\n\n            touchStart(3, 40.0, 40.0);\n            touchEnd(1);\n            touchEnd(2);\n            touchEnd(3);\n\n            expect(gestures).to.not.have.been.called;\n        });\n\n        it('should ignore slow ending three finger tap', function () {\n            touchStart(1, 20.0, 30.0);\n            touchStart(2, 30.0, 50.0);\n            touchStart(3, 40.0, 40.0);\n            touchEnd(1);\n            touchEnd(2);\n\n            clock.tick(500);\n\n            touchEnd(3);\n\n            expect(gestures).to.not.have.been.called;\n        });\n\n        it('should ignore three finger drag', function () {\n            touchStart(1, 20.0, 30.0);\n            touchStart(2, 30.0, 50.0);\n            touchStart(3, 40.0, 40.0);\n\n            touchMove(1, 120.0, 130.0);\n            touchMove(2, 130.0, 150.0);\n            touchMove(3, 140.0, 140.0);\n\n            touchEnd(1);\n            touchEnd(2);\n            touchEnd(3);\n\n            expect(gestures).to.not.have.been.called;\n        });\n\n        it('should ignore slow three finger tap', function () {\n            touchStart(1, 20.0, 30.0);\n            touchStart(2, 30.0, 50.0);\n            touchStart(3, 40.0, 40.0);\n\n            clock.tick(1500);\n\n            touchEnd(1);\n            touchEnd(2);\n            touchEnd(3);\n\n            expect(gestures).to.not.have.been.called;\n        });\n    });\n\n    describe('Single finger drag', function () {\n        it('should handle horizontal single finger drag', function () {\n            touchStart(1, 20.0, 30.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchMove(1, 40.0, 30.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchMove(1, 80.0, 30.0);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'drag',\n                                        clientX: 20.0,\n                                        clientY: 30.0 } }));\n\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'drag',\n                                        clientX: 80.0,\n                                        clientY: 30.0 } }));\n\n            gestures.resetHistory();\n\n            touchEnd(1);\n\n            expect(gestures).to.have.been.calledOnceWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'drag',\n                                        clientX: 80.0,\n                                        clientY: 30.0 } }));\n        });\n\n        it('should handle vertical single finger drag', function () {\n            touchStart(1, 20.0, 30.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchMove(1, 20.0, 50.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchMove(1, 20.0, 90.0);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'drag',\n                                        clientX: 20.0,\n                                        clientY: 30.0 } }));\n\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'drag',\n                                        clientX: 20.0,\n                                        clientY: 90.0 } }));\n\n            gestures.resetHistory();\n\n            touchEnd(1);\n\n            expect(gestures).to.have.been.calledOnceWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'drag',\n                                        clientX: 20.0,\n                                        clientY: 90.0 } }));\n        });\n\n        it('should handle diagonal single finger drag', function () {\n            touchStart(1, 120.0, 130.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchMove(1, 90.0, 100.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchMove(1, 60.0, 70.0);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'drag',\n                                        clientX: 120.0,\n                                        clientY: 130.0 } }));\n\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'drag',\n                                        clientX: 60.0,\n                                        clientY: 70.0 } }));\n\n            gestures.resetHistory();\n\n            touchEnd(1);\n\n            expect(gestures).to.have.been.calledOnceWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'drag',\n                                        clientX: 60.0,\n                                        clientY: 70.0 } }));\n        });\n    });\n\n    describe('Long press', function () {\n        it('should handle long press', function () {\n            touchStart(1, 20.0, 30.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            clock.tick(1500);\n\n            expect(gestures).to.have.been.calledOnceWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'longpress',\n                                        clientX: 20.0,\n                                        clientY: 30.0 } }));\n\n            gestures.resetHistory();\n\n            touchEnd(1);\n\n            expect(gestures).to.have.been.calledOnceWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'longpress',\n                                        clientX: 20.0,\n                                        clientY: 30.0 } }));\n        });\n\n        it('should handle long press drag', function () {\n            touchStart(1, 20.0, 30.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            clock.tick(1500);\n\n            expect(gestures).to.have.been.calledOnceWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'longpress',\n                                        clientX: 20.0,\n                                        clientY: 30.0 } }));\n\n            gestures.resetHistory();\n\n            touchMove(1, 120.0, 50.0);\n\n            expect(gestures).to.have.been.calledOnceWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'longpress',\n                                        clientX: 120.0,\n                                        clientY: 50.0 } }));\n\n            gestures.resetHistory();\n\n            touchEnd(1);\n\n            expect(gestures).to.have.been.calledOnceWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'longpress',\n                                        clientX: 120.0,\n                                        clientY: 50.0 } }));\n        });\n    });\n\n    describe('Two finger drag', function () {\n        it('should handle fast and distinct horizontal two finger drag', function () {\n            touchStart(1, 20.0, 30.0);\n            touchStart(2, 30.0, 30.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchMove(1, 40.0, 30.0);\n            touchMove(2, 50.0, 30.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchMove(2, 90.0, 30.0);\n            touchMove(1, 80.0, 30.0);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'twodrag',\n                                        clientX: 25.0,\n                                        clientY: 30.0,\n                                        magnitudeX: 0.0,\n                                        magnitudeY: 0.0 } }));\n\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'twodrag',\n                                        clientX: 25.0,\n                                        clientY: 30.0,\n                                        magnitudeX: 60.0,\n                                        magnitudeY: 0.0 } }));\n\n            gestures.resetHistory();\n\n            touchEnd(1);\n\n            expect(gestures).to.have.been.calledOnceWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'twodrag',\n                                        clientX: 25.0,\n                                        clientY: 30.0,\n                                        magnitudeX: 60.0,\n                                        magnitudeY: 0.0 } }));\n        });\n\n        it('should handle fast and distinct vertical two finger drag', function () {\n            touchStart(1, 20.0, 30.0);\n            touchStart(2, 30.0, 30.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchMove(1, 20.0, 100.0);\n            touchMove(2, 30.0, 40.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchMove(2, 30.0, 90.0);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'twodrag',\n                                        clientX: 25.0,\n                                        clientY: 30.0,\n                                        magnitudeX: 0.0,\n                                        magnitudeY: 0.0 } }));\n\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'twodrag',\n                                        clientX: 25.0,\n                                        clientY: 30.0,\n                                        magnitudeX: 0.0,\n                                        magnitudeY: 65.0 } }));\n\n            gestures.resetHistory();\n\n            touchEnd(1);\n\n            expect(gestures).to.have.been.calledOnceWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'twodrag',\n                                        clientX: 25.0,\n                                        clientY: 30.0,\n                                        magnitudeX: 0.0,\n                                        magnitudeY: 65.0 } }));\n        });\n\n        it('should handle fast and distinct diagonal two finger drag', function () {\n            touchStart(1, 120.0, 130.0);\n            touchStart(2, 130.0, 130.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchMove(1, 80.0, 90.0);\n            touchMove(2, 100.0, 130.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchMove(2, 60.0, 70.0);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'twodrag',\n                                        clientX: 125.0,\n                                        clientY: 130.0,\n                                        magnitudeX: 0.0,\n                                        magnitudeY: 0.0 } }));\n\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'twodrag',\n                                        clientX: 125.0,\n                                        clientY: 130.0,\n                                        magnitudeX: -55.0,\n                                        magnitudeY: -50.0 } }));\n\n            gestures.resetHistory();\n\n            touchEnd(1);\n\n            expect(gestures).to.have.been.calledOnceWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'twodrag',\n                                        clientX: 125.0,\n                                        clientY: 130.0,\n                                        magnitudeX: -55.0,\n                                        magnitudeY: -50.0 } }));\n        });\n\n        it('should ignore fast almost two finger dragging', function () {\n            touchStart(1, 20.0, 30.0);\n            touchStart(2, 30.0, 30.0);\n            touchMove(1, 80.0, 30.0);\n            touchMove(2, 70.0, 30.0);\n            touchEnd(1);\n            touchEnd(2);\n\n            expect(gestures).to.not.have.been.called;\n\n            clock.tick(1500);\n\n            expect(gestures).to.not.have.been.called;\n        });\n\n        it('should handle slow horizontal two finger drag', function () {\n            touchStart(1, 50.0, 40.0);\n            touchStart(2, 60.0, 40.0);\n            touchMove(1, 80.0, 40.0);\n            touchMove(2, 110.0, 40.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            clock.tick(60);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'twodrag',\n                                        clientX: 55.0,\n                                        clientY: 40.0,\n                                        magnitudeX: 0.0,\n                                        magnitudeY: 0.0 } }));\n\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'twodrag',\n                                        clientX: 55.0,\n                                        clientY: 40.0,\n                                        magnitudeX: 40.0,\n                                        magnitudeY: 0.0 } }));\n        });\n\n        it('should handle slow vertical two finger drag', function () {\n            touchStart(1, 40.0, 40.0);\n            touchStart(2, 40.0, 60.0);\n            touchMove(2, 40.0, 80.0);\n            touchMove(1, 40.0, 100.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            clock.tick(60);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'twodrag',\n                                        clientX: 40.0,\n                                        clientY: 50.0,\n                                        magnitudeX: 0.0,\n                                        magnitudeY: 0.0 } }));\n\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'twodrag',\n                                        clientX: 40.0,\n                                        clientY: 50.0,\n                                        magnitudeX: 0.0,\n                                        magnitudeY: 40.0 } }));\n        });\n\n        it('should handle slow diagonal two finger drag', function () {\n            touchStart(1, 50.0, 40.0);\n            touchStart(2, 40.0, 60.0);\n            touchMove(1, 70.0, 60.0);\n            touchMove(2, 90.0, 110.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            clock.tick(60);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'twodrag',\n                                        clientX: 45.0,\n                                        clientY: 50.0,\n                                        magnitudeX: 0.0,\n                                        magnitudeY: 0.0 } }));\n\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'twodrag',\n                                        clientX: 45.0,\n                                        clientY: 50.0,\n                                        magnitudeX: 35.0,\n                                        magnitudeY: 35.0 } }));\n        });\n\n        it('should ignore too slow two finger drag', function () {\n            touchStart(1, 20.0, 30.0);\n\n            clock.tick(500);\n\n            touchStart(2, 30.0, 30.0);\n            touchMove(1, 40.0, 30.0);\n            touchMove(2, 50.0, 30.0);\n            touchMove(1, 80.0, 30.0);\n\n            expect(gestures).to.not.have.been.called;\n        });\n    });\n\n    describe('Pinch', function () {\n        it('should handle pinching distinctly and fast inwards', function () {\n            touchStart(1, 0.0, 0.0);\n            touchStart(2, 130.0, 130.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchMove(1, 50.0, 40.0);\n            touchMove(2, 100.0, 130.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchMove(2, 60.0, 70.0);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'pinch',\n                                        clientX: 65.0,\n                                        clientY: 65.0,\n                                        magnitudeX: 130.0,\n                                        magnitudeY: 130.0 } }));\n\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'pinch',\n                                        clientX: 65.0,\n                                        clientY: 65.0,\n                                        magnitudeX: 10.0,\n                                        magnitudeY: 30.0 } }));\n\n            gestures.resetHistory();\n\n            touchEnd(1);\n\n            expect(gestures).to.have.been.calledOnceWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'pinch',\n                                        clientX: 65.0,\n                                        clientY: 65.0,\n                                        magnitudeX: 10.0,\n                                        magnitudeY: 30.0 } }));\n        });\n\n        it('should handle pinching fast and distinctly outwards', function () {\n            touchStart(1, 100.0, 100.0);\n            touchStart(2, 110.0, 100.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchMove(1, 130.0, 70.0);\n            touchMove(2, 0.0, 200.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchMove(1, 180.0, 20.0);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'pinch',\n                                        clientX: 105.0,\n                                        clientY: 100.0,\n                                        magnitudeX: 10.0,\n                                        magnitudeY: 0.0 } }));\n\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'pinch',\n                                        clientX: 105.0,\n                                        clientY: 100.0,\n                                        magnitudeX: 180.0,\n                                        magnitudeY: 180.0 } }));\n\n            gestures.resetHistory();\n\n            touchEnd(1);\n\n            expect(gestures).to.have.been.calledOnceWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'pinch',\n                                        clientX: 105.0,\n                                        clientY: 100.0,\n                                        magnitudeX: 180.0,\n                                        magnitudeY: 180.0 } }));\n        });\n\n        it('should ignore fast almost pinching', function () {\n            touchStart(1, 20.0, 30.0);\n            touchStart(2, 130.0, 130.0);\n            touchMove(1, 80.0, 70.0);\n            touchEnd(1);\n            touchEnd(2);\n\n            expect(gestures).to.not.have.been.called;\n\n            clock.tick(1500);\n\n            expect(gestures).to.not.have.been.called;\n        });\n\n        it('should handle pinching inwards slowly', function () {\n            touchStart(1, 0.0, 0.0);\n            touchStart(2, 130.0, 130.0);\n            touchMove(1, 50.0, 40.0);\n            touchMove(2, 100.0, 130.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            clock.tick(60);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'pinch',\n                                        clientX: 65.0,\n                                        clientY: 65.0,\n                                        magnitudeX: 130.0,\n                                        magnitudeY: 130.0 } }));\n\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'pinch',\n                                        clientX: 65.0,\n                                        clientY: 65.0,\n                                        magnitudeX: 50.0,\n                                        magnitudeY: 90.0 } }));\n        });\n\n        it('should handle pinching outwards slowly', function () {\n            touchStart(1, 100.0, 130.0);\n            touchStart(2, 110.0, 130.0);\n            touchMove(2, 200.0, 130.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            clock.tick(60);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'pinch',\n                                        clientX: 105.0,\n                                        clientY: 130.0,\n                                        magnitudeX: 10.0,\n                                        magnitudeY: 0.0 } }));\n\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'pinch',\n                                        clientX: 105.0,\n                                        clientY: 130.0,\n                                        magnitudeX: 100.0,\n                                        magnitudeY: 0.0 } }));\n        });\n\n        it('should ignore pinching too slowly', function () {\n            touchStart(1, 0.0, 0.0);\n\n            clock.tick(500);\n\n            touchStart(2, 130.0, 130.0);\n            touchMove(2, 100.0, 130.0);\n            touchMove(1, 50.0, 40.0);\n\n            expect(gestures).to.not.have.been.called;\n        });\n    });\n\n    describe('Ignoring', function () {\n        it('should ignore extra touches during gesture', function () {\n            touchStart(1, 20.0, 30.0);\n            touchMove(1, 40.0, 30.0);\n            touchMove(1, 80.0, 30.0);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'drag' } }));\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'drag' } }));\n\n            gestures.resetHistory();\n\n            touchStart(2, 10.0, 10.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchMove(1, 100.0, 50.0);\n\n            expect(gestures).to.have.been.calledOnceWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'drag',\n                                        clientX: 100.0,\n                                        clientY: 50.0 } }));\n\n            gestures.resetHistory();\n\n            touchEnd(1);\n\n            expect(gestures).to.have.been.calledOnceWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'drag',\n                                        clientX: 100.0,\n                                        clientY: 50.0 } }));\n        });\n\n        it('should ignore extra touches when waiting for gesture to end', function () {\n            touchStart(1, 20.0, 30.0);\n            touchStart(2, 30.0, 30.0);\n            touchMove(1, 40.0, 30.0);\n            touchMove(2, 90.0, 30.0);\n            touchMove(1, 80.0, 30.0);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'twodrag' } }));\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'twodrag' } }));\n\n            gestures.resetHistory();\n\n            touchEnd(1);\n\n            expect(gestures).to.have.been.calledOnceWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'twodrag' } }));\n\n            gestures.resetHistory();\n\n            touchStart(3, 10.0, 10.0);\n            touchEnd(3);\n\n            expect(gestures).to.not.have.been.called;\n        });\n\n        it('should ignore extra touches after gesture', function () {\n            touchStart(1, 20.0, 30.0);\n            touchMove(1, 40.0, 30.0);\n            touchMove(1, 80.0, 30.0);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'drag' } }));\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'drag' } }));\n\n            gestures.resetHistory();\n\n            touchStart(2, 10.0, 10.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchMove(1, 100.0, 50.0);\n\n            expect(gestures).to.have.been.calledOnceWith(\n                sinon.match({ type: 'gesturemove',\n                              detail: { type: 'drag' } }));\n\n            gestures.resetHistory();\n\n            touchEnd(1);\n\n            expect(gestures).to.have.been.calledOnceWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'drag' } }));\n\n            gestures.resetHistory();\n\n            touchEnd(2);\n\n            expect(gestures).to.not.have.been.called;\n\n            // Check that everything is reseted after trailing ignores are released\n\n            touchStart(3, 20.0, 30.0);\n            touchEnd(3);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'onetap' } }));\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'onetap' } }));\n        });\n\n        it('should properly reset after a gesture', function () {\n            touchStart(1, 20.0, 30.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchEnd(1);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'onetap',\n                                        clientX: 20.0,\n                                        clientY: 30.0 } }));\n\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'onetap',\n                                        clientX: 20.0,\n                                        clientY: 30.0 } }));\n\n            gestures.resetHistory();\n\n            touchStart(2, 70.0, 80.0);\n\n            expect(gestures).to.not.have.been.called;\n\n            touchEnd(2);\n\n            expect(gestures).to.have.been.calledTwice;\n\n            expect(gestures.firstCall).to.have.been.calledWith(\n                sinon.match({ type: 'gesturestart',\n                              detail: { type: 'onetap',\n                                        clientX: 70.0,\n                                        clientY: 80.0 } }));\n\n            expect(gestures.secondCall).to.have.been.calledWith(\n                sinon.match({ type: 'gestureend',\n                              detail: { type: 'onetap',\n                                        clientX: 70.0,\n                                        clientY: 80.0 } }));\n        });\n    });\n});\n"
  },
  {
    "path": "tests/test.h264.js",
    "content": "import Websock from '../core/websock.js';\nimport Display from '../core/display.js';\n\nimport { H264Parser } from '../core/decoders/h264.js';\nimport H264Decoder from '../core/decoders/h264.js';\nimport Base64 from '../core/base64.js';\nimport { supportsWebCodecsH264Decode } from '../core/util/browser.js';\n\nimport FakeWebSocket from './fake.websocket.js';\n\n/* This is a 3 frame 16x16 video where the first frame is solid red, the second\n * is solid green and the third is solid blue.\n *\n * The colour space is BT.709. It is encoded into the stream.\n */\nconst redGreenBlue16x16Video = new Uint8Array(Base64.decode(\n    'AAAAAWdCwBTZnpuAgICgAAADACAAAAZB4oVNAAAAAWjJYyyAAAABBgX//4HcRem95tlIt5Ys' +\n    '2CDZI+7veDI2NCAtIGNvcmUgMTY0IHIzMTA4IDMxZTE5ZjkgLSBILjI2NC9NUEVHLTQgQVZD' +\n    'IGNvZGVjIC0gQ29weWxlZnQgMjAwMy0yMDIzIC0gaHR0cDovL3d3dy52aWRlb2xhbi5vcmcv' +\n    'eDI2NC5odG1sIC0gb3B0aW9uczogY2FiYWM9MCByZWY9NSBkZWJsb2NrPTE6MDowIGFuYWx5' +\n    'c2U9MHgxOjB4MTExIG1lPWhleCBzdWJtZT04IHBzeT0xIHBzeV9yZD0xLjAwOjAuMDAgbWl4' +\n    'ZWRfcmVmPTEgbWVfcmFuZ2U9MTYgY2hyb21hX21lPTEgdHJlbGxpcz0yIDh4OGRjdD0wIGNx' +\n    'bT0wIGRlYWR6b25lPTIxLDExIGZhc3RfcHNraXA9MSBjaHJvbWFfcXBfb2Zmc2V0PS0yIHRo' +\n    'cmVhZHM9MSBsb29rYWhlYWRfdGhyZWFkcz0xIHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNp' +\n    'bWF0ZT0xIGludGVybGFjZWQ9MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9' +\n    'MCBiZnJhbWVzPTAgd2VpZ2h0cD0wIGtleWludD1pbmZpbml0ZSBrZXlpbnRfbWluPTI1IHNj' +\n    'ZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NTAgcmM9YWJyIG1idHJl' +\n    'ZT0xIGJpdHJhdGU9NDAwIHJhdGV0b2w9MS4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02' +\n    'OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAABZYiEBrxmKAAPVccAAS04' +\n    '4AA5DRJMnkycJk4TPwAAAAFBiIga8RigADVVHAAGaGOAANtuAAAAAUGIkBr///wRRQABVf8c' +\n    'AAcho4AAiD4='));\n\nfunction createSolidColorFrameBuffer(color, width, height) {\n    const r = (color >> 24) & 0xff;\n    const g = (color >> 16) & 0xff;\n    const b = (color >> 8) & 0xff;\n    const a = (color >> 0) & 0xff;\n\n    const size = width * height * 4;\n    let array = new Uint8ClampedArray(size);\n\n    for (let i = 0; i < size / 4; ++i) {\n        array[i * 4 + 0] = r;\n        array[i * 4 + 1] = g;\n        array[i * 4 + 2] = b;\n        array[i * 4 + 3] = a;\n    }\n\n    return array;\n}\n\nfunction makeMessageHeader(length, resetContext, resetAllContexts) {\n    let flags = 0;\n    if (resetContext) {\n        flags |= 1;\n    }\n    if (resetAllContexts) {\n        flags |= 2;\n    }\n\n    let header = new Uint8Array(8);\n    let i = 0;\n\n    let appendU32 = (v) => {\n        header[i++] = (v >> 24) & 0xff;\n        header[i++] = (v >> 16) & 0xff;\n        header[i++] = (v >> 8) & 0xff;\n        header[i++] = v & 0xff;\n    };\n\n    appendU32(length);\n    appendU32(flags);\n\n    return header;\n}\n\nfunction wrapRectData(data, resetContext, resetAllContexts) {\n    let header = makeMessageHeader(data.length, resetContext, resetAllContexts);\n    return Array.from(header).concat(Array.from(data));\n}\n\nfunction testDecodeRect(decoder, x, y, width, height, data, display, depth) {\n    let sock;\n    let done = false;\n\n    sock = new Websock;\n    sock.open(\"ws://example.com\");\n\n    sock.on('message', () => {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    });\n\n    // Empty messages are filtered at multiple layers, so we need to\n    // do a direct call\n    if (data.length === 0) {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    } else {\n        sock._websocket._receiveData(new Uint8Array(data));\n    }\n\n    display.flip();\n\n    return done;\n}\n\nfunction almost(a, b) {\n    let diff = Math.abs(a - b);\n    return diff < 5;\n}\n\ndescribe('H.264 parser', function () {\n    it('should parse constrained baseline video', function () {\n        let parser = new H264Parser(redGreenBlue16x16Video);\n\n        let frame = parser.parse();\n        expect(frame).to.have.property('key', true);\n\n        expect(parser).to.have.property('profileIdc', 66);\n        expect(parser).to.have.property('constraintSet', 192);\n        expect(parser).to.have.property('levelIdc', 20);\n\n        frame = parser.parse();\n        expect(frame).to.have.property('key', false);\n\n        frame = parser.parse();\n        expect(frame).to.have.property('key', false);\n\n        frame = parser.parse();\n        expect(frame).to.be.null;\n    });\n});\n\ndescribe('H.264 decoder unit test', function () {\n    let decoder;\n\n    beforeEach(function () {\n        if (!supportsWebCodecsH264Decode) {\n            this.skip();\n            return;\n        }\n        decoder = new H264Decoder();\n    });\n\n    it('creates and resets context', function () {\n        let context = decoder._getContext(1, 2, 3, 4);\n        expect(context._width).to.equal(3);\n        expect(context._height).to.equal(4);\n        expect(decoder._contexts).to.not.be.empty;\n        decoder._resetContext(1, 2, 3, 4);\n        expect(decoder._contexts).to.be.empty;\n    });\n\n    it('resets all contexts', function () {\n        decoder._getContext(0, 0, 1, 1);\n        decoder._getContext(2, 2, 1, 1);\n        expect(decoder._contexts).to.not.be.empty;\n        decoder._resetAllContexts();\n        expect(decoder._contexts).to.be.empty;\n    });\n\n    it('caches contexts', function () {\n        let c1 = decoder._getContext(1, 2, 3, 4);\n        c1.lastUsed = 1;\n        let c2 = decoder._getContext(1, 2, 3, 4);\n        c2.lastUsed = 2;\n        expect(Object.keys(decoder._contexts).length).to.equal(1);\n        expect(c1.lastUsed).to.equal(c2.lastUsed);\n    });\n\n    it('deletes oldest context', function () {\n        for (let i = 0; i < 65; ++i) {\n            let context = decoder._getContext(i, 0, 1, 1);\n            context.lastUsed = i;\n        }\n\n        expect(decoder._findOldestContextId()).to.equal('1,0,1,1');\n        expect(decoder._contexts[decoder._contextId(0, 0, 1, 1)]).to.be.undefined;\n        expect(decoder._contexts[decoder._contextId(1, 0, 1, 1)]).to.not.be.null;\n        expect(decoder._contexts[decoder._contextId(63, 0, 1, 1)]).to.not.be.null;\n        expect(decoder._contexts[decoder._contextId(64, 0, 1, 1)]).to.not.be.null;\n    });\n});\n\ndescribe('H.264 decoder functional test', function () {\n    let decoder;\n    let display;\n\n    before(FakeWebSocket.replace);\n    after(FakeWebSocket.restore);\n\n    beforeEach(function () {\n        if (!supportsWebCodecsH264Decode) {\n            this.skip();\n            return;\n        }\n        decoder = new H264Decoder();\n        display = new Display(document.createElement('canvas'));\n        display.resize(16, 16);\n    });\n\n    it('should handle H.264 rect', async function () {\n        let data = wrapRectData(redGreenBlue16x16Video, false, false);\n        let done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24);\n        expect(done).to.be.true;\n        await display.flush();\n        let targetData = createSolidColorFrameBuffer(0x0000ffff, 16, 16);\n        expect(display).to.have.displayed(targetData, almost);\n    });\n\n    it('should handle specific context reset', async function () {\n        let data = wrapRectData(redGreenBlue16x16Video, false, false);\n        let done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24);\n        expect(done).to.be.true;\n        await display.flush();\n        let targetData = createSolidColorFrameBuffer(0x0000ffff, 16, 16);\n        expect(display).to.have.displayed(targetData, almost);\n\n        data = wrapRectData([], true, false);\n        done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24);\n        expect(done).to.be.true;\n        await display.flush();\n\n        expect(decoder._contexts[decoder._contextId(0, 0, 16, 16)]._decoder).to.be.null;\n    });\n\n    it('should handle global context reset', async function () {\n        let data = wrapRectData(redGreenBlue16x16Video, false, false);\n        let done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24);\n        expect(done).to.be.true;\n        await display.flush();\n        let targetData = createSolidColorFrameBuffer(0x0000ffff, 16, 16);\n        expect(display).to.have.displayed(targetData, almost);\n\n        data = wrapRectData([], false, true);\n        done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24);\n        expect(done).to.be.true;\n        await display.flush();\n\n        expect(decoder._contexts[decoder._contextId(0, 0, 16, 16)]._decoder).to.be.null;\n    });\n});\n"
  },
  {
    "path": "tests/test.helper.js",
    "content": "import keysyms from '../core/input/keysymdef.js';\nimport * as KeyboardUtil from \"../core/input/util.js\";\n\ndescribe('Helpers', function () {\n    \"use strict\";\n\n    describe('keysyms.lookup', function () {\n        it('should map ASCII characters to keysyms', function () {\n            expect(keysyms.lookup('a'.charCodeAt())).to.be.equal(0x61);\n            expect(keysyms.lookup('A'.charCodeAt())).to.be.equal(0x41);\n        });\n        it('should map Latin-1 characters to keysyms', function () {\n            expect(keysyms.lookup('ø'.charCodeAt())).to.be.equal(0xf8);\n\n            expect(keysyms.lookup('é'.charCodeAt())).to.be.equal(0xe9);\n        });\n        it('should map characters that are in Windows-1252 but not in Latin-1 to keysyms', function () {\n            expect(keysyms.lookup('Š'.charCodeAt())).to.be.equal(0x01a9);\n        });\n        it('should map characters which aren\\'t in Latin1 *or* Windows-1252 to keysyms', function () {\n            expect(keysyms.lookup('ũ'.charCodeAt())).to.be.equal(0x03fd);\n        });\n        it('should map unknown codepoints to the Unicode range', function () {\n            expect(keysyms.lookup('\\n'.charCodeAt())).to.be.equal(0x100000a);\n            expect(keysyms.lookup('\\u262D'.charCodeAt())).to.be.equal(0x100262d);\n        });\n        // This requires very recent versions of most browsers... skipping for now\n        it.skip('should map UCS-4 codepoints to the Unicode range', function () {\n            //expect(keysyms.lookup('\\u{1F686}'.codePointAt())).to.be.equal(0x101f686);\n        });\n    });\n\n    describe('getKeycode', function () {\n        it('should pass through proper code', function () {\n            expect(KeyboardUtil.getKeycode({code: 'Semicolon'})).to.be.equal('Semicolon');\n        });\n        it('should map legacy values', function () {\n            expect(KeyboardUtil.getKeycode({code: ''})).to.be.equal('Unidentified');\n            expect(KeyboardUtil.getKeycode({code: 'OSLeft'})).to.be.equal('MetaLeft');\n        });\n        it('should map keyCode to code when possible', function () {\n            expect(KeyboardUtil.getKeycode({keyCode: 0x14})).to.be.equal('CapsLock');\n            expect(KeyboardUtil.getKeycode({keyCode: 0x5b})).to.be.equal('MetaLeft');\n            expect(KeyboardUtil.getKeycode({keyCode: 0x35})).to.be.equal('Digit5');\n            expect(KeyboardUtil.getKeycode({keyCode: 0x65})).to.be.equal('Numpad5');\n        });\n        it('should map keyCode left/right side', function () {\n            expect(KeyboardUtil.getKeycode({keyCode: 0x10, location: 1})).to.be.equal('ShiftLeft');\n            expect(KeyboardUtil.getKeycode({keyCode: 0x10, location: 2})).to.be.equal('ShiftRight');\n            expect(KeyboardUtil.getKeycode({keyCode: 0x11, location: 1})).to.be.equal('ControlLeft');\n            expect(KeyboardUtil.getKeycode({keyCode: 0x11, location: 2})).to.be.equal('ControlRight');\n        });\n        it('should map keyCode on numpad', function () {\n            expect(KeyboardUtil.getKeycode({keyCode: 0x0d, location: 0})).to.be.equal('Enter');\n            expect(KeyboardUtil.getKeycode({keyCode: 0x0d, location: 3})).to.be.equal('NumpadEnter');\n            expect(KeyboardUtil.getKeycode({keyCode: 0x23, location: 0})).to.be.equal('End');\n            expect(KeyboardUtil.getKeycode({keyCode: 0x23, location: 3})).to.be.equal('Numpad1');\n        });\n        it('should return Unidentified when it cannot map the keyCode', function () {\n            expect(KeyboardUtil.getKeycode({keycode: 0x42})).to.be.equal('Unidentified');\n        });\n\n        describe('Fix Meta on macOS', function () {\n            let origNavigator;\n            beforeEach(function () {\n                // window.navigator is a protected read-only property in many\n                // environments, so we need to redefine it whilst running these\n                // tests.\n                origNavigator = Object.getOwnPropertyDescriptor(window, \"navigator\");\n\n                Object.defineProperty(window, \"navigator\", {value: {}});\n                window.navigator.platform = \"Mac x86_64\";\n            });\n            afterEach(function () {\n                Object.defineProperty(window, \"navigator\", origNavigator);\n            });\n\n            it('should respect ContextMenu on modern browser', function () {\n                expect(KeyboardUtil.getKeycode({code: 'ContextMenu', keyCode: 0x5d})).to.be.equal('ContextMenu');\n            });\n            it('should translate legacy ContextMenu to MetaRight', function () {\n                expect(KeyboardUtil.getKeycode({keyCode: 0x5d})).to.be.equal('MetaRight');\n            });\n        });\n    });\n\n    describe('getKey', function () {\n        it('should prefer key', function () {\n            expect(KeyboardUtil.getKey({key: 'a', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.be.equal('a');\n        });\n        it('should map legacy values', function () {\n            expect(KeyboardUtil.getKey({key: 'OS'})).to.be.equal('Meta');\n            expect(KeyboardUtil.getKey({key: 'UIKeyInputLeftArrow'})).to.be.equal('ArrowLeft');\n        });\n        it('should handle broken Delete', function () {\n            expect(KeyboardUtil.getKey({key: '\\x00', code: 'NumpadDecimal'})).to.be.equal('Delete');\n        });\n        it('should use code if no key', function () {\n            expect(KeyboardUtil.getKey({code: 'NumpadBackspace'})).to.be.equal('Backspace');\n        });\n        it('should not use code fallback for character keys', function () {\n            expect(KeyboardUtil.getKey({code: 'KeyA'})).to.be.equal('Unidentified');\n            expect(KeyboardUtil.getKey({code: 'Digit1'})).to.be.equal('Unidentified');\n            expect(KeyboardUtil.getKey({code: 'Period'})).to.be.equal('Unidentified');\n            expect(KeyboardUtil.getKey({code: 'Numpad1'})).to.be.equal('Unidentified');\n        });\n        it('should use charCode if no key', function () {\n            expect(KeyboardUtil.getKey({charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.be.equal('Š');\n            // Broken Oculus browser\n            expect(KeyboardUtil.getKey({charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43, key: 'Unidentified'})).to.be.equal('Š');\n        });\n        it('should return Unidentified when it cannot map the key', function () {\n            expect(KeyboardUtil.getKey({keycode: 0x42})).to.be.equal('Unidentified');\n        });\n    });\n\n    describe('getKeysym', function () {\n        describe('Non-character keys', function () {\n            it('should recognize the right keys', function () {\n                expect(KeyboardUtil.getKeysym({key: 'Enter'})).to.be.equal(0xFF0D);\n                expect(KeyboardUtil.getKeysym({key: 'Backspace'})).to.be.equal(0xFF08);\n                expect(KeyboardUtil.getKeysym({key: 'Tab'})).to.be.equal(0xFF09);\n                expect(KeyboardUtil.getKeysym({key: 'Shift'})).to.be.equal(0xFFE1);\n                expect(KeyboardUtil.getKeysym({key: 'Control'})).to.be.equal(0xFFE3);\n                expect(KeyboardUtil.getKeysym({key: 'Alt'})).to.be.equal(0xFFE9);\n                expect(KeyboardUtil.getKeysym({key: 'Meta'})).to.be.equal(0xFFEB);\n                expect(KeyboardUtil.getKeysym({key: 'Escape'})).to.be.equal(0xFF1B);\n                expect(KeyboardUtil.getKeysym({key: 'ArrowUp'})).to.be.equal(0xFF52);\n            });\n            it('should map left/right side', function () {\n                expect(KeyboardUtil.getKeysym({key: 'Shift', location: 1})).to.be.equal(0xFFE1);\n                expect(KeyboardUtil.getKeysym({key: 'Shift', location: 2})).to.be.equal(0xFFE2);\n                expect(KeyboardUtil.getKeysym({key: 'Control', location: 1})).to.be.equal(0xFFE3);\n                expect(KeyboardUtil.getKeysym({key: 'Control', location: 2})).to.be.equal(0xFFE4);\n            });\n            it('should handle AltGraph', function () {\n                expect(KeyboardUtil.getKeysym({code: 'AltRight', key: 'Alt', location: 2})).to.be.equal(0xFFEA);\n                expect(KeyboardUtil.getKeysym({code: 'AltRight', key: 'AltGraph', location: 2})).to.be.equal(0xFE03);\n            });\n            it('should handle Windows key with incorrect location', function () {\n                expect(KeyboardUtil.getKeysym({key: 'Meta', location: 0})).to.be.equal(0xFFEC);\n            });\n            it('should handle Clear/NumLock key with incorrect location', function () {\n                this.skip(); // Broken because of Clear/NumLock override\n                expect(KeyboardUtil.getKeysym({key: 'Clear', code: 'NumLock', location: 3})).to.be.equal(0xFF0B);\n            });\n            it('should handle Meta/Windows distinction', function () {\n                expect(KeyboardUtil.getKeysym({code: 'AltLeft', key: 'Meta', location: 1})).to.be.equal(0xFFE7);\n                expect(KeyboardUtil.getKeysym({code: 'AltRight', key: 'Meta', location: 2})).to.be.equal(0xFFE8);\n                expect(KeyboardUtil.getKeysym({code: 'MetaLeft', key: 'Meta', location: 1})).to.be.equal(0xFFEB);\n                expect(KeyboardUtil.getKeysym({code: 'MetaRight', key: 'Meta', location: 2})).to.be.equal(0xFFEC);\n            });\n            it('should send NumLock even if key is Clear', function () {\n                expect(KeyboardUtil.getKeysym({key: 'Clear', code: 'NumLock'})).to.be.equal(0xFF7F);\n            });\n            it('should return null for unknown keys', function () {\n                expect(KeyboardUtil.getKeysym({key: 'Semicolon'})).to.be.null;\n                expect(KeyboardUtil.getKeysym({key: 'BracketRight'})).to.be.null;\n            });\n            it('should handle remappings', function () {\n                expect(KeyboardUtil.getKeysym({code: 'ControlLeft', key: 'Tab'})).to.be.equal(0xFF09);\n            });\n        });\n\n        describe('Numpad', function () {\n            it('should handle Numpad numbers', function () {\n                expect(KeyboardUtil.getKeysym({code: 'Digit5', key: '5', location: 0})).to.be.equal(0x0035);\n                expect(KeyboardUtil.getKeysym({code: 'Numpad5', key: '5', location: 3})).to.be.equal(0xFFB5);\n            });\n            it('should handle Numpad non-character keys', function () {\n                expect(KeyboardUtil.getKeysym({code: 'Home', key: 'Home', location: 0})).to.be.equal(0xFF50);\n                expect(KeyboardUtil.getKeysym({code: 'Numpad5', key: 'Home', location: 3})).to.be.equal(0xFF95);\n                expect(KeyboardUtil.getKeysym({code: 'Delete', key: 'Delete', location: 0})).to.be.equal(0xFFFF);\n                expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: 'Delete', location: 3})).to.be.equal(0xFF9F);\n            });\n            it('should handle Numpad Decimal key', function () {\n                expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: '.', location: 3})).to.be.equal(0xFFAE);\n                expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: ',', location: 3})).to.be.equal(0xFFAC);\n            });\n        });\n\n        describe('Japanese IM keys on Windows', function () {\n            let origNavigator;\n            beforeEach(function () {\n                // window.navigator is a protected read-only property in many\n                // environments, so we need to redefine it whilst running these\n                // tests.\n                origNavigator = Object.getOwnPropertyDescriptor(window, \"navigator\");\n\n                Object.defineProperty(window, \"navigator\", {value: {}});\n                window.navigator.platform = \"Windows\";\n            });\n\n            afterEach(function () {\n                Object.defineProperty(window, \"navigator\", origNavigator);\n            });\n\n            const keys = { 'Zenkaku': 0xff2a, 'Hankaku': 0xff2a,\n                           'Romaji': 0xff24, 'KanaMode': 0xff24 };\n            for (let [key, keysym] of Object.entries(keys)) {\n                it(`should fake combined key for ${key} on Windows`, function () {\n                    expect(KeyboardUtil.getKeysym({code: 'FakeIM', key: key})).to.be.equal(keysym);\n                });\n            }\n        });\n    });\n});\n"
  },
  {
    "path": "tests/test.hextile.js",
    "content": "import Websock from '../core/websock.js';\nimport Display from '../core/display.js';\n\nimport HextileDecoder from '../core/decoders/hextile.js';\n\nimport FakeWebSocket from './fake.websocket.js';\n\nfunction testDecodeRect(decoder, x, y, width, height, data, display, depth) {\n    let sock;\n    let done = false;\n\n    sock = new Websock;\n    sock.open(\"ws://example.com\");\n\n    sock.on('message', () => {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    });\n\n    // Empty messages are filtered at multiple layers, so we need to\n    // do a direct call\n    if (data.length === 0) {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    } else {\n        sock._websocket._receiveData(new Uint8Array(data));\n    }\n\n    display.flip();\n\n    return done;\n}\n\nfunction push32(arr, num) {\n    arr.push((num >> 24) & 0xFF,\n             (num >> 16) & 0xFF,\n             (num >>  8) & 0xFF,\n             num & 0xFF);\n}\n\ndescribe('Hextile decoder', function () {\n    let decoder;\n    let display;\n\n    before(FakeWebSocket.replace);\n    after(FakeWebSocket.restore);\n\n    beforeEach(function () {\n        decoder = new HextileDecoder();\n        display = new Display(document.createElement('canvas'));\n        display.resize(4, 4);\n    });\n\n    it('should handle a tile with fg, bg specified, normal subrects', function () {\n        let data = [];\n        data.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects\n        push32(data, 0x00ff0000); // becomes 00ff0000 --> #00FF00 bg color\n        data.push(0x00); // becomes 0000ff00 --> #0000FF fg color\n        data.push(0x00);\n        data.push(0xff);\n        data.push(0x00);\n        data.push(2); // 2 subrects\n        data.push(0); // x: 0, y: 0\n        data.push(1 | (1 << 4)); // width: 2, height: 2\n        data.push(2 | (2 << 4)); // x: 2, y: 2\n        data.push(1 | (1 << 4)); // width: 2, height: 2\n\n        let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle a raw tile', function () {\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        let data = [];\n        data.push(0x01); // raw\n        for (let i = 0; i < targetData.length; i += 4) {\n            data.push(targetData[i]);\n            data.push(targetData[i + 1]);\n            data.push(targetData[i + 2]);\n            // Last byte zero to test correct alpha handling\n            data.push(0);\n        }\n\n        let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle a tile with only bg specified (solid bg)', function () {\n        let data = [];\n        data.push(0x02);\n        push32(data, 0x00ff0000); // becomes 00ff0000 --> #00FF00 bg color\n\n        let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);\n\n        let expected = [];\n        for (let i = 0; i < 16; i++) {\n            push32(expected, 0x00ff00ff);\n        }\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(new Uint8Array(expected));\n    });\n\n    it('should handle a tile with only bg specified and an empty frame afterwards', function () {\n        // set the width so we can have two tiles\n        display.resize(8, 4);\n\n        let data = [];\n\n        // send a bg frame\n        data.push(0x02);\n        push32(data, 0x00ff0000); // becomes 00ff0000 --> #00FF00 bg color\n\n        // send an empty frame\n        data.push(0x00);\n\n        let done = testDecodeRect(decoder, 0, 0, 32, 4, data, display, 24);\n\n        let expected = [];\n        for (let i = 0; i < 16; i++) {\n            push32(expected, 0x00ff00ff);     // rect 1: solid\n        }\n        for (let i = 0; i < 16; i++) {\n            push32(expected, 0x00ff00ff);    // rect 2: same bkground color\n        }\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(new Uint8Array(expected));\n    });\n\n    it('should handle a tile with bg and coloured subrects', function () {\n        let data = [];\n        data.push(0x02 | 0x08 | 0x10); // bg spec, anysubrects, colouredsubrects\n        push32(data, 0x00ff0000); // becomes 00ff0000 --> #00FF00 bg color\n        data.push(2); // 2 subrects\n        data.push(0x00); // becomes 0000ff00 --> #0000FF fg color\n        data.push(0x00);\n        data.push(0xff);\n        data.push(0x00);\n        data.push(0); // x: 0, y: 0\n        data.push(1 | (1 << 4)); // width: 2, height: 2\n        data.push(0x00); // becomes 0000ff00 --> #0000FF fg color\n        data.push(0x00);\n        data.push(0xff);\n        data.push(0x00);\n        data.push(2 | (2 << 4)); // x: 2, y: 2\n        data.push(1 | (1 << 4)); // width: 2, height: 2\n\n        let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should carry over fg and bg colors from the previous tile if not specified', function () {\n        display.resize(4, 17);\n\n        let data = [];\n        data.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects\n        push32(data, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color\n        data.push(0x00); // becomes 0000ffff --> #0000FF fg color\n        data.push(0x00);\n        data.push(0xff);\n        data.push(0xff);\n        data.push(8); // 8 subrects\n        for (let i = 0; i < 4; i++) {\n            data.push((0 << 4) | (i * 4)); // x: 0, y: i*4\n            data.push(1 | (1 << 4)); // width: 2, height: 2\n            data.push((2 << 4) | (i * 4 + 2)); // x: 2, y: i * 4 + 2\n            data.push(1 | (1 << 4)); // width: 2, height: 2\n        }\n        data.push(0x08); // anysubrects\n        data.push(1); // 1 subrect\n        data.push(0); // x: 0, y: 0\n        data.push(1 | (1 << 4)); // width: 2, height: 2\n\n        let done = testDecodeRect(decoder, 0, 0, 4, 17, data, display, 24);\n\n        let targetData = [\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ];\n\n        let expected = [];\n        for (let i = 0; i < 4; i++) {\n            expected = expected.concat(targetData);\n        }\n        expected = expected.concat(targetData.slice(0, 16));\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(new Uint8Array(expected));\n    });\n\n    it('should fail on an invalid subencoding', function () {\n        let data = [45];  // an invalid subencoding\n        expect(() => testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24)).to.throw();\n    });\n\n    it('should handle empty rects', function () {\n        display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);\n        display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);\n        display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);\n\n        let done = testDecodeRect(decoder, 1, 2, 0, 0, [], display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n});\n"
  },
  {
    "path": "tests/test.inflator.js",
    "content": "import { deflateInit, deflate, Z_FULL_FLUSH } from \"../vendor/pako/lib/zlib/deflate.js\";\nimport ZStream from \"../vendor/pako/lib/zlib/zstream.js\";\nimport Inflator from \"../core/inflator.js\";\n\nfunction _deflator(data) {\n    let strm = new ZStream();\n\n    deflateInit(strm, 5);\n\n    /* eslint-disable camelcase */\n    strm.input = data;\n    strm.avail_in = strm.input.length;\n    strm.next_in = 0;\n    /* eslint-enable camelcase */\n\n    let chunks = [];\n    let totalLen = 0;\n    while (strm.avail_in > 0) {\n        /* eslint-disable camelcase */\n        strm.output = new Uint8Array(1024 * 10 * 10);\n        strm.avail_out = strm.output.length;\n        strm.next_out = 0;\n        /* eslint-enable camelcase */\n\n        let ret = deflate(strm, Z_FULL_FLUSH);\n\n        // Check that return code is not an error\n        expect(ret).to.be.greaterThan(-1);\n\n        let chunk = new Uint8Array(strm.output.buffer, 0, strm.next_out);\n        totalLen += chunk.length;\n        chunks.push(chunk);\n    }\n\n    // Combine chunks into a single data\n\n    let outData = new Uint8Array(totalLen);\n    let offset = 0;\n\n    for (let i = 0; i < chunks.length; i++) {\n        outData.set(chunks[i], offset);\n        offset += chunks[i].length;\n    }\n\n    return outData;\n}\n\ndescribe('Inflate data', function () {\n\n    it('should be able to inflate messages', function () {\n        let inflator = new Inflator();\n\n        let text = \"123asdf\";\n        let preText = new Uint8Array(text.length);\n        for (let i = 0; i < preText.length; i++) {\n            preText[i] = text.charCodeAt(i);\n        }\n\n        let compText = _deflator(preText);\n\n        inflator.setInput(compText);\n        let inflatedText = inflator.inflate(preText.length);\n\n        expect(inflatedText).to.array.equal(preText);\n\n    });\n\n    it('should be able to inflate large messages', function () {\n        let inflator = new Inflator();\n\n        /* Generate a big string with random characters. Used because\n           repetition of letters might be deflated more effectively than\n           random ones. */\n        let text = \"\";\n        let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n        for (let i = 0; i < 300000; i++) {\n            text += characters.charAt(Math.floor(Math.random() * characters.length));\n        }\n\n        let preText = new Uint8Array(text.length);\n        for (let i = 0; i < preText.length; i++) {\n            preText[i] = text.charCodeAt(i);\n        }\n\n        let compText = _deflator(preText);\n\n        //Check that the compressed size is expected size\n        expect(compText.length).to.be.greaterThan((1024 * 10 * 10) * 2);\n\n        inflator.setInput(compText);\n        let inflatedText = inflator.inflate(preText.length);\n\n        expect(inflatedText).to.array.equal(preText);\n    });\n\n    it('should throw an error on insufficient data', function () {\n        let inflator = new Inflator();\n\n        let text = \"123asdf\";\n        let preText = new Uint8Array(text.length);\n        for (let i = 0; i < preText.length; i++) {\n            preText[i] = text.charCodeAt(i);\n        }\n\n        let compText = _deflator(preText);\n\n        inflator.setInput(compText);\n        expect(() => inflator.inflate(preText.length * 2)).to.throw();\n    });\n});\n"
  },
  {
    "path": "tests/test.int.js",
    "content": "import { toUnsigned32bit, toSigned32bit } from '../core/util/int.js';\n\ndescribe('Integer casting', function () {\n    it('should cast unsigned to signed', function () {\n        let expected = 4294967286;\n        expect(toUnsigned32bit(-10)).to.equal(expected);\n    });\n\n    it('should cast signed to unsigned', function () {\n        let expected = -10;\n        expect(toSigned32bit(4294967286)).to.equal(expected);\n    });\n});\n"
  },
  {
    "path": "tests/test.jpeg.js",
    "content": "import Websock from '../core/websock.js';\nimport Display from '../core/display.js';\n\nimport JPEGDecoder from '../core/decoders/jpeg.js';\n\nimport FakeWebSocket from './fake.websocket.js';\n\nfunction testDecodeRect(decoder, x, y, width, height, data, display, depth) {\n    let sock;\n    let done = false;\n\n    sock = new Websock;\n    sock.open(\"ws://example.com\");\n\n    sock.on('message', () => {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    });\n\n    // Empty messages are filtered at multiple layers, so we need to\n    // do a direct call\n    if (data.length === 0) {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    } else {\n        sock._websocket._receiveData(new Uint8Array(data));\n    }\n\n    display.flip();\n\n    return done;\n}\n\ndescribe('JPEG decoder', function () {\n    let decoder;\n    let display;\n\n    before(FakeWebSocket.replace);\n    after(FakeWebSocket.restore);\n\n    beforeEach(function () {\n        decoder = new JPEGDecoder();\n        display = new Display(document.createElement('canvas'));\n        display.resize(4, 4);\n    });\n\n    it('should handle JPEG rects', async function () {\n        let data = [\n            // JPEG data\n            0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46,\n            0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x01, 0x2c,\n            0x01, 0x2c, 0x00, 0x42, 0xff, 0xdb, 0x00, 0x43,\n            0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03,\n            0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x05,\n            0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0a, 0x07,\n            0x07, 0x06, 0x08, 0x0c, 0x0a, 0x0c, 0x0c, 0x0b,\n            0x0a, 0x0b, 0x0b, 0x0d, 0x0e, 0x12, 0x10, 0x0d,\n            0x0e, 0x11, 0x0e, 0x0b, 0x0b, 0x10, 0x16, 0x10,\n            0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0c, 0x0f,\n            0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15,\n            0x14, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x03, 0x04,\n            0x04, 0x05, 0x04, 0x05, 0x09, 0x05, 0x05, 0x09,\n            0x14, 0x0d, 0x0b, 0x0d, 0x14, 0x14, 0x14, 0x14,\n            0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,\n            0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,\n            0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,\n            0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,\n            0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,\n            0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0xff, 0xc0,\n            0x00, 0x11, 0x08, 0x00, 0x04, 0x00, 0x04, 0x03,\n            0x01, 0x11, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11,\n            0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01,\n            0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,\n            0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,\n            0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00,\n            0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05,\n            0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01,\n            0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21,\n            0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22,\n            0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23,\n            0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24,\n            0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17,\n            0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29,\n            0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,\n            0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a,\n            0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a,\n            0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,\n            0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a,\n            0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a,\n            0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,\n            0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8,\n            0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7,\n            0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6,\n            0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5,\n            0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3,\n            0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1,\n            0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9,\n            0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00, 0x03,\n            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,\n            0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,\n            0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00,\n            0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07,\n            0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00,\n            0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,\n            0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13,\n            0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1,\n            0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15,\n            0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1,\n            0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, 0x27,\n            0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39,\n            0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,\n            0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,\n            0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,\n            0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,\n            0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88,\n            0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,\n            0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6,\n            0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5,\n            0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4,\n            0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3,\n            0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2,\n            0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,\n            0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9,\n            0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00,\n            0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf9,\n            0xf7, 0xfb, 0x67, 0x56, 0xff, 0x00, 0x9f, 0xf8,\n            0x3f, 0xf0, 0x51, 0xa7, 0xff, 0x00, 0xf2, 0x3d,\n            0x7e, 0x6f, 0xfd, 0xab, 0x94, 0x7f, 0xd0, 0x9a,\n            0x8f, 0xfe, 0x0d, 0xc7, 0x7f, 0xf3, 0x61, 0xfd,\n            0xa7, 0xff, 0x00, 0x10, 0x77, 0x0d, 0xff, 0x00,\n            0x43, 0xec, 0xcf, 0xff, 0x00, 0x0b, 0xab, 0x1f,\n            0xff, 0xd9,\n        ];\n\n        let decodeDone = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);\n        expect(decodeDone).to.be.true;\n\n        let targetData = new Uint8Array([\n            0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255\n        ]);\n\n        // Browsers have rounding errors, so we need an approximate\n        // comparing function\n        function almost(a, b) {\n            let diff = Math.abs(a - b);\n            return diff < 5;\n        }\n\n        await display.flush();\n        expect(display).to.have.displayed(targetData, almost);\n    });\n\n    it('should handle JPEG rects without Huffman and quantification tables', async function () {\n        let data1 = [\n            // JPEG data\n            0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46,\n            0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x01, 0x2c,\n            0x01, 0x2c, 0x00, 0x42, 0xff, 0xdb, 0x00, 0x43,\n            0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03,\n            0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x05,\n            0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0a, 0x07,\n            0x07, 0x06, 0x08, 0x0c, 0x0a, 0x0c, 0x0c, 0x0b,\n            0x0a, 0x0b, 0x0b, 0x0d, 0x0e, 0x12, 0x10, 0x0d,\n            0x0e, 0x11, 0x0e, 0x0b, 0x0b, 0x10, 0x16, 0x10,\n            0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0c, 0x0f,\n            0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15,\n            0x14, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x03, 0x04,\n            0x04, 0x05, 0x04, 0x05, 0x09, 0x05, 0x05, 0x09,\n            0x14, 0x0d, 0x0b, 0x0d, 0x14, 0x14, 0x14, 0x14,\n            0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,\n            0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,\n            0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,\n            0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,\n            0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,\n            0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0xff, 0xc0,\n            0x00, 0x11, 0x08, 0x00, 0x04, 0x00, 0x04, 0x03,\n            0x01, 0x11, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11,\n            0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01,\n            0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,\n            0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,\n            0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00,\n            0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05,\n            0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01,\n            0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21,\n            0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22,\n            0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23,\n            0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24,\n            0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17,\n            0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29,\n            0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,\n            0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a,\n            0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a,\n            0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,\n            0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a,\n            0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a,\n            0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,\n            0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8,\n            0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7,\n            0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6,\n            0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5,\n            0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3,\n            0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1,\n            0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9,\n            0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00, 0x03,\n            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,\n            0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,\n            0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00,\n            0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07,\n            0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00,\n            0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,\n            0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13,\n            0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1,\n            0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15,\n            0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1,\n            0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, 0x27,\n            0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39,\n            0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,\n            0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,\n            0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,\n            0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,\n            0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88,\n            0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,\n            0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6,\n            0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5,\n            0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4,\n            0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3,\n            0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2,\n            0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,\n            0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9,\n            0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00,\n            0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf9,\n            0xf7, 0xfb, 0x67, 0x56, 0xff, 0x00, 0x9f, 0xf8,\n            0x3f, 0xf0, 0x51, 0xa7, 0xff, 0x00, 0xf2, 0x3d,\n            0x7e, 0x6f, 0xfd, 0xab, 0x94, 0x7f, 0xd0, 0x9a,\n            0x8f, 0xfe, 0x0d, 0xc7, 0x7f, 0xf3, 0x61, 0xfd,\n            0xa7, 0xff, 0x00, 0x10, 0x77, 0x0d, 0xff, 0x00,\n            0x43, 0xec, 0xcf, 0xff, 0x00, 0x0b, 0xab, 0x1f,\n            0xff, 0xd9,\n        ];\n\n        let decodeDone;\n\n        decodeDone = testDecodeRect(decoder, 0, 0, 4, 4, data1, display, 24);\n        expect(decodeDone).to.be.true;\n\n        display.fillRect(0, 0, 4, 4, [128, 128, 128, 255]);\n\n        let data2 = [\n            // JPEG data\n            0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46,\n            0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x01, 0x2c,\n            0x01, 0x2c, 0x00, 0x73, 0xff, 0xc0, 0x00, 0x11,\n            0x08, 0x00, 0x04, 0x00, 0x04, 0x03, 0x01, 0x11,\n            0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff,\n            0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11,\n            0x03, 0x11, 0x00, 0x3f, 0x00, 0xf9, 0xf7, 0xfb,\n            0x67, 0x56, 0xff, 0x00, 0x9f, 0xf8, 0x3f, 0xf0,\n            0x51, 0xa7, 0xff, 0x00, 0xf2, 0x3d, 0x7e, 0x6f,\n            0xfd, 0xab, 0x94, 0x7f, 0xd0, 0x9a, 0x8f, 0xfe,\n            0x0d, 0xc7, 0x7f, 0xf3, 0x61, 0xfd, 0xa7, 0xff,\n            0x00, 0x10, 0x77, 0x0d, 0xff, 0x00, 0x43, 0xec,\n            0xcf, 0xff, 0x00, 0x0b, 0xab, 0x1f, 0xff, 0xd9,\n        ];\n\n        decodeDone = testDecodeRect(decoder, 0, 0, 4, 4, data2, display, 24);\n        expect(decodeDone).to.be.true;\n\n        let targetData = new Uint8Array([\n            0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255\n        ]);\n\n        // Browsers have rounding errors, so we need an approximate\n        // comparing function\n        function almost(a, b) {\n            let diff = Math.abs(a - b);\n            return diff < 5;\n        }\n\n        await display.flush();\n        expect(display).to.have.displayed(targetData, almost);\n    });\n});\n"
  },
  {
    "path": "tests/test.keyboard.js",
    "content": "import Keyboard from '../core/input/keyboard.js';\n\ndescribe('Key event handling', function () {\n    \"use strict\";\n\n    // The real KeyboardEvent constructor might not work everywhere we\n    // want to run these tests\n    function keyevent(typeArg, KeyboardEventInit) {\n        const e = { type: typeArg };\n        for (let key in KeyboardEventInit) {\n            e[key] = KeyboardEventInit[key];\n        }\n        e.stopPropagation = sinon.spy();\n        e.preventDefault = sinon.spy();\n        e.getModifierState = function (key) {\n            return e[key];\n        };\n\n        return e;\n    }\n\n    describe('Decode keyboard events', function () {\n        it('should decode keydown events', function (done) {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = (keysym, code, down) => {\n                expect(keysym).to.be.equal(0x61);\n                expect(code).to.be.equal('KeyA');\n                expect(down).to.be.equal(true);\n                done();\n            };\n            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));\n        });\n        it('should decode keyup events', function (done) {\n            let calls = 0;\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = (keysym, code, down) => {\n                expect(keysym).to.be.equal(0x61);\n                expect(code).to.be.equal('KeyA');\n                if (calls++ === 1) {\n                    expect(down).to.be.equal(false);\n                    done();\n                }\n            };\n            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));\n            kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'}));\n        });\n    });\n\n    describe('Fake keyup', function () {\n        it('should fake keyup events for virtual keyboards', function (done) {\n            let count = 0;\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = (keysym, code, down) => {\n                switch (count++) {\n                    case 0:\n                        expect(keysym).to.be.equal(0x61);\n                        expect(code).to.be.equal('Unidentified');\n                        expect(down).to.be.equal(true);\n                        break;\n                    case 1:\n                        expect(keysym).to.be.equal(0x61);\n                        expect(code).to.be.equal('Unidentified');\n                        expect(down).to.be.equal(false);\n                        done();\n                }\n            };\n            kbd._handleKeyDown(keyevent('keydown', {code: 'Unidentified', key: 'a'}));\n        });\n    });\n\n    describe('Track key state', function () {\n        it('should send release using the same keysym as the press', function (done) {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = (keysym, code, down) => {\n                expect(keysym).to.be.equal(0x61);\n                expect(code).to.be.equal('KeyA');\n                if (!down) {\n                    done();\n                }\n            };\n            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));\n            kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'b'}));\n        });\n        it('should send the same keysym for multiple presses', function () {\n            let count = 0;\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = (keysym, code, down) => {\n                expect(keysym).to.be.equal(0x61);\n                expect(code).to.be.equal('KeyA');\n                expect(down).to.be.equal(true);\n                count++;\n            };\n            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));\n            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'b'}));\n            expect(count).to.be.equal(2);\n        });\n        it('should do nothing on keyup events if no keys are down', function () {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'}));\n            expect(kbd.onkeyevent).to.not.have.been.called;\n        });\n\n        describe('Legacy events', function () {\n            it('should track keys using keyCode if no code', function (done) {\n                const kbd = new Keyboard(document);\n                kbd.onkeyevent = (keysym, code, down) => {\n                    expect(keysym).to.be.equal(0x61);\n                    expect(code).to.be.equal('Platform65');\n                    if (!down) {\n                        done();\n                    }\n                };\n                kbd._handleKeyDown(keyevent('keydown', {keyCode: 65, key: 'a'}));\n                kbd._handleKeyUp(keyevent('keyup', {keyCode: 65, key: 'b'}));\n            });\n            it('should ignore compositing code', function () {\n                const kbd = new Keyboard(document);\n                kbd.onkeyevent = (keysym, code, down) => {\n                    expect(keysym).to.be.equal(0x61);\n                    expect(code).to.be.equal('Unidentified');\n                };\n                kbd._handleKeyDown(keyevent('keydown', {keyCode: 229, key: 'a'}));\n            });\n            it('should track keys using keyIdentifier if no code', function (done) {\n                const kbd = new Keyboard(document);\n                kbd.onkeyevent = (keysym, code, down) => {\n                    expect(keysym).to.be.equal(0x61);\n                    expect(code).to.be.equal('Platform65');\n                    if (!down) {\n                        done();\n                    }\n                };\n                kbd._handleKeyDown(keyevent('keydown', {keyIdentifier: 'U+0041', key: 'a'}));\n                kbd._handleKeyUp(keyevent('keyup', {keyIdentifier: 'U+0041', key: 'b'}));\n            });\n        });\n    });\n\n    describe('Shuffle modifiers on macOS', function () {\n        let origNavigator;\n        beforeEach(function () {\n            // window.navigator is a protected read-only property in many\n            // environments, so we need to redefine it whilst running these\n            // tests.\n            origNavigator = Object.getOwnPropertyDescriptor(window, \"navigator\");\n\n            Object.defineProperty(window, \"navigator\", {value: {}});\n            window.navigator.platform = \"Mac x86_64\";\n        });\n        afterEach(function () {\n            Object.defineProperty(window, \"navigator\", origNavigator);\n        });\n\n        it('should change Alt to AltGraph', function () {\n            let count = 0;\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = (keysym, code, down) => {\n                switch (count++) {\n                    case 0:\n                        expect(keysym).to.be.equal(0xFF7E);\n                        expect(code).to.be.equal('AltLeft');\n                        break;\n                    case 1:\n                        expect(keysym).to.be.equal(0xFE03);\n                        expect(code).to.be.equal('AltRight');\n                        break;\n                }\n            };\n            kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt', location: 1}));\n            kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2}));\n            expect(count).to.be.equal(2);\n        });\n        it('should change left Super to Alt', function (done) {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = (keysym, code, down) => {\n                expect(keysym).to.be.equal(0xFFE9);\n                expect(code).to.be.equal('MetaLeft');\n                done();\n            };\n            kbd._handleKeyDown(keyevent('keydown', {code: 'MetaLeft', key: 'Meta', location: 1}));\n        });\n        it('should change right Super to left Super', function (done) {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = (keysym, code, down) => {\n                expect(keysym).to.be.equal(0xFFEB);\n                expect(code).to.be.equal('MetaRight');\n                done();\n            };\n            kbd._handleKeyDown(keyevent('keydown', {code: 'MetaRight', key: 'Meta', location: 2}));\n        });\n    });\n\n    describe('Meta key combination on iOS and macOS', function () {\n        let origNavigator;\n        beforeEach(function () {\n            // window.navigator is a protected read-only property in many\n            // environments, so we need to redefine it whilst running these\n            // tests.\n            origNavigator = Object.getOwnPropertyDescriptor(window, \"navigator\");\n\n            Object.defineProperty(window, \"navigator\", {value: {}});\n            if (window.navigator.platform !== undefined) {\n                // Object.defineProperty() doesn't work properly in old\n                // versions of Chrome\n                this.skip();\n            }\n        });\n\n        afterEach(function () {\n            if (origNavigator !== undefined) {\n                Object.defineProperty(window, \"navigator\", origNavigator);\n            }\n        });\n\n        it('should send keyup when meta key is pressed on iOS', function () {\n            window.navigator.platform = \"iPad\";\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n\n            kbd._handleKeyDown(keyevent('keydown', {code: 'MetaRight', key: 'Meta', location: 2, metaKey: true}));\n            expect(kbd.onkeyevent).to.have.been.calledOnce;\n            kbd.onkeyevent.resetHistory();\n\n            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a', metaKey: true}));\n            expect(kbd.onkeyevent).to.have.been.calledTwice;\n            expect(kbd.onkeyevent).to.have.been.calledWith(0x61, \"KeyA\", true);\n            expect(kbd.onkeyevent).to.have.been.calledWith(0x61, \"KeyA\", false);\n            kbd.onkeyevent.resetHistory();\n\n            kbd._handleKeyUp(keyevent('keyup', {code: 'MetaRight', key: 'Meta', location: 2, metaKey: true}));\n            expect(kbd.onkeyevent).to.have.been.calledOnce;\n        });\n\n        it('should send keyup when meta key is pressed on macOS', function () {\n            window.navigator.platform = \"Mac\";\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n\n            kbd._handleKeyDown(keyevent('keydown', {code: 'MetaRight', key: 'Meta', location: 2, metaKey: true}));\n            expect(kbd.onkeyevent).to.have.been.calledOnce;\n            kbd.onkeyevent.resetHistory();\n\n            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a', metaKey: true}));\n            expect(kbd.onkeyevent).to.have.been.calledTwice;\n            expect(kbd.onkeyevent).to.have.been.calledWith(0x61, \"KeyA\", true);\n            expect(kbd.onkeyevent).to.have.been.calledWith(0x61, \"KeyA\", false);\n            kbd.onkeyevent.resetHistory();\n\n            kbd._handleKeyUp(keyevent('keyup', {code: 'MetaRight', key: 'Meta', location: 2, metaKey: true}));\n            expect(kbd.onkeyevent).to.have.been.calledOnce;\n        });\n    });\n\n    describe('Caps Lock on iOS and macOS', function () {\n        let origNavigator;\n        beforeEach(function () {\n            // window.navigator is a protected read-only property in many\n            // environments, so we need to redefine it whilst running these\n            // tests.\n            origNavigator = Object.getOwnPropertyDescriptor(window, \"navigator\");\n\n            Object.defineProperty(window, \"navigator\", {value: {}});\n        });\n\n        afterEach(function () {\n            Object.defineProperty(window, \"navigator\", origNavigator);\n        });\n\n        it('should toggle caps lock on key press on iOS', function () {\n            window.navigator.platform = \"iPad\";\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyDown(keyevent('keydown', {code: 'CapsLock', key: 'CapsLock'}));\n\n            expect(kbd.onkeyevent).to.have.been.calledTwice;\n            expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xFFE5, \"CapsLock\", true);\n            expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xFFE5, \"CapsLock\", false);\n        });\n\n        it('should toggle caps lock on key press on mac', function () {\n            window.navigator.platform = \"Mac\";\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyDown(keyevent('keydown', {code: 'CapsLock', key: 'CapsLock'}));\n\n            expect(kbd.onkeyevent).to.have.been.calledTwice;\n            expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xFFE5, \"CapsLock\", true);\n            expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xFFE5, \"CapsLock\", false);\n        });\n\n        it('should toggle caps lock on key release on iOS', function () {\n            window.navigator.platform = \"iPad\";\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyUp(keyevent('keyup', {code: 'CapsLock', key: 'CapsLock'}));\n\n            expect(kbd.onkeyevent).to.have.been.calledTwice;\n            expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xFFE5, \"CapsLock\", true);\n            expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xFFE5, \"CapsLock\", false);\n        });\n\n        it('should toggle caps lock on key release on mac', function () {\n            window.navigator.platform = \"Mac\";\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyUp(keyevent('keyup', {code: 'CapsLock', key: 'CapsLock'}));\n\n            expect(kbd.onkeyevent).to.have.been.calledTwice;\n            expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xFFE5, \"CapsLock\", true);\n            expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xFFE5, \"CapsLock\", false);\n        });\n    });\n\n    describe('Modifier status info', function () {\n        let origNavigator;\n        beforeEach(function () {\n            // window.navigator is a protected read-only property in many\n            // environments, so we need to redefine it whilst running these\n            // tests.\n            origNavigator = Object.getOwnPropertyDescriptor(window, \"navigator\");\n\n            Object.defineProperty(window, \"navigator\", {value: {}});\n        });\n\n        afterEach(function () {\n            Object.defineProperty(window, \"navigator\", origNavigator);\n        });\n\n        it('should provide caps lock state', function () {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'A', NumLock: false, CapsLock: true}));\n\n            expect(kbd.onkeyevent).to.have.been.calledOnce;\n            expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0x41, \"KeyA\", true, false, true);\n        });\n\n        it('should provide num lock state', function () {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'A', NumLock: true, CapsLock: false}));\n\n            expect(kbd.onkeyevent).to.have.been.calledOnce;\n            expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0x41, \"KeyA\", true, true, false);\n        });\n\n        it('should have no num lock state on mac', function () {\n            window.navigator.platform = \"Mac\";\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'A', NumLock: false, CapsLock: true}));\n\n            expect(kbd.onkeyevent).to.have.been.calledOnce;\n            expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0x41, \"KeyA\", true, null, true);\n        });\n    });\n\n    describe('Japanese IM keys on Windows', function () {\n        let origNavigator;\n        beforeEach(function () {\n            // window.navigator is a protected read-only property in many\n            // environments, so we need to redefine it whilst running these\n            // tests.\n            origNavigator = Object.getOwnPropertyDescriptor(window, \"navigator\");\n\n            Object.defineProperty(window, \"navigator\", {value: {}});\n            window.navigator.platform = \"Windows\";\n        });\n\n        afterEach(function () {\n            Object.defineProperty(window, \"navigator\", origNavigator);\n        });\n\n        const keys = { 'Zenkaku': 0xff2a, 'Hankaku': 0xff2a,\n                       'Alphanumeric': 0xff30, 'Katakana': 0xff26,\n                       'Hiragana': 0xff25, 'Romaji': 0xff24,\n                       'KanaMode': 0xff24 };\n        for (let [key, keysym] of Object.entries(keys)) {\n            it(`should fake key release for ${key} on Windows`, function () {\n                let kbd = new Keyboard(document);\n                kbd.onkeyevent = sinon.spy();\n                kbd._handleKeyDown(keyevent('keydown', {code: 'FakeIM', key: key}));\n\n                expect(kbd.onkeyevent).to.have.been.calledTwice;\n                expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(keysym, \"FakeIM\", true);\n                expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(keysym, \"FakeIM\", false);\n            });\n        }\n    });\n\n    describe('Escape AltGraph on Windows', function () {\n        let origNavigator;\n        beforeEach(function () {\n            // window.navigator is a protected read-only property in many\n            // environments, so we need to redefine it whilst running these\n            // tests.\n            origNavigator = Object.getOwnPropertyDescriptor(window, \"navigator\");\n\n            Object.defineProperty(window, \"navigator\", {value: {}});\n            window.navigator.platform = \"Windows x86_64\";\n\n            this.clock = sinon.useFakeTimers();\n        });\n        afterEach(function () {\n            Object.defineProperty(window, \"navigator\", origNavigator);\n            if (this.clock !== undefined) {\n                this.clock.restore();\n            }\n        });\n\n        it('should supress ControlLeft until it knows if it is AltGr', function () {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1}));\n            expect(kbd.onkeyevent).to.not.have.been.called;\n        });\n\n        it('should not trigger on repeating ControlLeft', function () {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1}));\n            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1}));\n            expect(kbd.onkeyevent).to.have.been.calledTwice;\n            expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, \"ControlLeft\", true);\n            expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffe3, \"ControlLeft\", true);\n        });\n\n        it('should not supress ControlRight', function () {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlRight', key: 'Control', location: 2}));\n            expect(kbd.onkeyevent).to.have.been.calledOnce;\n            expect(kbd.onkeyevent).to.have.been.calledWith(0xffe4, \"ControlRight\", true);\n        });\n\n        it('should release ControlLeft after 100 ms', function () {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1}));\n            expect(kbd.onkeyevent).to.not.have.been.called;\n            this.clock.tick(100);\n            expect(kbd.onkeyevent).to.have.been.calledOnce;\n            expect(kbd.onkeyevent).to.have.been.calledWith(0xffe3, \"ControlLeft\", true);\n        });\n\n        it('should release ControlLeft on other key press', function () {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1}));\n            expect(kbd.onkeyevent).to.not.have.been.called;\n            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));\n            expect(kbd.onkeyevent).to.have.been.calledTwice;\n            expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, \"ControlLeft\", true);\n            expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0x61, \"KeyA\", true);\n\n            // Check that the timer is properly dead\n            kbd.onkeyevent.resetHistory();\n            this.clock.tick(100);\n            expect(kbd.onkeyevent).to.not.have.been.called;\n        });\n\n        it('should release ControlLeft on other key release', function () {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));\n            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1}));\n            expect(kbd.onkeyevent).to.have.been.calledOnce;\n            expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0x61, \"KeyA\", true);\n            kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'}));\n            expect(kbd.onkeyevent).to.have.been.calledThrice;\n            expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffe3, \"ControlLeft\", true);\n            expect(kbd.onkeyevent.thirdCall).to.have.been.calledWith(0x61, \"KeyA\", false);\n\n            // Check that the timer is properly dead\n            kbd.onkeyevent.resetHistory();\n            this.clock.tick(100);\n            expect(kbd.onkeyevent).to.not.have.been.called;\n        });\n\n        it('should release ControlLeft on blur', function () {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1}));\n            expect(kbd.onkeyevent).to.not.have.been.called;\n            kbd._allKeysUp();\n            expect(kbd.onkeyevent).to.have.been.calledTwice;\n            expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, \"ControlLeft\", true);\n            expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffe3, \"ControlLeft\", false);\n\n            // Check that the timer is properly dead\n            kbd.onkeyevent.resetHistory();\n            this.clock.tick(100);\n            expect(kbd.onkeyevent).to.not.have.been.called;\n        });\n\n        it('should generate AltGraph for quick Ctrl+Alt sequence', function () {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1, timeStamp: Date.now()}));\n            this.clock.tick(20);\n            kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2, timeStamp: Date.now()}));\n            expect(kbd.onkeyevent).to.have.been.calledOnce;\n            expect(kbd.onkeyevent).to.have.been.calledWith(0xfe03, 'AltRight', true);\n\n            // Check that the timer is properly dead\n            kbd.onkeyevent.resetHistory();\n            this.clock.tick(100);\n            expect(kbd.onkeyevent).to.not.have.been.called;\n        });\n\n        it('should generate Ctrl, Alt for slow Ctrl+Alt sequence', function () {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1, timeStamp: Date.now()}));\n            this.clock.tick(60);\n            kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2, timeStamp: Date.now()}));\n            expect(kbd.onkeyevent).to.have.been.calledTwice;\n            expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, \"ControlLeft\", true);\n            expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffea, \"AltRight\", true);\n\n            // Check that the timer is properly dead\n            kbd.onkeyevent.resetHistory();\n            this.clock.tick(100);\n            expect(kbd.onkeyevent).to.not.have.been.called;\n        });\n\n        it('should generate AltGraph for quick Ctrl+AltGraph sequence', function () {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1, timeStamp: Date.now()}));\n            this.clock.tick(20);\n            kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'AltGraph', location: 2, timeStamp: Date.now()}));\n            expect(kbd.onkeyevent).to.have.been.calledOnce;\n            expect(kbd.onkeyevent).to.have.been.calledWith(0xfe03, 'AltRight', true);\n\n            // Check that the timer is properly dead\n            kbd.onkeyevent.resetHistory();\n            this.clock.tick(100);\n            expect(kbd.onkeyevent).to.not.have.been.called;\n        });\n\n        it('should generate Ctrl, AltGraph for slow Ctrl+AltGraph sequence', function () {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1, timeStamp: Date.now()}));\n            this.clock.tick(60);\n            kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'AltGraph', location: 2, timeStamp: Date.now()}));\n            expect(kbd.onkeyevent).to.have.been.calledTwice;\n            expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, \"ControlLeft\", true);\n            expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xfe03, \"AltRight\", true);\n\n            // Check that the timer is properly dead\n            kbd.onkeyevent.resetHistory();\n            this.clock.tick(100);\n            expect(kbd.onkeyevent).to.not.have.been.called;\n        });\n\n        it('should pass through single Alt', function () {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2}));\n            expect(kbd.onkeyevent).to.have.been.calledOnce;\n            expect(kbd.onkeyevent).to.have.been.calledWith(0xffea, 'AltRight', true);\n        });\n\n        it('should pass through single AltGr', function () {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n            kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'AltGraph', location: 2}));\n            expect(kbd.onkeyevent).to.have.been.calledOnce;\n            expect(kbd.onkeyevent).to.have.been.calledWith(0xfe03, 'AltRight', true);\n        });\n    });\n\n    describe('Missing Shift keyup on Windows', function () {\n        let origNavigator;\n        beforeEach(function () {\n            // window.navigator is a protected read-only property in many\n            // environments, so we need to redefine it whilst running these\n            // tests.\n            origNavigator = Object.getOwnPropertyDescriptor(window, \"navigator\");\n\n            Object.defineProperty(window, \"navigator\", {value: {}});\n            window.navigator.platform = \"Windows x86_64\";\n\n            this.clock = sinon.useFakeTimers();\n        });\n        afterEach(function () {\n            Object.defineProperty(window, \"navigator\", origNavigator);\n            if (this.clock !== undefined) {\n                this.clock.restore();\n            }\n        });\n\n        it('should fake a left Shift keyup', function () {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n\n            kbd._handleKeyDown(keyevent('keydown', {code: 'ShiftLeft', key: 'Shift', location: 1}));\n            expect(kbd.onkeyevent).to.have.been.calledOnce;\n            expect(kbd.onkeyevent).to.have.been.calledWith(0xffe1, 'ShiftLeft', true);\n            kbd.onkeyevent.resetHistory();\n\n            kbd._handleKeyDown(keyevent('keydown', {code: 'ShiftRight', key: 'Shift', location: 2}));\n            expect(kbd.onkeyevent).to.have.been.calledOnce;\n            expect(kbd.onkeyevent).to.have.been.calledWith(0xffe2, 'ShiftRight', true);\n            kbd.onkeyevent.resetHistory();\n\n            kbd._handleKeyUp(keyevent('keyup', {code: 'ShiftLeft', key: 'Shift', location: 1}));\n            expect(kbd.onkeyevent).to.have.been.calledTwice;\n            expect(kbd.onkeyevent).to.have.been.calledWith(0xffe2, 'ShiftRight', false);\n            expect(kbd.onkeyevent).to.have.been.calledWith(0xffe1, 'ShiftLeft', false);\n        });\n\n        it('should fake a right Shift keyup', function () {\n            const kbd = new Keyboard(document);\n            kbd.onkeyevent = sinon.spy();\n\n            kbd._handleKeyDown(keyevent('keydown', {code: 'ShiftLeft', key: 'Shift', location: 1}));\n            expect(kbd.onkeyevent).to.have.been.calledOnce;\n            expect(kbd.onkeyevent).to.have.been.calledWith(0xffe1, 'ShiftLeft', true);\n            kbd.onkeyevent.resetHistory();\n\n            kbd._handleKeyDown(keyevent('keydown', {code: 'ShiftRight', key: 'Shift', location: 2}));\n            expect(kbd.onkeyevent).to.have.been.calledOnce;\n            expect(kbd.onkeyevent).to.have.been.calledWith(0xffe2, 'ShiftRight', true);\n            kbd.onkeyevent.resetHistory();\n\n            kbd._handleKeyUp(keyevent('keyup', {code: 'ShiftRight', key: 'Shift', location: 2}));\n            expect(kbd.onkeyevent).to.have.been.calledTwice;\n            expect(kbd.onkeyevent).to.have.been.calledWith(0xffe2, 'ShiftRight', false);\n            expect(kbd.onkeyevent).to.have.been.calledWith(0xffe1, 'ShiftLeft', false);\n        });\n    });\n});\n"
  },
  {
    "path": "tests/test.localization.js",
    "content": "import _, { Localizer, l10n } from '../app/localization.js';\n\ndescribe('Localization', function () {\n    \"use strict\";\n\n    let origNavigator;\n    let fetch;\n\n    beforeEach(function () {\n        // window.navigator is a protected read-only property in many\n        // environments, so we need to redefine it whilst running these\n        // tests.\n        origNavigator = Object.getOwnPropertyDescriptor(window, \"navigator\");\n\n        Object.defineProperty(window, \"navigator\", {value: {}});\n        window.navigator.languages = [];\n\n        fetch = sinon.stub(window, \"fetch\");\n        fetch.resolves(new Response(\"{}\"));\n    });\n    afterEach(function () {\n        fetch.restore();\n\n        Object.defineProperty(window, \"navigator\", origNavigator);\n    });\n\n    describe('Singleton', function () {\n        it('should export a singleton object', function () {\n            expect(l10n).to.be.instanceOf(Localizer);\n        });\n        it('should export a singleton translation function', async function () {\n            // FIXME: Can we use some spy instead?\n            window.navigator.languages = [\"de\"];\n            fetch.resolves(new Response(JSON.stringify({ \"Foobar\": \"gazonk\" })));\n            await l10n.setup([\"de\"]);\n            expect(_(\"Foobar\")).to.equal(\"gazonk\");\n        });\n    });\n\n    describe('language selection', function () {\n        it('should use English by default', function () {\n            let lclz = new Localizer();\n            expect(lclz.language).to.equal('en');\n        });\n        it('should use English if no user language matches', async function () {\n            window.navigator.languages = [\"nl\", \"de\"];\n            let lclz = new Localizer();\n            await lclz.setup([\"es\", \"fr\"]);\n            expect(lclz.language).to.equal('en');\n        });\n        it('should fall back to generic English for other English', async function () {\n            window.navigator.languages = [\"en-AU\", \"de\"];\n            let lclz = new Localizer();\n            await lclz.setup([\"de\", \"fr\", \"en-GB\"]);\n            expect(lclz.language).to.equal('en');\n        });\n        it('should prefer specific English over generic', async function () {\n            window.navigator.languages = [\"en-GB\", \"de\"];\n            let lclz = new Localizer();\n            await lclz.setup([\"de\", \"en-AU\", \"en-GB\"]);\n            expect(lclz.language).to.equal('en-GB');\n        });\n        it('should use the most preferred user language', async function () {\n            window.navigator.languages = [\"nl\", \"de\", \"fr\"];\n            let lclz = new Localizer();\n            await lclz.setup([\"es\", \"fr\", \"de\"]);\n            expect(lclz.language).to.equal('de');\n        });\n        it('should prefer sub-languages languages', async function () {\n            window.navigator.languages = [\"pt-BR\"];\n            let lclz = new Localizer();\n            await lclz.setup([\"pt\", \"pt-BR\"]);\n            expect(lclz.language).to.equal('pt-BR');\n        });\n        it('should fall back to language \"parents\"', async function () {\n            window.navigator.languages = [\"pt-BR\"];\n            let lclz = new Localizer();\n            await lclz.setup([\"fr\", \"pt\", \"de\"]);\n            expect(lclz.language).to.equal('pt');\n        });\n        it('should not use specific language when user asks for a generic language', async function () {\n            window.navigator.languages = [\"pt\", \"de\"];\n            let lclz = new Localizer();\n            await lclz.setup([\"fr\", \"pt-BR\", \"de\"]);\n            expect(lclz.language).to.equal('de');\n        });\n        it('should handle underscore as a separator', async function () {\n            window.navigator.languages = [\"pt-BR\"];\n            let lclz = new Localizer();\n            await lclz.setup([\"pt_BR\"]);\n            expect(lclz.language).to.equal('pt_BR');\n        });\n        it('should handle difference in case', async function () {\n            window.navigator.languages = [\"pt-br\"];\n            let lclz = new Localizer();\n            await lclz.setup([\"pt-BR\"]);\n            expect(lclz.language).to.equal('pt-BR');\n        });\n    });\n\n    describe('Translation loading', function () {\n        it('should not fetch a translation for English', async function () {\n            window.navigator.languages = [];\n            let lclz = new Localizer();\n            await lclz.setup([]);\n            expect(fetch).to.not.have.been.called;\n        });\n        it('should fetch dictionary relative base URL', async function () {\n            window.navigator.languages = [\"de\", \"fr\"];\n            fetch.resolves(new Response('{ \"Foobar\": \"gazonk\" }'));\n            let lclz = new Localizer();\n            await lclz.setup([\"ru\", \"fr\"], \"/some/path/\");\n            expect(fetch).to.have.been.calledOnceWith(\"/some/path/fr.json\");\n            expect(lclz.get(\"Foobar\")).to.equal(\"gazonk\");\n        });\n        it('should handle base URL without trailing slash', async function () {\n            window.navigator.languages = [\"de\", \"fr\"];\n            fetch.resolves(new Response('{ \"Foobar\": \"gazonk\" }'));\n            let lclz = new Localizer();\n            await lclz.setup([\"ru\", \"fr\"], \"/some/path\");\n            expect(fetch).to.have.been.calledOnceWith(\"/some/path/fr.json\");\n            expect(lclz.get(\"Foobar\")).to.equal(\"gazonk\");\n        });\n        it('should handle current base URL', async function () {\n            window.navigator.languages = [\"de\", \"fr\"];\n            fetch.resolves(new Response('{ \"Foobar\": \"gazonk\" }'));\n            let lclz = new Localizer();\n            await lclz.setup([\"ru\", \"fr\"]);\n            expect(fetch).to.have.been.calledOnceWith(\"fr.json\");\n            expect(lclz.get(\"Foobar\")).to.equal(\"gazonk\");\n        });\n        it('should fail if dictionary cannot be found', async function () {\n            window.navigator.languages = [\"de\", \"fr\"];\n            fetch.resolves(new Response('{}', { status: 404 }));\n            let lclz = new Localizer();\n            let ok = false;\n            try {\n                await lclz.setup([\"ru\", \"fr\"], \"/some/path/\");\n            } catch (e) {\n                ok = true;\n            }\n            expect(ok).to.be.true;\n        });\n    });\n});\n"
  },
  {
    "path": "tests/test.raw.js",
    "content": "import Websock from '../core/websock.js';\nimport Display from '../core/display.js';\n\nimport RawDecoder from '../core/decoders/raw.js';\n\nimport FakeWebSocket from './fake.websocket.js';\n\nfunction testDecodeRect(decoder, x, y, width, height, data, display, depth) {\n    let sock;\n    let done = false;\n\n    sock = new Websock;\n    sock.open(\"ws://example.com\");\n\n    sock.on('message', () => {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    });\n\n    // Empty messages are filtered at multiple layers, so we need to\n    // do a direct call\n    if (data.length === 0) {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    } else {\n        sock._websocket._receiveData(new Uint8Array(data));\n    }\n\n    display.flip();\n\n    return done;\n}\n\ndescribe('Raw decoder', function () {\n    let decoder;\n    let display;\n\n    before(FakeWebSocket.replace);\n    after(FakeWebSocket.restore);\n\n    beforeEach(function () {\n        decoder = new RawDecoder();\n        display = new Display(document.createElement('canvas'));\n        display.resize(4, 4);\n    });\n\n    it('should handle the Raw encoding', function () {\n        let done;\n\n        done = testDecodeRect(decoder, 0, 0, 2, 2,\n                              [0xff, 0x00, 0x00, 0,\n                               0x00, 0xff, 0x00, 0,\n                               0x00, 0xff, 0x00, 0,\n                               0xff, 0x00, 0x00, 0],\n                              display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 2, 0, 2, 2,\n                              [0x00, 0x00, 0xff, 0,\n                               0x00, 0x00, 0xff, 0,\n                               0x00, 0x00, 0xff, 0,\n                               0x00, 0x00, 0xff, 0],\n                              display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 0, 2, 4, 1,\n                              [0xee, 0x00, 0xff, 0,\n                               0x00, 0xee, 0xff, 0,\n                               0xaa, 0xee, 0xff, 0,\n                               0xab, 0xee, 0xff, 0],\n                              display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 0, 3, 4, 1,\n                              [0xee, 0x00, 0xff, 0,\n                               0x00, 0xee, 0xff, 0,\n                               0xaa, 0xee, 0xff, 0,\n                               0xab, 0xee, 0xff, 0],\n                              display, 24);\n        expect(done).to.be.true;\n\n        let targetData = new Uint8Array([\n            0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255,\n            0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255\n        ]);\n\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle the Raw encoding in low colour mode', function () {\n        let done;\n\n        done = testDecodeRect(decoder, 0, 0, 2, 2,\n                              [0x30, 0x30, 0x30, 0x30],\n                              display, 8);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 2, 0, 2, 2,\n                              [0x0c, 0x0c, 0x0c, 0x0c],\n                              display, 8);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 0, 2, 4, 1,\n                              [0x0c, 0x0c, 0x30, 0x30],\n                              display, 8);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 0, 3, 4, 1,\n                              [0x0c, 0x0c, 0x30, 0x30],\n                              display, 8);\n        expect(done).to.be.true;\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle empty rects', function () {\n        display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);\n        display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);\n        display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);\n\n        let done = testDecodeRect(decoder, 1, 2, 0, 0, [], display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle empty rects in low colour mode', function () {\n        display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);\n        display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);\n        display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);\n\n        let done = testDecodeRect(decoder, 1, 2, 0, 0, [], display, 8);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n});\n"
  },
  {
    "path": "tests/test.rfb.js",
    "content": "import RFB from '../core/rfb.js';\nimport Websock from '../core/websock.js';\nimport ZStream from \"../vendor/pako/lib/zlib/zstream.js\";\nimport { deflateInit, deflate, Z_DEFAULT_COMPRESSION } from \"../vendor/pako/lib/zlib/deflate.js\";\nimport { encodings } from '../core/encodings.js';\nimport { toUnsigned32bit } from '../core/util/int.js';\nimport { encodeUTF8 } from '../core/util/strings.js';\nimport KeyTable from '../core/input/keysym.js';\nimport legacyCrypto from '../core/crypto/crypto.js';\n\nimport FakeWebSocket from './fake.websocket.js';\n\nfunction push8(arr, num) {\n    \"use strict\";\n    arr.push(num & 0xFF);\n}\n\nfunction push16(arr, num) {\n    \"use strict\";\n    arr.push((num >> 8) & 0xFF,\n             num & 0xFF);\n}\n\nfunction push32(arr, num) {\n    \"use strict\";\n    arr.push((num >> 24) & 0xFF,\n             (num >> 16) & 0xFF,\n             (num >>  8) & 0xFF,\n             num & 0xFF);\n}\n\nfunction pushString(arr, string) {\n    let utf8 = unescape(encodeURIComponent(string));\n    for (let i = 0; i < utf8.length; i++) {\n        arr.push(utf8.charCodeAt(i));\n    }\n}\n\nfunction deflateWithSize(data) {\n    // Adds the size of the string in front before deflating\n\n    let unCompData = [];\n    unCompData.push((data.length >> 24) & 0xFF,\n                    (data.length >> 16) & 0xFF,\n                    (data.length >>  8) & 0xFF,\n                    (data.length & 0xFF));\n\n    for (let i = 0; i < data.length; i++) {\n        unCompData.push(data.charCodeAt(i));\n    }\n\n    let strm = new ZStream();\n    let chunkSize = 1024 * 10 * 10;\n    strm.output = new Uint8Array(chunkSize);\n    deflateInit(strm, Z_DEFAULT_COMPRESSION);\n\n    /* eslint-disable camelcase */\n    strm.input = unCompData;\n    strm.avail_in = strm.input.length;\n    strm.next_in = 0;\n    strm.next_out = 0;\n    strm.avail_out = chunkSize;\n    /* eslint-enable camelcase */\n\n    deflate(strm, 3);\n\n    return new Uint8Array(strm.output.buffer, 0, strm.next_out);\n}\n\ndescribe('Remote Frame Buffer protocol client', function () {\n    let clock;\n    let raf;\n    let fakeResizeObserver = null;\n    const realObserver = window.ResizeObserver;\n\n    // Since we are using fake timers we don't actually want\n    // to wait for the browser to observe the size change,\n    // that's why we use a fake ResizeObserver\n    class FakeResizeObserver {\n        constructor(handler) {\n            this.fire = handler;\n            fakeResizeObserver = this;\n        }\n        disconnect() {}\n        observe(target, options) {}\n        unobserve(target) {}\n    }\n\n    before(FakeWebSocket.replace);\n    after(FakeWebSocket.restore);\n\n    before(function () {\n        this.clock = clock = sinon.useFakeTimers(Date.now());\n        // sinon doesn't support this yet\n        raf = window.requestAnimationFrame;\n        window.requestAnimationFrame = setTimeout;\n        // We must do this in a 'before' since it needs to be set before\n        // the RFB constructor, which runs in beforeEach further down\n        window.ResizeObserver = FakeResizeObserver;\n        // Use a single set of buffers instead of reallocating to\n        // speed up tests\n        const sock = new Websock();\n        const _sQ = new Uint8Array(sock._sQbufferSize);\n        const rQ = new Uint8Array(sock._rQbufferSize);\n\n        Websock.prototype._oldAllocateBuffers = Websock.prototype._allocateBuffers;\n        Websock.prototype._allocateBuffers = function () {\n            this._sQ = _sQ;\n            this._rQ = rQ;\n        };\n\n        // Avoiding printing the entire Websock buffer on errors\n        Websock.prototype.inspect = function () { return \"[object Websock]\"; };\n    });\n\n    after(function () {\n        Websock.prototype._allocateBuffers = Websock.prototype._oldAllocateBuffers;\n        delete Websock.prototype.inspect;\n        this.clock.restore();\n        window.requestAnimationFrame = raf;\n        window.ResizeObserver = realObserver;\n    });\n\n    let container;\n    let rfbs;\n\n    beforeEach(function () {\n        // Create a container element for all RFB objects to attach to\n        container = document.createElement('div');\n        container.style.width = \"100%\";\n        container.style.height = \"100%\";\n        document.body.appendChild(container);\n\n        // And track all created RFB objects\n        rfbs = [];\n    });\n    afterEach(function () {\n        // Make sure every created RFB object is properly cleaned up\n        // or they might affect subsequent tests\n        rfbs.forEach(function (rfb) {\n            rfb.disconnect();\n            expect(rfb._disconnect).to.have.been.called;\n        });\n        rfbs = [];\n\n        document.body.removeChild(container);\n        container = null;\n    });\n\n    function makeRFB(url, options) {\n        url = url || 'wss://host:8675';\n        const rfb = new RFB(container, url, options);\n        clock.tick();\n        rfb._sock._websocket._open();\n        rfb._rfbConnectionState = 'connected';\n        sinon.spy(rfb, \"_disconnect\");\n        rfbs.push(rfb);\n        return rfb;\n    }\n\n    function elementToClient(x, y, client) {\n        let res = { x: 0, y: 0 };\n\n        let bounds = client._canvas.getBoundingClientRect();\n\n        /*\n         * If the canvas is on a fractional position we will calculate\n         * a fractional mouse position. But that gets truncated when we\n         * send the event, AND the same thing happens in RFB when it\n         * generates the PointerEvent message. To compensate for that\n         * fact we round the value upwards here.\n         */\n        res.x = Math.ceil(bounds.left + x);\n        res.y = Math.ceil(bounds.top + y);\n\n        return res;\n    }\n\n    function sendMouseMoveEvent(x, y, buttons, client) {\n        let pos = elementToClient(x, y, client);\n        let ev;\n\n        ev = new MouseEvent('mousemove',\n                            { 'screenX': pos.x + window.screenX,\n                              'screenY': pos.y + window.screenY,\n                              'clientX': pos.x,\n                              'clientY': pos.y,\n                              'buttons': buttons });\n        client._canvas.dispatchEvent(ev);\n    }\n\n    function sendMouseButtonEvent(x, y, down, buttons, client) {\n        let pos = elementToClient(x, y, client);\n        let ev;\n\n        ev = new MouseEvent(down ? 'mousedown' : 'mouseup',\n                            { 'screenX': pos.x + window.screenX,\n                              'screenY': pos.y + window.screenY,\n                              'clientX': pos.x,\n                              'clientY': pos.y,\n                              'buttons': buttons});\n        client._canvas.dispatchEvent(ev);\n    }\n\n    function gestureStart(gestureType, x, y, client,\n                          magnitudeX = 0, magnitudeY = 0) {\n        let pos = elementToClient(x, y, client);\n        let detail = { type: gestureType, clientX: pos.x, clientY: pos.y };\n\n        detail.magnitudeX = magnitudeX;\n        detail.magnitudeY = magnitudeY;\n\n        let ev = new CustomEvent('gesturestart', { detail: detail });\n        client._canvas.dispatchEvent(ev);\n    }\n\n    function gestureMove(gestureType, x, y, client,\n                         magnitudeX = 0, magnitudeY = 0) {\n        let pos = elementToClient(x, y, client);\n        let detail = { type: gestureType, clientX: pos.x, clientY: pos.y };\n\n        detail.magnitudeX = magnitudeX;\n        detail.magnitudeY = magnitudeY;\n\n        let ev = new CustomEvent('gesturemove', { detail: detail }, client);\n        client._canvas.dispatchEvent(ev);\n    }\n\n    function gestureEnd(gestureType, x, y, client) {\n        let pos = elementToClient(x, y, client);\n        let detail = { type: gestureType, clientX: pos.x, clientY: pos.y };\n        let ev = new CustomEvent('gestureend', { detail: detail });\n        client._canvas.dispatchEvent(ev);\n    }\n\n    function sendFbuMsg(rectInfo, rectData, client, rectCnt) {\n        let data = [];\n\n        if (!rectCnt || rectCnt > -1) {\n            // header\n            data.push(0);  // msg type\n            data.push(0);  // padding\n            push16(data, rectCnt || rectData.length);\n        }\n\n        for (let i = 0; i < rectData.length; i++) {\n            if (rectInfo[i]) {\n                push16(data, rectInfo[i].x);\n                push16(data, rectInfo[i].y);\n                push16(data, rectInfo[i].width);\n                push16(data, rectInfo[i].height);\n                push32(data, rectInfo[i].encoding);\n            }\n            data = data.concat(rectData[i]);\n        }\n\n        client._sock._websocket._receiveData(new Uint8Array(data));\n    }\n\n    function sendExtendedDesktopSize(client, reason, result, width, height, screenId, screenFlags) {\n        let rectInfo = { x: reason, y: result, width: width, height: height, encoding: -308 };\n        let rectData = [\n            0x01,        // number of screens = 1\n            0x00, 0x00,\n            0x00,        // padding\n            (screenId >> 24) & 0xff,\n            (screenId >> 16) & 0xff,\n            (screenId >> 8) & 0xff,\n            screenId & 0xff,\n            0x00, 0x00,  // screen x\n            0x00, 0x00,  // screen y\n            (width >> 8) & 0xff,\n            width & 0xff,\n            (height >> 8) & 0xff,\n            height & 0xff,\n            (screenFlags >> 24) & 0xff,\n            (screenFlags >> 16) & 0xff,\n            (screenFlags >> 8) & 0xff,\n            screenFlags & 0xff];\n        sendFbuMsg([rectInfo], [rectData], client);\n    }\n\n    describe('Connecting/Disconnecting', function () {\n        describe('#RFB (constructor)', function () {\n            let open, attach;\n            beforeEach(function () {\n                open = sinon.spy(Websock.prototype, 'open');\n                attach = sinon.spy(Websock.prototype, 'attach');\n            });\n            afterEach(function () {\n                open.restore();\n                attach.restore();\n            });\n\n            it('should actually connect to the websocket', function () {\n                new RFB(document.createElement('div'), 'ws://HOST:8675/PATH');\n                expect(open).to.have.been.calledOnceWithExactly('ws://HOST:8675/PATH', []);\n            });\n\n            it('should pass on connection problems', function () {\n                open.restore();\n                open = sinon.stub(Websock.prototype, 'open');\n                open.throws(new Error('Failure'));\n                expect(() => new RFB(document.createElement('div'), 'ws://HOST:8675/PATH')).to.throw('Failure');\n            });\n\n            it('should handle WebSocket/RTCDataChannel objects', function () {\n                let sock = new FakeWebSocket('ws://HOST:8675/PATH', []);\n                new RFB(document.createElement('div'), sock);\n                expect(open).to.not.have.been.called;\n                expect(attach).to.have.been.calledOnceWithExactly(sock);\n            });\n\n            it('should handle already open WebSocket/RTCDataChannel objects', function () {\n                let sock = new FakeWebSocket('ws://HOST:8675/PATH', []);\n                sock._open();\n                const client = new RFB(document.createElement('div'), sock);\n                let callback = sinon.spy();\n                client.addEventListener('disconnect', callback);\n                expect(open).to.not.have.been.called;\n                expect(attach).to.have.been.calledOnceWithExactly(sock);\n                // Check if it is ready for some data\n                sock._receiveData(new Uint8Array(['R', 'F', 'B', '0', '0', '3', '0', '0', '8']));\n                expect(callback).to.not.have.been.called;\n            });\n\n            it('should refuse closed WebSocket/RTCDataChannel objects', function () {\n                let sock = new FakeWebSocket('ws://HOST:8675/PATH', []);\n                sock.readyState = WebSocket.CLOSED;\n                expect(() => new RFB(document.createElement('div'), sock)).to.throw();\n            });\n\n            it('should pass on attach problems', function () {\n                attach.restore();\n                attach = sinon.stub(Websock.prototype, 'attach');\n                attach.throws(new Error('Failure'));\n                let sock = new FakeWebSocket('ws://HOST:8675/PATH', []);\n                expect(() => new RFB(document.createElement('div'), sock)).to.throw('Failure');\n            });\n        });\n\n        describe('#disconnect', function () {\n            let client;\n            let close;\n\n            beforeEach(function () {\n                client = makeRFB();\n                close = sinon.stub(Websock.prototype, \"close\");\n            });\n            afterEach(function () {\n                close.restore();\n            });\n\n            it('should start closing WebSocket', function () {\n                let callback = sinon.spy();\n                client.addEventListener('disconnect', callback);\n                client.disconnect();\n                expect(close).to.have.been.calledOnceWithExactly();\n                expect(callback).to.not.have.been.called;\n            });\n\n            it('should send disconnect event', function () {\n                let callback = sinon.spy();\n                client.addEventListener('disconnect', callback);\n                client.disconnect();\n                close.thisValues[0]._eventHandlers.close(new CloseEvent(\"close\", { 'code': 1000, 'reason': \"\", 'wasClean': true }));\n                expect(callback).to.have.been.calledOnce;\n                expect(callback.args[0][0].detail.clean).to.be.true;\n            });\n\n            it('should force disconnect if disconnecting takes too long', function () {\n                let callback = sinon.spy();\n                client.addEventListener('disconnect', callback);\n                client.disconnect();\n                this.clock.tick(3 * 1000);\n                expect(callback).to.have.been.calledOnce;\n                expect(callback.args[0][0].detail.clean).to.be.true;\n            });\n\n            it('should not fail if disconnect completes before timeout', function () {\n                let callback = sinon.spy();\n                client.addEventListener('disconnect', callback);\n                client.disconnect();\n                client._updateConnectionState('disconnecting');\n                this.clock.tick(3 * 1000 / 2);\n                close.thisValues[0]._eventHandlers.close(new CloseEvent(\"close\", { 'code': 1000, 'reason': \"\", 'wasClean': true }));\n                this.clock.tick(3 * 1000 / 2 + 1);\n                expect(callback).to.have.been.calledOnce;\n                expect(callback.args[0][0].detail.clean).to.be.true;\n            });\n\n            it('should unregister error event handler', function () {\n                sinon.spy(client._sock, 'off');\n                client.disconnect();\n                expect(client._sock.off).to.have.been.calledWith('error');\n            });\n\n            it('should unregister message event handler', function () {\n                sinon.spy(client._sock, 'off');\n                client.disconnect();\n                expect(client._sock.off).to.have.been.calledWith('message');\n            });\n\n            it('should unregister open event handler', function () {\n                sinon.spy(client._sock, 'off');\n                client.disconnect();\n                expect(client._sock.off).to.have.been.calledWith('open');\n            });\n        });\n    });\n\n    describe('Public API basic behavior', function () {\n        let client;\n        beforeEach(function () {\n            client = makeRFB();\n        });\n\n        describe('#sendCtrlAlDel', function () {\n            it('should sent ctrl[down]-alt[down]-del[down] then del[up]-alt[up]-ctrl[up]', function () {\n                let esock = new Websock();\n                let ews = new FakeWebSocket();\n                ews._open();\n                esock.attach(ews);\n                RFB.messages.keyEvent(esock, 0xFFE3, 1);\n                RFB.messages.keyEvent(esock, 0xFFE9, 1);\n                RFB.messages.keyEvent(esock, 0xFFFF, 1);\n                RFB.messages.keyEvent(esock, 0xFFFF, 0);\n                RFB.messages.keyEvent(esock, 0xFFE9, 0);\n                RFB.messages.keyEvent(esock, 0xFFE3, 0);\n                let expected = ews._getSentData();\n\n                client.sendCtrlAltDel();\n\n                expect(client._sock).to.have.sent(expected);\n            });\n\n            it('should not send the keys if we are not in a normal state', function () {\n                client._rfbConnectionState = \"connecting\";\n                client.sendCtrlAltDel();\n                expect(client._sock).to.have.sent(new Uint8Array([]));\n            });\n\n            it('should not send the keys if we are set as view_only', function () {\n                client._viewOnly = true;\n                client.sendCtrlAltDel();\n                expect(client._sock).to.have.sent(new Uint8Array([]));\n            });\n        });\n\n        describe('#sendKey', function () {\n            it('should send a single key with the given code and state (down = true)', function () {\n                let esock = new Websock();\n                let ews = new FakeWebSocket();\n                ews._open();\n                esock.attach(ews);\n                RFB.messages.keyEvent(esock, 123, 1);\n                let expected = ews._getSentData();\n\n                client.sendKey(123, 'Key123', true);\n\n                expect(client._sock).to.have.sent(expected);\n            });\n\n            it('should send both a down and up event if the state is not specified', function () {\n                let esock = new Websock();\n                let ews = new FakeWebSocket();\n                ews._open();\n                esock.attach(ews);\n                RFB.messages.keyEvent(esock, 123, 1);\n                RFB.messages.keyEvent(esock, 123, 0);\n                let expected = ews._getSentData();\n\n                client.sendKey(123, 'Key123');\n\n                expect(client._sock).to.have.sent(expected);\n            });\n\n            it('should not send the key if we are not in a normal state', function () {\n                client._rfbConnectionState = \"connecting\";\n                client.sendKey(123, 'Key123');\n                expect(client._sock).to.have.sent(new Uint8Array([]));\n            });\n\n            it('should not send the key if we are set as view_only', function () {\n                client._viewOnly = true;\n                client.sendKey(123, 'Key123');\n                expect(client._sock).to.have.sent(new Uint8Array([]));\n            });\n\n            it('should send QEMU extended events if supported', function () {\n                client._qemuExtKeyEventSupported = true;\n                let esock = new Websock();\n                let ews = new FakeWebSocket();\n                ews._open();\n                esock.attach(ews);\n                RFB.messages.QEMUExtendedKeyEvent(esock, 0x20, true, 0x0039);\n                let expected = ews._getSentData();\n\n                client.sendKey(0x20, 'Space', true);\n\n                expect(client._sock).to.have.sent(expected);\n            });\n\n            it('should not send QEMU extended events if unknown key code', function () {\n                client._qemuExtKeyEventSupported = true;\n                let esock = new Websock();\n                let ews = new FakeWebSocket();\n                ews._open();\n                esock.attach(ews);\n                RFB.messages.keyEvent(esock, 123, 1);\n                let expected = ews._getSentData();\n\n                client.sendKey(123, 'FooBar', true);\n\n                expect(client._sock).to.have.sent(expected);\n            });\n        });\n\n        describe('#focus', function () {\n            it('should move focus to canvas object', function () {\n                sinon.spy(client._canvas, \"focus\");\n                client.focus();\n                expect(client._canvas.focus).to.have.been.calledOnce;\n            });\n\n            it('should include focus options', function () {\n                sinon.spy(client._canvas, \"focus\");\n                client.focus({ foobar: 12, gazonk: true });\n                expect(client._canvas.focus).to.have.been.calledOnce;\n                expect(client._canvas.focus).to.have.been.calledWith({ foobar: 12, gazonk: true});\n            });\n        });\n\n        describe('#blur', function () {\n            it('should remove focus from canvas object', function () {\n                sinon.spy(client._canvas, \"blur\");\n                client.blur();\n                expect(client._canvas.blur).to.have.been.calledOnce;\n            });\n        });\n\n        describe('#clipboardPasteFrom', function () {\n            describe('Clipboard update handling', function () {\n                beforeEach(function () {\n                    sinon.spy(RFB.messages, 'clientCutText');\n                    sinon.spy(RFB.messages, 'extendedClipboardNotify');\n                });\n\n                afterEach(function () {\n                    RFB.messages.clientCutText.restore();\n                    RFB.messages.extendedClipboardNotify.restore();\n                });\n\n                it('should send the given text in an clipboard update', function () {\n                    client.clipboardPasteFrom('abc');\n\n                    expect(RFB.messages.clientCutText).to.have.been.calledOnce;\n                    expect(RFB.messages.clientCutText).to.have.been.calledWith(client._sock,\n                                                                               new Uint8Array([97, 98, 99]));\n                });\n\n                it('should mask unsupported characters', function () {\n                    client.clipboardPasteFrom('abc€');\n\n                    expect(RFB.messages.clientCutText).to.have.been.calledOnce;\n                    expect(RFB.messages.clientCutText).to.have.been.calledWith(client._sock,\n                                                                               new Uint8Array([97, 98, 99, 63]));\n                });\n\n                it('should mask characters, not UTF-16 code points', function () {\n                    client.clipboardPasteFrom('😂');\n\n                    expect(RFB.messages.clientCutText).to.have.been.calledOnce;\n                    expect(RFB.messages.clientCutText).to.have.been.calledWith(client._sock,\n                                                                               new Uint8Array([63]));\n                });\n\n                it('should send an notify if extended clipboard is supported by server', function () {\n                    // Send our capabilities\n                    let data = [3, 0, 0, 0];\n                    const flags = [0x1F, 0x00, 0x00, 0x01];\n                    let fileSizes = [0x00, 0x00, 0x00, 0x1E];\n\n                    push32(data, toUnsigned32bit(-8));\n                    data = data.concat(flags);\n                    data = data.concat(fileSizes);\n                    client._sock._websocket._receiveData(new Uint8Array(data));\n\n                    client.clipboardPasteFrom('extended test');\n                    expect(RFB.messages.extendedClipboardNotify).to.have.been.calledOnce;\n                });\n            });\n\n            it('should flush multiple times for large clipboards', function () {\n                sinon.spy(client._sock, 'flush');\n                let longText = \"\";\n                for (let i = 0; i < client._sock._sQbufferSize + 100; i++) {\n                    longText += 'a';\n                }\n                client.clipboardPasteFrom(longText);\n                expect(client._sock.flush).to.have.been.calledTwice;\n            });\n\n            it('should not send the text if we are not in a normal state', function () {\n                sinon.spy(client._sock, 'flush');\n                client._rfbConnectionState = \"connecting\";\n                client.clipboardPasteFrom('abc');\n                expect(client._sock.flush).to.not.have.been.called;\n            });\n        });\n\n        describe(\"XVP operations\", function () {\n            beforeEach(function () {\n                client._rfbXvpVer = 1;\n            });\n\n            it('should send the shutdown signal on #machineShutdown', function () {\n                client.machineShutdown();\n                expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x02]));\n            });\n\n            it('should send the reboot signal on #machineReboot', function () {\n                client.machineReboot();\n                expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x03]));\n            });\n\n            it('should send the reset signal on #machineReset', function () {\n                client.machineReset();\n                expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x04]));\n            });\n\n            it('should not send XVP operations with higher versions than we support', function () {\n                sinon.spy(client._sock, 'flush');\n                client._xvpOp(2, 7);\n                expect(client._sock.flush).to.not.have.been.called;\n            });\n        });\n    });\n\n    describe('Clipping', function () {\n        let client;\n\n        beforeEach(function () {\n            client = makeRFB();\n            container.style.width = '70px';\n            container.style.height = '80px';\n            client.clipViewport = true;\n        });\n\n        it('should update display clip state when changing the property', function () {\n            const spy = sinon.spy(client._display, \"clipViewport\", [\"set\"]);\n\n            client.clipViewport = false;\n            expect(spy.set).to.have.been.calledOnce;\n            expect(spy.set).to.have.been.calledWith(false);\n            spy.set.resetHistory();\n\n            client.clipViewport = true;\n            expect(spy.set).to.have.been.calledOnce;\n            expect(spy.set).to.have.been.calledWith(true);\n        });\n\n        it('should update the viewport when the container size changes', function () {\n            sinon.spy(client._display, \"viewportChangeSize\");\n\n            container.style.width = '40px';\n            container.style.height = '50px';\n            fakeResizeObserver.fire();\n            clock.tick(1000);\n\n            expect(client._display.viewportChangeSize).to.have.been.calledOnce;\n            expect(client._display.viewportChangeSize).to.have.been.calledWith(40, 50);\n        });\n\n        it('should update the viewport when the remote session resizes', function () {\n            sinon.spy(client._display, \"viewportChangeSize\");\n\n            // Simple ExtendedDesktopSize FBU message\n            sendExtendedDesktopSize(client, 0, 0, 4, 4, 0x7890abcd, 0x12345678);\n            // The resize will cause scrollbars on the container, this causes a\n            // resize observation in the browsers\n            fakeResizeObserver.fire();\n\n            // FIXME: Display implicitly calls viewportChangeSize() when\n            //        resizing the framebuffer, hence calledTwice.\n            expect(client._display.viewportChangeSize).to.have.been.calledTwice;\n            expect(client._display.viewportChangeSize).to.have.been.calledWith(70, 80);\n        });\n\n        it('should not update the viewport if not clipping', function () {\n            client.clipViewport = false;\n            sinon.spy(client._display, \"viewportChangeSize\");\n\n            container.style.width = '40px';\n            container.style.height = '50px';\n            fakeResizeObserver.fire();\n            clock.tick(1000);\n\n            expect(client._display.viewportChangeSize).to.not.have.been.called;\n        });\n\n        it('should not update the viewport if scaling', function () {\n            client.scaleViewport = true;\n            sinon.spy(client._display, \"viewportChangeSize\");\n\n            container.style.width = '40px';\n            container.style.height = '50px';\n            fakeResizeObserver.fire();\n            clock.tick(1000);\n\n            expect(client._display.viewportChangeSize).to.not.have.been.called;\n        });\n\n        describe('Clipping and remote resize', function () {\n            beforeEach(function () {\n                // Given a remote (100, 100) larger than the container (70x80),\n                client._resize(100, 100);\n                client._supportsSetDesktopSize = true;\n                client.resizeSession = true;\n                sinon.spy(RFB.messages, \"setDesktopSize\");\n            });\n            afterEach(function () {\n                RFB.messages.setDesktopSize.restore();\n            });\n            it('should not change remote size when changing clipping', function () {\n                // When changing clipping the scrollbars of the container\n                // will appear and disappear and thus trigger resize observations\n                client.clipViewport = false;\n                fakeResizeObserver.fire();\n                clock.tick(1000);\n                client.clipViewport = true;\n                fakeResizeObserver.fire();\n                clock.tick(1000);\n\n                // Then no resize requests should be sent\n                expect(RFB.messages.setDesktopSize).to.not.have.been.called;\n            });\n        });\n\n        describe('Dragging', function () {\n            beforeEach(function () {\n                client = makeRFB();\n                client.dragViewport = true;\n                client._display.resize(100, 100);\n\n                sinon.spy(RFB.messages, \"pointerEvent\");\n            });\n\n            afterEach(function () {\n                RFB.messages.pointerEvent.restore();\n            });\n\n            it('should not send button messages when initiating viewport dragging', function () {\n                sendMouseButtonEvent(13, 9, true, 0x1, client);\n                expect(RFB.messages.pointerEvent).to.not.have.been.called;\n            });\n\n            it('should send button messages when release without movement', function () {\n                // Just up and down\n                sendMouseButtonEvent(13, 9, true, 0x1, client);\n                sendMouseButtonEvent(13, 9, false, 0x0, client);\n\n                expect(RFB.messages.pointerEvent).to.have.been.calledTwice;\n                expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                                    13, 9, 0x1);\n                expect(RFB.messages.pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                                     13, 9, 0x0);\n            });\n\n            it('should send button messages when tapping', function () {\n                // Just up and down\n                gestureStart('onetap', 13, 9, client);\n                gestureEnd('onetap', 13, 9, client);\n\n                expect(RFB.messages.pointerEvent).to.have.been.calledThrice;\n                expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                                    13, 9, 0x0);\n                expect(RFB.messages.pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                                     13, 9, 0x1);\n                expect(RFB.messages.pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                                    13, 9, 0x0);\n            });\n\n            it('should send button messages when release with small movement', function () {\n                // Small movement\n                sendMouseButtonEvent(13, 9, true, 0x1, client);\n                sendMouseMoveEvent(15, 14, 0x1, client);\n                sendMouseButtonEvent(15, 14, false, 0x0, client);\n\n                expect(RFB.messages.pointerEvent).to.have.been.calledTwice;\n                expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                                    15, 14, 0x1);\n                expect(RFB.messages.pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                                     15, 14, 0x0);\n            });\n\n            it('should not send button messages when in view only', function () {\n                client._viewOnly = true;\n\n                sendMouseButtonEvent(13, 9, true, 0x1, client);\n                sendMouseButtonEvent(13, 9, false, 0x0, client);\n\n                expect(RFB.messages.pointerEvent).to.not.have.been.called;\n            });\n\n            it('should send button message directly when drag is disabled', function () {\n                client.dragViewport = false;\n                sendMouseButtonEvent(13, 9, true, 0x1, client);\n                expect(RFB.messages.pointerEvent).to.have.been.calledOnce;\n                expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                                    13, 9, 0x1);\n            });\n\n            it('should be initiate viewport dragging on sufficient movement', function () {\n                sinon.spy(client._display, \"viewportChangePos\");\n\n                // Too small movement\n                sendMouseButtonEvent(13, 9, true, 0x1, client);\n                sendMouseMoveEvent(18, 9, 0x1, client);\n\n\n                expect(RFB.messages.pointerEvent).to.not.have.been.called;\n                expect(client._display.viewportChangePos).to.not.have.been.called;\n\n                // Sufficient movement\n\n                sendMouseMoveEvent(43, 9, 0x1, client);\n\n                expect(RFB.messages.pointerEvent).to.not.have.been.called;\n                expect(client._display.viewportChangePos).to.have.been.calledOnce;\n                expect(client._display.viewportChangePos).to.have.been.calledWith(-30, 0);\n\n                client._display.viewportChangePos.resetHistory();\n\n                // Now a small movement should move right away\n\n                sendMouseMoveEvent(43, 14, 0x1, client);\n\n                expect(RFB.messages.pointerEvent).to.not.have.been.called;\n                expect(client._display.viewportChangePos).to.have.been.calledOnce;\n                expect(client._display.viewportChangePos).to.have.been.calledWith(0, -5);\n            });\n\n            it('should initiate viewport dragging on sufficient drag gesture movement', function () {\n                sinon.spy(client._display, \"viewportChangePos\");\n\n                // Sufficient movement\n                gestureStart('drag', 13, 9, client);\n                gestureMove('drag', 43, 9, client);\n\n                expect(RFB.messages.pointerEvent).to.not.have.been.called;\n                expect(client._display.viewportChangePos).to.have.been.calledOnce;\n                expect(client._display.viewportChangePos).to.have.been.calledWith(-30, 0);\n\n                client._display.viewportChangePos.resetHistory();\n                RFB.messages.pointerEvent.resetHistory();\n\n                // Now a small movement should move right away\n\n                gestureMove('drag', 43, 14, client);\n                gestureEnd('drag', 43, 14, client);\n\n                expect(RFB.messages.pointerEvent).to.not.have.been.called;\n\n                expect(client._display.viewportChangePos).to.have.been.calledOnce;\n                expect(client._display.viewportChangePos).to.have.been.calledWith(0, -5);\n            });\n\n            it('should initiate viewport dragging on sufficient longpress gesture movement', function () {\n                sinon.spy(client._display, \"viewportChangePos\");\n\n                // A small movement below the threshold should not move.\n                gestureStart('longpress', 13, 9, client);\n                gestureMove('longpress', 14, 9, client);\n\n                expect(RFB.messages.pointerEvent).to.not.have.been.called;\n                expect(client._display.viewportChangePos).to.not.have.been.called;\n\n                client._display.viewportChangePos.resetHistory();\n                RFB.messages.pointerEvent.resetHistory();\n\n                gestureMove('longpress', 43, 9, client);\n                gestureEnd('longpress', 43, 9, client);\n\n                expect(RFB.messages.pointerEvent).to.not.have.been.called;\n                expect(client._display.viewportChangePos).to.have.been.calledOnce;\n                expect(client._display.viewportChangePos).to.have.been.calledWith(-30, 0);\n            });\n\n            it('should send button messages on small longpress gesture movement', function () {\n                sinon.spy(client._display, \"viewportChangePos\");\n\n                // A small movement below the threshold should not move.\n                gestureStart('longpress', 13, 9, client);\n                gestureMove('longpress', 14, 10, client);\n\n                expect(RFB.messages.pointerEvent).to.not.have.been.called;\n                expect(client._display.viewportChangePos).to.not.have.been.called;\n\n                client._display.viewportChangePos.resetHistory();\n                RFB.messages.pointerEvent.resetHistory();\n\n                gestureEnd('longpress', 14, 9, client);\n\n                expect(RFB.messages.pointerEvent).to.have.been.calledThrice;\n                expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                                    14, 9, 0x0);\n                expect(RFB.messages.pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                                     14, 9, 0x4);\n                expect(RFB.messages.pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                                    14, 9, 0x0);\n\n                expect(client._display.viewportChangePos).to.not.have.been.called;\n            });\n\n            it('should not send button messages when dragging ends', function () {\n                // First the movement\n\n                sendMouseButtonEvent(13, 9, true, 0x1, client);\n                sendMouseMoveEvent(43, 9, 0x1, client);\n                sendMouseButtonEvent(43, 9, false, 0x0, client);\n\n                expect(RFB.messages.pointerEvent).to.not.have.been.called;\n            });\n\n            it('should terminate viewport dragging on a button up event', function () {\n                // First the dragging movement\n\n                sendMouseButtonEvent(13, 9, true, 0x1, client);\n                sendMouseMoveEvent(43, 9, 0x1, client);\n                sendMouseButtonEvent(43, 9, false, 0x0, client);\n\n                // Another movement now should not move the viewport\n\n                sinon.spy(client._display, \"viewportChangePos\");\n\n                sendMouseMoveEvent(43, 59, 0x0, client);\n\n                expect(client._display.viewportChangePos).to.not.have.been.called;\n            });\n\n            it('should flush move events when initiating viewport drag', function () {\n                sendMouseMoveEvent(13, 9, 0x0, client);\n                sendMouseMoveEvent(14, 9, 0x0, client);\n                sendMouseButtonEvent(14, 9, true, 0x1, client);\n\n                expect(RFB.messages.pointerEvent).to.have.been.calledTwice;\n                expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                                    13, 9, 0x0);\n                expect(RFB.messages.pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                                     14, 9, 0x0);\n\n                RFB.messages.pointerEvent.resetHistory();\n\n                clock.tick(100);\n\n                expect(RFB.messages.pointerEvent).to.not.have.been.called;;\n            });\n        });\n    });\n\n    describe('Scaling', function () {\n        let client;\n        beforeEach(function () {\n            client = makeRFB();\n            container.style.width = '70px';\n            container.style.height = '80px';\n            client.scaleViewport = true;\n\n            sendExtendedDesktopSize(client, 0, 0, 4, 4, 0x7890abcd, 0x12345678);\n        });\n\n        it('should update display scale factor when changing the property', function () {\n            const spy = sinon.spy(client._display, \"scale\", [\"set\"]);\n            sinon.spy(client._display, \"autoscale\");\n\n            client.scaleViewport = false;\n            expect(spy.set).to.have.been.calledOnce;\n            expect(spy.set).to.have.been.calledWith(1.0);\n            expect(client._display.autoscale).to.not.have.been.called;\n\n            client.scaleViewport = true;\n            expect(client._display.autoscale).to.have.been.calledOnce;\n            expect(client._display.autoscale).to.have.been.calledWith(70, 80);\n        });\n\n        it('should update the clipping setting when changing the property', function () {\n            client.clipViewport = true;\n\n            const spy = sinon.spy(client._display, \"clipViewport\", [\"set\"]);\n\n            client.scaleViewport = false;\n            expect(spy.set).to.have.been.calledOnce;\n            expect(spy.set).to.have.been.calledWith(true);\n\n            spy.set.resetHistory();\n\n            client.scaleViewport = true;\n            expect(spy.set).to.have.been.calledOnce;\n            expect(spy.set).to.have.been.calledWith(false);\n        });\n\n        it('should update the scaling when the container size changes', function () {\n            sinon.spy(client._display, \"autoscale\");\n\n            container.style.width = '40px';\n            container.style.height = '50px';\n            fakeResizeObserver.fire();\n            clock.tick(1000);\n\n            expect(client._display.autoscale).to.have.been.calledOnce;\n            expect(client._display.autoscale).to.have.been.calledWith(40, 50);\n        });\n\n        it('should update the scaling resized back to initial size', function () {\n            sinon.spy(client._display, \"autoscale\");\n\n            container.style.width = '40px';\n            container.style.height = '50px';\n            fakeResizeObserver.fire();\n            clock.tick(1000);\n\n            expect(client._display.autoscale).to.have.been.calledOnce;\n            expect(client._display.autoscale).to.have.been.calledWith(40, 50);\n            client._display.autoscale.resetHistory();\n\n            container.style.width = '70px';\n            container.style.height = '80px';\n            fakeResizeObserver.fire();\n            clock.tick(1000);\n\n            expect(client._display.autoscale).to.have.been.calledOnce;\n            expect(client._display.autoscale).to.have.been.calledWith(70, 80);\n            client._display.autoscale.resetHistory();\n        });\n\n        it('should update the scaling when the remote session resizes', function () {\n            sinon.spy(client._display, \"autoscale\");\n\n            sendExtendedDesktopSize(client, 0, 0, 4, 4, 0x7890abcd, 0x12345678);\n            // The resize will cause scrollbars on the container, this causes a\n            // resize observation in the browsers\n            fakeResizeObserver.fire();\n\n            expect(client._display.autoscale).to.have.been.calledOnce;\n            expect(client._display.autoscale).to.have.been.calledWith(70, 80);\n        });\n\n        it('should not update the display scale factor if not scaling', function () {\n            client.scaleViewport = false;\n\n            sinon.spy(client._display, \"autoscale\");\n\n            container.style.width = '40px';\n            container.style.height = '50px';\n            fakeResizeObserver.fire();\n            clock.tick(1000);\n\n            expect(client._display.autoscale).to.not.have.been.called;\n        });\n    });\n\n    describe('Remote resize', function () {\n        let client;\n        beforeEach(function () {\n            client = makeRFB();\n            client.resizeSession = true;\n            container.style.width = '70px';\n            container.style.height = '80px';\n\n            sinon.spy(RFB.messages, \"setDesktopSize\");\n\n            sendExtendedDesktopSize(client, 0, 0, 4, 4, 0x7890abcd, 0x12345678);\n\n            if (RFB.messages.setDesktopSize.calledOnce) {\n                let width = RFB.messages.setDesktopSize.args[0][1];\n                let height = RFB.messages.setDesktopSize.args[0][2];\n                sendExtendedDesktopSize(client, 1, 0, width, height, 0x7890abcd, 0x12345678);\n                RFB.messages.setDesktopSize.resetHistory();\n                clock.tick(10000);\n            }\n        });\n\n        afterEach(function () {\n            RFB.messages.setDesktopSize.restore();\n        });\n\n        it('should only request a resize when turned on', function () {\n            client.resizeSession = false;\n            expect(RFB.messages.setDesktopSize).to.not.have.been.called;\n\n            container.style.width = '40px';\n            container.style.height = '50px';\n            fakeResizeObserver.fire();\n            expect(RFB.messages.setDesktopSize).to.not.have.been.called;\n\n            client.resizeSession = true;\n            expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;\n        });\n\n        it('should request a resize when initially connecting', function () {\n            // Create a new object that hasn't yet seen a\n            // ExtendedDesktopSize rect\n            client = makeRFB();\n            client.resizeSession = true;\n            container.style.width = '70px';\n            container.style.height = '80px';\n\n            // First message should trigger a resize\n\n            sendExtendedDesktopSize(client, 0, 0, 4, 4, 0x7890abcd, 0x12345678);\n\n            // It should match the current size of the container,\n            // not the reported size from the server\n            expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;\n            expect(RFB.messages.setDesktopSize).to.have.been.calledWith(\n                sinon.match.object, 70, 80, 0x7890abcd, 0x12345678);\n\n            sendExtendedDesktopSize(client, 1, 0, 70, 80, 0x7890abcd, 0x12345678);\n            RFB.messages.setDesktopSize.resetHistory();\n\n            // Second message should not trigger a resize\n\n            sendExtendedDesktopSize(client, 0, 0, 4, 4, 0x7890abcd, 0x12345678);\n\n            expect(RFB.messages.setDesktopSize).to.not.have.been.called;\n        });\n\n        it('should request a resize when the container resizes', function () {\n            container.style.width = '40px';\n            container.style.height = '50px';\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;\n            expect(RFB.messages.setDesktopSize).to.have.been.calledWith(\n                sinon.match.object, 40, 50, 0x7890abcd, 0x12345678);\n        });\n\n        it('should not request the same size twice', function () {\n            container.style.width = '40px';\n            container.style.height = '50px';\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;\n            expect(RFB.messages.setDesktopSize).to.have.been.calledWith(\n                sinon.match.object, 40, 50, 0x7890abcd, 0x12345678);\n\n            // Server responds with the requested size 40x50\n            sendExtendedDesktopSize(client, 1, 0, 40, 50, 0x7890abcd, 0x12345678);\n\n            RFB.messages.setDesktopSize.resetHistory();\n\n            // size is still 40x50\n            clock.tick(1000);\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.not.have.been.called;\n        });\n\n        it('should request a resize when resized back to initial size', function () {\n            container.style.width = '40px';\n            container.style.height = '50px';\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;\n            expect(RFB.messages.setDesktopSize).to.have.been.calledWith(\n                sinon.match.object, 40, 50, 0x7890abcd, 0x12345678);\n\n            sendExtendedDesktopSize(client, 1, 0, 40, 50, 0x7890abcd, 0x12345678);\n            RFB.messages.setDesktopSize.resetHistory();\n\n            clock.tick(1000);\n            container.style.width = '70px';\n            container.style.height = '80px';\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;\n            expect(RFB.messages.setDesktopSize).to.have.been.calledWith(\n                sinon.match.object, 70, 80, 0x7890abcd, 0x12345678);\n        });\n\n        it('should rate limit resizes', function () {\n            container.style.width = '20px';\n            container.style.height = '30px';\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;\n            expect(RFB.messages.setDesktopSize).to.have.been.calledWith(\n                sinon.match.object, 20, 30, 0x7890abcd, 0x12345678);\n\n            sendExtendedDesktopSize(client, 1, 0, 20, 30, 0x7890abcd, 0x12345678);\n            RFB.messages.setDesktopSize.resetHistory();\n\n            clock.tick(20);\n\n            container.style.width = '30px';\n            container.style.height = '40px';\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.not.have.been.called;\n\n            clock.tick(20);\n\n            container.style.width = '40px';\n            container.style.height = '50px';\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.not.have.been.called;\n\n            clock.tick(80);\n\n            expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;\n            expect(RFB.messages.setDesktopSize).to.have.been.calledWith(\n                sinon.match.object, 40, 50, 0x7890abcd, 0x12345678);\n        });\n\n        it('should not have overlapping resize requests', function () {\n            container.style.width = '40px';\n            container.style.height = '50px';\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;\n\n            RFB.messages.setDesktopSize.resetHistory();\n\n            clock.tick(1000);\n            container.style.width = '20px';\n            container.style.height = '30px';\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.not.have.been.called;\n        });\n\n        it('should finalize any pending resizes', function () {\n            container.style.width = '40px';\n            container.style.height = '50px';\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;\n\n            RFB.messages.setDesktopSize.resetHistory();\n\n            clock.tick(1000);\n            container.style.width = '20px';\n            container.style.height = '30px';\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.not.have.been.called;\n\n            // Server responds with the requested size 40x50\n            sendExtendedDesktopSize(client, 1, 0, 40, 50, 0x7890abcd, 0x12345678);\n\n            expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;\n            expect(RFB.messages.setDesktopSize).to.have.been.calledWith(\n                sinon.match.object, 20, 30, 0x7890abcd, 0x12345678);\n        });\n\n        it('should not finalize any pending resize if not needed', function () {\n            container.style.width = '40px';\n            container.style.height = '50px';\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;\n\n            RFB.messages.setDesktopSize.resetHistory();\n\n            // Server responds with the requested size 40x50\n            sendExtendedDesktopSize(client, 1, 0, 40, 50, 0x7890abcd, 0x12345678);\n\n            expect(RFB.messages.setDesktopSize).to.not.have.been.called;\n        });\n\n        it('should not finalize any pending resizes on errors', function () {\n            container.style.width = '40px';\n            container.style.height = '50px';\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;\n\n            RFB.messages.setDesktopSize.resetHistory();\n\n            clock.tick(1000);\n            container.style.width = '20px';\n            container.style.height = '30px';\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.not.have.been.called;\n\n            // Server failed the requested size 40x50\n            sendExtendedDesktopSize(client, 1, 1, 40, 50, 0x7890abcd, 0x12345678);\n\n            expect(RFB.messages.setDesktopSize).to.not.have.been.called;\n        });\n\n        it('should not resize when resize is disabled', function () {\n            client._resizeSession = false;\n\n            container.style.width = '40px';\n            container.style.height = '50px';\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.not.have.been.called;\n        });\n\n        it('should not resize when resize is not supported', function () {\n            client._supportsSetDesktopSize = false;\n\n            container.style.width = '40px';\n            container.style.height = '50px';\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.not.have.been.called;\n        });\n\n        it('should not resize when in view only mode', function () {\n            client._viewOnly = true;\n\n            container.style.width = '40px';\n            container.style.height = '50px';\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.not.have.been.called;\n        });\n\n        it('should not try to override a server resize', function () {\n            // Note that this will cause the browser to display scrollbars\n            // since the framebuffer is 100x100 and the container is 70x80.\n            // The usable space (clientWidth/clientHeight) will be even smaller\n            // due to the scrollbars taking up space.\n            sendExtendedDesktopSize(client, 0, 0, 100, 100, 0xabababab, 0x11223344);\n            // The scrollbars cause the ResizeObserver to fire\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.not.have.been.called;\n\n            // An actual size change must not be ignored afterwards\n            container.style.width = '120px';\n            container.style.height = '130px';\n            fakeResizeObserver.fire();\n\n            expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;\n            expect(RFB.messages.setDesktopSize).to.have.been.calledWith(\n                sinon.match.object, 120, 130, 0xabababab, 0x11223344);\n        });\n    });\n\n    describe('Misc internals', function () {\n        describe('#_fail', function () {\n            let client;\n            beforeEach(function () {\n                client = makeRFB();\n            });\n\n            it('should close the WebSocket connection', function () {\n                sinon.spy(client._sock, 'close');\n                client._fail();\n                expect(client._sock.close).to.have.been.calledOnce;\n            });\n\n            it('should transition to disconnected', function () {\n                sinon.spy(client, '_updateConnectionState');\n                client._fail();\n                this.clock.tick(2000);\n                expect(client._updateConnectionState).to.have.been.called;\n                expect(client._rfbConnectionState).to.equal('disconnected');\n            });\n\n            it('should set clean_disconnect variable', function () {\n                client._rfbCleanDisconnect = true;\n                client._rfbConnectionState = 'connected';\n                client._fail();\n                expect(client._rfbCleanDisconnect).to.be.false;\n            });\n\n            it('should result in disconnect event with clean set to false', function () {\n                client._rfbConnectionState = 'connected';\n                const spy = sinon.spy();\n                client.addEventListener(\"disconnect\", spy);\n                client._fail();\n                this.clock.tick(2000);\n                expect(spy).to.have.been.calledOnce;\n                expect(spy.args[0][0].detail.clean).to.be.false;\n            });\n\n        });\n    });\n\n    describe('Protocol initialization states', function () {\n        let client;\n        beforeEach(function () {\n            client = makeRFB();\n            client._rfbConnectionState = 'connecting';\n        });\n\n        function sendVer(ver, client) {\n            const arr = new Uint8Array(12);\n            for (let i = 0; i < ver.length; i++) {\n                arr[i+4] = ver.charCodeAt(i);\n            }\n            arr[0] = 'R'; arr[1] = 'F'; arr[2] = 'B'; arr[3] = ' ';\n            arr[11] = '\\n';\n            client._sock._websocket._receiveData(arr);\n        }\n\n        function sendSecurity(type, cl) {\n            cl._sock._websocket._receiveData(new Uint8Array([1, type]));\n        }\n\n        describe('ProtocolVersion', function () {\n            describe('version parsing', function () {\n                it('should interpret version 003.003 as version 3.3', function () {\n                    sendVer('003.003', client);\n                    expect(client._rfbVersion).to.equal(3.3);\n                });\n\n                it('should interpret version 003.006 as version 3.3', function () {\n                    sendVer('003.006', client);\n                    expect(client._rfbVersion).to.equal(3.3);\n                });\n\n                it('should interpret version 003.889 as version 3.8', function () {\n                    sendVer('003.889', client);\n                    expect(client._rfbVersion).to.equal(3.8);\n                });\n\n                it('should interpret version 003.007 as version 3.7', function () {\n                    sendVer('003.007', client);\n                    expect(client._rfbVersion).to.equal(3.7);\n                });\n\n                it('should interpret version 003.008 as version 3.8', function () {\n                    sendVer('003.008', client);\n                    expect(client._rfbVersion).to.equal(3.8);\n                });\n\n                it('should interpret version 004.000 as version 3.8', function () {\n                    sendVer('004.000', client);\n                    expect(client._rfbVersion).to.equal(3.8);\n                });\n\n                it('should interpret version 004.001 as version 3.8', function () {\n                    sendVer('004.001', client);\n                    expect(client._rfbVersion).to.equal(3.8);\n                });\n\n                it('should interpret version 005.000 as version 3.8', function () {\n                    sendVer('005.000', client);\n                    expect(client._rfbVersion).to.equal(3.8);\n                });\n\n                it('should fail on an invalid version', function () {\n                    let callback = sinon.spy();\n                    client.addEventListener(\"disconnect\", callback);\n\n                    sendVer('002.000', client);\n\n                    expect(callback).to.have.been.calledOnce;\n                    expect(callback.args[0][0].detail.clean).to.be.false;\n                });\n            });\n\n            it('should send back the interpreted version', function () {\n                sendVer('004.000', client);\n\n                const expectedStr = 'RFB 003.008\\n';\n                const expected = [];\n                for (let i = 0; i < expectedStr.length; i++) {\n                    expected[i] = expectedStr.charCodeAt(i);\n                }\n\n                expect(client._sock).to.have.sent(new Uint8Array(expected));\n            });\n\n            it('should transition to the Security state on successful negotiation', function () {\n                sendVer('003.008', client);\n                expect(client._rfbInitState).to.equal('Security');\n            });\n\n            describe('Repeater', function () {\n                beforeEach(function () {\n                    client = makeRFB('wss://host:8675', { repeaterID: \"12345\" });\n                    client._rfbConnectionState = 'connecting';\n                });\n\n                it('should interpret version 000.000 as a repeater', function () {\n                    sendVer('000.000', client);\n                    expect(client._rfbVersion).to.equal(0);\n\n                    const sentData = client._sock._websocket._getSentData();\n                    expect(new Uint8Array(sentData.buffer, 0, 9)).to.array.equal(new Uint8Array([73, 68, 58, 49, 50, 51, 52, 53, 0]));\n                    expect(sentData).to.have.length(250);\n                });\n\n                it('should handle two step repeater negotiation', function () {\n                    sendVer('000.000', client);\n                    sendVer('003.008', client);\n                    expect(client._rfbVersion).to.equal(3.8);\n                });\n            });\n        });\n\n        describe('Security', function () {\n            beforeEach(function () {\n                sendVer('003.008\\n', client);\n                client._sock._websocket._getSentData();\n            });\n\n            it('should respect server preference order', function () {\n                const authSchemes = [ 6, 79, 30, 188, 16, 6, 1 ];\n                client._sock._websocket._receiveData(new Uint8Array(authSchemes));\n                expect(client._sock).to.have.sent(new Uint8Array([30]));\n            });\n\n            it('should fail if there are no supported schemes', function () {\n                let callback = sinon.spy();\n                client.addEventListener(\"disconnect\", callback);\n\n                const authSchemes = [1, 32];\n                client._sock._websocket._receiveData(new Uint8Array(authSchemes));\n\n                expect(callback).to.have.been.calledOnce;\n                expect(callback.args[0][0].detail.clean).to.be.false;\n            });\n\n            it('should fail with the appropriate message if no types are sent', function () {\n                const failureData = [0, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115];\n                let callback = sinon.spy();\n                client.addEventListener(\"securityfailure\", callback);\n\n                client._sock._websocket._receiveData(new Uint8Array(failureData));\n\n                expect(callback).to.have.been.calledOnce;\n                expect(callback.args[0][0].detail.status).to.equal(1);\n                expect(callback.args[0][0].detail.reason).to.equal(\"whoops\");\n            });\n\n            it('should transition to the Authentication state and continue on successful negotiation', function () {\n                const authSchemes = [1, 2];\n                sinon.spy(client, \"_negotiateAuthentication\");\n                client._sock._websocket._receiveData(new Uint8Array(authSchemes));\n                expect(client._rfbInitState).to.equal('Authentication');\n                expect(client._negotiateAuthentication).to.have.been.calledOnce;\n            });\n        });\n\n        describe('Legacy authentication', function () {\n            it('should fail on auth scheme 0 (pre 3.7) with the given message', function () {\n                const errMsg = \"Whoopsies\";\n                const data = [0, 0, 0, 0];\n                const errLen = errMsg.length;\n                push32(data, errLen);\n                for (let i = 0; i < errLen; i++) {\n                    data.push(errMsg.charCodeAt(i));\n                }\n\n                sendVer('003.006\\n', client);\n                client._sock._websocket._getSentData();\n                let callback = sinon.spy();\n                client.addEventListener(\"securityfailure\", callback);\n\n                client._sock._websocket._receiveData(new Uint8Array(data));\n\n                expect(callback).to.have.been.calledOnce;\n                expect(callback.args[0][0].detail.status).to.equal(1);\n                expect(callback.args[0][0].detail.reason).to.equal(\"Whoopsies\");\n            });\n\n            it('should transition straight to ServerInitialisation on \"no auth\" for versions < 3.7', function () {\n                sendVer('003.006\\n', client);\n                client._sock._websocket._getSentData();\n\n                client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 1]));\n                expect(client._rfbInitState).to.equal('ServerInitialisation');\n            });\n\n            it('should transition straight to ServerInitialisation on \"no auth\" for versions < 3.8', function () {\n                sendVer('003.007\\n', client);\n                client._sock._websocket._getSentData();\n\n                sendSecurity(1, client);\n                expect(client._rfbInitState).to.equal('ServerInitialisation');\n            });\n        });\n\n        describe('Authentication', function () {\n            beforeEach(function () {\n                sendVer('003.008\\n', client);\n                client._sock._websocket._getSentData();\n            });\n\n            it('should transition straight to SecurityResult on \"no auth\" (1)', function () {\n                sendSecurity(1, client);\n                expect(client._rfbInitState).to.equal('SecurityResult');\n            });\n\n            it('should fail on an unknown auth scheme', function () {\n                let callback = sinon.spy();\n                client.addEventListener(\"disconnect\", callback);\n\n                sendSecurity(57, client);\n\n                expect(callback).to.have.been.calledOnce;\n                expect(callback.args[0][0].detail.clean).to.be.false;\n            });\n\n            describe('VNC authentication (type 2) handler', function () {\n                it('should fire the credentialsrequired event if missing a password', function () {\n                    const spy = sinon.spy();\n                    client.addEventListener(\"credentialsrequired\", spy);\n                    sendSecurity(2, client);\n\n                    const challenge = [];\n                    for (let i = 0; i < 16; i++) { challenge[i] = i; }\n                    client._sock._websocket._receiveData(new Uint8Array(challenge));\n\n                    expect(spy).to.have.been.calledOnce;\n                    expect(spy.args[0][0].detail.types).to.have.members([\"password\"]);\n                });\n\n                it('should encrypt the password with DES and then send it back', function () {\n                    client.addEventListener(\"credentialsrequired\", () => {\n                        client.sendCredentials({ password: 'passwd' });\n                    });\n                    sendSecurity(2, client);\n                    client._sock._websocket._getSentData(); // skip the choice of auth reply\n\n                    const challenge = [];\n                    for (let i = 0; i < 16; i++) { challenge[i] = i; }\n                    client._sock._websocket._receiveData(new Uint8Array(challenge));\n                    clock.tick();\n\n                    const desPass = RFB.genDES('passwd', challenge);\n                    expect(client._sock).to.have.sent(new Uint8Array(desPass));\n                });\n\n                it('should transition to SecurityResult immediately after sending the password', function () {\n                    client.addEventListener(\"credentialsrequired\", () => {\n                        client.sendCredentials({ password: 'passwd' });\n                    });\n                    sendSecurity(2, client);\n\n                    const challenge = [];\n                    for (let i = 0; i < 16; i++) { challenge[i] = i; }\n                    client._sock._websocket._receiveData(new Uint8Array(challenge));\n                    clock.tick();\n\n                    expect(client._rfbInitState).to.equal('SecurityResult');\n                });\n            });\n\n            describe('RSA-AES authentication (type 6) handler', function () {\n                function fakeGetRandomValues(arr) {\n                    if (arr.length === 16) {\n                        arr.set(new Uint8Array([\n                            0x1c, 0x08, 0xfe, 0x21, 0x78, 0xef, 0x4e, 0xf9,\n                            0x3f, 0x05, 0xec, 0xea, 0xd4, 0x6b, 0xa5, 0xd5,\n                        ]));\n                    } else {\n                        arr.set(new Uint8Array([\n                            0xee, 0xe2, 0xf1, 0x5a, 0x3c, 0xa7, 0xbe, 0x95,\n                            0x6f, 0x2a, 0x75, 0xfd, 0x62, 0x01, 0xcb, 0xbf,\n                            0x43, 0x74, 0xca, 0x47, 0x4d, 0xfb, 0x0f, 0xcf,\n                            0x3a, 0x6d, 0x55, 0x6b, 0x59, 0x3a, 0xf6, 0x87,\n                            0xcb, 0x03, 0xb7, 0x28, 0x35, 0x7b, 0x15, 0x8e,\n                            0xb6, 0xc8, 0x8f, 0x2d, 0x5e, 0x7b, 0x1c, 0x9a,\n                            0x32, 0x55, 0xe7, 0x64, 0x36, 0x25, 0x7b, 0xa3,\n                            0xe9, 0x4f, 0x6f, 0x97, 0xdc, 0xa4, 0xd4, 0x62,\n                            0x6d, 0x7f, 0xab, 0x02, 0x6b, 0x13, 0x56, 0x69,\n                            0xfb, 0xd0, 0xd4, 0x13, 0x76, 0xcd, 0x0d, 0xd0,\n                            0x1f, 0xd1, 0x0c, 0x63, 0x3a, 0x34, 0x20, 0x6c,\n                            0xbb, 0x60, 0x45, 0x82, 0x23, 0xfd, 0x7c, 0x77,\n                            0x6d, 0xcc, 0x5e, 0xaa, 0xc3, 0x0c, 0x43, 0xb7,\n                            0x8d, 0xc0, 0x27, 0x6e, 0xeb, 0x1d, 0x6c, 0x5f,\n                            0xd8, 0x1c, 0x3c, 0x1c, 0x60, 0x2e, 0x82, 0x15,\n                            0xfd, 0x2e, 0x5f, 0x3a, 0x15, 0x53, 0x14, 0x70,\n                            0x4f, 0xe1, 0x65, 0x68, 0x35, 0x6d, 0xc7, 0x64,\n                            0xdb, 0xdd, 0x09, 0x31, 0x4f, 0x7b, 0x6d, 0x6c,\n                            0x77, 0x59, 0x5e, 0x1e, 0xfa, 0x4b, 0x06, 0x14,\n                            0xbe, 0xdc, 0x9c, 0x3d, 0x7b, 0xed, 0xf3, 0x2b,\n                            0x19, 0x26, 0x11, 0x8e, 0x3f, 0xab, 0x73, 0x9a,\n                            0x0a, 0x3a, 0xaa, 0x85, 0x06, 0xd5, 0xca, 0x3f,\n                            0xc3, 0xe2, 0x33, 0x7f, 0x97, 0x74, 0x98, 0x8f,\n                            0x2f, 0xa5, 0xfc, 0x7e, 0xb1, 0x77, 0x71, 0x58,\n                            0xf0, 0xbc, 0x04, 0x59, 0xbb, 0xb4, 0xc6, 0xcc,\n                            0x0f, 0x06, 0xcd, 0xa2, 0xd5, 0x01, 0x2f, 0xb2,\n                            0x22, 0x0b, 0xfc, 0x1e, 0x59, 0x9f, 0xd3, 0x4f,\n                            0x30, 0x95, 0xc6, 0x80, 0x0f, 0x69, 0xf3, 0x4a,\n                            0xd4, 0x36, 0xb6, 0x5a, 0x0b, 0x16, 0x0d, 0x81,\n                            0x31, 0xb0, 0x69, 0xd4, 0x4e,\n                        ]));\n                    }\n                }\n\n                async function fakeGeneratekey() {\n                    let key = { \"alg\": \"RSA-OAEP-256\",\n                                \"d\": \"B7QR2yI8sXjo8vQhJpX9odqqR6wIuPr\" +\n                                     \"TM1B1JJEKVeSrr7OYcc1FRJ52Vap9LI\" +\n                                     \"AU-ezigs9QDvWMxknB8motLnG69Wck3\" +\n                                     \"7nt9_z4s8lFQp0nROA-oaR92HW34KNL\" +\n                                     \"1b2fEVWGI0N86h730MvTJC5O2cmKeMe\" +\n                                     \"zIG-oNqbbfFyP8AW-WLdDlgZm11-Fjz\" +\n                                     \"hbVpb0Bc7nRSgBPSV-EY6Sl-LuglxDx\" +\n                                     \"4LaTdQW7QE_WXoRUt-GYGfTseuFQQK5\" +\n                                     \"WeoyX3yBtQydpauW6rrgyWdtP4hDFIo\" +\n                                     \"ZsX6w1i-UMWMMwlIB5FdnUSi26igVGA\" +\n                                     \"DGpV_vGMP36bv-EHp0bY-Qp0gpIfLfgQ\",\n                                \"dp\": \"Z1v5UceFfV2bhmbG19eGYb30jFxqoR\" +\n                                      \"Bq36PKNY7IunMs1keYy0FpLbyGhtgM\" +\n                                      \"Z1Ymmc8wEzGYsCPEP-ykcun_rlyu7Y\" +\n                                      \"xmcnyC9YQqTqLyqvO-7rUqDvk9TMfd\" +\n                                      \"qWFP6heADRhKZmEbmcau6_m2MwwK9k\" +\n                                      \"OkMKWvpqp8_TpJMnAH7zE\",\n                                \"dq\": \"OBacRE15aY3NtCR4cvP5os3sT70JbD\" +\n                                      \"dDLHT3IHZM6rE35CYNpLDia2chm_wn\" +\n                                      \"McYvKFW9zC2ajRZ15i9c_VXQzS7ZlT\" +\n                                      \"aQYBFyMt7kVhxMEMFsPv1crD6t3uEI\" +\n                                      \"j0LNuNYyy0jkon_LPZKQFK654CiL-L\" +\n                                      \"2YaNXOH4HbHP02dWeVQIE\",\n                                \"e\": \"AQAB\",\n                                \"ext\": true,\n                                \"key_ops\": [\"decrypt\"],\n                                \"kty\": \"RSA\",\n                                \"n\": \"m1c92ZFk9ZI6l_O4YFiNxbv0Ng94SB3\" +\n                                     \"yThy1P_mcqrGDQkRiGVdcTxAk38T9Pg\" +\n                                     \"LztmspF-6U5TAHO-gSmmW88AC9m6f1M\" +\n                                     \"spps6r7zl-M_OG-TwvGzf3BDz8zEg1F\" +\n                                     \"PbZV7whO1M4TCAZ0PqwG7qCc6nK1WiA\" +\n                                     \"haKrSpzuPdL1igfNBsX7qu5wgw4ZTTG\" +\n                                     \"SLbVC_LfULQ5FADgFTRXUSaxm1F8C_L\" +\n                                     \"wy6a2e4nTcXilmtN2IHUjHegzm-Tq2H\" +\n                                     \"izmR3ARdWJpESYIW5-AXoiqj29tDrqC\" +\n                                     \"mu2WPkB2psVp83IzZfaQNQzjNfvA8Gp\" +\n                                     \"imkcDCkP5VMRrtKCcG4ZAFnO-A3NBX_Q\",\n                                \"p\": \"2Q_lNL7vCOBzAppYzCZo3WSh0hX-MOZ\" +\n                                     \"yPUznks5U2TjmfdNZoL6_FJRiGyyLvw\" +\n                                     \"SiZFdEAAvpAyESFfFigngAqMLSf448n\" +\n                                     \"Pg15VUGj533CotsEM0WpoEr1JCgqdUb\" +\n                                     \"gDAfJQIBcwOmegBqd7lWm7uzEnRCvou\" +\n                                     \"B70ybkJfpdprhkVE\",\n                                \"q\": \"tzTt-F3g2u_3Ctj26Ho9iN_wC_W0lXG\" +\n                                     \"zslLt5nLmss8JqdLoDDrijjU-gjeRh7\" +\n                                     \"lgiuHdUc3dorfFKbaMNOjoW3QKqt9oZ\" +\n                                     \"1JM0HKeRw0X2PnWW_0WK6DK5ASWDTXb\" +\n                                     \"Mq2sUZqJvYEyL74H2Zrt0RPAux7XQLE\" +\n                                     \"VgND6ROdXnMJ70O0\",\n                                \"qi\": \"qfl4cXQkz4BNqa2De0-PfdU-8d1w3o\" +\n                                      \"nnaGqx1Ds2fHzD_SJ4cNghn2TksoT9\" +\n                                      \"Qo64b3pUjH9igi2pyEjomk6D12N6FG\" +\n                                      \"0e10u7vFKv3W5YqUOgTpYdbcWHdZ2q\" +\n                                      \"ZWJU0XQZIrF8jLGTOO4GYP6_9sJ5R7\" +\n                                      \"Wk_0MdqQy8qvixWD4zLcY\",\n                    };\n                    key = await window.crypto.subtle.importKey(\"jwk\", key, {\n                        name: \"RSA-OAEP\",\n                        hash: {name: \"SHA-256\"}\n                    }, true, [\"decrypt\"]);\n                    return {privateKey: key};\n                }\n\n                before(() => {\n                    sinon.stub(window.crypto, \"getRandomValues\").callsFake(fakeGetRandomValues);\n                    sinon.stub(window.crypto.subtle, \"generateKey\").callsFake(fakeGeneratekey);\n                });\n                after(() => {\n                    window.crypto.getRandomValues.restore();\n                    window.crypto.subtle.generateKey.restore();\n                });\n\n                beforeEach(function () {\n                    sendSecurity(6, client);\n                    expect(client._sock).to.have.sent(new Uint8Array([6]));\n                });\n\n                const serverPublicKey = [\n                    0x00, 0x00, 0x08, 0x00, 0xac, 0x1a, 0xbc, 0x42,\n                    0x8a, 0x2a, 0x69, 0x65, 0x54, 0xf8, 0x9a, 0xe6,\n                    0x43, 0xaa, 0xf7, 0x27, 0xf6, 0x2a, 0xf8, 0x8f,\n                    0x36, 0xd4, 0xae, 0x54, 0x0f, 0x16, 0x28, 0x08,\n                    0xc2, 0x5b, 0xca, 0x23, 0xdc, 0x27, 0x88, 0x1a,\n                    0x12, 0x82, 0xa8, 0x54, 0xea, 0x00, 0x99, 0x8d,\n                    0x02, 0x1d, 0x77, 0x4a, 0xeb, 0xd0, 0x93, 0x40,\n                    0x79, 0x86, 0xcb, 0x37, 0xd4, 0xb2, 0xc7, 0xcd,\n                    0x93, 0xe1, 0x00, 0x4d, 0x86, 0xff, 0x97, 0x33,\n                    0x0c, 0xad, 0x51, 0x47, 0x45, 0x85, 0x56, 0x07,\n                    0x65, 0x21, 0x7c, 0x57, 0x6d, 0x68, 0x7d, 0xd7,\n                    0x00, 0x43, 0x0c, 0x9d, 0x3b, 0xa1, 0x5a, 0x11,\n                    0xed, 0x51, 0x77, 0xf9, 0xd1, 0x5b, 0x33, 0xd7,\n                    0x1a, 0xeb, 0x65, 0x57, 0xc0, 0x01, 0x51, 0xff,\n                    0x9b, 0x82, 0xb3, 0xeb, 0x82, 0xc2, 0x1f, 0xca,\n                    0x47, 0xc0, 0x6a, 0x09, 0xe0, 0xf7, 0xda, 0x39,\n                    0x85, 0x12, 0xe7, 0x45, 0x8d, 0xb4, 0x1a, 0xda,\n                    0xcb, 0x86, 0x58, 0x52, 0x37, 0x66, 0x9d, 0x8a,\n                    0xce, 0xf2, 0x18, 0x78, 0x7d, 0x7f, 0xf0, 0x07,\n                    0x94, 0x8e, 0x6b, 0x17, 0xd9, 0x00, 0x2a, 0x3a,\n                    0xb9, 0xd4, 0x77, 0xde, 0x70, 0x85, 0xc4, 0x3a,\n                    0x62, 0x10, 0x02, 0xee, 0xba, 0xd8, 0xc0, 0x62,\n                    0xd0, 0x8e, 0xc1, 0x98, 0x19, 0x8e, 0x39, 0x0f,\n                    0x3e, 0x1d, 0x61, 0xb1, 0x93, 0x13, 0x59, 0x39,\n                    0xcb, 0x96, 0xf2, 0x17, 0xc9, 0xe1, 0x41, 0xd3,\n                    0x20, 0xdd, 0x62, 0x5e, 0x7d, 0x53, 0xd6, 0xb7,\n                    0x1d, 0xfe, 0x02, 0x18, 0x1f, 0xe0, 0xef, 0x3d,\n                    0x94, 0xe3, 0x0a, 0x9c, 0x59, 0x54, 0xd8, 0x98,\n                    0x16, 0x9c, 0x31, 0xda, 0x41, 0x0f, 0x2e, 0x71,\n                    0x68, 0xe0, 0xa2, 0x62, 0x3e, 0xe5, 0x25, 0x31,\n                    0xcf, 0xfc, 0x67, 0x63, 0xc3, 0xb0, 0xda, 0x3f,\n                    0x7b, 0x59, 0xbe, 0x7e, 0x9e, 0xa8, 0xd0, 0x01,\n                    0x4f, 0x43, 0x7f, 0x8d, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x01, 0x00, 0x01,\n                ];\n\n                const serverRandom = [\n                    0x01, 0x00, 0x5b, 0x58, 0x2a, 0x96, 0x2d, 0xbb,\n                    0x88, 0xec, 0xc3, 0x54, 0x00, 0xf3, 0xbb, 0xbe,\n                    0x17, 0xa3, 0x84, 0xd3, 0xef, 0xd8, 0x4a, 0x31,\n                    0x09, 0x20, 0xdd, 0xbc, 0x16, 0x9d, 0xc9, 0x5b,\n                    0x99, 0x62, 0x86, 0xfe, 0x0b, 0x28, 0x4b, 0xfe,\n                    0x5b, 0x56, 0x2d, 0xcb, 0x6e, 0x6f, 0xec, 0xf0,\n                    0x53, 0x0c, 0x33, 0x84, 0x93, 0xc9, 0xbf, 0x79,\n                    0xde, 0xb3, 0xb9, 0x29, 0x60, 0x78, 0xde, 0xe6,\n                    0x1d, 0xa7, 0x89, 0x48, 0x3f, 0xd1, 0x58, 0x66,\n                    0x27, 0x9c, 0xd4, 0x6e, 0x72, 0x9c, 0x6e, 0x4a,\n                    0xc0, 0x69, 0x79, 0x6f, 0x79, 0x0f, 0x13, 0xc4,\n                    0x20, 0xcf, 0xa6, 0xbb, 0xce, 0x18, 0x6d, 0xd5,\n                    0x9e, 0xd9, 0x67, 0xbe, 0x61, 0x43, 0x67, 0x11,\n                    0x76, 0x2f, 0xfd, 0x78, 0x75, 0x2b, 0x89, 0x35,\n                    0xdd, 0x0f, 0x13, 0x7f, 0xee, 0x78, 0xad, 0x32,\n                    0x56, 0x21, 0x81, 0x08, 0x1f, 0xcf, 0x4c, 0x29,\n                    0xa3, 0xeb, 0x89, 0x2d, 0xbe, 0xba, 0x8d, 0xe4,\n                    0x69, 0x28, 0xba, 0x53, 0x82, 0xce, 0x5c, 0xf6,\n                    0x5e, 0x5e, 0xa5, 0xb3, 0x88, 0xd8, 0x3d, 0xab,\n                    0xf4, 0x24, 0x9e, 0x3f, 0x04, 0xaf, 0xdc, 0x48,\n                    0x90, 0x53, 0x37, 0xe6, 0x82, 0x1d, 0xe0, 0x15,\n                    0x91, 0xa1, 0xc6, 0xa9, 0x54, 0xe5, 0x2a, 0xb5,\n                    0x64, 0x2d, 0x93, 0xc0, 0xc0, 0xe1, 0x0f, 0x6a,\n                    0x4b, 0xdb, 0x77, 0xf8, 0x4a, 0x0f, 0x83, 0x36,\n                    0xdd, 0x5e, 0x1e, 0xdd, 0x39, 0x65, 0xa2, 0x11,\n                    0xc2, 0xcf, 0x56, 0x1e, 0xa1, 0x29, 0xae, 0x11,\n                    0x9f, 0x3a, 0x82, 0xc7, 0xbd, 0x89, 0x6e, 0x59,\n                    0xb8, 0x59, 0x17, 0xcb, 0x65, 0xa0, 0x4b, 0x4d,\n                    0xbe, 0x33, 0x32, 0x85, 0x9c, 0xca, 0x5e, 0x95,\n                    0xc2, 0x5a, 0xd0, 0xc9, 0x8b, 0xf1, 0xf5, 0x14,\n                    0xcf, 0x76, 0x80, 0xc2, 0x24, 0x0a, 0x39, 0x7e,\n                    0x60, 0x64, 0xce, 0xd9, 0xb8, 0xad, 0x24, 0xa8,\n                    0xdf, 0xcb,\n                ];\n\n                const serverHash = [\n                    0x00, 0x14, 0x39, 0x30, 0x66, 0xb5, 0x66, 0x8a,\n                    0xcd, 0xb9, 0xda, 0xe0, 0xde, 0xcb, 0xf6, 0x47,\n                    0x5f, 0x54, 0x66, 0xe0, 0xbc, 0x49, 0x37, 0x01,\n                    0xf2, 0x9e, 0xef, 0xcc, 0xcd, 0x4d, 0x6c, 0x0e,\n                    0xc6, 0xab, 0x28, 0xd4, 0x7b, 0x13,\n                ];\n\n                const subType = [\n                    0x00, 0x01, 0x30, 0x2a, 0xc3, 0x0b, 0xc2, 0x1c,\n                    0xeb, 0x02, 0x44, 0x92, 0x5d, 0xfd, 0xf9, 0xa7,\n                    0x94, 0xd0, 0x19,\n                ];\n\n                const clientPublicKey = [\n                    0x00, 0x00, 0x08, 0x00, 0x9b, 0x57, 0x3d, 0xd9,\n                    0x91, 0x64, 0xf5, 0x92, 0x3a, 0x97, 0xf3, 0xb8,\n                    0x60, 0x58, 0x8d, 0xc5, 0xbb, 0xf4, 0x36, 0x0f,\n                    0x78, 0x48, 0x1d, 0xf2, 0x4e, 0x1c, 0xb5, 0x3f,\n                    0xf9, 0x9c, 0xaa, 0xb1, 0x83, 0x42, 0x44, 0x62,\n                    0x19, 0x57, 0x5c, 0x4f, 0x10, 0x24, 0xdf, 0xc4,\n                    0xfd, 0x3e, 0x02, 0xf3, 0xb6, 0x6b, 0x29, 0x17,\n                    0xee, 0x94, 0xe5, 0x30, 0x07, 0x3b, 0xe8, 0x12,\n                    0x9a, 0x65, 0xbc, 0xf0, 0x00, 0xbd, 0x9b, 0xa7,\n                    0xf5, 0x32, 0xca, 0x69, 0xb3, 0xaa, 0xfb, 0xce,\n                    0x5f, 0x8c, 0xfc, 0xe1, 0xbe, 0x4f, 0x0b, 0xc6,\n                    0xcd, 0xfd, 0xc1, 0x0f, 0x3f, 0x33, 0x12, 0x0d,\n                    0x45, 0x3d, 0xb6, 0x55, 0xef, 0x08, 0x4e, 0xd4,\n                    0xce, 0x13, 0x08, 0x06, 0x74, 0x3e, 0xac, 0x06,\n                    0xee, 0xa0, 0x9c, 0xea, 0x72, 0xb5, 0x5a, 0x20,\n                    0x21, 0x68, 0xaa, 0xd2, 0xa7, 0x3b, 0x8f, 0x74,\n                    0xbd, 0x62, 0x81, 0xf3, 0x41, 0xb1, 0x7e, 0xea,\n                    0xbb, 0x9c, 0x20, 0xc3, 0x86, 0x53, 0x4c, 0x64,\n                    0x8b, 0x6d, 0x50, 0xbf, 0x2d, 0xf5, 0x0b, 0x43,\n                    0x91, 0x40, 0x0e, 0x01, 0x53, 0x45, 0x75, 0x12,\n                    0x6b, 0x19, 0xb5, 0x17, 0xc0, 0xbf, 0x2f, 0x0c,\n                    0xba, 0x6b, 0x67, 0xb8, 0x9d, 0x37, 0x17, 0x8a,\n                    0x59, 0xad, 0x37, 0x62, 0x07, 0x52, 0x31, 0xde,\n                    0x83, 0x39, 0xbe, 0x4e, 0xad, 0x87, 0x8b, 0x39,\n                    0x91, 0xdc, 0x04, 0x5d, 0x58, 0x9a, 0x44, 0x49,\n                    0x82, 0x16, 0xe7, 0xe0, 0x17, 0xa2, 0x2a, 0xa3,\n                    0xdb, 0xdb, 0x43, 0xae, 0xa0, 0xa6, 0xbb, 0x65,\n                    0x8f, 0x90, 0x1d, 0xa9, 0xb1, 0x5a, 0x7c, 0xdc,\n                    0x8c, 0xd9, 0x7d, 0xa4, 0x0d, 0x43, 0x38, 0xcd,\n                    0x7e, 0xf0, 0x3c, 0x1a, 0x98, 0xa6, 0x91, 0xc0,\n                    0xc2, 0x90, 0xfe, 0x55, 0x31, 0x1a, 0xed, 0x28,\n                    0x27, 0x06, 0xe1, 0x90, 0x05, 0x9c, 0xef, 0x80,\n                    0xdc, 0xd0, 0x57, 0xfd, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                    0x00, 0x01, 0x00, 0x01,\n                ];\n\n                const clientRandom = [\n                    0x01, 0x00, 0x84, 0x7f, 0x26, 0x54, 0x74, 0xf6,\n                    0x47, 0xaf, 0x33, 0x64, 0x0d, 0xa6, 0xe5, 0x30,\n                    0xba, 0xe6, 0xe4, 0x8e, 0x50, 0x40, 0x71, 0x1c,\n                    0x0e, 0x06, 0x63, 0xf5, 0x07, 0x2a, 0x26, 0x68,\n                    0xd6, 0xcf, 0xa6, 0x80, 0x84, 0x5e, 0x64, 0xd4,\n                    0x5e, 0x62, 0x31, 0xfe, 0x44, 0x51, 0x0b, 0x7c,\n                    0x4d, 0x55, 0xc5, 0x4a, 0x7e, 0x0d, 0x4d, 0x9b,\n                    0x84, 0xb4, 0x32, 0x2b, 0x4d, 0x8a, 0x34, 0x8d,\n                    0xc8, 0xcf, 0x19, 0x3b, 0x64, 0x82, 0x27, 0x9e,\n                    0xa7, 0x70, 0x2a, 0xc1, 0xb8, 0xf3, 0x6a, 0x3a,\n                    0xf2, 0x75, 0x6e, 0x1d, 0xeb, 0xb6, 0x70, 0x7a,\n                    0x15, 0x18, 0x38, 0x00, 0xb4, 0x4f, 0x55, 0xb5,\n                    0xd8, 0x03, 0x4e, 0xb8, 0x53, 0xff, 0x80, 0x62,\n                    0xf1, 0x9d, 0x27, 0xe8, 0x2a, 0x3d, 0x98, 0x19,\n                    0x32, 0x09, 0x7e, 0x9a, 0xb0, 0xc7, 0x46, 0x23,\n                    0x10, 0x85, 0x35, 0x00, 0x96, 0xce, 0xb3, 0x2c,\n                    0x84, 0x8d, 0xf4, 0x9e, 0xa8, 0x42, 0x67, 0xed,\n                    0x09, 0xa6, 0x09, 0x97, 0xb3, 0x64, 0x26, 0xfb,\n                    0x71, 0x11, 0x9b, 0x3f, 0xbb, 0x57, 0xb8, 0x5b,\n                    0x2e, 0xc5, 0x2d, 0x8c, 0x5c, 0xf7, 0xef, 0x27,\n                    0x25, 0x88, 0x42, 0x45, 0x43, 0xa4, 0xe7, 0xde,\n                    0xea, 0xf9, 0x15, 0x7b, 0x5d, 0x66, 0x24, 0xce,\n                    0xf7, 0xc8, 0x2f, 0xc5, 0xc0, 0x3d, 0xcd, 0xf2,\n                    0x62, 0xfc, 0x1a, 0x5e, 0xec, 0xff, 0xf1, 0x1b,\n                    0xc8, 0xdb, 0xc1, 0x0f, 0x54, 0x66, 0x9e, 0xfd,\n                    0x99, 0x9b, 0x23, 0x70, 0x62, 0x37, 0x80, 0xad,\n                    0x91, 0x6b, 0x84, 0x85, 0x6a, 0x4c, 0x80, 0x9e,\n                    0x60, 0x8a, 0x93, 0xa3, 0xc8, 0x8e, 0xc4, 0x4b,\n                    0x4d, 0xb4, 0x8e, 0x3e, 0xaf, 0xce, 0xcd, 0x83,\n                    0xe5, 0x21, 0x90, 0x95, 0x20, 0x3c, 0x82, 0xb4,\n                    0x7c, 0xab, 0x63, 0x9c, 0xae, 0xc3, 0xc9, 0x71,\n                    0x1a, 0xec, 0x34, 0x18, 0x47, 0xec, 0x5c, 0x4d,\n                    0xed, 0x84,\n                ];\n\n                const clientHash = [\n                    0x00, 0x14, 0x9c, 0x91, 0x9e, 0x76, 0xcf, 0x1e,\n                    0x66, 0x87, 0x5e, 0x29, 0xf1, 0x13, 0x80, 0xea,\n                    0x7d, 0xec, 0xae, 0xf9, 0x60, 0x01, 0xd3, 0x6f,\n                    0xb7, 0x9e, 0xb2, 0xcd, 0x2d, 0xc8, 0xf8, 0x84,\n                    0xb2, 0x9f, 0xc3, 0x7e, 0xb4, 0xbe,\n                ];\n\n                const credentialsData = [\n                    0x00, 0x08, 0x9d, 0xc8, 0x3a, 0xb8, 0x80, 0x4f,\n                    0xe3, 0x52, 0xdb, 0x62, 0x9e, 0x97, 0x64, 0x82,\n                    0xa8, 0xa1, 0x6b, 0x7e, 0x4d, 0x68, 0x8c, 0x29,\n                    0x91, 0x38,\n                ];\n\n                it('should fire the serververification event', async function () {\n                    let verification = new Promise((resolve, reject) => {\n                        client.addEventListener(\"serververification\", (e) => {\n                            resolve(e.detail.publickey);\n                        });\n                    });\n\n                    client._sock._websocket._receiveData(new Uint8Array(serverPublicKey));\n                    client._sock._websocket._receiveData(new Uint8Array(serverRandom));\n\n                    expect(await verification).to.deep.equal(new Uint8Array(serverPublicKey));\n                });\n\n                it('should handle approveServer and fire the credentialsrequired event', async function () {\n                    let verification = new Promise((resolve, reject) => {\n                        client.addEventListener(\"serververification\", (e) => {\n                            resolve(e.detail.publickey);\n                        });\n                    });\n                    let credentials = new Promise((resolve, reject) => {\n                        client.addEventListener(\"credentialsrequired\", (e) => {\n                            resolve(e.detail.types);\n                        });\n                    });\n\n                    client._sock._websocket._receiveData(new Uint8Array(serverPublicKey));\n                    client._sock._websocket._receiveData(new Uint8Array(serverRandom));\n\n                    await verification;\n                    client.approveServer();\n\n                    client._sock._websocket._receiveData(new Uint8Array(serverHash));\n                    client._sock._websocket._receiveData(new Uint8Array(subType));\n\n                    expect(await credentials).to.have.members([\"password\"]);\n                });\n\n                it('should send credentials to server', async function () {\n                    let verification = new Promise((resolve, reject) => {\n                        client.addEventListener(\"serververification\", (e) => {\n                            resolve(e.detail.publickey);\n                        });\n                    });\n                    let credentials = new Promise((resolve, reject) => {\n                        client.addEventListener(\"credentialsrequired\", (e) => {\n                            resolve(e.detail.types);\n                        });\n                    });\n\n                    client._sock._websocket._receiveData(new Uint8Array(serverPublicKey));\n                    client._sock._websocket._receiveData(new Uint8Array(serverRandom));\n\n                    await verification;\n                    client.approveServer();\n\n                    client._sock._websocket._receiveData(new Uint8Array(serverHash));\n                    client._sock._websocket._receiveData(new Uint8Array(subType));\n\n                    await credentials;\n\n                    let expected = [];\n                    expected = expected.concat(clientPublicKey);\n                    expected = expected.concat(clientRandom);\n                    expected = expected.concat(clientHash);\n                    expect(client._sock).to.have.sent(new Uint8Array(expected));\n\n                    client.sendCredentials({ \"password\": \"123456\" });\n                    clock.tick();\n\n                    // FIXME: We don't have a good way to know when\n                    //        the async stuff is done, so we hook in\n                    //        to this internal function that is\n                    //        called at the end\n                    await new Promise((resolve, reject) => {\n                        sinon.stub(client._sock._websocket, \"send\")\n                            .callsFake((data) => {\n                                FakeWebSocket.prototype.send.call(client._sock._websocket, data);\n                                resolve();\n                            });\n                    });\n\n                    expect(client._sock).to.have.sent(new Uint8Array(credentialsData));\n                });\n            });\n\n            describe('ARD authentication (type 30) handler', function () {\n                let byteArray = new Uint8Array(Array.from(new Uint8Array(128).keys()));\n                function fakeGetRandomValues(arr) {\n                    if (arr.length == 128) {\n                        arr.set(byteArray);\n                    }\n                    return arr;\n                }\n                before(() => {\n                    sinon.stub(window.crypto, \"getRandomValues\").callsFake(fakeGetRandomValues);\n                });\n                after(() => {\n                    window.crypto.getRandomValues.restore();\n                });\n                it('should fire the credentialsrequired event if all credentials are missing', function () {\n                    const spy = sinon.spy();\n                    client.addEventListener(\"credentialsrequired\", spy);\n                    sendSecurity(30, client);\n\n                    expect(spy).to.have.been.calledOnce;\n                    expect(spy.args[0][0].detail.types).to.have.members([\"username\", \"password\"]);\n                });\n\n                it('should fire the credentialsrequired event if some credentials are missing', function () {\n                    const spy = sinon.spy();\n                    client.addEventListener(\"credentialsrequired\", spy);\n                    client.sendCredentials({ password: 'password'});\n                    sendSecurity(30, client);\n\n                    expect(spy).to.have.been.calledOnce;\n                    expect(spy.args[0][0].detail.types).to.have.members([\"username\", \"password\"]);\n                });\n\n                it('should return properly encrypted credentials and public key', async function () {\n                    client.addEventListener(\"credentialsrequired\", () => {\n                        client.sendCredentials({ username: 'user',\n                                                 password: 'password' });\n                    });\n                    sendSecurity(30, client);\n\n                    expect(client._sock).to.have.sent([30]);\n\n                    const generator = new Uint8Array([127, 255]);\n                    const prime = new Uint8Array(byteArray);\n                    const serverKey = legacyCrypto.generateKey(\n                        { name: \"DH\", g: generator, p: prime }, false, [\"deriveBits\"]);\n                    const clientKey = legacyCrypto.generateKey(\n                        { name: \"DH\", g: generator, p: prime }, false, [\"deriveBits\"]);\n                    const serverPublicKey = legacyCrypto.exportKey(\"raw\", serverKey.publicKey);\n                    const clientPublicKey = legacyCrypto.exportKey(\"raw\", clientKey.publicKey);\n\n                    let data = [];\n\n                    data = data.concat(Array.from(generator));\n                    push16(data, prime.length);\n                    data = data.concat(Array.from(prime));\n                    data = data.concat(Array.from(serverPublicKey));\n\n                    client._sock._websocket._receiveData(new Uint8Array(data));\n\n                    // FIXME: We don't have a good way to know when the\n                    //        async stuff is done, so we hook in to this\n                    //        internal function that is called at the\n                    //        end\n                    await new Promise((resolve, reject) => {\n                        sinon.stub(client, \"_resumeAuthentication\")\n                            .callsFake(() => {\n                                RFB.prototype._resumeAuthentication.call(client);\n                                resolve();\n                            });\n                    });\n                    clock.tick();\n\n                    expect(client._rfbInitState).to.equal('SecurityResult');\n\n                    let expectEncrypted = new Uint8Array([\n                        199, 39, 204, 95, 190, 70, 127, 66, 5, 106, 153, 228, 123, 236, 150, 206,\n                        62, 107, 11, 4, 21, 242, 92, 184, 9, 81, 35, 125, 56, 167, 1, 215,\n                        182, 145, 183, 75, 245, 197, 47, 19, 122, 94, 64, 76, 77, 163, 222, 143,\n                        186, 174, 84, 39, 244, 179, 227, 114, 83, 231, 42, 106, 205, 43, 159, 110,\n                        209, 240, 157, 246, 237, 206, 134, 153, 195, 112, 92, 60, 28, 234, 91, 66,\n                        131, 38, 187, 195, 110, 167, 212, 241, 32, 250, 212, 213, 202, 89, 180, 21,\n                        71, 217, 209, 81, 42, 61, 118, 248, 65, 123, 98, 78, 139, 111, 202, 137,\n                        50, 185, 37, 173, 58, 99, 187, 53, 42, 125, 13, 165, 232, 163, 151, 42, 0]);\n\n                    let output = new Uint8Array(256);\n                    output.set(expectEncrypted, 0);\n                    output.set(clientPublicKey, 128);\n\n                    expect(client._sock).to.have.sent(output);\n                });\n            });\n\n            describe('MSLogonII authentication (type 113) handler', function () {\n                function fakeGetRandomValues(arr) {\n                    if (arr.length == 8) {\n                        arr.set(new Uint8Array([0, 0, 0, 0, 5, 6, 7, 8]));\n                    } else if (arr.length == 256) {\n                        arr.set(new Uint8Array(256));\n                    } else if (arr.length == 64) {\n                        arr.set(new Uint8Array(64));\n                    }\n                    return arr;\n                }\n                const expected = new Uint8Array([\n                    0x00, 0x00, 0x00, 0x00, 0x0a, 0xbc, 0x7c, 0xfd,\n                    0x58, 0x34, 0xd2, 0x24, 0x44, 0x60, 0xf0, 0xd1,\n                    0xa3, 0x73, 0x32, 0x02, 0x07, 0xce, 0xc1, 0x3f,\n                    0x10, 0x53, 0xf1, 0xdd, 0x99, 0xad, 0x44, 0x18,\n                    0xa1, 0xc4, 0xac, 0xc1, 0x1c, 0x13, 0x11, 0x85,\n                    0x3a, 0x6f, 0xcb, 0xc6, 0xb1, 0x6c, 0x68, 0x47,\n                    0x85, 0x01, 0xbb, 0xfa, 0x23, 0x8c, 0x59, 0x47,\n                    0x67, 0x47, 0x56, 0x6e, 0x6f, 0x9f, 0x07, 0x76,\n                    0x2e, 0x90, 0x1e, 0xdc, 0x80, 0xc4, 0x4b, 0x72,\n                    0xd2, 0xd5, 0xcd, 0x4b, 0x14, 0xff, 0x05, 0x8b,\n                    0x8d, 0xf1, 0x9b, 0xe0, 0xff, 0xa5, 0x3b, 0x56,\n                    0xb9, 0x6f, 0x84, 0x3e, 0x15, 0x84, 0x31, 0x4e,\n                    0x10, 0x0b, 0x56, 0xf4, 0x10, 0x05, 0x02, 0xc7,\n                    0x05, 0x0b, 0xc9, 0x66, 0x75, 0x32, 0xd3, 0x74,\n                    0xfc, 0x8c, 0xcf, 0xbd, 0x2d, 0x53, 0xd7, 0xa7,\n                    0xca, 0x82, 0x12, 0xce, 0xbb, 0x33, 0x09, 0x3f,\n                    0xff, 0x76, 0x7c, 0xdf, 0x2c, 0x2f, 0x4d, 0x95,\n                    0x86, 0xe4, 0x10, 0x07, 0x75, 0x1a, 0x6d, 0xdb,\n                    0x05, 0x91, 0x70, 0x34, 0x5c, 0x12, 0xbc, 0x4e,\n                    0x5e, 0xd0, 0x21, 0x39, 0x25, 0x2b, 0x62, 0x19,\n                    0x29, 0xa5, 0xe6, 0x93, 0x7b, 0xf8, 0x3f, 0xcf,\n                    0xd7, 0x3f, 0x0c, 0xd2, 0x68, 0x2d, 0x1e, 0x01,\n                    0x1a, 0x31, 0xc1, 0x59, 0x04, 0x06, 0xf6, 0x3b,\n                    0xec, 0x38, 0xef, 0x1b, 0x5b, 0x39, 0x88, 0xd3,\n                    0xe0, 0x5b, 0xb9, 0xef, 0xc3, 0x82, 0xfa, 0xdf,\n                    0x04, 0xf7, 0x65, 0x56, 0x82, 0x77, 0xfd, 0x63,\n                    0x10, 0xd7, 0xab, 0x0b, 0x5e, 0xd9, 0x07, 0x81,\n                    0x9d, 0xce, 0x26, 0xfb, 0x5d, 0xa8, 0x59, 0x2a,\n                    0xd9, 0xb8, 0xac, 0xcd, 0x6e, 0x61, 0x07, 0x39,\n                    0x9f, 0x8d, 0xdf, 0x53, 0x44, 0xab, 0x28, 0x01,\n                    0x86, 0x4d, 0x07, 0x8a, 0x5b, 0xdd, 0xc1, 0x18,\n                    0x29, 0xaa, 0xa2, 0xbe, 0xe2, 0x9c, 0x9e, 0xb0,\n                    0xb3, 0x2b, 0x2c, 0x93, 0x3e, 0x82, 0x07, 0xa6,\n                    0xef, 0x21, 0x2c, 0xa7, 0xf0, 0x65, 0xba, 0xda,\n                    0x13, 0xe4, 0x41, 0x87, 0x36, 0x1c, 0xa5, 0x81,\n                    0xae, 0xf3, 0x3e, 0xda, 0x03, 0x09, 0x63, 0x4b,\n                    0xb5, 0x29, 0x49, 0xfa, 0xbb, 0xa6, 0x31, 0x3c,\n                    0xc8, 0x15, 0xfb, 0xfc, 0xd6, 0xff, 0x04, 0x92,\n                    0x56, 0xbc, 0x66, 0xf1, 0x78, 0xfb, 0x14, 0x79,\n                    0x48, 0xd2, 0xcf, 0x87, 0x60, 0x23, 0xcf, 0xdb,\n                    0x1b, 0xad, 0x42, 0x32, 0x4e, 0x6d, 0x1f, 0x49,\n                ]);\n                before(() => {\n                    sinon.stub(window.crypto, \"getRandomValues\").callsFake(fakeGetRandomValues);\n                });\n                after(() => {\n                    window.crypto.getRandomValues.restore();\n                });\n                it('should send public value and encrypted credentials', function () {\n                    client.addEventListener(\"credentialsrequired\", () => {\n                        client.sendCredentials({ username: 'username',\n                                                 password: 'password123456' });\n                    });\n                    sendSecurity(113, client);\n\n                    expect(client._sock).to.have.sent([113]);\n\n                    const g = new Uint8Array([0, 0, 0, 0, 0, 1, 0, 1]);\n                    const p = new Uint8Array([0, 0, 0, 0, 0x25, 0x18, 0x26, 0x17]);\n                    const A = new Uint8Array([0, 0, 0, 0, 0x0e, 0x12, 0xd0, 0xf5]);\n\n                    client._sock._websocket._receiveData(g);\n                    client._sock._websocket._receiveData(p);\n                    client._sock._websocket._receiveData(A);\n                    clock.tick();\n\n                    expect(client._sock).to.have.sent(expected);\n                    expect(client._rfbInitState).to.equal('SecurityResult');\n                });\n            });\n\n            describe('XVP authentication (type 22) handler', function () {\n                it('should fall through to standard VNC authentication upon completion', function () {\n                    client.addEventListener(\"credentialsrequired\", () => {\n                        client.sendCredentials({ username: 'user',\n                                                 target: 'target',\n                                                 password: 'password' });\n                    });\n                    sinon.spy(client, \"_negotiateStdVNCAuth\");\n                    sendSecurity(22, client);\n                    clock.tick();\n                    expect(client._negotiateStdVNCAuth).to.have.been.calledOnce;\n                });\n\n                it('should fire the credentialsrequired event if all credentials are missing', function () {\n                    const spy = sinon.spy();\n                    client.addEventListener(\"credentialsrequired\", spy);\n                    sendSecurity(22, client);\n\n                    expect(spy).to.have.been.calledOnce;\n                    expect(spy.args[0][0].detail.types).to.have.members([\"username\", \"password\", \"target\"]);\n                });\n\n                it('should fire the credentialsrequired event if some credentials are missing', function () {\n                    const spy = sinon.spy();\n                    client.addEventListener(\"credentialsrequired\", spy);\n                    client.sendCredentials({ username: 'user',\n                                             target: 'target' });\n                    sendSecurity(22, client);\n\n                    expect(spy).to.have.been.calledOnce;\n                    expect(spy.args[0][0].detail.types).to.have.members([\"username\", \"password\", \"target\"]);\n                });\n\n                it('should send user and target separately', function () {\n                    client.addEventListener(\"credentialsrequired\", () => {\n                        client.sendCredentials({ username: 'user',\n                                                 target: 'target',\n                                                 password: 'password' });\n                    });\n                    sendSecurity(22, client);\n                    clock.tick();\n\n                    const expected = [22, 4, 6]; // auth selection, len user, len target\n                    for (let i = 0; i < 10; i++) { expected[i+3] = 'usertarget'.charCodeAt(i); }\n\n                    expect(client._sock).to.have.sent(new Uint8Array(expected));\n                });\n            });\n\n            describe('TightVNC authentication (type 16) handler', function () {\n                beforeEach(function () {\n                    sendSecurity(16, client);\n                    client._sock._websocket._getSentData();  // skip the security reply\n                });\n\n                function sendNumStrPairs(pairs, client) {\n                    const data = [];\n                    push32(data, pairs.length);\n\n                    for (let i = 0; i < pairs.length; i++) {\n                        push32(data, pairs[i][0]);\n                        for (let j = 0; j < 4; j++) {\n                            data.push(pairs[i][1].charCodeAt(j));\n                        }\n                        for (let j = 0; j < 8; j++) {\n                            data.push(pairs[i][2].charCodeAt(j));\n                        }\n                    }\n\n                    client._sock._websocket._receiveData(new Uint8Array(data));\n                }\n\n                it('should skip tunnel negotiation if no tunnels are requested', function () {\n                    client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0]));\n                    expect(client._rfbTightVNC).to.be.true;\n                });\n\n                it('should fail if no supported tunnels are listed', function () {\n                    let callback = sinon.spy();\n                    client.addEventListener(\"disconnect\", callback);\n\n                    sendNumStrPairs([[123, 'OTHR', 'SOMETHNG']], client);\n\n                    expect(callback).to.have.been.calledOnce;\n                    expect(callback.args[0][0].detail.clean).to.be.false;\n                });\n\n                it('should choose the notunnel tunnel type', function () {\n                    sendNumStrPairs([[0, 'TGHT', 'NOTUNNEL'], [123, 'OTHR', 'SOMETHNG']], client);\n                    expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 0]));\n                });\n\n                it('should choose the notunnel tunnel type for Siemens devices', function () {\n                    sendNumStrPairs([[1, 'SICR', 'SCHANNEL'], [2, 'SICR', 'SCHANLPW']], client);\n                    expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 0]));\n                });\n\n                it('should continue to sub-auth negotiation after tunnel negotiation', function () {\n                    sendNumStrPairs([[0, 'TGHT', 'NOTUNNEL']], client);\n                    client._sock._websocket._getSentData();  // skip the tunnel choice here\n                    sendNumStrPairs([[1, 'STDV', 'NOAUTH__']], client);\n                    expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1]));\n                    expect(client._rfbInitState).to.equal('SecurityResult');\n                });\n\n                it('should accept the \"no auth\" auth type and transition to SecurityResult', function () {\n                    sendNumStrPairs([[0, 'TGHT', 'NOTUNNEL']], client);\n                    client._sock._websocket._getSentData();  // skip the tunnel choice here\n                    sendNumStrPairs([[1, 'STDV', 'NOAUTH__']], client);\n                    expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1]));\n                    expect(client._rfbInitState).to.equal('SecurityResult');\n                });\n\n                it('should accept VNC authentication and transition to that', function () {\n                    sendNumStrPairs([[0, 'TGHT', 'NOTUNNEL']], client);\n                    client._sock._websocket._getSentData();  // skip the tunnel choice here\n                    sinon.spy(client, \"_negotiateStdVNCAuth\");\n                    sendNumStrPairs([[2, 'STDV', 'VNCAUTH__']], client);\n                    expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 2]));\n                    expect(client._negotiateStdVNCAuth).to.have.been.calledOnce;\n                    expect(client._rfbAuthScheme).to.equal(2);\n                });\n\n                it('should fail if there are no supported auth types', function () {\n                    let callback = sinon.spy();\n                    client.addEventListener(\"disconnect\", callback);\n\n                    sendNumStrPairs([[0, 'TGHT', 'NOTUNNEL']], client);\n                    client._sock._websocket._getSentData();  // skip the tunnel choice here\n                    sendNumStrPairs([[23, 'stdv', 'badval__']], client);\n\n                    expect(callback).to.have.been.calledOnce;\n                    expect(callback.args[0][0].detail.clean).to.be.false;\n                });\n            });\n\n            describe('VeNCrypt authentication (type 19) handler', function () {\n                beforeEach(function () {\n                    sendSecurity(19, client);\n                    expect(client._sock).to.have.sent(new Uint8Array([19]));\n                });\n\n                it('should fail with non-0.2 versions', function () {\n                    let callback = sinon.spy();\n                    client.addEventListener(\"disconnect\", callback);\n\n                    client._sock._websocket._receiveData(new Uint8Array([0, 1]));\n\n                    expect(callback).to.have.been.calledOnce;\n                    expect(callback.args[0][0].detail.clean).to.be.false;\n                });\n\n                it('should fail if there are no supported subtypes', function () {\n                    // VeNCrypt version\n                    client._sock._websocket._receiveData(new Uint8Array([0, 2]));\n                    expect(client._sock).to.have.sent(new Uint8Array([0, 2]));\n                    // Server ACK.\n                    client._sock._websocket._receiveData(new Uint8Array([0]));\n                    // Subtype list\n                    let callback = sinon.spy();\n                    client.addEventListener(\"disconnect\", callback);\n                    client._sock._websocket._receiveData(new Uint8Array([2, 0, 0, 0, 9, 0, 0, 1, 4]));\n                    expect(callback).to.have.been.calledOnce;\n                    expect(callback.args[0][0].detail.clean).to.be.false;\n                });\n\n                it('should support standard types', function () {\n                    // VeNCrypt version\n                    client._sock._websocket._receiveData(new Uint8Array([0, 2]));\n                    expect(client._sock).to.have.sent(new Uint8Array([0, 2]));\n                    // Server ACK.\n                    client._sock._websocket._receiveData(new Uint8Array([0]));\n                    // Subtype list\n                    client._sock._websocket._receiveData(new Uint8Array([2, 0, 0, 0, 2, 0, 0, 1, 4]));\n\n                    let expectedResponse = [];\n                    push32(expectedResponse, 2); // Chosen subtype.\n\n                    expect(client._sock).to.have.sent(new Uint8Array(expectedResponse));\n                });\n\n                it('should respect server preference order', function () {\n                    // VeNCrypt version\n                    client._sock._websocket._receiveData(new Uint8Array([0, 2]));\n                    expect(client._sock).to.have.sent(new Uint8Array([0, 2]));\n                    // Server ACK.\n                    client._sock._websocket._receiveData(new Uint8Array([0]));\n                    // Subtype list\n                    let subtypes = [ 6 ];\n                    push32(subtypes, 79);\n                    push32(subtypes, 30);\n                    push32(subtypes, 188);\n                    push32(subtypes, 256);\n                    push32(subtypes, 6);\n                    push32(subtypes, 1);\n                    client._sock._websocket._receiveData(new Uint8Array(subtypes));\n\n                    let expectedResponse = [];\n                    push32(expectedResponse, 30); // Chosen subtype.\n\n                    expect(client._sock).to.have.sent(new Uint8Array(expectedResponse));\n                });\n\n                it('should ignore redundant VeNCrypt subtype', function () {\n                    // VeNCrypt version\n                    client._sock._websocket._receiveData(new Uint8Array([0, 2]));\n                    expect(client._sock).to.have.sent(new Uint8Array([0, 2]));\n                    // Server ACK.\n                    client._sock._websocket._receiveData(new Uint8Array([0]));\n                    // Subtype list\n                    client._sock._websocket._receiveData(new Uint8Array([2, 0, 0, 0, 19, 0, 0, 0, 2]));\n\n                    let expectedResponse = [];\n                    push32(expectedResponse, 2); // Chosen subtype.\n\n                    expect(client._sock).to.have.sent(new Uint8Array(expectedResponse));\n                });\n            });\n\n            describe('Plain authentication (type 256) handler', function () {\n                beforeEach(function () {\n                    sendSecurity(19, client);\n                    expect(client._sock).to.have.sent(new Uint8Array([19]));\n                    // VeNCrypt version\n                    client._sock._websocket._receiveData(new Uint8Array([0, 2]));\n                    expect(client._sock).to.have.sent(new Uint8Array([0, 2]));\n                    // Server ACK.\n                    client._sock._websocket._receiveData(new Uint8Array([0]));\n                });\n\n                it('should support Plain authentication', function () {\n                    client.addEventListener(\"credentialsrequired\", () => {\n                        client.sendCredentials({ username: 'username', password: 'password' });\n                    });\n                    client._sock._websocket._receiveData(new Uint8Array([1, 0, 0, 1, 0]));\n                    expect(client._sock).to.have.sent(new Uint8Array([0, 0, 1, 0]));\n\n                    clock.tick();\n\n                    const expectedResponse = [];\n                    push32(expectedResponse, 8);\n                    push32(expectedResponse, 8);\n                    pushString(expectedResponse, 'username');\n                    pushString(expectedResponse, 'password');\n                    expect(client._sock).to.have.sent(new Uint8Array(expectedResponse));\n                });\n\n                it('should support Plain authentication with an empty password', function () {\n                    client.addEventListener(\"credentialsrequired\", () => {\n                        client.sendCredentials({ username: 'username', password: '' });\n                    });\n                    client._sock._websocket._receiveData(new Uint8Array([1, 0, 0, 1, 0]));\n                    expect(client._sock).to.have.sent(new Uint8Array([0, 0, 1, 0]));\n\n                    clock.tick();\n\n                    const expectedResponse = [];\n                    push32(expectedResponse, 8);\n                    push32(expectedResponse, 0);\n                    pushString(expectedResponse, 'username');\n                    pushString(expectedResponse, '');\n                    expect(client._sock).to.have.sent(new Uint8Array(expectedResponse));\n                });\n\n                it('should support Plain authentication with a very long username and password', function () {\n                    client.addEventListener(\"credentialsrequired\", () => {\n                        client.sendCredentials({ username: 'a'.repeat(300), password: 'b'.repeat(300) });\n                    });\n                    client._sock._websocket._receiveData(new Uint8Array([1, 0, 0, 1, 0]));\n                    expect(client._sock).to.have.sent(new Uint8Array([0, 0, 1, 0]));\n\n                    clock.tick();\n\n                    const expectedResponse = [];\n                    push32(expectedResponse, 300);\n                    push32(expectedResponse, 300);\n                    pushString(expectedResponse, 'a'.repeat(300));\n                    pushString(expectedResponse, 'b'.repeat(300));\n                    expect(client._sock).to.have.sent(new Uint8Array(expectedResponse));\n                });\n            });\n        });\n\n        describe('Legacy SecurityResult', function () {\n            it('should not include reason in securityfailure event for versions < 3.7', function () {\n                client.addEventListener(\"credentialsrequired\", () => {\n                    client.sendCredentials({ password: 'passwd' });\n                });\n                const spy = sinon.spy();\n                client.addEventListener(\"securityfailure\", spy);\n                sendVer('003.006\\n', client);\n                client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 2]));\n                const challenge = [];\n                for (let i = 0; i < 16; i++) { challenge[i] = i; }\n                client._sock._websocket._receiveData(new Uint8Array(challenge));\n\n                client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 2]));\n                expect(spy).to.have.been.calledOnce;\n                expect(spy.args[0][0].detail.status).to.equal(2);\n                expect('reason' in spy.args[0][0].detail).to.be.false;\n            });\n\n            it('should not include reason in securityfailure event for versions < 3.8', function () {\n                client.addEventListener(\"credentialsrequired\", () => {\n                    client.sendCredentials({ password: 'passwd' });\n                });\n                const spy = sinon.spy();\n                client.addEventListener(\"securityfailure\", spy);\n                sendVer('003.007\\n', client);\n                sendSecurity(2, client);\n                const challenge = [];\n                for (let i = 0; i < 16; i++) { challenge[i] = i; }\n                client._sock._websocket._receiveData(new Uint8Array(challenge));\n\n                client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 2]));\n                expect(spy).to.have.been.calledOnce;\n                expect(spy.args[0][0].detail.status).to.equal(2);\n                expect('reason' in spy.args[0][0].detail).to.be.false;\n            });\n        });\n\n        describe('SecurityResult', function () {\n            beforeEach(function () {\n                sendVer('003.008\\n', client);\n                client._sock._websocket._getSentData();\n                sendSecurity(1, client);\n                client._sock._websocket._getSentData();\n            });\n\n            it('should fall through to ServerInitialisation on a response code of 0', function () {\n                client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0]));\n                expect(client._rfbInitState).to.equal('ServerInitialisation');\n            });\n\n            it('should include reason when provided in securityfailure event', function () {\n                const spy = sinon.spy();\n                client.addEventListener(\"securityfailure\", spy);\n                const failureData = [0, 0, 0, 1, 0, 0, 0, 12, 115, 117, 99, 104,\n                                     32, 102, 97, 105, 108, 117, 114, 101];\n                client._sock._websocket._receiveData(new Uint8Array(failureData));\n                expect(spy).to.have.been.calledOnce;\n                expect(spy.args[0][0].detail.status).to.equal(1);\n                expect(spy.args[0][0].detail.reason).to.equal('such failure');\n            });\n\n            it('should not include reason when length is zero in securityfailure event', function () {\n                const spy = sinon.spy();\n                client.addEventListener(\"securityfailure\", spy);\n                const failureData = [0, 0, 0, 1, 0, 0, 0, 0];\n                client._sock._websocket._receiveData(new Uint8Array(failureData));\n                expect(spy).to.have.been.calledOnce;\n                expect(spy.args[0][0].detail.status).to.equal(1);\n                expect('reason' in spy.args[0][0].detail).to.be.false;\n            });\n        });\n\n        describe('ClientInitialisation', function () {\n            it('should transition to the ServerInitialisation state', function () {\n                const client = makeRFB();\n                client._rfbConnectionState = 'connecting';\n                client._rfbInitState = 'SecurityResult';\n                client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0]));\n                expect(client._rfbInitState).to.equal('ServerInitialisation');\n            });\n\n            it('should send 1 if we are in shared mode', function () {\n                const client = makeRFB('wss://host:8675', { shared: true });\n                client._rfbConnectionState = 'connecting';\n                client._rfbInitState = 'SecurityResult';\n                client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0]));\n                expect(client._sock).to.have.sent(new Uint8Array([1]));\n            });\n\n            it('should send 0 if we are not in shared mode', function () {\n                const client = makeRFB('wss://host:8675', { shared: false });\n                client._rfbConnectionState = 'connecting';\n                client._rfbInitState = 'SecurityResult';\n                client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0]));\n                expect(client._sock).to.have.sent(new Uint8Array([0]));\n            });\n        });\n\n        describe('ServerInitialisation', function () {\n            beforeEach(function () {\n                client._rfbInitState = 'ServerInitialisation';\n            });\n\n            function sendServerInit(opts, client) {\n                const fullOpts = { width: 10, height: 12, bpp: 24, depth: 24, bigEndian: 0,\n                                   trueColor: 1, redMax: 255, greenMax: 255, blueMax: 255,\n                                   redShift: 16, greenShift: 8, blueShift: 0, name: 'a name' };\n                for (let opt in opts) {\n                    fullOpts[opt] = opts[opt];\n                }\n                const data = [];\n\n                push16(data, fullOpts.width);\n                push16(data, fullOpts.height);\n\n                data.push(fullOpts.bpp);\n                data.push(fullOpts.depth);\n                data.push(fullOpts.bigEndian);\n                data.push(fullOpts.trueColor);\n\n                push16(data, fullOpts.redMax);\n                push16(data, fullOpts.greenMax);\n                push16(data, fullOpts.blueMax);\n                push8(data, fullOpts.redShift);\n                push8(data, fullOpts.greenShift);\n                push8(data, fullOpts.blueShift);\n\n                // padding\n                push8(data, 0);\n                push8(data, 0);\n                push8(data, 0);\n\n                client._sock._websocket._receiveData(new Uint8Array(data));\n\n                const nameData = [];\n                let nameLen = [];\n                pushString(nameData, fullOpts.name);\n                push32(nameLen, nameData.length);\n\n                client._sock._websocket._receiveData(new Uint8Array(nameLen));\n                client._sock._websocket._receiveData(new Uint8Array(nameData));\n            }\n\n            it('should set the framebuffer width and height', function () {\n                sendServerInit({ width: 32, height: 84 }, client);\n                expect(client._fbWidth).to.equal(32);\n                expect(client._fbHeight).to.equal(84);\n            });\n\n            // NB(sross): we just warn, not fail, for endian-ness and shifts, so we don't test them\n\n            it('should set the framebuffer name and call the callback', function () {\n                const spy = sinon.spy();\n                client.addEventListener(\"desktopname\", spy);\n                sendServerInit({ name: 'som€ nam€' }, client);\n\n                expect(client._fbName).to.equal('som€ nam€');\n                expect(spy).to.have.been.calledOnce;\n                expect(spy.args[0][0].detail.name).to.equal('som€ nam€');\n            });\n\n            it('should handle the extended init message of the tight encoding', function () {\n                // NB(sross): we don't actually do anything with it, so just test that we can\n                //            read it w/o throwing an error\n                client._rfbTightVNC = true;\n                sendServerInit({}, client);\n\n                const tightData = [];\n                push16(tightData, 1);\n                push16(tightData, 2);\n                push16(tightData, 3);\n                push16(tightData, 0);\n                for (let i = 0; i < 16 + 32 + 48; i++) {\n                    tightData.push(i);\n                }\n                client._sock._websocket._receiveData(new Uint8Array(tightData));\n\n                expect(client._rfbConnectionState).to.equal('connected');\n            });\n\n            it('should resize the display', function () {\n                sinon.spy(client._display, 'resize');\n                sendServerInit({ width: 27, height: 32 }, client);\n\n                expect(client._display.resize).to.have.been.calledOnce;\n                expect(client._display.resize).to.have.been.calledWith(27, 32);\n            });\n\n            it('should grab the keyboard', function () {\n                sinon.spy(client._keyboard, 'grab');\n                sendServerInit({}, client);\n                expect(client._keyboard.grab).to.have.been.calledOnce;\n            });\n\n            describe('Initial update request', function () {\n                beforeEach(function () {\n                    sinon.spy(RFB.messages, \"pixelFormat\");\n                    sinon.spy(RFB.messages, \"clientEncodings\");\n                    sinon.spy(RFB.messages, \"fbUpdateRequest\");\n                });\n\n                afterEach(function () {\n                    RFB.messages.pixelFormat.restore();\n                    RFB.messages.clientEncodings.restore();\n                    RFB.messages.fbUpdateRequest.restore();\n                });\n\n                // TODO(directxman12): test the various options in this configuration matrix\n                it('should reply with the pixel format, client encodings, and initial update request', function () {\n                    sendServerInit({ width: 27, height: 32 }, client);\n\n                    expect(RFB.messages.pixelFormat).to.have.been.calledOnce;\n                    expect(RFB.messages.pixelFormat).to.have.been.calledWith(client._sock, 24, true);\n                    expect(RFB.messages.pixelFormat).to.have.been.calledBefore(RFB.messages.clientEncodings);\n                    expect(RFB.messages.clientEncodings).to.have.been.calledOnce;\n                    expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.encodingTight);\n                    RFB.messages.clientEncodings.getCall(0).args[1].forEach((enc) => {\n                        expect(enc).to.be.a('number');\n                        expect(Number.isInteger(enc)).to.be.true;\n                    });\n                    expect(RFB.messages.clientEncodings).to.have.been.calledBefore(RFB.messages.fbUpdateRequest);\n                    expect(RFB.messages.fbUpdateRequest).to.have.been.calledOnce;\n                    expect(RFB.messages.fbUpdateRequest).to.have.been.calledWith(client._sock, false, 0, 0, 27, 32);\n                });\n\n                it('should reply with restricted settings for Intel AMT servers', function () {\n                    sendServerInit({ width: 27, height: 32, name: \"Intel(r) AMT KVM\"}, client);\n\n                    expect(RFB.messages.pixelFormat).to.have.been.calledOnce;\n                    expect(RFB.messages.pixelFormat).to.have.been.calledWith(client._sock, 8, true);\n                    expect(RFB.messages.pixelFormat).to.have.been.calledBefore(RFB.messages.clientEncodings);\n                    expect(RFB.messages.clientEncodings).to.have.been.calledOnce;\n                    expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.not.include(encodings.encodingTight);\n                    expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.not.include(encodings.encodingHextile);\n                    expect(RFB.messages.clientEncodings).to.have.been.calledBefore(RFB.messages.fbUpdateRequest);\n                    expect(RFB.messages.fbUpdateRequest).to.have.been.calledOnce;\n                    expect(RFB.messages.fbUpdateRequest).to.have.been.calledWith(client._sock, false, 0, 0, 27, 32);\n                });\n            });\n\n            it('should send the \"connect\" event', function () {\n                let spy = sinon.spy();\n                client.addEventListener('connect', spy);\n                sendServerInit({}, client);\n                expect(spy).to.have.been.calledOnce;\n            });\n        });\n    });\n\n    describe('Protocol message processing after completing initialization', function () {\n        let client;\n\n        beforeEach(function () {\n            client = makeRFB();\n            client._fbName = 'some device';\n            client._fbWidth = 640;\n            client._fbHeight = 20;\n        });\n\n        describe('Framebuffer update handling', function () {\n            it('should send an update request if there is sufficient data', function () {\n                let esock = new Websock();\n                let ews = new FakeWebSocket();\n                ews._open();\n                esock.attach(ews);\n                RFB.messages.fbUpdateRequest(esock, true, 0, 0, 640, 20);\n                let expected = ews._getSentData();\n\n                client._framebufferUpdate = () => true;\n                client._sock._websocket._receiveData(new Uint8Array([0]));\n\n                expect(client._sock).to.have.sent(expected);\n            });\n\n            it('should not send an update request if we need more data', function () {\n                client._sock._websocket._receiveData(new Uint8Array([0]));\n                expect(client._sock).to.have.sent(new Uint8Array([]));\n            });\n\n            it('should resume receiving an update if we previously did not have enough data', function () {\n                let esock = new Websock();\n                let ews = new FakeWebSocket();\n                ews._open();\n                esock.attach(ews);\n                RFB.messages.fbUpdateRequest(esock, true, 0, 0, 640, 20);\n                let expected = ews._getSentData();\n\n                // just enough to set FBU.rects\n                client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 3]));\n                expect(client._sock._websocket._getSentData()).to.have.length(0);\n\n                client._framebufferUpdate = function () { this._sock.rQskipBytes(1); return true; };  // we magically have enough data\n                // 247 should *not* be used as the message type here\n                client._sock._websocket._receiveData(new Uint8Array([247]));\n                expect(client._sock).to.have.sent(expected);\n            });\n\n            it('should not send a request in continuous updates mode', function () {\n                client._enabledContinuousUpdates = true;\n                client._framebufferUpdate = () => true;\n                client._sock._websocket._receiveData(new Uint8Array([0]));\n\n                expect(client._sock).to.have.sent(new Uint8Array([]));\n            });\n\n            it('should fail on an unsupported encoding', function () {\n                let callback = sinon.spy();\n                client.addEventListener(\"disconnect\", callback);\n\n                const rectInfo = { x: 8, y: 11, width: 27, height: 32, encoding: 234 };\n                sendFbuMsg([rectInfo], [[]], client);\n\n                expect(callback).to.have.been.calledOnce;\n                expect(callback.args[0][0].detail.clean).to.be.false;\n            });\n\n            describe('Message encoding handlers', function () {\n                beforeEach(function () {\n                    // a really small frame\n                    client._fbWidth = 4;\n                    client._fbHeight = 4;\n                    client._fbDepth = 24;\n                    client._display.resize(4, 4);\n                });\n\n                it('should handle the DesktopSize pseduo-encoding', function () {\n                    sinon.spy(client._display, 'resize');\n                    sendFbuMsg([{ x: 0, y: 0, width: 20, height: 50, encoding: -223 }], [[]], client);\n\n                    expect(client._fbWidth).to.equal(20);\n                    expect(client._fbHeight).to.equal(50);\n\n                    expect(client._display.resize).to.have.been.calledOnce;\n                    expect(client._display.resize).to.have.been.calledWith(20, 50);\n                });\n\n                describe('the ExtendedDesktopSize pseudo-encoding handler', function () {\n                    beforeEach(function () {\n                        // a really small frame\n                        client._fbWidth = 4;\n                        client._fbHeight = 4;\n                        client._display.resize(4, 4);\n                        sinon.spy(client._display, 'resize');\n                    });\n\n                    function makeScreenData(nrOfScreens) {\n                        const data = [];\n                        push8(data, nrOfScreens);   // number-of-screens\n                        push8(data, 0);               // padding\n                        push16(data, 0);              // padding\n                        for (let i=0; i<nrOfScreens; i += 1) {\n                            push32(data, 0);  // id\n                            push16(data, 0);  // x-position\n                            push16(data, 0);  // y-position\n                            push16(data, 20); // width\n                            push16(data, 50); // height\n                            push32(data, 0);  // flags\n                        }\n                        return data;\n                    }\n\n                    it('should handle a resize requested by this client', function () {\n                        const reasonForChange = 1; // requested by this client\n                        const statusCode      = 0; // No error\n\n                        sendFbuMsg([{ x: reasonForChange, y: statusCode,\n                                      width: 20, height: 50, encoding: -308 }],\n                                   makeScreenData(1), client);\n\n                        expect(client._fbWidth).to.equal(20);\n                        expect(client._fbHeight).to.equal(50);\n\n                        expect(client._display.resize).to.have.been.calledOnce;\n                        expect(client._display.resize).to.have.been.calledWith(20, 50);\n                    });\n\n                    it('should handle a resize requested by another client', function () {\n                        const reasonForChange = 2; // requested by another client\n                        const statusCode      = 0; // No error\n\n                        sendFbuMsg([{ x: reasonForChange, y: statusCode,\n                                      width: 20, height: 50, encoding: -308 }],\n                                   makeScreenData(1), client);\n\n                        expect(client._fbWidth).to.equal(20);\n                        expect(client._fbHeight).to.equal(50);\n\n                        expect(client._display.resize).to.have.been.calledOnce;\n                        expect(client._display.resize).to.have.been.calledWith(20, 50);\n                    });\n\n                    it('should be able to recieve requests which contain data for multiple screens', function () {\n                        const reasonForChange = 2; // requested by another client\n                        const statusCode      = 0; // No error\n\n                        sendFbuMsg([{ x: reasonForChange, y: statusCode,\n                                      width: 60, height: 50, encoding: -308 }],\n                                   makeScreenData(3), client);\n\n                        expect(client._fbWidth).to.equal(60);\n                        expect(client._fbHeight).to.equal(50);\n\n                        expect(client._display.resize).to.have.been.calledOnce;\n                        expect(client._display.resize).to.have.been.calledWith(60, 50);\n                    });\n\n                    it('should not handle a failed request', function () {\n                        const reasonForChange = 1; // requested by this client\n                        const statusCode      = 1; // Resize is administratively prohibited\n\n                        sendFbuMsg([{ x: reasonForChange, y: statusCode,\n                                      width: 20, height: 50, encoding: -308 }],\n                                   makeScreenData(1), client);\n\n                        expect(client._fbWidth).to.equal(4);\n                        expect(client._fbHeight).to.equal(4);\n\n                        expect(client._display.resize).to.not.have.been.called;\n                    });\n                });\n\n                describe('the Cursor pseudo-encoding handler', function () {\n                    beforeEach(function () {\n                        sinon.spy(client._cursor, 'change');\n                    });\n\n                    it('should handle a standard cursor', function () {\n                        const info = { x: 5, y: 7,\n                                       width: 4, height: 4,\n                                       encoding: -239};\n                        let rect = [];\n                        let expected = [];\n\n                        for (let i = 0;i < info.width*info.height;i++) {\n                            push32(rect, 0x11223300);\n                        }\n                        push32(rect, 0xa0a0a0a0);\n\n                        for (let i = 0;i < info.width*info.height/2;i++) {\n                            push32(expected, 0x332211ff);\n                            push32(expected, 0x33221100);\n                        }\n                        expected = new Uint8Array(expected);\n\n                        sendFbuMsg([info], [rect], client);\n\n                        expect(client._cursor.change).to.have.been.calledOnce;\n                        expect(client._cursor.change).to.have.been.calledWith(expected, 5, 7, 4, 4);\n                    });\n\n                    it('should handle an empty cursor', function () {\n                        const info = { x: 0, y: 0,\n                                       width: 0, height: 0,\n                                       encoding: -239};\n                        const rect = [];\n\n                        sendFbuMsg([info], [rect], client);\n\n                        expect(client._cursor.change).to.have.been.calledOnce;\n                        expect(client._cursor.change).to.have.been.calledWith(new Uint8Array, 0, 0, 0, 0);\n                    });\n\n                    it('should handle a transparent cursor', function () {\n                        const info = { x: 5, y: 7,\n                                       width: 4, height: 4,\n                                       encoding: -239};\n                        let rect = [];\n                        let expected = [];\n\n                        for (let i = 0;i < info.width*info.height;i++) {\n                            push32(rect, 0x11223300);\n                        }\n                        push32(rect, 0x00000000);\n\n                        for (let i = 0;i < info.width*info.height;i++) {\n                            push32(expected, 0x33221100);\n                        }\n                        expected = new Uint8Array(expected);\n\n                        sendFbuMsg([info], [rect], client);\n\n                        expect(client._cursor.change).to.have.been.calledOnce;\n                        expect(client._cursor.change).to.have.been.calledWith(expected, 5, 7, 4, 4);\n                    });\n\n                    describe('dot for empty cursor', function () {\n                        beforeEach(function () {\n                            client.showDotCursor = true;\n                            // Was called when we enabled dot cursor\n                            client._cursor.change.resetHistory();\n                        });\n\n                        it('should show a standard cursor', function () {\n                            const info = { x: 5, y: 7,\n                                           width: 4, height: 4,\n                                           encoding: -239};\n                            let rect = [];\n                            let expected = [];\n\n                            for (let i = 0;i < info.width*info.height;i++) {\n                                push32(rect, 0x11223300);\n                            }\n                            push32(rect, 0xa0a0a0a0);\n\n                            for (let i = 0;i < info.width*info.height/2;i++) {\n                                push32(expected, 0x332211ff);\n                                push32(expected, 0x33221100);\n                            }\n                            expected = new Uint8Array(expected);\n\n                            sendFbuMsg([info], [rect], client);\n\n                            expect(client._cursor.change).to.have.been.calledOnce;\n                            expect(client._cursor.change).to.have.been.calledWith(expected, 5, 7, 4, 4);\n                        });\n\n                        it('should handle an empty cursor', function () {\n                            const info = { x: 0, y: 0,\n                                           width: 0, height: 0,\n                                           encoding: -239};\n                            const rect = [];\n                            const dot = RFB.cursors.dot;\n\n                            sendFbuMsg([info], [rect], client);\n\n                            expect(client._cursor.change).to.have.been.calledOnce;\n                            expect(client._cursor.change).to.have.been.calledWith(dot.rgbaPixels,\n                                                                                  dot.hotx,\n                                                                                  dot.hoty,\n                                                                                  dot.w,\n                                                                                  dot.h);\n                        });\n\n                        it('should handle a transparent cursor', function () {\n                            const info = { x: 5, y: 7,\n                                           width: 4, height: 4,\n                                           encoding: -239};\n                            let rect = [];\n                            const dot = RFB.cursors.dot;\n\n                            for (let i = 0;i < info.width*info.height;i++) {\n                                push32(rect, 0x11223300);\n                            }\n                            push32(rect, 0x00000000);\n\n                            sendFbuMsg([info], [rect], client);\n\n                            expect(client._cursor.change).to.have.been.calledOnce;\n                            expect(client._cursor.change).to.have.been.calledWith(dot.rgbaPixels,\n                                                                                  dot.hotx,\n                                                                                  dot.hoty,\n                                                                                  dot.w,\n                                                                                  dot.h);\n                        });\n                    });\n                });\n\n                describe('the VMware cursor pseudo-encoding handler', function () {\n                    beforeEach(function () {\n                        sinon.spy(client._cursor, 'change');\n                    });\n                    afterEach(function () {\n                        client._cursor.change.resetHistory();\n                    });\n\n                    it('should handle the VMware cursor pseudo-encoding', function () {\n                        let data = [0x00, 0x00, 0xff, 0,\n                                    0x00, 0xff, 0x00, 0,\n                                    0x00, 0xff, 0x00, 0,\n                                    0x00, 0x00, 0xff, 0];\n                        let rect = [];\n                        push8(rect, 0);\n                        push8(rect, 0);\n\n                        //AND-mask\n                        for (let i = 0; i < data.length; i++) {\n                            push8(rect, data[i]);\n                        }\n                        //XOR-mask\n                        for (let i = 0; i < data.length; i++) {\n                            push8(rect, data[i]);\n                        }\n\n                        sendFbuMsg([{ x: 0, y: 0, width: 2, height: 2,\n                                      encoding: 0x574d5664}],\n                                   [rect], client);\n                        expect(client._FBU.rects).to.equal(0);\n                    });\n\n                    it('should handle insufficient cursor pixel data', function () {\n\n                        // Specified 14x23 pixels for the cursor,\n                        // but only send 2x2 pixels worth of data\n                        let w = 14;\n                        let h = 23;\n                        let data = [0x00, 0x00, 0xff, 0,\n                                    0x00, 0xff, 0x00, 0];\n                        let rect = [];\n\n                        push8(rect, 0);\n                        push8(rect, 0);\n\n                        //AND-mask\n                        for (let i = 0; i < data.length; i++) {\n                            push8(rect, data[i]);\n                        }\n                        //XOR-mask\n                        for (let i = 0; i < data.length; i++) {\n                            push8(rect, data[i]);\n                        }\n\n                        sendFbuMsg([{ x: 0, y: 0, width: w, height: h,\n                                      encoding: 0x574d5664}],\n                                   [rect], client);\n\n                        // expect one FBU to remain unhandled\n                        expect(client._FBU.rects).to.equal(1);\n                    });\n\n                    it('should update the cursor when type is classic', function () {\n                        let andMask =\n                            [0xff, 0xff, 0xff, 0xff,  //Transparent\n                             0xff, 0xff, 0xff, 0xff,  //Transparent\n                             0x00, 0x00, 0x00, 0x00,  //Opaque\n                             0xff, 0xff, 0xff, 0xff]; //Inverted\n\n                        let xorMask =\n                            [0x00, 0x00, 0x00, 0x00,  //Transparent\n                             0x00, 0x00, 0x00, 0x00,  //Transparent\n                             0x11, 0x22, 0x33, 0x44,  //Opaque\n                             0xff, 0xff, 0xff, 0x44]; //Inverted\n\n                        let rect = [];\n                        push8(rect, 0); //cursor_type\n                        push8(rect, 0); //padding\n                        let hotx = 0;\n                        let hoty = 0;\n                        let w = 2;\n                        let h = 2;\n\n                        //AND-mask\n                        for (let i = 0; i < andMask.length; i++) {\n                            push8(rect, andMask[i]);\n                        }\n                        //XOR-mask\n                        for (let i = 0; i < xorMask.length; i++) {\n                            push8(rect, xorMask[i]);\n                        }\n\n                        let expectedRgba = [0x00, 0x00, 0x00, 0x00,\n                                            0x00, 0x00, 0x00, 0x00,\n                                            0x33, 0x22, 0x11, 0xff,\n                                            0x00, 0x00, 0x00, 0xff];\n\n                        sendFbuMsg([{ x: hotx, y: hoty,\n                                      width: w, height: h,\n                                      encoding: 0x574d5664}],\n                                   [rect], client);\n\n                        expect(client._cursor.change)\n                            .to.have.been.calledOnce;\n                        expect(client._cursor.change)\n                            .to.have.been.calledWith(expectedRgba,\n                                                     hotx, hoty,\n                                                     w, h);\n                    });\n\n                    it('should update the cursor when type is alpha', function () {\n                        let data = [0xee, 0x55, 0xff, 0x00, // rgba\n                                    0x00, 0xff, 0x00, 0xff,\n                                    0x00, 0xff, 0x00, 0x22,\n                                    0x00, 0xff, 0x00, 0x22,\n                                    0x00, 0xff, 0x00, 0x22,\n                                    0x00, 0x00, 0xff, 0xee];\n                        let rect = [];\n                        push8(rect, 1); //cursor_type\n                        push8(rect, 0); //padding\n                        let hotx = 0;\n                        let hoty = 0;\n                        let w = 3;\n                        let h = 2;\n\n                        for (let i = 0; i < data.length; i++) {\n                            push8(rect, data[i]);\n                        }\n\n                        let expectedRgba = [0xee, 0x55, 0xff, 0x00,\n                                            0x00, 0xff, 0x00, 0xff,\n                                            0x00, 0xff, 0x00, 0x22,\n                                            0x00, 0xff, 0x00, 0x22,\n                                            0x00, 0xff, 0x00, 0x22,\n                                            0x00, 0x00, 0xff, 0xee];\n\n                        sendFbuMsg([{ x: hotx, y: hoty,\n                                      width: w, height: h,\n                                      encoding: 0x574d5664}],\n                                   [rect], client);\n\n                        expect(client._cursor.change)\n                            .to.have.been.calledOnce;\n                        expect(client._cursor.change)\n                            .to.have.been.calledWith(expectedRgba,\n                                                     hotx, hoty,\n                                                     w, h);\n                    });\n\n                    it('should not update cursor when incorrect cursor type given', function () {\n                        let rect = [];\n                        push8(rect, 3); // invalid cursor type\n                        push8(rect, 0); // padding\n\n                        client._cursor.change.resetHistory();\n                        sendFbuMsg([{ x: 0, y: 0, width: 2, height: 2,\n                                      encoding: 0x574d5664}],\n                                   [rect], client);\n\n                        expect(client._cursor.change)\n                            .to.not.have.been.called;\n                    });\n                });\n\n                it('should handle the last_rect pseudo-encoding', function () {\n                    sendFbuMsg([{ x: 0, y: 0, width: 0, height: 0, encoding: -224}], [[]], client, 100);\n                    // Send a bell message and make sure it is parsed\n                    let spy = sinon.spy();\n                    client.addEventListener(\"bell\", spy);\n                    client._sock._websocket._receiveData(new Uint8Array([0x02]));\n                    expect(spy).to.have.been.calledOnce;\n                });\n\n                it('should handle the DesktopName pseudo-encoding', function () {\n                    let data = [];\n                    push32(data, 13);\n                    pushString(data, \"som€ nam€\");\n\n                    const spy = sinon.spy();\n                    client.addEventListener(\"desktopname\", spy);\n\n                    sendFbuMsg([{ x: 0, y: 0, width: 0, height: 0, encoding: -307 }], [data], client);\n\n                    expect(client._fbName).to.equal('som€ nam€');\n                    expect(spy).to.have.been.calledOnce;\n                    expect(spy.args[0][0].detail.name).to.equal('som€ nam€');\n                });\n\n            });\n\n            describe('Caps Lock and Num Lock remote fixup', function () {\n                function sendLedStateUpdate(state) {\n                    let data = [];\n                    push8(data, state);\n                    sendFbuMsg([{ x: 0, y: 0, width: 0, height: 0, encoding: -261 }], [data], client);\n                }\n\n                let client;\n                beforeEach(function () {\n                    client = makeRFB();\n                    sinon.stub(client, 'sendKey');\n                });\n\n                it('should toggle caps lock if remote caps lock is on and local is off', function () {\n                    sendLedStateUpdate(0b100);\n                    client._handleKeyEvent(0x61, 'KeyA', true, null, false);\n\n                    expect(client.sendKey).to.have.been.calledThrice;\n                    expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFE5, \"CapsLock\", true);\n                    expect(client.sendKey.secondCall).to.have.been.calledWith(0xFFE5, \"CapsLock\", false);\n                    expect(client.sendKey.thirdCall).to.have.been.calledWith(0x61, \"KeyA\", true);\n                });\n\n                it('should toggle caps lock if remote caps lock is off and local is on', function () {\n                    sendLedStateUpdate(0b011);\n                    client._handleKeyEvent(0x41, 'KeyA', true, null, true);\n\n                    expect(client.sendKey).to.have.been.calledThrice;\n                    expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFE5, \"CapsLock\", true);\n                    expect(client.sendKey.secondCall).to.have.been.calledWith(0xFFE5, \"CapsLock\", false);\n                    expect(client.sendKey.thirdCall).to.have.been.calledWith(0x41, \"KeyA\", true);\n                });\n\n                it('should not toggle caps lock if remote caps lock is on and local is on', function () {\n                    sendLedStateUpdate(0b100);\n                    client._handleKeyEvent(0x41, 'KeyA', true, null, true);\n\n                    expect(client.sendKey).to.have.been.calledOnce;\n                    expect(client.sendKey.firstCall).to.have.been.calledWith(0x41, \"KeyA\", true);\n                });\n\n                it('should not toggle caps lock if remote caps lock is off and local is off', function () {\n                    sendLedStateUpdate(0b011);\n                    client._handleKeyEvent(0x61, 'KeyA', true, null, false);\n\n                    expect(client.sendKey).to.have.been.calledOnce;\n                    expect(client.sendKey.firstCall).to.have.been.calledWith(0x61, \"KeyA\", true);\n                });\n\n                it('should not toggle caps lock if the key is caps lock', function () {\n                    sendLedStateUpdate(0b011);\n                    client._handleKeyEvent(0xFFE5, 'CapsLock', true, null, true);\n\n                    expect(client.sendKey).to.have.been.calledOnce;\n                    expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFE5, \"CapsLock\", true);\n                });\n\n                it('should toggle caps lock only once', function () {\n                    sendLedStateUpdate(0b100);\n                    client._handleKeyEvent(0x61, 'KeyA', true, null, false);\n                    client._handleKeyEvent(0x61, 'KeyA', true, null, false);\n\n                    expect(client.sendKey).to.have.callCount(4);\n                    expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFE5, \"CapsLock\", true);\n                    expect(client.sendKey.secondCall).to.have.been.calledWith(0xFFE5, \"CapsLock\", false);\n                    expect(client.sendKey.thirdCall).to.have.been.calledWith(0x61, \"KeyA\", true);\n                    expect(client.sendKey.lastCall).to.have.been.calledWith(0x61, \"KeyA\", true);\n                });\n\n                it('should retain remote caps lock state on capslock key up', function () {\n                    sendLedStateUpdate(0b100);\n                    client._handleKeyEvent(0xFFE5, 'CapsLock', false, null, true);\n\n                    expect(client.sendKey).to.have.been.calledOnce;\n                    expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFE5, \"CapsLock\", false);\n                    expect(client._remoteCapsLock).to.equal(true);\n                });\n\n                it('should toggle num lock if remote num lock is on and local is off', function () {\n                    sendLedStateUpdate(0b010);\n                    client._handleKeyEvent(0xFF9C, 'NumPad1', true, false, null);\n\n                    expect(client.sendKey).to.have.been.calledThrice;\n                    expect(client.sendKey.firstCall).to.have.been.calledWith(0xFF7F, \"NumLock\", true);\n                    expect(client.sendKey.secondCall).to.have.been.calledWith(0xFF7F, \"NumLock\", false);\n                    expect(client.sendKey.thirdCall).to.have.been.calledWith(0xFF9C, \"NumPad1\", true);\n                });\n\n                it('should toggle num lock if remote num lock is off and local is on', function () {\n                    sendLedStateUpdate(0b101);\n                    client._handleKeyEvent(0xFFB1, 'NumPad1', true, true, null);\n\n                    expect(client.sendKey).to.have.been.calledThrice;\n                    expect(client.sendKey.firstCall).to.have.been.calledWith(0xFF7F, \"NumLock\", true);\n                    expect(client.sendKey.secondCall).to.have.been.calledWith(0xFF7F, \"NumLock\", false);\n                    expect(client.sendKey.thirdCall).to.have.been.calledWith(0xFFB1, \"NumPad1\", true);\n                });\n\n                it('should not toggle num lock if remote num lock is on and local is on', function () {\n                    sendLedStateUpdate(0b010);\n                    client._handleKeyEvent(0xFFB1, 'NumPad1', true,  true, null);\n\n                    expect(client.sendKey).to.have.been.calledOnce;\n                    expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFB1, \"NumPad1\", true);\n                });\n\n                it('should not toggle num lock if remote num lock is off and local is off', function () {\n                    sendLedStateUpdate(0b101);\n                    client._handleKeyEvent(0xFF9C, 'NumPad1', true, false, null);\n\n                    expect(client.sendKey).to.have.been.calledOnce;\n                    expect(client.sendKey.firstCall).to.have.been.calledWith(0xFF9C, \"NumPad1\", true);\n                });\n\n                it('should not toggle num lock if the key is num lock', function () {\n                    sendLedStateUpdate(0b101);\n                    client._handleKeyEvent(0xFF7F, 'NumLock', true, true, null);\n\n                    expect(client.sendKey).to.have.been.calledOnce;\n                    expect(client.sendKey.firstCall).to.have.been.calledWith(0xFF7F, \"NumLock\", true);\n                });\n\n                it('should not toggle num lock if local state is unknown', function () {\n                    sendLedStateUpdate(0b010);\n                    client._handleKeyEvent(0xFFB1, 'NumPad1', true, null, null);\n\n                    expect(client.sendKey).to.have.been.calledOnce;\n                    expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFB1, \"NumPad1\", true);\n                });\n\n                it('should toggle num lock only once', function () {\n                    sendLedStateUpdate(0b010);\n                    client._handleKeyEvent(0xFF9C, 'NumPad1', true, false, null);\n                    client._handleKeyEvent(0xFF9C, 'NumPad1', true, false, null);\n\n                    expect(client.sendKey).to.have.callCount(4);\n                    expect(client.sendKey.firstCall).to.have.been.calledWith(0xFF7F, \"NumLock\", true);\n                    expect(client.sendKey.secondCall).to.have.been.calledWith(0xFF7F, \"NumLock\", false);\n                    expect(client.sendKey.thirdCall).to.have.been.calledWith(0xFF9C, \"NumPad1\", true);\n                    expect(client.sendKey.lastCall).to.have.been.calledWith(0xFF9C, \"NumPad1\", true);\n                });\n            });\n        });\n\n        describe('XVP message handling', function () {\n            it('should set the XVP version and fire the callback with the version on XVP_INIT', function () {\n                const spy = sinon.spy();\n                client.addEventListener(\"capabilities\", spy);\n                client._sock._websocket._receiveData(new Uint8Array([250, 0, 10, 1]));\n                expect(client._rfbXvpVer).to.equal(10);\n                expect(spy).to.have.been.calledOnce;\n                expect(spy.args[0][0].detail.capabilities.power).to.be.true;\n                expect(client.capabilities.power).to.be.true;\n            });\n\n            it('should fail on unknown XVP message types', function () {\n                let callback = sinon.spy();\n                client.addEventListener(\"disconnect\", callback);\n\n                client._sock._websocket._receiveData(new Uint8Array([250, 0, 10, 237]));\n\n                expect(callback).to.have.been.calledOnce;\n                expect(callback.args[0][0].detail.clean).to.be.false;\n            });\n        });\n\n        describe('Normal clipboard handling receive', function () {\n            it('should not dispatch a clipboard event following successful async write clipboard', async function () {\n                client._viewOnly = false;\n                client._asyncClipboard = {\n                    writeClipboard: sinon.stub().returns(true),\n                };\n                const expectedStr = 'cheese!';\n                const data = [3, 0, 0, 0];\n                push32(data, expectedStr.length);\n                for (let i = 0; i < expectedStr.length; i++) { data.push(expectedStr.charCodeAt(i)); }\n\n                const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');\n\n                client._sock._websocket._receiveData(new Uint8Array(data));\n\n                expect(client._asyncClipboard.writeClipboard.calledOnceWith(\n                    expectedStr\n                )).to.be.true;\n                expect(dispatchEventSpy.calledWith(\n                    new CustomEvent(\"clipboard\", {detail: {expectedStr: expectedStr}})\n                )).to.be.false;\n            });\n\n            it('should dispatch a clipboard event following unsuccessful async write clipboard', async function () {\n                client._viewOnly = false;\n                client._asyncClipboard = {\n                    writeClipboard: sinon.stub().returns(false),\n                };\n                const expectedStr = 'cheese!';\n                const data = [3, 0, 0, 0];\n                push32(data, expectedStr.length);\n                for (let i = 0; i < expectedStr.length; i++) { data.push(expectedStr.charCodeAt(i)); }\n\n                const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');\n\n                client._sock._websocket._receiveData(new Uint8Array(data));\n\n                expect(client._asyncClipboard.writeClipboard.calledOnceWith(\n                    expectedStr\n                )).to.be.true;\n                expect(dispatchEventSpy.calledOnceWith(\n                    new CustomEvent(\"clipboard\", {detail: {expectedStr: expectedStr}})\n                )).to.be.true;\n            });\n        });\n\n        describe('Extended clipboard handling', function () {\n\n            describe('Extended clipboard initialization', function () {\n                beforeEach(function () {\n                    sinon.spy(RFB.messages, 'extendedClipboardCaps');\n                });\n\n                afterEach(function () {\n                    RFB.messages.extendedClipboardCaps.restore();\n                });\n\n                it('should update capabilities when receiving a Caps message', function () {\n                    let data = [3, 0, 0, 0];\n                    const flags = [0x1F, 0x00, 0x00, 0x03];\n                    let fileSizes = [0x00, 0x00, 0x00, 0x1E,\n                                     0x00, 0x00, 0x00, 0x3C];\n\n                    push32(data, toUnsigned32bit(-12));\n                    data = data.concat(flags);\n                    data = data.concat(fileSizes);\n                    client._sock._websocket._receiveData(new Uint8Array(data));\n\n                    // Check that we give an response caps when we receive one\n                    expect(RFB.messages.extendedClipboardCaps).to.have.been.calledOnce;\n\n                    // FIXME: Can we avoid checking internal variables?\n                    expect(client._clipboardServerCapabilitiesFormats[0]).to.not.equal(true);\n                    expect(client._clipboardServerCapabilitiesFormats[1]).to.equal(true);\n                    expect(client._clipboardServerCapabilitiesFormats[2]).to.equal(true);\n                    expect(client._clipboardServerCapabilitiesActions[(1 << 24)]).to.equal(true);\n                });\n\n\n            });\n\n            describe('Extended clipboard handling receive', function () {\n\n                beforeEach(function () {\n                    // Send our capabilities\n                    let data = [3, 0, 0, 0];\n                    const flags = [0x1F, 0x00, 0x00, 0x01];\n                    let fileSizes = [0x00, 0x00, 0x00, 0x1E];\n\n                    push32(data, toUnsigned32bit(-8));\n                    data = data.concat(flags);\n                    data = data.concat(fileSizes);\n                    client._sock._websocket._receiveData(new Uint8Array(data));\n                });\n\n                it('should not dispatch a clipboard event following successful async write clipboard', async function () {\n                    client._viewOnly = false;\n                    client._asyncClipboard = {\n                        writeClipboard: sinon.stub().returns(true),\n                    };\n                    let expectedData = \"Schnitzel\";\n                    let data = [3, 0, 0, 0];\n                    const flags = [0x10, 0x00, 0x00, 0x01];\n\n                    let text = encodeUTF8(\"Schnitzel\");\n                    let deflatedText = deflateWithSize(text);\n\n                    // How much data we are sending.\n                    push32(data, toUnsigned32bit(-(4 + deflatedText.length)));\n\n                    data = data.concat(flags);\n                    data = data.concat(Array.from(deflatedText));\n\n                    const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');\n\n                    client._sock._websocket._receiveData(new Uint8Array(data));\n\n                    expect(client._asyncClipboard.writeClipboard.calledOnceWith(\n                        expectedData\n                    )).to.be.true;\n                    expect(dispatchEventSpy.calledOnceWith(\n                        new CustomEvent(\"clipboard\", {detail: {expectedData: expectedData}})\n                    )).to.be.false;\n                });\n                it('should dispatch a clipboard event following unsuccessful async write clipboard', async function () {\n                    client._viewOnly = false;\n                    client._asyncClipboard = {\n                        writeClipboard: sinon.stub().returns(false),\n                    };\n                    let expectedData = \"Potatoes\";\n                    let data = [3, 0, 0, 0];\n                    const flags = [0x10, 0x00, 0x00, 0x01];\n\n                    let text = encodeUTF8(\"Potatoes\");\n                    let deflatedText = deflateWithSize(text);\n\n                    // How much data we are sending.\n                    push32(data, toUnsigned32bit(-(4 + deflatedText.length)));\n\n                    data = data.concat(flags);\n                    data = data.concat(Array.from(deflatedText));\n\n                    const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');\n\n                    client._sock._websocket._receiveData(new Uint8Array(data));\n\n                    expect(client._asyncClipboard.writeClipboard.calledOnceWith(\n                        expectedData\n                    )).to.be.true;\n                    expect(dispatchEventSpy.calledOnceWith(\n                        new CustomEvent(\"clipboard\", {detail: {expectedData: expectedData}})\n                    )).to.be.true;\n                });\n\n                describe('Handle Provide', function () {\n                    it('should update clipboard with correct Unicode data from a Provide message', async function () {\n                        client._viewOnly = false;\n                        client._asyncClipboard = {\n                            writeClipboard: sinon.stub().returns(false),\n                        };\n                        let expectedData = \"Aå漢字!\";\n                        let data = [3, 0, 0, 0];\n                        const flags = [0x10, 0x00, 0x00, 0x01];\n\n                        let text = encodeUTF8(\"Aå漢字!\");\n                        let deflatedText = deflateWithSize(text);\n\n                        // How much data we are sending.\n                        push32(data, toUnsigned32bit(-(4 + deflatedText.length)));\n\n                        data = data.concat(flags);\n                        data = data.concat(Array.from(deflatedText));\n\n                        const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');\n\n                        client._sock._websocket._receiveData(new Uint8Array(data));\n\n                        expect(client._asyncClipboard.writeClipboard.calledOnceWith(\n                            expectedData\n                        )).to.be.true;\n                        expect(dispatchEventSpy.calledOnceWith(\n                            new CustomEvent(\"clipboard\", {detail: {expectedData: expectedData}})\n                        )).to.be.true;\n                    });\n\n                    it('should update clipboard with correct escape characters from a Provide message ', async function () {\n                        client._viewOnly = false;\n                        client._asyncClipboard = {\n                            writeClipboard: sinon.stub().returns(false),\n                        };\n                        let expectedData = \"Oh\\nmy\\n!\";\n                        let data = [3, 0, 0, 0];\n                        const flags = [0x10, 0x00, 0x00, 0x01];\n\n                        let text = encodeUTF8(\"Oh\\r\\nmy\\r\\n!\\0\");\n\n                        let deflatedText = deflateWithSize(text);\n\n                        // How much data we are sending.\n                        push32(data, toUnsigned32bit(-(4 + deflatedText.length)));\n\n                        data = data.concat(flags);\n                        data = data.concat(Array.from(deflatedText));\n\n                        const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');\n\n                        client._sock._websocket._receiveData(new Uint8Array(data));\n\n                        expect(client._asyncClipboard.writeClipboard.calledOnceWith(\n                            expectedData\n                        )).to.be.true;\n                        expect(dispatchEventSpy.calledOnceWith(\n                            new CustomEvent(\"clipboard\", {detail: {expectedData: expectedData}})\n                        )).to.be.true;\n                    });\n\n                    it('should be able to handle large Provide messages', async function () {\n                        client._viewOnly = false;\n                        client._asyncClipboard = {\n                            writeClipboard: sinon.stub().returns(false),\n                        };\n                        let expectedData = \"hello\".repeat(100000);\n                        let data = [3, 0, 0, 0];\n                        const flags = [0x10, 0x00, 0x00, 0x01];\n\n                        let text = encodeUTF8(expectedData + \"\\0\");\n\n                        let deflatedText = deflateWithSize(text);\n\n                        // How much data we are sending.\n                        push32(data, toUnsigned32bit(-(4 + deflatedText.length)));\n\n                        data = data.concat(flags);\n                        data = data.concat(Array.from(deflatedText));\n\n                        const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');\n\n                        client._sock._websocket._receiveData(new Uint8Array(data));\n\n                        expect(client._asyncClipboard.writeClipboard.calledOnceWith(\n                            expectedData\n                        )).to.be.true;\n                        expect(dispatchEventSpy.calledOnceWith(\n                            new CustomEvent(\"clipboard\", {detail: {expectedData: expectedData}})\n                        )).to.be.true;\n                    });\n\n                });\n\n                describe('Handle Notify', function () {\n                    beforeEach(function () {\n                        sinon.spy(RFB.messages, 'extendedClipboardRequest');\n                    });\n\n                    afterEach(function () {\n                        RFB.messages.extendedClipboardRequest.restore();\n                    });\n\n                    it('should make a request with supported formats when receiving a notify message', function () {\n                        let data = [3, 0, 0, 0];\n                        const flags = [0x08, 0x00, 0x00, 0x07];\n                        push32(data, toUnsigned32bit(-4));\n                        data = data.concat(flags);\n                        let expectedData = [0x01];\n\n                        client._sock._websocket._receiveData(new Uint8Array(data));\n\n                        expect(RFB.messages.extendedClipboardRequest).to.have.been.calledOnce;\n                        expect(RFB.messages.extendedClipboardRequest).to.have.been.calledWith(client._sock, expectedData);\n                    });\n                });\n\n                describe('Handle Peek', function () {\n                    beforeEach(function () {\n                        sinon.spy(RFB.messages, 'extendedClipboardNotify');\n                    });\n\n                    afterEach(function () {\n                        RFB.messages.extendedClipboardNotify.restore();\n                    });\n\n                    it('should send an empty Notify when receiving a Peek and no excisting clipboard data', function () {\n                        let data = [3, 0, 0, 0];\n                        const flags = [0x04, 0x00, 0x00, 0x00];\n                        push32(data, toUnsigned32bit(-4));\n                        data = data.concat(flags);\n                        let expectedData = [];\n\n                        client._sock._websocket._receiveData(new Uint8Array(data));\n\n                        expect(RFB.messages.extendedClipboardNotify).to.have.been.calledOnce;\n                        expect(RFB.messages.extendedClipboardNotify).to.have.been.calledWith(client._sock, expectedData);\n                    });\n\n                    it('should send a Notify message with supported formats when receiving a Peek', function () {\n                        let data = [3, 0, 0, 0];\n                        const flags = [0x04, 0x00, 0x00, 0x00];\n                        push32(data, toUnsigned32bit(-4));\n                        data = data.concat(flags);\n                        let expectedData = [0x01];\n\n                        // Needed to have clipboard data to read.\n                        // This will trigger a call to Notify, reset history\n                        client.clipboardPasteFrom(\"HejHej\");\n                        RFB.messages.extendedClipboardNotify.resetHistory();\n\n                        client._sock._websocket._receiveData(new Uint8Array(data));\n\n                        expect(RFB.messages.extendedClipboardNotify).to.have.been.calledOnce;\n                        expect(RFB.messages.extendedClipboardNotify).to.have.been.calledWith(client._sock, expectedData);\n                    });\n                });\n\n                describe('Handle Request', function () {\n                    beforeEach(function () {\n                        sinon.spy(RFB.messages, 'extendedClipboardProvide');\n                    });\n\n                    afterEach(function () {\n                        RFB.messages.extendedClipboardProvide.restore();\n                    });\n\n                    it('should send a Provide message with supported formats when receiving a Request', function () {\n                        let data = [3, 0, 0, 0];\n                        const flags = [0x02, 0x00, 0x00, 0x01];\n                        push32(data, toUnsigned32bit(-4));\n                        data = data.concat(flags);\n                        let expectedData = [0x01];\n\n                        client.clipboardPasteFrom(\"HejHej\");\n                        expect(RFB.messages.extendedClipboardProvide).to.not.have.been.called;\n\n                        client._sock._websocket._receiveData(new Uint8Array(data));\n\n                        expect(RFB.messages.extendedClipboardProvide).to.have.been.calledOnce;\n                        expect(RFB.messages.extendedClipboardProvide).to.have.been.calledWith(client._sock, expectedData, [\"HejHej\"]);\n                    });\n                });\n            });\n\n        });\n\n        it('should fire the bell callback on Bell', function () {\n            const spy = sinon.spy();\n            client.addEventListener(\"bell\", spy);\n            client._sock._websocket._receiveData(new Uint8Array([2]));\n            expect(spy).to.have.been.calledOnce;\n        });\n\n        it('should respond correctly to ServerFence', function () {\n            const payload = \"foo\\x00ab9\";\n\n            let esock = new Websock();\n            let ews = new FakeWebSocket();\n            ews._open();\n            esock.attach(ews);\n\n            // ClientFence and ServerFence are identical in structure\n            RFB.messages.clientFence(esock, (1<<0) | (1<<1), payload);\n            let expected = ews._getSentData();\n            RFB.messages.clientFence(esock, 0xffffffff, payload);\n            let incoming = ews._getSentData();\n\n            client._sock._websocket._receiveData(incoming);\n\n            expect(client._sock).to.have.sent(expected);\n\n            RFB.messages.clientFence(esock, (1<<0), payload);\n            expected = ews._getSentData();\n            RFB.messages.clientFence(esock, (1<<0) | (1<<31), payload);\n            incoming = ews._getSentData();\n\n            client._sock._websocket._receiveData(incoming);\n\n            expect(client._sock).to.have.sent(expected);\n        });\n\n        it('should enable continuous updates on first EndOfContinousUpdates', function () {\n            let esock = new Websock();\n            let ews = new FakeWebSocket();\n            ews._open();\n            esock.attach(ews);\n            RFB.messages.enableContinuousUpdates(esock, true, 0, 0, 640, 20);\n            let expected = ews._getSentData();\n\n            expect(client._enabledContinuousUpdates).to.be.false;\n\n            client._sock._websocket._receiveData(new Uint8Array([150]));\n\n            expect(client._enabledContinuousUpdates).to.be.true;\n            expect(client._sock).to.have.sent(expected);\n        });\n\n        it('should disable continuous updates on subsequent EndOfContinousUpdates', function () {\n            client._enabledContinuousUpdates = true;\n            client._supportsContinuousUpdates = true;\n\n            client._sock._websocket._receiveData(new Uint8Array([150]));\n\n            expect(client._enabledContinuousUpdates).to.be.false;\n        });\n\n        it('should update continuous updates on resize', function () {\n            let esock = new Websock();\n            let ews = new FakeWebSocket();\n            ews._open();\n            esock.attach(ews);\n            RFB.messages.enableContinuousUpdates(esock, true, 0, 0, 90, 700);\n            let expected = ews._getSentData();\n\n            client._resize(450, 160);\n\n            expect(client._sock).to.have.sent(new Uint8Array([]));\n\n            client._enabledContinuousUpdates = true;\n\n            client._resize(90, 700);\n\n            expect(client._sock).to.have.sent(expected);\n        });\n\n        it('should fail on an unknown message type', function () {\n            let callback = sinon.spy();\n            client.addEventListener(\"disconnect\", callback);\n\n            client._sock._websocket._receiveData(new Uint8Array([87]));\n\n            expect(callback).to.have.been.calledOnce;\n            expect(callback.args[0][0].detail.clean).to.be.false;\n        });\n    });\n\n    describe('Asynchronous events', function () {\n        let client;\n        let pointerEvent;\n        let extendedPointerEvent;\n        let keyEvent;\n        let qemuKeyEvent;\n\n        beforeEach(function () {\n            client = makeRFB();\n            client._display.resize(100, 100);\n\n            // We need to disable this as focusing the canvas will\n            // cause the browser to scoll to it, messing up our\n            // client coordinate calculations\n            client.focusOnClick = false;\n\n            pointerEvent = sinon.spy(RFB.messages, 'pointerEvent');\n            extendedPointerEvent = sinon.spy(RFB.messages, 'extendedPointerEvent');\n            keyEvent = sinon.spy(RFB.messages, 'keyEvent');\n            qemuKeyEvent = sinon.spy(RFB.messages, 'QEMUExtendedKeyEvent');\n        });\n\n        afterEach(function () {\n            pointerEvent.restore();\n            extendedPointerEvent.restore();\n            keyEvent.restore();\n            qemuKeyEvent.restore();\n        });\n\n        describe('Mouse events', function () {\n\n            it('should not send button messages in view-only mode', function () {\n                client._viewOnly = true;\n                sendMouseButtonEvent(10, 10, true, 0x1, client);\n\n                clock.tick(50);\n                expect(pointerEvent).to.not.have.been.called;\n            });\n\n            it('should not send movement messages in view-only mode', function () {\n                client._viewOnly = true;\n                sendMouseMoveEvent(10, 10, 0x0, client);\n\n                clock.tick(50);\n                expect(pointerEvent).to.not.have.been.called;\n            });\n\n            it('should handle left mouse button', function () {\n                sendMouseButtonEvent(10, 10, true, 0x1, client);\n\n                expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                 10, 10, 0x1);\n                pointerEvent.resetHistory();\n\n                sendMouseButtonEvent(10, 10, false, 0x0, client);\n\n                expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                 10, 10, 0x0);\n            });\n\n            it('should handle middle mouse button', function () {\n                sendMouseButtonEvent(10, 10, true, 0x4, client);\n\n                expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                 10, 10, 0x2);\n                pointerEvent.resetHistory();\n\n                sendMouseButtonEvent(10, 10, false, 0x0, client);\n\n                expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                 10, 10, 0x0);\n            });\n\n            it('should handle right mouse button', function () {\n                sendMouseButtonEvent(10, 10, true, 0x2, client);\n\n                expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                 10, 10, 0x4);\n                pointerEvent.resetHistory();\n\n                sendMouseButtonEvent(10, 10, false, 0x0, client);\n\n                expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                 10, 10, 0x0);\n            });\n\n            it('should handle multiple mouse buttons', function () {\n                sendMouseButtonEvent(10, 10, true, 0x1, client);\n                sendMouseButtonEvent(10, 10, true, 0x3, client);\n\n                expect(pointerEvent).to.have.been.calledTwice;\n                expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                       10, 10, 0x1);\n                expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                        10, 10, 0x5);\n\n                pointerEvent.resetHistory();\n\n\n                sendMouseButtonEvent(10, 10, false, 0x2, client);\n                sendMouseButtonEvent(10, 10, false, 0x0, client);\n\n                expect(pointerEvent).to.have.been.calledTwice;\n                expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                       10, 10, 0x4);\n                expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                        10, 10, 0x0);\n            });\n\n            it('should handle mouse movement', function () {\n                sendMouseMoveEvent(50, 70, 0x0, client);\n                expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                 50, 70, 0x0);\n            });\n\n            it('should handle click and drag', function () {\n                sendMouseButtonEvent(10, 10, true, 0x1, client);\n                sendMouseMoveEvent(50, 70, 0x1, client);\n\n                expect(pointerEvent).to.have.been.calledTwice;\n                expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                       10, 10, 0x1);\n                expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                        50, 70, 0x1);\n\n                pointerEvent.resetHistory();\n\n                sendMouseButtonEvent(50, 70, false, 0x0, client);\n\n                expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                 50, 70, 0x0);\n            });\n\n            it('should send extended pointer event when server supports extended pointer events', function () {\n                // Enable extended pointer events\n                sendFbuMsg([{ x: 0, y: 0, width: 0, height: 0, encoding: -316 }], [[]], client);\n\n                sendMouseButtonEvent(50, 70, true, 0x10, client);\n\n                expect(extendedPointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                         50, 70, 0x100);\n            });\n\n            it('should send normal pointer event when server does not support extended pointer events', function () {\n                sendMouseButtonEvent(50, 70, true, 0x10, client);\n\n                expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                 50, 70, 0x100);\n            });\n\n            describe('Event aggregation', function () {\n                it('should send a single pointer event on mouse movement', function () {\n                    sendMouseMoveEvent(50, 70, 0x0, client);\n                    clock.tick(100);\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     50, 70, 0x0);\n                });\n\n                it('should delay one move if two events are too close', function () {\n                    sendMouseMoveEvent(18, 30, 0x0, client);\n                    sendMouseMoveEvent(20, 50, 0x0, client);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     18, 30, 0x0);\n                    pointerEvent.resetHistory();\n\n                    clock.tick(100);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     20, 50, 0x0);\n                });\n\n                it('should only send first and last move of many close events', function () {\n                    sendMouseMoveEvent(18, 30, 0x0, client);\n                    sendMouseMoveEvent(20, 50, 0x0, client);\n                    sendMouseMoveEvent(21, 55, 0x0, client);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     18, 30, 0x0);\n                    pointerEvent.resetHistory();\n\n                    clock.tick(100);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     21, 55, 0x0);\n                });\n\n                // We selected the 17ms since that is ~60 FPS\n                it('should send move events every 17 ms', function () {\n                    sendMouseMoveEvent(1, 10, 0x0, client);  // instant send\n                    clock.tick(10);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     1, 10, 0x0);\n                    pointerEvent.resetHistory();\n\n                    sendMouseMoveEvent(2, 20, 0x0, client);  // delayed\n                    clock.tick(10);        // timeout send\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     2, 20, 0x0);\n                    pointerEvent.resetHistory();\n\n                    sendMouseMoveEvent(3, 30, 0x0, client);  // delayed\n                    clock.tick(10);\n                    sendMouseMoveEvent(4, 40, 0x0, client);  // delayed\n                    clock.tick(10);        // timeout send\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     4, 40, 0x0);\n                    pointerEvent.resetHistory();\n\n                    sendMouseMoveEvent(5, 50, 0x0, client);  // delayed\n\n                    expect(pointerEvent).to.not.have.been.called;\n                });\n\n                it('should send waiting move events before a button press', function () {\n                    sendMouseMoveEvent(13, 9, 0x0, client);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     13, 9, 0x0);\n                    pointerEvent.resetHistory();\n\n                    sendMouseMoveEvent(20, 70, 0x0, client);\n\n                    expect(pointerEvent).to.not.have.been.called;\n\n                    sendMouseButtonEvent(20, 70, true, 0x1, client);\n\n                    expect(pointerEvent).to.have.been.calledTwice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           20, 70, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            20, 70, 0x1);\n                });\n\n                it('should send move events with enough time apart normally', function () {\n                    sendMouseMoveEvent(58, 60, 0x0, client);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     58, 60, 0x0);\n                    pointerEvent.resetHistory();\n\n                    clock.tick(20);\n\n                    sendMouseMoveEvent(25, 60, 0x0, client);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     25, 60, 0x0);\n                    pointerEvent.resetHistory();\n                });\n\n                it('should not send waiting move events if disconnected', function () {\n                    sendMouseMoveEvent(88, 99, 0x0, client);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     88, 99, 0x0);\n                    pointerEvent.resetHistory();\n\n                    sendMouseMoveEvent(66, 77, 0x0, client);\n                    client.disconnect();\n                    clock.tick(20);\n\n                    expect(pointerEvent).to.not.have.been.called;\n                });\n            });\n\n            it.skip('should block click events', function () {\n                /* FIXME */\n            });\n\n            it.skip('should block contextmenu events', function () {\n                /* FIXME */\n            });\n        });\n\n        describe('Wheel events', function () {\n            function sendWheelEvent(x, y, dx, dy, mode=0, buttons=0) {\n                let pos = elementToClient(x, y, client);\n                let ev;\n\n                ev = new WheelEvent('wheel',\n                                    { 'screenX': pos.x + window.screenX,\n                                      'screenY': pos.y + window.screenY,\n                                      'clientX': pos.x,\n                                      'clientY': pos.y,\n                                      'deltaX': dx,\n                                      'deltaY': dy,\n                                      'deltaMode': mode,\n                                      'buttons': buttons });\n                client._canvas.dispatchEvent(ev);\n            }\n\n            it('should handle wheel up event', function () {\n                sendWheelEvent(10, 10, 0, -50);\n\n                expect(pointerEvent).to.have.been.calledTwice;\n                expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                       10, 10, 1<<3);\n                expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                        10, 10, 0);\n            });\n\n            it('should handle wheel down event', function () {\n                sendWheelEvent(10, 10, 0, 50);\n\n                expect(pointerEvent).to.have.been.calledTwice;\n                expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                       10, 10, 1<<4);\n                expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                        10, 10, 0);\n            });\n\n            it('should handle wheel left event', function () {\n                sendWheelEvent(10, 10, -50, 0);\n\n                expect(pointerEvent).to.have.been.calledTwice;\n                expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                       10, 10, 1<<5);\n                expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                        10, 10, 0);\n            });\n\n            it('should handle wheel right event', function () {\n                sendWheelEvent(10, 10, 50, 0);\n\n                expect(pointerEvent).to.have.been.calledTwice;\n                expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                       10, 10, 1<<6);\n                expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                        10, 10, 0);\n            });\n\n            it('should ignore wheel when in view only', function () {\n                client._viewOnly = true;\n\n                sendWheelEvent(10, 10, 50, 0);\n\n                expect(pointerEvent).to.not.have.been.called;\n            });\n\n            it('should accumulate wheel events if small enough', function () {\n                sendWheelEvent(10, 10, 0, 20);\n                sendWheelEvent(10, 10, 0, 20);\n\n                expect(pointerEvent).to.not.have.been.called;\n\n                sendWheelEvent(10, 10, 0, 20);\n\n                expect(pointerEvent).to.have.been.calledTwice;\n                expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                       10, 10, 1<<4);\n                expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                        10, 10, 0);\n            });\n\n            it('should not accumulate large wheel events', function () {\n                sendWheelEvent(10, 10, 0, 400);\n\n                expect(pointerEvent).to.have.been.calledTwice;\n                expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                       10, 10, 1<<4);\n                expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                        10, 10, 0);\n            });\n\n            it('should handle line based wheel event', function () {\n                sendWheelEvent(10, 10, 0, 3, 1);\n\n                expect(pointerEvent).to.have.been.calledTwice;\n                expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                       10, 10, 1<<4);\n                expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                        10, 10, 0);\n            });\n\n            it('should handle page based wheel event', function () {\n                sendWheelEvent(10, 10, 0, 3, 2);\n\n                expect(pointerEvent).to.have.been.calledTwice;\n                expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                       10, 10, 1<<4);\n                expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                        10, 10, 0);\n            });\n\n            it('should handle wheel event with buttons pressed', function () {\n                sendMouseButtonEvent(10, 10, true, 0x1, client);\n                sendWheelEvent(10, 10, 0, 50, 0, 0x1);\n\n                expect(pointerEvent).to.have.been.called.calledThrice;\n\n                expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                       10, 10, 0x1);\n                expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                        10, 10, 0x11);\n                expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                       10, 10, 0x1);\n            });\n\n        });\n\n        describe('Keyboard events', function () {\n            it('should send a key message on a key press', function () {\n                let esock = new Websock();\n                let ews = new FakeWebSocket();\n                ews._open();\n                esock.attach(ews);\n                RFB.messages.keyEvent(esock, 0x41, 1);\n                let expected = ews._getSentData();\n\n                client._handleKeyEvent(0x41, 'KeyA', true);\n\n                expect(client._sock).to.have.sent(expected);\n            });\n\n            it('should not send messages in view-only mode', function () {\n                client._viewOnly = true;\n                sinon.spy(client._sock, 'flush');\n                client._handleKeyEvent('a', 'KeyA', true);\n                expect(client._sock.flush).to.not.have.been.called;\n            });\n        });\n\n        describe('Gesture event handlers', function () {\n            describe('Gesture onetap', function () {\n                it('should handle onetap events', function () {\n                    let bmask = 0x1;\n\n                    gestureStart('onetap', 20, 40, client);\n                    gestureEnd('onetap', 20, 40, client);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                });\n\n                it('should keep same position for multiple onetap events', function () {\n                    let bmask = 0x1;\n\n                    gestureStart('onetap', 20, 40, client);\n                    gestureEnd('onetap', 20, 40, client);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n\n                    pointerEvent.resetHistory();\n\n                    gestureStart('onetap', 20, 50, client);\n                    gestureEnd('onetap', 20, 50, client);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n\n                    pointerEvent.resetHistory();\n\n                    gestureStart('onetap', 30, 50, client);\n                    gestureEnd('onetap', 30, 50, client);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                });\n\n                it('should not keep same position for onetap events when too far apart', function () {\n                    let bmask = 0x1;\n\n                    gestureStart('onetap', 20, 40, client);\n                    gestureEnd('onetap', 20, 40, client);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n\n                    pointerEvent.resetHistory();\n\n                    gestureStart('onetap', 80, 95, client);\n                    gestureEnd('onetap', 80, 95, client);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           80, 95, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            80, 95, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           80, 95, 0x0);\n                });\n\n                it('should not keep same position for onetap events when enough time inbetween', function () {\n                    let bmask = 0x1;\n\n                    gestureStart('onetap', 10, 20, client);\n                    gestureEnd('onetap', 10, 20, client);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           10, 20, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            10, 20, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           10, 20, 0x0);\n\n                    pointerEvent.resetHistory();\n                    this.clock.tick(1500);\n\n                    gestureStart('onetap', 15, 20, client);\n                    gestureEnd('onetap', 15, 20, client);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           15, 20, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            15, 20, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           15, 20, 0x0);\n\n                    pointerEvent.resetHistory();\n                });\n            });\n\n            describe('Gesture twotap', function () {\n                it('should handle gesture twotap events', function () {\n                    let bmask = 0x4;\n\n                    gestureStart(\"twotap\", 20, 40, client);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                });\n\n                it('should keep same position for multiple twotap events', function () {\n                    let bmask = 0x4;\n\n                    for (let offset = 0;offset < 30;offset += 10) {\n                        pointerEvent.resetHistory();\n\n                        gestureStart('twotap', 20, 40 + offset, client);\n                        gestureEnd('twotap', 20, 40 + offset, client);\n\n                        expect(pointerEvent).to.have.been.calledThrice;\n                        expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                               20, 40, 0x0);\n                        expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                                20, 40, bmask);\n                        expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                               20, 40, 0x0);\n                    }\n                });\n            });\n\n            describe('Gesture threetap', function () {\n                it('should handle gesture start for threetap events', function () {\n                    let bmask = 0x2;\n\n                    gestureStart(\"threetap\", 20, 40, client);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                });\n\n                it('should keep same position for multiple threetap events', function () {\n                    let bmask = 0x2;\n\n                    for (let offset = 0;offset < 30;offset += 10) {\n                        pointerEvent.resetHistory();\n\n                        gestureStart('threetap', 20, 40 + offset, client);\n                        gestureEnd('threetap', 20, 40 + offset, client);\n\n                        expect(pointerEvent).to.have.been.calledThrice;\n                        expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                               20, 40, 0x0);\n                        expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                                20, 40, bmask);\n                        expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                               20, 40, 0x0);\n                    }\n                });\n            });\n\n            describe('Gesture drag', function () {\n                it('should handle gesture drag events', function () {\n                    let bmask = 0x1;\n\n                    gestureStart('drag', 20, 40, client);\n\n                    expect(pointerEvent).to.have.been.calledTwice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n\n                    pointerEvent.resetHistory();\n\n                    gestureMove('drag', 30, 50, client);\n                    clock.tick(50);\n\n                    expect(pointerEvent).to.have.been.calledOnce;\n                    expect(pointerEvent).to.have.been.calledWith(client._sock,\n                                                                 30, 50, bmask);\n\n                    pointerEvent.resetHistory();\n\n                    gestureEnd('drag', 30, 50, client);\n\n                    expect(pointerEvent).to.have.been.calledTwice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           30, 50, bmask);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            30, 50, 0x0);\n                });\n            });\n\n            describe('Gesture long press', function () {\n                it('should handle long press events', function () {\n                    let bmask = 0x4;\n\n                    gestureStart('longpress', 20, 40, client);\n\n                    expect(pointerEvent).to.have.been.calledTwice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n                    pointerEvent.resetHistory();\n\n                    gestureMove('longpress', 40, 60, client);\n                    clock.tick(50);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     40, 60, bmask);\n\n                    pointerEvent.resetHistory();\n\n                    gestureEnd('longpress', 40, 60, client);\n\n                    expect(pointerEvent).to.have.been.calledTwice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           40, 60, bmask);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            40, 60, 0x0);\n                });\n            });\n\n            describe('Gesture twodrag', function () {\n                it('should handle gesture twodrag up events', function () {\n                    let bmask = 0x10; // Button mask for scroll down\n\n                    gestureStart('twodrag', 20, 40, client, 0, 0);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     20, 40, 0x0);\n\n                    pointerEvent.resetHistory();\n\n                    gestureMove('twodrag', 20, 40, client, 0, -60);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                });\n\n                it('should handle gesture twodrag down events', function () {\n                    let bmask = 0x8; // Button mask for scroll up\n\n                    gestureStart('twodrag', 20, 40, client, 0, 0);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     20, 40, 0x0);\n\n                    pointerEvent.resetHistory();\n\n                    gestureMove('twodrag', 20, 40, client, 0, 60);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                });\n\n                it('should handle gesture twodrag right events', function () {\n                    let bmask = 0x20; // Button mask for scroll right\n\n                    gestureStart('twodrag', 20, 40, client, 0, 0);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     20, 40, 0x0);\n\n                    pointerEvent.resetHistory();\n\n                    gestureMove('twodrag', 20, 40, client, 60, 0);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                });\n\n                it('should handle gesture twodrag left events', function () {\n                    let bmask = 0x40; // Button mask for scroll left\n\n                    gestureStart('twodrag', 20, 40, client, 0, 0);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     20, 40, 0x0);\n\n                    pointerEvent.resetHistory();\n\n                    gestureMove('twodrag', 20, 40, client, -60, 0);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                });\n\n                it('should handle gesture twodrag diag events', function () {\n                    let scrlUp = 0x8; // Button mask for scroll up\n                    let scrlRight = 0x20; // Button mask for scroll right\n\n                    gestureStart('twodrag', 20, 40, client, 0, 0);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     20, 40, 0x0);\n\n                    pointerEvent.resetHistory();\n\n                    gestureMove('twodrag', 20, 40, client, 60, 60);\n\n                    expect(pointerEvent).to.have.been.callCount(5);\n                    expect(pointerEvent.getCall(0)).to.have.been.calledWith(client._sock,\n                                                                            20, 40, 0x0);\n                    expect(pointerEvent.getCall(1)).to.have.been.calledWith(client._sock,\n                                                                            20, 40, scrlUp);\n                    expect(pointerEvent.getCall(2)).to.have.been.calledWith(client._sock,\n                                                                            20, 40, 0x0);\n                    expect(pointerEvent.getCall(3)).to.have.been.calledWith(client._sock,\n                                                                            20, 40, scrlRight);\n                    expect(pointerEvent.getCall(4)).to.have.been.calledWith(client._sock,\n                                                                            20, 40, 0x0);\n                });\n\n                it('should handle multiple small gesture twodrag events', function () {\n                    let bmask = 0x8; // Button mask for scroll up\n\n                    gestureStart('twodrag', 20, 40, client, 0, 0);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     20, 40, 0x0);\n\n                    pointerEvent.resetHistory();\n\n                    gestureMove('twodrag', 20, 40, client, 0, 10);\n                    clock.tick(50);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     20, 40, 0x0);\n\n                    pointerEvent.resetHistory();\n\n                    gestureMove('twodrag', 20, 40, client, 0, 20);\n                    clock.tick(50);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     20, 40, 0x0);\n\n                    pointerEvent.resetHistory();\n\n                    gestureMove('twodrag', 20, 40, client, 0, 60);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                });\n\n                it('should handle large gesture twodrag events', function () {\n                    let bmask = 0x8; // Button mask for scroll up\n\n                    gestureStart('twodrag', 30, 50, client, 0, 0);\n\n                    expect(pointerEvent).\n                        to.have.been.calledOnceWith(client._sock, 30, 50, 0x0);\n\n                    pointerEvent.resetHistory();\n\n                    gestureMove('twodrag', 30, 50, client, 0, 200);\n\n                    expect(pointerEvent).to.have.callCount(7);\n                    expect(pointerEvent.getCall(0)).to.have.been.calledWith(client._sock,\n                                                                            30, 50, 0x0);\n                    expect(pointerEvent.getCall(1)).to.have.been.calledWith(client._sock,\n                                                                            30, 50, bmask);\n                    expect(pointerEvent.getCall(2)).to.have.been.calledWith(client._sock,\n                                                                            30, 50, 0x0);\n                    expect(pointerEvent.getCall(3)).to.have.been.calledWith(client._sock,\n                                                                            30, 50, bmask);\n                    expect(pointerEvent.getCall(4)).to.have.been.calledWith(client._sock,\n                                                                            30, 50, 0x0);\n                    expect(pointerEvent.getCall(5)).to.have.been.calledWith(client._sock,\n                                                                            30, 50, bmask);\n                    expect(pointerEvent.getCall(6)).to.have.been.calledWith(client._sock,\n                                                                            30, 50, 0x0);\n                });\n            });\n\n            describe('Gesture pinch', function () {\n                it('should handle gesture pinch in events', function () {\n                    let keysym = KeyTable.XK_Control_L;\n                    let bmask = 0x10; // Button mask for scroll down\n\n                    gestureStart('pinch', 20, 40, client, 90, 90);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     20, 40, 0x0);\n                    expect(keyEvent).to.not.have.been.called;\n\n                    pointerEvent.resetHistory();\n\n                    gestureMove('pinch', 20, 40, client, 30, 30);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n\n                    expect(keyEvent).to.have.been.calledTwice;\n                    expect(keyEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                       keysym, 1);\n                    expect(keyEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                        keysym, 0);\n\n                    expect(keyEvent.firstCall).to.have.been.calledBefore(pointerEvent.secondCall);\n                    expect(keyEvent.lastCall).to.have.been.calledAfter(pointerEvent.lastCall);\n\n                    pointerEvent.resetHistory();\n                    keyEvent.resetHistory();\n\n                    gestureEnd('pinch', 20, 40, client);\n\n                    expect(pointerEvent).to.not.have.been.called;\n                    expect(keyEvent).to.not.have.been.called;\n                });\n\n                it('should handle gesture pinch out events', function () {\n                    let keysym = KeyTable.XK_Control_L;\n                    let bmask = 0x8; // Button mask for scroll up\n\n                    gestureStart('pinch', 10, 20, client, 10, 20);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     10, 20, 0x0);\n                    expect(keyEvent).to.not.have.been.called;\n\n                    pointerEvent.resetHistory();\n\n                    gestureMove('pinch', 10, 20, client, 70, 80);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           10, 20, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            10, 20, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           10, 20, 0x0);\n\n                    expect(keyEvent).to.have.been.calledTwice;\n                    expect(keyEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                       keysym, 1);\n                    expect(keyEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                        keysym, 0);\n\n                    expect(keyEvent.firstCall).to.have.been.calledBefore(pointerEvent.secondCall);\n                    expect(keyEvent.lastCall).to.have.been.calledAfter(pointerEvent.lastCall);\n\n                    pointerEvent.resetHistory();\n                    keyEvent.resetHistory();\n\n                    gestureEnd('pinch', 10, 20, client);\n\n                    expect(pointerEvent).to.not.have.been.called;\n                    expect(keyEvent).to.not.have.been.called;\n                });\n\n                it('should handle large gesture pinch', function () {\n                    let keysym = KeyTable.XK_Control_L;\n                    let bmask = 0x10; // Button mask for scroll down\n\n                    gestureStart('pinch', 20, 40, client, 150, 150);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     20, 40, 0x0);\n                    expect(keyEvent).to.not.have.been.called;\n\n                    pointerEvent.resetHistory();\n\n                    gestureMove('pinch', 20, 40, client, 30, 30);\n\n                    expect(pointerEvent).to.have.been.callCount(5);\n                    expect(pointerEvent.getCall(0)).to.have.been.calledWith(client._sock,\n                                                                            20, 40, 0x0);\n                    expect(pointerEvent.getCall(1)).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n                    expect(pointerEvent.getCall(2)).to.have.been.calledWith(client._sock,\n                                                                            20, 40, 0x0);\n                    expect(pointerEvent.getCall(3)).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n                    expect(pointerEvent.getCall(4)).to.have.been.calledWith(client._sock,\n                                                                            20, 40, 0x0);\n\n                    expect(keyEvent).to.have.been.calledTwice;\n                    expect(keyEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                       keysym, 1);\n                    expect(keyEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                        keysym, 0);\n\n                    expect(keyEvent.firstCall).to.have.been.calledBefore(pointerEvent.secondCall);\n                    expect(keyEvent.lastCall).to.have.been.calledAfter(pointerEvent.lastCall);\n\n                    pointerEvent.resetHistory();\n                    keyEvent.resetHistory();\n\n                    gestureEnd('pinch', 20, 40, client);\n\n                    expect(pointerEvent).to.not.have.been.called;\n                    expect(keyEvent).to.not.have.been.called;\n                });\n\n                it('should handle multiple small gesture pinch out events', function () {\n                    let keysym = KeyTable.XK_Control_L;\n                    let bmask = 0x8; // Button mask for scroll down\n\n                    gestureStart('pinch', 20, 40, client, 0, 10);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     20, 40, 0x0);\n                    expect(keyEvent).to.not.have.been.called;\n\n                    pointerEvent.resetHistory();\n\n                    gestureMove('pinch', 20, 40, client, 0, 30);\n                    clock.tick(50);\n\n                    expect(pointerEvent).to.have.been.calledWith(client._sock,\n                                                                 20, 40, 0x0);\n\n                    pointerEvent.resetHistory();\n\n                    gestureMove('pinch', 20, 40, client, 0, 60);\n                    clock.tick(50);\n\n                    expect(pointerEvent).to.have.been.calledWith(client._sock,\n                                                                 20, 40, 0x0);\n\n                    pointerEvent.resetHistory();\n                    keyEvent.resetHistory();\n\n                    gestureMove('pinch', 20, 40, client, 0, 90);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n\n                    expect(keyEvent).to.have.been.calledTwice;\n                    expect(keyEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                       keysym, 1);\n                    expect(keyEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                        keysym, 0);\n\n                    expect(keyEvent.firstCall).to.have.been.calledBefore(pointerEvent.secondCall);\n                    expect(keyEvent.lastCall).to.have.been.calledAfter(pointerEvent.lastCall);\n\n                    pointerEvent.resetHistory();\n                    keyEvent.resetHistory();\n\n                    gestureEnd('pinch', 20, 40, client);\n\n                    expect(keyEvent).to.not.have.been.called;\n                });\n\n                it('should send correct key control code', function () {\n                    let keysym = KeyTable.XK_Control_L;\n                    let code = 0x1d;\n                    let bmask = 0x10; // Button mask for scroll down\n\n                    client._qemuExtKeyEventSupported = true;\n\n                    gestureStart('pinch', 20, 40, client, 90, 90);\n\n                    expect(pointerEvent).to.have.been.calledOnceWith(client._sock,\n                                                                     20, 40, 0x0);\n                    expect(qemuKeyEvent).to.not.have.been.called;\n\n                    pointerEvent.resetHistory();\n\n                    gestureMove('pinch', 20, 40, client, 30, 30);\n\n                    expect(pointerEvent).to.have.been.calledThrice;\n                    expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n                    expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            20, 40, bmask);\n                    expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,\n                                                                           20, 40, 0x0);\n\n                    expect(qemuKeyEvent).to.have.been.calledTwice;\n                    expect(qemuKeyEvent.firstCall).to.have.been.calledWith(client._sock,\n                                                                           keysym,\n                                                                           true,\n                                                                           code);\n                    expect(qemuKeyEvent.secondCall).to.have.been.calledWith(client._sock,\n                                                                            keysym,\n                                                                            false,\n                                                                            code);\n\n                    expect(qemuKeyEvent.firstCall).to.have.been.calledBefore(pointerEvent.secondCall);\n                    expect(qemuKeyEvent.lastCall).to.have.been.calledAfter(pointerEvent.lastCall);\n\n                    pointerEvent.resetHistory();\n                    qemuKeyEvent.resetHistory();\n\n                    gestureEnd('pinch', 20, 40, client);\n\n                    expect(pointerEvent).to.not.have.been.called;\n                    expect(qemuKeyEvent).to.not.have.been.called;\n                });\n            });\n        });\n\n        describe('WebSocket events', function () {\n            // message events\n            it('should do nothing if we receive an empty message and have nothing in the queue', function () {\n                sinon.spy(client, \"_normalMsg\");\n                client._sock._websocket._receiveData(new Uint8Array([]));\n                expect(client._normalMsg).to.not.have.been.called;\n            });\n\n            it('should handle a message in the connected state as a normal message', function () {\n                sinon.spy(client, \"_normalMsg\");\n                client._sock._websocket._receiveData(new Uint8Array([1, 2, 3]));\n                expect(client._normalMsg).to.have.been.called;\n            });\n\n            it('should handle a message in any non-disconnected/failed state like an init message', function () {\n                client._rfbConnectionState = 'connecting';\n                client._rfbInitState = 'ProtocolVersion';\n                sinon.spy(client, \"_initMsg\");\n                client._sock._websocket._receiveData(new Uint8Array([1, 2, 3]));\n                expect(client._initMsg).to.have.been.called;\n            });\n\n            it('should process all normal messages directly', function () {\n                const spy = sinon.spy();\n                client.addEventListener(\"bell\", spy);\n                client._sock._websocket._receiveData(new Uint8Array([0x02, 0x02]));\n                expect(spy).to.have.been.calledTwice;\n            });\n\n            // open events\n            it('should update the state to ProtocolVersion on open (if the state is \"connecting\")', function () {\n                client = new RFB(document.createElement('div'), 'wss://host:8675');\n                this.clock.tick();\n                client._sock._websocket._open();\n                expect(client._rfbInitState).to.equal('ProtocolVersion');\n            });\n\n            it('should fail if we are not currently ready to connect and we get an \"open\" event', function () {\n                sinon.spy(client, \"_fail\");\n                client._rfbConnectionState = 'connected';\n                client._sock._websocket._open();\n                expect(client._fail).to.have.been.calledOnce;\n            });\n\n            // close events\n            it('should transition to \"disconnected\" from \"disconnecting\" on a close event', function () {\n                const real = client._sock._websocket.close;\n                client._sock._websocket.close = () => {};\n                client.disconnect();\n                expect(client._rfbConnectionState).to.equal('disconnecting');\n                client._sock._websocket.close = real;\n                client._sock._websocket.close();\n                expect(client._rfbConnectionState).to.equal('disconnected');\n            });\n\n            it('should fail if we get a close event while connecting', function () {\n                sinon.spy(client, \"_fail\");\n                client._rfbConnectionState = 'connecting';\n                client._sock._websocket.close();\n                expect(client._fail).to.have.been.calledOnce;\n            });\n\n            it('should unregister close event handler', function () {\n                sinon.spy(client._sock, 'off');\n                client.disconnect();\n                client._sock._websocket.close();\n                expect(client._sock.off).to.have.been.calledWith('close');\n            });\n\n            // error events do nothing\n        });\n    });\n\n    describe('Quality level setting', function () {\n        const defaultQuality = 6;\n\n        let client;\n\n        beforeEach(function () {\n            client = makeRFB();\n            sinon.spy(RFB.messages, \"clientEncodings\");\n        });\n\n        afterEach(function () {\n            RFB.messages.clientEncodings.restore();\n        });\n\n        it(`should equal ${defaultQuality} by default`, function () {\n            expect(client._qualityLevel).to.equal(defaultQuality);\n        });\n\n        it('should ignore non-integers when set', function () {\n            client.qualityLevel = '1';\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n\n            RFB.messages.clientEncodings.resetHistory();\n\n            client.qualityLevel = 1.5;\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n\n            RFB.messages.clientEncodings.resetHistory();\n\n            client.qualityLevel = null;\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n\n            RFB.messages.clientEncodings.resetHistory();\n\n            client.qualityLevel = undefined;\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n\n            RFB.messages.clientEncodings.resetHistory();\n\n            client.qualityLevel = {};\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n        });\n\n        it('should ignore integers out of range [0, 9]', function () {\n            client.qualityLevel = -1;\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n\n            RFB.messages.clientEncodings.resetHistory();\n\n            client.qualityLevel = 10;\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n        });\n\n        it('should send clientEncodings with new quality value', function () {\n            let newQuality;\n\n            newQuality = 8;\n            client.qualityLevel = newQuality;\n            expect(client.qualityLevel).to.equal(newQuality);\n            expect(RFB.messages.clientEncodings).to.have.been.calledOnce;\n            expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.pseudoEncodingQualityLevel0 + newQuality);\n        });\n\n        it('should not send clientEncodings if quality is the same', function () {\n            let newQuality;\n\n            newQuality = 2;\n            client.qualityLevel = newQuality;\n            expect(RFB.messages.clientEncodings).to.have.been.calledOnce;\n            expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.pseudoEncodingQualityLevel0 + newQuality);\n\n            RFB.messages.clientEncodings.resetHistory();\n\n            client.qualityLevel = newQuality;\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n        });\n\n        it('should not send clientEncodings if not in connected state', function () {\n            let newQuality;\n\n            client._rfbConnectionState = '';\n            newQuality = 2;\n            client.qualityLevel = newQuality;\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n\n            RFB.messages.clientEncodings.resetHistory();\n\n            client._rfbConnectionState = 'connnecting';\n            newQuality = 6;\n            client.qualityLevel = newQuality;\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n\n            RFB.messages.clientEncodings.resetHistory();\n\n            client._rfbConnectionState = 'connected';\n            newQuality = 5;\n            client.qualityLevel = newQuality;\n            expect(RFB.messages.clientEncodings).to.have.been.calledOnce;\n            expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.pseudoEncodingQualityLevel0 + newQuality);\n        });\n    });\n\n    describe('Compression level setting', function () {\n        const defaultCompression = 2;\n\n        let client;\n\n        beforeEach(function () {\n            client = makeRFB();\n            sinon.spy(RFB.messages, \"clientEncodings\");\n        });\n\n        afterEach(function () {\n            RFB.messages.clientEncodings.restore();\n        });\n\n        it(`should equal ${defaultCompression} by default`, function () {\n            expect(client._compressionLevel).to.equal(defaultCompression);\n        });\n\n        it('should ignore non-integers when set', function () {\n            client.compressionLevel = '1';\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n\n            RFB.messages.clientEncodings.resetHistory();\n\n            client.compressionLevel = 1.5;\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n\n            RFB.messages.clientEncodings.resetHistory();\n\n            client.compressionLevel = null;\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n\n            RFB.messages.clientEncodings.resetHistory();\n\n            client.compressionLevel = undefined;\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n\n            RFB.messages.clientEncodings.resetHistory();\n\n            client.compressionLevel = {};\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n        });\n\n        it('should ignore integers out of range [0, 9]', function () {\n            client.compressionLevel = -1;\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n\n            RFB.messages.clientEncodings.resetHistory();\n\n            client.compressionLevel = 10;\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n        });\n\n        it('should send clientEncodings with new compression value', function () {\n            let newCompression;\n\n            newCompression = 5;\n            client.compressionLevel = newCompression;\n            expect(client.compressionLevel).to.equal(newCompression);\n            expect(RFB.messages.clientEncodings).to.have.been.calledOnce;\n            expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.pseudoEncodingCompressLevel0 + newCompression);\n        });\n\n        it('should not send clientEncodings if compression is the same', function () {\n            let newCompression;\n\n            newCompression = 9;\n            client.compressionLevel = newCompression;\n            expect(RFB.messages.clientEncodings).to.have.been.calledOnce;\n            expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.pseudoEncodingCompressLevel0 + newCompression);\n\n            RFB.messages.clientEncodings.resetHistory();\n\n            client.compressionLevel = newCompression;\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n        });\n\n        it('should not send clientEncodings if not in connected state', function () {\n            let newCompression;\n\n            client._rfbConnectionState = '';\n            newCompression = 7;\n            client.compressionLevel = newCompression;\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n\n            RFB.messages.clientEncodings.resetHistory();\n\n            client._rfbConnectionState = 'connnecting';\n            newCompression = 6;\n            client.compressionLevel = newCompression;\n            expect(RFB.messages.clientEncodings).to.not.have.been.called;\n\n            RFB.messages.clientEncodings.resetHistory();\n\n            client._rfbConnectionState = 'connected';\n            newCompression = 5;\n            client.compressionLevel = newCompression;\n            expect(RFB.messages.clientEncodings).to.have.been.calledOnce;\n            expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.pseudoEncodingCompressLevel0 + newCompression);\n        });\n    });\n});\n\ndescribe('RFB messages', function () {\n    let sock;\n\n    beforeEach(function () {\n        let websock = new FakeWebSocket();\n        websock._open();\n        sock = new Websock();\n        sock.attach(websock);\n    });\n\n    describe('Input events', function () {\n        it('should send correct data for keyboard events', function () {\n            // FIXME: down should be boolean\n            RFB.messages.keyEvent(sock, 0x12345678, 0);\n            let expected =\n                [ 4, 0, 0, 0, 0x12, 0x34, 0x56, 0x78];\n            expect(sock).to.have.sent(new Uint8Array(expected));\n\n            RFB.messages.keyEvent(sock, 0x90abcdef, 1);\n            expected =\n                [ 4, 1, 0, 0, 0x90, 0xab, 0xcd, 0xef];\n            expect(sock).to.have.sent(new Uint8Array(expected));\n        });\n\n        it('should send correct data for QEMU keyboard events', function () {\n            // FIXME: down should be boolean\n            RFB.messages.QEMUExtendedKeyEvent(sock, 0x12345678, 0, 0x55);\n            let expected =\n                [ 255, 0, 0, 0, 0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x00, 0x55];\n            expect(sock).to.have.sent(new Uint8Array(expected));\n\n            RFB.messages.QEMUExtendedKeyEvent(sock, 0x90abcdef, 1, 0xe055);\n            expected =\n                [ 255, 0, 0, 1, 0x90, 0xab, 0xcd, 0xef, 0x00, 0x00, 0x00, 0xd5];\n            expect(sock).to.have.sent(new Uint8Array(expected));\n        });\n\n        it('should send correct data for pointer events', function () {\n            RFB.messages.pointerEvent(sock, 12345, 54321, 0x2b);\n            let expected =\n                [ 5, 0x2b, 0x30, 0x39, 0xd4, 0x31];\n            expect(sock).to.have.sent(new Uint8Array(expected));\n        });\n\n        it('should send correct data for pointer events with marker bit set', function () {\n            RFB.messages.pointerEvent(sock, 12345, 54321, 0xab);\n            let expected =\n                [ 5, 0x2b, 0x30, 0x39, 0xd4, 0x31];\n            expect(sock).to.have.sent(new Uint8Array(expected));\n        });\n\n        it('should send correct data for pointer events with extended button bits set', function () {\n            RFB.messages.pointerEvent(sock, 12345, 54321, 0x3ab);\n            let expected =\n                [ 5, 0x2b, 0x30, 0x39, 0xd4, 0x31];\n            expect(sock).to.have.sent(new Uint8Array(expected));\n        });\n\n        it('should send correct data for extended pointer events', function () {\n            RFB.messages.extendedPointerEvent(sock, 12345, 54321, 0xab);\n            let expected =\n                [ 5, 0xab, 0x30, 0x39, 0xd4, 0x31, 0x1];\n            expect(sock).to.have.sent(new Uint8Array(expected));\n        });\n\n        it('should not send invalid data for extended pointer events', function () {\n            expect(() => RFB.messages.extendedPointerEvent(sock, 12345, 54321, 0x3ab)).to.throw(Error);\n        });\n    });\n\n    describe('Clipboard events', function () {\n        it('should send correct data for clipboard events', function () {\n            RFB.messages.clientCutText(sock, new Uint8Array([ 0x01, 0x23, 0x45, 0x67 ]));\n            let expected =\n                [ 6, 0, 0, 0, 0x00, 0x00, 0x00, 0x04,\n                  0x01, 0x23, 0x45, 0x67 ];\n            expect(sock).to.have.sent(new Uint8Array(expected));\n        });\n    });\n\n    describe('Extended clipboard handling send', function () {\n        it('should call clientCutText with correct Caps data', function () {\n            let formats = {\n                0: 2,\n                2: 4121\n            };\n            let expectedData = new Uint8Array([0x06, 0x00, 0x00, 0x00,\n                                               0xFF, 0xFF, 0xFF, 0xF4,\n                                               0x1F, 0x00, 0x00, 0x05,\n                                               0x00, 0x00, 0x00, 0x02,\n                                               0x00, 0x00, 0x10, 0x19]);\n            let actions = [\n                1 << 24,  // Caps\n                1 << 25,  // Request\n                1 << 26,  // Peek\n                1 << 27,  // Notify\n                1 << 28   // Provide\n            ];\n\n            RFB.messages.extendedClipboardCaps(sock, actions, formats);\n\n            expect(sock).to.have.sent(expectedData);\n        });\n\n        it('should call clientCutText with correct Request data', function () {\n            let formats = new Uint8Array([0x01]);\n            let expectedData = new Uint8Array([0x06, 0x00, 0x00, 0x00,\n                                               0xFF, 0xFF, 0xFF, 0xFC,\n                                               0x02, 0x00, 0x00, 0x01]);\n\n            RFB.messages.extendedClipboardRequest(sock, formats);\n\n            expect(sock).to.have.sent(expectedData);\n        });\n\n        it('should call clientCutText with correct Notify data', function () {\n            let formats = new Uint8Array([0x01]);\n            let expectedData = new Uint8Array([0x06, 0x00, 0x00, 0x00,\n                                               0xFF, 0xFF, 0xFF, 0xFC,\n                                               0x08, 0x00, 0x00, 0x01]);\n\n            RFB.messages.extendedClipboardNotify(sock, formats);\n\n            expect(sock).to.have.sent(expectedData);\n        });\n\n        it('should call clientCutText with correct Provide data', function () {\n            let testText = \"Test string\";\n            let expectedText = encodeUTF8(testText + \"\\0\");\n\n            let deflatedData =  deflateWithSize(expectedText);\n\n            // Build Expected with flags and deflated data\n            let expectedData = new Uint8Array(8 + 4 + deflatedData.length);\n            expectedData[0] = 0x06; // Message type\n            expectedData[1] = 0x00;\n            expectedData[2] = 0x00;\n            expectedData[3] = 0x00;\n            expectedData[4] = 0xFF; // Size\n            expectedData[5] = 0xFF;\n            expectedData[6] = 0xFF;\n            expectedData[7] = 256 - (4 + deflatedData.length);\n            expectedData[8] = 0x10; // The client capabilities\n            expectedData[9] = 0x00; // Reserved flags\n            expectedData[10] = 0x00; // Reserved flags\n            expectedData[11] = 0x01; // The formats client supports\n            expectedData.set(deflatedData, 12);\n\n            RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]);\n\n            expect(sock).to.have.sent(expectedData);\n\n        });\n\n        describe('End of line characters', function () {\n            it('Carriage return', function () {\n\n                let testText = \"Hello\\rworld\\r\\r!\";\n                let expectedText = encodeUTF8(\"Hello\\r\\nworld\\r\\n\\r\\n!\\0\");\n\n                let deflatedData =  deflateWithSize(expectedText);\n\n                // Build Expected with flags and deflated data\n                let expectedData = new Uint8Array(8 + 4 + deflatedData.length);\n                expectedData[0] = 0x06; // Message type\n                expectedData[1] = 0x00;\n                expectedData[2] = 0x00;\n                expectedData[3] = 0x00;\n                expectedData[4] = 0xFF; // Size\n                expectedData[5] = 0xFF;\n                expectedData[6] = 0xFF;\n                expectedData[7] = 256 - (4 + deflatedData.length);\n                expectedData[8] = 0x10; // The client capabilities\n                expectedData[9] = 0x00; // Reserved flags\n                expectedData[10] = 0x00; // Reserved flags\n                expectedData[11] = 0x01; // The formats client supports\n                expectedData.set(deflatedData, 12);\n\n                RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]);\n\n                expect(sock).to.have.sent(expectedData);\n            });\n\n            it('Carriage return Line feed', function () {\n\n                let testText = \"Hello\\r\\n\\r\\nworld\\r\\n!\";\n                let expectedText = encodeUTF8(testText + \"\\0\");\n\n                let deflatedData =  deflateWithSize(expectedText);\n\n                // Build Expected with flags and deflated data\n                let expectedData = new Uint8Array(8 + 4 + deflatedData.length);\n                expectedData[0] = 0x06; // Message type\n                expectedData[1] = 0x00;\n                expectedData[2] = 0x00;\n                expectedData[3] = 0x00;\n                expectedData[4] = 0xFF; // Size\n                expectedData[5] = 0xFF;\n                expectedData[6] = 0xFF;\n                expectedData[7] = 256 - (4 + deflatedData.length);\n                expectedData[8] = 0x10; // The client capabilities\n                expectedData[9] = 0x00; // Reserved flags\n                expectedData[10] = 0x00; // Reserved flags\n                expectedData[11] = 0x01; // The formats client supports\n                expectedData.set(deflatedData, 12);\n\n                RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]);\n\n                expect(sock).to.have.sent(expectedData);\n            });\n\n            it('Line feed', function () {\n                let testText = \"Hello\\n\\n\\nworld\\n!\";\n                let expectedText = encodeUTF8(\"Hello\\r\\n\\r\\n\\r\\nworld\\r\\n!\\0\");\n\n                let deflatedData =  deflateWithSize(expectedText);\n\n                // Build Expected with flags and deflated data\n                let expectedData = new Uint8Array(8 + 4 + deflatedData.length);\n                expectedData[0] = 0x06; // Message type\n                expectedData[1] = 0x00;\n                expectedData[2] = 0x00;\n                expectedData[3] = 0x00;\n                expectedData[4] = 0xFF; // Size\n                expectedData[5] = 0xFF;\n                expectedData[6] = 0xFF;\n                expectedData[7] = 256 - (4 + deflatedData.length);\n                expectedData[8] = 0x10; // The client capabilities\n                expectedData[9] = 0x00; // Reserved flags\n                expectedData[10] = 0x00; // Reserved flags\n                expectedData[11] = 0x01; // The formats client supports\n                expectedData.set(deflatedData, 12);\n\n                RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]);\n\n                expect(sock).to.have.sent(expectedData);\n            });\n\n            it('Carriage return and Line feed mixed', function () {\n                let testText = \"\\rHello\\r\\n\\rworld\\n\\n!\";\n                let expectedText = encodeUTF8(\"\\r\\nHello\\r\\n\\r\\nworld\\r\\n\\r\\n!\\0\");\n\n                let deflatedData =  deflateWithSize(expectedText);\n\n                // Build Expected with flags and deflated data\n                let expectedData = new Uint8Array(8 + 4 + deflatedData.length);\n                expectedData[0] = 0x06; // Message type\n                expectedData[1] = 0x00;\n                expectedData[2] = 0x00;\n                expectedData[3] = 0x00;\n                expectedData[4] = 0xFF; // Size\n                expectedData[5] = 0xFF;\n                expectedData[6] = 0xFF;\n                expectedData[7] = 256 - (4 + deflatedData.length);\n                expectedData[8] = 0x10; // The client capabilities\n                expectedData[9] = 0x00; // Reserved flags\n                expectedData[10] = 0x00; // Reserved flags\n                expectedData[11] = 0x01; // The formats client supports\n                expectedData.set(deflatedData, 12);\n\n                RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]);\n\n                expect(sock).to.have.sent(expectedData);\n            });\n        });\n    });\n\n    describe('Screen layout', function () {\n        it('should send correct data for screen layout changes', function () {\n            RFB.messages.setDesktopSize(sock, 12345, 54321, 0x12345678, 0x90abcdef);\n            let expected =\n                [ 251, 0, 0x30, 0x39, 0xd4, 0x31, 0x01, 0x00,\n                  0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x00, 0x00,\n                  0x30, 0x39, 0xd4, 0x31, 0x90, 0xab, 0xcd, 0xef ];\n            expect(sock).to.have.sent(new Uint8Array(expected));\n        });\n    });\n\n    describe('Fences', function () {\n        it('should send correct data for fences', function () {\n            // FIXME: Payload should be a byte array\n            RFB.messages.clientFence(sock, 0x12345678, \"text\");\n            let expected =\n                [ 248, 0, 0, 0, 0x12, 0x34, 0x56, 0x78,\n                  4, 0x74, 0x65, 0x78, 0x74 ];\n            expect(sock).to.have.sent(new Uint8Array(expected));\n        });\n    });\n\n    describe('Continuous updates', function () {\n        it('should send correct data for continuous updates configuration', function () {\n            // FIXME: enable should be boolean\n            RFB.messages.enableContinuousUpdates(sock, 0, 12345, 54321, 34343, 18181);\n            let expected =\n                [ 150, 0, 0x30, 0x39, 0xd4, 0x31, 0x86, 0x27, 0x47, 0x05 ];\n            expect(sock).to.have.sent(new Uint8Array(expected));\n        });\n    });\n\n    describe('Pixel format', function () {\n        it('should send correct data for normal depth', function () {\n            RFB.messages.pixelFormat(sock, 24, true);\n            let expected =\n                [ 0, 0, 0, 0, 32, 24, 0, 1,\n                  0, 255, 0, 255, 0, 255, 0, 8, 16, 0, 0, 0 ];\n            expect(sock).to.have.sent(new Uint8Array(expected));\n        });\n\n        it('should send correct data for low depth', function () {\n            RFB.messages.pixelFormat(sock, 8, true);\n            let expected =\n                [ 0, 0, 0, 0, 8, 8, 0, 1,\n                  0, 3, 0, 3, 0, 3, 0, 2, 4, 0, 0, 0 ];\n            expect(sock).to.have.sent(new Uint8Array(expected));\n        });\n    });\n\n    describe('Encodings', function () {\n        it('should send correct data for supported encodings', function () {\n            RFB.messages.clientEncodings(sock, [ 0x12345678,\n                                                 0x90abcdef,\n                                                 0x10293847 ]);\n            let expected =\n                [ 2, 0, 0, 3, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd,\n                  0xef, 0x10, 0x29, 0x38, 0x47 ];\n            expect(sock).to.have.sent(new Uint8Array(expected));\n        });\n    });\n\n    describe('Update request', function () {\n        it('should send correct data for update request', function () {\n            RFB.messages.fbUpdateRequest(sock, true, 12345, 54321, 34343, 18181);\n            let expected =\n                [ 3, 1, 0x30, 0x39, 0xd4, 0x31, 0x86, 0x27, 0x47, 0x05 ];\n            expect(sock).to.have.sent(new Uint8Array(expected));\n        });\n    });\n\n    describe('XVP operations', function () {\n        it('should send correct data for XVP operations', function () {\n            RFB.messages.xvpOp(sock, 123, 45);\n            let expected =\n                [ 250, 0, 123, 45 ];\n            expect(sock).to.have.sent(new Uint8Array(expected));\n        });\n    });\n});\n"
  },
  {
    "path": "tests/test.rre.js",
    "content": "import Websock from '../core/websock.js';\nimport Display from '../core/display.js';\n\nimport RREDecoder from '../core/decoders/rre.js';\n\nimport FakeWebSocket from './fake.websocket.js';\n\nfunction testDecodeRect(decoder, x, y, width, height, data, display, depth) {\n    let sock;\n    let done = false;\n\n    sock = new Websock;\n    sock.open(\"ws://example.com\");\n\n    sock.on('message', () => {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    });\n\n    // Empty messages are filtered at multiple layers, so we need to\n    // do a direct call\n    if (data.length === 0) {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    } else {\n        sock._websocket._receiveData(new Uint8Array(data));\n    }\n\n    display.flip();\n\n    return done;\n}\n\nfunction push16(arr, num) {\n    arr.push((num >> 8) & 0xFF,\n             num & 0xFF);\n}\n\nfunction push32(arr, num) {\n    arr.push((num >> 24) & 0xFF,\n             (num >> 16) & 0xFF,\n             (num >>  8) & 0xFF,\n             num & 0xFF);\n}\n\ndescribe('RRE decoder', function () {\n    let decoder;\n    let display;\n\n    before(FakeWebSocket.replace);\n    after(FakeWebSocket.restore);\n\n    beforeEach(function () {\n        decoder = new RREDecoder();\n        display = new Display(document.createElement('canvas'));\n        display.resize(4, 4);\n    });\n\n    // TODO(directxman12): test rre_chunk_sz?\n\n    it('should handle the RRE encoding', function () {\n        let data = [];\n        push32(data, 2); // 2 subrects\n        push32(data, 0x00ff0000); // becomes 00ff0000 --> #00FF00 bg color\n        data.push(0x00); // becomes 0000ff00 --> #0000FF fg color\n        data.push(0x00);\n        data.push(0xff);\n        data.push(0x00);\n        push16(data, 0); // x: 0\n        push16(data, 0); // y: 0\n        push16(data, 2); // width: 2\n        push16(data, 2); // height: 2\n        data.push(0x00); // becomes 0000ff00 --> #0000FF fg color\n        data.push(0x00);\n        data.push(0xff);\n        data.push(0x00);\n        push16(data, 2); // x: 2\n        push16(data, 2); // y: 2\n        push16(data, 2); // width: 2\n        push16(data, 2); // height: 2\n\n        let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle empty rects', function () {\n        display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);\n        display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);\n        display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);\n\n        let done = testDecodeRect(decoder, 1, 2, 0, 0,\n                                  [ 0x00, 0x00, 0x00, 0x00,\n                                    0xff, 0xff, 0xff, 0xff ],\n                                  display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n});\n"
  },
  {
    "path": "tests/test.tight.js",
    "content": "import Websock from '../core/websock.js';\nimport Display from '../core/display.js';\n\nimport TightDecoder from '../core/decoders/tight.js';\n\nimport FakeWebSocket from './fake.websocket.js';\n\nfunction testDecodeRect(decoder, x, y, width, height, data, display, depth) {\n    let sock;\n    let done = false;\n\n    sock = new Websock;\n    sock.open(\"ws://example.com\");\n\n    sock.on('message', () => {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    });\n\n    // Empty messages are filtered at multiple layers, so we need to\n    // do a direct call\n    if (data.length === 0) {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    } else {\n        sock._websocket._receiveData(new Uint8Array(data));\n    }\n\n    display.flip();\n\n    return done;\n}\n\ndescribe('Tight decoder', function () {\n    let decoder;\n    let display;\n\n    before(FakeWebSocket.replace);\n    after(FakeWebSocket.restore);\n\n    beforeEach(function () {\n        decoder = new TightDecoder();\n        display = new Display(document.createElement('canvas'));\n        display.resize(4, 4);\n    });\n\n    it('should handle fill rects', function () {\n        let done = testDecodeRect(decoder, 0, 0, 4, 4,\n                                  [0x80, 0xff, 0x88, 0x44],\n                                  display, 24);\n\n        let targetData = new Uint8Array([\n            0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255,\n            0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255,\n            0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255,\n            0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255,\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle uncompressed copy rects', function () {\n        let done;\n        let blueData = [ 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0xff ];\n        let greenData = [ 0x00, 0x00, 0xff, 0x00, 0x00, 0xff, 0x00 ];\n\n        done = testDecodeRect(decoder, 0, 0, 2, 1, blueData, display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 0, 1, 2, 1, blueData, display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 2, 0, 2, 1, greenData, display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 2, 1, 2, 1, greenData, display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 0, 2, 2, 1, greenData, display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 0, 3, 2, 1, greenData, display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 2, 2, 2, 1, blueData, display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 2, 3, 2, 1, blueData, display, 24);\n        expect(done).to.be.true;\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle compressed copy rects', function () {\n        let data = [\n            // Control byte\n            0x00,\n            // Pixels (compressed)\n            0x15,\n            0x78, 0x9c, 0x63, 0x60, 0xf8, 0xcf, 0x00, 0x44,\n            0x60, 0x82, 0x01, 0x99, 0x8d, 0x29, 0x02, 0xa6,\n            0x00, 0x7e, 0xbf, 0x0f, 0xf1 ];\n\n        let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle uncompressed mono rects', function () {\n        let data = [\n            // Control bytes\n            0x40, 0x01,\n            // Palette\n            0x01, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00,\n            // Pixels\n            0x30, 0x30, 0xc0, 0xc0 ];\n\n        let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle compressed mono rects', function () {\n        display.resize(4, 12);\n\n        let data = [\n            // Control bytes\n            0x40, 0x01,\n            // Palette\n            0x01, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00,\n            // Pixels (compressed)\n            0x0e,\n            0x78, 0x9c, 0x33, 0x30, 0x38, 0x70, 0xc0, 0x00,\n            0x8a, 0x01, 0x21, 0x3c, 0x05, 0xa1 ];\n\n        let done = testDecodeRect(decoder, 0, 0, 4, 12, data, display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle uncompressed palette rects', function () {\n        let done;\n        let data1 = [\n            // Control bytes\n            0x40, 0x01,\n            // Palette\n            0x02, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00,\n            // Pixels\n            0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01 ];\n        let data2 = [\n            // Control bytes\n            0x40, 0x01,\n            // Palette\n            0x02, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00,\n            // Pixels\n            0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00 ];\n\n        done = testDecodeRect(decoder, 0, 0, 4, 2, data1, display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 0, 2, 4, 2, data2, display, 24);\n        expect(done).to.be.true;\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle compressed palette rects', function () {\n        let data = [\n            // Control bytes\n            0x40, 0x01,\n            // Palette\n            0x02, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00,\n            // Pixels (compressed)\n            0x12,\n            0x78, 0x9c, 0x63, 0x60, 0x60, 0x64, 0x64, 0x00,\n            0x62, 0x08, 0xc9, 0xc0, 0x00, 0x00, 0x00, 0x54,\n            0x00, 0x09 ];\n\n        let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle uncompressed gradient rects', function () {\n        let done;\n        let blueData = [ 0x40, 0x02, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00 ];\n        let greenData = [ 0x40, 0x02, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00 ];\n\n        done = testDecodeRect(decoder, 0, 0, 2, 1, blueData, display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 0, 1, 2, 1, blueData, display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 2, 0, 2, 1, greenData, display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 2, 1, 2, 1, greenData, display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 0, 2, 2, 1, greenData, display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 0, 3, 2, 1, greenData, display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 2, 2, 2, 1, blueData, display, 24);\n        expect(done).to.be.true;\n        done = testDecodeRect(decoder, 2, 3, 2, 1, blueData, display, 24);\n        expect(done).to.be.true;\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle compressed gradient rects', function () {\n        let data = [\n            // Control byte\n            0x40, 0x02,\n            // Pixels (compressed)\n            0x18,\n            0x78, 0x9c, 0x62, 0x60, 0xf8, 0xcf, 0x00, 0x04,\n            0xff, 0x19, 0x19, 0xd0, 0x00, 0x44, 0x84, 0xf1,\n            0x3f, 0x9a, 0x30, 0x00, 0x00, 0x00, 0xff, 0xff ];\n\n        let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle empty copy rects', function () {\n        display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);\n        display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);\n        display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);\n\n        let done = testDecodeRect(decoder, 1, 2, 0, 0, [ 0x00 ], display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle empty palette rects', function () {\n        display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);\n        display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);\n        display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);\n\n        let done = testDecodeRect(decoder, 1, 2, 0, 0,\n                                  [ 0x40, 0x01, 0x01,\n                                    0xff, 0xff, 0xff,\n                                    0xff, 0xff, 0xff ], display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle empty gradient rects', function () {\n        display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);\n        display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);\n        display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);\n\n        let done = testDecodeRect(decoder, 1, 2, 0, 0,\n                                  [ 0x40, 0x02 ], display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle empty fill rects', function () {\n        display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);\n        display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);\n        display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);\n\n        let done = testDecodeRect(decoder, 1, 2, 0, 0,\n                                  [ 0x80, 0xff, 0xff, 0xff ],\n                                  display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle JPEG rects', async function () {\n        let data = [\n            // Control bytes\n            0x90, 0xd6, 0x05,\n            // JPEG data\n            0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46,\n            0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x00, 0x48,\n            0x00, 0x48, 0x00, 0x00, 0xff, 0xfe, 0x00, 0x13,\n            0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20,\n            0x77, 0x69, 0x74, 0x68, 0x20, 0x47, 0x49, 0x4d,\n            0x50, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x01, 0x01,\n            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0xff, 0xdb,\n            0x00, 0x43, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n            0x01, 0x01, 0x01, 0xff, 0xc2, 0x00, 0x11, 0x08,\n            0x00, 0x04, 0x00, 0x04, 0x03, 0x01, 0x11, 0x00,\n            0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4,\n            0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x07, 0xff, 0xc4, 0x00, 0x14,\n            0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x08, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01,\n            0x00, 0x02, 0x10, 0x03, 0x10, 0x00, 0x00, 0x01,\n            0x1e, 0x0a, 0xa7, 0x7f, 0xff, 0xc4, 0x00, 0x14,\n            0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x05, 0xff, 0xda, 0x00, 0x08, 0x01, 0x01,\n            0x00, 0x01, 0x05, 0x02, 0x5d, 0x74, 0x41, 0x47,\n            0xff, 0xc4, 0x00, 0x1f, 0x11, 0x00, 0x01, 0x04,\n            0x02, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x04, 0x05,\n            0x07, 0x08, 0x14, 0x16, 0x03, 0x15, 0x17, 0x25,\n            0x26, 0xff, 0xda, 0x00, 0x08, 0x01, 0x03, 0x01,\n            0x01, 0x3f, 0x01, 0xad, 0x35, 0xa6, 0x13, 0xb8,\n            0x10, 0x98, 0x5d, 0x8a, 0xb1, 0x41, 0x7e, 0x43,\n            0x99, 0x24, 0x3d, 0x8f, 0x70, 0x30, 0xd8, 0xcb,\n            0x44, 0xbb, 0x7d, 0x48, 0xb5, 0xf8, 0x18, 0x7f,\n            0xe7, 0xc1, 0x9f, 0x86, 0x45, 0x9b, 0xfa, 0xf1,\n            0x61, 0x96, 0x46, 0xbf, 0x56, 0xc8, 0x8b, 0x2b,\n            0x0b, 0x35, 0x6e, 0x4b, 0x8a, 0x95, 0x6a, 0xf9,\n            0xff, 0x00, 0xff, 0xc4, 0x00, 0x1f, 0x11, 0x00,\n            0x01, 0x04, 0x02, 0x02, 0x03, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,\n            0x02, 0x04, 0x05, 0x12, 0x13, 0x14, 0x01, 0x06,\n            0x11, 0x22, 0x23, 0xff, 0xda, 0x00, 0x08, 0x01,\n            0x02, 0x01, 0x01, 0x3f, 0x01, 0x85, 0x85, 0x8c,\n            0xec, 0x31, 0x8d, 0xa6, 0x26, 0x1b, 0x6e, 0x48,\n            0xbc, 0xcd, 0xb0, 0xe3, 0x33, 0x86, 0xf9, 0x35,\n            0xdc, 0x15, 0xa8, 0xbe, 0x4d, 0x4a, 0x10, 0x22,\n            0x80, 0x00, 0x91, 0xe8, 0x24, 0xda, 0xb6, 0x57,\n            0x95, 0xf2, 0xa5, 0x73, 0xff, 0xc4, 0x00, 0x1e,\n            0x10, 0x00, 0x01, 0x04, 0x03, 0x00, 0x03, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x03, 0x01, 0x02, 0x04, 0x12, 0x05, 0x11,\n            0x13, 0x14, 0x22, 0x23, 0xff, 0xda, 0x00, 0x08,\n            0x01, 0x01, 0x00, 0x06, 0x3f, 0x02, 0x91, 0x89,\n            0xc4, 0xc8, 0xf1, 0x60, 0x45, 0xe5, 0xc0, 0x1c,\n            0x80, 0x7a, 0x77, 0x00, 0xe4, 0x97, 0xeb, 0x24,\n            0x66, 0x33, 0xac, 0x63, 0x11, 0xfe, 0xe4, 0x76,\n            0xad, 0x56, 0xe9, 0xa8, 0x88, 0x9f, 0xff, 0xc4,\n            0x00, 0x14, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0xff, 0xda, 0x00, 0x08,\n            0x01, 0x01, 0x00, 0x01, 0x3f, 0x21, 0x68, 0x3f,\n            0x92, 0x17, 0x81, 0x1f, 0x7f, 0xff, 0xda, 0x00,\n            0x0c, 0x03, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00,\n            0x00, 0x00, 0x10, 0x5f, 0xff, 0xc4, 0x00, 0x14,\n            0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0xff, 0xda, 0x00, 0x08, 0x01, 0x03,\n            0x01, 0x01, 0x3f, 0x10, 0x03, 0xeb, 0x11, 0xe4,\n            0xa7, 0xe3, 0xff, 0x00, 0xff, 0xc4, 0x00, 0x14,\n            0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0xff, 0xda, 0x00, 0x08, 0x01, 0x02,\n            0x01, 0x01, 0x3f, 0x10, 0x6b, 0xd3, 0x02, 0xdc,\n            0x9a, 0xf4, 0xff, 0x00, 0xff, 0xc4, 0x00, 0x14,\n            0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0xff, 0xda, 0x00, 0x08, 0x01, 0x01,\n            0x00, 0x01, 0x3f, 0x10, 0x62, 0x7b, 0x3a, 0xd0,\n            0x3f, 0xeb, 0xff, 0x00, 0xff, 0xd9,\n        ];\n\n        let decodeDone = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);\n        expect(decodeDone).to.be.true;\n\n        let targetData = new Uint8Array([\n            0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255\n        ]);\n\n        // Browsers have rounding errors, so we need an approximate\n        // comparing function\n        function almost(a, b) {\n            let diff = Math.abs(a - b);\n            return diff < 5;\n        }\n\n        await display.flush();\n        expect(display).to.have.displayed(targetData, almost);\n    });\n});\n"
  },
  {
    "path": "tests/test.tightpng.js",
    "content": "import Websock from '../core/websock.js';\nimport Display from '../core/display.js';\n\nimport TightPngDecoder from '../core/decoders/tightpng.js';\n\nimport FakeWebSocket from './fake.websocket.js';\n\nfunction testDecodeRect(decoder, x, y, width, height, data, display, depth) {\n    let sock;\n    let done = false;\n\n    sock = new Websock;\n    sock.open(\"ws://example.com\");\n\n    sock.on('message', () => {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    });\n\n    // Empty messages are filtered at multiple layers, so we need to\n    // do a direct call\n    if (data.length === 0) {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    } else {\n        sock._websocket._receiveData(new Uint8Array(data));\n    }\n\n    display.flip();\n\n    return done;\n}\n\ndescribe('TightPng decoder', function () {\n    let decoder;\n    let display;\n\n    before(FakeWebSocket.replace);\n    after(FakeWebSocket.restore);\n\n    beforeEach(function () {\n        decoder = new TightPngDecoder();\n        display = new Display(document.createElement('canvas'));\n        display.resize(4, 4);\n    });\n\n    it('should handle the TightPng encoding', async function () {\n        let data = [\n            // Control bytes\n            0xa0, 0xb4, 0x04,\n            // PNG data\n            0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,\n            0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,\n            0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04,\n            0x08, 0x02, 0x00, 0x00, 0x00, 0x26, 0x93, 0x09,\n            0x29, 0x00, 0x00, 0x01, 0x84, 0x69, 0x43, 0x43,\n            0x50, 0x49, 0x43, 0x43, 0x20, 0x70, 0x72, 0x6f,\n            0x66, 0x69, 0x6c, 0x65, 0x00, 0x00, 0x28, 0x91,\n            0x7d, 0x91, 0x3d, 0x48, 0xc3, 0x40, 0x18, 0x86,\n            0xdf, 0xa6, 0x6a, 0x45, 0x2a, 0x0e, 0x76, 0x10,\n            0x71, 0x08, 0x52, 0x9d, 0x2c, 0x88, 0x8a, 0x38,\n            0x6a, 0x15, 0x8a, 0x50, 0x21, 0xd4, 0x0a, 0xad,\n            0x3a, 0x98, 0x5c, 0xfa, 0x07, 0x4d, 0x1a, 0x92,\n            0x14, 0x17, 0x47, 0xc1, 0xb5, 0xe0, 0xe0, 0xcf,\n            0x62, 0xd5, 0xc1, 0xc5, 0x59, 0x57, 0x07, 0x57,\n            0x41, 0x10, 0xfc, 0x01, 0x71, 0x72, 0x74, 0x52,\n            0x74, 0x91, 0x12, 0xbf, 0x4b, 0x0a, 0x2d, 0x62,\n            0xbc, 0xe3, 0xb8, 0x87, 0xf7, 0xbe, 0xf7, 0xe5,\n            0xee, 0x3b, 0x40, 0xa8, 0x97, 0x99, 0x66, 0x75,\n            0x8c, 0x03, 0x9a, 0x6e, 0x9b, 0xa9, 0x44, 0x5c,\n            0xcc, 0x64, 0x57, 0xc5, 0xd0, 0x2b, 0xba, 0x68,\n            0x86, 0x31, 0x8c, 0x2e, 0x99, 0x59, 0xc6, 0x9c,\n            0x24, 0x25, 0xe1, 0x3b, 0xbe, 0xee, 0x11, 0xe0,\n            0xfb, 0x5d, 0x8c, 0x67, 0xf9, 0xd7, 0xfd, 0x39,\n            0x7a, 0xd5, 0x9c, 0xc5, 0x80, 0x80, 0x48, 0x3c,\n            0xcb, 0x0c, 0xd3, 0x26, 0xde, 0x20, 0x9e, 0xde,\n            0xb4, 0x0d, 0xce, 0xfb, 0xc4, 0x11, 0x56, 0x94,\n            0x55, 0xe2, 0x73, 0xe2, 0x31, 0x93, 0x2e, 0x48,\n            0xfc, 0xc8, 0x75, 0xc5, 0xe3, 0x37, 0xce, 0x05,\n            0x97, 0x05, 0x9e, 0x19, 0x31, 0xd3, 0xa9, 0x79,\n            0xe2, 0x08, 0xb1, 0x58, 0x68, 0x63, 0xa5, 0x8d,\n            0x59, 0xd1, 0xd4, 0x88, 0xa7, 0x88, 0xa3, 0xaa,\n            0xa6, 0x53, 0xbe, 0x90, 0xf1, 0x58, 0xe5, 0xbc,\n            0xc5, 0x59, 0x2b, 0x57, 0x59, 0xf3, 0x9e, 0xfc,\n            0x85, 0xe1, 0x9c, 0xbe, 0xb2, 0xcc, 0x75, 0x5a,\n            0x43, 0x48, 0x60, 0x11, 0x4b, 0x90, 0x20, 0x42,\n            0x41, 0x15, 0x25, 0x94, 0x61, 0x23, 0x46, 0xbb,\n            0x4e, 0x8a, 0x85, 0x14, 0x9d, 0xc7, 0x7d, 0xfc,\n            0x83, 0xae, 0x5f, 0x22, 0x97, 0x42, 0xae, 0x12,\n            0x18, 0x39, 0x16, 0x50, 0x81, 0x06, 0xd9, 0xf5,\n            0x83, 0xff, 0xc1, 0xef, 0xde, 0x5a, 0xf9, 0xc9,\n            0x09, 0x2f, 0x29, 0x1c, 0x07, 0x3a, 0x5f, 0x1c,\n            0xe7, 0x63, 0x04, 0x08, 0xed, 0x02, 0x8d, 0x9a,\n            0xe3, 0x7c, 0x1f, 0x3b, 0x4e, 0xe3, 0x04, 0x08,\n            0x3e, 0x03, 0x57, 0x7a, 0xcb, 0x5f, 0xa9, 0x03,\n            0x33, 0x9f, 0xa4, 0xd7, 0x5a, 0x5a, 0xf4, 0x08,\n            0xe8, 0xdb, 0x06, 0x2e, 0xae, 0x5b, 0x9a, 0xb2,\n            0x07, 0x5c, 0xee, 0x00, 0x03, 0x4f, 0x86, 0x6c,\n            0xca, 0xae, 0x14, 0xa4, 0x25, 0xe4, 0xf3, 0xc0,\n            0xfb, 0x19, 0x7d, 0x53, 0x16, 0xe8, 0xbf, 0x05,\n            0x7a, 0xd6, 0xbc, 0xbe, 0x35, 0xcf, 0x71, 0xfa,\n            0x00, 0xa4, 0xa9, 0x57, 0xc9, 0x1b, 0xe0, 0xe0,\n            0x10, 0x18, 0x2d, 0x50, 0xf6, 0xba, 0xcf, 0xbb,\n            0xbb, 0xdb, 0xfb, 0xf6, 0x6f, 0x4d, 0xb3, 0x7f,\n            0x3f, 0x0a, 0x27, 0x72, 0x7d, 0x49, 0x29, 0x8b,\n            0xbb, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59,\n            0x73, 0x00, 0x00, 0x2e, 0x23, 0x00, 0x00, 0x2e,\n            0x23, 0x01, 0x78, 0xa5, 0x3f, 0x76, 0x00, 0x00,\n            0x00, 0x07, 0x74, 0x49, 0x4d, 0x45, 0x07, 0xe4,\n            0x06, 0x06, 0x0c, 0x23, 0x1d, 0x3f, 0x9f, 0xbb,\n            0x94, 0x00, 0x00, 0x00, 0x19, 0x74, 0x45, 0x58,\n            0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74,\n            0x00, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,\n            0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x47, 0x49,\n            0x4d, 0x50, 0x57, 0x81, 0x0e, 0x17, 0x00, 0x00,\n            0x00, 0x1e, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7,\n            0x65, 0xc9, 0xb1, 0x0d, 0x00, 0x00, 0x08, 0x03,\n            0x20, 0xea, 0xff, 0x3f, 0xd7, 0xd5, 0x44, 0x56,\n            0x52, 0x90, 0xc2, 0x38, 0xa2, 0xd0, 0xbc, 0x59,\n            0x8a, 0x9f, 0x04, 0x05, 0x6b, 0x38, 0x7b, 0xb2,\n            0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44,\n            0xae, 0x42, 0x60, 0x82,\n        ];\n\n        let decodeDone = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);\n        expect(decodeDone).to.be.true;\n\n        let targetData = new Uint8Array([\n            0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255\n        ]);\n\n        // Firefox currently has some very odd rounding bug:\n        // https://bugzilla.mozilla.org/show_bug.cgi?id=1667747\n        function almost(a, b) {\n            let diff = Math.abs(a - b);\n            return diff < 30;\n        }\n\n        await display.flush();\n        expect(display).to.have.displayed(targetData, almost);\n    });\n});\n"
  },
  {
    "path": "tests/test.util.js",
    "content": "/* eslint-disable no-console */\nimport * as Log from '../core/util/logging.js';\nimport { encodeUTF8, decodeUTF8 } from '../core/util/strings.js';\n\ndescribe('Utils', function () {\n    \"use strict\";\n\n    describe('logging functions', function () {\n        beforeEach(function () {\n            sinon.spy(console, 'log');\n            sinon.spy(console, 'debug');\n            sinon.spy(console, 'warn');\n            sinon.spy(console, 'error');\n            sinon.spy(console, 'info');\n        });\n\n        afterEach(function () {\n            console.log.restore();\n            console.debug.restore();\n            console.warn.restore();\n            console.error.restore();\n            console.info.restore();\n            Log.initLogging();\n        });\n\n        it('should use noop for levels lower than the min level', function () {\n            Log.initLogging('warn');\n            Log.Debug('hi');\n            Log.Info('hello');\n            expect(console.log).to.not.have.been.called;\n        });\n\n        it('should use console.debug for Debug', function () {\n            Log.initLogging('debug');\n            Log.Debug('dbg');\n            expect(console.debug).to.have.been.calledWith('dbg');\n        });\n\n        it('should use console.info for Info', function () {\n            Log.initLogging('debug');\n            Log.Info('inf');\n            expect(console.info).to.have.been.calledWith('inf');\n        });\n\n        it('should use console.warn for Warn', function () {\n            Log.initLogging('warn');\n            Log.Warn('wrn');\n            expect(console.warn).to.have.been.called;\n            expect(console.warn).to.have.been.calledWith('wrn');\n        });\n\n        it('should use console.error for Error', function () {\n            Log.initLogging('error');\n            Log.Error('err');\n            expect(console.error).to.have.been.called;\n            expect(console.error).to.have.been.calledWith('err');\n        });\n    });\n\n    describe('string functions', function () {\n        it('should decode UTF-8 to DOMString correctly', function () {\n            const utf8string = '\\xd0\\x9f';\n            const domstring = decodeUTF8(utf8string);\n            expect(domstring).to.equal(\"П\");\n        });\n\n        it('should encode DOMString to UTF-8 correctly', function () {\n            const domstring = \"åäöa\";\n            const utf8string = encodeUTF8(domstring);\n            expect(utf8string).to.equal('\\xc3\\xa5\\xc3\\xa4\\xc3\\xb6\\x61');\n        });\n\n        it('should allow Latin-1 strings if allowLatin1 is set when decoding', function () {\n            const latin1string = '\\xe5\\xe4\\xf6';\n            expect(() => decodeUTF8(latin1string)).to.throw(Error);\n            expect(decodeUTF8(latin1string, true)).to.equal('åäö');\n        });\n    });\n\n    // TODO(directxman12): test the conf_default and conf_defaults methods\n    // TODO(directxman12): test the event methods (addEvent, removeEvent, stopEvent)\n    // TODO(directxman12): figure out a good way to test getPosition and getEventPosition\n    // TODO(directxman12): figure out how to test the browser detection functions properly\n    //                     (we can't really test them against the browsers, except for Gecko\n    //                     via PhantomJS, the default test driver)\n});\n/* eslint-enable no-console */\n"
  },
  {
    "path": "tests/test.wakelock.js",
    "content": "/* jshint expr: true */\n\nimport WakeLockManager from '../app/wakelock.js';\n\nclass FakeWakeLockSentinal extends EventTarget {\n    constructor() {\n        super();\n        this.released = false;\n    }\n\n    async release() {\n        if (this.released) {\n            return;\n        }\n        this.released = true;\n        this.dispatchEvent(new Event(\"release\"));\n    }\n}\n\nfunction waitForStateTransition(wakelockManager, newState) {\n    const {promise, resolve} = Promise.withResolvers();\n\n    const eventListener = (event) => {\n        if (event.newState !== newState) {\n            return;\n        }\n        wakelockManager.removeEventListener(\"testOnlyStateChange\", eventListener);\n        resolve();\n    };\n    wakelockManager.addEventListener(\"testOnlyStateChange\", eventListener);\n\n    return promise;\n}\n\ndescribe('WakeLockManager', function () {\n    \"use strict\";\n\n    let wakelockRequest;\n    beforeEach(function () {\n        wakelockRequest = sinon.stub(navigator.wakeLock, 'request');\n    });\n    afterEach(function () {\n        wakelockRequest.restore();\n    });\n\n    it('can acquire and release lock', async function () {\n        let wakeLockSentinal = new FakeWakeLockSentinal();\n        wakelockRequest.onFirstCall().resolves(wakeLockSentinal);\n\n        let wlm = new WakeLockManager();\n        expect(wakelockRequest).to.not.have.been.called;\n\n        let done = waitForStateTransition(wlm, 'acquired');\n        wlm.acquire();\n        await done;\n        expect(wakelockRequest).to.have.been.calledOnce;\n        expect(wakeLockSentinal.released).to.be.false;\n\n        done = waitForStateTransition(wlm, 'released');\n        wlm.release();\n        await done;\n        expect(wakelockRequest).to.have.been.calledOnce;\n        expect(wakeLockSentinal.released).to.be.true;\n    });\n\n    it('can release without holding wakelock', async function () {\n        let wlm = new WakeLockManager();\n        wlm.release();\n        expect(wakelockRequest).to.not.have.been.called;\n    });\n\n    it('can release while waiting for wakelock', async function () {\n        let wakeLockSentinal = new FakeWakeLockSentinal();\n        let {promise, resolve} = Promise.withResolvers();\n\n        wakelockRequest.onFirstCall().returns(promise);\n\n        let wlm = new WakeLockManager();\n        expect(wakelockRequest).to.not.have.been.called;\n\n        let seenAcquiring = waitForStateTransition(wlm, 'acquiring');\n        let seenReleasing = waitForStateTransition(wlm, 'releasing');\n        let seenReleased = waitForStateTransition(wlm, 'released');\n\n        wlm.acquire();\n        await seenAcquiring;\n        expect(wakelockRequest).to.have.been.calledOnce;\n\n        // We can call acquire multiple times, while waiting for the promise\n        // to resolve.\n        wlm.acquire();\n        // It should not request a second wakelock.\n        expect(wakelockRequest).to.have.been.calledOnce;\n\n        wlm.release();\n        await seenReleasing;\n\n        expect(wakeLockSentinal.released).to.be.false;\n\n        // Now return the wake lock, we should immediately release it.\n        resolve(wakeLockSentinal);\n        await seenReleased;\n        expect(wakeLockSentinal.released).to.be.true;\n    });\n\n    it('handles visibility loss', async function () {\n        let documentHidden = sinon.stub(document, 'hidden');\n        let documentVisibility = sinon.stub(document, 'visibilityState');\n        afterEach(function () {\n            documentHidden.restore();\n            documentVisibility.restore();\n        });\n        documentHidden.value(false);\n        documentVisibility.value('visible');\n\n        let wakeLockSentinal1 = new FakeWakeLockSentinal();\n        let wakeLockSentinal2 = new FakeWakeLockSentinal();\n        wakelockRequest.onFirstCall().resolves(wakeLockSentinal1);\n        wakelockRequest.onSecondCall().resolves(wakeLockSentinal2);\n\n        let wlm = new WakeLockManager();\n        let seenAcquired = waitForStateTransition(wlm, 'acquired');\n        let seenAwaitingVisible = waitForStateTransition(wlm, 'awaiting_visible');\n\n        wlm.acquire();\n        await seenAcquired;\n        expect(wakelockRequest).to.have.been.calledOnce;\n\n        // Fake a visibility change.\n        documentHidden.value(true);\n        documentVisibility.value('hidden');\n        wakeLockSentinal1.release();\n\n        await seenAwaitingVisible;\n        seenAcquired = waitForStateTransition(wlm, 'acquired');\n\n        // Fake a visibility change back\n        documentHidden.value(false);\n        documentVisibility.value('visible');\n        document.dispatchEvent(new Event('visibilitychange'));\n        await seenAcquired;\n\n        expect(wakelockRequest).to.have.been.calledTwice;\n        expect(wakeLockSentinal2.released).to.be.false;\n    });\n\n    it('can start hidden', async function () {\n        let documentHidden = sinon.stub(document, 'hidden');\n        let documentVisibility = sinon.stub(document, 'visibilityState');\n        afterEach(function () {\n            documentHidden.restore();\n            documentVisibility.restore();\n        });\n        documentHidden.value(true);\n        documentVisibility.value('hidden');\n\n        let wakeLockSentinal = new FakeWakeLockSentinal();\n        wakelockRequest.onFirstCall().resolves(wakeLockSentinal);\n\n        let wlm = new WakeLockManager();\n        let seenAwaitingVisible = waitForStateTransition(wlm, 'awaiting_visible');\n        let seenAcquired = waitForStateTransition(wlm, 'acquired');\n\n        wlm.acquire();\n        await seenAwaitingVisible;\n        expect(wakelockRequest).to.not.have.been.called;\n\n        // Fake a visibility change.\n        documentHidden.value(false);\n        documentVisibility.value('visible');\n        document.dispatchEvent(new Event('visibilitychange'));\n        await seenAcquired;\n\n        expect(wakelockRequest).to.have.been.calledOnce;\n        expect(wakeLockSentinal.released).to.be.false;\n    });\n\n    it('handles acquire errors', async function () {\n        wakelockRequest.onFirstCall().rejects('WakeLockError');\n        let wakeLockSentinal = new FakeWakeLockSentinal();\n        wakelockRequest.onSecondCall().resolves(wakeLockSentinal);\n\n        let wlm = new WakeLockManager();\n\n        let seenError = waitForStateTransition(wlm, 'error');\n        wlm.acquire();\n        await seenError;\n        expect(wakelockRequest).to.have.been.calledOnce;\n\n        // Even though we saw an error previously, it will retry when\n        // requested.\n        let seenAcquired = waitForStateTransition(wlm, 'acquired');\n        wlm.acquire();\n        await seenAcquired;\n        expect(wakelockRequest).to.have.been.calledTwice;\n    });\n});\n"
  },
  {
    "path": "tests/test.websock.js",
    "content": "import Websock from '../core/websock.js';\nimport FakeWebSocket from './fake.websocket.js';\n\ndescribe('Websock', function () {\n    \"use strict\";\n\n    describe('Receive queue methods', function () {\n        let sock, websock;\n\n        beforeEach(function () {\n            sock = new Websock();\n            websock = new FakeWebSocket();\n            websock._open();\n            sock.attach(websock);\n        });\n\n        describe('rQpeek8', function () {\n            it('should peek at the next byte without poping it off the queue', function () {\n                websock._receiveData(new Uint8Array([0xab, 0xcd]));\n                expect(sock.rQpeek8()).to.equal(0xab);\n                expect(sock.rQpeek8()).to.equal(0xab);\n            });\n        });\n\n        describe('rQshift8()', function () {\n            it('should pop a single byte from the receive queue', function () {\n                websock._receiveData(new Uint8Array([0xab, 0xcd]));\n                expect(sock.rQshift8()).to.equal(0xab);\n                expect(sock.rQshift8()).to.equal(0xcd);\n            });\n        });\n\n        describe('rQshift16()', function () {\n            it('should pop two bytes from the receive queue and return a single number', function () {\n                websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34]));\n                expect(sock.rQshift16()).to.equal(0xabcd);\n                expect(sock.rQshift16()).to.equal(0x1234);\n            });\n        });\n\n        describe('rQshift32()', function () {\n            it('should pop four bytes from the receive queue and return a single number', function () {\n                websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,\n                                                     0x88, 0xee, 0x11, 0x33]));\n                expect(sock.rQshift32()).to.equal(0xabcd1234);\n                expect(sock.rQshift32()).to.equal(0x88ee1133);\n            });\n        });\n\n        describe('rQlen())', function () {\n            it('should return the number of buffered bytes in the receive queue', function () {\n                websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,\n                                                     0x88, 0xee, 0x11, 0x33]));\n                expect(sock.rQlen()).to.equal(8);\n                sock.rQshift8();\n                expect(sock.rQlen()).to.equal(7);\n                sock.rQshift16();\n                expect(sock.rQlen()).to.equal(5);\n                sock.rQshift32();\n                expect(sock.rQlen()).to.equal(1);\n            });\n        });\n\n        describe('rQshiftStr', function () {\n            it('should shift the given number of bytes off of the receive queue and return a string', function () {\n                websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,\n                                                     0x88, 0xee, 0x11, 0x33]));\n                expect(sock.rQshiftStr(4)).to.equal('\\xab\\xcd\\x12\\x34');\n                expect(sock.rQshiftStr(4)).to.equal('\\x88\\xee\\x11\\x33');\n            });\n\n            it('should be able to handle very large strings', function () {\n                const BIG_LEN = 500000;\n                const incoming = new Uint8Array(BIG_LEN);\n                let expected = \"\";\n                let letterCode = 'a'.charCodeAt(0);\n                for (let i = 0; i < BIG_LEN; i++) {\n                    incoming[i] = letterCode;\n                    expected += String.fromCharCode(letterCode);\n\n                    if (letterCode < 'z'.charCodeAt(0)) {\n                        letterCode++;\n                    } else {\n                        letterCode = 'a'.charCodeAt(0);\n                    }\n                }\n                websock._receiveData(incoming);\n\n                const shifted = sock.rQshiftStr(BIG_LEN);\n\n                expect(shifted).to.be.equal(expected);\n            });\n        });\n\n        describe('rQshiftBytes', function () {\n            it('should shift the given number of bytes of the receive queue and return an array', function () {\n                websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,\n                                                     0x88, 0xee, 0x11, 0x33]));\n                expect(sock.rQshiftBytes(4)).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34]));\n                expect(sock.rQshiftBytes(4)).to.array.equal(new Uint8Array([0x88, 0xee, 0x11, 0x33]));\n            });\n\n            it('should return a shared array if requested', function () {\n                websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,\n                                                     0x88, 0xee, 0x11, 0x33]));\n                const bytes = sock.rQshiftBytes(4, false);\n                expect(bytes).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34]));\n                expect(bytes.buffer.byteLength).to.not.equal(bytes.length);\n            });\n        });\n\n        describe('rQpeekBytes', function () {\n            it('should not modify the receive queue', function () {\n                websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,\n                                                     0x88, 0xee, 0x11, 0x33]));\n                expect(sock.rQpeekBytes(4)).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34]));\n                expect(sock.rQpeekBytes(4)).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34]));\n            });\n\n            it('should return a shared array if requested', function () {\n                websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,\n                                                     0x88, 0xee, 0x11, 0x33]));\n                const bytes = sock.rQpeekBytes(4, false);\n                expect(bytes).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34]));\n                expect(bytes.buffer.byteLength).to.not.equal(bytes.length);\n            });\n        });\n\n        describe('rQwait', function () {\n            beforeEach(function () {\n                websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,\n                                                     0x88, 0xee, 0x11, 0x33]));\n            });\n\n            it('should return true if there are not enough bytes in the receive queue', function () {\n                expect(sock.rQwait('hi', 9)).to.be.true;\n            });\n\n            it('should return false if there are enough bytes in the receive queue', function () {\n                expect(sock.rQwait('hi', 8)).to.be.false;\n            });\n\n            it('should return true and reduce rQi by \"goback\" if there are not enough bytes', function () {\n                expect(sock.rQshift32()).to.equal(0xabcd1234);\n                expect(sock.rQwait('hi', 8, 2)).to.be.true;\n                expect(sock.rQshift32()).to.equal(0x123488ee);\n            });\n\n            it('should raise an error if we try to go back more than possible', function () {\n                expect(sock.rQshift32()).to.equal(0xabcd1234);\n                expect(() => sock.rQwait('hi', 8, 6)).to.throw(Error);\n            });\n\n            it('should not reduce rQi if there are enough bytes', function () {\n                expect(sock.rQshift32()).to.equal(0xabcd1234);\n                expect(sock.rQwait('hi', 4, 2)).to.be.false;\n                expect(sock.rQshift32()).to.equal(0x88ee1133);\n            });\n        });\n    });\n\n    describe('Send queue methods', function () {\n        let sock;\n\n        const bufferSize = 10 * 1024;\n\n        beforeEach(function () {\n            let websock = new FakeWebSocket();\n            websock._open();\n            sock = new Websock();\n            sock.attach(websock);\n        });\n\n        describe('sQpush8()', function () {\n            it('should send a single byte', function () {\n                sock.sQpush8(42);\n                sock.flush();\n                expect(sock).to.have.sent(new Uint8Array([42]));\n            });\n            it('should not send any data until flushing', function () {\n                sock.sQpush8(42);\n                expect(sock).to.have.sent(new Uint8Array([]));\n            });\n            it('should implicitly flush if the queue is full', function () {\n                for (let i = 0;i <= bufferSize;i++) {\n                    sock.sQpush8(42);\n                }\n\n                let expected = [];\n                for (let i = 0;i < bufferSize;i++) {\n                    expected.push(42);\n                }\n\n                expect(sock).to.have.sent(new Uint8Array(expected));\n            });\n        });\n\n        describe('sQpush16()', function () {\n            it('should send a number as two bytes', function () {\n                sock.sQpush16(420);\n                sock.flush();\n                expect(sock).to.have.sent(new Uint8Array([1, 164]));\n            });\n            it('should not send any data until flushing', function () {\n                sock.sQpush16(420);\n                expect(sock).to.have.sent(new Uint8Array([]));\n            });\n            it('should implicitly flush if the queue is full', function () {\n                for (let i = 0;i <= bufferSize/2;i++) {\n                    sock.sQpush16(420);\n                }\n\n                let expected = [];\n                for (let i = 0;i < bufferSize/2;i++) {\n                    expected.push(1);\n                    expected.push(164);\n                }\n\n                expect(sock).to.have.sent(new Uint8Array(expected));\n            });\n        });\n\n        describe('sQpush32()', function () {\n            it('should send a number as two bytes', function () {\n                sock.sQpush32(420420);\n                sock.flush();\n                expect(sock).to.have.sent(new Uint8Array([0, 6, 106, 68]));\n            });\n            it('should not send any data until flushing', function () {\n                sock.sQpush32(420420);\n                expect(sock).to.have.sent(new Uint8Array([]));\n            });\n            it('should implicitly flush if the queue is full', function () {\n                for (let i = 0;i <= bufferSize/4;i++) {\n                    sock.sQpush32(420420);\n                }\n\n                let expected = [];\n                for (let i = 0;i < bufferSize/4;i++) {\n                    expected.push(0);\n                    expected.push(6);\n                    expected.push(106);\n                    expected.push(68);\n                }\n\n                expect(sock).to.have.sent(new Uint8Array(expected));\n            });\n        });\n\n        describe('sQpushString()', function () {\n            it('should send a string buffer', function () {\n                sock.sQpushString('\\x12\\x34\\x56\\x78\\x90');\n                sock.flush();\n                expect(sock).to.have.sent(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90]));\n            });\n            it('should not send any data until flushing', function () {\n                sock.sQpushString('\\x12\\x34\\x56\\x78\\x90');\n                expect(sock).to.have.sent(new Uint8Array([]));\n            });\n            it('should implicitly flush if the queue is full', function () {\n                for (let i = 0;i <= bufferSize/5;i++) {\n                    sock.sQpushString('\\x12\\x34\\x56\\x78\\x90');\n                }\n\n                let expected = [];\n                for (let i = 0;i < bufferSize/5;i++) {\n                    expected.push(0x12);\n                    expected.push(0x34);\n                    expected.push(0x56);\n                    expected.push(0x78);\n                    expected.push(0x90);\n                }\n\n                expect(sock).to.have.sent(new Uint8Array(expected));\n            });\n            it('should implicitly split a large buffer', function () {\n                let str = '';\n                let expected = [];\n                for (let i = 0;i < bufferSize * 3;i++) {\n                    let byte = Math.random() * 0xff;\n                    str += String.fromCharCode(byte);\n                    expected.push(byte);\n                }\n\n                sock.sQpushString(str);\n                sock.flush();\n\n                expect(sock).to.have.sent(new Uint8Array(expected));\n            });\n        });\n\n        describe('sQpushBytes()', function () {\n            it('should send a byte buffer', function () {\n                sock.sQpushBytes(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90]));\n                sock.flush();\n                expect(sock).to.have.sent(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90]));\n            });\n            it('should not send any data until flushing', function () {\n                sock.sQpushBytes(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90]));\n                expect(sock).to.have.sent(new Uint8Array([]));\n            });\n            it('should implicitly flush if the queue is full', function () {\n                for (let i = 0;i <= bufferSize/5;i++) {\n                    sock.sQpushBytes(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90]));\n                }\n\n                let expected = [];\n                for (let i = 0;i < bufferSize/5;i++) {\n                    expected.push(0x12);\n                    expected.push(0x34);\n                    expected.push(0x56);\n                    expected.push(0x78);\n                    expected.push(0x90);\n                }\n\n                expect(sock).to.have.sent(new Uint8Array(expected));\n            });\n            it('should implicitly split a large buffer', function () {\n                let buffer = [];\n                let expected = [];\n                for (let i = 0;i < bufferSize * 3;i++) {\n                    let byte = Math.random() * 0xff;\n                    buffer.push(byte);\n                    expected.push(byte);\n                }\n\n                sock.sQpushBytes(new Uint8Array(buffer));\n                sock.flush();\n\n                expect(sock).to.have.sent(new Uint8Array(expected));\n            });\n        });\n\n        describe('flush', function () {\n            it('should actually send on the websocket', function () {\n                sock._sQ = new Uint8Array([1, 2, 3]);\n                sock._sQlen = 3;\n\n                sock.flush();\n                expect(sock).to.have.sent(new Uint8Array([1, 2, 3]));\n            });\n\n            it('should not call send if we do not have anything queued up', function () {\n                sock._sQlen = 0;\n\n                sock.flush();\n\n                expect(sock).to.have.sent(new Uint8Array([]));\n            });\n        });\n    });\n\n    describe('lifecycle methods', function () {\n        let oldWS;\n        before(function () {\n            oldWS = WebSocket;\n        });\n\n        let sock;\n        beforeEach(function () {\n            sock = new Websock();\n            // eslint-disable-next-line no-global-assign\n            WebSocket = sinon.spy(FakeWebSocket);\n        });\n\n        describe('opening', function () {\n            it('should pick the correct protocols if none are given', function () {\n\n            });\n\n            it('should open the actual websocket', function () {\n                sock.open('ws://localhost:8675', 'binary');\n                expect(WebSocket).to.have.been.calledWith('ws://localhost:8675', 'binary');\n            });\n\n            // it('should initialize the event handlers')?\n        });\n\n        describe('attaching', function () {\n            it('should attach to an existing websocket', function () {\n                let ws = new FakeWebSocket('ws://localhost:8675');\n                sock.attach(ws);\n                expect(WebSocket).to.not.have.been.called;\n            });\n        });\n\n        describe('closing', function () {\n            beforeEach(function () {\n                sock.open('ws://localhost');\n                sock._websocket.close = sinon.spy();\n            });\n\n            it('should close the actual websocket if it is open', function () {\n                sock._websocket.readyState = WebSocket.OPEN;\n                sock.close();\n                expect(sock._websocket.close).to.have.been.calledOnce;\n            });\n\n            it('should close the actual websocket if it is connecting', function () {\n                sock._websocket.readyState = WebSocket.CONNECTING;\n                sock.close();\n                expect(sock._websocket.close).to.have.been.calledOnce;\n            });\n\n            it('should not try to close the actual websocket if closing', function () {\n                sock._websocket.readyState = WebSocket.CLOSING;\n                sock.close();\n                expect(sock._websocket.close).not.to.have.been.called;\n            });\n\n            it('should not try to close the actual websocket if closed', function () {\n                sock._websocket.readyState = WebSocket.CLOSED;\n                sock.close();\n                expect(sock._websocket.close).not.to.have.been.called;\n            });\n\n            it('should reset onmessage to not call _recvMessage', function () {\n                sinon.spy(sock, '_recvMessage');\n                sock.close();\n                sock._websocket.onmessage(null);\n                try {\n                    expect(sock._recvMessage).not.to.have.been.called;\n                } finally {\n                    sock._recvMessage.restore();\n                }\n            });\n        });\n\n        describe('event handlers', function () {\n            beforeEach(function () {\n                sock._recvMessage = sinon.spy();\n                sock.on('open', sinon.spy());\n                sock.on('close', sinon.spy());\n                sock.on('error', sinon.spy());\n                sock.open('ws://localhost');\n            });\n\n            it('should call _recvMessage on a message', function () {\n                sock._websocket.onmessage(null);\n                expect(sock._recvMessage).to.have.been.calledOnce;\n            });\n\n            it('should call the open event handler on opening', function () {\n                sock._websocket.onopen();\n                expect(sock._eventHandlers.open).to.have.been.calledOnce;\n            });\n\n            it('should call the close event handler on closing', function () {\n                sock._websocket.onclose();\n                expect(sock._eventHandlers.close).to.have.been.calledOnce;\n            });\n\n            it('should call the error event handler on error', function () {\n                sock._websocket.onerror();\n                expect(sock._eventHandlers.error).to.have.been.calledOnce;\n            });\n        });\n\n        describe('ready state', function () {\n            it('should be \"unused\" after construction', function () {\n                let sock = new Websock();\n                expect(sock.readyState).to.equal('unused');\n            });\n\n            it('should be \"connecting\" if WebSocket is connecting', function () {\n                let sock = new Websock();\n                let ws = new FakeWebSocket();\n                ws.readyState = WebSocket.CONNECTING;\n                sock.attach(ws);\n                expect(sock.readyState).to.equal('connecting');\n            });\n\n            it('should be \"open\" if WebSocket is open', function () {\n                let sock = new Websock();\n                let ws = new FakeWebSocket();\n                ws.readyState = WebSocket.OPEN;\n                sock.attach(ws);\n                expect(sock.readyState).to.equal('open');\n            });\n\n            it('should be \"closing\" if WebSocket is closing', function () {\n                let sock = new Websock();\n                let ws = new FakeWebSocket();\n                ws.readyState = WebSocket.CLOSING;\n                sock.attach(ws);\n                expect(sock.readyState).to.equal('closing');\n            });\n\n            it('should be \"closed\" if WebSocket is closed', function () {\n                let sock = new Websock();\n                let ws = new FakeWebSocket();\n                ws.readyState = WebSocket.CLOSED;\n                sock.attach(ws);\n                expect(sock.readyState).to.equal('closed');\n            });\n\n            it('should be \"unknown\" if WebSocket state is unknown', function () {\n                let sock = new Websock();\n                let ws = new FakeWebSocket();\n                ws.readyState = 666;\n                sock.attach(ws);\n                expect(sock.readyState).to.equal('unknown');\n            });\n\n            it('should be \"connecting\" if RTCDataChannel is connecting', function () {\n                let sock = new Websock();\n                let ws = new FakeWebSocket();\n                ws.readyState = 'connecting';\n                sock.attach(ws);\n                expect(sock.readyState).to.equal('connecting');\n            });\n\n            it('should be \"open\" if RTCDataChannel is open', function () {\n                let sock = new Websock();\n                let ws = new FakeWebSocket();\n                ws.readyState = 'open';\n                sock.attach(ws);\n                expect(sock.readyState).to.equal('open');\n            });\n\n            it('should be \"closing\" if RTCDataChannel is closing', function () {\n                let sock = new Websock();\n                let ws = new FakeWebSocket();\n                ws.readyState = 'closing';\n                sock.attach(ws);\n                expect(sock.readyState).to.equal('closing');\n            });\n\n            it('should be \"closed\" if RTCDataChannel is closed', function () {\n                let sock = new Websock();\n                let ws = new FakeWebSocket();\n                ws.readyState = 'closed';\n                sock.attach(ws);\n                expect(sock.readyState).to.equal('closed');\n            });\n\n            it('should be \"unknown\" if RTCDataChannel state is unknown', function () {\n                let sock = new Websock();\n                let ws = new FakeWebSocket();\n                ws.readyState = 'foobar';\n                sock.attach(ws);\n                expect(sock.readyState).to.equal('unknown');\n            });\n        });\n\n        after(function () {\n            // eslint-disable-next-line no-global-assign\n            WebSocket = oldWS;\n        });\n    });\n\n    describe('WebSocket receiving', function () {\n        let sock;\n        beforeEach(function () {\n            sock = new Websock();\n            sock._allocateBuffers();\n        });\n\n        it('should support adding data to the receive queue', function () {\n            const msg = { data: new Uint8Array([1, 2, 3]) };\n            sock._recvMessage(msg);\n            expect(sock.rQshiftStr(3)).to.equal('\\x01\\x02\\x03');\n        });\n\n        it('should call the message event handler if present', function () {\n            sock._eventHandlers.message = sinon.spy();\n            const msg = { data: new Uint8Array([1, 2, 3]).buffer };\n            sock._mode = 'binary';\n            sock._recvMessage(msg);\n            expect(sock._eventHandlers.message).to.have.been.calledOnce;\n        });\n\n        it('should not call the message event handler if there is nothing in the receive queue', function () {\n            sock._eventHandlers.message = sinon.spy();\n            const msg = { data: new Uint8Array([]).buffer };\n            sock._mode = 'binary';\n            sock._recvMessage(msg);\n            expect(sock._eventHandlers.message).not.to.have.been.called;\n        });\n\n        it('should compact the receive queue when fully read', function () {\n            sock._rQ = new Uint8Array([0, 1, 2, 3, 4, 5, 0, 0, 0, 0]);\n            sock._rQlen = 6;\n            sock._rQi = 6;\n            const msg = { data: new Uint8Array([1, 2, 3]).buffer };\n            sock._recvMessage(msg);\n            expect(sock._rQlen).to.equal(3);\n            expect(sock._rQi).to.equal(0);\n        });\n\n        it('should compact the receive queue when we reach the end of the buffer', function () {\n            sock._rQ = new Uint8Array(20);\n            sock._rQbufferSize = 20;\n            sock._rQlen = 20;\n            sock._rQi = 10;\n            const msg = { data: new Uint8Array([1, 2]).buffer };\n            sock._recvMessage(msg);\n            expect(sock._rQlen).to.equal(12);\n            expect(sock._rQi).to.equal(0);\n        });\n\n        it('should automatically resize the receive queue if the incoming message is larger than the buffer', function () {\n            sock._rQ = new Uint8Array(20);\n            sock._rQlen = 0;\n            sock._rQi = 0;\n            sock._rQbufferSize = 20;\n            const msg = { data: new Uint8Array(30).buffer };\n            sock._recvMessage(msg);\n            expect(sock._rQlen).to.equal(30);\n            expect(sock._rQi).to.equal(0);\n            expect(sock._rQ.length).to.equal(240);  // keep the invariant that rQbufferSize / 8 >= rQlen\n        });\n\n        it('should automatically resize the receive queue if the incoming message is larger than 1/8th of the buffer and we reach the end of the buffer', function () {\n            sock._rQ = new Uint8Array(20);\n            sock._rQlen = 16;\n            sock._rQi = 15;\n            sock._rQbufferSize = 20;\n            const msg = { data: new Uint8Array(6).buffer };\n            sock._recvMessage(msg);\n            expect(sock._rQlen).to.equal(7);\n            expect(sock._rQi).to.equal(0);\n            expect(sock._rQ.length).to.equal(56);\n        });\n    });\n});\n"
  },
  {
    "path": "tests/test.webutil.js",
    "content": "/* jshint expr: true */\n\nimport * as WebUtil from '../app/webutil.js';\n\ndescribe('WebUtil', function () {\n    \"use strict\";\n\n    describe('config variables', function () {\n        let origState, origHref;\n        beforeEach(function () {\n            origState = history.state;\n            origHref = location.href;\n        });\n        afterEach(function () {\n            history.replaceState(origState, '', origHref);\n        });\n\n        it('should parse query string variables', function () {\n            // history.pushState() will not cause the browser to attempt loading\n            // the URL, this is exactly what we want here for the tests.\n            history.replaceState({}, '', \"test?myvar=myval\");\n            expect(WebUtil.getConfigVar(\"myvar\")).to.be.equal(\"myval\");\n        });\n        it('should return default value when no query match', function () {\n            history.replaceState({}, '', \"test?myvar=myval\");\n            expect(WebUtil.getConfigVar(\"other\", \"def\")).to.be.equal(\"def\");\n        });\n        it('should handle no query match and no default value', function () {\n            history.replaceState({}, '', \"test?myvar=myval\");\n            expect(WebUtil.getConfigVar(\"other\")).to.be.equal(null);\n        });\n        it('should parse fragment variables', function () {\n            history.replaceState({}, '', \"test#myvar=myval\");\n            expect(WebUtil.getConfigVar(\"myvar\")).to.be.equal(\"myval\");\n        });\n        it('should return default value when no fragment match', function () {\n            history.replaceState({}, '', \"test#myvar=myval\");\n            expect(WebUtil.getConfigVar(\"other\", \"def\")).to.be.equal(\"def\");\n        });\n        it('should handle no fragment match and no default value', function () {\n            history.replaceState({}, '', \"test#myvar=myval\");\n            expect(WebUtil.getConfigVar(\"other\")).to.be.equal(null);\n        });\n        it('should handle both query and fragment', function () {\n            history.replaceState({}, '', \"test?myquery=1#myhash=2\");\n            expect(WebUtil.getConfigVar(\"myquery\")).to.be.equal(\"1\");\n            expect(WebUtil.getConfigVar(\"myhash\")).to.be.equal(\"2\");\n        });\n        it('should prioritize fragment if both provide same var', function () {\n            history.replaceState({}, '', \"test?myvar=1#myvar=2\");\n            expect(WebUtil.getConfigVar(\"myvar\")).to.be.equal(\"2\");\n        });\n    });\n\n    describe('cookies', function () {\n        // TODO\n    });\n\n    describe('settings', function () {\n\n        describe('localStorage', function () {\n            let chrome = window.chrome;\n            before(function () {\n                chrome = window.chrome;\n                window.chrome = null;\n            });\n            after(function () {\n                window.chrome = chrome;\n            });\n\n            let origLocalStorage;\n            beforeEach(function () {\n                origLocalStorage = Object.getOwnPropertyDescriptor(window, \"localStorage\");\n\n                Object.defineProperty(window, \"localStorage\", {value: {}});\n\n                window.localStorage.setItem = sinon.stub();\n                window.localStorage.getItem = sinon.stub();\n                window.localStorage.removeItem = sinon.stub();\n\n                return WebUtil.initSettings();\n            });\n            afterEach(function () {\n                Object.defineProperty(window, \"localStorage\", origLocalStorage);\n            });\n\n            describe('writeSetting', function () {\n                it('should save the setting value to local storage', function () {\n                    WebUtil.writeSetting('test', 'value');\n                    expect(window.localStorage.setItem).to.have.been.calledWithExactly('test', 'value');\n                    expect(WebUtil.readSetting('test')).to.equal('value');\n                });\n\n                it('should not crash when local storage save fails', function () {\n                    localStorage.setItem.throws(new DOMException());\n                    expect(WebUtil.writeSetting('test', 'value')).to.not.throw;\n                });\n            });\n\n            describe('setSetting', function () {\n                it('should update the setting but not save to local storage', function () {\n                    WebUtil.setSetting('test', 'value');\n                    expect(window.localStorage.setItem).to.not.have.been.called;\n                    expect(WebUtil.readSetting('test')).to.equal('value');\n                });\n            });\n\n            describe('readSetting', function () {\n                it('should read the setting value from local storage', function () {\n                    localStorage.getItem.returns('value');\n                    expect(WebUtil.readSetting('test')).to.equal('value');\n                });\n\n                it('should return the default value when not in local storage', function () {\n                    expect(WebUtil.readSetting('test', 'default')).to.equal('default');\n                });\n\n                it('should return the cached value even if local storage changed', function () {\n                    localStorage.getItem.returns('value');\n                    expect(WebUtil.readSetting('test')).to.equal('value');\n                    localStorage.getItem.returns('something else');\n                    expect(WebUtil.readSetting('test')).to.equal('value');\n                });\n\n                it('should cache the value even if it is not initially in local storage', function () {\n                    expect(WebUtil.readSetting('test')).to.be.null;\n                    localStorage.getItem.returns('value');\n                    expect(WebUtil.readSetting('test')).to.be.null;\n                });\n\n                it('should return the default value always if the first read was not in local storage', function () {\n                    expect(WebUtil.readSetting('test', 'default')).to.equal('default');\n                    localStorage.getItem.returns('value');\n                    expect(WebUtil.readSetting('test', 'another default')).to.equal('another default');\n                });\n\n                it('should return the last local written value', function () {\n                    localStorage.getItem.returns('value');\n                    expect(WebUtil.readSetting('test')).to.equal('value');\n                    WebUtil.writeSetting('test', 'something else');\n                    expect(WebUtil.readSetting('test')).to.equal('something else');\n                });\n\n                it('should not crash when local storage read fails', function () {\n                    localStorage.getItem.throws(new DOMException());\n                    expect(WebUtil.readSetting('test')).to.not.throw;\n                });\n            });\n\n            // this doesn't appear to be used anywhere\n            describe('eraseSetting', function () {\n                it('should remove the setting from local storage', function () {\n                    WebUtil.eraseSetting('test');\n                    expect(window.localStorage.removeItem).to.have.been.calledWithExactly('test');\n                });\n\n                it('should not crash when local storage remove fails', function () {\n                    localStorage.removeItem.throws(new DOMException());\n                    expect(WebUtil.eraseSetting('test')).to.not.throw;\n                });\n            });\n        });\n\n        describe('chrome.storage', function () {\n            let chrome = window.chrome;\n            let settings = {};\n            before(function () {\n                chrome = window.chrome;\n                window.chrome = {\n                    storage: {\n                        sync: {\n                            get(cb) { cb(settings); },\n                            set() {},\n                            remove() {}\n                        }\n                    }\n                };\n            });\n            after(function () {\n                window.chrome = chrome;\n            });\n\n            beforeEach(function () {\n                settings = {};\n                sinon.spy(window.chrome.storage.sync, 'set');\n                sinon.spy(window.chrome.storage.sync, 'remove');\n                return WebUtil.initSettings();\n            });\n            afterEach(function () {\n                window.chrome.storage.sync.set.restore();\n                window.chrome.storage.sync.remove.restore();\n            });\n\n            describe('writeSetting', function () {\n                it('should save the setting value to chrome storage', function () {\n                    WebUtil.writeSetting('test', 'value');\n                    expect(window.chrome.storage.sync.set).to.have.been.calledWithExactly(sinon.match({ test: 'value' }));\n                    expect(WebUtil.readSetting('test')).to.equal('value');\n                });\n            });\n\n            describe('setSetting', function () {\n                it('should update the setting but not save to chrome storage', function () {\n                    WebUtil.setSetting('test', 'value');\n                    expect(window.chrome.storage.sync.set).to.not.have.been.called;\n                    expect(WebUtil.readSetting('test')).to.equal('value');\n                });\n            });\n\n            describe('readSetting', function () {\n                it('should read the setting value from chrome storage', function () {\n                    settings.test = 'value';\n                    expect(WebUtil.readSetting('test')).to.equal('value');\n                });\n\n                it('should return the default value when not in chrome storage', function () {\n                    expect(WebUtil.readSetting('test', 'default')).to.equal('default');\n                });\n\n                it('should return the last local written value', function () {\n                    settings.test = 'value';\n                    expect(WebUtil.readSetting('test')).to.equal('value');\n                    WebUtil.writeSetting('test', 'something else');\n                    expect(WebUtil.readSetting('test')).to.equal('something else');\n                });\n            });\n\n            // this doesn't appear to be used anywhere\n            describe('eraseSetting', function () {\n                it('should remove the setting from chrome storage', function () {\n                    WebUtil.eraseSetting('test');\n                    expect(window.chrome.storage.sync.remove).to.have.been.calledWithExactly('test');\n                });\n            });\n        });\n    });\n});\n"
  },
  {
    "path": "tests/test.zlib.js",
    "content": "import Websock from '../core/websock.js';\nimport Display from '../core/display.js';\n\nimport ZlibDecoder from '../core/decoders/zlib.js';\n\nimport FakeWebSocket from './fake.websocket.js';\n\nfunction testDecodeRect(decoder, x, y, width, height, data, display, depth) {\n    let sock;\n    let done = false;\n\n    sock = new Websock;\n    sock.open(\"ws://example.com\");\n\n    sock.on('message', () => {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    });\n\n    // Empty messages are filtered at multiple layers, so we need to\n    // do a direct call\n    if (data.length === 0) {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    } else {\n        sock._websocket._receiveData(new Uint8Array(data));\n    }\n\n    display.flip();\n\n    return done;\n}\n\ndescribe('Zlib decoder', function () {\n    let decoder;\n    let display;\n\n    before(FakeWebSocket.replace);\n    after(FakeWebSocket.restore);\n\n    beforeEach(function () {\n        decoder = new ZlibDecoder();\n        display = new Display(document.createElement('canvas'));\n        display.resize(4, 4);\n    });\n\n    it('should handle the Zlib encoding', function () {\n        let done;\n\n        let zlibData = new Uint8Array([\n            0x00, 0x00, 0x00, 0x23, /* length */\n            0x78, 0x01, 0xfa, 0xcf, 0x00, 0x04, 0xff, 0x61, 0x04, 0x90, 0x01, 0x41, 0x50, 0xc1, 0xff, 0x0c,\n            0xef, 0x40, 0x02, 0xef, 0xfe, 0x33, 0xac, 0x02, 0xe2, 0xd5, 0x40, 0x8c, 0xce, 0x07, 0x00, 0x00,\n            0x00, 0xff, 0xff,\n        ]);\n        done = testDecodeRect(decoder, 0, 0, 4, 4, zlibData, display, 24);\n        expect(done).to.be.true;\n\n        let targetData = new Uint8ClampedArray([\n            0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255,\n            0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255\n        ]);\n\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle empty rects', function () {\n        display.fillRect(0, 0, 4, 4, [0x00, 0x00, 0xff]);\n        display.fillRect(2, 0, 2, 2, [0x00, 0xff, 0x00]);\n        display.fillRect(0, 2, 2, 2, [0x00, 0xff, 0x00]);\n\n        let done = testDecodeRect(decoder, 1, 2, 0, 0, [], display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,\n            0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n});\n"
  },
  {
    "path": "tests/test.zrle.js",
    "content": "import Websock from '../core/websock.js';\nimport Display from '../core/display.js';\n\nimport ZRLEDecoder from '../core/decoders/zrle.js';\n\nimport FakeWebSocket from './fake.websocket.js';\n\nfunction testDecodeRect(decoder, x, y, width, height, data, display, depth) {\n    let sock;\n    let done = false;\n\n    sock = new Websock;\n    sock.open(\"ws://example.com\");\n\n    sock.on('message', () => {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    });\n\n    // Empty messages are filtered at multiple layers, so we need to\n    // do a direct call\n    if (data.length === 0) {\n        done = decoder.decodeRect(x, y, width, height, sock, display, depth);\n    } else {\n        sock._websocket._receiveData(new Uint8Array(data));\n    }\n\n    display.flip();\n\n    return done;\n}\n\ndescribe('ZRLE decoder', function () {\n    let decoder;\n    let display;\n\n    before(FakeWebSocket.replace);\n    after(FakeWebSocket.restore);\n\n    beforeEach(function () {\n        decoder = new ZRLEDecoder();\n        display = new Display(document.createElement('canvas'));\n        display.resize(4, 4);\n    });\n\n    it('should handle the Raw subencoding', function () {\n        let done = testDecodeRect(decoder, 0, 0, 4, 4,\n                                  [0x00, 0x00, 0x00, 0x0e, 0x78, 0x5e,\n                                   0x62, 0x60, 0x60, 0xf8, 0x4f, 0x12,\n                                   0x02, 0x00, 0x00, 0x00, 0xff, 0xff],\n                                  display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,\n            0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,\n            0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,\n            0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle the Solid subencoding', function () {\n        let done = testDecodeRect(decoder, 0, 0, 4, 4,\n                                  [0x00, 0x00, 0x00, 0x0c, 0x78, 0x5e,\n                                   0x62, 0x64, 0x60, 0xf8, 0x0f, 0x00,\n                                   0x00, 0x00, 0xff, 0xff],\n                                  display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,\n            0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,\n            0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,\n            0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n\n    it('should handle the Palette Tile subencoding', function () {\n        let done = testDecodeRect(decoder, 0, 0, 4, 4,\n                                  [0x00, 0x00, 0x00, 0x12, 0x78, 0x5E,\n                                   0x62, 0x62, 0x60,  248, 0xff, 0x9F,\n                                   0x01, 0x08, 0x3E, 0x7C, 0x00, 0x00,\n                                   0x00, 0x00, 0xff, 0xff],\n                                  display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,\n            0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,\n            0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff,\n            0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle the RLE Tile subencoding', function () {\n        let done = testDecodeRect(decoder, 0, 0, 4, 4,\n                                  [0x00, 0x00, 0x00, 0x0d, 0x78, 0x5e,\n                                   0x6a, 0x60, 0x60, 0xf8, 0x2f, 0x00,\n                                   0x00, 0x00, 0x00, 0xff, 0xff],\n                                  display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,\n            0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,\n            0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,\n            0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should handle the RLE Palette Tile subencoding', function () {\n        let done = testDecodeRect(decoder, 0, 0, 4, 4,\n                                  [0x00, 0x00, 0x00, 0x11, 0x78, 0x5e,\n                                   0x6a, 0x62, 0x60, 0xf8, 0xff, 0x9f,\n                                   0x81, 0xa1, 0x81, 0x1f, 0x00, 0x00,\n                                   0x00, 0xff, 0xff],\n                                  display, 24);\n\n        let targetData = new Uint8Array([\n            0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,\n            0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,\n            0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,\n            0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff\n        ]);\n\n        expect(done).to.be.true;\n        expect(display).to.have.displayed(targetData);\n    });\n\n    it('should fail on an invalid subencoding', function () {\n        let data = [0x00, 0x00, 0x00, 0x0c, 0x78, 0x5e, 0x6a, 0x64, 0x60, 0xf8, 0x0f, 0x00, 0x00, 0x00, 0xff, 0xff];\n        expect(() => testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24)).to.throw();\n    });\n});\n"
  },
  {
    "path": "tests/vnc_playback.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <title>VNC playback</title>\n        <script type=\"module\" src=\"./playback-ui.js\"></script>\n    </head>\n    <body>\n\n        Iterations: <input id='iterations'>&nbsp;\n        Perftest:<input type='radio' id='mode1' name='mode' checked>&nbsp;\n        Realtime:<input type='radio' id='mode2' name='mode'>&nbsp;&nbsp;\n\n        <input id='startButton' type='button' value='Start' disabled>&nbsp;\n\n        <br><br>\n\n        Results:<br>\n        <textarea id=\"messages\" cols=80 rows=25></textarea>\n\n        <br><br>\n\n        <div id=\"VNC_screen\">\n            <div id=\"VNC_status\">Loading</div>\n        </div>\n    </body>\n</html>\n"
  },
  {
    "path": "utils/README.md",
    "content": "## WebSockets Proxy/Bridge\n\nWebsockify has been forked out into its own project. `novnc_proxy` will\nautomatically download it here if it is not already present and not\ninstalled as system-wide.\n\nFor more detailed description and usage information please refer to\nthe [websockify README](https://github.com/novnc/websockify/blob/master/README.md).\n\nThe other versions of websockify (C, Node.js) and the associated test\nprograms have been moved to\n[websockify](https://github.com/novnc/websockify).  Websockify was\nformerly named wsproxy.\n\n"
  },
  {
    "path": "utils/b64-to-binary.pl",
    "content": "#!/usr/bin/env perl\nuse MIME::Base64;\n\nfor (<>) {\n    unless (/^'([{}])(\\d+)\\1(.+?)',$/) {\n        print;\n        next;\n    }\n\n    my ($dir, $amt, $b64) = ($1, $2, $3);\n\n    my $decoded = MIME::Base64::decode($b64) or die \"Could not base64-decode line `$_`\";\n\n    my $decoded_escaped = join \"\", map { \"\\\\x$_\" } unpack(\"(H2)*\", $decoded);\n\n    print \"'${dir}${amt}${dir}${decoded_escaped}',\\n\";\n}\n"
  },
  {
    "path": "utils/genkeysymdef.js",
    "content": "#!/usr/bin/env node\n/*\n * genkeysymdef: X11 keysymdef.h to JavaScript converter\n * Copyright (C) 2018 The noVNC authors\n * Licensed under MPL 2.0 (see LICENSE.txt)\n */\n\n\"use strict\";\n\nimport fs from 'fs';\n\nlet showHelp = process.argv.length === 2;\nlet filename;\n\nfor (let i = 2; i < process.argv.length; ++i) {\n    switch (process.argv[i]) {\n        case \"--help\":\n        case \"-h\":\n            showHelp = true;\n            break;\n        case \"--file\":\n        case \"-f\":\n        default:\n            filename = process.argv[i];\n    }\n}\n\nif (!filename) {\n    showHelp = true;\n    console.log(\"Error: No filename specified\\n\");\n}\n\nif (showHelp) {\n    console.log(\"Parses a *nix keysymdef.h to generate Unicode code point mappings\");\n    console.log(\"Usage: node parse.js [options] filename:\");\n    console.log(\"  -h [ --help ]                 Produce this help message\");\n    console.log(\"  filename                      The keysymdef.h file to parse\");\n    process.exit(0);\n}\n\nconst buf = fs.readFileSync(filename);\nconst str = buf.toString('utf8');\n\nconst re = /^#define XK_([a-zA-Z_0-9]+)\\s+0x([0-9a-fA-F]+)\\s*(\\/\\*\\s*(.*)\\s*\\*\\/)?\\s*$/m;\n\nconst arr = str.split('\\n');\n\nconst codepoints = {};\n\nfor (let i = 0; i < arr.length; ++i) {\n    const result = re.exec(arr[i]);\n    if (result) {\n        const keyname = result[1];\n        const keysym = parseInt(result[2], 16);\n        const remainder = result[3];\n\n        const unicodeRes = /U\\+([0-9a-fA-F]+)/.exec(remainder);\n        if (unicodeRes) {\n            const unicode = parseInt(unicodeRes[1], 16);\n            // The first entry is the preferred one\n            if (!codepoints[unicode]) {\n                codepoints[unicode] = { keysym: keysym, name: keyname };\n            }\n        }\n    }\n}\n\nlet out =\n\"/*\\n\" +\n\" * Mapping from Unicode codepoints to X11/RFB keysyms\\n\" +\n\" *\\n\" +\n\" * This file was automatically generated from keysymdef.h\\n\" +\n\" * DO NOT EDIT!\\n\" +\n\" */\\n\" +\n\"\\n\" +\n\"/* Functions at the bottom */\\n\" +\n\"\\n\" +\n\"const codepoints = {\\n\";\n\nfunction toHex(num) {\n    let s = num.toString(16);\n    if (s.length < 4) {\n        s = (\"0000\" + s).slice(-4);\n    }\n    return \"0x\" + s;\n}\n\nfor (let codepoint in codepoints) {\n    codepoint = parseInt(codepoint);\n\n    // Latin-1?\n    if ((codepoint >= 0x20) && (codepoint <= 0xff)) {\n        continue;\n    }\n\n    // Handled by the general Unicode mapping?\n    if ((codepoint | 0x01000000) === codepoints[codepoint].keysym) {\n        continue;\n    }\n\n    out += \"    \" + toHex(codepoint) + \": \" +\n           toHex(codepoints[codepoint].keysym) +\n           \", // XK_\" + codepoints[codepoint].name + \"\\n\";\n}\n\nout +=\n\"};\\n\" +\n\"\\n\" +\n\"export default {\\n\" +\n\"    lookup(u) {\\n\" +\n\"        // Latin-1 is one-to-one mapping\\n\" +\n\"        if ((u >= 0x20) && (u <= 0xff)) {\\n\" +\n\"            return u;\\n\" +\n\"        }\\n\" +\n\"\\n\" +\n\"        // Lookup table (fairly random)\\n\" +\n\"        const keysym = codepoints[u];\\n\" +\n\"        if (keysym !== undefined) {\\n\" +\n\"            return keysym;\\n\" +\n\"        }\\n\" +\n\"\\n\" +\n\"        // General mapping as final fallback\\n\" +\n\"        return 0x01000000 | u;\\n\" +\n\"    },\\n\" +\n\"};\";\n\nconsole.log(out);\n"
  },
  {
    "path": "utils/novnc_proxy",
    "content": "#!/usr/bin/env bash\n\n# Copyright (C) 2018 The noVNC authors\n# Licensed under MPL 2.0 or any later version (see LICENSE.txt)\n\nusage() {\n    if [ \"$*\" ]; then\n        echo \"$*\"\n        echo\n    fi\n    echo \"Usage: ${NAME} [--listen [HOST:]PORT] [--vnc VNC_HOST:PORT] [--cert CERT] [--ssl-only]\"\n    echo\n    echo \"Starts the WebSockets proxy and a mini-webserver and \"\n    echo \"provides a cut-and-paste URL to go to.\"\n    echo\n    echo \"    --listen [HOST:]PORT  Port for proxy/webserver to listen on\"\n    echo \"                          Default: 6080 (on all interfaces)\"\n    echo \"    --vnc VNC_HOST:PORT   VNC server host:port proxy target\"\n    echo \"                          Default: localhost:5900\"\n    echo \"    --cert CERT           Path to combined cert/key file, or just\"\n    echo \"                          the cert file if used with --key\"\n    echo \"                          Default: self.pem\"\n    echo \"    --key KEY             Path to key file, when not combined with cert\"\n    echo \"    --web WEB             Path to web files (e.g. vnc.html)\"\n    echo \"                          Default: ./\"\n    echo \"    --ssl-only            Disable non-https connections.\"\n    echo \"                                    \"\n    echo \"    --file-only           Disable directory listing in web server.\"\n    echo \"                                    \"\n    echo \"    --record FILE         Record traffic to FILE.session.js\"\n    echo \"                                    \"\n    echo \"    --syslog SERVER       Can be local socket such as /dev/log, or a UDP host:port pair.\"\n    echo \"                                    \"\n    echo \"    --heartbeat SEC       send a ping to the client every SEC seconds\"\n    echo \"    --timeout SEC         after SEC seconds exit when not connected\"\n    echo \"    --idle-timeout SEC    server exits after SEC seconds if there are no\"\n    echo \"                                    \"\n    echo \"    --web-auth            enable authentication\"\n    echo \"    --auth-plugin CLASS   authentication plugin to use\"\n    echo \"    --auth-source ARG     plugin configuration\"\n    echo \"                                    \"\n    echo \"                          active connections\"\n    echo \"                                    \"\n    exit 2\n}\n\nNAME=\"$(basename $0)\"\nREAL_NAME=\"$(readlink -f $0)\"\nHERE=\"$(cd \"$(dirname \"$REAL_NAME\")\" && pwd)\"\nHOST=\"\"\nPORT=\"6080\"\nLISTEN=\"$PORT\"\nVNC_DEST=\"localhost:5900\"\nCERT=\"\"\nKEY=\"\"\nWEB=\"\"\nproxy_pid=\"\"\nSSLONLY=\"\"\nRECORD=\"\"\nSYSLOG_ARG=\"\"\nHEARTBEAT_ARG=\"\"\nIDLETIMEOUT_ARG=\"\"\nTIMEOUT_ARG=\"\"\nWEBAUTH_ARG=\"\"\nAUTHPLUGIN_ARG=\"\"\nAUTHSOURCE_ARG=\"\"\nFILEONLY_ARG=\"\"\n\n\ndie() {\n    echo \"$*\"\n    exit 1\n}\n\ncleanup() {\n    trap - TERM QUIT INT EXIT\n    trap \"true\" CHLD   # Ignore cleanup messages\n    echo\n    if [ -n \"${proxy_pid}\" ]; then\n        echo \"Terminating WebSockets proxy (${proxy_pid})\"\n        kill ${proxy_pid}\n    fi\n}\n\n# Process arguments\n\n# Arguments that only apply to chrooter itself\nwhile [ \"$*\" ]; do\n    param=$1; shift; OPTARG=$1\n    case $param in\n    --listen)  LISTEN=\"${OPTARG}\"; shift            ;;\n    --vnc)     VNC_DEST=\"${OPTARG}\"; shift        ;;\n    --cert)    CERT=\"${OPTARG}\"; shift            ;;\n    --key)     KEY=\"${OPTARG}\"; shift             ;;\n    --web)     WEB=\"${OPTARG}\"; shift            ;;\n    --ssl-only) SSLONLY=\"--ssl-only\"             ;;\n    --file-only) FILEONLY_ARG=\"--file-only\"      ;;\n    --record) RECORD=\"${OPTARG}\"; shift ;;\n    --syslog) SYSLOG_ARG=\"--syslog ${OPTARG}\"; shift ;;\n    --heartbeat) HEARTBEAT_ARG=\"--heartbeat ${OPTARG}\"; shift ;;\n    --idle-timeout) IDLETIMEOUT_ARG=\"--idle-timeout ${OPTARG}\"; shift ;;\n    --timeout) TIMEOUT_ARG=\"--timeout ${OPTARG}\"; shift ;;\n    --web-auth) WEBAUTH_ARG=\"--web-auth\"                ;;\n    --auth-plugin) AUTHPLUGIN_ARG=\"--auth-plugin ${OPTARG}\"; shift ;;\n    --auth-source) AUTHSOURCE_ARG=\"--auth-source ${OPTARG}\"; shift ;;\n    -h|--help) usage                              ;;\n    -*) usage \"Unknown chrooter option: ${param}\" ;;\n    *) break                                      ;;\n    esac\ndone\n\nif [ \"$LISTEN\" != \"$PORT\" ]; then\n    HOST=${LISTEN%:*}\n    PORT=${LISTEN##*:}\n    # if no host was given, restore\n    [ \"$HOST\" = \"$PORT\" ] && HOST=\"\"\nfi\n\n# Sanity checks\nif [ -z \"${HOST}\" ]; then\n    if bash -c \"exec 7<>/dev/tcp/localhost/${PORT}\" &> /dev/null; then\n        exec 7<&-\n        exec 7>&-\n        die \"Port ${PORT} in use. Try --listen PORT\"\n    else\n        exec 7<&-\n        exec 7>&-\n    fi\nfi\n\ntrap \"cleanup\" TERM QUIT INT EXIT\n\n# Find vnc.html\nif [ -n \"${WEB}\" ]; then\n    if [ ! -e \"${WEB}/vnc.html\" ]; then\n        die \"Could not find ${WEB}/vnc.html\"\n    fi\nelif [ -e \"$(pwd)/vnc.html\" ]; then\n    WEB=$(pwd)\nelif [ -e \"${HERE}/../vnc.html\" ]; then\n    WEB=${HERE}/../\nelif [ -e \"${HERE}/vnc.html\" ]; then\n    WEB=${HERE}\nelif [ -e \"${HERE}/../share/novnc/vnc.html\" ]; then\n    WEB=${HERE}/../share/novnc/\nelse\n    die \"Could not find vnc.html\"\nfi\n\n# Find self.pem\nif [ -n \"${CERT}\" ]; then\n    if [ ! -e \"${CERT}\" ]; then\n        die \"Could not find ${CERT}\"\n    fi\nelif [ -e \"$(pwd)/self.pem\" ]; then\n    CERT=\"$(pwd)/self.pem\"\nelif [ -e \"${HERE}/../self.pem\" ]; then\n    CERT=\"${HERE}/../self.pem\"\nelif [ -e \"${HERE}/self.pem\" ]; then\n    CERT=\"${HERE}/self.pem\"\nelse\n    echo \"Warning: could not find self.pem\"\nfi\n\n# Check key file\nif [ -n \"${KEY}\" ]; then\n    if [ ! -e \"${KEY}\" ]; then\n        die \"Could not find ${KEY}\"\n    fi\nfi\n\n# try to find websockify (prefer local, try global, then download local)\nif [[ -d ${HERE}/websockify ]]; then\n    WEBSOCKIFY=${HERE}/websockify/run\n\n    if [[ ! -x $WEBSOCKIFY ]]; then\n        echo \"The path ${HERE}/websockify exists, but $WEBSOCKIFY either does not exist or is not executable.\"\n        echo \"If you intended to use an installed websockify package, please remove ${HERE}/websockify.\"\n        exit 1\n    fi\n\n    echo \"Using local websockify at $WEBSOCKIFY\"\nelse\n    WEBSOCKIFY_FROMSYSTEM=$(type -P websockify 2>/dev/null)\n    [ -f $WEBSOCKIFY_FROMSYSTEM ] && WEBSOCKIFY=$WEBSOCKIFY_FROMSYSTEM\n\n    if [ ! -f \"$WEBSOCKIFY\" ]; then\n        echo \"No installed websockify, attempting to clone websockify...\"\n        WEBSOCKIFY=${HERE}/websockify/run\n        git clone https://github.com/novnc/websockify ${HERE}/websockify\n\n        if [[ ! -e $WEBSOCKIFY ]]; then\n            echo \"Unable to locate ${HERE}/websockify/run after downloading\"\n            exit 1\n        fi\n\n        echo \"Using local websockify at $WEBSOCKIFY\"\n    else\n        echo \"Using installed websockify at $WEBSOCKIFY\"\n    fi\nfi\n\n# Make all file paths absolute as websockify changes working directory\nWEB=`realpath \"${WEB}\"`\n[ -n \"${CERT}\" ] && CERT=`realpath \"${CERT}\"`\n[ -n \"${KEY}\" ] && KEY=`realpath \"${KEY}\"`\n[ -n \"${RECORD}\" ] && RECORD=`realpath \"${RECORD}\"`\n\necho \"Starting webserver and WebSockets proxy on${HOST:+ host ${HOST}} port ${PORT}\"\n${WEBSOCKIFY} ${SYSLOG_ARG} ${SSLONLY} ${FILEONLY_ARG} --web ${WEB} ${CERT:+--cert ${CERT}} ${KEY:+--key ${KEY}} ${LISTEN} ${VNC_DEST} ${HEARTBEAT_ARG} ${IDLETIMEOUT_ARG} ${RECORD:+--record ${RECORD}} ${TIMEOUT_ARG} ${WEBAUTH_ARG} ${AUTHPLUGIN_ARG} ${AUTHSOURCE_ARG} &\nproxy_pid=\"$!\"\nsleep 1\nif [ -z \"$proxy_pid\" ] || ! ps -eo pid= | grep -w \"$proxy_pid\" > /dev/null; then\n    proxy_pid=\n    echo \"Failed to start WebSockets proxy\"\n    exit 1\nfi\n\nif [ -z \"$HOST\" ]; then\n    HOST=$(hostname)\nfi\n\necho -e \"\\n\\nNavigate to this URL:\\n\"\nif [ \"x$SSLONLY\" == \"x\" ]; then\n    echo -e \"    http://${HOST}:${PORT}/vnc.html\\n\"\nelse\n    echo -e \"    https://${HOST}:${PORT}/vnc.html\\n\"\nfi\n\necho -e \"Press Ctrl-C to exit\\n\\n\"\n\nwait ${proxy_pid}\n"
  },
  {
    "path": "utils/u2x11",
    "content": "#!/usr/bin/env bash\n#\n# Convert \"U+...\" commented entries in /usr/include/X11/keysymdef.h\n# into JavaScript for use by noVNC.  Note this is likely to produce\n# a few duplicate properties with clashing values, that will need\n# resolving manually.\n#\n# Colin Dean <colin@xvpsource.org>\n#\n\nregex=\"^#define[ \\t]+XK_[A-Za-z0-9_]+[ \\t]+0x([0-9a-fA-F]+)[ \\t]+\\/\\*[ \\t]+U\\+([0-9a-fA-F]+)[ \\t]+[^*]+.[ \\t]+\\*\\/[ \\t]*$\"\necho \"unicodeTable = {\"\nwhile read line; do\n    if echo \"${line}\" | egrep -qs \"${regex}\"; then\n\n        x11=$(echo \"${line}\" | sed -r \"s/${regex}/\\1/\")\n        vnc=$(echo \"${line}\" | sed -r \"s/${regex}/\\2/\")\n\t\n\tif echo \"${vnc}\" | egrep -qs \"^00[2-9A-F][0-9A-F]$\"; then\n\t    : # skip ISO Latin-1 (U+0020 to U+00FF) as 1-to-1 mapping\n\telse\n\t    # note 1-to-1 is possible (e.g. for Euro symbol, U+20AC)\n\t    echo \"    0x${vnc} : 0x${x11},\"\n\tfi\n    fi\ndone < /usr/include/X11/keysymdef.h | uniq\necho \"};\"\n\n"
  },
  {
    "path": "utils/validate",
    "content": "#!/bin/bash\n\nset -e\n\nRET=0\n\nOUT=`mktemp`\n\nfor fn in \"$@\"; do\n\techo \"Validating $fn...\"\n\techo\n\n\tcase $fn in\n\t\t*.html)\n\t\t\ttype=\"text/html\"\n\t\t\t;;\n\t\t*.css)\n\t\t\ttype=\"text/css\"\n\t\t\t;;\n\t\t*)\n\t\t\techo \"Unknown format!\"\n\t\t\techo\n\t\t\tRET=1\n\t\t\tcontinue\n\t\t\t;;\n\tesac\n\n\tcurl --silent \\\n\t\t--header \"Content-Type: ${type}; charset=utf-8\" \\\n\t\t--data-binary @${fn} \\\n\t\t\"https://validator.w3.org/nu/?out=gnu&level=error&asciiquotes=yes\" \\\n\t\t> $OUT\n\n\t# We don't fail the check for warnings as some warnings are\n\t# not relevant for us, and we don't currently have a way to\n\t# ignore just those\n\twhile read -r line; do\n\t\techo\n\n\t\tline_info=$(echo $line | cut -d \":\" -f 2)\n\t\tstart_info=$(echo $line_info | cut -d \"-\" -f 1)\n\t\tend_info=$(echo $line_info | cut -d \"-\" -f 2)\n\n\t\tline_start=$(echo $start_info | cut -d \".\" -f 1)\n\t\tcol_start=$(echo $start_info | cut -d \".\" -f 2)\n\n\t\tline_end=$(echo $end_info | cut -d \".\" -f 1)\n\t\tcol_end=$(echo $end_info | cut -d \".\" -f 2)\n\n\t\terror=$(echo $line | cut -d \":\" -f 4-)\n\n\t\tcase $error in\n\t\t\t*\"\\\"scrollbar-gutter\\\": Property \\\"scrollbar-gutter\\\" doesn't exist.\")\n\t\t\t\t# FIXME: https://github.com/validator/validator/issues/1788\n\t\t\t\techo \"Ignoring below error on line ${line_start},\" \\\n\t\t\t\t     \"the scrollbar-gutter property actually exist and is widely\" \\\n\t\t\t\t     \"supported:\"\n\t\t\t\techo $error\n\t\t\t\tcontinue\n\t\t\t\t;;\n\t\t\t*\"\\\"clip-path\\\": \\\"path(\"*)\n\t\t\t\t# FIXME: https://github.com/validator/validator/issues/1786\n\t\t\t\techo \"Ignoring below error on line ${line_start},\" \\\n\t\t\t\t     \"the path() function is valid for clip-path and is\" \\\n\t\t\t\t     \"widely supported:\"\n\t\t\t\techo $error\n\t\t\t\tcontinue\n\t\t\t\t;;\n\t\t\t*\"Parse Error.\")\n\t\t\t\t# FIXME: https://github.com/validator/validator/issues/1786\n\t\t\t\tlineofselector=$(grep -n \"@supports selector(\" $fn | cut -d \":\" -f 1)\n\t\t\t\tlinediff=$((lineofselector-line_start))\n\t\t\t\t# Only ignore if parse error is within 50 lines of \"selector()\"\n\t\t\t\tif [ ${linediff#-} -lt 50 ]; then\n\t\t\t\t\techo \"Ignoring below error on line ${line_start},\" \\\n\t\t\t\t\t     \"the @supports selector() function should not give a parse\" \\\n\t\t\t\t\t     \"error:\"\n\t\t\t\t\techo $error\n\t\t\t\t\tcontinue\n\t\t\t\tfi\n\t\t\t\t;;\n\t\tesac\n\t\techo \"ERROR between line ${line_start} (col ${col_start})\" \\\n\t\t     \"and line ${line_end} (col ${col_end}):\"\n\t\techo $error\n\t\tRET=1\n\tdone < \"$OUT\"\ndone\n\nrm $OUT\n\nexit $RET\n"
  },
  {
    "path": "vendor/pako/LICENSE",
    "content": "(The MIT License)\n\nCopyright (C) 2014-2016 by Vitaly Puzrin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "vendor/pako/README.md",
    "content": "This is an ES6-modules-compatible version of\nhttps://github.com/nodeca/pako, based on pako version 1.0.3.\n\nIt's more-or-less a direct translation of the original, with unused parts\nremoved, and the dynamic support for non-typed arrays removed (since ES6\nmodules don't work well with dynamic exports).\n"
  },
  {
    "path": "vendor/pako/lib/utils/common.js",
    "content": "// reduce buffer size, avoiding mem copy\nexport function shrinkBuf (buf, size) {\n  if (buf.length === size) { return buf; }\n  if (buf.subarray) { return buf.subarray(0, size); }\n  buf.length = size;\n  return buf;\n};\n\n\nexport function arraySet (dest, src, src_offs, len, dest_offs) {\n  if (src.subarray && dest.subarray) {\n    dest.set(src.subarray(src_offs, src_offs + len), dest_offs);\n    return;\n  }\n  // Fallback to ordinary array\n  for (var i = 0; i < len; i++) {\n    dest[dest_offs + i] = src[src_offs + i];\n  }\n}\n\n// Join array of chunks to single array.\nexport function flattenChunks (chunks) {\n  var i, l, len, pos, chunk, result;\n\n  // calculate data length\n  len = 0;\n  for (i = 0, l = chunks.length; i < l; i++) {\n    len += chunks[i].length;\n  }\n\n  // join chunks\n  result = new Uint8Array(len);\n  pos = 0;\n  for (i = 0, l = chunks.length; i < l; i++) {\n    chunk = chunks[i];\n    result.set(chunk, pos);\n    pos += chunk.length;\n  }\n\n  return result;\n}\n\nexport var Buf8  = Uint8Array;\nexport var Buf16 = Uint16Array;\nexport var Buf32 = Int32Array;\n"
  },
  {
    "path": "vendor/pako/lib/zlib/adler32.js",
    "content": "// Note: adler32 takes 12% for level 0 and 2% for level 6.\n// It doesn't worth to make additional optimizationa as in original.\n// Small size is preferable.\n\nexport default function adler32(adler, buf, len, pos) {\n  var s1 = (adler & 0xffff) |0,\n      s2 = ((adler >>> 16) & 0xffff) |0,\n      n = 0;\n\n  while (len !== 0) {\n    // Set limit ~ twice less than 5552, to keep\n    // s2 in 31-bits, because we force signed ints.\n    // in other case %= will fail.\n    n = len > 2000 ? 2000 : len;\n    len -= n;\n\n    do {\n      s1 = (s1 + buf[pos++]) |0;\n      s2 = (s2 + s1) |0;\n    } while (--n);\n\n    s1 %= 65521;\n    s2 %= 65521;\n  }\n\n  return (s1 | (s2 << 16)) |0;\n}\n"
  },
  {
    "path": "vendor/pako/lib/zlib/constants.js",
    "content": "export default {\n\n  /* Allowed flush values; see deflate() and inflate() below for details */\n  Z_NO_FLUSH:         0,\n  Z_PARTIAL_FLUSH:    1,\n  Z_SYNC_FLUSH:       2,\n  Z_FULL_FLUSH:       3,\n  Z_FINISH:           4,\n  Z_BLOCK:            5,\n  Z_TREES:            6,\n\n  /* Return codes for the compression/decompression functions. Negative values\n  * are errors, positive values are used for special but normal events.\n  */\n  Z_OK:               0,\n  Z_STREAM_END:       1,\n  Z_NEED_DICT:        2,\n  Z_ERRNO:           -1,\n  Z_STREAM_ERROR:    -2,\n  Z_DATA_ERROR:      -3,\n  //Z_MEM_ERROR:     -4,\n  Z_BUF_ERROR:       -5,\n  //Z_VERSION_ERROR: -6,\n\n  /* compression levels */\n  Z_NO_COMPRESSION:         0,\n  Z_BEST_SPEED:             1,\n  Z_BEST_COMPRESSION:       9,\n  Z_DEFAULT_COMPRESSION:   -1,\n\n\n  Z_FILTERED:               1,\n  Z_HUFFMAN_ONLY:           2,\n  Z_RLE:                    3,\n  Z_FIXED:                  4,\n  Z_DEFAULT_STRATEGY:       0,\n\n  /* Possible values of the data_type field (though see inflate()) */\n  Z_BINARY:                 0,\n  Z_TEXT:                   1,\n  //Z_ASCII:                1, // = Z_TEXT (deprecated)\n  Z_UNKNOWN:                2,\n\n  /* The deflate compression method */\n  Z_DEFLATED:               8\n  //Z_NULL:                 null // Use -1 or null inline, depending on var type\n};\n"
  },
  {
    "path": "vendor/pako/lib/zlib/crc32.js",
    "content": "// Note: we can't get significant speed boost here.\n// So write code to minimize size - no pregenerated tables\n// and array tools dependencies.\n\n\n// Use ordinary array, since untyped makes no boost here\nexport default function makeTable() {\n  var c, table = [];\n\n  for (var n = 0; n < 256; n++) {\n    c = n;\n    for (var k = 0; k < 8; k++) {\n      c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));\n    }\n    table[n] = c;\n  }\n\n  return table;\n}\n\n// Create table on load. Just 255 signed longs. Not a problem.\nvar crcTable = makeTable();\n\n\nfunction crc32(crc, buf, len, pos) {\n  var t = crcTable,\n      end = pos + len;\n\n  crc ^= -1;\n\n  for (var i = pos; i < end; i++) {\n    crc = (crc >>> 8) ^ t[(crc ^ buf[i]) & 0xFF];\n  }\n\n  return (crc ^ (-1)); // >>> 0;\n}\n"
  },
  {
    "path": "vendor/pako/lib/zlib/deflate.js",
    "content": "import * as utils from \"../utils/common.js\";\nimport * as trees from \"./trees.js\";\nimport adler32 from \"./adler32.js\";\nimport crc32 from \"./crc32.js\";\nimport msg from \"./messages.js\";\n\n/* Public constants ==========================================================*/\n/* ===========================================================================*/\n\n\n/* Allowed flush values; see deflate() and inflate() below for details */\nexport const Z_NO_FLUSH      = 0;\nexport const Z_PARTIAL_FLUSH = 1;\n//export const Z_SYNC_FLUSH    = 2;\nexport const Z_FULL_FLUSH    = 3;\nexport const Z_FINISH        = 4;\nexport const Z_BLOCK         = 5;\n//export const Z_TREES         = 6;\n\n\n/* Return codes for the compression/decompression functions. Negative values\n * are errors, positive values are used for special but normal events.\n */\nexport const Z_OK            = 0;\nexport const Z_STREAM_END    = 1;\n//export const Z_NEED_DICT     = 2;\n//export const Z_ERRNO         = -1;\nexport const Z_STREAM_ERROR  = -2;\nexport const Z_DATA_ERROR    = -3;\n//export const Z_MEM_ERROR     = -4;\nexport const Z_BUF_ERROR     = -5;\n//export const Z_VERSION_ERROR = -6;\n\n\n/* compression levels */\n//export const Z_NO_COMPRESSION      = 0;\n//export const Z_BEST_SPEED          = 1;\n//export const Z_BEST_COMPRESSION    = 9;\nexport const Z_DEFAULT_COMPRESSION = -1;\n\n\nexport const Z_FILTERED            = 1;\nexport const Z_HUFFMAN_ONLY        = 2;\nexport const Z_RLE                 = 3;\nexport const Z_FIXED               = 4;\nexport const Z_DEFAULT_STRATEGY    = 0;\n\n/* Possible values of the data_type field (though see inflate()) */\n//export const Z_BINARY              = 0;\n//export const Z_TEXT                = 1;\n//export const Z_ASCII               = 1; // = Z_TEXT\nexport const Z_UNKNOWN             = 2;\n\n\n/* The deflate compression method */\nexport const Z_DEFLATED  = 8;\n\n/*============================================================================*/\n\n\nvar MAX_MEM_LEVEL = 9;\n/* Maximum value for memLevel in deflateInit2 */\nvar MAX_WBITS = 15;\n/* 32K LZ77 window */\nvar DEF_MEM_LEVEL = 8;\n\n\nvar LENGTH_CODES  = 29;\n/* number of length codes, not counting the special END_BLOCK code */\nvar LITERALS      = 256;\n/* number of literal bytes 0..255 */\nvar L_CODES       = LITERALS + 1 + LENGTH_CODES;\n/* number of Literal or Length codes, including the END_BLOCK code */\nvar D_CODES       = 30;\n/* number of distance codes */\nvar BL_CODES      = 19;\n/* number of codes used to transfer the bit lengths */\nvar HEAP_SIZE     = 2 * L_CODES + 1;\n/* maximum heap size */\nvar MAX_BITS  = 15;\n/* All codes must not exceed MAX_BITS bits */\n\nvar MIN_MATCH = 3;\nvar MAX_MATCH = 258;\nvar MIN_LOOKAHEAD = (MAX_MATCH + MIN_MATCH + 1);\n\nvar PRESET_DICT = 0x20;\n\nvar INIT_STATE = 42;\nvar EXTRA_STATE = 69;\nvar NAME_STATE = 73;\nvar COMMENT_STATE = 91;\nvar HCRC_STATE = 103;\nvar BUSY_STATE = 113;\nvar FINISH_STATE = 666;\n\nvar BS_NEED_MORE      = 1; /* block not completed, need more input or more output */\nvar BS_BLOCK_DONE     = 2; /* block flush performed */\nvar BS_FINISH_STARTED = 3; /* finish started, need only more output at next deflate */\nvar BS_FINISH_DONE    = 4; /* finish done, accept no more input or output */\n\nvar OS_CODE = 0x03; // Unix :) . Don't detect, use this default.\n\nfunction err(strm, errorCode) {\n  strm.msg = msg[errorCode];\n  return errorCode;\n}\n\nfunction rank(f) {\n  return ((f) << 1) - ((f) > 4 ? 9 : 0);\n}\n\nfunction zero(buf) { var len = buf.length; while (--len >= 0) { buf[len] = 0; } }\n\n\n/* =========================================================================\n * Flush as much pending output as possible. All deflate() output goes\n * through this function so some applications may wish to modify it\n * to avoid allocating a large strm->output buffer and copying into it.\n * (See also read_buf()).\n */\nfunction flush_pending(strm) {\n  var s = strm.state;\n\n  //_tr_flush_bits(s);\n  var len = s.pending;\n  if (len > strm.avail_out) {\n    len = strm.avail_out;\n  }\n  if (len === 0) { return; }\n\n  utils.arraySet(strm.output, s.pending_buf, s.pending_out, len, strm.next_out);\n  strm.next_out += len;\n  s.pending_out += len;\n  strm.total_out += len;\n  strm.avail_out -= len;\n  s.pending -= len;\n  if (s.pending === 0) {\n    s.pending_out = 0;\n  }\n}\n\n\nfunction flush_block_only(s, last) {\n  trees._tr_flush_block(s, (s.block_start >= 0 ? s.block_start : -1), s.strstart - s.block_start, last);\n  s.block_start = s.strstart;\n  flush_pending(s.strm);\n}\n\n\nfunction put_byte(s, b) {\n  s.pending_buf[s.pending++] = b;\n}\n\n\n/* =========================================================================\n * Put a short in the pending buffer. The 16-bit value is put in MSB order.\n * IN assertion: the stream state is correct and there is enough room in\n * pending_buf.\n */\nfunction putShortMSB(s, b) {\n//  put_byte(s, (Byte)(b >> 8));\n//  put_byte(s, (Byte)(b & 0xff));\n  s.pending_buf[s.pending++] = (b >>> 8) & 0xff;\n  s.pending_buf[s.pending++] = b & 0xff;\n}\n\n\n/* ===========================================================================\n * Read a new buffer from the current input stream, update the adler32\n * and total number of bytes read.  All deflate() input goes through\n * this function so some applications may wish to modify it to avoid\n * allocating a large strm->input buffer and copying from it.\n * (See also flush_pending()).\n */\nfunction read_buf(strm, buf, start, size) {\n  var len = strm.avail_in;\n\n  if (len > size) { len = size; }\n  if (len === 0) { return 0; }\n\n  strm.avail_in -= len;\n\n  // zmemcpy(buf, strm->next_in, len);\n  utils.arraySet(buf, strm.input, strm.next_in, len, start);\n  if (strm.state.wrap === 1) {\n    strm.adler = adler32(strm.adler, buf, len, start);\n  }\n\n  else if (strm.state.wrap === 2) {\n    strm.adler = crc32(strm.adler, buf, len, start);\n  }\n\n  strm.next_in += len;\n  strm.total_in += len;\n\n  return len;\n}\n\n\n/* ===========================================================================\n * Set match_start to the longest match starting at the given string and\n * return its length. Matches shorter or equal to prev_length are discarded,\n * in which case the result is equal to prev_length and match_start is\n * garbage.\n * IN assertions: cur_match is the head of the hash chain for the current\n *   string (strstart) and its distance is <= MAX_DIST, and prev_length >= 1\n * OUT assertion: the match length is not greater than s->lookahead.\n */\nfunction longest_match(s, cur_match) {\n  var chain_length = s.max_chain_length;      /* max hash chain length */\n  var scan = s.strstart; /* current string */\n  var match;                       /* matched string */\n  var len;                           /* length of current match */\n  var best_len = s.prev_length;              /* best match length so far */\n  var nice_match = s.nice_match;             /* stop if match long enough */\n  var limit = (s.strstart > (s.w_size - MIN_LOOKAHEAD)) ?\n      s.strstart - (s.w_size - MIN_LOOKAHEAD) : 0/*NIL*/;\n\n  var _win = s.window; // shortcut\n\n  var wmask = s.w_mask;\n  var prev  = s.prev;\n\n  /* Stop when cur_match becomes <= limit. To simplify the code,\n   * we prevent matches with the string of window index 0.\n   */\n\n  var strend = s.strstart + MAX_MATCH;\n  var scan_end1  = _win[scan + best_len - 1];\n  var scan_end   = _win[scan + best_len];\n\n  /* The code is optimized for HASH_BITS >= 8 and MAX_MATCH-2 multiple of 16.\n   * It is easy to get rid of this optimization if necessary.\n   */\n  // Assert(s->hash_bits >= 8 && MAX_MATCH == 258, \"Code too clever\");\n\n  /* Do not waste too much time if we already have a good match: */\n  if (s.prev_length >= s.good_match) {\n    chain_length >>= 2;\n  }\n  /* Do not look for matches beyond the end of the input. This is necessary\n   * to make deflate deterministic.\n   */\n  if (nice_match > s.lookahead) { nice_match = s.lookahead; }\n\n  // Assert((ulg)s->strstart <= s->window_size-MIN_LOOKAHEAD, \"need lookahead\");\n\n  do {\n    // Assert(cur_match < s->strstart, \"no future\");\n    match = cur_match;\n\n    /* Skip to next match if the match length cannot increase\n     * or if the match length is less than 2.  Note that the checks below\n     * for insufficient lookahead only occur occasionally for performance\n     * reasons.  Therefore uninitialized memory will be accessed, and\n     * conditional jumps will be made that depend on those values.\n     * However the length of the match is limited to the lookahead, so\n     * the output of deflate is not affected by the uninitialized values.\n     */\n\n    if (_win[match + best_len]     !== scan_end  ||\n        _win[match + best_len - 1] !== scan_end1 ||\n        _win[match]                !== _win[scan] ||\n        _win[++match]              !== _win[scan + 1]) {\n      continue;\n    }\n\n    /* The check at best_len-1 can be removed because it will be made\n     * again later. (This heuristic is not always a win.)\n     * It is not necessary to compare scan[2] and match[2] since they\n     * are always equal when the other bytes match, given that\n     * the hash keys are equal and that HASH_BITS >= 8.\n     */\n    scan += 2;\n    match++;\n    // Assert(*scan == *match, \"match[2]?\");\n\n    /* We check for insufficient lookahead only every 8th comparison;\n     * the 256th check will be made at strstart+258.\n     */\n    do {\n      // Do nothing\n    } while (_win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&\n             _win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&\n             _win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&\n             _win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&\n             scan < strend);\n\n    // Assert(scan <= s->window+(unsigned)(s->window_size-1), \"wild scan\");\n\n    len = MAX_MATCH - (strend - scan);\n    scan = strend - MAX_MATCH;\n\n    if (len > best_len) {\n      s.match_start = cur_match;\n      best_len = len;\n      if (len >= nice_match) {\n        break;\n      }\n      scan_end1  = _win[scan + best_len - 1];\n      scan_end   = _win[scan + best_len];\n    }\n  } while ((cur_match = prev[cur_match & wmask]) > limit && --chain_length !== 0);\n\n  if (best_len <= s.lookahead) {\n    return best_len;\n  }\n  return s.lookahead;\n}\n\n\n/* ===========================================================================\n * Fill the window when the lookahead becomes insufficient.\n * Updates strstart and lookahead.\n *\n * IN assertion: lookahead < MIN_LOOKAHEAD\n * OUT assertions: strstart <= window_size-MIN_LOOKAHEAD\n *    At least one byte has been read, or avail_in == 0; reads are\n *    performed for at least two bytes (required for the zip translate_eol\n *    option -- not supported here).\n */\nfunction fill_window(s) {\n  var _w_size = s.w_size;\n  var p, n, m, more, str;\n\n  //Assert(s->lookahead < MIN_LOOKAHEAD, \"already enough lookahead\");\n\n  do {\n    more = s.window_size - s.lookahead - s.strstart;\n\n    // JS ints have 32 bit, block below not needed\n    /* Deal with !@#$% 64K limit: */\n    //if (sizeof(int) <= 2) {\n    //    if (more == 0 && s->strstart == 0 && s->lookahead == 0) {\n    //        more = wsize;\n    //\n    //  } else if (more == (unsigned)(-1)) {\n    //        /* Very unlikely, but possible on 16 bit machine if\n    //         * strstart == 0 && lookahead == 1 (input done a byte at time)\n    //         */\n    //        more--;\n    //    }\n    //}\n\n\n    /* If the window is almost full and there is insufficient lookahead,\n     * move the upper half to the lower one to make room in the upper half.\n     */\n    if (s.strstart >= _w_size + (_w_size - MIN_LOOKAHEAD)) {\n\n      utils.arraySet(s.window, s.window, _w_size, _w_size, 0);\n      s.match_start -= _w_size;\n      s.strstart -= _w_size;\n      /* we now have strstart >= MAX_DIST */\n      s.block_start -= _w_size;\n\n      /* Slide the hash table (could be avoided with 32 bit values\n       at the expense of memory usage). We slide even when level == 0\n       to keep the hash table consistent if we switch back to level > 0\n       later. (Using level 0 permanently is not an optimal usage of\n       zlib, so we don't care about this pathological case.)\n       */\n\n      n = s.hash_size;\n      p = n;\n      do {\n        m = s.head[--p];\n        s.head[p] = (m >= _w_size ? m - _w_size : 0);\n      } while (--n);\n\n      n = _w_size;\n      p = n;\n      do {\n        m = s.prev[--p];\n        s.prev[p] = (m >= _w_size ? m - _w_size : 0);\n        /* If n is not on any hash chain, prev[n] is garbage but\n         * its value will never be used.\n         */\n      } while (--n);\n\n      more += _w_size;\n    }\n    if (s.strm.avail_in === 0) {\n      break;\n    }\n\n    /* If there was no sliding:\n     *    strstart <= WSIZE+MAX_DIST-1 && lookahead <= MIN_LOOKAHEAD - 1 &&\n     *    more == window_size - lookahead - strstart\n     * => more >= window_size - (MIN_LOOKAHEAD-1 + WSIZE + MAX_DIST-1)\n     * => more >= window_size - 2*WSIZE + 2\n     * In the BIG_MEM or MMAP case (not yet supported),\n     *   window_size == input_size + MIN_LOOKAHEAD  &&\n     *   strstart + s->lookahead <= input_size => more >= MIN_LOOKAHEAD.\n     * Otherwise, window_size == 2*WSIZE so more >= 2.\n     * If there was sliding, more >= WSIZE. So in all cases, more >= 2.\n     */\n    //Assert(more >= 2, \"more < 2\");\n    n = read_buf(s.strm, s.window, s.strstart + s.lookahead, more);\n    s.lookahead += n;\n\n    /* Initialize the hash value now that we have some input: */\n    if (s.lookahead + s.insert >= MIN_MATCH) {\n      str = s.strstart - s.insert;\n      s.ins_h = s.window[str];\n\n      /* UPDATE_HASH(s, s->ins_h, s->window[str + 1]); */\n      s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[str + 1]) & s.hash_mask;\n//#if MIN_MATCH != 3\n//        Call update_hash() MIN_MATCH-3 more times\n//#endif\n      while (s.insert) {\n        /* UPDATE_HASH(s, s->ins_h, s->window[str + MIN_MATCH-1]); */\n        s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[str + MIN_MATCH - 1]) & s.hash_mask;\n\n        s.prev[str & s.w_mask] = s.head[s.ins_h];\n        s.head[s.ins_h] = str;\n        str++;\n        s.insert--;\n        if (s.lookahead + s.insert < MIN_MATCH) {\n          break;\n        }\n      }\n    }\n    /* If the whole input has less than MIN_MATCH bytes, ins_h is garbage,\n     * but this is not important since only literal bytes will be emitted.\n     */\n\n  } while (s.lookahead < MIN_LOOKAHEAD && s.strm.avail_in !== 0);\n\n  /* If the WIN_INIT bytes after the end of the current data have never been\n   * written, then zero those bytes in order to avoid memory check reports of\n   * the use of uninitialized (or uninitialised as Julian writes) bytes by\n   * the longest match routines.  Update the high water mark for the next\n   * time through here.  WIN_INIT is set to MAX_MATCH since the longest match\n   * routines allow scanning to strstart + MAX_MATCH, ignoring lookahead.\n   */\n//  if (s.high_water < s.window_size) {\n//    var curr = s.strstart + s.lookahead;\n//    var init = 0;\n//\n//    if (s.high_water < curr) {\n//      /* Previous high water mark below current data -- zero WIN_INIT\n//       * bytes or up to end of window, whichever is less.\n//       */\n//      init = s.window_size - curr;\n//      if (init > WIN_INIT)\n//        init = WIN_INIT;\n//      zmemzero(s->window + curr, (unsigned)init);\n//      s->high_water = curr + init;\n//    }\n//    else if (s->high_water < (ulg)curr + WIN_INIT) {\n//      /* High water mark at or above current data, but below current data\n//       * plus WIN_INIT -- zero out to current data plus WIN_INIT, or up\n//       * to end of window, whichever is less.\n//       */\n//      init = (ulg)curr + WIN_INIT - s->high_water;\n//      if (init > s->window_size - s->high_water)\n//        init = s->window_size - s->high_water;\n//      zmemzero(s->window + s->high_water, (unsigned)init);\n//      s->high_water += init;\n//    }\n//  }\n//\n//  Assert((ulg)s->strstart <= s->window_size - MIN_LOOKAHEAD,\n//    \"not enough room for search\");\n}\n\n/* ===========================================================================\n * Copy without compression as much as possible from the input stream, return\n * the current block state.\n * This function does not insert new strings in the dictionary since\n * uncompressible data is probably not useful. This function is used\n * only for the level=0 compression option.\n * NOTE: this function should be optimized to avoid extra copying from\n * window to pending_buf.\n */\nfunction deflate_stored(s, flush) {\n  /* Stored blocks are limited to 0xffff bytes, pending_buf is limited\n   * to pending_buf_size, and each stored block has a 5 byte header:\n   */\n  var max_block_size = 0xffff;\n\n  if (max_block_size > s.pending_buf_size - 5) {\n    max_block_size = s.pending_buf_size - 5;\n  }\n\n  /* Copy as much as possible from input to output: */\n  for (;;) {\n    /* Fill the window as much as possible: */\n    if (s.lookahead <= 1) {\n\n      //Assert(s->strstart < s->w_size+MAX_DIST(s) ||\n      //  s->block_start >= (long)s->w_size, \"slide too late\");\n//      if (!(s.strstart < s.w_size + (s.w_size - MIN_LOOKAHEAD) ||\n//        s.block_start >= s.w_size)) {\n//        throw  new Error(\"slide too late\");\n//      }\n\n      fill_window(s);\n      if (s.lookahead === 0 && flush === Z_NO_FLUSH) {\n        return BS_NEED_MORE;\n      }\n\n      if (s.lookahead === 0) {\n        break;\n      }\n      /* flush the current block */\n    }\n    //Assert(s->block_start >= 0L, \"block gone\");\n//    if (s.block_start < 0) throw new Error(\"block gone\");\n\n    s.strstart += s.lookahead;\n    s.lookahead = 0;\n\n    /* Emit a stored block if pending_buf will be full: */\n    var max_start = s.block_start + max_block_size;\n\n    if (s.strstart === 0 || s.strstart >= max_start) {\n      /* strstart == 0 is possible when wraparound on 16-bit machine */\n      s.lookahead = s.strstart - max_start;\n      s.strstart = max_start;\n      /*** FLUSH_BLOCK(s, 0); ***/\n      flush_block_only(s, false);\n      if (s.strm.avail_out === 0) {\n        return BS_NEED_MORE;\n      }\n      /***/\n\n\n    }\n    /* Flush if we may have to slide, otherwise block_start may become\n     * negative and the data will be gone:\n     */\n    if (s.strstart - s.block_start >= (s.w_size - MIN_LOOKAHEAD)) {\n      /*** FLUSH_BLOCK(s, 0); ***/\n      flush_block_only(s, false);\n      if (s.strm.avail_out === 0) {\n        return BS_NEED_MORE;\n      }\n      /***/\n    }\n  }\n\n  s.insert = 0;\n\n  if (flush === Z_FINISH) {\n    /*** FLUSH_BLOCK(s, 1); ***/\n    flush_block_only(s, true);\n    if (s.strm.avail_out === 0) {\n      return BS_FINISH_STARTED;\n    }\n    /***/\n    return BS_FINISH_DONE;\n  }\n\n  if (s.strstart > s.block_start) {\n    /*** FLUSH_BLOCK(s, 0); ***/\n    flush_block_only(s, false);\n    if (s.strm.avail_out === 0) {\n      return BS_NEED_MORE;\n    }\n    /***/\n  }\n\n  return BS_NEED_MORE;\n}\n\n/* ===========================================================================\n * Compress as much as possible from the input stream, return the current\n * block state.\n * This function does not perform lazy evaluation of matches and inserts\n * new strings in the dictionary only for unmatched strings or for short\n * matches. It is used only for the fast compression options.\n */\nfunction deflate_fast(s, flush) {\n  var hash_head;        /* head of the hash chain */\n  var bflush;           /* set if current block must be flushed */\n\n  for (;;) {\n    /* Make sure that we always have enough lookahead, except\n     * at the end of the input file. We need MAX_MATCH bytes\n     * for the next match, plus MIN_MATCH bytes to insert the\n     * string following the next match.\n     */\n    if (s.lookahead < MIN_LOOKAHEAD) {\n      fill_window(s);\n      if (s.lookahead < MIN_LOOKAHEAD && flush === Z_NO_FLUSH) {\n        return BS_NEED_MORE;\n      }\n      if (s.lookahead === 0) {\n        break; /* flush the current block */\n      }\n    }\n\n    /* Insert the string window[strstart .. strstart+2] in the\n     * dictionary, and set hash_head to the head of the hash chain:\n     */\n    hash_head = 0/*NIL*/;\n    if (s.lookahead >= MIN_MATCH) {\n      /*** INSERT_STRING(s, s.strstart, hash_head); ***/\n      s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + MIN_MATCH - 1]) & s.hash_mask;\n      hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];\n      s.head[s.ins_h] = s.strstart;\n      /***/\n    }\n\n    /* Find the longest match, discarding those <= prev_length.\n     * At this point we have always match_length < MIN_MATCH\n     */\n    if (hash_head !== 0/*NIL*/ && ((s.strstart - hash_head) <= (s.w_size - MIN_LOOKAHEAD))) {\n      /* To simplify the code, we prevent matches with the string\n       * of window index 0 (in particular we have to avoid a match\n       * of the string with itself at the start of the input file).\n       */\n      s.match_length = longest_match(s, hash_head);\n      /* longest_match() sets match_start */\n    }\n    if (s.match_length >= MIN_MATCH) {\n      // check_match(s, s.strstart, s.match_start, s.match_length); // for debug only\n\n      /*** _tr_tally_dist(s, s.strstart - s.match_start,\n                     s.match_length - MIN_MATCH, bflush); ***/\n      bflush = trees._tr_tally(s, s.strstart - s.match_start, s.match_length - MIN_MATCH);\n\n      s.lookahead -= s.match_length;\n\n      /* Insert new strings in the hash table only if the match length\n       * is not too large. This saves time but degrades compression.\n       */\n      if (s.match_length <= s.max_lazy_match/*max_insert_length*/ && s.lookahead >= MIN_MATCH) {\n        s.match_length--; /* string at strstart already in table */\n        do {\n          s.strstart++;\n          /*** INSERT_STRING(s, s.strstart, hash_head); ***/\n          s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + MIN_MATCH - 1]) & s.hash_mask;\n          hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];\n          s.head[s.ins_h] = s.strstart;\n          /***/\n          /* strstart never exceeds WSIZE-MAX_MATCH, so there are\n           * always MIN_MATCH bytes ahead.\n           */\n        } while (--s.match_length !== 0);\n        s.strstart++;\n      } else\n      {\n        s.strstart += s.match_length;\n        s.match_length = 0;\n        s.ins_h = s.window[s.strstart];\n        /* UPDATE_HASH(s, s.ins_h, s.window[s.strstart+1]); */\n        s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + 1]) & s.hash_mask;\n\n//#if MIN_MATCH != 3\n//                Call UPDATE_HASH() MIN_MATCH-3 more times\n//#endif\n        /* If lookahead < MIN_MATCH, ins_h is garbage, but it does not\n         * matter since it will be recomputed at next deflate call.\n         */\n      }\n    } else {\n      /* No match, output a literal byte */\n      //Tracevv((stderr,\"%c\", s.window[s.strstart]));\n      /*** _tr_tally_lit(s, s.window[s.strstart], bflush); ***/\n      bflush = trees._tr_tally(s, 0, s.window[s.strstart]);\n\n      s.lookahead--;\n      s.strstart++;\n    }\n    if (bflush) {\n      /*** FLUSH_BLOCK(s, 0); ***/\n      flush_block_only(s, false);\n      if (s.strm.avail_out === 0) {\n        return BS_NEED_MORE;\n      }\n      /***/\n    }\n  }\n  s.insert = ((s.strstart < (MIN_MATCH - 1)) ? s.strstart : MIN_MATCH - 1);\n  if (flush === Z_FINISH) {\n    /*** FLUSH_BLOCK(s, 1); ***/\n    flush_block_only(s, true);\n    if (s.strm.avail_out === 0) {\n      return BS_FINISH_STARTED;\n    }\n    /***/\n    return BS_FINISH_DONE;\n  }\n  if (s.last_lit) {\n    /*** FLUSH_BLOCK(s, 0); ***/\n    flush_block_only(s, false);\n    if (s.strm.avail_out === 0) {\n      return BS_NEED_MORE;\n    }\n    /***/\n  }\n  return BS_BLOCK_DONE;\n}\n\n/* ===========================================================================\n * Same as above, but achieves better compression. We use a lazy\n * evaluation for matches: a match is finally adopted only if there is\n * no better match at the next window position.\n */\nfunction deflate_slow(s, flush) {\n  var hash_head;          /* head of hash chain */\n  var bflush;              /* set if current block must be flushed */\n\n  var max_insert;\n\n  /* Process the input block. */\n  for (;;) {\n    /* Make sure that we always have enough lookahead, except\n     * at the end of the input file. We need MAX_MATCH bytes\n     * for the next match, plus MIN_MATCH bytes to insert the\n     * string following the next match.\n     */\n    if (s.lookahead < MIN_LOOKAHEAD) {\n      fill_window(s);\n      if (s.lookahead < MIN_LOOKAHEAD && flush === Z_NO_FLUSH) {\n        return BS_NEED_MORE;\n      }\n      if (s.lookahead === 0) { break; } /* flush the current block */\n    }\n\n    /* Insert the string window[strstart .. strstart+2] in the\n     * dictionary, and set hash_head to the head of the hash chain:\n     */\n    hash_head = 0/*NIL*/;\n    if (s.lookahead >= MIN_MATCH) {\n      /*** INSERT_STRING(s, s.strstart, hash_head); ***/\n      s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + MIN_MATCH - 1]) & s.hash_mask;\n      hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];\n      s.head[s.ins_h] = s.strstart;\n      /***/\n    }\n\n    /* Find the longest match, discarding those <= prev_length.\n     */\n    s.prev_length = s.match_length;\n    s.prev_match = s.match_start;\n    s.match_length = MIN_MATCH - 1;\n\n    if (hash_head !== 0/*NIL*/ && s.prev_length < s.max_lazy_match &&\n        s.strstart - hash_head <= (s.w_size - MIN_LOOKAHEAD)/*MAX_DIST(s)*/) {\n      /* To simplify the code, we prevent matches with the string\n       * of window index 0 (in particular we have to avoid a match\n       * of the string with itself at the start of the input file).\n       */\n      s.match_length = longest_match(s, hash_head);\n      /* longest_match() sets match_start */\n\n      if (s.match_length <= 5 &&\n         (s.strategy === Z_FILTERED || (s.match_length === MIN_MATCH && s.strstart - s.match_start > 4096/*TOO_FAR*/))) {\n\n        /* If prev_match is also MIN_MATCH, match_start is garbage\n         * but we will ignore the current match anyway.\n         */\n        s.match_length = MIN_MATCH - 1;\n      }\n    }\n    /* If there was a match at the previous step and the current\n     * match is not better, output the previous match:\n     */\n    if (s.prev_length >= MIN_MATCH && s.match_length <= s.prev_length) {\n      max_insert = s.strstart + s.lookahead - MIN_MATCH;\n      /* Do not insert strings in hash table beyond this. */\n\n      //check_match(s, s.strstart-1, s.prev_match, s.prev_length);\n\n      /***_tr_tally_dist(s, s.strstart - 1 - s.prev_match,\n                     s.prev_length - MIN_MATCH, bflush);***/\n      bflush = trees._tr_tally(s, s.strstart - 1 - s.prev_match, s.prev_length - MIN_MATCH);\n      /* Insert in hash table all strings up to the end of the match.\n       * strstart-1 and strstart are already inserted. If there is not\n       * enough lookahead, the last two strings are not inserted in\n       * the hash table.\n       */\n      s.lookahead -= s.prev_length - 1;\n      s.prev_length -= 2;\n      do {\n        if (++s.strstart <= max_insert) {\n          /*** INSERT_STRING(s, s.strstart, hash_head); ***/\n          s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + MIN_MATCH - 1]) & s.hash_mask;\n          hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];\n          s.head[s.ins_h] = s.strstart;\n          /***/\n        }\n      } while (--s.prev_length !== 0);\n      s.match_available = 0;\n      s.match_length = MIN_MATCH - 1;\n      s.strstart++;\n\n      if (bflush) {\n        /*** FLUSH_BLOCK(s, 0); ***/\n        flush_block_only(s, false);\n        if (s.strm.avail_out === 0) {\n          return BS_NEED_MORE;\n        }\n        /***/\n      }\n\n    } else if (s.match_available) {\n      /* If there was no match at the previous position, output a\n       * single literal. If there was a match but the current match\n       * is longer, truncate the previous match to a single literal.\n       */\n      //Tracevv((stderr,\"%c\", s->window[s->strstart-1]));\n      /*** _tr_tally_lit(s, s.window[s.strstart-1], bflush); ***/\n      bflush = trees._tr_tally(s, 0, s.window[s.strstart - 1]);\n\n      if (bflush) {\n        /*** FLUSH_BLOCK_ONLY(s, 0) ***/\n        flush_block_only(s, false);\n        /***/\n      }\n      s.strstart++;\n      s.lookahead--;\n      if (s.strm.avail_out === 0) {\n        return BS_NEED_MORE;\n      }\n    } else {\n      /* There is no previous match to compare with, wait for\n       * the next step to decide.\n       */\n      s.match_available = 1;\n      s.strstart++;\n      s.lookahead--;\n    }\n  }\n  //Assert (flush != Z_NO_FLUSH, \"no flush?\");\n  if (s.match_available) {\n    //Tracevv((stderr,\"%c\", s->window[s->strstart-1]));\n    /*** _tr_tally_lit(s, s.window[s.strstart-1], bflush); ***/\n    bflush = trees._tr_tally(s, 0, s.window[s.strstart - 1]);\n\n    s.match_available = 0;\n  }\n  s.insert = s.strstart < MIN_MATCH - 1 ? s.strstart : MIN_MATCH - 1;\n  if (flush === Z_FINISH) {\n    /*** FLUSH_BLOCK(s, 1); ***/\n    flush_block_only(s, true);\n    if (s.strm.avail_out === 0) {\n      return BS_FINISH_STARTED;\n    }\n    /***/\n    return BS_FINISH_DONE;\n  }\n  if (s.last_lit) {\n    /*** FLUSH_BLOCK(s, 0); ***/\n    flush_block_only(s, false);\n    if (s.strm.avail_out === 0) {\n      return BS_NEED_MORE;\n    }\n    /***/\n  }\n\n  return BS_BLOCK_DONE;\n}\n\n\n/* ===========================================================================\n * For Z_RLE, simply look for runs of bytes, generate matches only of distance\n * one.  Do not maintain a hash table.  (It will be regenerated if this run of\n * deflate switches away from Z_RLE.)\n */\nfunction deflate_rle(s, flush) {\n  var bflush;            /* set if current block must be flushed */\n  var prev;              /* byte at distance one to match */\n  var scan, strend;      /* scan goes up to strend for length of run */\n\n  var _win = s.window;\n\n  for (;;) {\n    /* Make sure that we always have enough lookahead, except\n     * at the end of the input file. We need MAX_MATCH bytes\n     * for the longest run, plus one for the unrolled loop.\n     */\n    if (s.lookahead <= MAX_MATCH) {\n      fill_window(s);\n      if (s.lookahead <= MAX_MATCH && flush === Z_NO_FLUSH) {\n        return BS_NEED_MORE;\n      }\n      if (s.lookahead === 0) { break; } /* flush the current block */\n    }\n\n    /* See how many times the previous byte repeats */\n    s.match_length = 0;\n    if (s.lookahead >= MIN_MATCH && s.strstart > 0) {\n      scan = s.strstart - 1;\n      prev = _win[scan];\n      if (prev === _win[++scan] && prev === _win[++scan] && prev === _win[++scan]) {\n        strend = s.strstart + MAX_MATCH;\n        do {\n          // Do nothing\n        } while (prev === _win[++scan] && prev === _win[++scan] &&\n                 prev === _win[++scan] && prev === _win[++scan] &&\n                 prev === _win[++scan] && prev === _win[++scan] &&\n                 prev === _win[++scan] && prev === _win[++scan] &&\n                 scan < strend);\n        s.match_length = MAX_MATCH - (strend - scan);\n        if (s.match_length > s.lookahead) {\n          s.match_length = s.lookahead;\n        }\n      }\n      //Assert(scan <= s->window+(uInt)(s->window_size-1), \"wild scan\");\n    }\n\n    /* Emit match if have run of MIN_MATCH or longer, else emit literal */\n    if (s.match_length >= MIN_MATCH) {\n      //check_match(s, s.strstart, s.strstart - 1, s.match_length);\n\n      /*** _tr_tally_dist(s, 1, s.match_length - MIN_MATCH, bflush); ***/\n      bflush = trees._tr_tally(s, 1, s.match_length - MIN_MATCH);\n\n      s.lookahead -= s.match_length;\n      s.strstart += s.match_length;\n      s.match_length = 0;\n    } else {\n      /* No match, output a literal byte */\n      //Tracevv((stderr,\"%c\", s->window[s->strstart]));\n      /*** _tr_tally_lit(s, s.window[s.strstart], bflush); ***/\n      bflush = trees._tr_tally(s, 0, s.window[s.strstart]);\n\n      s.lookahead--;\n      s.strstart++;\n    }\n    if (bflush) {\n      /*** FLUSH_BLOCK(s, 0); ***/\n      flush_block_only(s, false);\n      if (s.strm.avail_out === 0) {\n        return BS_NEED_MORE;\n      }\n      /***/\n    }\n  }\n  s.insert = 0;\n  if (flush === Z_FINISH) {\n    /*** FLUSH_BLOCK(s, 1); ***/\n    flush_block_only(s, true);\n    if (s.strm.avail_out === 0) {\n      return BS_FINISH_STARTED;\n    }\n    /***/\n    return BS_FINISH_DONE;\n  }\n  if (s.last_lit) {\n    /*** FLUSH_BLOCK(s, 0); ***/\n    flush_block_only(s, false);\n    if (s.strm.avail_out === 0) {\n      return BS_NEED_MORE;\n    }\n    /***/\n  }\n  return BS_BLOCK_DONE;\n}\n\n/* ===========================================================================\n * For Z_HUFFMAN_ONLY, do not look for matches.  Do not maintain a hash table.\n * (It will be regenerated if this run of deflate switches away from Huffman.)\n */\nfunction deflate_huff(s, flush) {\n  var bflush;             /* set if current block must be flushed */\n\n  for (;;) {\n    /* Make sure that we have a literal to write. */\n    if (s.lookahead === 0) {\n      fill_window(s);\n      if (s.lookahead === 0) {\n        if (flush === Z_NO_FLUSH) {\n          return BS_NEED_MORE;\n        }\n        break;      /* flush the current block */\n      }\n    }\n\n    /* Output a literal byte */\n    s.match_length = 0;\n    //Tracevv((stderr,\"%c\", s->window[s->strstart]));\n    /*** _tr_tally_lit(s, s.window[s.strstart], bflush); ***/\n    bflush = trees._tr_tally(s, 0, s.window[s.strstart]);\n    s.lookahead--;\n    s.strstart++;\n    if (bflush) {\n      /*** FLUSH_BLOCK(s, 0); ***/\n      flush_block_only(s, false);\n      if (s.strm.avail_out === 0) {\n        return BS_NEED_MORE;\n      }\n      /***/\n    }\n  }\n  s.insert = 0;\n  if (flush === Z_FINISH) {\n    /*** FLUSH_BLOCK(s, 1); ***/\n    flush_block_only(s, true);\n    if (s.strm.avail_out === 0) {\n      return BS_FINISH_STARTED;\n    }\n    /***/\n    return BS_FINISH_DONE;\n  }\n  if (s.last_lit) {\n    /*** FLUSH_BLOCK(s, 0); ***/\n    flush_block_only(s, false);\n    if (s.strm.avail_out === 0) {\n      return BS_NEED_MORE;\n    }\n    /***/\n  }\n  return BS_BLOCK_DONE;\n}\n\n/* Values for max_lazy_match, good_match and max_chain_length, depending on\n * the desired pack level (0..9). The values given below have been tuned to\n * exclude worst case performance for pathological files. Better values may be\n * found for specific files.\n */\nfunction Config(good_length, max_lazy, nice_length, max_chain, func) {\n  this.good_length = good_length;\n  this.max_lazy = max_lazy;\n  this.nice_length = nice_length;\n  this.max_chain = max_chain;\n  this.func = func;\n}\n\nvar configuration_table;\n\nconfiguration_table = [\n  /*      good lazy nice chain */\n  new Config(0, 0, 0, 0, deflate_stored),          /* 0 store only */\n  new Config(4, 4, 8, 4, deflate_fast),            /* 1 max speed, no lazy matches */\n  new Config(4, 5, 16, 8, deflate_fast),           /* 2 */\n  new Config(4, 6, 32, 32, deflate_fast),          /* 3 */\n\n  new Config(4, 4, 16, 16, deflate_slow),          /* 4 lazy matches */\n  new Config(8, 16, 32, 32, deflate_slow),         /* 5 */\n  new Config(8, 16, 128, 128, deflate_slow),       /* 6 */\n  new Config(8, 32, 128, 256, deflate_slow),       /* 7 */\n  new Config(32, 128, 258, 1024, deflate_slow),    /* 8 */\n  new Config(32, 258, 258, 4096, deflate_slow)     /* 9 max compression */\n];\n\n\n/* ===========================================================================\n * Initialize the \"longest match\" routines for a new zlib stream\n */\nfunction lm_init(s) {\n  s.window_size = 2 * s.w_size;\n\n  /*** CLEAR_HASH(s); ***/\n  zero(s.head); // Fill with NIL (= 0);\n\n  /* Set the default configuration parameters:\n   */\n  s.max_lazy_match = configuration_table[s.level].max_lazy;\n  s.good_match = configuration_table[s.level].good_length;\n  s.nice_match = configuration_table[s.level].nice_length;\n  s.max_chain_length = configuration_table[s.level].max_chain;\n\n  s.strstart = 0;\n  s.block_start = 0;\n  s.lookahead = 0;\n  s.insert = 0;\n  s.match_length = s.prev_length = MIN_MATCH - 1;\n  s.match_available = 0;\n  s.ins_h = 0;\n}\n\n\nfunction DeflateState() {\n  this.strm = null;            /* pointer back to this zlib stream */\n  this.status = 0;            /* as the name implies */\n  this.pending_buf = null;      /* output still pending */\n  this.pending_buf_size = 0;  /* size of pending_buf */\n  this.pending_out = 0;       /* next pending byte to output to the stream */\n  this.pending = 0;           /* nb of bytes in the pending buffer */\n  this.wrap = 0;              /* bit 0 true for zlib, bit 1 true for gzip */\n  this.gzhead = null;         /* gzip header information to write */\n  this.gzindex = 0;           /* where in extra, name, or comment */\n  this.method = Z_DEFLATED; /* can only be DEFLATED */\n  this.last_flush = -1;   /* value of flush param for previous deflate call */\n\n  this.w_size = 0;  /* LZ77 window size (32K by default) */\n  this.w_bits = 0;  /* log2(w_size)  (8..16) */\n  this.w_mask = 0;  /* w_size - 1 */\n\n  this.window = null;\n  /* Sliding window. Input bytes are read into the second half of the window,\n   * and move to the first half later to keep a dictionary of at least wSize\n   * bytes. With this organization, matches are limited to a distance of\n   * wSize-MAX_MATCH bytes, but this ensures that IO is always\n   * performed with a length multiple of the block size.\n   */\n\n  this.window_size = 0;\n  /* Actual size of window: 2*wSize, except when the user input buffer\n   * is directly used as sliding window.\n   */\n\n  this.prev = null;\n  /* Link to older string with same hash index. To limit the size of this\n   * array to 64K, this link is maintained only for the last 32K strings.\n   * An index in this array is thus a window index modulo 32K.\n   */\n\n  this.head = null;   /* Heads of the hash chains or NIL. */\n\n  this.ins_h = 0;       /* hash index of string to be inserted */\n  this.hash_size = 0;   /* number of elements in hash table */\n  this.hash_bits = 0;   /* log2(hash_size) */\n  this.hash_mask = 0;   /* hash_size-1 */\n\n  this.hash_shift = 0;\n  /* Number of bits by which ins_h must be shifted at each input\n   * step. It must be such that after MIN_MATCH steps, the oldest\n   * byte no longer takes part in the hash key, that is:\n   *   hash_shift * MIN_MATCH >= hash_bits\n   */\n\n  this.block_start = 0;\n  /* Window position at the beginning of the current output block. Gets\n   * negative when the window is moved backwards.\n   */\n\n  this.match_length = 0;      /* length of best match */\n  this.prev_match = 0;        /* previous match */\n  this.match_available = 0;   /* set if previous match exists */\n  this.strstart = 0;          /* start of string to insert */\n  this.match_start = 0;       /* start of matching string */\n  this.lookahead = 0;         /* number of valid bytes ahead in window */\n\n  this.prev_length = 0;\n  /* Length of the best match at previous step. Matches not greater than this\n   * are discarded. This is used in the lazy match evaluation.\n   */\n\n  this.max_chain_length = 0;\n  /* To speed up deflation, hash chains are never searched beyond this\n   * length.  A higher limit improves compression ratio but degrades the\n   * speed.\n   */\n\n  this.max_lazy_match = 0;\n  /* Attempt to find a better match only when the current match is strictly\n   * smaller than this value. This mechanism is used only for compression\n   * levels >= 4.\n   */\n  // That's alias to max_lazy_match, don't use directly\n  //this.max_insert_length = 0;\n  /* Insert new strings in the hash table only if the match length is not\n   * greater than this length. This saves time but degrades compression.\n   * max_insert_length is used only for compression levels <= 3.\n   */\n\n  this.level = 0;     /* compression level (1..9) */\n  this.strategy = 0;  /* favor or force Huffman coding*/\n\n  this.good_match = 0;\n  /* Use a faster search when the previous match is longer than this */\n\n  this.nice_match = 0; /* Stop searching when current match exceeds this */\n\n              /* used by trees.c: */\n\n  /* Didn't use ct_data typedef below to suppress compiler warning */\n\n  // struct ct_data_s dyn_ltree[HEAP_SIZE];   /* literal and length tree */\n  // struct ct_data_s dyn_dtree[2*D_CODES+1]; /* distance tree */\n  // struct ct_data_s bl_tree[2*BL_CODES+1];  /* Huffman tree for bit lengths */\n\n  // Use flat array of DOUBLE size, with interleaved fata,\n  // because JS does not support effective\n  this.dyn_ltree  = new utils.Buf16(HEAP_SIZE * 2);\n  this.dyn_dtree  = new utils.Buf16((2 * D_CODES + 1) * 2);\n  this.bl_tree    = new utils.Buf16((2 * BL_CODES + 1) * 2);\n  zero(this.dyn_ltree);\n  zero(this.dyn_dtree);\n  zero(this.bl_tree);\n\n  this.l_desc   = null;         /* desc. for literal tree */\n  this.d_desc   = null;         /* desc. for distance tree */\n  this.bl_desc  = null;         /* desc. for bit length tree */\n\n  //ush bl_count[MAX_BITS+1];\n  this.bl_count = new utils.Buf16(MAX_BITS + 1);\n  /* number of codes at each bit length for an optimal tree */\n\n  //int heap[2*L_CODES+1];      /* heap used to build the Huffman trees */\n  this.heap = new utils.Buf16(2 * L_CODES + 1);  /* heap used to build the Huffman trees */\n  zero(this.heap);\n\n  this.heap_len = 0;               /* number of elements in the heap */\n  this.heap_max = 0;               /* element of largest frequency */\n  /* The sons of heap[n] are heap[2*n] and heap[2*n+1]. heap[0] is not used.\n   * The same heap array is used to build all trees.\n   */\n\n  this.depth = new utils.Buf16(2 * L_CODES + 1); //uch depth[2*L_CODES+1];\n  zero(this.depth);\n  /* Depth of each subtree used as tie breaker for trees of equal frequency\n   */\n\n  this.l_buf = 0;          /* buffer index for literals or lengths */\n\n  this.lit_bufsize = 0;\n  /* Size of match buffer for literals/lengths.  There are 4 reasons for\n   * limiting lit_bufsize to 64K:\n   *   - frequencies can be kept in 16 bit counters\n   *   - if compression is not successful for the first block, all input\n   *     data is still in the window so we can still emit a stored block even\n   *     when input comes from standard input.  (This can also be done for\n   *     all blocks if lit_bufsize is not greater than 32K.)\n   *   - if compression is not successful for a file smaller than 64K, we can\n   *     even emit a stored file instead of a stored block (saving 5 bytes).\n   *     This is applicable only for zip (not gzip or zlib).\n   *   - creating new Huffman trees less frequently may not provide fast\n   *     adaptation to changes in the input data statistics. (Take for\n   *     example a binary file with poorly compressible code followed by\n   *     a highly compressible string table.) Smaller buffer sizes give\n   *     fast adaptation but have of course the overhead of transmitting\n   *     trees more frequently.\n   *   - I can't count above 4\n   */\n\n  this.last_lit = 0;      /* running index in l_buf */\n\n  this.d_buf = 0;\n  /* Buffer index for distances. To simplify the code, d_buf and l_buf have\n   * the same number of elements. To use different lengths, an extra flag\n   * array would be necessary.\n   */\n\n  this.opt_len = 0;       /* bit length of current block with optimal trees */\n  this.static_len = 0;    /* bit length of current block with static trees */\n  this.matches = 0;       /* number of string matches in current block */\n  this.insert = 0;        /* bytes at end of window left to insert */\n\n\n  this.bi_buf = 0;\n  /* Output buffer. bits are inserted starting at the bottom (least\n   * significant bits).\n   */\n  this.bi_valid = 0;\n  /* Number of valid bits in bi_buf.  All bits above the last valid bit\n   * are always zero.\n   */\n\n  // Used for window memory init. We safely ignore it for JS. That makes\n  // sense only for pointers and memory check tools.\n  //this.high_water = 0;\n  /* High water mark offset in window for initialized bytes -- bytes above\n   * this are set to zero in order to avoid memory check warnings when\n   * longest match routines access bytes past the input.  This is then\n   * updated to the new high water mark.\n   */\n}\n\n\nfunction deflateResetKeep(strm) {\n  var s;\n\n  if (!strm || !strm.state) {\n    return err(strm, Z_STREAM_ERROR);\n  }\n\n  strm.total_in = strm.total_out = 0;\n  strm.data_type = Z_UNKNOWN;\n\n  s = strm.state;\n  s.pending = 0;\n  s.pending_out = 0;\n\n  if (s.wrap < 0) {\n    s.wrap = -s.wrap;\n    /* was made negative by deflate(..., Z_FINISH); */\n  }\n  s.status = (s.wrap ? INIT_STATE : BUSY_STATE);\n  strm.adler = (s.wrap === 2) ?\n    0  // crc32(0, Z_NULL, 0)\n  :\n    1; // adler32(0, Z_NULL, 0)\n  s.last_flush = Z_NO_FLUSH;\n  trees._tr_init(s);\n  return Z_OK;\n}\n\n\nfunction deflateReset(strm) {\n  var ret = deflateResetKeep(strm);\n  if (ret === Z_OK) {\n    lm_init(strm.state);\n  }\n  return ret;\n}\n\n\nfunction deflateSetHeader(strm, head) {\n  if (!strm || !strm.state) { return Z_STREAM_ERROR; }\n  if (strm.state.wrap !== 2) { return Z_STREAM_ERROR; }\n  strm.state.gzhead = head;\n  return Z_OK;\n}\n\n\nfunction deflateInit2(strm, level, method, windowBits, memLevel, strategy) {\n  if (!strm) { // === Z_NULL\n    return Z_STREAM_ERROR;\n  }\n  var wrap = 1;\n\n  if (level === Z_DEFAULT_COMPRESSION) {\n    level = 6;\n  }\n\n  if (windowBits < 0) { /* suppress zlib wrapper */\n    wrap = 0;\n    windowBits = -windowBits;\n  }\n\n  else if (windowBits > 15) {\n    wrap = 2;           /* write gzip wrapper instead */\n    windowBits -= 16;\n  }\n\n\n  if (memLevel < 1 || memLevel > MAX_MEM_LEVEL || method !== Z_DEFLATED ||\n    windowBits < 8 || windowBits > 15 || level < 0 || level > 9 ||\n    strategy < 0 || strategy > Z_FIXED) {\n    return err(strm, Z_STREAM_ERROR);\n  }\n\n\n  if (windowBits === 8) {\n    windowBits = 9;\n  }\n  /* until 256-byte window bug fixed */\n\n  var s = new DeflateState();\n\n  strm.state = s;\n  s.strm = strm;\n\n  s.wrap = wrap;\n  s.gzhead = null;\n  s.w_bits = windowBits;\n  s.w_size = 1 << s.w_bits;\n  s.w_mask = s.w_size - 1;\n\n  s.hash_bits = memLevel + 7;\n  s.hash_size = 1 << s.hash_bits;\n  s.hash_mask = s.hash_size - 1;\n  s.hash_shift = ~~((s.hash_bits + MIN_MATCH - 1) / MIN_MATCH);\n\n  s.window = new utils.Buf8(s.w_size * 2);\n  s.head = new utils.Buf16(s.hash_size);\n  s.prev = new utils.Buf16(s.w_size);\n\n  // Don't need mem init magic for JS.\n  //s.high_water = 0;  /* nothing written to s->window yet */\n\n  s.lit_bufsize = 1 << (memLevel + 6); /* 16K elements by default */\n\n  s.pending_buf_size = s.lit_bufsize * 4;\n\n  //overlay = (ushf *) ZALLOC(strm, s->lit_bufsize, sizeof(ush)+2);\n  //s->pending_buf = (uchf *) overlay;\n  s.pending_buf = new utils.Buf8(s.pending_buf_size);\n\n  // It is offset from `s.pending_buf` (size is `s.lit_bufsize * 2`)\n  //s->d_buf = overlay + s->lit_bufsize/sizeof(ush);\n  s.d_buf = 1 * s.lit_bufsize;\n\n  //s->l_buf = s->pending_buf + (1+sizeof(ush))*s->lit_bufsize;\n  s.l_buf = (1 + 2) * s.lit_bufsize;\n\n  s.level = level;\n  s.strategy = strategy;\n  s.method = method;\n\n  return deflateReset(strm);\n}\n\nfunction deflateInit(strm, level) {\n  return deflateInit2(strm, level, Z_DEFLATED, MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY);\n}\n\n\nfunction deflate(strm, flush) {\n  var old_flush, s;\n  var beg, val; // for gzip header write only\n\n  if (!strm || !strm.state ||\n    flush > Z_BLOCK || flush < 0) {\n    return strm ? err(strm, Z_STREAM_ERROR) : Z_STREAM_ERROR;\n  }\n\n  s = strm.state;\n\n  if (!strm.output ||\n      (!strm.input && strm.avail_in !== 0) ||\n      (s.status === FINISH_STATE && flush !== Z_FINISH)) {\n    return err(strm, (strm.avail_out === 0) ? Z_BUF_ERROR : Z_STREAM_ERROR);\n  }\n\n  s.strm = strm; /* just in case */\n  old_flush = s.last_flush;\n  s.last_flush = flush;\n\n  /* Write the header */\n  if (s.status === INIT_STATE) {\n\n    if (s.wrap === 2) { // GZIP header\n      strm.adler = 0;  //crc32(0L, Z_NULL, 0);\n      put_byte(s, 31);\n      put_byte(s, 139);\n      put_byte(s, 8);\n      if (!s.gzhead) { // s->gzhead == Z_NULL\n        put_byte(s, 0);\n        put_byte(s, 0);\n        put_byte(s, 0);\n        put_byte(s, 0);\n        put_byte(s, 0);\n        put_byte(s, s.level === 9 ? 2 :\n                    (s.strategy >= Z_HUFFMAN_ONLY || s.level < 2 ?\n                     4 : 0));\n        put_byte(s, OS_CODE);\n        s.status = BUSY_STATE;\n      }\n      else {\n        put_byte(s, (s.gzhead.text ? 1 : 0) +\n                    (s.gzhead.hcrc ? 2 : 0) +\n                    (!s.gzhead.extra ? 0 : 4) +\n                    (!s.gzhead.name ? 0 : 8) +\n                    (!s.gzhead.comment ? 0 : 16)\n                );\n        put_byte(s, s.gzhead.time & 0xff);\n        put_byte(s, (s.gzhead.time >> 8) & 0xff);\n        put_byte(s, (s.gzhead.time >> 16) & 0xff);\n        put_byte(s, (s.gzhead.time >> 24) & 0xff);\n        put_byte(s, s.level === 9 ? 2 :\n                    (s.strategy >= Z_HUFFMAN_ONLY || s.level < 2 ?\n                     4 : 0));\n        put_byte(s, s.gzhead.os & 0xff);\n        if (s.gzhead.extra && s.gzhead.extra.length) {\n          put_byte(s, s.gzhead.extra.length & 0xff);\n          put_byte(s, (s.gzhead.extra.length >> 8) & 0xff);\n        }\n        if (s.gzhead.hcrc) {\n          strm.adler = crc32(strm.adler, s.pending_buf, s.pending, 0);\n        }\n        s.gzindex = 0;\n        s.status = EXTRA_STATE;\n      }\n    }\n    else // DEFLATE header\n    {\n      var header = (Z_DEFLATED + ((s.w_bits - 8) << 4)) << 8;\n      var level_flags = -1;\n\n      if (s.strategy >= Z_HUFFMAN_ONLY || s.level < 2) {\n        level_flags = 0;\n      } else if (s.level < 6) {\n        level_flags = 1;\n      } else if (s.level === 6) {\n        level_flags = 2;\n      } else {\n        level_flags = 3;\n      }\n      header |= (level_flags << 6);\n      if (s.strstart !== 0) { header |= PRESET_DICT; }\n      header += 31 - (header % 31);\n\n      s.status = BUSY_STATE;\n      putShortMSB(s, header);\n\n      /* Save the adler32 of the preset dictionary: */\n      if (s.strstart !== 0) {\n        putShortMSB(s, strm.adler >>> 16);\n        putShortMSB(s, strm.adler & 0xffff);\n      }\n      strm.adler = 1; // adler32(0L, Z_NULL, 0);\n    }\n  }\n\n//#ifdef GZIP\n  if (s.status === EXTRA_STATE) {\n    if (s.gzhead.extra/* != Z_NULL*/) {\n      beg = s.pending;  /* start of bytes to update crc */\n\n      while (s.gzindex < (s.gzhead.extra.length & 0xffff)) {\n        if (s.pending === s.pending_buf_size) {\n          if (s.gzhead.hcrc && s.pending > beg) {\n            strm.adler = crc32(strm.adler, s.pending_buf, s.pending - beg, beg);\n          }\n          flush_pending(strm);\n          beg = s.pending;\n          if (s.pending === s.pending_buf_size) {\n            break;\n          }\n        }\n        put_byte(s, s.gzhead.extra[s.gzindex] & 0xff);\n        s.gzindex++;\n      }\n      if (s.gzhead.hcrc && s.pending > beg) {\n        strm.adler = crc32(strm.adler, s.pending_buf, s.pending - beg, beg);\n      }\n      if (s.gzindex === s.gzhead.extra.length) {\n        s.gzindex = 0;\n        s.status = NAME_STATE;\n      }\n    }\n    else {\n      s.status = NAME_STATE;\n    }\n  }\n  if (s.status === NAME_STATE) {\n    if (s.gzhead.name/* != Z_NULL*/) {\n      beg = s.pending;  /* start of bytes to update crc */\n      //int val;\n\n      do {\n        if (s.pending === s.pending_buf_size) {\n          if (s.gzhead.hcrc && s.pending > beg) {\n            strm.adler = crc32(strm.adler, s.pending_buf, s.pending - beg, beg);\n          }\n          flush_pending(strm);\n          beg = s.pending;\n          if (s.pending === s.pending_buf_size) {\n            val = 1;\n            break;\n          }\n        }\n        // JS specific: little magic to add zero terminator to end of string\n        if (s.gzindex < s.gzhead.name.length) {\n          val = s.gzhead.name.charCodeAt(s.gzindex++) & 0xff;\n        } else {\n          val = 0;\n        }\n        put_byte(s, val);\n      } while (val !== 0);\n\n      if (s.gzhead.hcrc && s.pending > beg) {\n        strm.adler = crc32(strm.adler, s.pending_buf, s.pending - beg, beg);\n      }\n      if (val === 0) {\n        s.gzindex = 0;\n        s.status = COMMENT_STATE;\n      }\n    }\n    else {\n      s.status = COMMENT_STATE;\n    }\n  }\n  if (s.status === COMMENT_STATE) {\n    if (s.gzhead.comment/* != Z_NULL*/) {\n      beg = s.pending;  /* start of bytes to update crc */\n      //int val;\n\n      do {\n        if (s.pending === s.pending_buf_size) {\n          if (s.gzhead.hcrc && s.pending > beg) {\n            strm.adler = crc32(strm.adler, s.pending_buf, s.pending - beg, beg);\n          }\n          flush_pending(strm);\n          beg = s.pending;\n          if (s.pending === s.pending_buf_size) {\n            val = 1;\n            break;\n          }\n        }\n        // JS specific: little magic to add zero terminator to end of string\n        if (s.gzindex < s.gzhead.comment.length) {\n          val = s.gzhead.comment.charCodeAt(s.gzindex++) & 0xff;\n        } else {\n          val = 0;\n        }\n        put_byte(s, val);\n      } while (val !== 0);\n\n      if (s.gzhead.hcrc && s.pending > beg) {\n        strm.adler = crc32(strm.adler, s.pending_buf, s.pending - beg, beg);\n      }\n      if (val === 0) {\n        s.status = HCRC_STATE;\n      }\n    }\n    else {\n      s.status = HCRC_STATE;\n    }\n  }\n  if (s.status === HCRC_STATE) {\n    if (s.gzhead.hcrc) {\n      if (s.pending + 2 > s.pending_buf_size) {\n        flush_pending(strm);\n      }\n      if (s.pending + 2 <= s.pending_buf_size) {\n        put_byte(s, strm.adler & 0xff);\n        put_byte(s, (strm.adler >> 8) & 0xff);\n        strm.adler = 0; //crc32(0L, Z_NULL, 0);\n        s.status = BUSY_STATE;\n      }\n    }\n    else {\n      s.status = BUSY_STATE;\n    }\n  }\n//#endif\n\n  /* Flush as much pending output as possible */\n  if (s.pending !== 0) {\n    flush_pending(strm);\n    if (strm.avail_out === 0) {\n      /* Since avail_out is 0, deflate will be called again with\n       * more output space, but possibly with both pending and\n       * avail_in equal to zero. There won't be anything to do,\n       * but this is not an error situation so make sure we\n       * return OK instead of BUF_ERROR at next call of deflate:\n       */\n      s.last_flush = -1;\n      return Z_OK;\n    }\n\n    /* Make sure there is something to do and avoid duplicate consecutive\n     * flushes. For repeated and useless calls with Z_FINISH, we keep\n     * returning Z_STREAM_END instead of Z_BUF_ERROR.\n     */\n  } else if (strm.avail_in === 0 && rank(flush) <= rank(old_flush) &&\n    flush !== Z_FINISH) {\n    return err(strm, Z_BUF_ERROR);\n  }\n\n  /* User must not provide more input after the first FINISH: */\n  if (s.status === FINISH_STATE && strm.avail_in !== 0) {\n    return err(strm, Z_BUF_ERROR);\n  }\n\n  /* Start a new block or continue the current one.\n   */\n  if (strm.avail_in !== 0 || s.lookahead !== 0 ||\n    (flush !== Z_NO_FLUSH && s.status !== FINISH_STATE)) {\n    var bstate = (s.strategy === Z_HUFFMAN_ONLY) ? deflate_huff(s, flush) :\n      (s.strategy === Z_RLE ? deflate_rle(s, flush) :\n        configuration_table[s.level].func(s, flush));\n\n    if (bstate === BS_FINISH_STARTED || bstate === BS_FINISH_DONE) {\n      s.status = FINISH_STATE;\n    }\n    if (bstate === BS_NEED_MORE || bstate === BS_FINISH_STARTED) {\n      if (strm.avail_out === 0) {\n        s.last_flush = -1;\n        /* avoid BUF_ERROR next call, see above */\n      }\n      return Z_OK;\n      /* If flush != Z_NO_FLUSH && avail_out == 0, the next call\n       * of deflate should use the same flush parameter to make sure\n       * that the flush is complete. So we don't have to output an\n       * empty block here, this will be done at next call. This also\n       * ensures that for a very small output buffer, we emit at most\n       * one empty block.\n       */\n    }\n    if (bstate === BS_BLOCK_DONE) {\n      if (flush === Z_PARTIAL_FLUSH) {\n        trees._tr_align(s);\n      }\n      else if (flush !== Z_BLOCK) { /* FULL_FLUSH or SYNC_FLUSH */\n\n        trees._tr_stored_block(s, 0, 0, false);\n        /* For a full flush, this empty block will be recognized\n         * as a special marker by inflate_sync().\n         */\n        if (flush === Z_FULL_FLUSH) {\n          /*** CLEAR_HASH(s); ***/             /* forget history */\n          zero(s.head); // Fill with NIL (= 0);\n\n          if (s.lookahead === 0) {\n            s.strstart = 0;\n            s.block_start = 0;\n            s.insert = 0;\n          }\n        }\n      }\n      flush_pending(strm);\n      if (strm.avail_out === 0) {\n        s.last_flush = -1; /* avoid BUF_ERROR at next call, see above */\n        return Z_OK;\n      }\n    }\n  }\n  //Assert(strm->avail_out > 0, \"bug2\");\n  //if (strm.avail_out <= 0) { throw new Error(\"bug2\");}\n\n  if (flush !== Z_FINISH) { return Z_OK; }\n  if (s.wrap <= 0) { return Z_STREAM_END; }\n\n  /* Write the trailer */\n  if (s.wrap === 2) {\n    put_byte(s, strm.adler & 0xff);\n    put_byte(s, (strm.adler >> 8) & 0xff);\n    put_byte(s, (strm.adler >> 16) & 0xff);\n    put_byte(s, (strm.adler >> 24) & 0xff);\n    put_byte(s, strm.total_in & 0xff);\n    put_byte(s, (strm.total_in >> 8) & 0xff);\n    put_byte(s, (strm.total_in >> 16) & 0xff);\n    put_byte(s, (strm.total_in >> 24) & 0xff);\n  }\n  else\n  {\n    putShortMSB(s, strm.adler >>> 16);\n    putShortMSB(s, strm.adler & 0xffff);\n  }\n\n  flush_pending(strm);\n  /* If avail_out is zero, the application will call deflate again\n   * to flush the rest.\n   */\n  if (s.wrap > 0) { s.wrap = -s.wrap; }\n  /* write the trailer only once! */\n  return s.pending !== 0 ? Z_OK : Z_STREAM_END;\n}\n\nfunction deflateEnd(strm) {\n  var status;\n\n  if (!strm/*== Z_NULL*/ || !strm.state/*== Z_NULL*/) {\n    return Z_STREAM_ERROR;\n  }\n\n  status = strm.state.status;\n  if (status !== INIT_STATE &&\n    status !== EXTRA_STATE &&\n    status !== NAME_STATE &&\n    status !== COMMENT_STATE &&\n    status !== HCRC_STATE &&\n    status !== BUSY_STATE &&\n    status !== FINISH_STATE\n  ) {\n    return err(strm, Z_STREAM_ERROR);\n  }\n\n  strm.state = null;\n\n  return status === BUSY_STATE ? err(strm, Z_DATA_ERROR) : Z_OK;\n}\n\n\n/* =========================================================================\n * Initializes the compression dictionary from the given byte\n * sequence without producing any compressed output.\n */\nfunction deflateSetDictionary(strm, dictionary) {\n  var dictLength = dictionary.length;\n\n  var s;\n  var str, n;\n  var wrap;\n  var avail;\n  var next;\n  var input;\n  var tmpDict;\n\n  if (!strm/*== Z_NULL*/ || !strm.state/*== Z_NULL*/) {\n    return Z_STREAM_ERROR;\n  }\n\n  s = strm.state;\n  wrap = s.wrap;\n\n  if (wrap === 2 || (wrap === 1 && s.status !== INIT_STATE) || s.lookahead) {\n    return Z_STREAM_ERROR;\n  }\n\n  /* when using zlib wrappers, compute Adler-32 for provided dictionary */\n  if (wrap === 1) {\n    /* adler32(strm->adler, dictionary, dictLength); */\n    strm.adler = adler32(strm.adler, dictionary, dictLength, 0);\n  }\n\n  s.wrap = 0;   /* avoid computing Adler-32 in read_buf */\n\n  /* if dictionary would fill window, just replace the history */\n  if (dictLength >= s.w_size) {\n    if (wrap === 0) {            /* already empty otherwise */\n      /*** CLEAR_HASH(s); ***/\n      zero(s.head); // Fill with NIL (= 0);\n      s.strstart = 0;\n      s.block_start = 0;\n      s.insert = 0;\n    }\n    /* use the tail */\n    // dictionary = dictionary.slice(dictLength - s.w_size);\n    tmpDict = new utils.Buf8(s.w_size);\n    utils.arraySet(tmpDict, dictionary, dictLength - s.w_size, s.w_size, 0);\n    dictionary = tmpDict;\n    dictLength = s.w_size;\n  }\n  /* insert dictionary into window and hash */\n  avail = strm.avail_in;\n  next = strm.next_in;\n  input = strm.input;\n  strm.avail_in = dictLength;\n  strm.next_in = 0;\n  strm.input = dictionary;\n  fill_window(s);\n  while (s.lookahead >= MIN_MATCH) {\n    str = s.strstart;\n    n = s.lookahead - (MIN_MATCH - 1);\n    do {\n      /* UPDATE_HASH(s, s->ins_h, s->window[str + MIN_MATCH-1]); */\n      s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[str + MIN_MATCH - 1]) & s.hash_mask;\n\n      s.prev[str & s.w_mask] = s.head[s.ins_h];\n\n      s.head[s.ins_h] = str;\n      str++;\n    } while (--n);\n    s.strstart = str;\n    s.lookahead = MIN_MATCH - 1;\n    fill_window(s);\n  }\n  s.strstart += s.lookahead;\n  s.block_start = s.strstart;\n  s.insert = s.lookahead;\n  s.lookahead = 0;\n  s.match_length = s.prev_length = MIN_MATCH - 1;\n  s.match_available = 0;\n  strm.next_in = next;\n  strm.input = input;\n  strm.avail_in = avail;\n  s.wrap = wrap;\n  return Z_OK;\n}\n\n\nexport { deflateInit, deflateInit2, deflateReset, deflateResetKeep, deflateSetHeader, deflate, deflateEnd, deflateSetDictionary };\nexport var deflateInfo = 'pako deflate (from Nodeca project)';\n\n/* Not implemented\nexports.deflateBound = deflateBound;\nexports.deflateCopy = deflateCopy;\nexports.deflateParams = deflateParams;\nexports.deflatePending = deflatePending;\nexports.deflatePrime = deflatePrime;\nexports.deflateTune = deflateTune;\n*/\n"
  },
  {
    "path": "vendor/pako/lib/zlib/gzheader.js",
    "content": "export default function GZheader() {\n  /* true if compressed data believed to be text */\n  this.text       = 0;\n  /* modification time */\n  this.time       = 0;\n  /* extra flags (not used when writing a gzip file) */\n  this.xflags     = 0;\n  /* operating system */\n  this.os         = 0;\n  /* pointer to extra field or Z_NULL if none */\n  this.extra      = null;\n  /* extra field length (valid if extra != Z_NULL) */\n  this.extra_len  = 0; // Actually, we don't need it in JS,\n                       // but leave for few code modifications\n\n  //\n  // Setup limits is not necessary because in js we should not preallocate memory\n  // for inflate use constant limit in 65536 bytes\n  //\n\n  /* space at extra (only when reading header) */\n  // this.extra_max  = 0;\n  /* pointer to zero-terminated file name or Z_NULL */\n  this.name       = '';\n  /* space at name (only when reading header) */\n  // this.name_max   = 0;\n  /* pointer to zero-terminated comment or Z_NULL */\n  this.comment    = '';\n  /* space at comment (only when reading header) */\n  // this.comm_max   = 0;\n  /* true if there was or will be a header crc */\n  this.hcrc       = 0;\n  /* true when done reading gzip header (not used when writing a gzip file) */\n  this.done       = false;\n}\n"
  },
  {
    "path": "vendor/pako/lib/zlib/inffast.js",
    "content": "// See state defs from inflate.js\nvar BAD = 30;       /* got a data error -- remain here until reset */\nvar TYPE = 12;      /* i: waiting for type bits, including last-flag bit */\n\n/*\n   Decode literal, length, and distance codes and write out the resulting\n   literal and match bytes until either not enough input or output is\n   available, an end-of-block is encountered, or a data error is encountered.\n   When large enough input and output buffers are supplied to inflate(), for\n   example, a 16K input buffer and a 64K output buffer, more than 95% of the\n   inflate execution time is spent in this routine.\n\n   Entry assumptions:\n\n        state.mode === LEN\n        strm.avail_in >= 6\n        strm.avail_out >= 258\n        start >= strm.avail_out\n        state.bits < 8\n\n   On return, state.mode is one of:\n\n        LEN -- ran out of enough output space or enough available input\n        TYPE -- reached end of block code, inflate() to interpret next block\n        BAD -- error in block data\n\n   Notes:\n\n    - The maximum input bits used by a length/distance pair is 15 bits for the\n      length code, 5 bits for the length extra, 15 bits for the distance code,\n      and 13 bits for the distance extra.  This totals 48 bits, or six bytes.\n      Therefore if strm.avail_in >= 6, then there is enough input to avoid\n      checking for available input while decoding.\n\n    - The maximum bytes that a single length/distance pair can output is 258\n      bytes, which is the maximum length that can be coded.  inflate_fast()\n      requires strm.avail_out >= 258 for each loop to avoid checking for\n      output space.\n */\nexport default function inflate_fast(strm, start) {\n  var state;\n  var _in;                    /* local strm.input */\n  var last;                   /* have enough input while in < last */\n  var _out;                   /* local strm.output */\n  var beg;                    /* inflate()'s initial strm.output */\n  var end;                    /* while out < end, enough space available */\n//#ifdef INFLATE_STRICT\n  var dmax;                   /* maximum distance from zlib header */\n//#endif\n  var wsize;                  /* window size or zero if not using window */\n  var whave;                  /* valid bytes in the window */\n  var wnext;                  /* window write index */\n  // Use `s_window` instead `window`, avoid conflict with instrumentation tools\n  var s_window;               /* allocated sliding window, if wsize != 0 */\n  var hold;                   /* local strm.hold */\n  var bits;                   /* local strm.bits */\n  var lcode;                  /* local strm.lencode */\n  var dcode;                  /* local strm.distcode */\n  var lmask;                  /* mask for first level of length codes */\n  var dmask;                  /* mask for first level of distance codes */\n  var here;                   /* retrieved table entry */\n  var op;                     /* code bits, operation, extra bits, or */\n                              /*  window position, window bytes to copy */\n  var len;                    /* match length, unused bytes */\n  var dist;                   /* match distance */\n  var from;                   /* where to copy match from */\n  var from_source;\n\n\n  var input, output; // JS specific, because we have no pointers\n\n  /* copy state to local variables */\n  state = strm.state;\n  //here = state.here;\n  _in = strm.next_in;\n  input = strm.input;\n  last = _in + (strm.avail_in - 5);\n  _out = strm.next_out;\n  output = strm.output;\n  beg = _out - (start - strm.avail_out);\n  end = _out + (strm.avail_out - 257);\n//#ifdef INFLATE_STRICT\n  dmax = state.dmax;\n//#endif\n  wsize = state.wsize;\n  whave = state.whave;\n  wnext = state.wnext;\n  s_window = state.window;\n  hold = state.hold;\n  bits = state.bits;\n  lcode = state.lencode;\n  dcode = state.distcode;\n  lmask = (1 << state.lenbits) - 1;\n  dmask = (1 << state.distbits) - 1;\n\n\n  /* decode literals and length/distances until end-of-block or not enough\n     input data or output space */\n\n  top:\n  do {\n    if (bits < 15) {\n      hold += input[_in++] << bits;\n      bits += 8;\n      hold += input[_in++] << bits;\n      bits += 8;\n    }\n\n    here = lcode[hold & lmask];\n\n    dolen:\n    for (;;) { // Goto emulation\n      op = here >>> 24/*here.bits*/;\n      hold >>>= op;\n      bits -= op;\n      op = (here >>> 16) & 0xff/*here.op*/;\n      if (op === 0) {                          /* literal */\n        //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ?\n        //        \"inflate:         literal '%c'\\n\" :\n        //        \"inflate:         literal 0x%02x\\n\", here.val));\n        output[_out++] = here & 0xffff/*here.val*/;\n      }\n      else if (op & 16) {                     /* length base */\n        len = here & 0xffff/*here.val*/;\n        op &= 15;                           /* number of extra bits */\n        if (op) {\n          if (bits < op) {\n            hold += input[_in++] << bits;\n            bits += 8;\n          }\n          len += hold & ((1 << op) - 1);\n          hold >>>= op;\n          bits -= op;\n        }\n        //Tracevv((stderr, \"inflate:         length %u\\n\", len));\n        if (bits < 15) {\n          hold += input[_in++] << bits;\n          bits += 8;\n          hold += input[_in++] << bits;\n          bits += 8;\n        }\n        here = dcode[hold & dmask];\n\n        dodist:\n        for (;;) { // goto emulation\n          op = here >>> 24/*here.bits*/;\n          hold >>>= op;\n          bits -= op;\n          op = (here >>> 16) & 0xff/*here.op*/;\n\n          if (op & 16) {                      /* distance base */\n            dist = here & 0xffff/*here.val*/;\n            op &= 15;                       /* number of extra bits */\n            if (bits < op) {\n              hold += input[_in++] << bits;\n              bits += 8;\n              if (bits < op) {\n                hold += input[_in++] << bits;\n                bits += 8;\n              }\n            }\n            dist += hold & ((1 << op) - 1);\n//#ifdef INFLATE_STRICT\n            if (dist > dmax) {\n              strm.msg = 'invalid distance too far back';\n              state.mode = BAD;\n              break top;\n            }\n//#endif\n            hold >>>= op;\n            bits -= op;\n            //Tracevv((stderr, \"inflate:         distance %u\\n\", dist));\n            op = _out - beg;                /* max distance in output */\n            if (dist > op) {                /* see if copy from window */\n              op = dist - op;               /* distance back in window */\n              if (op > whave) {\n                if (state.sane) {\n                  strm.msg = 'invalid distance too far back';\n                  state.mode = BAD;\n                  break top;\n                }\n\n// (!) This block is disabled in zlib defailts,\n// don't enable it for binary compatibility\n//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR\n//                if (len <= op - whave) {\n//                  do {\n//                    output[_out++] = 0;\n//                  } while (--len);\n//                  continue top;\n//                }\n//                len -= op - whave;\n//                do {\n//                  output[_out++] = 0;\n//                } while (--op > whave);\n//                if (op === 0) {\n//                  from = _out - dist;\n//                  do {\n//                    output[_out++] = output[from++];\n//                  } while (--len);\n//                  continue top;\n//                }\n//#endif\n              }\n              from = 0; // window index\n              from_source = s_window;\n              if (wnext === 0) {           /* very common case */\n                from += wsize - op;\n                if (op < len) {         /* some from window */\n                  len -= op;\n                  do {\n                    output[_out++] = s_window[from++];\n                  } while (--op);\n                  from = _out - dist;  /* rest from output */\n                  from_source = output;\n                }\n              }\n              else if (wnext < op) {      /* wrap around window */\n                from += wsize + wnext - op;\n                op -= wnext;\n                if (op < len) {         /* some from end of window */\n                  len -= op;\n                  do {\n                    output[_out++] = s_window[from++];\n                  } while (--op);\n                  from = 0;\n                  if (wnext < len) {  /* some from start of window */\n                    op = wnext;\n                    len -= op;\n                    do {\n                      output[_out++] = s_window[from++];\n                    } while (--op);\n                    from = _out - dist;      /* rest from output */\n                    from_source = output;\n                  }\n                }\n              }\n              else {                      /* contiguous in window */\n                from += wnext - op;\n                if (op < len) {         /* some from window */\n                  len -= op;\n                  do {\n                    output[_out++] = s_window[from++];\n                  } while (--op);\n                  from = _out - dist;  /* rest from output */\n                  from_source = output;\n                }\n              }\n              while (len > 2) {\n                output[_out++] = from_source[from++];\n                output[_out++] = from_source[from++];\n                output[_out++] = from_source[from++];\n                len -= 3;\n              }\n              if (len) {\n                output[_out++] = from_source[from++];\n                if (len > 1) {\n                  output[_out++] = from_source[from++];\n                }\n              }\n            }\n            else {\n              from = _out - dist;          /* copy direct from output */\n              do {                        /* minimum length is three */\n                output[_out++] = output[from++];\n                output[_out++] = output[from++];\n                output[_out++] = output[from++];\n                len -= 3;\n              } while (len > 2);\n              if (len) {\n                output[_out++] = output[from++];\n                if (len > 1) {\n                  output[_out++] = output[from++];\n                }\n              }\n            }\n          }\n          else if ((op & 64) === 0) {          /* 2nd level distance code */\n            here = dcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];\n            continue dodist;\n          }\n          else {\n            strm.msg = 'invalid distance code';\n            state.mode = BAD;\n            break top;\n          }\n\n          break; // need to emulate goto via \"continue\"\n        }\n      }\n      else if ((op & 64) === 0) {              /* 2nd level length code */\n        here = lcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];\n        continue dolen;\n      }\n      else if (op & 32) {                     /* end-of-block */\n        //Tracevv((stderr, \"inflate:         end of block\\n\"));\n        state.mode = TYPE;\n        break top;\n      }\n      else {\n        strm.msg = 'invalid literal/length code';\n        state.mode = BAD;\n        break top;\n      }\n\n      break; // need to emulate goto via \"continue\"\n    }\n  } while (_in < last && _out < end);\n\n  /* return unused bytes (on entry, bits < 8, so in won't go too far back) */\n  len = bits >> 3;\n  _in -= len;\n  bits -= len << 3;\n  hold &= (1 << bits) - 1;\n\n  /* update state and return */\n  strm.next_in = _in;\n  strm.next_out = _out;\n  strm.avail_in = (_in < last ? 5 + (last - _in) : 5 - (_in - last));\n  strm.avail_out = (_out < end ? 257 + (end - _out) : 257 - (_out - end));\n  state.hold = hold;\n  state.bits = bits;\n  return;\n};\n"
  },
  {
    "path": "vendor/pako/lib/zlib/inflate.js",
    "content": "import * as utils from \"../utils/common.js\";\nimport adler32 from \"./adler32.js\";\nimport crc32 from \"./crc32.js\";\nimport inflate_fast from \"./inffast.js\";\nimport inflate_table from \"./inftrees.js\";\n\nvar CODES = 0;\nvar LENS = 1;\nvar DISTS = 2;\n\n/* Public constants ==========================================================*/\n/* ===========================================================================*/\n\n\n/* Allowed flush values; see deflate() and inflate() below for details */\n//export const Z_NO_FLUSH      = 0;\n//export const Z_PARTIAL_FLUSH = 1;\n//export const Z_SYNC_FLUSH    = 2;\n//export const Z_FULL_FLUSH    = 3;\nexport const Z_FINISH        = 4;\nexport const Z_BLOCK         = 5;\nexport const Z_TREES         = 6;\n\n\n/* Return codes for the compression/decompression functions. Negative values\n * are errors, positive values are used for special but normal events.\n */\nexport const Z_OK            = 0;\nexport const Z_STREAM_END    = 1;\nexport const Z_NEED_DICT     = 2;\n//export const Z_ERRNO         = -1;\nexport const Z_STREAM_ERROR  = -2;\nexport const Z_DATA_ERROR    = -3;\nexport const Z_MEM_ERROR     = -4;\nexport const Z_BUF_ERROR     = -5;\n//export const Z_VERSION_ERROR = -6;\n\n/* The deflate compression method */\nexport const Z_DEFLATED  = 8;\n\n\n/* STATES ====================================================================*/\n/* ===========================================================================*/\n\n\nvar    HEAD = 1;       /* i: waiting for magic header */\nvar    FLAGS = 2;      /* i: waiting for method and flags (gzip) */\nvar    TIME = 3;       /* i: waiting for modification time (gzip) */\nvar    OS = 4;         /* i: waiting for extra flags and operating system (gzip) */\nvar    EXLEN = 5;      /* i: waiting for extra length (gzip) */\nvar    EXTRA = 6;      /* i: waiting for extra bytes (gzip) */\nvar    NAME = 7;       /* i: waiting for end of file name (gzip) */\nvar    COMMENT = 8;    /* i: waiting for end of comment (gzip) */\nvar    HCRC = 9;       /* i: waiting for header crc (gzip) */\nvar    DICTID = 10;    /* i: waiting for dictionary check value */\nvar    DICT = 11;      /* waiting for inflateSetDictionary() call */\nvar        TYPE = 12;      /* i: waiting for type bits, including last-flag bit */\nvar        TYPEDO = 13;    /* i: same, but skip check to exit inflate on new block */\nvar        STORED = 14;    /* i: waiting for stored size (length and complement) */\nvar        COPY_ = 15;     /* i/o: same as COPY below, but only first time in */\nvar        COPY = 16;      /* i/o: waiting for input or output to copy stored block */\nvar        TABLE = 17;     /* i: waiting for dynamic block table lengths */\nvar        LENLENS = 18;   /* i: waiting for code length code lengths */\nvar        CODELENS = 19;  /* i: waiting for length/lit and distance code lengths */\nvar            LEN_ = 20;      /* i: same as LEN below, but only first time in */\nvar            LEN = 21;       /* i: waiting for length/lit/eob code */\nvar            LENEXT = 22;    /* i: waiting for length extra bits */\nvar            DIST = 23;      /* i: waiting for distance code */\nvar            DISTEXT = 24;   /* i: waiting for distance extra bits */\nvar            MATCH = 25;     /* o: waiting for output space to copy string */\nvar            LIT = 26;       /* o: waiting for output space to write literal */\nvar    CHECK = 27;     /* i: waiting for 32-bit check value */\nvar    LENGTH = 28;    /* i: waiting for 32-bit length (gzip) */\nvar    DONE = 29;      /* finished check, done -- remain here until reset */\nvar    BAD = 30;       /* got a data error -- remain here until reset */\nvar    MEM = 31;       /* got an inflate() memory error -- remain here until reset */\nvar    SYNC = 32;      /* looking for synchronization bytes to restart inflate() */\n\n/* ===========================================================================*/\n\n\n\nvar ENOUGH_LENS = 852;\nvar ENOUGH_DISTS = 592;\n//var ENOUGH =  (ENOUGH_LENS+ENOUGH_DISTS);\n\nvar MAX_WBITS = 15;\n/* 32K LZ77 window */\nvar DEF_WBITS = MAX_WBITS;\n\n\nfunction zswap32(q) {\n  return  (((q >>> 24) & 0xff) +\n          ((q >>> 8) & 0xff00) +\n          ((q & 0xff00) << 8) +\n          ((q & 0xff) << 24));\n}\n\n\nfunction InflateState() {\n  this.mode = 0;             /* current inflate mode */\n  this.last = false;          /* true if processing last block */\n  this.wrap = 0;              /* bit 0 true for zlib, bit 1 true for gzip */\n  this.havedict = false;      /* true if dictionary provided */\n  this.flags = 0;             /* gzip header method and flags (0 if zlib) */\n  this.dmax = 0;              /* zlib header max distance (INFLATE_STRICT) */\n  this.check = 0;             /* protected copy of check value */\n  this.total = 0;             /* protected copy of output count */\n  // TODO: may be {}\n  this.head = null;           /* where to save gzip header information */\n\n  /* sliding window */\n  this.wbits = 0;             /* log base 2 of requested window size */\n  this.wsize = 0;             /* window size or zero if not using window */\n  this.whave = 0;             /* valid bytes in the window */\n  this.wnext = 0;             /* window write index */\n  this.window = null;         /* allocated sliding window, if needed */\n\n  /* bit accumulator */\n  this.hold = 0;              /* input bit accumulator */\n  this.bits = 0;              /* number of bits in \"in\" */\n\n  /* for string and stored block copying */\n  this.length = 0;            /* literal or length of data to copy */\n  this.offset = 0;            /* distance back to copy string from */\n\n  /* for table and code decoding */\n  this.extra = 0;             /* extra bits needed */\n\n  /* fixed and dynamic code tables */\n  this.lencode = null;          /* starting table for length/literal codes */\n  this.distcode = null;         /* starting table for distance codes */\n  this.lenbits = 0;           /* index bits for lencode */\n  this.distbits = 0;          /* index bits for distcode */\n\n  /* dynamic table building */\n  this.ncode = 0;             /* number of code length code lengths */\n  this.nlen = 0;              /* number of length code lengths */\n  this.ndist = 0;             /* number of distance code lengths */\n  this.have = 0;              /* number of code lengths in lens[] */\n  this.next = null;              /* next available space in codes[] */\n\n  this.lens = new utils.Buf16(320); /* temporary storage for code lengths */\n  this.work = new utils.Buf16(288); /* work area for code table building */\n\n  /*\n   because we don't have pointers in js, we use lencode and distcode directly\n   as buffers so we don't need codes\n  */\n  //this.codes = new utils.Buf32(ENOUGH);       /* space for code tables */\n  this.lendyn = null;              /* dynamic table for length/literal codes (JS specific) */\n  this.distdyn = null;             /* dynamic table for distance codes (JS specific) */\n  this.sane = 0;                   /* if false, allow invalid distance too far */\n  this.back = 0;                   /* bits back of last unprocessed length/lit */\n  this.was = 0;                    /* initial length of match */\n}\n\nfunction inflateResetKeep(strm) {\n  var state;\n\n  if (!strm || !strm.state) { return Z_STREAM_ERROR; }\n  state = strm.state;\n  strm.total_in = strm.total_out = state.total = 0;\n  strm.msg = ''; /*Z_NULL*/\n  if (state.wrap) {       /* to support ill-conceived Java test suite */\n    strm.adler = state.wrap & 1;\n  }\n  state.mode = HEAD;\n  state.last = 0;\n  state.havedict = 0;\n  state.dmax = 32768;\n  state.head = null/*Z_NULL*/;\n  state.hold = 0;\n  state.bits = 0;\n  //state.lencode = state.distcode = state.next = state.codes;\n  state.lencode = state.lendyn = new utils.Buf32(ENOUGH_LENS);\n  state.distcode = state.distdyn = new utils.Buf32(ENOUGH_DISTS);\n\n  state.sane = 1;\n  state.back = -1;\n  //Tracev((stderr, \"inflate: reset\\n\"));\n  return Z_OK;\n}\n\nfunction inflateReset(strm) {\n  var state;\n\n  if (!strm || !strm.state) { return Z_STREAM_ERROR; }\n  state = strm.state;\n  state.wsize = 0;\n  state.whave = 0;\n  state.wnext = 0;\n  return inflateResetKeep(strm);\n\n}\n\nfunction inflateReset2(strm, windowBits) {\n  var wrap;\n  var state;\n\n  /* get the state */\n  if (!strm || !strm.state) { return Z_STREAM_ERROR; }\n  state = strm.state;\n\n  /* extract wrap request from windowBits parameter */\n  if (windowBits < 0) {\n    wrap = 0;\n    windowBits = -windowBits;\n  }\n  else {\n    wrap = (windowBits >> 4) + 1;\n    if (windowBits < 48) {\n      windowBits &= 15;\n    }\n  }\n\n  /* set number of window bits, free window if different */\n  if (windowBits && (windowBits < 8 || windowBits > 15)) {\n    return Z_STREAM_ERROR;\n  }\n  if (state.window !== null && state.wbits !== windowBits) {\n    state.window = null;\n  }\n\n  /* update state and reset the rest of it */\n  state.wrap = wrap;\n  state.wbits = windowBits;\n  return inflateReset(strm);\n}\n\nfunction inflateInit2(strm, windowBits) {\n  var ret;\n  var state;\n\n  if (!strm) { return Z_STREAM_ERROR; }\n  //strm.msg = Z_NULL;                 /* in case we return an error */\n\n  state = new InflateState();\n\n  //if (state === Z_NULL) return Z_MEM_ERROR;\n  //Tracev((stderr, \"inflate: allocated\\n\"));\n  strm.state = state;\n  state.window = null/*Z_NULL*/;\n  ret = inflateReset2(strm, windowBits);\n  if (ret !== Z_OK) {\n    strm.state = null/*Z_NULL*/;\n  }\n  return ret;\n}\n\nfunction inflateInit(strm) {\n  return inflateInit2(strm, DEF_WBITS);\n}\n\n\n/*\n Return state with length and distance decoding tables and index sizes set to\n fixed code decoding.  Normally this returns fixed tables from inffixed.h.\n If BUILDFIXED is defined, then instead this routine builds the tables the\n first time it's called, and returns those tables the first time and\n thereafter.  This reduces the size of the code by about 2K bytes, in\n exchange for a little execution time.  However, BUILDFIXED should not be\n used for threaded applications, since the rewriting of the tables and virgin\n may not be thread-safe.\n */\nvar virgin = true;\n\nvar lenfix, distfix; // We have no pointers in JS, so keep tables separate\n\nfunction fixedtables(state) {\n  /* build fixed huffman tables if first call (may not be thread safe) */\n  if (virgin) {\n    var sym;\n\n    lenfix = new utils.Buf32(512);\n    distfix = new utils.Buf32(32);\n\n    /* literal/length table */\n    sym = 0;\n    while (sym < 144) { state.lens[sym++] = 8; }\n    while (sym < 256) { state.lens[sym++] = 9; }\n    while (sym < 280) { state.lens[sym++] = 7; }\n    while (sym < 288) { state.lens[sym++] = 8; }\n\n    inflate_table(LENS,  state.lens, 0, 288, lenfix,   0, state.work, { bits: 9 });\n\n    /* distance table */\n    sym = 0;\n    while (sym < 32) { state.lens[sym++] = 5; }\n\n    inflate_table(DISTS, state.lens, 0, 32,   distfix, 0, state.work, { bits: 5 });\n\n    /* do this just once */\n    virgin = false;\n  }\n\n  state.lencode = lenfix;\n  state.lenbits = 9;\n  state.distcode = distfix;\n  state.distbits = 5;\n}\n\n\n/*\n Update the window with the last wsize (normally 32K) bytes written before\n returning.  If window does not exist yet, create it.  This is only called\n when a window is already in use, or when output has been written during this\n inflate call, but the end of the deflate stream has not been reached yet.\n It is also called to create a window for dictionary data when a dictionary\n is loaded.\n\n Providing output buffers larger than 32K to inflate() should provide a speed\n advantage, since only the last 32K of output is copied to the sliding window\n upon return from inflate(), and since all distances after the first 32K of\n output will fall in the output data, making match copies simpler and faster.\n The advantage may be dependent on the size of the processor's data caches.\n */\nfunction updatewindow(strm, src, end, copy) {\n  var dist;\n  var state = strm.state;\n\n  /* if it hasn't been done already, allocate space for the window */\n  if (state.window === null) {\n    state.wsize = 1 << state.wbits;\n    state.wnext = 0;\n    state.whave = 0;\n\n    state.window = new utils.Buf8(state.wsize);\n  }\n\n  /* copy state->wsize or less output bytes into the circular window */\n  if (copy >= state.wsize) {\n    utils.arraySet(state.window, src, end - state.wsize, state.wsize, 0);\n    state.wnext = 0;\n    state.whave = state.wsize;\n  }\n  else {\n    dist = state.wsize - state.wnext;\n    if (dist > copy) {\n      dist = copy;\n    }\n    //zmemcpy(state->window + state->wnext, end - copy, dist);\n    utils.arraySet(state.window, src, end - copy, dist, state.wnext);\n    copy -= dist;\n    if (copy) {\n      //zmemcpy(state->window, end - copy, copy);\n      utils.arraySet(state.window, src, end - copy, copy, 0);\n      state.wnext = copy;\n      state.whave = state.wsize;\n    }\n    else {\n      state.wnext += dist;\n      if (state.wnext === state.wsize) { state.wnext = 0; }\n      if (state.whave < state.wsize) { state.whave += dist; }\n    }\n  }\n  return 0;\n}\n\nfunction inflate(strm, flush) {\n  var state;\n  var input, output;          // input/output buffers\n  var next;                   /* next input INDEX */\n  var put;                    /* next output INDEX */\n  var have, left;             /* available input and output */\n  var hold;                   /* bit buffer */\n  var bits;                   /* bits in bit buffer */\n  var _in, _out;              /* save starting available input and output */\n  var copy;                   /* number of stored or match bytes to copy */\n  var from;                   /* where to copy match bytes from */\n  var from_source;\n  var here = 0;               /* current decoding table entry */\n  var here_bits, here_op, here_val; // paked \"here\" denormalized (JS specific)\n  //var last;                   /* parent table entry */\n  var last_bits, last_op, last_val; // paked \"last\" denormalized (JS specific)\n  var len;                    /* length to copy for repeats, bits to drop */\n  var ret;                    /* return code */\n  var hbuf = new utils.Buf8(4);    /* buffer for gzip header crc calculation */\n  var opts;\n\n  var n; // temporary var for NEED_BITS\n\n  var order = /* permutation of code lengths */\n    [ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ];\n\n\n  if (!strm || !strm.state || !strm.output ||\n      (!strm.input && strm.avail_in !== 0)) {\n    return Z_STREAM_ERROR;\n  }\n\n  state = strm.state;\n  if (state.mode === TYPE) { state.mode = TYPEDO; }    /* skip check */\n\n\n  //--- LOAD() ---\n  put = strm.next_out;\n  output = strm.output;\n  left = strm.avail_out;\n  next = strm.next_in;\n  input = strm.input;\n  have = strm.avail_in;\n  hold = state.hold;\n  bits = state.bits;\n  //---\n\n  _in = have;\n  _out = left;\n  ret = Z_OK;\n\n  inf_leave: // goto emulation\n  for (;;) {\n    switch (state.mode) {\n    case HEAD:\n      if (state.wrap === 0) {\n        state.mode = TYPEDO;\n        break;\n      }\n      //=== NEEDBITS(16);\n      while (bits < 16) {\n        if (have === 0) { break inf_leave; }\n        have--;\n        hold += input[next++] << bits;\n        bits += 8;\n      }\n      //===//\n      if ((state.wrap & 2) && hold === 0x8b1f) {  /* gzip header */\n        state.check = 0/*crc32(0L, Z_NULL, 0)*/;\n        //=== CRC2(state.check, hold);\n        hbuf[0] = hold & 0xff;\n        hbuf[1] = (hold >>> 8) & 0xff;\n        state.check = crc32(state.check, hbuf, 2, 0);\n        //===//\n\n        //=== INITBITS();\n        hold = 0;\n        bits = 0;\n        //===//\n        state.mode = FLAGS;\n        break;\n      }\n      state.flags = 0;           /* expect zlib header */\n      if (state.head) {\n        state.head.done = false;\n      }\n      if (!(state.wrap & 1) ||   /* check if zlib header allowed */\n        (((hold & 0xff)/*BITS(8)*/ << 8) + (hold >> 8)) % 31) {\n        strm.msg = 'incorrect header check';\n        state.mode = BAD;\n        break;\n      }\n      if ((hold & 0x0f)/*BITS(4)*/ !== Z_DEFLATED) {\n        strm.msg = 'unknown compression method';\n        state.mode = BAD;\n        break;\n      }\n      //--- DROPBITS(4) ---//\n      hold >>>= 4;\n      bits -= 4;\n      //---//\n      len = (hold & 0x0f)/*BITS(4)*/ + 8;\n      if (state.wbits === 0) {\n        state.wbits = len;\n      }\n      else if (len > state.wbits) {\n        strm.msg = 'invalid window size';\n        state.mode = BAD;\n        break;\n      }\n      state.dmax = 1 << len;\n      //Tracev((stderr, \"inflate:   zlib header ok\\n\"));\n      strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/;\n      state.mode = hold & 0x200 ? DICTID : TYPE;\n      //=== INITBITS();\n      hold = 0;\n      bits = 0;\n      //===//\n      break;\n    case FLAGS:\n      //=== NEEDBITS(16); */\n      while (bits < 16) {\n        if (have === 0) { break inf_leave; }\n        have--;\n        hold += input[next++] << bits;\n        bits += 8;\n      }\n      //===//\n      state.flags = hold;\n      if ((state.flags & 0xff) !== Z_DEFLATED) {\n        strm.msg = 'unknown compression method';\n        state.mode = BAD;\n        break;\n      }\n      if (state.flags & 0xe000) {\n        strm.msg = 'unknown header flags set';\n        state.mode = BAD;\n        break;\n      }\n      if (state.head) {\n        state.head.text = ((hold >> 8) & 1);\n      }\n      if (state.flags & 0x0200) {\n        //=== CRC2(state.check, hold);\n        hbuf[0] = hold & 0xff;\n        hbuf[1] = (hold >>> 8) & 0xff;\n        state.check = crc32(state.check, hbuf, 2, 0);\n        //===//\n      }\n      //=== INITBITS();\n      hold = 0;\n      bits = 0;\n      //===//\n      state.mode = TIME;\n      /* falls through */\n    case TIME:\n      //=== NEEDBITS(32); */\n      while (bits < 32) {\n        if (have === 0) { break inf_leave; }\n        have--;\n        hold += input[next++] << bits;\n        bits += 8;\n      }\n      //===//\n      if (state.head) {\n        state.head.time = hold;\n      }\n      if (state.flags & 0x0200) {\n        //=== CRC4(state.check, hold)\n        hbuf[0] = hold & 0xff;\n        hbuf[1] = (hold >>> 8) & 0xff;\n        hbuf[2] = (hold >>> 16) & 0xff;\n        hbuf[3] = (hold >>> 24) & 0xff;\n        state.check = crc32(state.check, hbuf, 4, 0);\n        //===\n      }\n      //=== INITBITS();\n      hold = 0;\n      bits = 0;\n      //===//\n      state.mode = OS;\n      /* falls through */\n    case OS:\n      //=== NEEDBITS(16); */\n      while (bits < 16) {\n        if (have === 0) { break inf_leave; }\n        have--;\n        hold += input[next++] << bits;\n        bits += 8;\n      }\n      //===//\n      if (state.head) {\n        state.head.xflags = (hold & 0xff);\n        state.head.os = (hold >> 8);\n      }\n      if (state.flags & 0x0200) {\n        //=== CRC2(state.check, hold);\n        hbuf[0] = hold & 0xff;\n        hbuf[1] = (hold >>> 8) & 0xff;\n        state.check = crc32(state.check, hbuf, 2, 0);\n        //===//\n      }\n      //=== INITBITS();\n      hold = 0;\n      bits = 0;\n      //===//\n      state.mode = EXLEN;\n      /* falls through */\n    case EXLEN:\n      if (state.flags & 0x0400) {\n        //=== NEEDBITS(16); */\n        while (bits < 16) {\n          if (have === 0) { break inf_leave; }\n          have--;\n          hold += input[next++] << bits;\n          bits += 8;\n        }\n        //===//\n        state.length = hold;\n        if (state.head) {\n          state.head.extra_len = hold;\n        }\n        if (state.flags & 0x0200) {\n          //=== CRC2(state.check, hold);\n          hbuf[0] = hold & 0xff;\n          hbuf[1] = (hold >>> 8) & 0xff;\n          state.check = crc32(state.check, hbuf, 2, 0);\n          //===//\n        }\n        //=== INITBITS();\n        hold = 0;\n        bits = 0;\n        //===//\n      }\n      else if (state.head) {\n        state.head.extra = null/*Z_NULL*/;\n      }\n      state.mode = EXTRA;\n      /* falls through */\n    case EXTRA:\n      if (state.flags & 0x0400) {\n        copy = state.length;\n        if (copy > have) { copy = have; }\n        if (copy) {\n          if (state.head) {\n            len = state.head.extra_len - state.length;\n            if (!state.head.extra) {\n              // Use untyped array for more conveniend processing later\n              state.head.extra = new Array(state.head.extra_len);\n            }\n            utils.arraySet(\n              state.head.extra,\n              input,\n              next,\n              // extra field is limited to 65536 bytes\n              // - no need for additional size check\n              copy,\n              /*len + copy > state.head.extra_max - len ? state.head.extra_max : copy,*/\n              len\n            );\n            //zmemcpy(state.head.extra + len, next,\n            //        len + copy > state.head.extra_max ?\n            //        state.head.extra_max - len : copy);\n          }\n          if (state.flags & 0x0200) {\n            state.check = crc32(state.check, input, copy, next);\n          }\n          have -= copy;\n          next += copy;\n          state.length -= copy;\n        }\n        if (state.length) { break inf_leave; }\n      }\n      state.length = 0;\n      state.mode = NAME;\n      /* falls through */\n    case NAME:\n      if (state.flags & 0x0800) {\n        if (have === 0) { break inf_leave; }\n        copy = 0;\n        do {\n          // TODO: 2 or 1 bytes?\n          len = input[next + copy++];\n          /* use constant limit because in js we should not preallocate memory */\n          if (state.head && len &&\n              (state.length < 65536 /*state.head.name_max*/)) {\n            state.head.name += String.fromCharCode(len);\n          }\n        } while (len && copy < have);\n\n        if (state.flags & 0x0200) {\n          state.check = crc32(state.check, input, copy, next);\n        }\n        have -= copy;\n        next += copy;\n        if (len) { break inf_leave; }\n      }\n      else if (state.head) {\n        state.head.name = null;\n      }\n      state.length = 0;\n      state.mode = COMMENT;\n      /* falls through */\n    case COMMENT:\n      if (state.flags & 0x1000) {\n        if (have === 0) { break inf_leave; }\n        copy = 0;\n        do {\n          len = input[next + copy++];\n          /* use constant limit because in js we should not preallocate memory */\n          if (state.head && len &&\n              (state.length < 65536 /*state.head.comm_max*/)) {\n            state.head.comment += String.fromCharCode(len);\n          }\n        } while (len && copy < have);\n        if (state.flags & 0x0200) {\n          state.check = crc32(state.check, input, copy, next);\n        }\n        have -= copy;\n        next += copy;\n        if (len) { break inf_leave; }\n      }\n      else if (state.head) {\n        state.head.comment = null;\n      }\n      state.mode = HCRC;\n      /* falls through */\n    case HCRC:\n      if (state.flags & 0x0200) {\n        //=== NEEDBITS(16); */\n        while (bits < 16) {\n          if (have === 0) { break inf_leave; }\n          have--;\n          hold += input[next++] << bits;\n          bits += 8;\n        }\n        //===//\n        if (hold !== (state.check & 0xffff)) {\n          strm.msg = 'header crc mismatch';\n          state.mode = BAD;\n          break;\n        }\n        //=== INITBITS();\n        hold = 0;\n        bits = 0;\n        //===//\n      }\n      if (state.head) {\n        state.head.hcrc = ((state.flags >> 9) & 1);\n        state.head.done = true;\n      }\n      strm.adler = state.check = 0;\n      state.mode = TYPE;\n      break;\n    case DICTID:\n      //=== NEEDBITS(32); */\n      while (bits < 32) {\n        if (have === 0) { break inf_leave; }\n        have--;\n        hold += input[next++] << bits;\n        bits += 8;\n      }\n      //===//\n      strm.adler = state.check = zswap32(hold);\n      //=== INITBITS();\n      hold = 0;\n      bits = 0;\n      //===//\n      state.mode = DICT;\n      /* falls through */\n    case DICT:\n      if (state.havedict === 0) {\n        //--- RESTORE() ---\n        strm.next_out = put;\n        strm.avail_out = left;\n        strm.next_in = next;\n        strm.avail_in = have;\n        state.hold = hold;\n        state.bits = bits;\n        //---\n        return Z_NEED_DICT;\n      }\n      strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/;\n      state.mode = TYPE;\n      /* falls through */\n    case TYPE:\n      if (flush === Z_BLOCK || flush === Z_TREES) { break inf_leave; }\n      /* falls through */\n    case TYPEDO:\n      if (state.last) {\n        //--- BYTEBITS() ---//\n        hold >>>= bits & 7;\n        bits -= bits & 7;\n        //---//\n        state.mode = CHECK;\n        break;\n      }\n      //=== NEEDBITS(3); */\n      while (bits < 3) {\n        if (have === 0) { break inf_leave; }\n        have--;\n        hold += input[next++] << bits;\n        bits += 8;\n      }\n      //===//\n      state.last = (hold & 0x01)/*BITS(1)*/;\n      //--- DROPBITS(1) ---//\n      hold >>>= 1;\n      bits -= 1;\n      //---//\n\n      switch ((hold & 0x03)/*BITS(2)*/) {\n      case 0:                             /* stored block */\n        //Tracev((stderr, \"inflate:     stored block%s\\n\",\n        //        state.last ? \" (last)\" : \"\"));\n        state.mode = STORED;\n        break;\n      case 1:                             /* fixed block */\n        fixedtables(state);\n        //Tracev((stderr, \"inflate:     fixed codes block%s\\n\",\n        //        state.last ? \" (last)\" : \"\"));\n        state.mode = LEN_;             /* decode codes */\n        if (flush === Z_TREES) {\n          //--- DROPBITS(2) ---//\n          hold >>>= 2;\n          bits -= 2;\n          //---//\n          break inf_leave;\n        }\n        break;\n      case 2:                             /* dynamic block */\n        //Tracev((stderr, \"inflate:     dynamic codes block%s\\n\",\n        //        state.last ? \" (last)\" : \"\"));\n        state.mode = TABLE;\n        break;\n      case 3:\n        strm.msg = 'invalid block type';\n        state.mode = BAD;\n      }\n      //--- DROPBITS(2) ---//\n      hold >>>= 2;\n      bits -= 2;\n      //---//\n      break;\n    case STORED:\n      //--- BYTEBITS() ---// /* go to byte boundary */\n      hold >>>= bits & 7;\n      bits -= bits & 7;\n      //---//\n      //=== NEEDBITS(32); */\n      while (bits < 32) {\n        if (have === 0) { break inf_leave; }\n        have--;\n        hold += input[next++] << bits;\n        bits += 8;\n      }\n      //===//\n      if ((hold & 0xffff) !== ((hold >>> 16) ^ 0xffff)) {\n        strm.msg = 'invalid stored block lengths';\n        state.mode = BAD;\n        break;\n      }\n      state.length = hold & 0xffff;\n      //Tracev((stderr, \"inflate:       stored length %u\\n\",\n      //        state.length));\n      //=== INITBITS();\n      hold = 0;\n      bits = 0;\n      //===//\n      state.mode = COPY_;\n      if (flush === Z_TREES) { break inf_leave; }\n      /* falls through */\n    case COPY_:\n      state.mode = COPY;\n      /* falls through */\n    case COPY:\n      copy = state.length;\n      if (copy) {\n        if (copy > have) { copy = have; }\n        if (copy > left) { copy = left; }\n        if (copy === 0) { break inf_leave; }\n        //--- zmemcpy(put, next, copy); ---\n        utils.arraySet(output, input, next, copy, put);\n        //---//\n        have -= copy;\n        next += copy;\n        left -= copy;\n        put += copy;\n        state.length -= copy;\n        break;\n      }\n      //Tracev((stderr, \"inflate:       stored end\\n\"));\n      state.mode = TYPE;\n      break;\n    case TABLE:\n      //=== NEEDBITS(14); */\n      while (bits < 14) {\n        if (have === 0) { break inf_leave; }\n        have--;\n        hold += input[next++] << bits;\n        bits += 8;\n      }\n      //===//\n      state.nlen = (hold & 0x1f)/*BITS(5)*/ + 257;\n      //--- DROPBITS(5) ---//\n      hold >>>= 5;\n      bits -= 5;\n      //---//\n      state.ndist = (hold & 0x1f)/*BITS(5)*/ + 1;\n      //--- DROPBITS(5) ---//\n      hold >>>= 5;\n      bits -= 5;\n      //---//\n      state.ncode = (hold & 0x0f)/*BITS(4)*/ + 4;\n      //--- DROPBITS(4) ---//\n      hold >>>= 4;\n      bits -= 4;\n      //---//\n//#ifndef PKZIP_BUG_WORKAROUND\n      if (state.nlen > 286 || state.ndist > 30) {\n        strm.msg = 'too many length or distance symbols';\n        state.mode = BAD;\n        break;\n      }\n//#endif\n      //Tracev((stderr, \"inflate:       table sizes ok\\n\"));\n      state.have = 0;\n      state.mode = LENLENS;\n      /* falls through */\n    case LENLENS:\n      while (state.have < state.ncode) {\n        //=== NEEDBITS(3);\n        while (bits < 3) {\n          if (have === 0) { break inf_leave; }\n          have--;\n          hold += input[next++] << bits;\n          bits += 8;\n        }\n        //===//\n        state.lens[order[state.have++]] = (hold & 0x07);//BITS(3);\n        //--- DROPBITS(3) ---//\n        hold >>>= 3;\n        bits -= 3;\n        //---//\n      }\n      while (state.have < 19) {\n        state.lens[order[state.have++]] = 0;\n      }\n      // We have separate tables & no pointers. 2 commented lines below not needed.\n      //state.next = state.codes;\n      //state.lencode = state.next;\n      // Switch to use dynamic table\n      state.lencode = state.lendyn;\n      state.lenbits = 7;\n\n      opts = { bits: state.lenbits };\n      ret = inflate_table(CODES, state.lens, 0, 19, state.lencode, 0, state.work, opts);\n      state.lenbits = opts.bits;\n\n      if (ret) {\n        strm.msg = 'invalid code lengths set';\n        state.mode = BAD;\n        break;\n      }\n      //Tracev((stderr, \"inflate:       code lengths ok\\n\"));\n      state.have = 0;\n      state.mode = CODELENS;\n      /* falls through */\n    case CODELENS:\n      while (state.have < state.nlen + state.ndist) {\n        for (;;) {\n          here = state.lencode[hold & ((1 << state.lenbits) - 1)];/*BITS(state.lenbits)*/\n          here_bits = here >>> 24;\n          here_op = (here >>> 16) & 0xff;\n          here_val = here & 0xffff;\n\n          if ((here_bits) <= bits) { break; }\n          //--- PULLBYTE() ---//\n          if (have === 0) { break inf_leave; }\n          have--;\n          hold += input[next++] << bits;\n          bits += 8;\n          //---//\n        }\n        if (here_val < 16) {\n          //--- DROPBITS(here.bits) ---//\n          hold >>>= here_bits;\n          bits -= here_bits;\n          //---//\n          state.lens[state.have++] = here_val;\n        }\n        else {\n          if (here_val === 16) {\n            //=== NEEDBITS(here.bits + 2);\n            n = here_bits + 2;\n            while (bits < n) {\n              if (have === 0) { break inf_leave; }\n              have--;\n              hold += input[next++] << bits;\n              bits += 8;\n            }\n            //===//\n            //--- DROPBITS(here.bits) ---//\n            hold >>>= here_bits;\n            bits -= here_bits;\n            //---//\n            if (state.have === 0) {\n              strm.msg = 'invalid bit length repeat';\n              state.mode = BAD;\n              break;\n            }\n            len = state.lens[state.have - 1];\n            copy = 3 + (hold & 0x03);//BITS(2);\n            //--- DROPBITS(2) ---//\n            hold >>>= 2;\n            bits -= 2;\n            //---//\n          }\n          else if (here_val === 17) {\n            //=== NEEDBITS(here.bits + 3);\n            n = here_bits + 3;\n            while (bits < n) {\n              if (have === 0) { break inf_leave; }\n              have--;\n              hold += input[next++] << bits;\n              bits += 8;\n            }\n            //===//\n            //--- DROPBITS(here.bits) ---//\n            hold >>>= here_bits;\n            bits -= here_bits;\n            //---//\n            len = 0;\n            copy = 3 + (hold & 0x07);//BITS(3);\n            //--- DROPBITS(3) ---//\n            hold >>>= 3;\n            bits -= 3;\n            //---//\n          }\n          else {\n            //=== NEEDBITS(here.bits + 7);\n            n = here_bits + 7;\n            while (bits < n) {\n              if (have === 0) { break inf_leave; }\n              have--;\n              hold += input[next++] << bits;\n              bits += 8;\n            }\n            //===//\n            //--- DROPBITS(here.bits) ---//\n            hold >>>= here_bits;\n            bits -= here_bits;\n            //---//\n            len = 0;\n            copy = 11 + (hold & 0x7f);//BITS(7);\n            //--- DROPBITS(7) ---//\n            hold >>>= 7;\n            bits -= 7;\n            //---//\n          }\n          if (state.have + copy > state.nlen + state.ndist) {\n            strm.msg = 'invalid bit length repeat';\n            state.mode = BAD;\n            break;\n          }\n          while (copy--) {\n            state.lens[state.have++] = len;\n          }\n        }\n      }\n\n      /* handle error breaks in while */\n      if (state.mode === BAD) { break; }\n\n      /* check for end-of-block code (better have one) */\n      if (state.lens[256] === 0) {\n        strm.msg = 'invalid code -- missing end-of-block';\n        state.mode = BAD;\n        break;\n      }\n\n      /* build code tables -- note: do not change the lenbits or distbits\n         values here (9 and 6) without reading the comments in inftrees.h\n         concerning the ENOUGH constants, which depend on those values */\n      state.lenbits = 9;\n\n      opts = { bits: state.lenbits };\n      ret = inflate_table(LENS, state.lens, 0, state.nlen, state.lencode, 0, state.work, opts);\n      // We have separate tables & no pointers. 2 commented lines below not needed.\n      // state.next_index = opts.table_index;\n      state.lenbits = opts.bits;\n      // state.lencode = state.next;\n\n      if (ret) {\n        strm.msg = 'invalid literal/lengths set';\n        state.mode = BAD;\n        break;\n      }\n\n      state.distbits = 6;\n      //state.distcode.copy(state.codes);\n      // Switch to use dynamic table\n      state.distcode = state.distdyn;\n      opts = { bits: state.distbits };\n      ret = inflate_table(DISTS, state.lens, state.nlen, state.ndist, state.distcode, 0, state.work, opts);\n      // We have separate tables & no pointers. 2 commented lines below not needed.\n      // state.next_index = opts.table_index;\n      state.distbits = opts.bits;\n      // state.distcode = state.next;\n\n      if (ret) {\n        strm.msg = 'invalid distances set';\n        state.mode = BAD;\n        break;\n      }\n      //Tracev((stderr, 'inflate:       codes ok\\n'));\n      state.mode = LEN_;\n      if (flush === Z_TREES) { break inf_leave; }\n      /* falls through */\n    case LEN_:\n      state.mode = LEN;\n      /* falls through */\n    case LEN:\n      if (have >= 6 && left >= 258) {\n        //--- RESTORE() ---\n        strm.next_out = put;\n        strm.avail_out = left;\n        strm.next_in = next;\n        strm.avail_in = have;\n        state.hold = hold;\n        state.bits = bits;\n        //---\n        inflate_fast(strm, _out);\n        //--- LOAD() ---\n        put = strm.next_out;\n        output = strm.output;\n        left = strm.avail_out;\n        next = strm.next_in;\n        input = strm.input;\n        have = strm.avail_in;\n        hold = state.hold;\n        bits = state.bits;\n        //---\n\n        if (state.mode === TYPE) {\n          state.back = -1;\n        }\n        break;\n      }\n      state.back = 0;\n      for (;;) {\n        here = state.lencode[hold & ((1 << state.lenbits) - 1)];  /*BITS(state.lenbits)*/\n        here_bits = here >>> 24;\n        here_op = (here >>> 16) & 0xff;\n        here_val = here & 0xffff;\n\n        if (here_bits <= bits) { break; }\n        //--- PULLBYTE() ---//\n        if (have === 0) { break inf_leave; }\n        have--;\n        hold += input[next++] << bits;\n        bits += 8;\n        //---//\n      }\n      if (here_op && (here_op & 0xf0) === 0) {\n        last_bits = here_bits;\n        last_op = here_op;\n        last_val = here_val;\n        for (;;) {\n          here = state.lencode[last_val +\n                  ((hold & ((1 << (last_bits + last_op)) - 1))/*BITS(last.bits + last.op)*/ >> last_bits)];\n          here_bits = here >>> 24;\n          here_op = (here >>> 16) & 0xff;\n          here_val = here & 0xffff;\n\n          if ((last_bits + here_bits) <= bits) { break; }\n          //--- PULLBYTE() ---//\n          if (have === 0) { break inf_leave; }\n          have--;\n          hold += input[next++] << bits;\n          bits += 8;\n          //---//\n        }\n        //--- DROPBITS(last.bits) ---//\n        hold >>>= last_bits;\n        bits -= last_bits;\n        //---//\n        state.back += last_bits;\n      }\n      //--- DROPBITS(here.bits) ---//\n      hold >>>= here_bits;\n      bits -= here_bits;\n      //---//\n      state.back += here_bits;\n      state.length = here_val;\n      if (here_op === 0) {\n        //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ?\n        //        \"inflate:         literal '%c'\\n\" :\n        //        \"inflate:         literal 0x%02x\\n\", here.val));\n        state.mode = LIT;\n        break;\n      }\n      if (here_op & 32) {\n        //Tracevv((stderr, \"inflate:         end of block\\n\"));\n        state.back = -1;\n        state.mode = TYPE;\n        break;\n      }\n      if (here_op & 64) {\n        strm.msg = 'invalid literal/length code';\n        state.mode = BAD;\n        break;\n      }\n      state.extra = here_op & 15;\n      state.mode = LENEXT;\n      /* falls through */\n    case LENEXT:\n      if (state.extra) {\n        //=== NEEDBITS(state.extra);\n        n = state.extra;\n        while (bits < n) {\n          if (have === 0) { break inf_leave; }\n          have--;\n          hold += input[next++] << bits;\n          bits += 8;\n        }\n        //===//\n        state.length += hold & ((1 << state.extra) - 1)/*BITS(state.extra)*/;\n        //--- DROPBITS(state.extra) ---//\n        hold >>>= state.extra;\n        bits -= state.extra;\n        //---//\n        state.back += state.extra;\n      }\n      //Tracevv((stderr, \"inflate:         length %u\\n\", state.length));\n      state.was = state.length;\n      state.mode = DIST;\n      /* falls through */\n    case DIST:\n      for (;;) {\n        here = state.distcode[hold & ((1 << state.distbits) - 1)];/*BITS(state.distbits)*/\n        here_bits = here >>> 24;\n        here_op = (here >>> 16) & 0xff;\n        here_val = here & 0xffff;\n\n        if ((here_bits) <= bits) { break; }\n        //--- PULLBYTE() ---//\n        if (have === 0) { break inf_leave; }\n        have--;\n        hold += input[next++] << bits;\n        bits += 8;\n        //---//\n      }\n      if ((here_op & 0xf0) === 0) {\n        last_bits = here_bits;\n        last_op = here_op;\n        last_val = here_val;\n        for (;;) {\n          here = state.distcode[last_val +\n                  ((hold & ((1 << (last_bits + last_op)) - 1))/*BITS(last.bits + last.op)*/ >> last_bits)];\n          here_bits = here >>> 24;\n          here_op = (here >>> 16) & 0xff;\n          here_val = here & 0xffff;\n\n          if ((last_bits + here_bits) <= bits) { break; }\n          //--- PULLBYTE() ---//\n          if (have === 0) { break inf_leave; }\n          have--;\n          hold += input[next++] << bits;\n          bits += 8;\n          //---//\n        }\n        //--- DROPBITS(last.bits) ---//\n        hold >>>= last_bits;\n        bits -= last_bits;\n        //---//\n        state.back += last_bits;\n      }\n      //--- DROPBITS(here.bits) ---//\n      hold >>>= here_bits;\n      bits -= here_bits;\n      //---//\n      state.back += here_bits;\n      if (here_op & 64) {\n        strm.msg = 'invalid distance code';\n        state.mode = BAD;\n        break;\n      }\n      state.offset = here_val;\n      state.extra = (here_op) & 15;\n      state.mode = DISTEXT;\n      /* falls through */\n    case DISTEXT:\n      if (state.extra) {\n        //=== NEEDBITS(state.extra);\n        n = state.extra;\n        while (bits < n) {\n          if (have === 0) { break inf_leave; }\n          have--;\n          hold += input[next++] << bits;\n          bits += 8;\n        }\n        //===//\n        state.offset += hold & ((1 << state.extra) - 1)/*BITS(state.extra)*/;\n        //--- DROPBITS(state.extra) ---//\n        hold >>>= state.extra;\n        bits -= state.extra;\n        //---//\n        state.back += state.extra;\n      }\n//#ifdef INFLATE_STRICT\n      if (state.offset > state.dmax) {\n        strm.msg = 'invalid distance too far back';\n        state.mode = BAD;\n        break;\n      }\n//#endif\n      //Tracevv((stderr, \"inflate:         distance %u\\n\", state.offset));\n      state.mode = MATCH;\n      /* falls through */\n    case MATCH:\n      if (left === 0) { break inf_leave; }\n      copy = _out - left;\n      if (state.offset > copy) {         /* copy from window */\n        copy = state.offset - copy;\n        if (copy > state.whave) {\n          if (state.sane) {\n            strm.msg = 'invalid distance too far back';\n            state.mode = BAD;\n            break;\n          }\n// (!) This block is disabled in zlib defailts,\n// don't enable it for binary compatibility\n//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR\n//          Trace((stderr, \"inflate.c too far\\n\"));\n//          copy -= state.whave;\n//          if (copy > state.length) { copy = state.length; }\n//          if (copy > left) { copy = left; }\n//          left -= copy;\n//          state.length -= copy;\n//          do {\n//            output[put++] = 0;\n//          } while (--copy);\n//          if (state.length === 0) { state.mode = LEN; }\n//          break;\n//#endif\n        }\n        if (copy > state.wnext) {\n          copy -= state.wnext;\n          from = state.wsize - copy;\n        }\n        else {\n          from = state.wnext - copy;\n        }\n        if (copy > state.length) { copy = state.length; }\n        from_source = state.window;\n      }\n      else {                              /* copy from output */\n        from_source = output;\n        from = put - state.offset;\n        copy = state.length;\n      }\n      if (copy > left) { copy = left; }\n      left -= copy;\n      state.length -= copy;\n      do {\n        output[put++] = from_source[from++];\n      } while (--copy);\n      if (state.length === 0) { state.mode = LEN; }\n      break;\n    case LIT:\n      if (left === 0) { break inf_leave; }\n      output[put++] = state.length;\n      left--;\n      state.mode = LEN;\n      break;\n    case CHECK:\n      if (state.wrap) {\n        //=== NEEDBITS(32);\n        while (bits < 32) {\n          if (have === 0) { break inf_leave; }\n          have--;\n          // Use '|' insdead of '+' to make sure that result is signed\n          hold |= input[next++] << bits;\n          bits += 8;\n        }\n        //===//\n        _out -= left;\n        strm.total_out += _out;\n        state.total += _out;\n        if (_out) {\n          strm.adler = state.check =\n              /*UPDATE(state.check, put - _out, _out);*/\n              (state.flags ? crc32(state.check, output, _out, put - _out) : adler32(state.check, output, _out, put - _out));\n\n        }\n        _out = left;\n        // NB: crc32 stored as signed 32-bit int, zswap32 returns signed too\n        if ((state.flags ? hold : zswap32(hold)) !== state.check) {\n          strm.msg = 'incorrect data check';\n          state.mode = BAD;\n          break;\n        }\n        //=== INITBITS();\n        hold = 0;\n        bits = 0;\n        //===//\n        //Tracev((stderr, \"inflate:   check matches trailer\\n\"));\n      }\n      state.mode = LENGTH;\n      /* falls through */\n    case LENGTH:\n      if (state.wrap && state.flags) {\n        //=== NEEDBITS(32);\n        while (bits < 32) {\n          if (have === 0) { break inf_leave; }\n          have--;\n          hold += input[next++] << bits;\n          bits += 8;\n        }\n        //===//\n        if (hold !== (state.total & 0xffffffff)) {\n          strm.msg = 'incorrect length check';\n          state.mode = BAD;\n          break;\n        }\n        //=== INITBITS();\n        hold = 0;\n        bits = 0;\n        //===//\n        //Tracev((stderr, \"inflate:   length matches trailer\\n\"));\n      }\n      state.mode = DONE;\n      /* falls through */\n    case DONE:\n      ret = Z_STREAM_END;\n      break inf_leave;\n    case BAD:\n      ret = Z_DATA_ERROR;\n      break inf_leave;\n    case MEM:\n      return Z_MEM_ERROR;\n    case SYNC:\n      /* falls through */\n    default:\n      return Z_STREAM_ERROR;\n    }\n  }\n\n  // inf_leave <- here is real place for \"goto inf_leave\", emulated via \"break inf_leave\"\n\n  /*\n     Return from inflate(), updating the total counts and the check value.\n     If there was no progress during the inflate() call, return a buffer\n     error.  Call updatewindow() to create and/or update the window state.\n     Note: a memory error from inflate() is non-recoverable.\n   */\n\n  //--- RESTORE() ---\n  strm.next_out = put;\n  strm.avail_out = left;\n  strm.next_in = next;\n  strm.avail_in = have;\n  state.hold = hold;\n  state.bits = bits;\n  //---\n\n  if (state.wsize || (_out !== strm.avail_out && state.mode < BAD &&\n                      (state.mode < CHECK || flush !== Z_FINISH))) {\n    if (updatewindow(strm, strm.output, strm.next_out, _out - strm.avail_out)) {\n      state.mode = MEM;\n      return Z_MEM_ERROR;\n    }\n  }\n  _in -= strm.avail_in;\n  _out -= strm.avail_out;\n  strm.total_in += _in;\n  strm.total_out += _out;\n  state.total += _out;\n  if (state.wrap && _out) {\n    strm.adler = state.check = /*UPDATE(state.check, strm.next_out - _out, _out);*/\n      (state.flags ? crc32(state.check, output, _out, strm.next_out - _out) : adler32(state.check, output, _out, strm.next_out - _out));\n  }\n  strm.data_type = state.bits + (state.last ? 64 : 0) +\n                    (state.mode === TYPE ? 128 : 0) +\n                    (state.mode === LEN_ || state.mode === COPY_ ? 256 : 0);\n  if (((_in === 0 && _out === 0) || flush === Z_FINISH) && ret === Z_OK) {\n    ret = Z_BUF_ERROR;\n  }\n  return ret;\n}\n\nfunction inflateEnd(strm) {\n\n  if (!strm || !strm.state /*|| strm->zfree == (free_func)0*/) {\n    return Z_STREAM_ERROR;\n  }\n\n  var state = strm.state;\n  if (state.window) {\n    state.window = null;\n  }\n  strm.state = null;\n  return Z_OK;\n}\n\nfunction inflateGetHeader(strm, head) {\n  var state;\n\n  /* check state */\n  if (!strm || !strm.state) { return Z_STREAM_ERROR; }\n  state = strm.state;\n  if ((state.wrap & 2) === 0) { return Z_STREAM_ERROR; }\n\n  /* save header structure */\n  state.head = head;\n  head.done = false;\n  return Z_OK;\n}\n\nfunction inflateSetDictionary(strm, dictionary) {\n  var dictLength = dictionary.length;\n\n  var state;\n  var dictid;\n  var ret;\n\n  /* check state */\n  if (!strm /* == Z_NULL */ || !strm.state /* == Z_NULL */) { return Z_STREAM_ERROR; }\n  state = strm.state;\n\n  if (state.wrap !== 0 && state.mode !== DICT) {\n    return Z_STREAM_ERROR;\n  }\n\n  /* check for correct dictionary identifier */\n  if (state.mode === DICT) {\n    dictid = 1; /* adler32(0, null, 0)*/\n    /* dictid = adler32(dictid, dictionary, dictLength); */\n    dictid = adler32(dictid, dictionary, dictLength, 0);\n    if (dictid !== state.check) {\n      return Z_DATA_ERROR;\n    }\n  }\n  /* copy dictionary to window using updatewindow(), which will amend the\n   existing dictionary if appropriate */\n  ret = updatewindow(strm, dictionary, dictLength, dictLength);\n  if (ret) {\n    state.mode = MEM;\n    return Z_MEM_ERROR;\n  }\n  state.havedict = 1;\n  // Tracev((stderr, \"inflate:   dictionary set\\n\"));\n  return Z_OK;\n}\n\nexport { inflateReset, inflateReset2, inflateResetKeep, inflateInit, inflateInit2, inflate, inflateEnd, inflateGetHeader, inflateSetDictionary };\nexport var inflateInfo = 'pako inflate (from Nodeca project)';\n\n/* Not implemented\nexports.inflateCopy = inflateCopy;\nexports.inflateGetDictionary = inflateGetDictionary;\nexports.inflateMark = inflateMark;\nexports.inflatePrime = inflatePrime;\nexports.inflateSync = inflateSync;\nexports.inflateSyncPoint = inflateSyncPoint;\nexports.inflateUndermine = inflateUndermine;\n*/\n"
  },
  {
    "path": "vendor/pako/lib/zlib/inftrees.js",
    "content": "import * as utils from \"../utils/common.js\";\n\nvar MAXBITS = 15;\nvar ENOUGH_LENS = 852;\nvar ENOUGH_DISTS = 592;\n//var ENOUGH = (ENOUGH_LENS+ENOUGH_DISTS);\n\nvar CODES = 0;\nvar LENS = 1;\nvar DISTS = 2;\n\nvar lbase = [ /* Length codes 257..285 base */\n  3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,\n  35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0\n];\n\nvar lext = [ /* Length codes 257..285 extra */\n  16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18,\n  19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 16, 72, 78\n];\n\nvar dbase = [ /* Distance codes 0..29 base */\n  1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,\n  257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145,\n  8193, 12289, 16385, 24577, 0, 0\n];\n\nvar dext = [ /* Distance codes 0..29 extra */\n  16, 16, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22,\n  23, 23, 24, 24, 25, 25, 26, 26, 27, 27,\n  28, 28, 29, 29, 64, 64\n];\n\nexport default function inflate_table(type, lens, lens_index, codes, table, table_index, work, opts)\n{\n  var bits = opts.bits;\n      //here = opts.here; /* table entry for duplication */\n\n  var len = 0;               /* a code's length in bits */\n  var sym = 0;               /* index of code symbols */\n  var min = 0, max = 0;          /* minimum and maximum code lengths */\n  var root = 0;              /* number of index bits for root table */\n  var curr = 0;              /* number of index bits for current table */\n  var drop = 0;              /* code bits to drop for sub-table */\n  var left = 0;                   /* number of prefix codes available */\n  var used = 0;              /* code entries in table used */\n  var huff = 0;              /* Huffman code */\n  var incr;              /* for incrementing code, index */\n  var fill;              /* index for replicating entries */\n  var low;               /* low bits for current root entry */\n  var mask;              /* mask for low root bits */\n  var next;             /* next available space in table */\n  var base = null;     /* base value table to use */\n  var base_index = 0;\n//  var shoextra;    /* extra bits table to use */\n  var end;                    /* use base and extra for symbol > end */\n  var count = new utils.Buf16(MAXBITS + 1); //[MAXBITS+1];    /* number of codes of each length */\n  var offs = new utils.Buf16(MAXBITS + 1); //[MAXBITS+1];     /* offsets in table for each length */\n  var extra = null;\n  var extra_index = 0;\n\n  var here_bits, here_op, here_val;\n\n  /*\n   Process a set of code lengths to create a canonical Huffman code.  The\n   code lengths are lens[0..codes-1].  Each length corresponds to the\n   symbols 0..codes-1.  The Huffman code is generated by first sorting the\n   symbols by length from short to long, and retaining the symbol order\n   for codes with equal lengths.  Then the code starts with all zero bits\n   for the first code of the shortest length, and the codes are integer\n   increments for the same length, and zeros are appended as the length\n   increases.  For the deflate format, these bits are stored backwards\n   from their more natural integer increment ordering, and so when the\n   decoding tables are built in the large loop below, the integer codes\n   are incremented backwards.\n\n   This routine assumes, but does not check, that all of the entries in\n   lens[] are in the range 0..MAXBITS.  The caller must assure this.\n   1..MAXBITS is interpreted as that code length.  zero means that that\n   symbol does not occur in this code.\n\n   The codes are sorted by computing a count of codes for each length,\n   creating from that a table of starting indices for each length in the\n   sorted table, and then entering the symbols in order in the sorted\n   table.  The sorted table is work[], with that space being provided by\n   the caller.\n\n   The length counts are used for other purposes as well, i.e. finding\n   the minimum and maximum length codes, determining if there are any\n   codes at all, checking for a valid set of lengths, and looking ahead\n   at length counts to determine sub-table sizes when building the\n   decoding tables.\n   */\n\n  /* accumulate lengths for codes (assumes lens[] all in 0..MAXBITS) */\n  for (len = 0; len <= MAXBITS; len++) {\n    count[len] = 0;\n  }\n  for (sym = 0; sym < codes; sym++) {\n    count[lens[lens_index + sym]]++;\n  }\n\n  /* bound code lengths, force root to be within code lengths */\n  root = bits;\n  for (max = MAXBITS; max >= 1; max--) {\n    if (count[max] !== 0) { break; }\n  }\n  if (root > max) {\n    root = max;\n  }\n  if (max === 0) {                     /* no symbols to code at all */\n    //table.op[opts.table_index] = 64;  //here.op = (var char)64;    /* invalid code marker */\n    //table.bits[opts.table_index] = 1;   //here.bits = (var char)1;\n    //table.val[opts.table_index++] = 0;   //here.val = (var short)0;\n    table[table_index++] = (1 << 24) | (64 << 16) | 0;\n\n\n    //table.op[opts.table_index] = 64;\n    //table.bits[opts.table_index] = 1;\n    //table.val[opts.table_index++] = 0;\n    table[table_index++] = (1 << 24) | (64 << 16) | 0;\n\n    opts.bits = 1;\n    return 0;     /* no symbols, but wait for decoding to report error */\n  }\n  for (min = 1; min < max; min++) {\n    if (count[min] !== 0) { break; }\n  }\n  if (root < min) {\n    root = min;\n  }\n\n  /* check for an over-subscribed or incomplete set of lengths */\n  left = 1;\n  for (len = 1; len <= MAXBITS; len++) {\n    left <<= 1;\n    left -= count[len];\n    if (left < 0) {\n      return -1;\n    }        /* over-subscribed */\n  }\n  if (left > 0 && (type === CODES || max !== 1)) {\n    return -1;                      /* incomplete set */\n  }\n\n  /* generate offsets into symbol table for each length for sorting */\n  offs[1] = 0;\n  for (len = 1; len < MAXBITS; len++) {\n    offs[len + 1] = offs[len] + count[len];\n  }\n\n  /* sort symbols by length, by symbol order within each length */\n  for (sym = 0; sym < codes; sym++) {\n    if (lens[lens_index + sym] !== 0) {\n      work[offs[lens[lens_index + sym]]++] = sym;\n    }\n  }\n\n  /*\n   Create and fill in decoding tables.  In this loop, the table being\n   filled is at next and has curr index bits.  The code being used is huff\n   with length len.  That code is converted to an index by dropping drop\n   bits off of the bottom.  For codes where len is less than drop + curr,\n   those top drop + curr - len bits are incremented through all values to\n   fill the table with replicated entries.\n\n   root is the number of index bits for the root table.  When len exceeds\n   root, sub-tables are created pointed to by the root entry with an index\n   of the low root bits of huff.  This is saved in low to check for when a\n   new sub-table should be started.  drop is zero when the root table is\n   being filled, and drop is root when sub-tables are being filled.\n\n   When a new sub-table is needed, it is necessary to look ahead in the\n   code lengths to determine what size sub-table is needed.  The length\n   counts are used for this, and so count[] is decremented as codes are\n   entered in the tables.\n\n   used keeps track of how many table entries have been allocated from the\n   provided *table space.  It is checked for LENS and DIST tables against\n   the constants ENOUGH_LENS and ENOUGH_DISTS to guard against changes in\n   the initial root table size constants.  See the comments in inftrees.h\n   for more information.\n\n   sym increments through all symbols, and the loop terminates when\n   all codes of length max, i.e. all codes, have been processed.  This\n   routine permits incomplete codes, so another loop after this one fills\n   in the rest of the decoding tables with invalid code markers.\n   */\n\n  /* set up for code type */\n  // poor man optimization - use if-else instead of switch,\n  // to avoid deopts in old v8\n  if (type === CODES) {\n    base = extra = work;    /* dummy value--not used */\n    end = 19;\n\n  } else if (type === LENS) {\n    base = lbase;\n    base_index -= 257;\n    extra = lext;\n    extra_index -= 257;\n    end = 256;\n\n  } else {                    /* DISTS */\n    base = dbase;\n    extra = dext;\n    end = -1;\n  }\n\n  /* initialize opts for loop */\n  huff = 0;                   /* starting code */\n  sym = 0;                    /* starting code symbol */\n  len = min;                  /* starting code length */\n  next = table_index;              /* current table to fill in */\n  curr = root;                /* current table index bits */\n  drop = 0;                   /* current bits to drop from code for index */\n  low = -1;                   /* trigger new sub-table when len > root */\n  used = 1 << root;          /* use root table entries */\n  mask = used - 1;            /* mask for comparing low */\n\n  /* check available table space */\n  if ((type === LENS && used > ENOUGH_LENS) ||\n    (type === DISTS && used > ENOUGH_DISTS)) {\n    return 1;\n  }\n\n  /* process all codes and make table entries */\n  for (;;) {\n    /* create table entry */\n    here_bits = len - drop;\n    if (work[sym] < end) {\n      here_op = 0;\n      here_val = work[sym];\n    }\n    else if (work[sym] > end) {\n      here_op = extra[extra_index + work[sym]];\n      here_val = base[base_index + work[sym]];\n    }\n    else {\n      here_op = 32 + 64;         /* end of block */\n      here_val = 0;\n    }\n\n    /* replicate for those indices with low len bits equal to huff */\n    incr = 1 << (len - drop);\n    fill = 1 << curr;\n    min = fill;                 /* save offset to next table */\n    do {\n      fill -= incr;\n      table[next + (huff >> drop) + fill] = (here_bits << 24) | (here_op << 16) | here_val |0;\n    } while (fill !== 0);\n\n    /* backwards increment the len-bit code huff */\n    incr = 1 << (len - 1);\n    while (huff & incr) {\n      incr >>= 1;\n    }\n    if (incr !== 0) {\n      huff &= incr - 1;\n      huff += incr;\n    } else {\n      huff = 0;\n    }\n\n    /* go to next symbol, update count, len */\n    sym++;\n    if (--count[len] === 0) {\n      if (len === max) { break; }\n      len = lens[lens_index + work[sym]];\n    }\n\n    /* create new sub-table if needed */\n    if (len > root && (huff & mask) !== low) {\n      /* if first time, transition to sub-tables */\n      if (drop === 0) {\n        drop = root;\n      }\n\n      /* increment past last table */\n      next += min;            /* here min is 1 << curr */\n\n      /* determine length of next table */\n      curr = len - drop;\n      left = 1 << curr;\n      while (curr + drop < max) {\n        left -= count[curr + drop];\n        if (left <= 0) { break; }\n        curr++;\n        left <<= 1;\n      }\n\n      /* check for enough space */\n      used += 1 << curr;\n      if ((type === LENS && used > ENOUGH_LENS) ||\n        (type === DISTS && used > ENOUGH_DISTS)) {\n        return 1;\n      }\n\n      /* point entry in root table to sub-table */\n      low = huff & mask;\n      /*table.op[low] = curr;\n      table.bits[low] = root;\n      table.val[low] = next - opts.table_index;*/\n      table[low] = (root << 24) | (curr << 16) | (next - table_index) |0;\n    }\n  }\n\n  /* fill in remaining table entry if code is incomplete (guaranteed to have\n   at most one remaining entry, since if the code is incomplete, the\n   maximum code length that was allowed to get this far is one bit) */\n  if (huff !== 0) {\n    //table.op[next + huff] = 64;            /* invalid code marker */\n    //table.bits[next + huff] = len - drop;\n    //table.val[next + huff] = 0;\n    table[next + huff] = ((len - drop) << 24) | (64 << 16) |0;\n  }\n\n  /* set return parameters */\n  //opts.table_index += used;\n  opts.bits = root;\n  return 0;\n};\n"
  },
  {
    "path": "vendor/pako/lib/zlib/messages.js",
    "content": "export default {\n  2:      'need dictionary',     /* Z_NEED_DICT       2  */\n  1:      'stream end',          /* Z_STREAM_END      1  */\n  0:      '',                    /* Z_OK              0  */\n  '-1':   'file error',          /* Z_ERRNO         (-1) */\n  '-2':   'stream error',        /* Z_STREAM_ERROR  (-2) */\n  '-3':   'data error',          /* Z_DATA_ERROR    (-3) */\n  '-4':   'insufficient memory', /* Z_MEM_ERROR     (-4) */\n  '-5':   'buffer error',        /* Z_BUF_ERROR     (-5) */\n  '-6':   'incompatible version' /* Z_VERSION_ERROR (-6) */\n};\n"
  },
  {
    "path": "vendor/pako/lib/zlib/trees.js",
    "content": "import * as utils from \"../utils/common.js\";\n\n/* Public constants ==========================================================*/\n/* ===========================================================================*/\n\n\n//var Z_FILTERED          = 1;\n//var Z_HUFFMAN_ONLY      = 2;\n//var Z_RLE               = 3;\nvar Z_FIXED               = 4;\n//var Z_DEFAULT_STRATEGY  = 0;\n\n/* Possible values of the data_type field (though see inflate()) */\nvar Z_BINARY              = 0;\nvar Z_TEXT                = 1;\n//var Z_ASCII             = 1; // = Z_TEXT\nvar Z_UNKNOWN             = 2;\n\n/*============================================================================*/\n\n\nfunction zero(buf) { var len = buf.length; while (--len >= 0) { buf[len] = 0; } }\n\n// From zutil.h\n\nvar STORED_BLOCK = 0;\nvar STATIC_TREES = 1;\nvar DYN_TREES    = 2;\n/* The three kinds of block type */\n\nvar MIN_MATCH    = 3;\nvar MAX_MATCH    = 258;\n/* The minimum and maximum match lengths */\n\n// From deflate.h\n/* ===========================================================================\n * Internal compression state.\n */\n\nvar LENGTH_CODES  = 29;\n/* number of length codes, not counting the special END_BLOCK code */\n\nvar LITERALS      = 256;\n/* number of literal bytes 0..255 */\n\nvar L_CODES       = LITERALS + 1 + LENGTH_CODES;\n/* number of Literal or Length codes, including the END_BLOCK code */\n\nvar D_CODES       = 30;\n/* number of distance codes */\n\nvar BL_CODES      = 19;\n/* number of codes used to transfer the bit lengths */\n\nvar HEAP_SIZE     = 2 * L_CODES + 1;\n/* maximum heap size */\n\nvar MAX_BITS      = 15;\n/* All codes must not exceed MAX_BITS bits */\n\nvar Buf_size      = 16;\n/* size of bit buffer in bi_buf */\n\n\n/* ===========================================================================\n * Constants\n */\n\nvar MAX_BL_BITS = 7;\n/* Bit length codes must not exceed MAX_BL_BITS bits */\n\nvar END_BLOCK   = 256;\n/* end of block literal code */\n\nvar REP_3_6     = 16;\n/* repeat previous bit length 3-6 times (2 bits of repeat count) */\n\nvar REPZ_3_10   = 17;\n/* repeat a zero length 3-10 times  (3 bits of repeat count) */\n\nvar REPZ_11_138 = 18;\n/* repeat a zero length 11-138 times  (7 bits of repeat count) */\n\n/* eslint-disable comma-spacing,array-bracket-spacing */\nvar extra_lbits =   /* extra bits for each length code */\n  [0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0];\n\nvar extra_dbits =   /* extra bits for each distance code */\n  [0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13];\n\nvar extra_blbits =  /* extra bits for each bit length code */\n  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7];\n\nvar bl_order =\n  [16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15];\n/* eslint-enable comma-spacing,array-bracket-spacing */\n\n/* The lengths of the bit length codes are sent in order of decreasing\n * probability, to avoid transmitting the lengths for unused bit length codes.\n */\n\n/* ===========================================================================\n * Local data. These are initialized only once.\n */\n\n// We pre-fill arrays with 0 to avoid uninitialized gaps\n\nvar DIST_CODE_LEN = 512; /* see definition of array dist_code below */\n\n// !!!! Use flat array insdead of structure, Freq = i*2, Len = i*2+1\nvar static_ltree  = new Array((L_CODES + 2) * 2);\nzero(static_ltree);\n/* The static literal tree. Since the bit lengths are imposed, there is no\n * need for the L_CODES extra codes used during heap construction. However\n * The codes 286 and 287 are needed to build a canonical tree (see _tr_init\n * below).\n */\n\nvar static_dtree  = new Array(D_CODES * 2);\nzero(static_dtree);\n/* The static distance tree. (Actually a trivial tree since all codes use\n * 5 bits.)\n */\n\nvar _dist_code    = new Array(DIST_CODE_LEN);\nzero(_dist_code);\n/* Distance codes. The first 256 values correspond to the distances\n * 3 .. 258, the last 256 values correspond to the top 8 bits of\n * the 15 bit distances.\n */\n\nvar _length_code  = new Array(MAX_MATCH - MIN_MATCH + 1);\nzero(_length_code);\n/* length code for each normalized match length (0 == MIN_MATCH) */\n\nvar base_length   = new Array(LENGTH_CODES);\nzero(base_length);\n/* First normalized length for each code (0 = MIN_MATCH) */\n\nvar base_dist     = new Array(D_CODES);\nzero(base_dist);\n/* First normalized distance for each code (0 = distance of 1) */\n\n\nfunction StaticTreeDesc(static_tree, extra_bits, extra_base, elems, max_length) {\n\n  this.static_tree  = static_tree;  /* static tree or NULL */\n  this.extra_bits   = extra_bits;   /* extra bits for each code or NULL */\n  this.extra_base   = extra_base;   /* base index for extra_bits */\n  this.elems        = elems;        /* max number of elements in the tree */\n  this.max_length   = max_length;   /* max bit length for the codes */\n\n  // show if `static_tree` has data or dummy - needed for monomorphic objects\n  this.has_stree    = static_tree && static_tree.length;\n}\n\n\nvar static_l_desc;\nvar static_d_desc;\nvar static_bl_desc;\n\n\nfunction TreeDesc(dyn_tree, stat_desc) {\n  this.dyn_tree = dyn_tree;     /* the dynamic tree */\n  this.max_code = 0;            /* largest code with non zero frequency */\n  this.stat_desc = stat_desc;   /* the corresponding static tree */\n}\n\n\n\nfunction d_code(dist) {\n  return dist < 256 ? _dist_code[dist] : _dist_code[256 + (dist >>> 7)];\n}\n\n\n/* ===========================================================================\n * Output a short LSB first on the stream.\n * IN assertion: there is enough room in pendingBuf.\n */\nfunction put_short(s, w) {\n//    put_byte(s, (uch)((w) & 0xff));\n//    put_byte(s, (uch)((ush)(w) >> 8));\n  s.pending_buf[s.pending++] = (w) & 0xff;\n  s.pending_buf[s.pending++] = (w >>> 8) & 0xff;\n}\n\n\n/* ===========================================================================\n * Send a value on a given number of bits.\n * IN assertion: length <= 16 and value fits in length bits.\n */\nfunction send_bits(s, value, length) {\n  if (s.bi_valid > (Buf_size - length)) {\n    s.bi_buf |= (value << s.bi_valid) & 0xffff;\n    put_short(s, s.bi_buf);\n    s.bi_buf = value >> (Buf_size - s.bi_valid);\n    s.bi_valid += length - Buf_size;\n  } else {\n    s.bi_buf |= (value << s.bi_valid) & 0xffff;\n    s.bi_valid += length;\n  }\n}\n\n\nfunction send_code(s, c, tree) {\n  send_bits(s, tree[c * 2]/*.Code*/, tree[c * 2 + 1]/*.Len*/);\n}\n\n\n/* ===========================================================================\n * Reverse the first len bits of a code, using straightforward code (a faster\n * method would use a table)\n * IN assertion: 1 <= len <= 15\n */\nfunction bi_reverse(code, len) {\n  var res = 0;\n  do {\n    res |= code & 1;\n    code >>>= 1;\n    res <<= 1;\n  } while (--len > 0);\n  return res >>> 1;\n}\n\n\n/* ===========================================================================\n * Flush the bit buffer, keeping at most 7 bits in it.\n */\nfunction bi_flush(s) {\n  if (s.bi_valid === 16) {\n    put_short(s, s.bi_buf);\n    s.bi_buf = 0;\n    s.bi_valid = 0;\n\n  } else if (s.bi_valid >= 8) {\n    s.pending_buf[s.pending++] = s.bi_buf & 0xff;\n    s.bi_buf >>= 8;\n    s.bi_valid -= 8;\n  }\n}\n\n\n/* ===========================================================================\n * Compute the optimal bit lengths for a tree and update the total bit length\n * for the current block.\n * IN assertion: the fields freq and dad are set, heap[heap_max] and\n *    above are the tree nodes sorted by increasing frequency.\n * OUT assertions: the field len is set to the optimal bit length, the\n *     array bl_count contains the frequencies for each bit length.\n *     The length opt_len is updated; static_len is also updated if stree is\n *     not null.\n */\nfunction gen_bitlen(s, desc)\n//    deflate_state *s;\n//    tree_desc *desc;    /* the tree descriptor */\n{\n  var tree            = desc.dyn_tree;\n  var max_code        = desc.max_code;\n  var stree           = desc.stat_desc.static_tree;\n  var has_stree       = desc.stat_desc.has_stree;\n  var extra           = desc.stat_desc.extra_bits;\n  var base            = desc.stat_desc.extra_base;\n  var max_length      = desc.stat_desc.max_length;\n  var h;              /* heap index */\n  var n, m;           /* iterate over the tree elements */\n  var bits;           /* bit length */\n  var xbits;          /* extra bits */\n  var f;              /* frequency */\n  var overflow = 0;   /* number of elements with bit length too large */\n\n  for (bits = 0; bits <= MAX_BITS; bits++) {\n    s.bl_count[bits] = 0;\n  }\n\n  /* In a first pass, compute the optimal bit lengths (which may\n   * overflow in the case of the bit length tree).\n   */\n  tree[s.heap[s.heap_max] * 2 + 1]/*.Len*/ = 0; /* root of the heap */\n\n  for (h = s.heap_max + 1; h < HEAP_SIZE; h++) {\n    n = s.heap[h];\n    bits = tree[tree[n * 2 + 1]/*.Dad*/ * 2 + 1]/*.Len*/ + 1;\n    if (bits > max_length) {\n      bits = max_length;\n      overflow++;\n    }\n    tree[n * 2 + 1]/*.Len*/ = bits;\n    /* We overwrite tree[n].Dad which is no longer needed */\n\n    if (n > max_code) { continue; } /* not a leaf node */\n\n    s.bl_count[bits]++;\n    xbits = 0;\n    if (n >= base) {\n      xbits = extra[n - base];\n    }\n    f = tree[n * 2]/*.Freq*/;\n    s.opt_len += f * (bits + xbits);\n    if (has_stree) {\n      s.static_len += f * (stree[n * 2 + 1]/*.Len*/ + xbits);\n    }\n  }\n  if (overflow === 0) { return; }\n\n  // Trace((stderr,\"\\nbit length overflow\\n\"));\n  /* This happens for example on obj2 and pic of the Calgary corpus */\n\n  /* Find the first bit length which could increase: */\n  do {\n    bits = max_length - 1;\n    while (s.bl_count[bits] === 0) { bits--; }\n    s.bl_count[bits]--;      /* move one leaf down the tree */\n    s.bl_count[bits + 1] += 2; /* move one overflow item as its brother */\n    s.bl_count[max_length]--;\n    /* The brother of the overflow item also moves one step up,\n     * but this does not affect bl_count[max_length]\n     */\n    overflow -= 2;\n  } while (overflow > 0);\n\n  /* Now recompute all bit lengths, scanning in increasing frequency.\n   * h is still equal to HEAP_SIZE. (It is simpler to reconstruct all\n   * lengths instead of fixing only the wrong ones. This idea is taken\n   * from 'ar' written by Haruhiko Okumura.)\n   */\n  for (bits = max_length; bits !== 0; bits--) {\n    n = s.bl_count[bits];\n    while (n !== 0) {\n      m = s.heap[--h];\n      if (m > max_code) { continue; }\n      if (tree[m * 2 + 1]/*.Len*/ !== bits) {\n        // Trace((stderr,\"code %d bits %d->%d\\n\", m, tree[m].Len, bits));\n        s.opt_len += (bits - tree[m * 2 + 1]/*.Len*/) * tree[m * 2]/*.Freq*/;\n        tree[m * 2 + 1]/*.Len*/ = bits;\n      }\n      n--;\n    }\n  }\n}\n\n\n/* ===========================================================================\n * Generate the codes for a given tree and bit counts (which need not be\n * optimal).\n * IN assertion: the array bl_count contains the bit length statistics for\n * the given tree and the field len is set for all tree elements.\n * OUT assertion: the field code is set for all tree elements of non\n *     zero code length.\n */\nfunction gen_codes(tree, max_code, bl_count)\n//    ct_data *tree;             /* the tree to decorate */\n//    int max_code;              /* largest code with non zero frequency */\n//    ushf *bl_count;            /* number of codes at each bit length */\n{\n  var next_code = new Array(MAX_BITS + 1); /* next code value for each bit length */\n  var code = 0;              /* running code value */\n  var bits;                  /* bit index */\n  var n;                     /* code index */\n\n  /* The distribution counts are first used to generate the code values\n   * without bit reversal.\n   */\n  for (bits = 1; bits <= MAX_BITS; bits++) {\n    next_code[bits] = code = (code + bl_count[bits - 1]) << 1;\n  }\n  /* Check that the bit counts in bl_count are consistent. The last code\n   * must be all ones.\n   */\n  //Assert (code + bl_count[MAX_BITS]-1 == (1<<MAX_BITS)-1,\n  //        \"inconsistent bit counts\");\n  //Tracev((stderr,\"\\ngen_codes: max_code %d \", max_code));\n\n  for (n = 0;  n <= max_code; n++) {\n    var len = tree[n * 2 + 1]/*.Len*/;\n    if (len === 0) { continue; }\n    /* Now reverse the bits */\n    tree[n * 2]/*.Code*/ = bi_reverse(next_code[len]++, len);\n\n    //Tracecv(tree != static_ltree, (stderr,\"\\nn %3d %c l %2d c %4x (%x) \",\n    //     n, (isgraph(n) ? n : ' '), len, tree[n].Code, next_code[len]-1));\n  }\n}\n\n\n/* ===========================================================================\n * Initialize the various 'constant' tables.\n */\nfunction tr_static_init() {\n  var n;        /* iterates over tree elements */\n  var bits;     /* bit counter */\n  var length;   /* length value */\n  var code;     /* code value */\n  var dist;     /* distance index */\n  var bl_count = new Array(MAX_BITS + 1);\n  /* number of codes at each bit length for an optimal tree */\n\n  // do check in _tr_init()\n  //if (static_init_done) return;\n\n  /* For some embedded targets, global variables are not initialized: */\n/*#ifdef NO_INIT_GLOBAL_POINTERS\n  static_l_desc.static_tree = static_ltree;\n  static_l_desc.extra_bits = extra_lbits;\n  static_d_desc.static_tree = static_dtree;\n  static_d_desc.extra_bits = extra_dbits;\n  static_bl_desc.extra_bits = extra_blbits;\n#endif*/\n\n  /* Initialize the mapping length (0..255) -> length code (0..28) */\n  length = 0;\n  for (code = 0; code < LENGTH_CODES - 1; code++) {\n    base_length[code] = length;\n    for (n = 0; n < (1 << extra_lbits[code]); n++) {\n      _length_code[length++] = code;\n    }\n  }\n  //Assert (length == 256, \"tr_static_init: length != 256\");\n  /* Note that the length 255 (match length 258) can be represented\n   * in two different ways: code 284 + 5 bits or code 285, so we\n   * overwrite length_code[255] to use the best encoding:\n   */\n  _length_code[length - 1] = code;\n\n  /* Initialize the mapping dist (0..32K) -> dist code (0..29) */\n  dist = 0;\n  for (code = 0; code < 16; code++) {\n    base_dist[code] = dist;\n    for (n = 0; n < (1 << extra_dbits[code]); n++) {\n      _dist_code[dist++] = code;\n    }\n  }\n  //Assert (dist == 256, \"tr_static_init: dist != 256\");\n  dist >>= 7; /* from now on, all distances are divided by 128 */\n  for (; code < D_CODES; code++) {\n    base_dist[code] = dist << 7;\n    for (n = 0; n < (1 << (extra_dbits[code] - 7)); n++) {\n      _dist_code[256 + dist++] = code;\n    }\n  }\n  //Assert (dist == 256, \"tr_static_init: 256+dist != 512\");\n\n  /* Construct the codes of the static literal tree */\n  for (bits = 0; bits <= MAX_BITS; bits++) {\n    bl_count[bits] = 0;\n  }\n\n  n = 0;\n  while (n <= 143) {\n    static_ltree[n * 2 + 1]/*.Len*/ = 8;\n    n++;\n    bl_count[8]++;\n  }\n  while (n <= 255) {\n    static_ltree[n * 2 + 1]/*.Len*/ = 9;\n    n++;\n    bl_count[9]++;\n  }\n  while (n <= 279) {\n    static_ltree[n * 2 + 1]/*.Len*/ = 7;\n    n++;\n    bl_count[7]++;\n  }\n  while (n <= 287) {\n    static_ltree[n * 2 + 1]/*.Len*/ = 8;\n    n++;\n    bl_count[8]++;\n  }\n  /* Codes 286 and 287 do not exist, but we must include them in the\n   * tree construction to get a canonical Huffman tree (longest code\n   * all ones)\n   */\n  gen_codes(static_ltree, L_CODES + 1, bl_count);\n\n  /* The static distance tree is trivial: */\n  for (n = 0; n < D_CODES; n++) {\n    static_dtree[n * 2 + 1]/*.Len*/ = 5;\n    static_dtree[n * 2]/*.Code*/ = bi_reverse(n, 5);\n  }\n\n  // Now data ready and we can init static trees\n  static_l_desc = new StaticTreeDesc(static_ltree, extra_lbits, LITERALS + 1, L_CODES, MAX_BITS);\n  static_d_desc = new StaticTreeDesc(static_dtree, extra_dbits, 0,          D_CODES, MAX_BITS);\n  static_bl_desc = new StaticTreeDesc(new Array(0), extra_blbits, 0,         BL_CODES, MAX_BL_BITS);\n\n  //static_init_done = true;\n}\n\n\n/* ===========================================================================\n * Initialize a new block.\n */\nfunction init_block(s) {\n  var n; /* iterates over tree elements */\n\n  /* Initialize the trees. */\n  for (n = 0; n < L_CODES;  n++) { s.dyn_ltree[n * 2]/*.Freq*/ = 0; }\n  for (n = 0; n < D_CODES;  n++) { s.dyn_dtree[n * 2]/*.Freq*/ = 0; }\n  for (n = 0; n < BL_CODES; n++) { s.bl_tree[n * 2]/*.Freq*/ = 0; }\n\n  s.dyn_ltree[END_BLOCK * 2]/*.Freq*/ = 1;\n  s.opt_len = s.static_len = 0;\n  s.last_lit = s.matches = 0;\n}\n\n\n/* ===========================================================================\n * Flush the bit buffer and align the output on a byte boundary\n */\nfunction bi_windup(s)\n{\n  if (s.bi_valid > 8) {\n    put_short(s, s.bi_buf);\n  } else if (s.bi_valid > 0) {\n    //put_byte(s, (Byte)s->bi_buf);\n    s.pending_buf[s.pending++] = s.bi_buf;\n  }\n  s.bi_buf = 0;\n  s.bi_valid = 0;\n}\n\n/* ===========================================================================\n * Copy a stored block, storing first the length and its\n * one's complement if requested.\n */\nfunction copy_block(s, buf, len, header)\n//DeflateState *s;\n//charf    *buf;    /* the input data */\n//unsigned len;     /* its length */\n//int      header;  /* true if block header must be written */\n{\n  bi_windup(s);        /* align on byte boundary */\n\n  if (header) {\n    put_short(s, len);\n    put_short(s, ~len);\n  }\n//  while (len--) {\n//    put_byte(s, *buf++);\n//  }\n  utils.arraySet(s.pending_buf, s.window, buf, len, s.pending);\n  s.pending += len;\n}\n\n/* ===========================================================================\n * Compares to subtrees, using the tree depth as tie breaker when\n * the subtrees have equal frequency. This minimizes the worst case length.\n */\nfunction smaller(tree, n, m, depth) {\n  var _n2 = n * 2;\n  var _m2 = m * 2;\n  return (tree[_n2]/*.Freq*/ < tree[_m2]/*.Freq*/ ||\n         (tree[_n2]/*.Freq*/ === tree[_m2]/*.Freq*/ && depth[n] <= depth[m]));\n}\n\n/* ===========================================================================\n * Restore the heap property by moving down the tree starting at node k,\n * exchanging a node with the smallest of its two sons if necessary, stopping\n * when the heap property is re-established (each father smaller than its\n * two sons).\n */\nfunction pqdownheap(s, tree, k)\n//    deflate_state *s;\n//    ct_data *tree;  /* the tree to restore */\n//    int k;               /* node to move down */\n{\n  var v = s.heap[k];\n  var j = k << 1;  /* left son of k */\n  while (j <= s.heap_len) {\n    /* Set j to the smallest of the two sons: */\n    if (j < s.heap_len &&\n      smaller(tree, s.heap[j + 1], s.heap[j], s.depth)) {\n      j++;\n    }\n    /* Exit if v is smaller than both sons */\n    if (smaller(tree, v, s.heap[j], s.depth)) { break; }\n\n    /* Exchange v with the smallest son */\n    s.heap[k] = s.heap[j];\n    k = j;\n\n    /* And continue down the tree, setting j to the left son of k */\n    j <<= 1;\n  }\n  s.heap[k] = v;\n}\n\n\n// inlined manually\n// var SMALLEST = 1;\n\n/* ===========================================================================\n * Send the block data compressed using the given Huffman trees\n */\nfunction compress_block(s, ltree, dtree)\n//    deflate_state *s;\n//    const ct_data *ltree; /* literal tree */\n//    const ct_data *dtree; /* distance tree */\n{\n  var dist;           /* distance of matched string */\n  var lc;             /* match length or unmatched char (if dist == 0) */\n  var lx = 0;         /* running index in l_buf */\n  var code;           /* the code to send */\n  var extra;          /* number of extra bits to send */\n\n  if (s.last_lit !== 0) {\n    do {\n      dist = (s.pending_buf[s.d_buf + lx * 2] << 8) | (s.pending_buf[s.d_buf + lx * 2 + 1]);\n      lc = s.pending_buf[s.l_buf + lx];\n      lx++;\n\n      if (dist === 0) {\n        send_code(s, lc, ltree); /* send a literal byte */\n        //Tracecv(isgraph(lc), (stderr,\" '%c' \", lc));\n      } else {\n        /* Here, lc is the match length - MIN_MATCH */\n        code = _length_code[lc];\n        send_code(s, code + LITERALS + 1, ltree); /* send the length code */\n        extra = extra_lbits[code];\n        if (extra !== 0) {\n          lc -= base_length[code];\n          send_bits(s, lc, extra);       /* send the extra length bits */\n        }\n        dist--; /* dist is now the match distance - 1 */\n        code = d_code(dist);\n        //Assert (code < D_CODES, \"bad d_code\");\n\n        send_code(s, code, dtree);       /* send the distance code */\n        extra = extra_dbits[code];\n        if (extra !== 0) {\n          dist -= base_dist[code];\n          send_bits(s, dist, extra);   /* send the extra distance bits */\n        }\n      } /* literal or match pair ? */\n\n      /* Check that the overlay between pending_buf and d_buf+l_buf is ok: */\n      //Assert((uInt)(s->pending) < s->lit_bufsize + 2*lx,\n      //       \"pendingBuf overflow\");\n\n    } while (lx < s.last_lit);\n  }\n\n  send_code(s, END_BLOCK, ltree);\n}\n\n\n/* ===========================================================================\n * Construct one Huffman tree and assigns the code bit strings and lengths.\n * Update the total bit length for the current block.\n * IN assertion: the field freq is set for all tree elements.\n * OUT assertions: the fields len and code are set to the optimal bit length\n *     and corresponding code. The length opt_len is updated; static_len is\n *     also updated if stree is not null. The field max_code is set.\n */\nfunction build_tree(s, desc)\n//    deflate_state *s;\n//    tree_desc *desc; /* the tree descriptor */\n{\n  var tree     = desc.dyn_tree;\n  var stree    = desc.stat_desc.static_tree;\n  var has_stree = desc.stat_desc.has_stree;\n  var elems    = desc.stat_desc.elems;\n  var n, m;          /* iterate over heap elements */\n  var max_code = -1; /* largest code with non zero frequency */\n  var node;          /* new node being created */\n\n  /* Construct the initial heap, with least frequent element in\n   * heap[SMALLEST]. The sons of heap[n] are heap[2*n] and heap[2*n+1].\n   * heap[0] is not used.\n   */\n  s.heap_len = 0;\n  s.heap_max = HEAP_SIZE;\n\n  for (n = 0; n < elems; n++) {\n    if (tree[n * 2]/*.Freq*/ !== 0) {\n      s.heap[++s.heap_len] = max_code = n;\n      s.depth[n] = 0;\n\n    } else {\n      tree[n * 2 + 1]/*.Len*/ = 0;\n    }\n  }\n\n  /* The pkzip format requires that at least one distance code exists,\n   * and that at least one bit should be sent even if there is only one\n   * possible code. So to avoid special checks later on we force at least\n   * two codes of non zero frequency.\n   */\n  while (s.heap_len < 2) {\n    node = s.heap[++s.heap_len] = (max_code < 2 ? ++max_code : 0);\n    tree[node * 2]/*.Freq*/ = 1;\n    s.depth[node] = 0;\n    s.opt_len--;\n\n    if (has_stree) {\n      s.static_len -= stree[node * 2 + 1]/*.Len*/;\n    }\n    /* node is 0 or 1 so it does not have extra bits */\n  }\n  desc.max_code = max_code;\n\n  /* The elements heap[heap_len/2+1 .. heap_len] are leaves of the tree,\n   * establish sub-heaps of increasing lengths:\n   */\n  for (n = (s.heap_len >> 1/*int /2*/); n >= 1; n--) { pqdownheap(s, tree, n); }\n\n  /* Construct the Huffman tree by repeatedly combining the least two\n   * frequent nodes.\n   */\n  node = elems;              /* next internal node of the tree */\n  do {\n    //pqremove(s, tree, n);  /* n = node of least frequency */\n    /*** pqremove ***/\n    n = s.heap[1/*SMALLEST*/];\n    s.heap[1/*SMALLEST*/] = s.heap[s.heap_len--];\n    pqdownheap(s, tree, 1/*SMALLEST*/);\n    /***/\n\n    m = s.heap[1/*SMALLEST*/]; /* m = node of next least frequency */\n\n    s.heap[--s.heap_max] = n; /* keep the nodes sorted by frequency */\n    s.heap[--s.heap_max] = m;\n\n    /* Create a new node father of n and m */\n    tree[node * 2]/*.Freq*/ = tree[n * 2]/*.Freq*/ + tree[m * 2]/*.Freq*/;\n    s.depth[node] = (s.depth[n] >= s.depth[m] ? s.depth[n] : s.depth[m]) + 1;\n    tree[n * 2 + 1]/*.Dad*/ = tree[m * 2 + 1]/*.Dad*/ = node;\n\n    /* and insert the new node in the heap */\n    s.heap[1/*SMALLEST*/] = node++;\n    pqdownheap(s, tree, 1/*SMALLEST*/);\n\n  } while (s.heap_len >= 2);\n\n  s.heap[--s.heap_max] = s.heap[1/*SMALLEST*/];\n\n  /* At this point, the fields freq and dad are set. We can now\n   * generate the bit lengths.\n   */\n  gen_bitlen(s, desc);\n\n  /* The field len is now set, we can generate the bit codes */\n  gen_codes(tree, max_code, s.bl_count);\n}\n\n\n/* ===========================================================================\n * Scan a literal or distance tree to determine the frequencies of the codes\n * in the bit length tree.\n */\nfunction scan_tree(s, tree, max_code)\n//    deflate_state *s;\n//    ct_data *tree;   /* the tree to be scanned */\n//    int max_code;    /* and its largest code of non zero frequency */\n{\n  var n;                     /* iterates over all tree elements */\n  var prevlen = -1;          /* last emitted length */\n  var curlen;                /* length of current code */\n\n  var nextlen = tree[0 * 2 + 1]/*.Len*/; /* length of next code */\n\n  var count = 0;             /* repeat count of the current code */\n  var max_count = 7;         /* max repeat count */\n  var min_count = 4;         /* min repeat count */\n\n  if (nextlen === 0) {\n    max_count = 138;\n    min_count = 3;\n  }\n  tree[(max_code + 1) * 2 + 1]/*.Len*/ = 0xffff; /* guard */\n\n  for (n = 0; n <= max_code; n++) {\n    curlen = nextlen;\n    nextlen = tree[(n + 1) * 2 + 1]/*.Len*/;\n\n    if (++count < max_count && curlen === nextlen) {\n      continue;\n\n    } else if (count < min_count) {\n      s.bl_tree[curlen * 2]/*.Freq*/ += count;\n\n    } else if (curlen !== 0) {\n\n      if (curlen !== prevlen) { s.bl_tree[curlen * 2]/*.Freq*/++; }\n      s.bl_tree[REP_3_6 * 2]/*.Freq*/++;\n\n    } else if (count <= 10) {\n      s.bl_tree[REPZ_3_10 * 2]/*.Freq*/++;\n\n    } else {\n      s.bl_tree[REPZ_11_138 * 2]/*.Freq*/++;\n    }\n\n    count = 0;\n    prevlen = curlen;\n\n    if (nextlen === 0) {\n      max_count = 138;\n      min_count = 3;\n\n    } else if (curlen === nextlen) {\n      max_count = 6;\n      min_count = 3;\n\n    } else {\n      max_count = 7;\n      min_count = 4;\n    }\n  }\n}\n\n\n/* ===========================================================================\n * Send a literal or distance tree in compressed form, using the codes in\n * bl_tree.\n */\nfunction send_tree(s, tree, max_code)\n//    deflate_state *s;\n//    ct_data *tree; /* the tree to be scanned */\n//    int max_code;       /* and its largest code of non zero frequency */\n{\n  var n;                     /* iterates over all tree elements */\n  var prevlen = -1;          /* last emitted length */\n  var curlen;                /* length of current code */\n\n  var nextlen = tree[0 * 2 + 1]/*.Len*/; /* length of next code */\n\n  var count = 0;             /* repeat count of the current code */\n  var max_count = 7;         /* max repeat count */\n  var min_count = 4;         /* min repeat count */\n\n  /* tree[max_code+1].Len = -1; */  /* guard already set */\n  if (nextlen === 0) {\n    max_count = 138;\n    min_count = 3;\n  }\n\n  for (n = 0; n <= max_code; n++) {\n    curlen = nextlen;\n    nextlen = tree[(n + 1) * 2 + 1]/*.Len*/;\n\n    if (++count < max_count && curlen === nextlen) {\n      continue;\n\n    } else if (count < min_count) {\n      do { send_code(s, curlen, s.bl_tree); } while (--count !== 0);\n\n    } else if (curlen !== 0) {\n      if (curlen !== prevlen) {\n        send_code(s, curlen, s.bl_tree);\n        count--;\n      }\n      //Assert(count >= 3 && count <= 6, \" 3_6?\");\n      send_code(s, REP_3_6, s.bl_tree);\n      send_bits(s, count - 3, 2);\n\n    } else if (count <= 10) {\n      send_code(s, REPZ_3_10, s.bl_tree);\n      send_bits(s, count - 3, 3);\n\n    } else {\n      send_code(s, REPZ_11_138, s.bl_tree);\n      send_bits(s, count - 11, 7);\n    }\n\n    count = 0;\n    prevlen = curlen;\n    if (nextlen === 0) {\n      max_count = 138;\n      min_count = 3;\n\n    } else if (curlen === nextlen) {\n      max_count = 6;\n      min_count = 3;\n\n    } else {\n      max_count = 7;\n      min_count = 4;\n    }\n  }\n}\n\n\n/* ===========================================================================\n * Construct the Huffman tree for the bit lengths and return the index in\n * bl_order of the last bit length code to send.\n */\nfunction build_bl_tree(s) {\n  var max_blindex;  /* index of last bit length code of non zero freq */\n\n  /* Determine the bit length frequencies for literal and distance trees */\n  scan_tree(s, s.dyn_ltree, s.l_desc.max_code);\n  scan_tree(s, s.dyn_dtree, s.d_desc.max_code);\n\n  /* Build the bit length tree: */\n  build_tree(s, s.bl_desc);\n  /* opt_len now includes the length of the tree representations, except\n   * the lengths of the bit lengths codes and the 5+5+4 bits for the counts.\n   */\n\n  /* Determine the number of bit length codes to send. The pkzip format\n   * requires that at least 4 bit length codes be sent. (appnote.txt says\n   * 3 but the actual value used is 4.)\n   */\n  for (max_blindex = BL_CODES - 1; max_blindex >= 3; max_blindex--) {\n    if (s.bl_tree[bl_order[max_blindex] * 2 + 1]/*.Len*/ !== 0) {\n      break;\n    }\n  }\n  /* Update opt_len to include the bit length tree and counts */\n  s.opt_len += 3 * (max_blindex + 1) + 5 + 5 + 4;\n  //Tracev((stderr, \"\\ndyn trees: dyn %ld, stat %ld\",\n  //        s->opt_len, s->static_len));\n\n  return max_blindex;\n}\n\n\n/* ===========================================================================\n * Send the header for a block using dynamic Huffman trees: the counts, the\n * lengths of the bit length codes, the literal tree and the distance tree.\n * IN assertion: lcodes >= 257, dcodes >= 1, blcodes >= 4.\n */\nfunction send_all_trees(s, lcodes, dcodes, blcodes)\n//    deflate_state *s;\n//    int lcodes, dcodes, blcodes; /* number of codes for each tree */\n{\n  var rank;                    /* index in bl_order */\n\n  //Assert (lcodes >= 257 && dcodes >= 1 && blcodes >= 4, \"not enough codes\");\n  //Assert (lcodes <= L_CODES && dcodes <= D_CODES && blcodes <= BL_CODES,\n  //        \"too many codes\");\n  //Tracev((stderr, \"\\nbl counts: \"));\n  send_bits(s, lcodes - 257, 5); /* not +255 as stated in appnote.txt */\n  send_bits(s, dcodes - 1,   5);\n  send_bits(s, blcodes - 4,  4); /* not -3 as stated in appnote.txt */\n  for (rank = 0; rank < blcodes; rank++) {\n    //Tracev((stderr, \"\\nbl code %2d \", bl_order[rank]));\n    send_bits(s, s.bl_tree[bl_order[rank] * 2 + 1]/*.Len*/, 3);\n  }\n  //Tracev((stderr, \"\\nbl tree: sent %ld\", s->bits_sent));\n\n  send_tree(s, s.dyn_ltree, lcodes - 1); /* literal tree */\n  //Tracev((stderr, \"\\nlit tree: sent %ld\", s->bits_sent));\n\n  send_tree(s, s.dyn_dtree, dcodes - 1); /* distance tree */\n  //Tracev((stderr, \"\\ndist tree: sent %ld\", s->bits_sent));\n}\n\n\n/* ===========================================================================\n * Check if the data type is TEXT or BINARY, using the following algorithm:\n * - TEXT if the two conditions below are satisfied:\n *    a) There are no non-portable control characters belonging to the\n *       \"black list\" (0..6, 14..25, 28..31).\n *    b) There is at least one printable character belonging to the\n *       \"white list\" (9 {TAB}, 10 {LF}, 13 {CR}, 32..255).\n * - BINARY otherwise.\n * - The following partially-portable control characters form a\n *   \"gray list\" that is ignored in this detection algorithm:\n *   (7 {BEL}, 8 {BS}, 11 {VT}, 12 {FF}, 26 {SUB}, 27 {ESC}).\n * IN assertion: the fields Freq of dyn_ltree are set.\n */\nfunction detect_data_type(s) {\n  /* black_mask is the bit mask of black-listed bytes\n   * set bits 0..6, 14..25, and 28..31\n   * 0xf3ffc07f = binary 11110011111111111100000001111111\n   */\n  var black_mask = 0xf3ffc07f;\n  var n;\n\n  /* Check for non-textual (\"black-listed\") bytes. */\n  for (n = 0; n <= 31; n++, black_mask >>>= 1) {\n    if ((black_mask & 1) && (s.dyn_ltree[n * 2]/*.Freq*/ !== 0)) {\n      return Z_BINARY;\n    }\n  }\n\n  /* Check for textual (\"white-listed\") bytes. */\n  if (s.dyn_ltree[9 * 2]/*.Freq*/ !== 0 || s.dyn_ltree[10 * 2]/*.Freq*/ !== 0 ||\n      s.dyn_ltree[13 * 2]/*.Freq*/ !== 0) {\n    return Z_TEXT;\n  }\n  for (n = 32; n < LITERALS; n++) {\n    if (s.dyn_ltree[n * 2]/*.Freq*/ !== 0) {\n      return Z_TEXT;\n    }\n  }\n\n  /* There are no \"black-listed\" or \"white-listed\" bytes:\n   * this stream either is empty or has tolerated (\"gray-listed\") bytes only.\n   */\n  return Z_BINARY;\n}\n\n\nvar static_init_done = false;\n\n/* ===========================================================================\n * Initialize the tree data structures for a new zlib stream.\n */\nfunction _tr_init(s)\n{\n\n  if (!static_init_done) {\n    tr_static_init();\n    static_init_done = true;\n  }\n\n  s.l_desc  = new TreeDesc(s.dyn_ltree, static_l_desc);\n  s.d_desc  = new TreeDesc(s.dyn_dtree, static_d_desc);\n  s.bl_desc = new TreeDesc(s.bl_tree, static_bl_desc);\n\n  s.bi_buf = 0;\n  s.bi_valid = 0;\n\n  /* Initialize the first block of the first file: */\n  init_block(s);\n}\n\n\n/* ===========================================================================\n * Send a stored block\n */\nfunction _tr_stored_block(s, buf, stored_len, last)\n//DeflateState *s;\n//charf *buf;       /* input block */\n//ulg stored_len;   /* length of input block */\n//int last;         /* one if this is the last block for a file */\n{\n  send_bits(s, (STORED_BLOCK << 1) + (last ? 1 : 0), 3);    /* send block type */\n  copy_block(s, buf, stored_len, true); /* with header */\n}\n\n\n/* ===========================================================================\n * Send one empty static block to give enough lookahead for inflate.\n * This takes 10 bits, of which 7 may remain in the bit buffer.\n */\nfunction _tr_align(s) {\n  send_bits(s, STATIC_TREES << 1, 3);\n  send_code(s, END_BLOCK, static_ltree);\n  bi_flush(s);\n}\n\n\n/* ===========================================================================\n * Determine the best encoding for the current block: dynamic trees, static\n * trees or store, and output the encoded block to the zip file.\n */\nfunction _tr_flush_block(s, buf, stored_len, last)\n//DeflateState *s;\n//charf *buf;       /* input block, or NULL if too old */\n//ulg stored_len;   /* length of input block */\n//int last;         /* one if this is the last block for a file */\n{\n  var opt_lenb, static_lenb;  /* opt_len and static_len in bytes */\n  var max_blindex = 0;        /* index of last bit length code of non zero freq */\n\n  /* Build the Huffman trees unless a stored block is forced */\n  if (s.level > 0) {\n\n    /* Check if the file is binary or text */\n    if (s.strm.data_type === Z_UNKNOWN) {\n      s.strm.data_type = detect_data_type(s);\n    }\n\n    /* Construct the literal and distance trees */\n    build_tree(s, s.l_desc);\n    // Tracev((stderr, \"\\nlit data: dyn %ld, stat %ld\", s->opt_len,\n    //        s->static_len));\n\n    build_tree(s, s.d_desc);\n    // Tracev((stderr, \"\\ndist data: dyn %ld, stat %ld\", s->opt_len,\n    //        s->static_len));\n    /* At this point, opt_len and static_len are the total bit lengths of\n     * the compressed block data, excluding the tree representations.\n     */\n\n    /* Build the bit length tree for the above two trees, and get the index\n     * in bl_order of the last bit length code to send.\n     */\n    max_blindex = build_bl_tree(s);\n\n    /* Determine the best encoding. Compute the block lengths in bytes. */\n    opt_lenb = (s.opt_len + 3 + 7) >>> 3;\n    static_lenb = (s.static_len + 3 + 7) >>> 3;\n\n    // Tracev((stderr, \"\\nopt %lu(%lu) stat %lu(%lu) stored %lu lit %u \",\n    //        opt_lenb, s->opt_len, static_lenb, s->static_len, stored_len,\n    //        s->last_lit));\n\n    if (static_lenb <= opt_lenb) { opt_lenb = static_lenb; }\n\n  } else {\n    // Assert(buf != (char*)0, \"lost buf\");\n    opt_lenb = static_lenb = stored_len + 5; /* force a stored block */\n  }\n\n  if ((stored_len + 4 <= opt_lenb) && (buf !== -1)) {\n    /* 4: two words for the lengths */\n\n    /* The test buf != NULL is only necessary if LIT_BUFSIZE > WSIZE.\n     * Otherwise we can't have processed more than WSIZE input bytes since\n     * the last block flush, because compression would have been\n     * successful. If LIT_BUFSIZE <= WSIZE, it is never too late to\n     * transform a block into a stored block.\n     */\n    _tr_stored_block(s, buf, stored_len, last);\n\n  } else if (s.strategy === Z_FIXED || static_lenb === opt_lenb) {\n\n    send_bits(s, (STATIC_TREES << 1) + (last ? 1 : 0), 3);\n    compress_block(s, static_ltree, static_dtree);\n\n  } else {\n    send_bits(s, (DYN_TREES << 1) + (last ? 1 : 0), 3);\n    send_all_trees(s, s.l_desc.max_code + 1, s.d_desc.max_code + 1, max_blindex + 1);\n    compress_block(s, s.dyn_ltree, s.dyn_dtree);\n  }\n  // Assert (s->compressed_len == s->bits_sent, \"bad compressed size\");\n  /* The above check is made mod 2^32, for files larger than 512 MB\n   * and uLong implemented on 32 bits.\n   */\n  init_block(s);\n\n  if (last) {\n    bi_windup(s);\n  }\n  // Tracev((stderr,\"\\ncomprlen %lu(%lu) \", s->compressed_len>>3,\n  //       s->compressed_len-7*last));\n}\n\n/* ===========================================================================\n * Save the match info and tally the frequency counts. Return true if\n * the current block must be flushed.\n */\nfunction _tr_tally(s, dist, lc)\n//    deflate_state *s;\n//    unsigned dist;  /* distance of matched string */\n//    unsigned lc;    /* match length-MIN_MATCH or unmatched char (if dist==0) */\n{\n  //var out_length, in_length, dcode;\n\n  s.pending_buf[s.d_buf + s.last_lit * 2]     = (dist >>> 8) & 0xff;\n  s.pending_buf[s.d_buf + s.last_lit * 2 + 1] = dist & 0xff;\n\n  s.pending_buf[s.l_buf + s.last_lit] = lc & 0xff;\n  s.last_lit++;\n\n  if (dist === 0) {\n    /* lc is the unmatched char */\n    s.dyn_ltree[lc * 2]/*.Freq*/++;\n  } else {\n    s.matches++;\n    /* Here, lc is the match length - MIN_MATCH */\n    dist--;             /* dist = match distance - 1 */\n    //Assert((ush)dist < (ush)MAX_DIST(s) &&\n    //       (ush)lc <= (ush)(MAX_MATCH-MIN_MATCH) &&\n    //       (ush)d_code(dist) < (ush)D_CODES,  \"_tr_tally: bad match\");\n\n    s.dyn_ltree[(_length_code[lc] + LITERALS + 1) * 2]/*.Freq*/++;\n    s.dyn_dtree[d_code(dist) * 2]/*.Freq*/++;\n  }\n\n// (!) This block is disabled in zlib defailts,\n// don't enable it for binary compatibility\n\n//#ifdef TRUNCATE_BLOCK\n//  /* Try to guess if it is profitable to stop the current block here */\n//  if ((s.last_lit & 0x1fff) === 0 && s.level > 2) {\n//    /* Compute an upper bound for the compressed length */\n//    out_length = s.last_lit*8;\n//    in_length = s.strstart - s.block_start;\n//\n//    for (dcode = 0; dcode < D_CODES; dcode++) {\n//      out_length += s.dyn_dtree[dcode*2]/*.Freq*/ * (5 + extra_dbits[dcode]);\n//    }\n//    out_length >>>= 3;\n//    //Tracev((stderr,\"\\nlast_lit %u, in %ld, out ~%ld(%ld%%) \",\n//    //       s->last_lit, in_length, out_length,\n//    //       100L - out_length*100L/in_length));\n//    if (s.matches < (s.last_lit>>1)/*int /2*/ && out_length < (in_length>>1)/*int /2*/) {\n//      return true;\n//    }\n//  }\n//#endif\n\n  return (s.last_lit === s.lit_bufsize - 1);\n  /* We avoid equality with lit_bufsize because of wraparound at 64K\n   * on 16 bit machines and because stored blocks are restricted to\n   * 64K-1 bytes.\n   */\n}\n\nexport { _tr_init, _tr_stored_block, _tr_flush_block, _tr_tally, _tr_align };\n"
  },
  {
    "path": "vendor/pako/lib/zlib/zstream.js",
    "content": "export default function ZStream() {\n  /* next input byte */\n  this.input = null; // JS specific, because we have no pointers\n  this.next_in = 0;\n  /* number of bytes available at input */\n  this.avail_in = 0;\n  /* total number of input bytes read so far */\n  this.total_in = 0;\n  /* next output byte should be put there */\n  this.output = null; // JS specific, because we have no pointers\n  this.next_out = 0;\n  /* remaining free space at output */\n  this.avail_out = 0;\n  /* total number of bytes output so far */\n  this.total_out = 0;\n  /* last error message, NULL if no error */\n  this.msg = ''/*Z_NULL*/;\n  /* not visible by applications */\n  this.state = null;\n  /* best guess about the data type: binary or text */\n  this.data_type = 2/*Z_UNKNOWN*/;\n  /* adler32 value of the uncompressed data */\n  this.adler = 0;\n}\n"
  },
  {
    "path": "vnc.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" class=\"noVNC_loading\">\n<head>\n\n    <!--\n    noVNC example: simple example using default UI\n    Copyright (C) 2019 The noVNC authors\n    noVNC is licensed under the MPL 2.0 (see LICENSE.txt)\n    This file is licensed under the 2-Clause BSD license (see LICENSE.txt).\n\n    Connect parameters are provided in query string:\n        http://example.com/?host=HOST&port=PORT&encrypt=1\n    or the fragment:\n        http://example.com/#host=HOST&port=PORT&encrypt=1\n    -->\n    <title>noVNC</title>\n\n    <link rel=\"icon\" type=\"image/x-icon\" href=\"app/images/icons/novnc.ico\">\n    <meta name=\"theme-color\" content=\"#313131\">\n\n    <!-- Apple iOS Safari settings -->\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n    <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n    <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">\n\n    <!-- @2x -->\n    <link rel=\"apple-touch-icon\" sizes=\"40x40\" type=\"image/png\" href=\"app/images/icons/novnc-ios-40.png\">\n    <link rel=\"apple-touch-icon\" sizes=\"58x58\" type=\"image/png\" href=\"app/images/icons/novnc-ios-58.png\">\n    <link rel=\"apple-touch-icon\" sizes=\"80x80\" type=\"image/png\" href=\"app/images/icons/novnc-ios-80.png\">\n    <link rel=\"apple-touch-icon\" sizes=\"120x120\" type=\"image/png\" href=\"app/images/icons/novnc-ios-120.png\">\n    <link rel=\"apple-touch-icon\" sizes=\"152x152\" type=\"image/png\" href=\"app/images/icons/novnc-ios-152.png\">\n    <link rel=\"apple-touch-icon\" sizes=\"167x167\" type=\"image/png\" href=\"app/images/icons/novnc-ios-167.png\">\n    <!-- @3x -->\n    <link rel=\"apple-touch-icon\" sizes=\"60x60\" type=\"image/png\" href=\"app/images/icons/novnc-ios-60.png\">\n    <link rel=\"apple-touch-icon\" sizes=\"87x87\" type=\"image/png\" href=\"app/images/icons/novnc-ios-87.png\">\n    <link rel=\"apple-touch-icon\" sizes=\"120x120\" type=\"image/png\" href=\"app/images/icons/novnc-ios-120.png\">\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" type=\"image/png\" href=\"app/images/icons/novnc-ios-180.png\">\n\n    <!-- Stylesheets -->\n    <link rel=\"stylesheet\" href=\"app/styles/constants.css\">\n    <link rel=\"stylesheet\" href=\"app/styles/base.css\">\n    <link rel=\"stylesheet\" href=\"app/styles/input.css\">\n\n    <!-- Images that will later appear via CSS -->\n    <link rel=\"preload\" as=\"image\" href=\"app/images/info.svg\">\n    <link rel=\"preload\" as=\"image\" href=\"app/images/error.svg\">\n    <link rel=\"preload\" as=\"image\" href=\"app/images/warning.svg\">\n\n    <script type=\"module\" crossorigin=\"anonymous\" src=\"app/error-handler.js\"></script>\n\n    <script type=\"module\">\n        import UI from \"./app/ui.js\";\n        import * as Log from './core/util/logging.js';\n\n        let response;\n\n        let defaults = {};\n        let mandatory = {};\n\n        // Default settings will be loaded from defaults.json. Mandatory\n        // settings will be loaded from mandatory.json, which the user\n        // cannot change.\n\n        try {\n            response = await fetch('./defaults.json');\n            if (!response.ok) {\n                throw Error(\"\" + response.status + \" \" + response.statusText);\n            }\n\n            defaults = await response.json();\n        } catch (err) {\n            Log.Error(\"Couldn't fetch defaults.json: \" + err);\n        }\n\n        try {\n            response = await fetch('./mandatory.json');\n            if (!response.ok) {\n                throw Error(\"\" + response.status + \" \" + response.statusText);\n            }\n\n            mandatory = await response.json();\n        } catch (err) {\n            Log.Error(\"Couldn't fetch mandatory.json: \" + err);\n        }\n\n        // You can also override any defaults you need here:\n        //\n        // defaults['host'] = 'vnc.example.com';\n\n        // Or force a specific setting, preventing the user from\n        // changing it:\n        //\n        // mandatory['view_only'] = true;\n\n        // See docs/EMBEDDING.md for a list of possible settings.\n\n        UI.start({ settings: { defaults: defaults,\n                               mandatory: mandatory } });\n    </script>\n</head>\n\n<body>\n\n    <div id=\"noVNC_fallback_error\" class=\"noVNC_center\">\n        <div>\n            <div>noVNC encountered an error:</div>\n            <br>\n            <div id=\"noVNC_fallback_errormsg\"></div>\n        </div>\n    </div>\n\n    <!-- noVNC control bar -->\n    <div id=\"noVNC_control_bar_anchor\" class=\"noVNC_vcenter\">\n\n        <div id=\"noVNC_control_bar\">\n            <div id=\"noVNC_control_bar_handle\" title=\"Hide/Show the control bar\"><div></div></div>\n\n            <div class=\"noVNC_scroll\">\n\n            <h1 class=\"noVNC_logo\" translate=\"no\"><span>no</span><br>VNC</h1>\n\n            <hr>\n\n            <!-- Drag/Pan the viewport -->\n            <input type=\"image\" alt=\"Drag\" src=\"app/images/drag.svg\"\n                id=\"noVNC_view_drag_button\" class=\"noVNC_button noVNC_hidden\"\n                title=\"Move/Drag viewport\">\n\n            <!--noVNC touch device only buttons-->\n            <div id=\"noVNC_mobile_buttons\">\n                <input type=\"image\" alt=\"Keyboard\" src=\"app/images/keyboard.svg\"\n                    id=\"noVNC_keyboard_button\" class=\"noVNC_button\" title=\"Show keyboard\">\n            </div>\n\n            <!-- Extra manual keys -->\n            <input type=\"image\" alt=\"Extra keys\" src=\"app/images/toggleextrakeys.svg\"\n                id=\"noVNC_toggle_extra_keys_button\" class=\"noVNC_button\"\n                title=\"Show extra keys\">\n            <div class=\"noVNC_vcenter\">\n            <div id=\"noVNC_modifiers\" class=\"noVNC_panel\">\n                <input type=\"image\" alt=\"Ctrl\" src=\"app/images/ctrl.svg\"\n                    id=\"noVNC_toggle_ctrl_button\" class=\"noVNC_button\"\n                    title=\"Toggle Ctrl\">\n                <input type=\"image\" alt=\"Alt\" src=\"app/images/alt.svg\"\n                    id=\"noVNC_toggle_alt_button\" class=\"noVNC_button\"\n                    title=\"Toggle Alt\">\n                <input type=\"image\" alt=\"Windows\" src=\"app/images/windows.svg\"\n                    id=\"noVNC_toggle_windows_button\" class=\"noVNC_button\"\n                    title=\"Toggle Windows\">\n                <input type=\"image\" alt=\"Tab\" src=\"app/images/tab.svg\"\n                    id=\"noVNC_send_tab_button\" class=\"noVNC_button\"\n                    title=\"Send Tab\">\n                <input type=\"image\" alt=\"Esc\" src=\"app/images/esc.svg\"\n                    id=\"noVNC_send_esc_button\" class=\"noVNC_button\"\n                    title=\"Send Escape\">\n                <input type=\"image\" alt=\"Ctrl+Alt+Del\" src=\"app/images/ctrlaltdel.svg\"\n                    id=\"noVNC_send_ctrl_alt_del_button\" class=\"noVNC_button\"\n                    title=\"Send Ctrl-Alt-Del\">\n            </div>\n            </div>\n\n            <!-- Shutdown/Reboot -->\n            <input type=\"image\" alt=\"Shutdown/Reboot\" src=\"app/images/power.svg\"\n                id=\"noVNC_power_button\" class=\"noVNC_button\"\n                title=\"Shutdown/Reboot...\">\n            <div class=\"noVNC_vcenter\">\n            <div id=\"noVNC_power\" class=\"noVNC_panel\">\n                <div class=\"noVNC_heading\">\n                    <img alt=\"\" src=\"app/images/power.svg\"> Power\n                </div>\n                <input type=\"button\" id=\"noVNC_shutdown_button\" value=\"Shutdown\">\n                <input type=\"button\" id=\"noVNC_reboot_button\" value=\"Reboot\">\n                <input type=\"button\" id=\"noVNC_reset_button\" value=\"Reset\">\n            </div>\n            </div>\n\n            <!-- Clipboard -->\n            <input type=\"image\" alt=\"Clipboard\" src=\"app/images/clipboard.svg\"\n                id=\"noVNC_clipboard_button\" class=\"noVNC_button\"\n                title=\"Clipboard\">\n            <div class=\"noVNC_vcenter\">\n            <div id=\"noVNC_clipboard\" class=\"noVNC_panel\">\n                <div class=\"noVNC_heading\">\n                    <img alt=\"\" src=\"app/images/clipboard.svg\"> Clipboard\n                </div>\n                <p class=\"noVNC_subheading\">\n                    Edit clipboard content in the textarea below.\n                </p>\n                <textarea id=\"noVNC_clipboard_text\" rows=5></textarea>\n            </div>\n            </div>\n\n            <!-- Toggle fullscreen -->\n            <input type=\"image\" alt=\"Full screen\" src=\"app/images/fullscreen.svg\"\n                id=\"noVNC_fullscreen_button\" class=\"noVNC_button noVNC_hidden\"\n                title=\"Full screen\">\n\n            <!-- Settings -->\n            <input type=\"image\" alt=\"Settings\" src=\"app/images/settings.svg\"\n                id=\"noVNC_settings_button\" class=\"noVNC_button\"\n                title=\"Settings\">\n            <div class=\"noVNC_vcenter\">\n            <div id=\"noVNC_settings\" class=\"noVNC_panel\">\n                <div class=\"noVNC_heading\">\n                    <img alt=\"\" src=\"app/images/settings.svg\"> Settings\n                </div>\n                <ul>\n                    <li>\n                        <label>\n                            <input id=\"noVNC_setting_shared\" type=\"checkbox\"\n                                   class=\"toggle\">\n                            Shared mode\n                        </label>\n                    </li>\n                    <li>\n                        <label>\n                            <input id=\"noVNC_setting_view_only\" type=\"checkbox\"\n                                   class=\"toggle\">\n                            View only\n                        </label>\n                    </li>\n                    <li><hr></li>\n                    <li>\n                        <label>\n                            <input id=\"noVNC_setting_view_clip\" type=\"checkbox\"\n                                   class=\"toggle\">\n                            Clip to window\n                        </label>\n                    </li>\n                    <li>\n                        <label for=\"noVNC_setting_resize\">Scaling mode:</label>\n                        <select id=\"noVNC_setting_resize\" name=\"vncResize\">\n                            <option value=\"off\">None</option>\n                            <option value=\"scale\">Local scaling</option>\n                            <option value=\"remote\">Remote resizing</option>\n                        </select>\n                    </li>\n                    <li><hr></li>\n                    <li>\n                        <div class=\"noVNC_expander\">Advanced</div>\n                        <div><ul>\n                            <li>\n                                <label for=\"noVNC_setting_quality\">Quality:</label>\n                                <input id=\"noVNC_setting_quality\" type=\"range\" min=\"0\" max=\"9\" value=\"6\">\n                            </li>\n                            <li>\n                                <label for=\"noVNC_setting_compression\">Compression level:</label>\n                                <input id=\"noVNC_setting_compression\" type=\"range\" min=\"0\" max=\"9\" value=\"2\">\n                            </li>\n                            <li><hr></li>\n                            <li>\n                                <label for=\"noVNC_setting_repeaterID\">Repeater ID:</label>\n                                <input id=\"noVNC_setting_repeaterID\" type=\"text\" value=\"\">\n                            </li>\n                            <li>\n                                <div class=\"noVNC_expander\">WebSocket</div>\n                                <div><ul>\n                                    <li>\n                                        <label>\n                                            <input id=\"noVNC_setting_encrypt\" type=\"checkbox\"\n                                                   class=\"toggle\">\n                                            Encrypt\n                                        </label>\n                                    </li>\n                                    <li>\n                                        <label for=\"noVNC_setting_host\">Host:</label>\n                                        <input id=\"noVNC_setting_host\">\n                                    </li>\n                                    <li>\n                                        <label for=\"noVNC_setting_port\">Port:</label>\n                                        <input id=\"noVNC_setting_port\" type=\"number\">\n                                    </li>\n                                    <li>\n                                        <label for=\"noVNC_setting_path\">Path:</label>\n                                        <input id=\"noVNC_setting_path\" type=\"text\" value=\"websockify\">\n                                    </li>\n                                </ul></div>\n                            </li>\n                            <li><hr></li>\n                            <li>\n                                <label>\n                                    <input id=\"noVNC_setting_reconnect\" type=\"checkbox\"\n                                           class=\"toggle\">\n                                    Automatic reconnect\n                                </label>\n                            </li>\n                            <li>\n                                <label for=\"noVNC_setting_reconnect_delay\">Reconnect delay (ms):</label>\n                                <input id=\"noVNC_setting_reconnect_delay\" type=\"number\">\n                            </li>\n                            <li><hr></li>\n                            <li>\n                                <label>\n                                    <input id=\"noVNC_setting_show_dot\" type=\"checkbox\"\n                                           class=\"toggle\">\n                                    Show dot when no cursor\n                                </label>\n                            </li>\n                            <li>\n                                <label>\n                                    <input id=\"noVNC_setting_keep_device_awake\" type=\"checkbox\"\n                                            class=\"toggle\">\n                                    Keep client display awake while connected\n                                </label>\n                            </li>\n                            <li><hr></li>\n                            <!-- Logging selection dropdown -->\n                            <li>\n                                <label>Logging:\n                                    <select id=\"noVNC_setting_logging\" name=\"vncLogging\">\n                                    </select>\n                                </label>\n                            </li>\n                        </ul></div>\n                    </li>\n                    <li class=\"noVNC_version_separator\"><hr></li>\n                    <li class=\"noVNC_version_wrapper\">\n                        <span>Version:</span>\n                        <span class=\"noVNC_version\"></span>\n                    </li>\n                </ul>\n            </div>\n            </div>\n\n            <!-- Connection controls -->\n            <input type=\"image\" alt=\"Disconnect\" src=\"app/images/disconnect.svg\"\n                id=\"noVNC_disconnect_button\" class=\"noVNC_button\"\n                title=\"Disconnect\">\n\n            </div>\n        </div>\n\n    </div> <!-- End of noVNC_control_bar -->\n\n    <div id=\"noVNC_hint_anchor\" class=\"noVNC_vcenter\">\n        <div id=\"noVNC_control_bar_hint\">\n        </div>\n    </div>\n\n    <!-- Status dialog -->\n    <div id=\"noVNC_status\"></div>\n\n    <!-- Connect button -->\n    <div class=\"noVNC_center\">\n        <div id=\"noVNC_connect_dlg\">\n            <p class=\"noVNC_logo\" translate=\"no\"><span>no</span>VNC</p>\n            <div>\n                <button id=\"noVNC_connect_button\">\n                    <img alt=\"\" src=\"app/images/connect.svg\"> Connect\n                </button>\n            </div>\n        </div>\n    </div>\n\n    <!-- Server key verification dialog -->\n    <div class=\"noVNC_center noVNC_connect_layer\">\n    <div id=\"noVNC_verify_server_dlg\" class=\"noVNC_panel\"><form>\n        <div class=\"noVNC_heading\">\n            Server identity\n        </div>\n        <div>\n            The server has provided the following identifying information:\n        </div>\n        <div id=\"noVNC_fingerprint_block\">\n            Fingerprint:\n            <span id=\"noVNC_fingerprint\"></span>\n        </div>\n        <div>\n            Please verify that the information is correct and press\n            \"Approve\". Otherwise press \"Reject\".\n        </div>\n        <div class=\"button_row\">\n            <input id=\"noVNC_approve_server_button\" type=\"submit\" value=\"Approve\">\n            <input id=\"noVNC_reject_server_button\" type=\"button\" value=\"Reject\">\n        </div>\n    </form></div>\n    </div>\n\n    <!-- Password dialog -->\n    <div class=\"noVNC_center noVNC_connect_layer\">\n    <div id=\"noVNC_credentials_dlg\" class=\"noVNC_panel\"><form>\n        <div class=\"noVNC_heading\">\n            Credentials\n        </div>\n        <div id=\"noVNC_username_block\">\n            <label for=\"noVNC_username_input\">Username:</label>\n            <input id=\"noVNC_username_input\">\n        </div>\n        <div id=\"noVNC_password_block\">\n            <label for=\"noVNC_password_input\">Password:</label>\n            <input id=\"noVNC_password_input\" type=\"password\">\n        </div>\n        <div class=\"button_row\">\n            <input id=\"noVNC_credentials_button\" type=\"submit\" value=\"Send credentials\">\n        </div>\n    </form></div>\n    </div>\n\n    <!-- Transition screens -->\n    <div id=\"noVNC_transition\">\n        <div id=\"noVNC_transition_text\"></div>\n        <div>\n        <input type=\"button\" id=\"noVNC_cancel_reconnect_button\" value=\"Cancel\">\n        </div>\n        <div class=\"noVNC_spinner\"></div>\n    </div>\n\n    <!-- This is where the RFB elements will attach -->\n    <div id=\"noVNC_container\">\n        <!-- Note that Google Chrome on Android doesn't respect any of these,\n             html attributes which attempt to disable text suggestions on the\n             on-screen keyboard. Let's hope Chrome implements the ime-mode\n             style for example -->\n        <textarea id=\"noVNC_keyboardinput\" autocapitalize=\"off\"\n            autocomplete=\"off\" spellcheck=\"false\" tabindex=\"-1\"></textarea>\n    </div>\n\n    <audio id=\"noVNC_bell\">\n        <source src=\"app/sounds/bell.oga\" type=\"audio/ogg\">\n        <source src=\"app/sounds/bell.mp3\" type=\"audio/mpeg\">\n    </audio>\n </body>\n</html>\n"
  },
  {
    "path": "vnc_lite.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\n    <!--\n    noVNC example: lightweight example using minimal UI and features\n\n    This is a self-contained file which doesn't import WebUtil or external CSS.\n\n    Copyright (C) 2019 The noVNC authors\n    noVNC is licensed under the MPL 2.0 (see LICENSE.txt)\n    This file is licensed under the 2-Clause BSD license (see LICENSE.txt).\n\n    Connect parameters are provided in query string:\n        http://example.com/?host=HOST&port=PORT&scale=true\n    -->\n    <title>noVNC</title>\n\n    <style>\n\n        body {\n            margin: 0;\n            background-color: dimgrey;\n            height: 100%;\n            display: flex;\n            flex-direction: column;\n        }\n        html {\n            height: 100%;\n        }\n\n        #top_bar {\n            background-color: #6e84a3;\n            color: white;\n            font: bold 12px Helvetica;\n            padding: 6px 5px 4px 5px;\n            border-bottom: 1px outset;\n        }\n        #status {\n            text-align: center;\n        }\n        #sendCtrlAltDelButton {\n            position: fixed;\n            top: 0px;\n            right: 0px;\n            border: 1px outset;\n            padding: 5px 5px 4px 5px;\n            cursor: pointer;\n        }\n\n        #screen {\n            flex: 1; /* fill remaining space */\n            overflow: hidden;\n        }\n\n    </style>\n\n    <script type=\"module\" crossorigin=\"anonymous\">\n        // RFB holds the API to connect and communicate with a VNC server\n        import RFB from './core/rfb.js';\n\n        let rfb;\n        let desktopName;\n\n        // When this function is called we have\n        // successfully connected to a server\n        function connectedToServer(e) {\n            status(\"Connected to \" + desktopName);\n        }\n\n        // This function is called when we are disconnected\n        function disconnectedFromServer(e) {\n            if (e.detail.clean) {\n                status(\"Disconnected\");\n            } else {\n                status(\"Something went wrong, connection is closed\");\n            }\n        }\n\n        // When this function is called, the server requires\n        // credentials to authenticate\n        function credentialsAreRequired(e) {\n            const password = prompt(\"Password required:\");\n            rfb.sendCredentials({ password: password });\n        }\n\n        // When this function is called we have received\n        // a desktop name from the server\n        function updateDesktopName(e) {\n            desktopName = e.detail.name;\n        }\n\n        // Since most operating systems will catch Ctrl+Alt+Del\n        // before they get a chance to be intercepted by the browser,\n        // we provide a way to emulate this key sequence.\n        function sendCtrlAltDel() {\n            rfb.sendCtrlAltDel();\n            return false;\n        }\n\n        // Show a status text in the top bar\n        function status(text) {\n            document.getElementById('status').textContent = text;\n        }\n\n        // This function extracts the value of one variable from the\n        // query string. If the variable isn't defined in the URL\n        // it returns the default value instead.\n        function readQueryVariable(name, defaultValue) {\n            // A URL with a query parameter can look like this:\n            // https://www.example.com?myqueryparam=myvalue\n            //\n            // Note that we use location.href instead of location.search\n            // because Firefox < 53 has a bug w.r.t location.search\n            const re = new RegExp('.*[?&]' + name + '=([^&#]*)'),\n                  match = document.location.href.match(re);\n\n            if (match) {\n                // We have to decode the URL since want the cleartext value\n                return decodeURIComponent(match[1]);\n            }\n\n            return defaultValue;\n        }\n\n        document.getElementById('sendCtrlAltDelButton')\n            .onclick = sendCtrlAltDel;\n\n        // Read parameters specified in the URL query string\n        // By default, use the host and port of server that served this file\n        const host = readQueryVariable('host', window.location.hostname);\n        let port = readQueryVariable('port', window.location.port);\n        const password = readQueryVariable('password');\n        const path = readQueryVariable('path', 'websockify');\n\n        // | | |         | | |\n        // | | | Connect | | |\n        // v v v         v v v\n\n        status(\"Connecting\");\n\n        // Build the websocket URL used to connect\n        let url;\n        if (window.location.protocol === \"https:\") {\n            url = 'wss';\n        } else {\n            url = 'ws';\n        }\n        url += '://' + host;\n        if(port) {\n            url += ':' + port;\n        }\n        url += '/' + path;\n\n        // Creating a new RFB object will start a new connection\n        rfb = new RFB(document.getElementById('screen'), url,\n                      { credentials: { password: password } });\n\n        // Add listeners to important events from the RFB module\n        rfb.addEventListener(\"connect\",  connectedToServer);\n        rfb.addEventListener(\"disconnect\", disconnectedFromServer);\n        rfb.addEventListener(\"credentialsrequired\", credentialsAreRequired);\n        rfb.addEventListener(\"desktopname\", updateDesktopName);\n\n        // Set parameters that can be changed on an active connection\n        rfb.viewOnly = readQueryVariable('view_only', false);\n        rfb.scaleViewport = readQueryVariable('scale', false);\n    </script>\n</head>\n\n<body>\n    <div id=\"top_bar\">\n        <div id=\"status\">Loading</div>\n        <div id=\"sendCtrlAltDelButton\">Send CtrlAltDel</div>\n    </div>\n    <div id=\"screen\">\n        <!-- This is where the remote screen will appear -->\n    </div>\n</body>\n</html>\n"
  }
]