Преглед на файлове

Merge pull request #368 from DirectXMan12/refactor/cleanup

Cleanup and test all the things (plus ditching Crockford)!
Solly преди 11 години
родител
ревизия
0ca7cf4867
променени са 24 файла, в които са добавени 7784 реда и са изтрити 4789 реда
  1. 13 0
      .travis.yml
  2. 1 0
      README.md
  3. 96 98
      include/base64.js
  4. 175 172
      include/des.js
  5. 677 713
      include/display.js
  6. 363 376
      include/input.js
  7. 19 10
      include/keyboard.js
  8. 1747 1845
      include/rfb.js
  9. 952 976
      include/ui.js
  10. 375 197
      include/util.js
  11. 309 349
      include/websock.js
  12. 61 43
      include/webutil.js
  13. 191 0
      karma.conf.js
  14. 50 0
      package.json
  15. 96 0
      tests/fake.websocket.js
  16. 1 1
      tests/run_from_console.casper.js
  17. 6 6
      tests/run_from_console.js
  18. 33 0
      tests/test.base64.js
  19. 332 0
      tests/test.display.js
  20. 3 1
      tests/test.helper.js
  21. 3 2
      tests/test.keyboard.js
  22. 1696 0
      tests/test.rfb.js
  23. 105 0
      tests/test.util.js
  24. 480 0
      tests/test.websock.js

+ 13 - 0
.travis.yml

@@ -0,0 +1,13 @@
+language: node_js
+node_js:
+- '0.11'
+env:
+  matrix:
+  - TEST_BROWSER_NAME=PhantomJS
+  - TEST_BROWSER_NAME=chrome TEST_BROWSER_OS='Windows 7,Linux'
+  - TEST_BROWSER_NAME=firefox TEST_BROWSER_OS='Windows 7,Linux' TEST_BROWSER_VERSION='30,26'
+  - TEST_BROWSER_NAME='internet explorer' TEST_BROWSER_OS='Windows 7' TEST_BROWSER_VERSION=10
+  - TEST_BROWSER_NAME='internet explorer' TEST_BROWSER_OS='Windows 8.1' TEST_BROWSER_VERSION=11
+  - TEST_BROWSER_NAME=safari TEST_BROWSER_OS='OS X 10.8' TEST_BROWSER_VERSION=6
+  - TEST_BROWSER_NAME=safari TEST_BROWSER_OS='OS X 10.9' TEST_BROWSER_VERSION=7
+before_script: npm install -g karma-cli

+ 1 - 0
README.md

@@ -1,5 +1,6 @@
 ## noVNC: HTML5 VNC Client
 
+[![Build Status](https://travis-ci.org/kanaka/noVNC.svg?branch=refactor%2Fcleanup)](https://travis-ci.org/kanaka/noVNC)
 
 ### Description
 

+ 96 - 98
include/base64.js

@@ -4,112 +4,110 @@
 
 // From: http://hg.mozilla.org/mozilla-central/raw-file/ec10630b1a54/js/src/devtools/jint/sunspider/string-base64.js
 
-/*jslint white: false, bitwise: false, plusplus: false */
+/*jslint white: false */
 /*global console */
 
 var Base64 = {
+    /* Convert data (an array of integers) to a Base64 string. */
+    toBase64Table : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''),
+    base64Pad     : '=',
+
+    encode: function (data) {
+        "use strict";
+        var result = '';
+        var toBase64Table = Base64.toBase64Table;
+        var length = data.length;
+        var lengthpad = (length % 3);
+        // Convert every three bytes to 4 ascii characters.
+
+        for (var i = 0; i < (length - 2); i += 3) {
+            result += toBase64Table[data[i] >> 2];
+            result += toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)];
+            result += toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)];
+            result += toBase64Table[data[i + 2] & 0x3f];
+        }
 
-/* Convert data (an array of integers) to a Base64 string. */
-toBase64Table : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''),
-base64Pad     : '=',
-
-encode: function (data) {
-    "use strict";
-    var result = '';
-    var toBase64Table = Base64.toBase64Table;
-    var length = data.length
-    var lengthpad = (length%3);
-    var i = 0, j = 0;
-    // Convert every three bytes to 4 ascii characters.
-  /* BEGIN LOOP */
-    for (i = 0; i < (length - 2); i += 3) {
-        result += toBase64Table[data[i] >> 2];
-        result += toBase64Table[((data[i] & 0x03) << 4) + (data[i+1] >> 4)];
-        result += toBase64Table[((data[i+1] & 0x0f) << 2) + (data[i+2] >> 6)];
-        result += toBase64Table[data[i+2] & 0x3f];
-    }
-  /* END LOOP */
-
-    // Convert the remaining 1 or 2 bytes, pad out to 4 characters.
-    if (lengthpad === 2) {
-        j = length - lengthpad;
-        result += toBase64Table[data[j] >> 2];
-        result += toBase64Table[((data[j] & 0x03) << 4) + (data[j+1] >> 4)];
-        result += toBase64Table[(data[j+1] & 0x0f) << 2];
-        result += toBase64Table[64];
-    } else if (lengthpad === 1) {
-        j = length - lengthpad;
-        result += toBase64Table[data[j] >> 2];
-        result += toBase64Table[(data[j] & 0x03) << 4];
-        result += toBase64Table[64];
-        result += toBase64Table[64];
-    }
-
-    return result;
-},
-
-/* Convert Base64 data to a string */
-toBinaryTable : [
-    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
-    52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
-    -1, 0, 1, 2,  3, 4, 5, 6,  7, 8, 9,10, 11,12,13,14,
-    15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
-    -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
-    41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
-],
-
-decode: function (data, offset) {
-    "use strict";
-    offset = typeof(offset) !== 'undefined' ? offset : 0;
-    var toBinaryTable = Base64.toBinaryTable;
-    var base64Pad = Base64.base64Pad;
-    var result, result_length, idx, i, c, padding;
-    var leftbits = 0; // number of bits decoded, but yet to be appended
-    var leftdata = 0; // bits decoded, but yet to be appended
-    var data_length = data.indexOf('=') - offset;
-
-    if (data_length < 0) { data_length = data.length - offset; }
-
-    /* Every four characters is 3 resulting numbers */
-    result_length = (data_length >> 2) * 3 + Math.floor((data_length%4)/1.5);
-    result = new Array(result_length);
-
-    // Convert one by one.
-  /* BEGIN LOOP */
-    for (idx = 0, i = offset; i < data.length; i++) {
-        c = toBinaryTable[data.charCodeAt(i) & 0x7f];
-        padding = (data.charAt(i) === base64Pad);
-        // Skip illegal characters and whitespace
-        if (c === -1) {
-            console.error("Illegal character code " + data.charCodeAt(i) + " at position " + i);
-            continue;
+        // Convert the remaining 1 or 2 bytes, pad out to 4 characters.
+        var j = 0;
+        if (lengthpad === 2) {
+            j = length - lengthpad;
+            result += toBase64Table[data[j] >> 2];
+            result += toBase64Table[((data[j] & 0x03) << 4) + (data[j + 1] >> 4)];
+            result += toBase64Table[(data[j + 1] & 0x0f) << 2];
+            result += toBase64Table[64];
+        } else if (lengthpad === 1) {
+            j = length - lengthpad;
+            result += toBase64Table[data[j] >> 2];
+            result += toBase64Table[(data[j] & 0x03) << 4];
+            result += toBase64Table[64];
+            result += toBase64Table[64];
         }
-        
-        // Collect data into leftdata, update bitcount
-        leftdata = (leftdata << 6) | c;
-        leftbits += 6;
 
-        // If we have 8 or more bits, append 8 bits to the result
-        if (leftbits >= 8) {
-            leftbits -= 8;
-            // Append if not padding.
-            if (!padding) {
-                result[idx++] = (leftdata >> leftbits) & 0xff;
+        return result;
+    },
+
+    /* Convert Base64 data to a string */
+    /* jshint -W013 */
+    toBinaryTable : [
+        -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+        -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+        -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
+        52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
+        -1, 0, 1, 2,  3, 4, 5, 6,  7, 8, 9,10, 11,12,13,14,
+        15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
+        -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
+        41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
+    ],
+    /* jshint +W013 */
+
+    decode: function (data, offset) {
+        "use strict";
+        offset = typeof(offset) !== 'undefined' ? offset : 0;
+        var toBinaryTable = Base64.toBinaryTable;
+        var base64Pad = Base64.base64Pad;
+        var result, result_length;
+        var leftbits = 0; // number of bits decoded, but yet to be appended
+        var leftdata = 0; // bits decoded, but yet to be appended
+        var data_length = data.indexOf('=') - offset;
+
+        if (data_length < 0) { data_length = data.length - offset; }
+
+        /* Every four characters is 3 resulting numbers */
+        result_length = (data_length >> 2) * 3 + Math.floor((data_length % 4) / 1.5);
+        result = new Array(result_length);
+
+        // Convert one by one.
+        for (var idx = 0, i = offset; i < data.length; i++) {
+            var c = toBinaryTable[data.charCodeAt(i) & 0x7f];
+            var padding = (data.charAt(i) === base64Pad);
+            // Skip illegal characters and whitespace
+            if (c === -1) {
+                console.error("Illegal character code " + data.charCodeAt(i) + " at position " + i);
+                continue;
+            }
+          
+            // Collect data into leftdata, update bitcount
+            leftdata = (leftdata << 6) | c;
+            leftbits += 6;
+
+            // If we have 8 or more bits, append 8 bits to the result
+            if (leftbits >= 8) {
+                leftbits -= 8;
+                // Append if not padding.
+                if (!padding) {
+                    result[idx++] = (leftdata >> leftbits) & 0xff;
+                }
+                leftdata &= (1 << leftbits) - 1;
             }
-            leftdata &= (1 << leftbits) - 1;
         }
-    }
-  /* END LOOP */
 
-    // If there are any bits left, the base64 string was corrupted
-    if (leftbits) {
-        throw {name: 'Base64-Error', 
-               message: 'Corrupted base64 string'};
-    }
-
-    return result;
-}
+        // If there are any bits left, the base64 string was corrupted
+        if (leftbits) {
+            err = new Error('Corrupted base64 string');
+            err.name = 'Base64-Error';
+            throw err;
+        }
 
+        return result;
+    }
 }; /* End of Base64 namespace */

+ 175 - 172
include/des.js

@@ -75,199 +75,202 @@
  * fine Java utilities: http://www.acme.com/java/
  */
 
-"use strict";
-/*jslint white: false, bitwise: false, plusplus: false */
+/* jslint white: false */
 
 function DES(passwd) {
+    "use strict";
 
-// Tables, permutations, S-boxes, etc.
-var PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3,
-           25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39,
-           50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ],
-    totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28],
-    z = 0x0, a,b,c,d,e,f, SP1,SP2,SP3,SP4,SP5,SP6,SP7,SP8,
-    keys = [];
+    // Tables, permutations, S-boxes, etc.
+    // jshint -W013
+    var PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3,
+               25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39,
+               50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ],
+        totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28],
+        z = 0x0, a,b,c,d,e,f, SP1,SP2,SP3,SP4,SP5,SP6,SP7,SP8,
+        keys = [];
 
-a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e;
-SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d,
-       z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z,
-       a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f,
-       c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d];
-a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e;
-SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d,
-       a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f,
-       z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z,
-       z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e];
-a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e;
-SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f,
-       b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z,
-       c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d,
-       b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e];
-a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e;
-SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d,
-       z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f,
-       b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e,
-       c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e];
-a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e;
-SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z,
-       a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f,
-       z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e,
-       c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d];
-a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e;
-SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f,
-       z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z,
-       b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z,
-       a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f];
-a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e;
-SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f,
-       b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e,
-       b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e,
-       z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d];
-a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e;
-SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d,
-       c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z,
-       a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f,
-       z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e];
+    // jshint -W015
+    a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e;
+    SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d,
+           z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z,
+           a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f,
+           c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d];
+    a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e;
+    SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d,
+           a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f,
+           z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z,
+           z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e];
+    a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e;
+    SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f,
+           b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z,
+           c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d,
+           b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e];
+    a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e;
+    SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d,
+           z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f,
+           b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e,
+           c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e];
+    a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e;
+    SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z,
+           a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f,
+           z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e,
+           c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d];
+    a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e;
+    SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f,
+           z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z,
+           b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z,
+           a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f];
+    a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e;
+    SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f,
+           b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e,
+           b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e,
+           z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d];
+    a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e;
+    SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d,
+           c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z,
+           a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f,
+           z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e];
+    // jshint +W013,+W015
 
-// Set the key.
-function setKeys(keyBlock) {
-    var i, j, l, m, n, o, pc1m = [], pcr = [], kn = [],
-        raw0, raw1, rawi, KnLi;
+    // Set the key.
+    function setKeys(keyBlock) {
+        var i, j, l, m, n, o, pc1m = [], pcr = [], kn = [],
+            raw0, raw1, rawi, KnLi;
 
-    for (j = 0, l = 56; j < 56; ++j, l-=8) {
-        l += l<-5 ? 65 : l<-3 ? 31 : l<-1 ? 63 : l===27 ? 35 : 0; // PC1
-        m = l & 0x7;
-        pc1m[j] = ((keyBlock[l >>> 3] & (1<<m)) !== 0) ? 1: 0;
-    }
+        for (j = 0, l = 56; j < 56; ++j, l -= 8) {
+            l += l < -5 ? 65 : l < -3 ? 31 : l < -1 ? 63 : l === 27 ? 35 : 0; // PC1
+            m = l & 0x7;
+            pc1m[j] = ((keyBlock[l >>> 3] & (1<<m)) !== 0) ? 1: 0;
+        }
 
-    for (i = 0; i < 16; ++i) {
-        m = i << 1;
-        n = m + 1;
-        kn[m] = kn[n] = 0;
-        for (o=28; o<59; o+=28) {
-            for (j = o-28; j < o; ++j) {
-                l = j + totrot[i];
-                if (l < o) {
-                    pcr[j] = pc1m[l];
-                } else {
-                    pcr[j] = pc1m[l - 28];
+        for (i = 0; i < 16; ++i) {
+            m = i << 1;
+            n = m + 1;
+            kn[m] = kn[n] = 0;
+            for (o = 28; o < 59; o += 28) {
+                for (j = o - 28; j < o; ++j) {
+                    l = j + totrot[i];
+                    if (l < o) {
+                        pcr[j] = pc1m[l];
+                    } else {
+                        pcr[j] = pc1m[l - 28];
+                    }
                 }
             }
-        }
-        for (j = 0; j < 24; ++j) {
-            if (pcr[PC2[j]] !== 0) {
-                kn[m] |= 1<<(23-j);
-            }
-            if (pcr[PC2[j + 24]] !== 0) {
-                kn[n] |= 1<<(23-j);
+            for (j = 0; j < 24; ++j) {
+                if (pcr[PC2[j]] !== 0) {
+                    kn[m] |= 1 << (23 - j);
+                }
+                if (pcr[PC2[j + 24]] !== 0) {
+                    kn[n] |= 1 << (23 - j);
+                }
             }
         }
-    }
 
-    // cookey
-    for (i = 0, rawi = 0, KnLi = 0; i < 16; ++i) {
-        raw0 = kn[rawi++];
-        raw1 = kn[rawi++];
-        keys[KnLi] = (raw0 & 0x00fc0000) << 6;
-        keys[KnLi] |= (raw0 & 0x00000fc0) << 10;
-        keys[KnLi] |= (raw1 & 0x00fc0000) >>> 10;
-        keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6;
-        ++KnLi;
-        keys[KnLi] = (raw0 & 0x0003f000) << 12;
-        keys[KnLi] |= (raw0 & 0x0000003f) << 16;
-        keys[KnLi] |= (raw1 & 0x0003f000) >>> 4;
-        keys[KnLi] |= (raw1 & 0x0000003f);
-        ++KnLi;
+        // cookey
+        for (i = 0, rawi = 0, KnLi = 0; i < 16; ++i) {
+            raw0 = kn[rawi++];
+            raw1 = kn[rawi++];
+            keys[KnLi] = (raw0 & 0x00fc0000) << 6;
+            keys[KnLi] |= (raw0 & 0x00000fc0) << 10;
+            keys[KnLi] |= (raw1 & 0x00fc0000) >>> 10;
+            keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6;
+            ++KnLi;
+            keys[KnLi] = (raw0 & 0x0003f000) << 12;
+            keys[KnLi] |= (raw0 & 0x0000003f) << 16;
+            keys[KnLi] |= (raw1 & 0x0003f000) >>> 4;
+            keys[KnLi] |= (raw1 & 0x0000003f);
+            ++KnLi;
+        }
     }
-}
 
-// Encrypt 8 bytes of text
-function enc8(text) {
-    var i = 0, b = text.slice(), fval, keysi = 0,
-        l, r, x; // left, right, accumulator
+    // Encrypt 8 bytes of text
+    function enc8(text) {
+        var i = 0, b = text.slice(), fval, keysi = 0,
+            l, r, x; // left, right, accumulator
 
-    // Squash 8 bytes to 2 ints
-    l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
-    r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
+        // Squash 8 bytes to 2 ints
+        l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
+        r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
 
-    x = ((l >>> 4) ^ r) & 0x0f0f0f0f;
-    r ^= x;
-    l ^= (x << 4);
-    x = ((l >>> 16) ^ r) & 0x0000ffff;
-    r ^= x;
-    l ^= (x << 16);
-    x = ((r >>> 2) ^ l) & 0x33333333;
-    l ^= x;
-    r ^= (x << 2);
-    x = ((r >>> 8) ^ l) & 0x00ff00ff;
-    l ^= x;
-    r ^= (x << 8);
-    r = (r << 1) | ((r >>> 31) & 1);
-    x = (l ^ r) & 0xaaaaaaaa;
-    l ^= x;
-    r ^= x;
-    l = (l << 1) | ((l >>> 31) & 1);
+        x = ((l >>> 4) ^ r) & 0x0f0f0f0f;
+        r ^= x;
+        l ^= (x << 4);
+        x = ((l >>> 16) ^ r) & 0x0000ffff;
+        r ^= x;
+        l ^= (x << 16);
+        x = ((r >>> 2) ^ l) & 0x33333333;
+        l ^= x;
+        r ^= (x << 2);
+        x = ((r >>> 8) ^ l) & 0x00ff00ff;
+        l ^= x;
+        r ^= (x << 8);
+        r = (r << 1) | ((r >>> 31) & 1);
+        x = (l ^ r) & 0xaaaaaaaa;
+        l ^= x;
+        r ^= x;
+        l = (l << 1) | ((l >>> 31) & 1);
 
-    for (i = 0; i < 8; ++i) {
-        x = (r << 28) | (r >>> 4);
-        x ^= keys[keysi++];
-        fval =  SP7[x & 0x3f];
-        fval |= SP5[(x >>> 8) & 0x3f];
-        fval |= SP3[(x >>> 16) & 0x3f];
-        fval |= SP1[(x >>> 24) & 0x3f];
-        x = r ^ keys[keysi++];
-        fval |= SP8[x & 0x3f];
-        fval |= SP6[(x >>> 8) & 0x3f];
-        fval |= SP4[(x >>> 16) & 0x3f];
-        fval |= SP2[(x >>> 24) & 0x3f];
-        l ^= fval;
-        x = (l << 28) | (l >>> 4);
-        x ^= keys[keysi++];
-        fval =  SP7[x & 0x3f];
-        fval |= SP5[(x >>> 8) & 0x3f];
-        fval |= SP3[(x >>> 16) & 0x3f];
-        fval |= SP1[(x >>> 24) & 0x3f];
-        x = l ^ keys[keysi++];
-        fval |= SP8[x & 0x0000003f];
-        fval |= SP6[(x >>> 8) & 0x3f];
-        fval |= SP4[(x >>> 16) & 0x3f];
-        fval |= SP2[(x >>> 24) & 0x3f];
-        r ^= fval;
-    }
+        for (i = 0; i < 8; ++i) {
+            x = (r << 28) | (r >>> 4);
+            x ^= keys[keysi++];
+            fval =  SP7[x & 0x3f];
+            fval |= SP5[(x >>> 8) & 0x3f];
+            fval |= SP3[(x >>> 16) & 0x3f];
+            fval |= SP1[(x >>> 24) & 0x3f];
+            x = r ^ keys[keysi++];
+            fval |= SP8[x & 0x3f];
+            fval |= SP6[(x >>> 8) & 0x3f];
+            fval |= SP4[(x >>> 16) & 0x3f];
+            fval |= SP2[(x >>> 24) & 0x3f];
+            l ^= fval;
+            x = (l << 28) | (l >>> 4);
+            x ^= keys[keysi++];
+            fval =  SP7[x & 0x3f];
+            fval |= SP5[(x >>> 8) & 0x3f];
+            fval |= SP3[(x >>> 16) & 0x3f];
+            fval |= SP1[(x >>> 24) & 0x3f];
+            x = l ^ keys[keysi++];
+            fval |= SP8[x & 0x0000003f];
+            fval |= SP6[(x >>> 8) & 0x3f];
+            fval |= SP4[(x >>> 16) & 0x3f];
+            fval |= SP2[(x >>> 24) & 0x3f];
+            r ^= fval;
+        }
 
-    r = (r << 31) | (r >>> 1);
-    x = (l ^ r) & 0xaaaaaaaa;
-    l ^= x;
-    r ^= x;
-    l = (l << 31) | (l >>> 1);
-    x = ((l >>> 8) ^ r) & 0x00ff00ff;
-    r ^= x;
-    l ^= (x << 8);
-    x = ((l >>> 2) ^ r) & 0x33333333;
-    r ^= x;
-    l ^= (x << 2);
-    x = ((r >>> 16) ^ l) & 0x0000ffff;
-    l ^= x;
-    r ^= (x << 16);
-    x = ((r >>> 4) ^ l) & 0x0f0f0f0f;
-    l ^= x;
-    r ^= (x << 4);
+        r = (r << 31) | (r >>> 1);
+        x = (l ^ r) & 0xaaaaaaaa;
+        l ^= x;
+        r ^= x;
+        l = (l << 31) | (l >>> 1);
+        x = ((l >>> 8) ^ r) & 0x00ff00ff;
+        r ^= x;
+        l ^= (x << 8);
+        x = ((l >>> 2) ^ r) & 0x33333333;
+        r ^= x;
+        l ^= (x << 2);
+        x = ((r >>> 16) ^ l) & 0x0000ffff;
+        l ^= x;
+        r ^= (x << 16);
+        x = ((r >>> 4) ^ l) & 0x0f0f0f0f;
+        l ^= x;
+        r ^= (x << 4);
 
-    // Spread ints to bytes
-    x = [r, l];
-    for (i = 0; i < 8; i++) {
-        b[i] = (x[i>>>2] >>> (8*(3 - (i%4)))) % 256;
-        if (b[i] < 0) { b[i] += 256; } // unsigned
+        // Spread ints to bytes
+        x = [r, l];
+        for (i = 0; i < 8; i++) {
+            b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256;
+            if (b[i] < 0) { b[i] += 256; } // unsigned
+        }
+        return b;
     }
-    return b;
-}
 
-// Encrypt 16 bytes of text using passwd as key
-function encrypt(t) {
-    return enc8(t.slice(0,8)).concat(enc8(t.slice(8,16)));
-}
+    // Encrypt 16 bytes of text using passwd as key
+    function encrypt(t) {
+        return enc8(t.slice(0, 8)).concat(enc8(t.slice(8, 16)));
+    }
 
-setKeys(passwd);             // Setup keys
-return {'encrypt': encrypt}; // Public interface
+    setKeys(passwd);             // Setup keys
+    return {'encrypt': encrypt}; // Public interface
 
 } // function DES

+ 677 - 713
include/display.js

@@ -6,765 +6,729 @@
  * See README.md for usage and integration instructions.
  */
 
-/*jslint browser: true, white: false, bitwise: false */
+/*jslint browser: true, white: false */
 /*global Util, Base64, changeCursor */
 
-function Display(defaults) {
-"use strict";
-
-var that           = {},  // Public API methods
-    conf           = {},  // Configuration attributes
-
-    // Private Display namespace variables
-    c_ctx          = null,
-    c_forceCanvas  = false,
-
-    // Queued drawing actions for in-order rendering
-    renderQ        = [],
-
-    // Predefine function variables (jslint)
-    imageDataGet, rgbImageData, bgrxImageData, cmapImageData,
-    setFillColor, rescale, scan_renderQ,
-
-    // The full frame buffer (logical canvas) size
-    fb_width        = 0,
-    fb_height       = 0,
-    // The visible "physical canvas" viewport
-    viewport       = {'x': 0, 'y': 0, 'w' : 0, 'h' : 0 },
-    cleanRect      = {'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1},
-
-    c_prevStyle    = "",
-    tile           = null,
-    tile16x16      = null,
-    tile_x         = 0,
-    tile_y         = 0;
-
-
-// Configuration attributes
-Util.conf_defaults(conf, that, defaults, [
-    ['target',      'wo', 'dom',  null, 'Canvas element for rendering'],
-    ['context',     'ro', 'raw',  null, 'Canvas 2D context for rendering (read-only)'],
-    ['logo',        'rw', 'raw',  null, 'Logo to display when cleared: {"width": width, "height": height, "data": data}'],
-    ['true_color',  'rw', 'bool', true, 'Use true-color pixel data'],
-    ['colourMap',   'rw', 'arr',  [], 'Colour map array (when not true-color)'],
-    ['scale',       'rw', 'float', 1.0, 'Display area scale factor 0.0 - 1.0'],
-    ['viewport',    'rw', 'bool', false, 'Use a viewport set with viewportChange()'],
-    ['width',       'rw', 'int', null, 'Display area width'],
-    ['height',      'rw', 'int', null, 'Display area height'],
-
-    ['render_mode', 'ro', 'str', '', 'Canvas rendering mode (read-only)'],
-
-    ['prefer_js',   'rw', 'str', null, 'Prefer Javascript over canvas methods'],
-    ['cursor_uri',  'rw', 'raw', null, 'Can we render cursor using data URI']
-    ]);
+var Display;
 
-// Override some specific getters/setters
-that.get_context = function () { return c_ctx; };
+(function () {
+    "use strict";
 
-that.set_scale = function(scale) { rescale(scale); };
+    Display = function (defaults) {
+        this._drawCtx = null;
+        this._c_forceCanvas = false;
 
-that.set_width = function (val) { that.resize(val, fb_height); };
-that.get_width = function() { return fb_width; };
+        this._renderQ = [];  // queue drawing actions for in-oder rendering
 
-that.set_height = function (val) { that.resize(fb_width, val); };
-that.get_height = function() { return fb_height; };
+        // the full frame buffer (logical canvas) size
+        this._fb_width = 0;
+        this._fb_height = 0;
 
+        // the visible "physical canvas" viewport
+        this._viewportLoc = { 'x': 0, 'y': 0, 'w': 0, 'h': 0 };
+        this._cleanRect = { 'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1 };
 
+        this._prevDrawStyle = "";
+        this._tile = null;
+        this._tile16x16 = null;
+        this._tile_x = 0;
+        this._tile_y = 0;
 
-//
-// Private functions
-//
+        Util.set_defaults(this, defaults, {
+            'true_color': true,
+            'colourMap': [],
+            'scale': 1.0,
+            'viewport': false,
+            'render_mode': ''
+        });
 
-// Create the public API interface
-function constructor() {
-    Util.Debug(">> Display.constructor");
+        Util.Debug(">> Display.constructor");
 
-    var c, func, i, curDat, curSave,
-        has_imageData = false, UE = Util.Engine;
+        if (!this._target) {
+            throw new Error("Target must be set");
+        }
 
-    if (! conf.target) { throw("target must be set"); }
+        if (typeof this._target === 'string') {
+            throw new Error('target must be a DOM element');
+        }
 
-    if (typeof conf.target === 'string') {
-        throw("target must be a DOM element");
-    }
+        if (!this._target.getContext) {
+            throw new Error("no getContext method");
+        }
 
-    c = conf.target;
+        if (!this._drawCtx) {
+            this._drawCtx = this._target.getContext('2d');
+        }
 
-    if (! c.getContext) { throw("no getContext method"); }
+        Util.Debug("User Agent: " + navigator.userAgent);
+        if (Util.Engine.gecko) { Util.Debug("Browser: gecko " + Util.Engine.gecko); }
+        if (Util.Engine.webkit) { Util.Debug("Browser: webkit " + Util.Engine.webkit); }
+        if (Util.Engine.trident) { Util.Debug("Browser: trident " + Util.Engine.trident); }
+        if (Util.Engine.presto) { Util.Debug("Browser: presto " + Util.Engine.presto); }
 
-    if (! c_ctx) { c_ctx = c.getContext('2d'); }
+        this.clear();
 
-    Util.Debug("User Agent: " + navigator.userAgent);
-    if (UE.gecko) { Util.Debug("Browser: gecko " + UE.gecko); }
-    if (UE.webkit) { Util.Debug("Browser: webkit " + UE.webkit); }
-    if (UE.trident) { Util.Debug("Browser: trident " + UE.trident); }
-    if (UE.presto) { Util.Debug("Browser: presto " + UE.presto); }
+        // Check canvas features
+        if ('createImageData' in this._drawCtx) {
+            this._render_mode = 'canvas rendering';
+        } else {
+            throw new Error("Canvas does not support createImageData");
+        }
 
-    that.clear();
+        if (this._prefer_js === null) {
+            Util.Info("Prefering javascript operations");
+            this._prefer_js = true;
+        }
 
-    // Check canvas features
-    if ('createImageData' in c_ctx) {
-        conf.render_mode = "canvas rendering";
-    } else {
-        throw("Canvas does not support createImageData");
-    }
-    if (conf.prefer_js === null) {
-        Util.Info("Prefering javascript operations");
-        conf.prefer_js = true;
-    }
+        // Determine browser support for setting the cursor via data URI scheme
+        var curDat = [];
+        for (var i = 0; i < 8 * 8 * 4; i++) {
+            curDat.push(255);
+        }
+        try {
+            var curSave = this._target.style.cursor;
+            Display.changeCursor(this._target, curDat, curDat, 2, 2, 8, 8);
+            if (this._target.style.cursor) {
+                if (this._cursor_uri === null) {
+                    this._cursor_uri = true;
+                }
+                Util.Info("Data URI scheme cursor supported");
+            } else {
+                if (this._cursor_uri === null) {
+                    this._cursor_uri = false;
+                }
+                Util.Warn("Data URI scheme cursor not supported");
+            }
+            this._target.style.cursor = curSave;
+        } catch (exc) {
+            Util.Error("Data URI scheme cursor test exception: " + exc);
+            this._cursor_uri = false;
+        }
 
-    // Initialize cached tile imageData
-    tile16x16 = c_ctx.createImageData(16, 16);
+        Util.Debug("<< Display.constructor");
+    };
 
-    /*
-     * Determine browser support for setting the cursor via data URI
-     * scheme
-     */
-    curDat = [];
-    for (i=0; i < 8 * 8 * 4; i += 1) {
-        curDat.push(255);
-    }
-    try {
-        curSave = c.style.cursor;
-        changeCursor(conf.target, curDat, curDat, 2, 2, 8, 8);
-        if (c.style.cursor) {
-            if (conf.cursor_uri === null) {
-                conf.cursor_uri = true;
+    Display.prototype = {
+        // Public methods
+        viewportChange: function (deltaX, deltaY, width, height) {
+            var vp = this._viewportLoc;
+            var cr = this._cleanRect;
+            var canvas = this._target;
+
+            if (!this._viewport) {
+                Util.Debug("Setting viewport to full display region");
+                deltaX = -vp.w;  // clamped later of out of bounds
+                deltaY = -vp.h;
+                width = this._fb_width;
+                height = this._fb_height;
             }
-            Util.Info("Data URI scheme cursor supported");
-        } else {
-            if (conf.cursor_uri === null) {
-                conf.cursor_uri = false;
+
+            if (typeof(deltaX) === "undefined") { deltaX = 0; }
+            if (typeof(deltaY) === "undefined") { deltaY = 0; }
+            if (typeof(width) === "undefined") { width = vp.w; }
+            if (typeof(height) === "undefined") { height = vp.h; }
+
+            // Size change
+            if (width > this._fb_width) { width = this._fb_width; }
+            if (height > this._fb_height) { height = this._fb_height; }
+
+            if (vp.w !== width || vp.h !== height) {
+                // Change width
+                if (width < vp.w &&  cr.x2 > vp.x + width - 1) {
+                    cr.x2 = vp.x + width - 1;
+                }
+                vp.w = width;
+
+                // Change height
+                if (height < vp.h &&  cr.y2 > vp.y + height - 1) {
+                    cr.y2 = vp.y + height - 1;
+                }
+                vp.h = height;
+
+                var saveImg = null;
+                if (vp.w > 0 && vp.h > 0 && canvas.width > 0 && canvas.height > 0) {
+                    var img_width = canvas.width < vp.w ? canvas.width : vp.w;
+                    var img_height = canvas.height < vp.h ? canvas.height : vp.h;
+                    saveImg = this._drawCtx.getImageData(0, 0, img_width, img_height);
+                }
+
+                canvas.width = vp.w;
+                canvas.height = vp.h;
+
+                if (saveImg) {
+                    this._drawCtx.putImageData(saveImg, 0, 0);
+                }
             }
-            Util.Warn("Data URI scheme cursor not supported");
-        }
-        c.style.cursor = curSave;
-    } catch (exc2) { 
-        Util.Error("Data URI scheme cursor test exception: " + exc2);
-        conf.cursor_uri = false;
-    }
-
-    Util.Debug("<< Display.constructor");
-    return that ;
-}
-
-rescale = function(factor) {
-    var c, tp, x, y, 
-        properties = ['transform', 'WebkitTransform', 'MozTransform', null];
-    c = conf.target;
-    tp = properties.shift();
-    while (tp) {
-        if (typeof c.style[tp] !== 'undefined') {
-            break;
-        }
-        tp = properties.shift();
-    }
-
-    if (tp === null) {
-        Util.Debug("No scaling support");
-        return;
-    }
-
-
-    if (typeof(factor) === "undefined") {
-        factor = conf.scale;
-    } else if (factor > 1.0) {
-        factor = 1.0;
-    } else if (factor < 0.1) {
-        factor = 0.1;
-    }
-
-    if (conf.scale === factor) {
-        //Util.Debug("Display already scaled to '" + factor + "'");
-        return;
-    }
-
-    conf.scale = factor;
-    x = c.width - c.width * factor;
-    y = c.height - c.height * factor;
-    c.style[tp] = "scale(" + conf.scale + ") translate(-" + x + "px, -" + y + "px)";
-};
-
-setFillColor = function(color) {
-    var bgr, newStyle;
-    if (conf.true_color) {
-        bgr = color;
-    } else {
-        bgr = conf.colourMap[color[0]];
-    }
-    newStyle = "rgb(" + bgr[2] + "," + bgr[1] + "," + bgr[0] + ")";
-    if (newStyle !== c_prevStyle) {
-        c_ctx.fillStyle = newStyle;
-        c_prevStyle = newStyle;
-    }
-};
-
-
-//
-// Public API interface functions
-//
-
-// Shift and/or resize the visible viewport
-that.viewportChange = function(deltaX, deltaY, width, height) {
-    var c = conf.target, v = viewport, cr = cleanRect,
-        saveImg = null, saveStyle, x1, y1, vx2, vy2, w, h;
-
-    if (!conf.viewport) {
-        Util.Debug("Setting viewport to full display region");
-        deltaX = -v.w; // Clamped later if out of bounds
-        deltaY = -v.h; // Clamped later if out of bounds
-        width = fb_width;
-        height = fb_height;
-    }
-
-    if (typeof(deltaX) === "undefined") { deltaX = 0; }
-    if (typeof(deltaY) === "undefined") { deltaY = 0; }
-    if (typeof(width) === "undefined") { width = v.w; }
-    if (typeof(height) === "undefined") { height = v.h; }
-
-    // Size change
-
-    if (width > fb_width) { width = fb_width; }
-    if (height > fb_height) { height = fb_height; }
-
-    if ((v.w !== width) || (v.h !== height)) {
-        // Change width
-        if ((width < v.w) && (cr.x2 > v.x + width -1)) {
-            cr.x2 = v.x + width - 1;
-        }
-        v.w = width;
 
-        // Change height
-        if ((height < v.h) && (cr.y2 > v.y + height -1)) {
-            cr.y2 = v.y + height - 1;
-        }
-        v.h = height;
+            var vx2 = vp.x + vp.w - 1;
+            var vy2 = vp.y + vp.h - 1;
 
+            // Position change
 
-        if (v.w > 0 && v.h > 0 && c.width > 0 && c.height > 0) {
-            saveImg = c_ctx.getImageData(0, 0,
-                    (c.width < v.w) ? c.width : v.w,
-                    (c.height < v.h) ? c.height : v.h);
-        }
+            if (deltaX < 0 && vp.x + deltaX < 0) {
+                deltaX = -vp.x;
+            }
+            if (vx2 + deltaX >= this._fb_width) {
+                deltaX -= vx2 + deltaX - this._fb_width + 1;
+            }
 
-        c.width = v.w;
-        c.height = v.h;
+            if (vp.y + deltaY < 0) {
+                deltaY = -vp.y;
+            }
+            if (vy2 + deltaY >= this._fb_height) {
+                deltaY -= (vy2 + deltaY - this._fb_height + 1);
+            }
 
-        if (saveImg) {
-            c_ctx.putImageData(saveImg, 0, 0);
-        }
-    }
-
-    vx2 = v.x + v.w - 1;
-    vy2 = v.y + v.h - 1;
-
-
-    // Position change
-
-    if ((deltaX < 0) && ((v.x + deltaX) < 0)) {
-        deltaX = - v.x;
-    }
-    if ((vx2 + deltaX) >= fb_width) {
-        deltaX -= ((vx2 + deltaX) - fb_width + 1);
-    }
-
-    if ((v.y + deltaY) < 0) {
-        deltaY = - v.y;
-    }
-    if ((vy2 + deltaY) >= fb_height) {
-        deltaY -= ((vy2 + deltaY) - fb_height + 1);
-    }
-
-    if ((deltaX === 0) && (deltaY === 0)) {
-        //Util.Debug("skipping viewport change");
-        return;
-    }
-    Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
-
-    v.x += deltaX;
-    vx2 += deltaX;
-    v.y += deltaY;
-    vy2 += deltaY;
-
-    // Update the clean rectangle
-    if (v.x > cr.x1) {
-        cr.x1 = v.x;
-    }
-    if (vx2 < cr.x2) {
-        cr.x2 = vx2;
-    }
-    if (v.y > cr.y1) {
-        cr.y1 = v.y;
-    }
-    if (vy2 < cr.y2) {
-        cr.y2 = vy2;
-    }
-
-    if (deltaX < 0) {
-        // Shift viewport left, redraw left section
-        x1 = 0;
-        w = - deltaX;
-    } else {
-        // Shift viewport right, redraw right section
-        x1 = v.w - deltaX;
-        w = deltaX;
-    }
-    if (deltaY < 0) {
-        // Shift viewport up, redraw top section
-        y1 = 0;
-        h = - deltaY;
-    } else {
-        // Shift viewport down, redraw bottom section
-        y1 = v.h - deltaY;
-        h = deltaY;
-    }
-
-    // Copy the valid part of the viewport to the shifted location
-    saveStyle = c_ctx.fillStyle;
-    c_ctx.fillStyle = "rgb(255,255,255)";
-    if (deltaX !== 0) {
-        //that.copyImage(0, 0, -deltaX, 0, v.w, v.h);
-        //that.fillRect(x1, 0, w, v.h, [255,255,255]);
-        c_ctx.drawImage(c, 0, 0, v.w, v.h, -deltaX, 0, v.w, v.h);
-        c_ctx.fillRect(x1, 0, w, v.h);
-    }
-    if (deltaY !== 0) {
-        //that.copyImage(0, 0, 0, -deltaY, v.w, v.h);
-        //that.fillRect(0, y1, v.w, h, [255,255,255]);
-        c_ctx.drawImage(c, 0, 0, v.w, v.h, 0, -deltaY, v.w, v.h);
-        c_ctx.fillRect(0, y1, v.w, h);
-    }
-    c_ctx.fillStyle = saveStyle;
-};
-
-
-// Return a map of clean and dirty areas of the viewport and reset the
-// tracking of clean and dirty areas.
-//
-// Returns: {'cleanBox':   {'x': x, 'y': y, 'w': w, 'h': h},
-//           'dirtyBoxes': [{'x': x, 'y': y, 'w': w, 'h': h}, ...]}
-that.getCleanDirtyReset = function() {
-    var v = viewport, c = cleanRect, cleanBox, dirtyBoxes = [],
-        vx2 = v.x + v.w - 1, vy2 = v.y + v.h - 1;
-
-
-    // Copy the cleanRect
-    cleanBox = {'x': c.x1, 'y': c.y1,
-                'w': c.x2 - c.x1 + 1, 'h': c.y2 - c.y1 + 1};
-
-    if ((c.x1 >= c.x2) || (c.y1 >= c.y2)) {
-        // Whole viewport is dirty
-        dirtyBoxes.push({'x': v.x, 'y': v.y, 'w': v.w, 'h': v.h});
-    } else {
-        // Redraw dirty regions
-        if (v.x < c.x1) {
-            // left side dirty region
-            dirtyBoxes.push({'x': v.x, 'y': v.y,
-                             'w': c.x1 - v.x + 1, 'h': v.h});
-        }
-        if (vx2 > c.x2) {
-            // right side dirty region
-            dirtyBoxes.push({'x': c.x2 + 1, 'y': v.y,
-                             'w': vx2 - c.x2, 'h': v.h});
-        }
-        if (v.y < c.y1) {
-            // top/middle dirty region
-            dirtyBoxes.push({'x': c.x1, 'y': v.y,
-                             'w': c.x2 - c.x1 + 1, 'h': c.y1 - v.y});
-        }
-        if (vy2 > c.y2) {
-            // bottom/middle dirty region
-            dirtyBoxes.push({'x': c.x1, 'y': c.y2 + 1,
-                             'w': c.x2 - c.x1 + 1, 'h': vy2 - c.y2});
-        }
-    }
-
-    // Reset the cleanRect to the whole viewport
-    cleanRect = {'x1': v.x, 'y1': v.y,
-                 'x2': v.x + v.w - 1, 'y2': v.y + v.h - 1};
-
-    return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes};
-};
-
-// Translate viewport coordinates to absolute coordinates
-that.absX = function(x) {
-    return x + viewport.x;
-};
-that.absY = function(y) {
-    return y + viewport.y;
-};
-
-
-that.resize = function(width, height) {
-    c_prevStyle    = "";
-
-    fb_width = width;
-    fb_height = height;
-
-    rescale(conf.scale);
-    that.viewportChange();
-};
-
-that.clear = function() {
-
-    if (conf.logo) {
-        that.resize(conf.logo.width, conf.logo.height);
-        that.blitStringImage(conf.logo.data, 0, 0);
-    } else {
-        that.resize(640, 20);
-        c_ctx.clearRect(0, 0, viewport.w, viewport.h);
-    }
-
-    renderQ = [];
-
-    // No benefit over default ("source-over") in Chrome and firefox
-    //c_ctx.globalCompositeOperation = "copy";
-};
-
-that.fillRect = function(x, y, width, height, color) {
-    setFillColor(color);
-    c_ctx.fillRect(x - viewport.x, y - viewport.y, width, height);
-};
-
-that.copyImage = function(old_x, old_y, new_x, new_y, w, h) {
-    var x1 = old_x - viewport.x, y1 = old_y - viewport.y,
-        x2 = new_x - viewport.x, y2 = new_y  - viewport.y;
-    c_ctx.drawImage(conf.target, x1, y1, w, h, x2, y2, w, h);
-};
-
-
-// Start updating a tile
-that.startTile = function(x, y, width, height, color) {
-    var data, bgr, red, green, blue, i;
-    tile_x = x;
-    tile_y = y;
-    if ((width === 16) && (height === 16)) {
-        tile = tile16x16;
-    } else {
-        tile = c_ctx.createImageData(width, height);
-    }
-    data = tile.data;
-    if (conf.prefer_js) {
-        if (conf.true_color) {
-            bgr = color;
-        } else {
-            bgr = conf.colourMap[color[0]];
-        }
-        red = bgr[2];
-        green = bgr[1];
-        blue = bgr[0];
-        for (i = 0; i < (width * height * 4); i+=4) {
-            data[i    ] = red;
-            data[i + 1] = green;
-            data[i + 2] = blue;
-            data[i + 3] = 255;
-        }
-    } else {
-        that.fillRect(x, y, width, height, color);
-    }
-};
-
-// Update sub-rectangle of the current tile
-that.subTile = function(x, y, w, h, color) {
-    var data, p, bgr, red, green, blue, width, j, i, xend, yend;
-    if (conf.prefer_js) {
-        data = tile.data;
-        width = tile.width;
-        if (conf.true_color) {
-            bgr = color;
-        } else {
-            bgr = conf.colourMap[color[0]];
-        }
-        red = bgr[2];
-        green = bgr[1];
-        blue = bgr[0];
-        xend = x + w;
-        yend = y + h;
-        for (j = y; j < yend; j += 1) {
-            for (i = x; i < xend; i += 1) {
-                p = (i + (j * width) ) * 4;
-                data[p    ] = red;
-                data[p + 1] = green;
-                data[p + 2] = blue;
-                data[p + 3] = 255;
-            }   
-        } 
-    } else {
-        that.fillRect(tile_x + x, tile_y + y, w, h, color);
-    }
-};
-
-// Draw the current tile to the screen
-that.finishTile = function() {
-    if (conf.prefer_js) {
-        c_ctx.putImageData(tile, tile_x - viewport.x, tile_y - viewport.y);
-    }
-    // else: No-op, if not prefer_js then already done by setSubTile
-};
-
-rgbImageData = function(x, y, vx, vy, width, height, arr, offset) {
-    var img, i, j, data;
-    /*
-    if ((x - v.x >= v.w) || (y - v.y >= v.h) ||
-        (x - v.x + width < 0) || (y - v.y + height < 0)) {
-        // Skipping because outside of viewport
-        return;
-    }
-    */
-    img = c_ctx.createImageData(width, height);
-    data = img.data;
-    for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+3) {
-        data[i    ] = arr[j    ];
-        data[i + 1] = arr[j + 1];
-        data[i + 2] = arr[j + 2];
-        data[i + 3] = 255; // Set Alpha
-    }
-    c_ctx.putImageData(img, x - vx, y - vy);
-};
-
-bgrxImageData = function(x, y, vx, vy, width, height, arr, offset) {
-    var img, i, j, data;
-    /*
-    if ((x - v.x >= v.w) || (y - v.y >= v.h) ||
-        (x - v.x + width < 0) || (y - v.y + height < 0)) {
-        // Skipping because outside of viewport
-        return;
-    }
-    */
-    img = c_ctx.createImageData(width, height);
-    data = img.data;
-    for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+4) {
-        data[i    ] = arr[j + 2];
-        data[i + 1] = arr[j + 1];
-        data[i + 2] = arr[j    ];
-        data[i + 3] = 255; // Set Alpha
-    }
-    c_ctx.putImageData(img, x - vx, y - vy);
-};
-
-cmapImageData = function(x, y, vx, vy, width, height, arr, offset) {
-    var img, i, j, data, bgr, cmap;
-    img = c_ctx.createImageData(width, height);
-    data = img.data;
-    cmap = conf.colourMap;
-    for (i=0, j=offset; i < (width * height * 4); i+=4, j+=1) {
-        bgr = cmap[arr[j]];
-        data[i    ] = bgr[2];
-        data[i + 1] = bgr[1];
-        data[i + 2] = bgr[0];
-        data[i + 3] = 255; // Set Alpha
-    }
-    c_ctx.putImageData(img, x - vx, y - vy);
-};
-
-that.blitImage = function(x, y, width, height, arr, offset) {
-    if (conf.true_color) {
-        bgrxImageData(x, y, viewport.x, viewport.y, width, height, arr, offset);
-    } else {
-        cmapImageData(x, y, viewport.x, viewport.y, width, height, arr, offset);
-    }
-};
-
-that.blitRgbImage = function(x, y, width, height, arr, offset) {
-    if (conf.true_color) {
-        rgbImageData(x, y, viewport.x, viewport.y, width, height, arr, offset);
-    } else {
-        // prolly wrong...
-        cmapImageData(x, y, viewport.x, viewport.y, width, height, arr, offset);
-    }
-};
-
-that.blitStringImage = function(str, x, y) {
-    var img = new Image();
-    img.onload = function () {
-        c_ctx.drawImage(img, x - viewport.x, y - viewport.y);
-    };
-    img.src = str;
-};
-
-// Wrap ctx.drawImage but relative to viewport
-that.drawImage = function(img, x, y) {
-    c_ctx.drawImage(img, x - viewport.x, y - viewport.y);
-};
-
-that.renderQ_push = function(action) {
-    renderQ.push(action);
-    if (renderQ.length === 1) {
-        // If this can be rendered immediately it will be, otherwise
-        // the scanner will start polling the queue (every
-        // requestAnimationFrame interval)
-        scan_renderQ();
-    }
-};
-
-scan_renderQ = function() {
-    var a, ready = true;
-    while (ready && renderQ.length > 0) {
-        a = renderQ[0];
-        switch (a.type) {
-            case 'copy':
-                that.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height);
-                break;
-            case 'fill':
-                that.fillRect(a.x, a.y, a.width, a.height, a.color);
-                break;
-            case 'blit':
-                that.blitImage(a.x, a.y, a.width, a.height, a.data, 0);
-                break;
-            case 'blitRgb':
-                that.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0);
-                break;
-            case 'img':    
-                if (a.img.complete) {
-                    that.drawImage(a.img, a.x, a.y);
+            if (deltaX === 0 && deltaY === 0) {
+                return;
+            }
+            Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
+
+            vp.x += deltaX;
+            vx2 += deltaX;
+            vp.y += deltaY;
+            vy2 += deltaY;
+
+            // Update the clean rectangle
+            if (vp.x > cr.x1) {
+                cr.x1 = vp.x;
+            }
+            if (vx2 < cr.x2) {
+                cr.x2 = vx2;
+            }
+            if (vp.y > cr.y1) {
+                cr.y1 = vp.y;
+            }
+            if (vy2 < cr.y2) {
+                cr.y2 = vy2;
+            }
+
+            var x1, w;
+            if (deltaX < 0) {
+                // Shift viewport left, redraw left section
+                x1 = 0;
+                w = -deltaX;
+            } else {
+                // Shift viewport right, redraw right section
+                x1 = vp.w - deltaX;
+                w = deltaX;
+            }
+
+            var y1, h;
+            if (deltaY < 0) {
+                // Shift viewport up, redraw top section
+                y1 = 0;
+                h = -deltaY;
+            } else {
+                // Shift viewport down, redraw bottom section
+                y1 = vp.h - deltaY;
+                h = deltaY;
+            }
+
+            // Copy the valid part of the viewport to the shifted location
+            var saveStyle = this._drawCtx.fillStyle;
+            this._drawCtx.fillStyle = "rgb(255,255,255)";
+            if (deltaX !== 0) {
+                this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, -deltaX, 0, vp.w, vp.h);
+                this._drawCtx.fillRect(x1, 0, w, vp.h);
+            }
+            if (deltaY !== 0) {
+                this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, 0, -deltaY, vp.w, vp.h);
+                this._drawCtx.fillRect(0, y1, vp.w, h);
+            }
+            this._drawCtx.fillStyle = saveStyle;
+        },
+
+        // Return a map of clean and dirty areas of the viewport and reset the
+        // tracking of clean and dirty areas
+        //
+        // Returns: { 'cleanBox': { 'x': x, 'y': y, 'w': w, 'h': h},
+        //            'dirtyBoxes': [{ 'x': x, 'y': y, 'w': w, 'h': h }, ...] }
+        getCleanDirtyReset: function () {
+            var vp = this._viewportLoc;
+            var cr = this._cleanRect;
+
+            var cleanBox = { 'x': cr.x1, 'y': cr.y1,
+                             'w': cr.x2 - cr.x1 + 1, 'h': cr.y2 - cr.y1 + 1 };
+
+            var dirtyBoxes = [];
+            if (cr.x1 >= cr.x2 || cr.y1 >= cr.y2) {
+                // Whole viewport is dirty
+                dirtyBoxes.push({ 'x': vp.x, 'y': vp.y, 'w': vp.w, 'h': vp.h });
+            } else {
+                // Redraw dirty regions
+                var vx2 = vp.x + vp.w - 1;
+                var vy2 = vp.y + vp.h - 1;
+
+                if (vp.x < cr.x1) {
+                    // left side dirty region
+                    dirtyBoxes.push({'x': vp.x, 'y': vp.y,
+                                     'w': cr.x1 - vp.x + 1, 'h': vp.h});
+                }
+                if (vx2 > cr.x2) {
+                    // right side dirty region
+                    dirtyBoxes.push({'x': cr.x2 + 1, 'y': vp.y,
+                                     'w': vx2 - cr.x2, 'h': vp.h});
+                }
+                if(vp.y < cr.y1) {
+                    // top/middle dirty region
+                    dirtyBoxes.push({'x': cr.x1, 'y': vp.y,
+                                     'w': cr.x2 - cr.x1 + 1, 'h': cr.y1 - vp.y});
+                }
+                if (vy2 > cr.y2) {
+                    // bottom/middle dirty region
+                    dirtyBoxes.push({'x': cr.x1, 'y': cr.y2 + 1,
+                                     'w': cr.x2 - cr.x1 + 1, 'h': vy2 - cr.y2});
+                }
+            }
+
+            this._cleanRect = {'x1': vp.x, 'y1': vp.y,
+                               'x2': vp.x + vp.w - 1, 'y2': vp.y + vp.h - 1};
+
+            return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes};
+        },
+
+        absX: function (x) {
+            return x + this._viewportLoc.x;
+        },
+
+        absY: function (y) {
+            return y + this._viewportLoc.y;
+        },
+
+        resize: function (width, height) {
+            this._prevDrawStyle = "";
+
+            this._fb_width = width;
+            this._fb_height = height;
+
+            this._rescale(this._scale);
+
+            this.viewportChange();
+        },
+
+        clear: function () {
+            if (this._logo) {
+                this.resize(this._logo.width, this._logo.height);
+                this.blitStringImage(this._logo.data, 0, 0);
+            } else {
+                this.resize(640, 20);
+                this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h);
+            }
+
+            this._renderQ = [];
+        },
+
+        fillRect: function (x, y, width, height, color) {
+            this._setFillColor(color);
+            this._drawCtx.fillRect(x - this._viewportLoc.x, y - this._viewportLoc.y, width, height);
+        },
+
+        copyImage: function (old_x, old_y, new_x, new_y, w, h) {
+            var x1 = old_x - this._viewportLoc.x;
+            var y1 = old_y - this._viewportLoc.y;
+            var x2 = new_x - this._viewportLoc.x;
+            var y2 = new_y - this._viewportLoc.y;
+
+            this._drawCtx.drawImage(this._target, x1, y1, w, h, x2, y2, w, h);
+        },
+
+        // start updating a tile
+        startTile: function (x, y, width, height, color) {
+            this._tile_x = x;
+            this._tile_y = y;
+            if (width === 16 && height === 16) {
+                this._tile = this._tile16x16;
+            } else {
+                this._tile = this._drawCtx.createImageData(width, height);
+            }
+
+            if (this._prefer_js) {
+                var bgr;
+                if (this._true_color) {
+                    bgr = color;
                 } else {
-                    // We need to wait for this image to 'load'
-                    // to keep things in-order
-                    ready = false;
+                    bgr = this._colourMap[color[0]];
                 }
-                break;
-        }
-        if (ready) {
-            a = renderQ.shift();
-        }
-    }
-    if (renderQ.length > 0) {
-        requestAnimFrame(scan_renderQ);
-    }
-};
+                var red = bgr[2];
+                var green = bgr[1];
+                var blue = bgr[0];
+
+                var data = this._tile.data;
+                for (var i = 0; i < width * height * 4; i += 4) {
+                    data[i] = red;
+                    data[i + 1] = green;
+                    data[i + 2] = blue;
+                    data[i + 3] = 255;
+                }
+            } else {
+                this.fillRect(x, y, width, height, color);
+            }
+        },
+
+        // update sub-rectangle of the current tile
+        subTile: function (x, y, w, h, color) {
+            if (this._prefer_js) {
+                var bgr;
+                if (this._true_color) {
+                    bgr = color;
+                } else {
+                    bgr = this._colourMap[color[0]];
+                }
+                var red = bgr[2];
+                var green = bgr[1];
+                var blue = bgr[0];
+                var xend = x + w;
+                var yend = y + h;
+
+                var data = this._tile.data;
+                var width = this._tile.width;
+                for (var j = y; j < yend; j++) {
+                    for (var i = x; i < xend; i++) {
+                        var p = (i + (j * width)) * 4;
+                        data[p] = red;
+                        data[p + 1] = green;
+                        data[p + 2] = blue;
+                        data[p + 3] = 255;
+                    }
+                }
+            } else {
+                this.fillRect(this._tile_x + x, this._tile_y + y, w, h, color);
+            }
+        },
 
+        // draw the current tile to the screen
+        finishTile: function () {
+            if (this._prefer_js) {
+                this._drawCtx.putImageData(this._tile, this._tile_x - this._viewportLoc.x,
+                                           this._tile_y - this._viewportLoc.y);
+            }
+            // else: No-op -- already done by setSubTile
+        },
 
-that.changeCursor = function(pixels, mask, hotx, hoty, w, h) {
-    if (conf.cursor_uri === false) {
-        Util.Warn("changeCursor called but no cursor data URI support");
-        return;
-    }
+        blitImage: function (x, y, width, height, arr, offset) {
+            if (this._true_color) {
+                this._bgrxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
+            } else {
+                this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
+            }
+        },
 
-    if (conf.true_color) {
-        changeCursor(conf.target, pixels, mask, hotx, hoty, w, h);
-    } else {
-        changeCursor(conf.target, pixels, mask, hotx, hoty, w, h, conf.colourMap);
-    }
-};
+        blitRgbImage: function (x, y , width, height, arr, offset) {
+            if (this._true_color) {
+                this._rgbImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
+            } else {
+                // probably wrong?
+                this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
+            }
+        },
+
+        blitStringImage: function (str, x, y) {
+            var img = new Image();
+            img.onload = function () {
+                this._drawCtx.drawImage(img, x - this._viewportLoc.x, y - this._viewportLoc.y);
+            }.bind(this);
+            img.src = str;
+            return img; // for debugging purposes
+        },
+
+        // wrap ctx.drawImage but relative to viewport
+        drawImage: function (img, x, y) {
+            this._drawCtx.drawImage(img, x - this._viewportLoc.x, y - this._viewportLoc.y);
+        },
+
+        renderQ_push: function (action) {
+            this._renderQ.push(action);
+            if (this._renderQ.length === 1) {
+                // If this can be rendered immediately it will be, otherwise
+                // the scanner will start polling the queue (every
+                // requestAnimationFrame interval)
+                this._scan_renderQ();
+            }
+        },
 
-that.defaultCursor = function() {
-    conf.target.style.cursor = "default";
-};
+        changeCursor: function (pixels, mask, hotx, hoty, w, h) {
+            if (this._cursor_uri === false) {
+                Util.Warn("changeCursor called but no cursor data URI support");
+                return;
+            }
 
-return constructor();  // Return the public API interface
+            if (this._true_color) {
+                Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h);
+            } else {
+                Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h, this._colourMap);
+            }
+        },
+
+        defaultCursor: function () {
+            this._target.style.cursor = "default";
+        },
+
+        // Overridden getters/setters
+        get_context: function () {
+            return this._drawCtx;
+        },
+
+        set_scale: function (scale) {
+            this._rescale(scale);
+        },
+
+        set_width: function (w) {
+            this.resize(w, this._fb_height);
+        },
+        get_width: function () {
+            return this._fb_width;
+        },
+
+        set_height: function (h) {
+            this.resize(this._fb_width, h);
+        },
+        get_height: function () {
+            return this._fb_height;
+        },
+
+        // Private Methods
+        _rescale: function (factor) {
+            var canvas = this._target;
+            var properties = ['transform', 'WebkitTransform', 'MozTransform'];
+            var transform_prop;
+            while ((transform_prop = properties.shift())) {
+                if (typeof canvas.style[transform_prop] !== 'undefined') {
+                    break;
+                }
+            }
 
-}  // End of Display()
+            if (transform_prop === null) {
+                Util.Debug("No scaling support");
+                return;
+            }
 
+            if (typeof(factor) === "undefined") {
+                factor = this._scale;
+            } else if (factor > 1.0) {
+                factor = 1.0;
+            } else if (factor < 0.1) {
+                factor = 0.1;
+            }
 
-/* Set CSS cursor property using data URI encoded cursor file */
-function changeCursor(target, pixels, mask, hotx, hoty, w0, h0, cmap) {
-    "use strict";
-    var cur = [], rgb, IHDRsz, RGBsz, ANDsz, XORsz, url, idx, alpha, x, y;
-    //Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w0: " + w0 + ", h0: " + h0);
-
-    var w = w0;
-    var h = h0;
-    if (h < w)
-        h = w;                 // increase h to make it square
-    else
-        w = h;                 // increace w to make it square
-
-    // Push multi-byte little-endian values
-    cur.push16le = function (num) {
-        this.push((num     ) & 0xFF,
-                  (num >> 8) & 0xFF  );
-    };
-    cur.push32le = function (num) {
-        this.push((num      ) & 0xFF,
-                  (num >>  8) & 0xFF,
-                  (num >> 16) & 0xFF,
-                  (num >> 24) & 0xFF  );
-    };
+            if (this._scale === factor) {
+                return;
+            }
+
+            this._scale = factor;
+            var x = canvas.width - (canvas.width * factor);
+            var y = canvas.height - (canvas.height * factor);
+            canvas.style[transform_prop] = 'scale(' + this._scale + ') translate(-' + x + 'px, -' + y + 'px)';
+        },
 
-    IHDRsz = 40;
-    RGBsz = w * h * 4;
-    XORsz = Math.ceil( (w * h) / 8.0 );
-    ANDsz = Math.ceil( (w * h) / 8.0 );
-
-    // Main header
-    cur.push16le(0);      // 0: Reserved
-    cur.push16le(2);      // 2: .CUR type
-    cur.push16le(1);      // 4: Number of images, 1 for non-animated ico
-
-    // Cursor #1 header (ICONDIRENTRY)
-    cur.push(w);          // 6: width
-    cur.push(h);          // 7: height
-    cur.push(0);          // 8: colors, 0 -> true-color
-    cur.push(0);          // 9: reserved
-    cur.push16le(hotx);   // 10: hotspot x coordinate
-    cur.push16le(hoty);   // 12: hotspot y coordinate
-    cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz);
-                          // 14: cursor data byte size
-    cur.push32le(22);     // 18: offset of cursor data in the file
-
-
-    // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
-    cur.push32le(IHDRsz); // 22: Infoheader size
-    cur.push32le(w);      // 26: Cursor width
-    cur.push32le(h*2);    // 30: XOR+AND height
-    cur.push16le(1);      // 34: number of planes
-    cur.push16le(32);     // 36: bits per pixel
-    cur.push32le(0);      // 38: Type of compression
-
-    cur.push32le(XORsz + ANDsz); // 43: Size of Image
-                                 // Gimp leaves this as 0
-
-    cur.push32le(0);      // 46: reserved
-    cur.push32le(0);      // 50: reserved
-    cur.push32le(0);      // 54: reserved
-    cur.push32le(0);      // 58: reserved
-
-    // 62: color data (RGBQUAD icColors[])
-    for (y = h-1; y >= 0; y -= 1) {
-        for (x = 0; x < w; x += 1) {
-            if (x >= w0 || y >= h0) {
-                cur.push(0);          // blue
-                cur.push(0);          // green
-                cur.push(0);          // red
-                cur.push(0);          // alpha
+        _setFillColor: function (color) {
+            var bgr;
+            if (this._true_color) {
+                bgr = color;
             } else {
-                idx = y * Math.ceil(w0 / 8) + Math.floor(x/8);
-                alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
-                if (cmap) {
-                    idx = (w0 * y) + x;
-                    rgb = cmap[pixels[idx]];
-                    cur.push(rgb[2]);          // blue
-                    cur.push(rgb[1]);          // green
-                    cur.push(rgb[0]);          // red
-                    cur.push(alpha);           // alpha
+                bgr = this._colourMap[color[0]];
+            }
+
+            var newStyle = 'rgb(' + bgr[2] + ',' + bgr[1] + ',' + bgr[0] + ')';
+            if (newStyle !== this._prevDrawStyle) {
+                this._drawCtx.fillStyle = newStyle;
+                this._prevDrawStyle = newStyle;
+            }
+        },
+
+        _rgbImageData: function (x, y, vx, vy, width, height, arr, offset) {
+            var img = this._drawCtx.createImageData(width, height);
+            var data = img.data;
+            for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 3) {
+                data[i]     = arr[j];
+                data[i + 1] = arr[j + 1];
+                data[i + 2] = arr[j + 2];
+                data[i + 3] = 255;  // Alpha
+            }
+            this._drawCtx.putImageData(img, x - vx, y - vy);
+        },
+
+        _bgrxImageData: function (x, y, vx, vy, width, height, arr, offset) {
+            var img = this._drawCtx.createImageData(width, height);
+            var data = img.data;
+            for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 4) {
+                data[i]     = arr[j + 2];
+                data[i + 1] = arr[j + 1];
+                data[i + 2] = arr[j];
+                data[i + 3] = 255;  // Alpha
+            }
+            this._drawCtx.putImageData(img, x - vx, y - vy);
+        },
+
+        _cmapImageData: function (x, y, vx, vy, width, height, arr, offset) {
+            var img = this._drawCtx.createImageData(width, height);
+            var data = img.data;
+            var cmap = this._colourMap;
+            for (var i = 0, j = offset; i < width * height * 4; i += 4, j++) {
+                var bgr = cmap[arr[j]];
+                data[i]     = bgr[2];
+                data[i + 1] = bgr[1];
+                data[i + 2] = bgr[0];
+                data[i + 3] = 255;  // Alpha
+            }
+            this._drawCtx.putImageData(img, x - vx, y - vy);
+        },
+
+        _scan_renderQ: function () {
+            var ready = true;
+            while (ready && this._renderQ.length > 0) {
+                var a = this._renderQ[0];
+                switch (a.type) {
+                    case 'copy':
+                        this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height);
+                        break;
+                    case 'fill':
+                        this.fillRect(a.x, a.y, a.width, a.height, a.color);
+                        break;
+                    case 'blit':
+                        this.blitImage(a.x, a.y, a.width, a.height, a.data, 0);
+                        break;
+                    case 'blitRgb':
+                        this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0);
+                        break;
+                    case 'img':
+                        if (a.img.complete) {
+                            this.drawImage(a.img, a.x, a.y);
+                        } else {
+                            // We need to wait for this image to 'load'
+                            // to keep things in-order
+                            ready = false;
+                        }
+                        break;
+                }
+
+                if (ready) {
+                    this._renderQ.shift();
+                }
+            }
+
+            if (this._renderQ.length > 0) {
+                requestAnimFrame(this._scan_renderQ.bind(this));
+            }
+        },
+    };
+
+    Util.make_properties(Display, [
+        ['target', 'wo', 'dom'],       // Canvas element for rendering
+        ['context', 'ro', 'raw'],      // Canvas 2D context for rendering (read-only)
+        ['logo', 'rw', 'raw'],         // Logo to display when cleared: {"width": w, "height": h, "data": data}
+        ['true_color', 'rw', 'bool'],  // Use true-color pixel data
+        ['colourMap', 'rw', 'arr'],    // Colour map array (when not true-color)
+        ['scale', 'rw', 'float'],      // Display area scale factor 0.0 - 1.0
+        ['viewport', 'rw', 'bool'],    // Use a viewport set with viewportChange()
+        ['width', 'rw', 'int'],        // Display area width
+        ['height', 'rw', 'int'],       // Display area height
+
+        ['render_mode', 'ro', 'str'],  // Canvas rendering mode (read-only)
+
+        ['prefer_js', 'rw', 'str'],    // Prefer Javascript over canvas methods
+        ['cursor_uri', 'rw', 'raw']    // Can we render cursor using data URI
+    ]);
+
+    // Class Methods
+    Display.changeCursor = function (target, pixels, mask, hotx, hoty, w0, h0, cmap) {
+        var w = w0;
+        var h = h0;
+        if (h < w) {
+            h = w;  // increase h to make it square
+        } else {
+            w = h;  // increase w to make it square
+        }
+
+        var cur = [];
+
+        // Push multi-byte little-endian values
+        cur.push16le = function (num) {
+            this.push(num & 0xFF, (num >> 8) & 0xFF);
+        };
+        cur.push32le = function (num) {
+            this.push(num & 0xFF,
+                      (num >> 8) & 0xFF,
+                      (num >> 16) & 0xFF,
+                      (num >> 24) & 0xFF);
+        };
+
+        var IHDRsz = 40;
+        var RGBsz = w * h * 4;
+        var XORsz = Math.ceil((w * h) / 8.0);
+        var ANDsz = Math.ceil((w * h) / 8.0);
+
+        cur.push16le(0);        // 0: Reserved
+        cur.push16le(2);        // 2: .CUR type
+        cur.push16le(1);        // 4: Number of images, 1 for non-animated ico
+
+        // Cursor #1 header (ICONDIRENTRY)
+        cur.push(w);            // 6: width
+        cur.push(h);            // 7: height
+        cur.push(0);            // 8: colors, 0 -> true-color
+        cur.push(0);            // 9: reserved
+        cur.push16le(hotx);     // 10: hotspot x coordinate
+        cur.push16le(hoty);     // 12: hotspot y coordinate
+        cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz);
+                                // 14: cursor data byte size
+        cur.push32le(22);       // 18: offset of cursor data in the file
+
+        // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
+        cur.push32le(IHDRsz);   // 22: InfoHeader size
+        cur.push32le(w);        // 26: Cursor width
+        cur.push32le(h * 2);    // 30: XOR+AND height
+        cur.push16le(1);        // 34: number of planes
+        cur.push16le(32);       // 36: bits per pixel
+        cur.push32le(0);        // 38: Type of compression
+
+        cur.push32le(XORsz + ANDsz);
+                                // 42: Size of Image
+        cur.push32le(0);        // 46: reserved
+        cur.push32le(0);        // 50: reserved
+        cur.push32le(0);        // 54: reserved
+        cur.push32le(0);        // 58: reserved
+
+        // 62: color data (RGBQUAD icColors[])
+        var y, x;
+        for (y = h - 1; y >= 0; y--) {
+            for (x = 0; x < w; x++) {
+                if (x >= w0 || y >= h0) {
+                    cur.push(0);  // blue
+                    cur.push(0);  // green
+                    cur.push(0);  // red
+                    cur.push(0);  // alpha
                 } else {
-                    idx = ((w0 * y) + x) * 4;
-                    cur.push(pixels[idx + 2]); // blue
-                    cur.push(pixels[idx + 1]); // green
-                    cur.push(pixels[idx    ]); // red
-                    cur.push(alpha);           // alpha
+                    var idx = y * Math.ceil(w0 / 8) + Math.floor(x / 8);
+                    var alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
+                    if (cmap) {
+                        idx = (w0 * y) + x;
+                        var rgb = cmap[pixels[idx]];
+                        cur.push(rgb[2]);  // blue
+                        cur.push(rgb[1]);  // green
+                        cur.push(rgb[0]);  // red
+                        cur.push(alpha);   // alpha
+                    }
                 }
             }
         }
-    }
 
-    // XOR/bitmask data (BYTE icXOR[])
-    // (ignored, just needs to be right size)
-    for (y = 0; y < h; y += 1) {
-        for (x = 0; x < Math.ceil(w / 8); x += 1) {
-            cur.push(0x00);
+        // XOR/bitmask data (BYTE icXOR[])
+        // (ignored, just needs to be the right size)
+        for (y = 0; y < h; y++) {
+            for (x = 0; x < Math.ceil(w / 8); x++) {
+                cur.push(0);
+            }
         }
-    }
 
-    // AND/bitmask data (BYTE icAND[])
-    // (ignored, just needs to be right size)
-    for (y = 0; y < h; y += 1) {
-        for (x = 0; x < Math.ceil(w / 8); x += 1) {
-            cur.push(0x00);
+        // AND/bitmask data (BYTE icAND[])
+        // (ignored, just needs to be the right size)
+        for (y = 0; y < h; y++) {
+            for (x = 0; x < Math.ceil(w / 8); x++) {
+                cur.push(0);
+            }
         }
-    }
 
-    url = "data:image/x-icon;base64," + Base64.encode(cur);
-    target.style.cursor = "url(" + url + ") " + hotx + " " + hoty + ", default";
-    //Util.Debug("<< changeCursor, cur.length: " + cur.length);
-}
+        var url = 'data:image/x-icon;base64,' + Base64.encode(cur);
+        target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
+    };
+})();

+ 363 - 376
include/input.js

@@ -5,397 +5,384 @@
  * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
  */
 
-/*jslint browser: true, white: false, bitwise: false */
+/*jslint browser: true, white: false */
 /*global window, Util */
 
+var Keyboard, Mouse;
+
+(function () {
+    "use strict";
+
+    //
+    // Keyboard event handler
+    //
+
+    Keyboard = function (defaults) {
+        this._keyDownList = [];         // List of depressed keys
+                                        // (even if they are happy)
+
+        Util.set_defaults(this, defaults, {
+            'target': document,
+            'focused': true
+        });
+
+        // create the keyboard handler
+        this._handler = new KeyEventDecoder(kbdUtil.ModifierSync(),
+            VerifyCharModifier( /* jshint newcap: false */
+                TrackKeyState(
+                    EscapeModifiers(this._handleRfbEvent.bind(this))
+                )
+            )
+        ); /* jshint newcap: true */
+
+        // keep these here so we can refer to them later
+        this._eventHandlers = {
+            'keyup': this._handleKeyUp.bind(this),
+            'keydown': this._handleKeyDown.bind(this),
+            'keypress': this._handleKeyPress.bind(this),
+            'blur': this._allKeysUp.bind(this)
+        };
+    };
+
+    Keyboard.prototype = {
+        // private methods
+
+        _handleRfbEvent: function (e) {
+            if (this._onKeyPress) {
+                Util.Debug("onKeyPress " + (e.type == 'keydown' ? "down" : "up") +
+                           ", keysym: " + e.keysym.keysym + "(" + e.keysym.keyname + ")");
+                this._onKeyPress(e.keysym.keysym, e.type == 'keydown');
+            }
+        },
 
-//
-// Keyboard event handler
-//
+        _handleKeyDown: function (e) {
+            if (!this._focused) { return true; }
 
-function Keyboard(defaults) {
-"use strict";
+            if (this._handler.keydown(e)) {
+                // Suppress bubbling/default actions
+                Util.stopEvent(e);
+                return false;
+            } else {
+                // Allow the event to bubble and become a keyPress event which
+                // will have the character code translated
+                return true;
+            }
+        },
 
-var that           = {},  // Public API methods
-    conf           = {},  // Configuration attributes
+        _handleKeyPress: function (e) {
+            if (!this._focused) { return true; }
 
-    keyDownList    = [];         // List of depressed keys 
-                                 // (even if they are happy)
+            if (this._handler.keypress(e)) {
+                // Suppress bubbling/default actions
+                Util.stopEvent(e);
+                return false;
+            } else {
+                // Allow the event to bubble and become a keyPress event which
+                // will have the character code translated
+                return true;
+            }
+        },
 
-// Configuration attributes
-Util.conf_defaults(conf, that, defaults, [
-    ['target',      'wo', 'dom',  document, 'DOM element that captures keyboard input'],
-    ['focused',     'rw', 'bool', true, 'Capture and send key events'],
+        _handleKeyUp: function (e) {
+            if (!this._focused) { return true; }
 
-    ['onKeyPress',  'rw', 'func', null, 'Handler for key press/release']
-    ]);
+            if (this._handler.keyup(e)) {
+                // Suppress bubbling/default actions
+                Util.stopEvent(e);
+                return false;
+            } else {
+                // Allow the event to bubble and become a keyPress event which
+                // will have the character code translated
+                return true;
+            }
+        },
+
+        _allKeysUp: function () {
+            Util.Debug(">> Keyboard.allKeysUp");
+            this._handler.releaseAll();
+            Util.Debug("<< Keyboard.allKeysUp");
+        },
+
+        // Public methods
+
+        grab: function () {
+            //Util.Debug(">> Keyboard.grab");
+            var c = this._target;
+
+            Util.addEvent(c, 'keydown', this._eventHandlers.keydown);
+            Util.addEvent(c, 'keyup', this._eventHandlers.keyup);
+            Util.addEvent(c, 'keypress', this._eventHandlers.keypress);
+
+            // Release (key up) if window loses focus
+            Util.addEvent(window, 'blur', this._eventHandlers.blur);
+
+            //Util.Debug("<< Keyboard.grab");
+        },
+
+        ungrab: function () {
+            //Util.Debug(">> Keyboard.ungrab");
+            var c = this._target;
+
+            Util.removeEvent(c, 'keydown', this._eventHandlers.keydown);
+            Util.removeEvent(c, 'keyup', this._eventHandlers.keyup);
+            Util.removeEvent(c, 'keypress', this._eventHandlers.keypress);
+            Util.removeEvent(window, 'blur', this._eventHandlers.blur);
 
+            // Release (key up) all keys that are in a down state
+            this._allKeysUp();
 
-// 
-// Private functions
-//
-
-/////// setup
-
-function onRfbEvent(evt) {
-    if (conf.onKeyPress) {
-        Util.Debug("onKeyPress " + (evt.type == 'keydown' ? "down" : "up")
-        + ", keysym: " + evt.keysym.keysym + "(" + evt.keysym.keyname + ")");
-        conf.onKeyPress(evt.keysym.keysym, evt.type == 'keydown');
-    }
-}
-
-// create the keyboard handler
-var k = KeyEventDecoder(kbdUtil.ModifierSync(),
-    VerifyCharModifier(
-        TrackKeyState(
-            EscapeModifiers(onRfbEvent)
-        )
-    )
-);
-
-function onKeyDown(e) {
-    if (! conf.focused) {
-        return true;
-    }
-    if (k.keydown(e)) {
-        // Suppress bubbling/default actions
-        Util.stopEvent(e);
-        return false;
-    } else {
-        // Allow the event to bubble and become a keyPress event which
-        // will have the character code translated
-        return true;
-    }
-}
-function onKeyPress(e) {
-    if (! conf.focused) {
-        return true;
-    }
-    if (k.keypress(e)) {
-        // Suppress bubbling/default actions
-        Util.stopEvent(e);
-        return false;
-    } else {
-        // Allow the event to bubble and become a keyPress event which
-        // will have the character code translated
-        return true;
-    }
-}
-
-function onKeyUp(e) {
-    if (! conf.focused) {
-        return true;
-    }
-    if (k.keyup(e)) {
-        // Suppress bubbling/default actions
-        Util.stopEvent(e);
-        return false;
-    } else {
-        // Allow the event to bubble and become a keyPress event which
-        // will have the character code translated
-        return true;
-    }
-}
-
-function onOther(e) {
-    k.syncModifiers(e);
-}
-
-function allKeysUp() {
-    Util.Debug(">> Keyboard.allKeysUp");
-
-    k.releaseAll();
-    Util.Debug("<< Keyboard.allKeysUp");
-}
-
-//
-// Public API interface functions
-//
-
-that.grab = function() {
-    //Util.Debug(">> Keyboard.grab");
-    var c = conf.target;
-
-    Util.addEvent(c, 'keydown', onKeyDown);
-    Util.addEvent(c, 'keyup', onKeyUp);
-    Util.addEvent(c, 'keypress', onKeyPress);
-
-    // Release (key up) if window loses focus
-    Util.addEvent(window, 'blur', allKeysUp);
-
-    //Util.Debug("<< Keyboard.grab");
-};
-
-that.ungrab = function() {
-    //Util.Debug(">> Keyboard.ungrab");
-    var c = conf.target;
-
-    Util.removeEvent(c, 'keydown', onKeyDown);
-    Util.removeEvent(c, 'keyup', onKeyUp);
-    Util.removeEvent(c, 'keypress', onKeyPress);
-    Util.removeEvent(window, 'blur', allKeysUp);
-
-    // Release (key up) all keys that are in a down state
-    allKeysUp();
-
-    //Util.Debug(">> Keyboard.ungrab");
-};
-
-that.sync = function(e) {
-    k.syncModifiers(e);
-}
-
-return that;  // Return the public API interface
-
-}  // End of Keyboard()
-
-
-//
-// Mouse event handler
-//
-
-function Mouse(defaults) {
-"use strict";
-
-var that           = {},  // Public API methods
-    conf           = {},  // Configuration attributes
-    mouseCaptured  = false;
-
-var doubleClickTimer = null,
-    lastTouchPos = null;
-
-// Configuration attributes
-Util.conf_defaults(conf, that, defaults, [
-    ['target',         'ro', 'dom',  document, 'DOM element that captures mouse input'],
-    ['notify',         'ro', 'func',  null, 'Function to call to notify whenever a mouse event is received'],
-    ['focused',        'rw', 'bool', true, 'Capture and send mouse clicks/movement'],
-    ['scale',          'rw', 'float', 1.0, 'Viewport scale factor 0.0 - 1.0'],
-
-    ['onMouseButton',  'rw', 'func', null, 'Handler for mouse button click/release'],
-    ['onMouseMove',    'rw', 'func', null, 'Handler for mouse movement'],
-    ['touchButton',    'rw', 'int', 1, 'Button mask (1, 2, 4) for touch devices (0 means ignore clicks)']
+            //Util.Debug(">> Keyboard.ungrab");
+        },
+
+        sync: function (e) {
+            this._handler.syncModifiers(e);
+        }
+    };
+
+    Util.make_properties(Keyboard, [
+        ['target',     'wo', 'dom'],  // DOM element that captures keyboard input
+        ['focused',    'rw', 'bool'], // Capture and send key events
+
+        ['onKeyPress', 'rw', 'func'] // Handler for key press/release
     ]);
 
-function captureMouse() {
-    // capturing the mouse ensures we get the mouseup event
-    if (conf.target.setCapture) {
-        conf.target.setCapture();
-    }
-
-    // some browsers give us mouseup events regardless,
-    // so if we never captured the mouse, we can disregard the event
-    mouseCaptured = true;
-}
-
-function releaseMouse() {
-    if (conf.target.releaseCapture) {
-        conf.target.releaseCapture();
-    }
-    mouseCaptured = false;
-}
-// 
-// Private functions
-//
-
-function resetDoubleClickTimer() {
-    doubleClickTimer = null;
-}
-
-function onMouseButton(e, down) {
-    var evt, pos, bmask;
-    if (! conf.focused) {
-        return true;
-    }
-
-    if (conf.notify) {
-        conf.notify(e);
-    }
-
-    evt = (e ? e : window.event);
-    pos = Util.getEventPosition(e, conf.target, conf.scale);
-
-    if (e.touches || e.changedTouches) {
-        // Touch device
-
-        // When two touches occur within 500 ms of each other and are
-        // closer than 20 pixels together a double click is triggered.
-        if (down == 1) {
-            if (doubleClickTimer == null) {
-                lastTouchPos = pos;
-            } else {
-                clearTimeout(doubleClickTimer); 
+    //
+    // Mouse event handler
+    //
+
+    Mouse = function (defaults) {
+        this._mouseCaptured  = false;
+
+        this._doubleClickTimer = null;
+        this._lastTouchPos = null;
+
+        // Configuration attributes
+        Util.set_defaults(this, defaults, {
+            'target': document,
+            'focused': true,
+            'scale': 1.0,
+            'touchButton': 1
+        });
+
+        this._eventHandlers = {
+            'mousedown': this._handleMouseDown.bind(this),
+            'mouseup': this._handleMouseUp.bind(this),
+            'mousemove': this._handleMouseMove.bind(this),
+            'mousewheel': this._handleMouseWheel.bind(this),
+            'mousedisable': this._handleMouseDisable.bind(this)
+        };
+    };
+
+    Mouse.prototype = {
+        // private methods
+        _captureMouse: function () {
+            // capturing the mouse ensures we get the mouseup event
+            if (this._target.setCapture) {
+                this._target.setCapture();
+            }
+
+            // some browsers give us mouseup events regardless,
+            // so if we never captured the mouse, we can disregard the event
+            this._mouseCaptured = true;
+        },
+
+        _releaseMouse: function () {
+            if (this._target.releaseCapture) {
+                this._target.releaseCapture();
+            }
+            this._mouseCaptured = false;
+        },
+
+        _resetDoubleClickTimer: function () {
+            this._doubleClickTimer = null;
+        },
 
-                // When the distance between the two touches is small enough
-                // force the position of the latter touch to the position of
-                // the first.
+        _handleMouseButton: function (e, down) {
+            if (!this._focused) { return true; }
 
-                var xs = lastTouchPos.x - pos.x;
-                var ys = lastTouchPos.y - pos.y;
-                var d = Math.sqrt((xs * xs) + (ys * ys));
+            if (this._notify) {
+                this._notify(e);
+            }
 
-                // The goal is to trigger on a certain physical width, the
-                // devicePixelRatio brings us a bit closer but is not optimal.
-                if (d < 20 * window.devicePixelRatio) {
-                    pos = lastTouchPos;
+            var evt = (e ? e : window.event);
+            var pos = Util.getEventPosition(e, this._target, this._scale);
+
+            var bmask;
+            if (e.touches || e.changedTouches) {
+                // Touch device
+
+                // When two touches occur within 500 ms of each other and are
+                // closer than 20 pixels together a double click is triggered.
+                if (down == 1) {
+                    if (this._doubleClickTimer === null) {
+                        this._lastTouchPos = pos;
+                    } else {
+                        clearTimeout(this._doubleClickTimer);
+
+                        // When the distance between the two touches is small enough
+                        // force the position of the latter touch to the position of
+                        // the first.
+
+                        var xs = this._lastTouchPos.x - pos.x;
+                        var ys = this._lastTouchPos.y - pos.y;
+                        var d = Math.sqrt((xs * xs) + (ys * ys));
+
+                        // The goal is to trigger on a certain physical width, the
+                        // devicePixelRatio brings us a bit closer but is not optimal.
+                        if (d < 20 * window.devicePixelRatio) {
+                            pos = this._lastTouchPos;
+                        }
+                    }
+                    this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500);
                 }
+                bmask = this._touchButton;
+                // If bmask is set
+            } else if (evt.which) {
+                /* everything except IE */
+                bmask = 1 << evt.button;
+            } else {
+                /* IE including 9 */
+                bmask = (evt.button & 0x1) +      // Left
+                        (evt.button & 0x2) * 2 +  // Right
+                        (evt.button & 0x4) / 2;   // Middle
             }
-            doubleClickTimer = setTimeout(resetDoubleClickTimer, 500);
+
+            if (this._onMouseButton) {
+                Util.Debug("onMouseButton " + (down ? "down" : "up") +
+                           ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask);
+                this._onMouseButton(pos.x, pos.y, down, bmask);
+            }
+            Util.stopEvent(e);
+            return false;
+        },
+
+        _handleMouseDown: function (e) {
+            this._captureMouse();
+            this._handleMouseButton(e, 1);
+        },
+
+        _handleMouseUp: function (e) {
+            if (!this._mouseCaptured) { return; }
+
+            this._handleMouseButton(e, 0);
+            this._releaseMouse();
+        },
+
+        _handleMouseWheel: function (e) {
+            if (!this._focused) { return true; }
+
+            if (this._notify) {
+                this._notify(e);
+            }
+
+            var evt = (e ? e : window.event);
+            var pos = Util.getEventPosition(e, this._target, this._scale);
+            var wheelData = evt.detail ? evt.detail * -1 : evt.wheelDelta / 40;
+            var bmask;
+            if (wheelData > 0) {
+                bmask = 1 << 3;
+            } else {
+                bmask = 1 << 4;
+            }
+
+            if (this._onMouseButton) {
+                this._onMouseButton(pos.x, pos.y, 1, bmask);
+                this._onMouseButton(pos.x, pos.y, 0, bmask);
+            }
+            Util.stopEvent(e);
+            return false;
+        },
+
+        _handleMouseMove: function (e) {
+            if (! this._focused) { return true; }
+
+            if (this._notify) {
+                this._notify(e);
+            }
+
+            var evt = (e ? e : window.event);
+            var pos = Util.getEventPosition(e, this._target, this._scale);
+            if (this._onMouseMove) {
+                this._onMouseMove(pos.x, pos.y);
+            }
+            Util.stopEvent(e);
+            return false;
+        },
+
+        _handleMouseDisable: function (e) {
+            if (!this._focused) { return true; }
+
+            var evt = (e ? e : window.event);
+            var pos = Util.getEventPosition(e, this._target, this._scale);
+
+            /* Stop propagation if inside canvas area */
+            if ((pos.realx >= 0) && (pos.realy >= 0) &&
+                (pos.realx < this._target.offsetWidth) &&
+                (pos.realy < this._target.offsetHeight)) {
+                //Util.Debug("mouse event disabled");
+                Util.stopEvent(e);
+                return false;
+            }
+
+            return true;
+        },
+
+
+        // Public methods
+        grab: function () {
+            var c = this._target;
+
+            if ('ontouchstart' in document.documentElement) {
+                Util.addEvent(c, 'touchstart', this._eventHandlers.mousedown);
+                Util.addEvent(window, 'touchend', this._eventHandlers.mouseup);
+                Util.addEvent(c, 'touchend', this._eventHandlers.mouseup);
+                Util.addEvent(c, 'touchmove', this._eventHandlers.mousemove);
+            } else {
+                Util.addEvent(c, 'mousedown', this._eventHandlers.mousedown);
+                Util.addEvent(window, 'mouseup', this._eventHandlers.mouseup);
+                Util.addEvent(c, 'mouseup', this._eventHandlers.mouseup);
+                Util.addEvent(c, 'mousemove', this._eventHandlers.mousemove);
+                Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel',
+                              this._eventHandlers.mousewheel);
+            }
+
+            /* Work around right and middle click browser behaviors */
+            Util.addEvent(document, 'click', this._eventHandlers.mousedisable);
+            Util.addEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable);
+        },
+
+        ungrab: function () {
+            var c = this._target;
+
+            if ('ontouchstart' in document.documentElement) {
+                Util.removeEvent(c, 'touchstart', this._eventHandlers.mousedown);
+                Util.removeEvent(window, 'touchend', this._eventHandlers.mouseup);
+                Util.removeEvent(c, 'touchend', this._eventHandlers.mouseup);
+                Util.removeEvent(c, 'touchmove', this._eventHandlers.mousemove);
+            } else {
+                Util.removeEvent(c, 'mousedown', this._eventHandlers.mousedown);
+                Util.removeEvent(window, 'mouseup', this._eventHandlers.mouseup);
+                Util.removeEvent(c, 'mouseup', this._eventHandlers.mouseup);
+                Util.removeEvent(c, 'mousemove', this._eventHandlers.mousemove);
+                Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel',
+                                 this._eventHandlers.mousewheel);
+            }
+
+            /* Work around right and middle click browser behaviors */
+            Util.removeEvent(document, 'click', this._eventHandlers.mousedisable);
+            Util.removeEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable);
+
         }
-        bmask = conf.touchButton;
-        // If bmask is set
-    } else if (evt.which) {
-        /* everything except IE */
-        bmask = 1 << evt.button;
-    } else {
-        /* IE including 9 */
-        bmask = (evt.button & 0x1) +      // Left
-                (evt.button & 0x2) * 2 +  // Right
-                (evt.button & 0x4) / 2;   // Middle
-    }
-    //Util.Debug("mouse " + pos.x + "," + pos.y + " down: " + down +
-    //           " bmask: " + bmask + "(evt.button: " + evt.button + ")");
-    if (conf.onMouseButton) {
-        Util.Debug("onMouseButton " + (down ? "down" : "up") +
-                   ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask);
-        conf.onMouseButton(pos.x, pos.y, down, bmask);
-    }
-    Util.stopEvent(e);
-    return false;
-}
-
-function onMouseDown(e) {
-    captureMouse();
-    onMouseButton(e, 1);
-}
-
-function onMouseUp(e) {
-    if (!mouseCaptured) {
-        return;
-    }
-
-    onMouseButton(e, 0);
-    releaseMouse();
-}
-
-function onMouseWheel(e) {
-    var evt, pos, bmask, wheelData;
-    if (! conf.focused) {
-        return true;
-    }
-    if (conf.notify) {
-        conf.notify(e);
-    }
-
-    evt = (e ? e : window.event);
-    pos = Util.getEventPosition(e, conf.target, conf.scale);
-    wheelData = evt.detail ? evt.detail * -1 : evt.wheelDelta / 40;
-    if (wheelData > 0) {
-        bmask = 1 << 3;
-    } else {
-        bmask = 1 << 4;
-    }
-    //Util.Debug('mouse scroll by ' + wheelData + ':' + pos.x + "," + pos.y);
-    if (conf.onMouseButton) {
-        conf.onMouseButton(pos.x, pos.y, 1, bmask);
-        conf.onMouseButton(pos.x, pos.y, 0, bmask);
-    }
-    Util.stopEvent(e);
-    return false;
-}
-
-function onMouseMove(e) {
-    var evt, pos;
-    if (! conf.focused) {
-        return true;
-    }
-    if (conf.notify) {
-        conf.notify(e);
-    }
-
-    evt = (e ? e : window.event);
-    pos = Util.getEventPosition(e, conf.target, conf.scale);
-    //Util.Debug('mouse ' + evt.which + '/' + evt.button + ' up:' + pos.x + "," + pos.y);
-    if (conf.onMouseMove) {
-        conf.onMouseMove(pos.x, pos.y);
-    }
-    Util.stopEvent(e);
-    return false;
-}
-
-function onMouseDisable(e) {
-    var evt, pos;
-    if (! conf.focused) {
-        return true;
-    }
-    evt = (e ? e : window.event);
-    pos = Util.getEventPosition(e, conf.target, conf.scale);
-    /* Stop propagation if inside canvas area */
-    if ((pos.realx >= 0) && (pos.realy >= 0) &&
-        (pos.realx < conf.target.offsetWidth) &&
-        (pos.realy < conf.target.offsetHeight)) {
-        //Util.Debug("mouse event disabled");
-        Util.stopEvent(e);
-        return false;
-    }
-    //Util.Debug("mouse event not disabled");
-    return true;
-}
-
-//
-// Public API interface functions
-//
-
-that.grab = function() {
-    //Util.Debug(">> Mouse.grab");
-    var c = conf.target;
-
-    if ('ontouchstart' in document.documentElement) {
-        Util.addEvent(c, 'touchstart', onMouseDown);
-        Util.addEvent(window, 'touchend', onMouseUp);
-        Util.addEvent(c, 'touchend', onMouseUp);
-        Util.addEvent(c, 'touchmove', onMouseMove);
-    } else {
-        Util.addEvent(c, 'mousedown', onMouseDown);
-        Util.addEvent(window, 'mouseup', onMouseUp);
-        Util.addEvent(c, 'mouseup', onMouseUp);
-        Util.addEvent(c, 'mousemove', onMouseMove);
-        Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel',
-                onMouseWheel);
-    }
-
-    /* Work around right and middle click browser behaviors */
-    Util.addEvent(document, 'click', onMouseDisable);
-    Util.addEvent(document.body, 'contextmenu', onMouseDisable);
-
-    //Util.Debug("<< Mouse.grab");
-};
-
-that.ungrab = function() {
-    //Util.Debug(">> Mouse.ungrab");
-    var c = conf.target;
-
-    if ('ontouchstart' in document.documentElement) {
-        Util.removeEvent(c, 'touchstart', onMouseDown);
-        Util.removeEvent(window, 'touchend', onMouseUp);
-        Util.removeEvent(c, 'touchend', onMouseUp);
-        Util.removeEvent(c, 'touchmove', onMouseMove);
-    } else {
-        Util.removeEvent(c, 'mousedown', onMouseDown);
-        Util.removeEvent(window, 'mouseup', onMouseUp);
-        Util.removeEvent(c, 'mouseup', onMouseUp);
-        Util.removeEvent(c, 'mousemove', onMouseMove);
-        Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel',
-                onMouseWheel);
-    }
-
-    /* Work around right and middle click browser behaviors */
-    Util.removeEvent(document, 'click', onMouseDisable);
-    Util.removeEvent(document.body, 'contextmenu', onMouseDisable);
-
-    //Util.Debug(">> Mouse.ungrab");
-};
-
-return that;  // Return the public API interface
-
-}  // End of Mouse()
+    };
+
+    Util.make_properties(Mouse, [
+        ['target',         'ro', 'dom'],   // DOM element that captures mouse input
+        ['notify',         'ro', 'func'],  // Function to call to notify whenever a mouse event is received
+        ['focused',        'rw', 'bool'],  // Capture and send mouse clicks/movement
+        ['scale',          'rw', 'float'], // Viewport scale factor 0.0 - 1.0
+
+        ['onMouseButton',  'rw', 'func'],  // Handler for mouse button click/release
+        ['onMouseMove',    'rw', 'func'],  // Handler for mouse movement
+        ['touchButton',    'rw', 'int']    // Button mask (1, 2, 4) for touch devices (0 means ignore clicks)
+    ]);
+})();

+ 19 - 10
include/keyboard.js

@@ -15,7 +15,7 @@ var kbdUtil = (function() {
 
         var sub = substitutions[cp];
         return sub ? sub : cp;
-    };
+    }
 
     function isMac() {
         return navigator && !!(/mac/i).exec(navigator.platform);
@@ -387,17 +387,22 @@ function VerifyCharModifier(next) {
         if (timer) {
             return;
         }
+
+        var delayProcess = function () {
+            clearTimeout(timer);
+            timer = null;
+            process();
+        };
+
         while (queue.length !== 0) {
             var cur = queue[0];
             queue = queue.splice(1);
             switch (cur.type) {
             case 'stall':
                 // insert a delay before processing available events.
-                timer = setTimeout(function() {
-                    clearTimeout(timer);
-                    timer = null;
-                    process();
-                }, 5);
+                /* jshint loopfunc: true */
+                timer = setTimeout(delayProcess, 5);
+                /* jshint loopfunc: false */
                 return;
             case 'keydown':
                 // is the next element a keypress? Then we should merge the two
@@ -489,23 +494,25 @@ function TrackKeyState(next) {
 
             var item = state.splice(idx, 1)[0];
             // for each keysym tracked by this key entry, clone the current event and override the keysym
+            var clone = (function(){
+                function Clone(){}
+                return function (obj) { Clone.prototype=obj; return new Clone(); };
+            }());
             for (var key in item.keysyms) {
-                var clone = (function(){
-                    function Clone(){}
-                    return function (obj) { Clone.prototype=obj; return new Clone(); };
-                }());
                 var out = clone(evt);
                 out.keysym = item.keysyms[key];
                 next(out);
             }
             break;
         case 'releaseall':
+            /* jshint shadow: true */
             for (var i = 0; i < state.length; ++i) {
                 for (var key in state[i].keysyms) {
                     var keysym = state[i].keysyms[key];
                     next({keyId: 0, keysym: keysym, type: 'keyup'});
                 }
             }
+            /* jshint shadow: false */
             state = [];
         }
     };
@@ -527,8 +534,10 @@ function EscapeModifiers(next) {
         // send the character event
         next(evt);
         // redo modifiers
+        /* jshint shadow: true */
         for (var i = 0; i < evt.escape.length; ++i) {
             next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])});
         }
+        /* jshint shadow: false */
     };
 }

+ 1747 - 1845
include/rfb.js

@@ -10,1972 +10,1874 @@
  * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca)
  */
 
-/*jslint white: false, browser: true, bitwise: false, plusplus: false */
+/*jslint white: false, browser: true */
 /*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES */
 
+var RFB;
 
-function RFB(defaults) {
-"use strict";
-
-var that           = {},  // Public API methods
-    conf           = {},  // Configuration attributes
-
-    // Pre-declare private functions used before definitions (jslint)
-    init_vars, updateState, fail, handle_message,
-    init_msg, normal_msg, framebufferUpdate, print_stats,
-
-    pixelFormat, clientEncodings, fbUpdateRequest, fbUpdateRequests,
-    keyEvent, pointerEvent, clientCutText,
-
-    getTightCLength, extract_data_uri,
-    keyPress, mouseButton, mouseMove,
-
-    checkEvents,  // Overridable for testing
-
-
-    //
-    // Private RFB namespace variables
-    //
-    rfb_host       = '',
-    rfb_port       = 5900,
-    rfb_password   = '',
-    rfb_path       = '',
-
-    rfb_state      = 'disconnected',
-    rfb_version    = 0,
-    rfb_max_version= 3.8,
-    rfb_auth_scheme= '',
-    rfb_tightvnc   = false,
-
-    rfb_xvp_ver    = 0,
-
-
-    // In preference order
-    encodings      = [
-        ['COPYRECT',         0x01 ],
-        ['TIGHT',            0x07 ],
-        ['TIGHT_PNG',        -260 ],
-        ['HEXTILE',          0x05 ],
-        ['RRE',              0x02 ],
-        ['RAW',              0x00 ],
-        ['DesktopSize',      -223 ],
-        ['Cursor',           -239 ],
-
-        // Psuedo-encoding settings
-        //['JPEG_quality_lo',   -32 ],
-        ['JPEG_quality_med',    -26 ],
-        //['JPEG_quality_hi',   -23 ],
-        //['compress_lo',      -255 ],
-        ['compress_hi',        -247 ],
-        ['last_rect',          -224 ],
-        ['xvp',                -309 ]
-        ],
-
-    encHandlers    = {},
-    encNames       = {}, 
-    encStats       = {},     // [rectCnt, rectCntTot]
-
-    ws             = null,   // Websock object
-    display        = null,   // Display object
-    keyboard       = null,   // Keyboard input handler object
-    mouse          = null,   // Mouse input handler object
-    sendTimer      = null,   // Send Queue check timer
-    disconnTimer   = null,   // disconnection timer
-    msgTimer       = null,   // queued handle_message timer
-
-    // Frame buffer update state
-    FBU            = {
-        rects          : 0,
-        subrects       : 0,  // RRE
-        lines          : 0,  // RAW
-        tiles          : 0,  // HEXTILE
-        bytes          : 0,
-        x              : 0,
-        y              : 0,
-        width          : 0, 
-        height         : 0,
-        encoding       : 0,
-        subencoding    : -1,
-        background     : null,
-        zlibs          : []   // TIGHT zlib streams
-    },
-
-    fb_Bpp         = 4,
-    fb_depth       = 3,
-    fb_width       = 0,
-    fb_height      = 0,
-    fb_name        = "",
-
-    rre_chunk_sz   = 100,
-
-    timing         = {
-        last_fbu       : 0,
-        fbu_total      : 0,
-        fbu_total_cnt  : 0,
-        full_fbu_total : 0,
-        full_fbu_cnt   : 0,
-
-        fbu_rt_start   : 0,
-        fbu_rt_total   : 0,
-        fbu_rt_cnt     : 0,
-        pixels         : 0
-    },
-
-    test_mode        = false,
-
-    /* Mouse state */
-    mouse_buttonMask = 0,
-    mouse_arr        = [],
-    viewportDragging = false,
-    viewportDragPos  = {};
-
-// Configuration attributes
-Util.conf_defaults(conf, that, defaults, [
-    ['target',             'wo', 'dom', null, 'VNC display rendering Canvas object'],
-    ['focusContainer',     'wo', 'dom', document, 'DOM element that captures keyboard input'],
-
-    ['encrypt',            'rw', 'bool', false, 'Use TLS/SSL/wss encryption'],
-    ['true_color',         'rw', 'bool', true,  'Request true color pixel data'],
-    ['local_cursor',       'rw', 'bool', false, 'Request locally rendered cursor'],
-    ['shared',             'rw', 'bool', true,  'Request shared mode'],
-    ['view_only',          'rw', 'bool', false, 'Disable client mouse/keyboard'],
-    ['xvp_password_sep',   'rw', 'str',  '@',   'Separator for XVP password fields'],
-    ['disconnectTimeout',  'rw', 'int', 3,    'Time (s) to wait for disconnection'],
-
-    ['wsProtocols',        'rw', 'arr', ['binary', 'base64'],
-        'Protocols to use in the WebSocket connection'],
-
-    // UltraVNC repeater ID to connect to
-    ['repeaterID',         'rw', 'str',  '',    'RepeaterID to connect to'],
-
-    ['viewportDrag',       'rw', 'bool', false, 'Move the viewport on mouse drags'],
-
-    // Callback functions
-    ['onUpdateState',      'rw', 'func', function() { },
-        'onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change '],
-    ['onPasswordRequired', 'rw', 'func', function() { },
-        'onPasswordRequired(rfb): VNC password is required '],
-    ['onClipboard',        'rw', 'func', function() { },
-        'onClipboard(rfb, text): RFB clipboard contents received'],
-    ['onBell',             'rw', 'func', function() { },
-        'onBell(rfb): RFB Bell message received '],
-    ['onFBUReceive',       'rw', 'func', function() { },
-        'onFBUReceive(rfb, fbu): RFB FBU received but not yet processed '],
-    ['onFBUComplete',      'rw', 'func', function() { },
-        'onFBUComplete(rfb, fbu): RFB FBU received and processed '],
-    ['onFBResize',         'rw', 'func', function() { },
-        'onFBResize(rfb, width, height): frame buffer resized'],
-    ['onDesktopName',      'rw', 'func', function() { },
-        'onDesktopName(rfb, name): desktop name received'],
-    ['onXvpInit',          'rw', 'func', function() { },
-        'onXvpInit(version): XVP extensions active for this connection'],
-
-    // These callback names are deprecated
-    ['updateState',        'rw', 'func', function() { },
-        'obsolete, use onUpdateState'],
-    ['clipboardReceive',   'rw', 'func', function() { },
-        'obsolete, use onClipboard']
-    ]);
+(function () {
+    "use strict";
+    RFB = function (defaults) {
+        if (!defaults) {
+            defaults = {};
+        }
 
+        this._rfb_host = '';
+        this._rfb_port = 5900;
+        this._rfb_password = '';
+        this._rfb_path = '';
+
+        this._rfb_state = 'disconnected';
+        this._rfb_version = 0;
+        this._rfb_max_version = 3.8;
+        this._rfb_auth_scheme = '';
+
+        this._rfb_tightvnc = false;
+        this._rfb_xvp_ver = 0;
+
+        // In preference order
+        this._encodings = [
+            ['COPYRECT',         0x01 ],
+            ['TIGHT',            0x07 ],
+            ['TIGHT_PNG',        -260 ],
+            ['HEXTILE',          0x05 ],
+            ['RRE',              0x02 ],
+            ['RAW',              0x00 ],
+            ['DesktopSize',      -223 ],
+            ['Cursor',           -239 ],
+
+            // Psuedo-encoding settings
+            //['JPEG_quality_lo',   -32 ],
+            ['JPEG_quality_med',    -26 ],
+            //['JPEG_quality_hi',   -23 ],
+            //['compress_lo',      -255 ],
+            ['compress_hi',        -247 ],
+            ['last_rect',          -224 ],
+            ['xvp',                -309 ]
+        ];
+
+        this._encHandlers = {};
+        this._encNames = {};
+        this._encStats = {};
+
+        this._sock = null;              // Websock object
+        this._display = null;           // Display object
+        this._keyboard = null;          // Keyboard input handler object
+        this._mouse = null;             // Mouse input handler object
+        this._sendTimer = null;         // Send Queue check timer
+        this._disconnTimer = null;      // disconnection timer
+        this._msgTimer = null;          // queued handle_msg timer
+
+        // Frame buffer update state
+        this._FBU = {
+            rects: 0,
+            subrects: 0,            // RRE
+            lines: 0,               // RAW
+            tiles: 0,               // HEXTILE
+            bytes: 0,
+            x: 0,
+            y: 0,
+            width: 0,
+            height: 0,
+            encoding: 0,
+            subencoding: -1,
+            background: null,
+            zlib: []                // TIGHT zlib streams
+        };
 
-// Override/add some specific configuration getters/setters
-that.set_local_cursor = function(cursor) {
-    if ((!cursor) || (cursor in {'0':1, 'no':1, 'false':1})) {
-        conf.local_cursor = false;
-    } else {
-        if (display.get_cursor_uri()) {
-            conf.local_cursor = true;
-        } else {
-            Util.Warn("Browser does not support local cursor");
-        }
-    }
-};
-
-// These are fake configuration getters
-that.get_display = function() { return display; };
-
-that.get_keyboard = function() { return keyboard; };
-
-that.get_mouse = function() { return mouse; };
-
-
-
-//
-// Setup routines
-//
-
-// Create the public API interface and initialize values that stay
-// constant across connect/disconnect
-function constructor() {
-    var i, rmode;
-    Util.Debug(">> RFB.constructor");
-
-    // Create lookup tables based encoding number
-    for (i=0; i < encodings.length; i+=1) {
-        encHandlers[encodings[i][1]] = encHandlers[encodings[i][0]];
-        encNames[encodings[i][1]] = encodings[i][0];
-        encStats[encodings[i][1]] = [0, 0];
-    }
-    // Initialize display, mouse, keyboard, and websock
-    try {
-        display   = new Display({'target': conf.target});
-    } catch (exc) {
-        Util.Error("Display exception: " + exc);
-        updateState('fatal', "No working Display");
-    }
-    keyboard = new Keyboard({'target': conf.focusContainer,
-                                'onKeyPress': keyPress});
-    mouse    = new Mouse({'target': conf.target,
-                            'onMouseButton': mouseButton,
-                            'onMouseMove': mouseMove,
-                            'notify': keyboard.sync});
-
-    rmode = display.get_render_mode();
-
-    ws = new Websock();
-    ws.on('message', handle_message);
-    ws.on('open', function() {
-        if (rfb_state === "connect") {
-            updateState('ProtocolVersion', "Starting VNC handshake");
-        } else {
-            fail("Got unexpected WebSockets connection");
-        }
-    });
-    ws.on('close', function(e) {
-        Util.Warn("WebSocket on-close event");
-        var msg = "";
-        if (e.code) {
-            msg = " (code: " + e.code;
-            if (e.reason) {
-                msg += ", reason: " + e.reason;
-            }
-            msg += ")";
+        this._fb_Bpp = 4;
+        this._fb_depth = 3;
+        this._fb_width = 0;
+        this._fb_height = 0;
+        this._fb_name = "";
+
+        this._rre_chunk_sz = 100;
+
+        this._timing = {
+            last_fbu: 0,
+            fbu_total: 0,
+            fbu_total_cnt: 0,
+            full_fbu_total: 0,
+            full_fbu_cnt: 0,
+
+            fbu_rt_start: 0,
+            fbu_rt_total: 0,
+            fbu_rt_cnt: 0,
+            pixels: 0
+        };
+
+        // Mouse state
+        this._mouse_buttonMask = 0;
+        this._mouse_arr = [];
+        this._viewportDragging = false;
+        this._viewportDragPos = {};
+
+        // set the default value on user-facing properties
+        Util.set_defaults(this, defaults, {
+            'target': 'null',                       // VNC display rendering Canvas object
+            'focusContainer': document,             // DOM element that captures keyboard input
+            'encrypt': false,                       // Use TLS/SSL/wss encryption
+            'true_color': true,                     // Request true color pixel data
+            'local_cursor': false,                  // Request locally rendered cursor
+            'shared': true,                         // Request shared mode
+            'view_only': false,                     // Disable client mouse/keyboard
+            'xvp_password_sep': '@',                // Separator for XVP password fields
+            'disconnectTimeout': 3,                 // Time (s) to wait for disconnection
+            'wsProtocols': ['binary', 'base64'],    // Protocols to use in the WebSocket connection
+            'repeaterID': '',                       // [UltraVNC] RepeaterID to connect to
+            'viewportDrag': false,                  // Move the viewport on mouse drags
+
+            // Callback functions
+            'onUpdateState': function () { },       // onUpdateState(rfb, state, oldstate, statusMsg): state update/change
+            'onPasswordRequired': function () { },  // onPasswordRequired(rfb): VNC password is required
+            'onClipboard': function () { },         // onClipboard(rfb, text): RFB clipboard contents received
+            'onBell': function () { },              // onBell(rfb): RFB Bell message received
+            'onFBUReceive': function () { },        // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed
+            'onFBUComplete': function () { },       // onFBUComplete(rfb, fbu): RFB FBU received and processed
+            'onFBResize': function () { },          // onFBResize(rfb, width, height): frame buffer resized
+            'onDesktopName': function () { },       // onDesktopName(rfb, name): desktop name received
+            'onXvpInit': function () { },           // onXvpInit(version): XVP extensions active for this connection
+        });
+
+        // main setup
+        Util.Debug(">> RFB.constructor");
+
+        // populate encHandlers with bound versions
+        Object.keys(RFB.encodingHandlers).forEach(function (encName) {
+            this._encHandlers[encName] = RFB.encodingHandlers[encName].bind(this);
+        }.bind(this));
+
+        // Create lookup tables based on encoding number
+        for (var i = 0; i < this._encodings.length; i++) {
+            this._encHandlers[this._encodings[i][1]] = this._encHandlers[this._encodings[i][0]];
+            this._encNames[this._encodings[i][1]] = this._encodings[i][0];
+            this._encStats[this._encodings[i][1]] = [0, 0];
         }
-        if (rfb_state === 'disconnect') {
-            updateState('disconnected', 'VNC disconnected' + msg);
-        } else if (rfb_state === 'ProtocolVersion') {
-            fail('Failed to connect to server' + msg);
-        } else if (rfb_state in {'failed':1, 'disconnected':1}) {
-            Util.Error("Received onclose while disconnected" + msg);
-        } else  {
-            fail('Server disconnected' + msg);
+
+        try {
+            this._display = new Display({target: this._target});
+        } catch (exc) {
+            Util.Error("Display exception: " + exc);
+            this._updateState('fatal', "No working Display");
         }
-    });
-    ws.on('error', function(e) {
-        Util.Warn("WebSocket on-error event");
-        //fail("WebSock reported an error");
-    });
-
-
-    init_vars();
-
-    /* Check web-socket-js if no builtin WebSocket support */
-    if (Websock_native) {
-        Util.Info("Using native WebSockets");
-        updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode);
-    } else {
-        Util.Warn("Using web-socket-js bridge. Flash version: " +
-                  Util.Flash.version);
-        if ((! Util.Flash) ||
-            (Util.Flash.version < 9)) {
-            updateState('fatal', "WebSockets or <a href='http://get.adobe.com/flashplayer'>Adobe Flash<\/a> is required");
-        } else if (document.location.href.substr(0, 7) === "file://") {
-            updateState('fatal',
-                    "'file://' URL is incompatible with Adobe Flash");
+
+        this._keyboard = new Keyboard({target: this._focusContainer,
+                                       onKeyPress: this._handleKeyPress.bind(this)});
+
+        this._mouse = new Mouse({target: this._target,
+                                 onMouseButton: this._handleMouseButton.bind(this),
+                                 onMouseMove: this._handleMouseMove.bind(this),
+                                 notify: this._keyboard.sync.bind(this._keyboard)});
+
+        this._sock = new Websock();
+        this._sock.on('message', this._handle_message.bind(this));
+        this._sock.on('open', function () {
+            if (this._rfb_state === 'connect') {
+                this._updateState('ProtocolVersion', "Starting VNC handshake");
+            } else {
+                this._fail("Got unexpected WebSocket connection");
+            }
+        }.bind(this));
+        this._sock.on('close', function (e) {
+            Util.Warn("WebSocket on-close event");
+            var msg = "";
+            if (e.code) {
+                msg = " (code: " + e.code;
+                if (e.reason) {
+                    msg += ", reason: " + e.reason;
+                }
+                msg += ")";
+            }
+            if (this._rfb_state === 'disconnect') {
+                this._updateState('disconnected', 'VNC disconnected' + msg);
+            } else if (this._rfb_state === 'ProtocolVersion') {
+                this._fail('Failed to connect to server' + msg);
+            } else if (this._rfb_state in {'failed': 1, 'disconnected': 1}) {
+                Util.Error("Received onclose while disconnected" + msg);
+            } else {
+                this._fail("Server disconnected" + msg);
+            }
+        }.bind(this));
+        this._sock.on('error', function (e) {
+            Util.Warn("WebSocket on-error event");
+        });
+
+        this._init_vars();
+
+        var rmode = this._display.get_render_mode();
+        if (Websock_native) {
+            Util.Info("Using native WebSockets");
+            this._updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode);
         } else {
-            updateState('loaded', 'noVNC ready: WebSockets emulation, ' + rmode);
-        }
-    }
-
-    Util.Debug("<< RFB.constructor");
-    return that;  // Return the public API interface
-}
-
-function connect() {
-    Util.Debug(">> RFB.connect");
-    var uri;
-    
-    if (typeof UsingSocketIO !== "undefined") {
-        uri = "http";
-    } else {
-        uri = conf.encrypt ? "wss" : "ws";
-    }
-    uri += "://" + rfb_host + ":" + rfb_port + "/" + rfb_path;
-    Util.Info("connecting to " + uri);
-
-    ws.open(uri, conf.wsProtocols);
-
-    Util.Debug("<< RFB.connect");
-}
-
-// Initialize variables that are reset before each connection
-init_vars = function() {
-    var i;
-
-    /* Reset state */
-    ws.init();
-
-    FBU.rects        = 0;
-    FBU.subrects     = 0;  // RRE and HEXTILE
-    FBU.lines        = 0;  // RAW
-    FBU.tiles        = 0;  // HEXTILE
-    FBU.zlibs        = []; // TIGHT zlib encoders
-    mouse_buttonMask = 0;
-    mouse_arr        = [];
-
-    // Clear the per connection encoding stats
-    for (i=0; i < encodings.length; i+=1) {
-        encStats[encodings[i][1]][0] = 0;
-    }
-    
-    for (i=0; i < 4; i++) {
-        //FBU.zlibs[i] = new InflateStream();
-        FBU.zlibs[i] = new TINF();
-        FBU.zlibs[i].init();
-    }
-};
-
-// Print statistics
-print_stats = function() {
-    var i, s;
-    Util.Info("Encoding stats for this connection:");
-    for (i=0; i < encodings.length; i+=1) {
-        s = encStats[encodings[i][1]];
-        if ((s[0] + s[1]) > 0) {
-            Util.Info("    " + encodings[i][0] + ": " +
-                      s[0] + " rects");
-        }
-    }
-    Util.Info("Encoding stats since page load:");
-    for (i=0; i < encodings.length; i+=1) {
-        s = encStats[encodings[i][1]];
-        if ((s[0] + s[1]) > 0) {
-            Util.Info("    " + encodings[i][0] + ": " +
-                      s[1] + " rects");
+            Util.Warn("Using web-socket-js bridge.  Flash version: " + Util.Flash.version);
+            if (!Util.Flash || Util.Flash.version < 9) {
+                this._updateState('fatal', "WebSockets or <a href='http://get.adobe.com/flashplayer'>Adobe Flash</a> is required");
+            } else if (document.location.href.substr(0, 7) === 'file://') {
+                this._updateState('fatal', "'file://' URL is incompatible with Adobe Flash");
+            } else {
+                this._updateState('loaded', 'noVNC ready: WebSockets emulation, ' + rmode);
+            }
         }
-    }
-};
 
-//
-// Utility routines
-//
+        Util.Debug("<< RFB.constructor");
+    };
 
+    RFB.prototype = {
+        // Public methods
+        connect: function (host, port, password, path) {
+            this._rfb_host = host;
+            this._rfb_port = port;
+            this._rfb_password = (password !== undefined) ? password : "";
+            this._rfb_path = (path !== undefined) ? path : "";
 
-/*
- * Page states:
- *   loaded       - page load, equivalent to disconnected
- *   disconnected - idle state
- *   connect      - starting to connect (to ProtocolVersion)
- *   normal       - connected
- *   disconnect   - starting to disconnect
- *   failed       - abnormal disconnect
- *   fatal        - failed to load page, or fatal error
- *
- * RFB protocol initialization states:
- *   ProtocolVersion 
- *   Security
- *   Authentication
- *   password     - waiting for password, not part of RFB
- *   SecurityResult
- *   ClientInitialization - not triggered by server message
- *   ServerInitialization (to normal)
- */
-updateState = function(state, statusMsg) {
-    var func, cmsg, oldstate = rfb_state;
-
-    if (state === oldstate) {
-        /* Already here, ignore */
-        Util.Debug("Already in state '" + state + "', ignoring.");
-        return;
-    }
-
-    /* 
-     * These are disconnected states. A previous connect may
-     * asynchronously cause a connection so make sure we are closed.
-     */
-    if (state in {'disconnected':1, 'loaded':1, 'connect':1,
-                  'disconnect':1, 'failed':1, 'fatal':1}) {
-        if (sendTimer) {
-            clearInterval(sendTimer);
-            sendTimer = null;
-        }
+            if (!this._rfb_host || !this._rfb_port) {
+                return this._fail("Must set host and port");
+            }
 
-        if (msgTimer) {
-            clearTimeout(msgTimer);
-            msgTimer = null;
-        }
+            this._updateState('connect');
+        },
+
+        disconnect: function () {
+            this._updateState('disconnect', 'Disconnecting');
+        },
+
+        sendPassword: function (passwd) {
+            this._rfb_password = passwd;
+            this._rfb_state = 'Authentication';
+            setTimeout(this._init_msg.bind(this), 1);
+        },
+
+        sendCtrlAltDel: function () {
+            if (this._rfb_state !== 'normal' || this._view_only) { return false; }
+            Util.Info("Sending Ctrl-Alt-Del");
+
+            var arr = [];
+            arr = arr.concat(RFB.messages.keyEvent(0xFFE3, 1)); // Control
+            arr = arr.concat(RFB.messages.keyEvent(0xFFE9, 1)); // Alt
+            arr = arr.concat(RFB.messages.keyEvent(0xFFFF, 1)); // Delete
+            arr = arr.concat(RFB.messages.keyEvent(0xFFFF, 0)); // Delete
+            arr = arr.concat(RFB.messages.keyEvent(0xFFE9, 0)); // Alt
+            arr = arr.concat(RFB.messages.keyEvent(0xFFE3, 0)); // Control
+            this._sock.send(arr);
+        },
+
+        xvpOp: function (ver, op) {
+            if (this._rfb_xvp_ver < ver) { return false; }
+            Util.Info("Sending XVP operation " + op + " (version " + ver + ")");
+            this._sock.send_string("\xFA\x00" + String.fromCharCode(ver) + String.fromCharCode(op));
+            return true;
+        },
+
+        xvpShutdown: function () {
+            return this.xvpOp(1, 2);
+        },
+
+        xvpReboot: function () {
+            return this.xvpOp(1, 3);
+        },
+
+        xvpReset: function () {
+            return this.xvpOp(1, 4);
+        },
+
+        // Send a key press. If 'down' is not specified then send a down key
+        // followed by an up key.
+        sendKey: function (code, down) {
+            if (this._rfb_state !== "normal" || this._view_only) { return false; }
+            var arr = [];
+            if (typeof down !== 'undefined') {
+                Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code);
+                arr = arr.concat(RFB.messages.keyEvent(code, down ? 1 : 0));
+            } else {
+                Util.Info("Sending key code (down + up): " + code);
+                arr = arr.concat(RFB.messages.keyEvent(code, 1));
+                arr = arr.concat(RFB.messages.keyEvent(code, 0));
+            }
+            this._sock.send(arr);
+        },
 
-        if (display && display.get_context()) {
-            keyboard.ungrab();
-            mouse.ungrab();
-            display.defaultCursor();
-            if ((Util.get_logging() !== 'debug') ||
-                (state === 'loaded')) {
-                // Show noVNC logo on load and when disconnected if
-                // debug is off
-                display.clear();
+        clipboardPasteFrom: function (text) {
+            if (this._rfb_state !== 'normal') { return; }
+            this._sock.send(RFB.messages.clientCutText(text));
+        },
+
+        // Private methods
+
+        _connect: function () {
+            Util.Debug(">> RFB.connect");
+
+            var uri;
+            if (typeof UsingSocketIO !== 'undefined') {
+                uri = 'http';
+            } else {
+                uri = this._encrypt ? 'wss' : 'ws';
             }
-        }
 
-        ws.close();
-    }
-
-    if (oldstate === 'fatal') {
-        Util.Error("Fatal error, cannot continue");
-    }
-
-    if ((state === 'failed') || (state === 'fatal')) {
-        func = Util.Error;
-    } else {
-        func = Util.Warn;
-    }
-
-    cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : "";
-    func("New state '" + state + "', was '" + oldstate + "'." + cmsg);
-
-    if ((oldstate === 'failed') && (state === 'disconnected')) {
-        // Do disconnect action, but stay in failed state
-        rfb_state = 'failed';
-    } else {
-        rfb_state = state;
-    }
-
-    if (disconnTimer && (rfb_state !== 'disconnect')) {
-        Util.Debug("Clearing disconnect timer");
-        clearTimeout(disconnTimer);
-        disconnTimer = null;
-    }
-
-    switch (state) {
-    case 'normal':
-        if ((oldstate === 'disconnected') || (oldstate === 'failed')) {
-            Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'");
-        }
+            uri += '://' + this._rfb_host + ':' + this._rfb_port + '/' + this._rfb_path;
+            Util.Info("connecting to " + uri);
 
-        break;
+            this._sock.open(uri, this._sockProtocols);
+
+            Util.Debug("<< RFB.connect");
+        },
+
+        _init_vars: function () {
+            // reset state
+            this._sock.init();
+
+            this._FBU.rects        = 0;
+            this._FBU.subrects     = 0;  // RRE and HEXTILE
+            this._FBU.lines        = 0;  // RAW
+            this._FBU.tiles        = 0;  // HEXTILE
+            this._FBU.zlibs        = []; // TIGHT zlib encoders
+            this._mouse_buttonMask = 0;
+            this._mouse_arr        = [];
+            this._rfb_tightvnc     = false;
+
+            // Clear the per connection encoding stats
+            var i;
+            for (i = 0; i < this._encodings.length; i++) {
+                this._encStats[this._encodings[i][1]][0] = 0;
+            }
 
+            for (i = 0; i < 4; i++) {
+                this._FBU.zlibs[i] = new TINF();
+                this._FBU.zlibs[i].init();
+            }
+        },
+
+        _print_stats: function () {
+            Util.Info("Encoding stats for this connection:");
+            var i, s;
+            for (i = 0; i < this._encodings.length; i++) {
+                s = this._encStats[this._encodings[i][1]];
+                if (s[0] + s[1] > 0) {
+                    Util.Info("    " + this._encodings[i][0] + ": " + s[0] + " rects");
+                }
+            }
 
-    case 'connect':
+            Util.Info("Encoding stats since page load:");
+            for (i = 0; i < this._encodings.length; i++) {
+                s = this._encStats[this._encodings[i][1]];
+                Util.Info("    " + this._encodings[i][0] + ": " + s[1] + " rects");
+            }
+        },
 
-        init_vars();
-        connect();
 
-        // WebSocket.onopen transitions to 'ProtocolVersion'
-        break;
+        /*
+         * Page states:
+         *   loaded       - page load, equivalent to disconnected
+         *   disconnected - idle state
+         *   connect      - starting to connect (to ProtocolVersion)
+         *   normal       - connected
+         *   disconnect   - starting to disconnect
+         *   failed       - abnormal disconnect
+         *   fatal        - failed to load page, or fatal error
+         *
+         * RFB protocol initialization states:
+         *   ProtocolVersion
+         *   Security
+         *   Authentication
+         *   password     - waiting for password, not part of RFB
+         *   SecurityResult
+         *   ClientInitialization - not triggered by server message
+         *   ServerInitialization (to normal)
+         */
+        _updateState: function (state, statusMsg) {
+            var oldstate = this._rfb_state;
+
+            if (state === oldstate) {
+                // Already here, ignore
+                Util.Debug("Already in state '" + state + "', ignoring");
+            }
 
+            /*
+             * These are disconnected states. A previous connect may
+             * asynchronously cause a connection so make sure we are closed.
+             */
+            if (state in {'disconnected': 1, 'loaded': 1, 'connect': 1,
+                          'disconnect': 1, 'failed': 1, 'fatal': 1}) {
 
-    case 'disconnect':
+                if (this._sendTimer) {
+                    clearInterval(this._sendTimer);
+                    this._sendTimer = null;
+                }
 
-        if (! test_mode) {
-            disconnTimer = setTimeout(function () {
-                    fail("Disconnect timeout");
-                }, conf.disconnectTimeout * 1000);
-        }
+                if (this._msgTimer) {
+                    clearInterval(this._msgTimer);
+                    this._msgTimer = null;
+                }
 
-        print_stats();
+                if (this._display && this._display.get_context()) {
+                    this._keyboard.ungrab();
+                    this._mouse.ungrab();
+                    this._display.defaultCursor();
+                    if (Util.get_logging() !== 'debug' || state === 'loaded') {
+                        // Show noVNC logo on load and when disconnected, unless in
+                        // debug mode
+                        this._display.clear();
+                    }
+                }
 
-        // WebSocket.onclose transitions to 'disconnected'
-        break;
+                this._sock.close();
+            }
 
+            if (oldstate === 'fatal') {
+                Util.Error('Fatal error, cannot continue');
+            }
 
-    case 'failed':
-        if (oldstate === 'disconnected') {
-            Util.Error("Invalid transition from 'disconnected' to 'failed'");
-        }
-        if (oldstate === 'normal') {
-            Util.Error("Error while connected.");
-        }
-        if (oldstate === 'init') {
-            Util.Error("Error while initializing.");
-        }
+            var cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : "";
+            var fullmsg = "New state '" + state + "', was '" + oldstate + "'." + cmsg;
+            if (state === 'failed' || state === 'fatal') {
+                Util.Error(cmsg);
+            } else {
+                Util.Warn(cmsg);
+            }
 
-        // Make sure we transition to disconnected
-        setTimeout(function() { updateState('disconnected'); }, 50);
-
-        break;
-
-
-    default:
-        // No state change action to take
-
-    }
-
-    if ((oldstate === 'failed') && (state === 'disconnected')) {
-        // Leave the failed message
-        conf.updateState(that, state, oldstate); // Obsolete
-        conf.onUpdateState(that, state, oldstate);
-    } else {
-        conf.updateState(that, state, oldstate, statusMsg); // Obsolete
-        conf.onUpdateState(that, state, oldstate, statusMsg);
-    }
-};
-
-fail = function(msg) {
-    updateState('failed', msg);
-    return false;
-};
-
-handle_message = function() {
-    //Util.Debug(">> handle_message ws.rQlen(): " + ws.rQlen());
-    //Util.Debug("ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")");
-    if (ws.rQlen() === 0) {
-        Util.Warn("handle_message called on empty receive queue");
-        return;
-    }
-    switch (rfb_state) {
-    case 'disconnected':
-    case 'failed':
-        Util.Error("Got data while disconnected");
-        break;
-    case 'normal':
-        if (normal_msg() && ws.rQlen() > 0) {
-            // true means we can continue processing
-            // Give other events a chance to run
-            if (msgTimer === null) {
-                Util.Debug("More data to process, creating timer");
-                msgTimer = setTimeout(function () {
-                            msgTimer = null;
-                            handle_message();
-                        }, 10);
+            if (oldstate === 'failed' && state === 'disconnected') {
+                // do disconnect action, but stay in failed state
+                this._rfb_state = 'failed';
             } else {
-                Util.Debug("More data to process, existing timer");
+                this._rfb_state = state;
             }
-        }
-        break;
-    default:
-        init_msg();
-        break;
-    }
-};
-
-
-function genDES(password, challenge) {
-    var i, passwd = [];
-    for (i=0; i < password.length; i += 1) {
-        passwd.push(password.charCodeAt(i));
-    }
-    return (new DES(passwd)).encrypt(challenge);
-}
-
-// overridable for testing
-checkEvents = function() {
-    if (rfb_state === 'normal' && !viewportDragging && mouse_arr.length > 0) {
-        ws.send(mouse_arr);
-        mouse_arr = [];
-    }
-};
-
-keyPress = function(keysym, down) {
-    if (conf.view_only) { return; } // View only, skip keyboard events
-
-    ws.send(keyEvent(keysym, down));
-};
-
-mouseButton = function(x, y, down, bmask) {
-    if (down) {
-        mouse_buttonMask |= bmask;
-    } else {
-        mouse_buttonMask ^= bmask;
-    }
-
-    if (conf.viewportDrag) {
-        if (down && !viewportDragging) {
-            viewportDragging = true;
-            viewportDragPos = {'x': x, 'y': y};
-
-            // Skip sending mouse events
-            return;
-        } else {
-            viewportDragging = false;
-        }
-    }
 
-    if (conf.view_only) { return; } // View only, skip mouse events
+            if (this._disconnTimer && this._rfb_state !== 'disconnect') {
+                Util.Debug("Clearing disconnect timer");
+                clearTimeout(this._disconnTimer);
+                this._disconnTimer = null;
+            }
 
-    mouse_arr = mouse_arr.concat(
-            pointerEvent(display.absX(x), display.absY(y)) );
-    ws.send(mouse_arr);
-    mouse_arr = [];
-};
+            switch (state) {
+                case 'normal':
+                    if (oldstate === 'disconnected' || oldstate === 'failed') {
+                        Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'");
+                    }
+                    break;
+
+                case 'connect':
+                    this._init_vars();
+                    this._connect();
+                    // WebSocket.onopen transitions to 'ProtocolVersion'
+                    break;
+
+                case 'disconnect':
+                    this._disconnTimer = setTimeout(function () {
+                        this._fail("Disconnect timeout");
+                    }.bind(this), this._disconnectTimeout * 1000);
+
+                    this._print_stats();
+
+                    // WebSocket.onclose transitions to 'disconnected'
+                    break;
+
+                case 'failed':
+                    if (oldstate === 'disconnected') {
+                        Util.Error("Invalid transition from 'disconnected' to 'failed'");
+                    } else if (oldstate === 'normal') {
+                        Util.Error("Error while connected.");
+                    } else if (oldstate === 'init') {
+                        Util.Error("Error while initializing.");
+                    }
 
-mouseMove = function(x, y) {
-    //Util.Debug('>> mouseMove ' + x + "," + y);
-    var deltaX, deltaY;
+                    // Make sure we transition to disconnected
+                    setTimeout(function () {
+                        this._updateState('disconnected');
+                    }.bind(this), 50);
 
-    if (viewportDragging) {
-        //deltaX = x - viewportDragPos.x; // drag viewport
-        deltaX = viewportDragPos.x - x; // drag frame buffer
-        //deltaY = y - viewportDragPos.y; // drag viewport
-        deltaY = viewportDragPos.y - y; // drag frame buffer
-        viewportDragPos = {'x': x, 'y': y};
+                    break;
 
-        display.viewportChange(deltaX, deltaY);
+                default:
+                    // No state change action to take
+            }
 
-        // Skip sending mouse events
-        return;
-    }
+            if (oldstate === 'failed' && state === 'disconnected') {
+                this._onUpdateState(this, state, oldstate);
+            } else {
+                this._onUpdateState(this, state, oldstate, statusMsg);
+            }
+        },
 
-    if (conf.view_only) { return; } // View only, skip mouse events
+        _fail: function (msg) {
+            this._updateState('failed', msg);
+            return false;
+        },
 
-    mouse_arr = mouse_arr.concat(
-            pointerEvent(display.absX(x), display.absY(y)));
-    
-    checkEvents();
-};
+        _handle_message: function () {
+            if (this._sock.rQlen() === 0) {
+                Util.Warn("handle_message called on an empty receive queue");
+                return;
+            }
 
+            switch (this._rfb_state) {
+                case 'disconnected':
+                case 'failed':
+                    Util.Error("Got data while disconnected");
+                    break;
+                case 'normal':
+                    if (this._normal_msg() && this._sock.rQlen() > 0) {
+                        // true means we can continue processing
+                        // Give other events a chance to run
+                        if (this._msgTimer === null) {
+                            Util.Debug("More data to process, creating timer");
+                            this._msgTimer = setTimeout(function () {
+                                this._msgTimer = null;
+                                this._handle_message();
+                            }.bind(this), 10);
+                        } else {
+                            Util.Debug("More data to process, existing timer");
+                        }
+                    }
+                    break;
+                default:
+                    this._init_msg();
+                    break;
+            }
+        },
 
-//
-// Server message handlers
-//
+        _checkEvents: function () {
+            if (this._rfb_state === 'normal' && !this._viewportDragging && this._mouse_arr.length > 0) {
+                this._sock.send(this._mouse_arr);
+                this._mouse_arr = [];
+            }
+        },
 
-// RFB/VNC initialisation message handler
-init_msg = function() {
-    //Util.Debug(">> init_msg [rfb_state '" + rfb_state + "']");
+        _handleKeyPress: function (keysym, down) {
+            if (this._view_only) { return; } // View only, skip keyboard, events
+            this._sock.send(RFB.messages.keyEvent(keysym, down));
+        },
 
-    var strlen, reason, length, sversion, cversion, repeaterID,
-        i, types, num_types, challenge, response, bpp, depth,
-        big_endian, red_max, green_max, blue_max, red_shift,
-        green_shift, blue_shift, true_color, name_length, is_repeater,
-        xvp_sep, xvp_auth, xvp_auth_str;
+        _handleMouseButton: function (x, y, down, bmask) {
+            if (down) {
+                this._mouse_buttonMask |= bmask;
+            } else {
+                this._mouse_buttonMaks ^= bmask;
+            }
 
-    //Util.Debug("ws.rQ (" + ws.rQlen() + ") " + ws.rQslice(0));
-    switch (rfb_state) {
+            if (this._viewportDrag) {
+                if (down && !this._viewportDragging) {
+                    this._viewportDragging = true;
+                    this._viewportDragPos = {'x': x, 'y': y};
 
-    case 'ProtocolVersion' :
-        if (ws.rQlen() < 12) {
-            return fail("Incomplete protocol version");
-        }
-        sversion = ws.rQshiftStr(12).substr(4,7);
-        Util.Info("Server ProtocolVersion: " + sversion);
-        is_repeater = 0;
-        switch (sversion) {
-            case "000.000": is_repeater = 1; break; // UltraVNC repeater
-            case "003.003": rfb_version = 3.3; break;
-            case "003.006": rfb_version = 3.3; break;  // UltraVNC
-            case "003.889": rfb_version = 3.3; break;  // Apple Remote Desktop
-            case "003.007": rfb_version = 3.7; break;
-            case "003.008": rfb_version = 3.8; break;
-            case "004.000": rfb_version = 3.8; break;  // Intel AMT KVM
-            case "004.001": rfb_version = 3.8; break;  // RealVNC 4.6
-            default:
-                return fail("Invalid server version " + sversion);
-        }
-        if (is_repeater) { 
-            repeaterID = conf.repeaterID;
-            while (repeaterID.length < 250) {
-                repeaterID += "\0";
+                    // Skip sending mouse events
+                    return;
+                } else {
+                    this._viewportDragging = false;
+                }
             }
-            ws.send_string(repeaterID);
-            break;
-        }
-        if (rfb_version > rfb_max_version) { 
-            rfb_version = rfb_max_version;
-        }
 
-        if (! test_mode) {
-            sendTimer = setInterval(function() {
-                    // Send updates either at a rate of one update
-                    // every 50ms, or whatever slower rate the network
-                    // can handle.
-                    ws.flush();
-                }, 50);
-        }
+            if (this._view_only) { return; } // View only, skip mouse events
+
+            this._mouse_arr = this._mouse_arr.concat(
+                    RFB.messages.pointerEvent(this._display.absX(x), this._display.absY(y), this._mouse_buttonMask));
+            this._sock.send(this._mouse_arr);
+            this._mouse_arr = [];
+        },
 
-        cversion = "00" + parseInt(rfb_version,10) +
-                   ".00" + ((rfb_version * 10) % 10);
-        ws.send_string("RFB " + cversion + "\n");
-        updateState('Security', "Sent ProtocolVersion: " + cversion);
-        break;
-
-    case 'Security' :
-        if (rfb_version >= 3.7) {
-            // Server sends supported list, client decides 
-            num_types = ws.rQshift8();
-            if (ws.rQwait("security type", num_types, 1)) { return false; }
-            if (num_types === 0) {
-                strlen = ws.rQshift32();
-                reason = ws.rQshiftStr(strlen);
-                return fail("Security failure: " + reason);
-            }
-            rfb_auth_scheme = 0;
-            types = ws.rQshiftBytes(num_types);
-            Util.Debug("Server security types: " + types);
-            for (i=0; i < types.length; i+=1) {
-                if ((types[i] > rfb_auth_scheme) && (types[i] <= 16 || types[i] == 22)) {
-                    rfb_auth_scheme = types[i];
+        _handleMouseMove: function (x, y) {
+            if (this._viewportDragging) {
+                var deltaX = this._viewportDragPos.x - x;
+                var deltaY = this._viewportDragPos.y - y;
+                this._viewportDragPos = {'x': x, 'y': y};
+
+                this._display.viewportChange(deltaX, deltaY);
+
+                // Skip sending mouse events
+                return;
+            }
+
+            if (this._view_only) { return; } // View only, skip mouse events
+
+            this._mouse_arr = this._mouse_arr.concat(
+                    RFB.messages.pointerEvent(this._display.absX(x), this._display.absY(y), this._mouse_buttonMask));
+
+            this._checkEvents();
+        },
+
+        // Message Handlers
+
+        _negotiate_protocol_version: function () {
+            if (this._sock.rQlen() < 12) {
+                return this._fail("Incomplete protocol version");
+            }
+
+            var sversion = this._sock.rQshiftStr(12).substr(4, 7);
+            Util.Info("Server ProtocolVersion: " + sversion);
+            var is_repeater = 0;
+            switch (sversion) {
+                case "000.000":  // UltraVNC repeater
+                    is_repeater = 1;
+                    break;
+                case "003.003":
+                case "003.006":  // UltraVNC
+                case "003.889":  // Apple Remote Desktop
+                    this._rfb_version = 3.3;
+                    break;
+                case "003.007":
+                    this._rfb_version = 3.7;
+                    break;
+                case "003.008":
+                case "004.000":  // Intel AMT KVM
+                case "004.001":  // RealVNC 4.6
+                    this._rfb_version = 3.8;
+                    break;
+                default:
+                    return this._fail("Invalid server version " + sversion);
+            }
+
+            if (is_repeater) {
+                var repeaterID = this._repeaterID;
+                while (repeaterID.length < 250) {
+                    repeaterID += "\0";
                 }
+                this._sock.send_string(repeaterID);
+                return true;
             }
-            if (rfb_auth_scheme === 0) {
-                return fail("Unsupported security types: " + types);
+
+            if (this._rfb_version > this._rfb_max_version) {
+                this._rfb_version = this._rfb_max_version;
             }
-            
-            ws.send([rfb_auth_scheme]);
-        } else {
-            // Server decides
-            if (ws.rQwait("security scheme", 4)) { return false; }
-            rfb_auth_scheme = ws.rQshift32();
-        }
-        updateState('Authentication',
-                "Authenticating using scheme: " + rfb_auth_scheme);
-        init_msg();  // Recursive fallthrough (workaround JSLint complaint)
-        break;
-
-    // Triggered by fallthough, not by server message
-    case 'Authentication' :
-        //Util.Debug("Security auth scheme: " + rfb_auth_scheme);
-        switch (rfb_auth_scheme) {
-            case 0:  // connection failed
-                if (ws.rQwait("auth reason", 4)) { return false; }
-                strlen = ws.rQshift32();
-                reason = ws.rQshiftStr(strlen);
-                return fail("Auth failure: " + reason);
-            case 1:  // no authentication
-                if (rfb_version >= 3.8) {
-                    updateState('SecurityResult');
-                    return;
+
+            // Send updates either at a rate of 1 update per 50ms, or
+            // whatever slower rate the network can handle
+            this._sendTimer = setInterval(this._sock.flush.bind(this._sock), 50);
+
+            var cversion = "00" + parseInt(this._rfb_version, 10) +
+                           ".00" + ((this._rfb_version * 10) % 10);
+            this._sock.send_string("RFB " + cversion + "\n");
+            this._updateState('Security', 'Sent ProtocolVersion: ' + cversion);
+        },
+
+        _negotiate_security: function () {
+            if (this._rfb_version >= 3.7) {
+                // Server sends supported list, client decides
+                var num_types = this._sock.rQshift8();
+                if (this._sock.rQwait("security type", num_types, 1)) { return false; }
+
+                if (num_types === 0) {
+                    var strlen = this._sock.rQshift32();
+                    var reason = this._sock.rQshiftStr(strlen);
+                    return this._fail("Security failure: " + reason);
                 }
-                // Fall through to ClientInitialisation
-                break;
-            case 22:  // XVP authentication
-                xvp_sep = conf.xvp_password_sep;
-                xvp_auth = rfb_password.split(xvp_sep);
-                if (xvp_auth.length < 3) {
-                    updateState('password', "XVP credentials required (user" + xvp_sep +
-                                "target" + xvp_sep + "password) -- got only " + rfb_password);
-                    conf.onPasswordRequired(that);
-                    return;
+
+                this._rfb_auth_scheme = 0;
+                var types = this._sock.rQshiftBytes(num_types);
+                Util.Debug("Server security types: " + types);
+                for (var i = 0; i < types.length; i++) {
+                    if (types[i] > this._rfb_auth_scheme && (types[i] <= 16 || types[i] == 22)) {
+                        this._rfb_auth_scheme = types[i];
+                    }
+                }
+
+                if (this._rfb_auth_scheme === 0) {
+                    return this._fail("Unsupported security types: " + types);
                 }
-                xvp_auth_str = String.fromCharCode(xvp_auth[0].length) +
+
+                this._sock.send([this._rfb_auth_scheme]);
+            } else {
+                // Server decides
+                if (this._sock.rQwait("security scheme", 4)) { return false; }
+                this._rfb_auth_scheme = this._sock.rQshift32();
+            }
+
+            this._updateState('Authentication', 'Authenticating using scheme: ' + this._rfb_auth_scheme);
+            return this._init_msg(); // jump to authentication
+        },
+
+        // authentication
+        _negotiate_xvp_auth: function () {
+            var xvp_sep = this._xvp_password_sep;
+            var xvp_auth = this._rfb_password.split(xvp_sep);
+            if (xvp_auth.length < 3) {
+                this._updateState('password', 'XVP credentials required (user' + xvp_sep +
+                                  'target' + xvp_sep + 'password) -- got only ' + this._rfb_password);
+                this._onPasswordRequired(this);
+                return false;
+            }
+
+            var xvp_auth_str = String.fromCharCode(xvp_auth[0].length) +
                                String.fromCharCode(xvp_auth[1].length) +
                                xvp_auth[0] +
                                xvp_auth[1];
-                ws.send_string(xvp_auth_str);
-                rfb_password = xvp_auth.slice(2).join(xvp_sep);
-                rfb_auth_scheme = 2;
-                // Fall through to standard VNC authentication with remaining part of password
-            case 2:  // VNC authentication
-                if (rfb_password.length === 0) {
-                    // Notify via both callbacks since it is kind of
-                    // a RFB state change and a UI interface issue.
-                    updateState('password', "Password Required");
-                    conf.onPasswordRequired(that);
-                    return;
+            this._sock.send_string(xvp_auth_str);
+            this._rfb_password = xvp_auth.slice(2).join(xvp_sep);
+            this._rfb_auth_scheme = 2;
+            return this._negotiate_authentication();
+        },
+
+        _negotiate_std_vnc_auth: function () {
+            if (this._rfb_password.length === 0) {
+                // Notify via both callbacks since it's kind of
+                // an RFB state change and a UI interface issue
+                this._updateState('password', "Password Required");
+                this._onPasswordRequired(this);
+            }
+
+            if (this._sock.rQwait("auth challenge", 16)) { return false; }
+
+            var challenge = this._sock.rQshiftBytes(16);
+            var response = RFB.genDES(this._rfb_password, challenge);
+            this._sock.send(response);
+            this._updateState("SecurityResult");
+            return true;
+        },
+
+        _negotiate_tight_tunnels: function (numTunnels) {
+            var clientSupportedTunnelTypes = {
+                0: { vendor: 'TGHT', signature: 'NOTUNNEL' }
+            };
+            var serverSupportedTunnelTypes = {};
+            // receive tunnel capabilities
+            for (var i = 0; i < numTunnels; i++) {
+                var cap_code = this._sock.rQshift32();
+                var cap_vendor = this._sock.rQshiftStr(4);
+                var cap_signature = this._sock.rQshiftStr(8);
+                serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature };
+            }
+
+            // choose the notunnel type
+            if (serverSupportedTunnelTypes[0]) {
+                if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor ||
+                    serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) {
+                    return this._fail("Client's tunnel type had the incorrect vendor or signature");
                 }
-                if (ws.rQwait("auth challenge", 16)) { return false; }
-                challenge = ws.rQshiftBytes(16);
-                //Util.Debug("Password: " + rfb_password);
-                //Util.Debug("Challenge: " + challenge +
-                //           " (" + challenge.length + ")");
-                response = genDES(rfb_password, challenge);
-                //Util.Debug("Response: " + response +
-                //           " (" + response.length + ")");
-                
-                //Util.Debug("Sending DES encrypted auth response");
-                ws.send(response);
-                updateState('SecurityResult');
-                return;
-            case 16: // TightVNC Security Type
-                if (ws.rQwait("num tunnels", 4)) { return false; }
-                var numTunnels = ws.rQshift32();
-                //console.log("Number of tunnels: "+numTunnels);
+                this._sock.send([0, 0, 0, 0]);  // use NOTUNNEL
+                return false; // wait until we receive the sub auth count to continue
+            } else {
+                return this._fail("Server wanted tunnels, but doesn't support the notunnel type");
+            }
+        },
 
-                rfb_tightvnc = true;
+        _negotiate_tight_auth: function () {
+            if (!this._rfb_tightvnc) {  // first pass, do the tunnel negotiation
+                if (this._sock.rQwait("num tunnels", 4)) { return false; }
+                var numTunnels = this._sock.rQshift32();
+                if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; }
 
-                if (numTunnels != 0)
-                {
-                    fail("Protocol requested tunnels, not currently supported. numTunnels: " + numTunnels);
-                    return;
+                this._rfb_tightvnc = true;
+
+                if (numTunnels > 0) {
+                    this._negotiate_tight_tunnels(numTunnels);
+                    return false;  // wait until we receive the sub auth to continue
                 }
+            }
 
-                var clientSupportedTypes = {
-                    'STDVNOAUTH__': 1,
-                    'STDVVNCAUTH_': 2
-                };
+            // second pass, do the sub-auth negotiation
+            if (this._sock.rQwait("sub auth count", 4)) { return false; }
+            var subAuthCount = this._sock.rQshift32();
+            if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; }
 
-                var serverSupportedTypes = [];
+            var clientSupportedTypes = {
+                'STDVNOAUTH__': 1,
+                'STDVVNCAUTH_': 2
+            };
 
-                if (ws.rQwait("sub auth count", 4)) { return false; }
-                var subAuthCount = ws.rQshift32();
-                //console.log("Sub auth count: "+subAuthCount);
-                for (var i=0;i<subAuthCount;i++)
-                {
+            var serverSupportedTypes = [];
 
-                    if (ws.rQwait("sub auth capabilities "+i, 16)) { return false; }
-                    var capNum = ws.rQshift32();
-                    var capabilities = ws.rQshiftStr(12);
-                    //console.log("queue: "+ws.rQlen());
-                    //console.log("auth type: "+capNum+": "+capabilities);
+            for (var i = 0; i < subAuthCount; i++) {
+                var capNum = this._sock.rQshift32();
+                var capabilities = this._sock.rQshiftStr(12);
+                serverSupportedTypes.push(capabilities);
+            }
 
-                    serverSupportedTypes.push(capabilities);
+            for (var authType in clientSupportedTypes) {
+                if (serverSupportedTypes.indexOf(authType) != -1) {
+                    this._sock.send([0, 0, 0, clientSupportedTypes[authType]]);
+
+                    switch (authType) {
+                        case 'STDVNOAUTH__':  // no auth
+                            this._updateState('SecurityResult');
+                            return true;
+                        case 'STDVVNCAUTH_': // VNC auth
+                            this._rfb_auth_scheme = 2;
+                            return this._init_msg();
+                        default:
+                            return this._fail("Unsupported tiny auth scheme: " + authType);
+                    }
                 }
+            }
 
-                for (var authType in clientSupportedTypes)
-                {
-                    if (serverSupportedTypes.indexOf(authType) != -1)
-                    {
-                        //console.log("selected authType "+authType);
-                        ws.send([0,0,0,clientSupportedTypes[authType]]);
-
-                        switch (authType)
-                        {
-                            case 'STDVNOAUTH__':
-                                // No authentication
-                                updateState('SecurityResult');
-                                return;
-                            case 'STDVVNCAUTH_':
-                                // VNC Authentication.  Reenter auth handler to complete auth
-                                rfb_auth_scheme = 2;
-                                init_msg();
-                                return;
-                            default:
-                                fail("Unsupported tiny auth scheme: " + authType);
-                                return;
-                        }
+            this._fail("No supported sub-auth types!");
+        },
+
+        _negotiate_authentication: function () {
+            switch (this._rfb_auth_scheme) {
+                case 0:  // connection failed
+                    if (this._sock.rQwait("auth reason", 4)) { return false; }
+                    var strlen = this._sock.rQshift32();
+                    var reason = this._sock.rQshiftStr(strlen);
+                    return this._fail("Auth failure: " + reason);
+
+                case 1:  // no auth
+                    if (this._rfb_version >= 3.8) {
+                        this._updateState('SecurityResult');
+                        return true;
                     }
+                    this._updateState('ClientInitialisation', "No auth required");
+                    return this._init_msg();
+
+                case 22:  // XVP auth
+                    return this._negotiate_xvp_auth();
+
+                case 2:  // VNC authentication
+                    return this._negotiate_std_vnc_auth();
+
+                case 16:  // TightVNC Security Type
+                    return this._negotiate_tight_auth();
+
+                default:
+                    return this._fail("Unsupported auth scheme: " + this._rfb_auth_scheme);
+            }
+        },
+
+        _handle_security_result: function () {
+            if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
+            switch (this._sock.rQshift32()) {
+                case 0:  // OK
+                    this._updateState('ClientInitialisation', 'Authentication OK');
+                    return this._init_msg();
+                case 1:  // failed
+                    if (this._rfb_version >= 3.8) {
+                        var length = this._sock.rQshift32();
+                        if (this._sock.rQwait("SecurityResult reason", length, 8)) { return false; }
+                        var reason = this._sock.rQshiftStr(length);
+                        return this._fail(reason);
+                    } else {
+                        return this._fail("Authentication failure");
+                    }
+                    return false;
+                case 2:
+                    return this._fail("Too many auth attempts");
+            }
+        },
+
+        _negotiate_server_init: function () {
+            if (this._sock.rQwait("server initialization", 24)) { return false; }
+
+            /* Screen size */
+            this._fb_width  = this._sock.rQshift16();
+            this._fb_height = this._sock.rQshift16();
+
+            /* PIXEL_FORMAT */
+            var bpp         = this._sock.rQshift8();
+            var depth       = this._sock.rQshift8();
+            var big_endian  = this._sock.rQshift8();
+            var true_color  = this._sock.rQshift8();
+
+            var red_max     = this._sock.rQshift16();
+            var green_max   = this._sock.rQshift16();
+            var blue_max    = this._sock.rQshift16();
+            var red_shift   = this._sock.rQshift8();
+            var green_shift = this._sock.rQshift8();
+            var blue_shift  = this._sock.rQshift8();
+            this._sock.rQskipBytes(3);  // padding
+
+            // NB(directxman12): we don't want to call any callbacks or print messages until
+            //                   *after* we're past the point where we could backtrack
+
+            /* Connection name/title */
+            var name_length = this._sock.rQshift32();
+            if (this._sock.rQwait('server init name', name_length, 24)) { return false; }
+            this._fb_name = Util.decodeUTF8(this._sock.rQshiftStr(name_length));
+
+            if (this._rfb_tightvnc) {
+                if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; }
+                // In TightVNC mode, ServerInit message is extended
+                var numServerMessages = this._sock.rQshift16();
+                var numClientMessages = this._sock.rQshift16();
+                var numEncodings = this._sock.rQshift16();
+                this._sock.rQskipBytes(2);  // padding
+
+                var totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
+                if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; }
+
+                var i;
+                for (i = 0; i < numServerMessages; i++) {
+                    var srvMsg = this._sock.rQshiftStr(16);
                 }
 
+                for (i = 0; i < numClientMessages; i++) {
+                    var clientMsg = this._sock.rQshiftStr(16);
+                }
 
-                return;
-            default:
-                fail("Unsupported auth scheme: " + rfb_auth_scheme);
-                return;
-        }
-        updateState('ClientInitialisation', "No auth required");
-        init_msg();  // Recursive fallthrough (workaround JSLint complaint)
-        break;
-
-    case 'SecurityResult' :
-        if (ws.rQwait("VNC auth response ", 4)) { return false; }
-        switch (ws.rQshift32()) {
-            case 0:  // OK
-                // Fall through to ClientInitialisation
-                break;
-            case 1:  // failed
-                if (rfb_version >= 3.8) {
-                    length = ws.rQshift32();
-                    if (ws.rQwait("SecurityResult reason", length, 8)) {
+                for (i = 0; i < numEncodings; i++) {
+                    var encoding = this._sock.rQshiftStr(16);
+                }
+            }
+
+            // NB(directxman12): these are down here so that we don't run them multiple times
+            //                   if we backtrack
+            Util.Info("Screen: " + this._fb_width + "x" + this._fb_height +
+                      ", bpp: " + bpp + ", depth: " + depth +
+                      ", big_endian: " + big_endian +
+                      ", true_color: " + true_color +
+                      ", red_max: " + red_max +
+                      ", green_max: " + green_max +
+                      ", blue_max: " + blue_max +
+                      ", red_shift: " + red_shift +
+                      ", green_shift: " + green_shift +
+                      ", blue_shift: " + blue_shift);
+
+            if (big_endian !== 0) {
+                Util.Warn("Server native endian is not little endian");
+            }
+
+            if (red_shift !== 16) {
+                Util.Warn("Server native red-shift is not 16");
+            }
+
+            if (blue_shift !== 0) {
+                Util.Warn("Server native blue-shift is not 0");
+            }
+
+            // we're past the point where we could backtrack, so it's safe to call this
+            this._onDesktopName(this, this._fb_name);
+
+            if (this._true_color && this._fb_name === "Intel(r) AMT KVM") {
+                Util.Warn("Intel AMT KVM only supports 8/16 bit depths.  Disabling true color");
+                this._true_color = false;
+            }
+
+            this._display.set_true_color(this._true_color);
+            this._onFBResize(this, this._fb_width, this._fb_height);
+            this._display.resize(this._fb_width, this._fb_height);
+            this._keyboard.grab();
+            this._mouse.grab();
+
+            if (this._true_color) {
+                this._fb_Bpp = 4;
+                this._fb_depth = 3;
+            } else {
+                this._fb_Bpp = 1;
+                this._fb_depth = 1;
+            }
+
+            var response = RFB.messages.pixelFormat(this._fb_Bpp, this._fb_depth, this._true_color);
+            response = response.concat(
+                            RFB.messages.clientEncodings(this._encodings, this._local_cursor, this._true_color));
+            response = response.concat(
+                            RFB.messages.fbUpdateRequests(this._display.getCleanDirtyReset(),
+                                                          this._fb_width, this._fb_height));
+
+            this._timing.fbu_rt_start = (new Date()).getTime();
+            this._timing.pixels = 0;
+            this._sock.send(response);
+
+            this._checkEvents();
+
+            if (this._encrypt) {
+                this._updateState('normal', 'Connected (encrypted) to: ' + this._fb_name);
+            } else {
+                this._updateState('normal', 'Connected (unencrypted) to: ' + this._fb_name);
+            }
+        },
+
+        _init_msg: function () {
+            switch (this._rfb_state) {
+                case 'ProtocolVersion':
+                    return this._negotiate_protocol_version();
+
+                case 'Security':
+                    return this._negotiate_security();
+
+                case 'Authentication':
+                    return this._negotiate_authentication();
+
+                case 'SecurityResult':
+                    return this._handle_security_result();
+
+                case 'ClientInitialisation':
+                    this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation
+                    this._updateState('ServerInitialisation', "Authentication OK");
+                    return true;
+
+                case 'ServerInitialisation':
+                    return this._negotiate_server_init();
+            }
+        },
+
+        _handle_set_colour_map_msg: function () {
+            Util.Debug("SetColorMapEntries");
+            this._sock.rQskip8();  // Padding
+
+            var first_colour = this._sock.rQshift16();
+            var num_colours = this._sock.rQshift16();
+            if (this._sock.rQwait('SetColorMapEntries', num_colours * 6, 6)) { return false; }
+
+            for (var c = 0; c < num_colours; c++) {
+                var red = parseInt(this._sock.rQshift16() / 256, 10);
+                var green = parseInt(this._sock.rQshift16() / 256, 10);
+                var blue = parseInt(this._sock.rQshift16() / 256, 10);
+                this._display.set_colourMap([blue, green, red], first_colour + c);
+            }
+            Util.Debug("colourMap: " + this._display.get_colourMap());
+            Util.Info("Registered " + num_colours + " colourMap entries");
+
+            return true;
+        },
+
+        _handle_server_cut_text: function () {
+            Util.Debug("ServerCutText");
+            if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; }
+            this._sock.rQskipBytes(3);  // Padding
+            var length = this._sock.rQshift32();
+            if (this._sock.rQwait("ServerCutText", length, 8)) { return false; }
+
+            var text = this._sock.rQshiftStr(length);
+            this._onClipboard(this, text);
+
+            return true;
+        },
+
+        _handle_xvp_msg: function () {
+            if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; }
+            this._sock.rQskip8();  // Padding
+            var xvp_ver = this._sock.rQshift8();
+            var xvp_msg = this._sock.rQshift8();
+
+            switch (xvp_msg) {
+                case 0:  // XVP_FAIL
+                    this._updateState(this._rfb_state, "Operation Failed");
+                    break;
+                case 1:  // XVP_INIT
+                    this._rfb_xvp_ver = xvp_ver;
+                    Util.Info("XVP extensions enabled (version " + this._rfb_xvp_ver + ")");
+                    this._onXvpInit(this._rfb_xvp_ver);
+                    break;
+                default:
+                    this._fail("Disconnected: illegal server XVP message " + xvp_msg);
+                    break;
+            }
+
+            return true;
+        },
+
+        _normal_msg: function () {
+            var msg_type;
+
+            if (this._FBU.rects > 0) {
+                msg_type = 0;
+            } else {
+                msg_type = this._sock.rQshift8();
+            }
+
+            switch (msg_type) {
+                case 0:  // FramebufferUpdate
+                    var ret = this._framebufferUpdate();
+                    if (ret) {
+                        this._sock.send(RFB.messages.fbUpdateRequests(this._display.getCleanDirtyReset(),
+                                                                      this._fb_width, this._fb_height));
+                    }
+                    return ret;
+
+                case 1:  // SetColorMapEntries
+                    return this._handle_set_colour_map_msg();
+
+                case 2:  // Bell
+                    Util.Debug("Bell");
+                    this._onBell(this);
+                    return true;
+
+                case 3:  // ServerCutText
+                    return this._handle_server_cut_text();
+
+                case 250:  // XVP
+                    return this._handle_xvp_msg();
+
+                default:
+                    this._fail("Disconnected: illegal server message type " + msg_type);
+                    Util.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30));
+                    return true;
+            }
+        },
+
+        _framebufferUpdate: function () {
+            var ret = true;
+            var now;
+
+            if (this._FBU.rects === 0) {
+                if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
+                this._sock.rQskip8();  // Padding
+                this._FBU.rects = this._sock.rQshift16();
+                this._FBU.bytes = 0;
+                this._timing.cur_fbu = 0;
+                if (this._timing.fbu_rt_start > 0) {
+                    now = (new Date()).getTime();
+                    Util.Info("First FBU latency: " + (now - this._timing.fbu_rt_start));
+                }
+            }
+
+            while (this._FBU.rects > 0) {
+                if (this._rfb_state !== "normal") { return false; }
+
+                if (this._sock.rQwait("FBU", this._FBU.bytes)) { return false; }
+                if (this._FBU.bytes === 0) {
+                    if (this._sock.rQwait("rect header", 12)) { return false; }
+                    /* New FramebufferUpdate */
+
+                    var hdr = this._sock.rQshiftBytes(12);
+                    this._FBU.x        = (hdr[0] << 8) + hdr[1];
+                    this._FBU.y        = (hdr[2] << 8) + hdr[3];
+                    this._FBU.width    = (hdr[4] << 8) + hdr[5];
+                    this._FBU.height   = (hdr[6] << 8) + hdr[7];
+                    this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) +
+                                                  (hdr[10] << 8) + hdr[11], 10);
+
+                    this._onFBUReceive(this,
+                        {'x': this._FBU.x, 'y': this._FBU.y,
+                         'width': this._FBU.width, 'height': this._FBU.height,
+                         'encoding': this._FBU.encoding,
+                         'encodingName': this._encNames[this._FBU.encoding]});
+
+                    if (!this._encNames[this._FBU.encoding]) {
+                        this._fail("Disconnected: unsupported encoding " +
+                                   this._FBU.encoding);
                         return false;
                     }
-                    reason = ws.rQshiftStr(length);
-                    fail(reason);
-                } else {
-                    fail("Authentication failed");
                 }
-                return;
-            case 2:  // too-many
-                return fail("Too many auth attempts");
-        }
-        updateState('ClientInitialisation', "Authentication OK");
-        init_msg();  // Recursive fallthrough (workaround JSLint complaint)
-        break;
-
-    // Triggered by fallthough, not by server message
-    case 'ClientInitialisation' :
-        ws.send([conf.shared ? 1 : 0]); // ClientInitialisation
-        updateState('ServerInitialisation', "Authentication OK");
-        break;
-
-    case 'ServerInitialisation' :
-        if (ws.rQwait("server initialization", 24)) { return false; }
-
-        /* Screen size */
-        fb_width  = ws.rQshift16();
-        fb_height = ws.rQshift16();
-
-        /* PIXEL_FORMAT */
-        bpp            = ws.rQshift8();
-        depth          = ws.rQshift8();
-        big_endian     = ws.rQshift8();
-        true_color     = ws.rQshift8();
-
-        red_max        = ws.rQshift16();
-        green_max      = ws.rQshift16();
-        blue_max       = ws.rQshift16();
-        red_shift      = ws.rQshift8();
-        green_shift    = ws.rQshift8();
-        blue_shift     = ws.rQshift8();
-        ws.rQshiftStr(3); // padding
-
-        Util.Info("Screen: " + fb_width + "x" + fb_height + 
-                  ", bpp: " + bpp + ", depth: " + depth +
-                  ", big_endian: " + big_endian +
-                  ", true_color: " + true_color +
-                  ", red_max: " + red_max +
-                  ", green_max: " + green_max +
-                  ", blue_max: " + blue_max +
-                  ", red_shift: " + red_shift +
-                  ", green_shift: " + green_shift +
-                  ", blue_shift: " + blue_shift);
-
-        if (big_endian !== 0) {
-            Util.Warn("Server native endian is not little endian");
-        }
-        if (red_shift !== 16) {
-            Util.Warn("Server native red-shift is not 16");
-        }
-        if (blue_shift !== 0) {
-            Util.Warn("Server native blue-shift is not 0");
-        }
 
-        /* Connection name/title */
-        name_length   = ws.rQshift32();
-        fb_name = Util.decodeUTF8(ws.rQshiftStr(name_length));
-        conf.onDesktopName(that, fb_name);
-        
-        if (conf.true_color && fb_name === "Intel(r) AMT KVM")
-        {
-            Util.Warn("Intel AMT KVM only support 8/16 bit depths. Disabling true color");
-            conf.true_color = false;
-        }
+                this._timing.last_fbu = (new Date()).getTime();
+
+                ret = this._encHandlers[this._FBU.encoding]();
+
+                now = (new Date()).getTime();
+                this._timing.cur_fbu += (now - this._timing.last_fbu);
 
-        if (rfb_tightvnc)
-        {
-            // In TightVNC mode, ServerInit message is extended
-            var numServerMessages = ws.rQshift16();
-            var numClientMessages = ws.rQshift16();
-            var numEncodings = ws.rQshift16();
-            ws.rQshift16(); // padding
-            //console.log("numServerMessages "+numServerMessages);
-            //console.log("numClientMessages "+numClientMessages);
-            //console.log("numEncodings "+numEncodings);
-
-            for (var i=0;i<numServerMessages;i++)
-            {
-                var srvMsg = ws.rQshiftStr(16);
-                //console.log("server message: "+srvMsg);
-            }
-            for (var i=0;i<numClientMessages;i++)
-            {
-                var clientMsg = ws.rQshiftStr(16);
-                //console.log("client message: "+clientMsg);
-            }
-            for (var i=0;i<numEncodings;i++)
-            {
-                var encoding = ws.rQshiftStr(16);
-                //console.log("encoding: "+encoding);
+                if (ret) {
+                    this._encStats[this._FBU.encoding][0]++;
+                    this._encStats[this._FBU.encoding][1]++;
+                    this._timing.pixels += this._FBU.width * this._FBU.height;
+                }
+
+                if (this._timing.pixels >= (this._fb_width * this._fb_height)) {
+                    if ((this._FBU.width === this._fb_width && this._FBU.height === this._fb_height) ||
+                        this._timing.fbu_rt_start > 0) {
+                        this._timing.full_fbu_total += this._timing.cur_fbu;
+                        this._timing.full_fbu_cnt++;
+                        Util.Info("Timing of full FBU, curr: " +
+                                  this._timing.cur_fbu + ", total: " +
+                                  this._timing.full_fbu_total + ", cnt: " +
+                                  this._timing.full_fbu_cnt + ", avg: " +
+                                  (this._timing.full_fbu_total / this._timing.full_fbu_cnt));
+                    }
+
+                    if (this._timing.fbu_rt_start > 0) {
+                        var fbu_rt_diff = now - this._timing.fbu_rt_start;
+                        this._timing.fbu_rt_total += fbu_rt_diff;
+                        this._timing.fbu_rt_cnt++;
+                        Util.Info("full FBU round-trip, cur: " +
+                                  fbu_rt_diff + ", total: " +
+                                  this._timing.fbu_rt_total + ", cnt: " +
+                                  this._timing.fbu_rt_cnt + ", avg: " +
+                                  (this._timing.fbu_rt_total / this._timing.fbu_rt_cnt));
+                        this._timing.fbu_rt_start = 0;
+                    }
+                }
+
+                if (!ret) { return ret; }  // need more data
             }
-        }
 
-        display.set_true_color(conf.true_color);
-        conf.onFBResize(that, fb_width, fb_height);
-        display.resize(fb_width, fb_height);
-        keyboard.grab();
-        mouse.grab();
+            this._onFBUComplete(this,
+                    {'x': this._FBU.x, 'y': this._FBU.y,
+                     'width': this._FBU.width, 'height': this._FBU.height,
+                     'encoding': this._FBU.encoding,
+                     'encodingName': this._encNames[this._FBU.encoding]});
 
-        if (conf.true_color) {
-            fb_Bpp           = 4;
-            fb_depth         = 3;
-        } else {
-            fb_Bpp           = 1;
-            fb_depth         = 1;
-        }
+            return true;  // We finished this FBU
+        },
+    };
 
-        response = pixelFormat();
-        response = response.concat(clientEncodings());
-        response = response.concat(fbUpdateRequests()); // initial fbu-request
-        timing.fbu_rt_start = (new Date()).getTime();
-        timing.pixels = 0;
-        ws.send(response);
-        
-        checkEvents();
-
-        if (conf.encrypt) {
-            updateState('normal', "Connected (encrypted) to: " + fb_name);
-        } else {
-            updateState('normal', "Connected (unencrypted) to: " + fb_name);
-        }
-        break;
-    }
-    //Util.Debug("<< init_msg");
-};
-
-
-/* Normal RFB/VNC server message handler */
-normal_msg = function() {
-    //Util.Debug(">> normal_msg");
-
-    var ret = true, msg_type, length, text,
-        c, first_colour, num_colours, red, green, blue,
-        xvp_ver, xvp_msg;
-
-    if (FBU.rects > 0) {
-        msg_type = 0;
-    } else {
-        msg_type = ws.rQshift8();
-    }
-    switch (msg_type) {
-    case 0:  // FramebufferUpdate
-        ret = framebufferUpdate(); // false means need more data
-        if (ret) {
-            // only allow one outstanding fbu-request at a time
-            ws.send(fbUpdateRequests());
-        }
-        break;
-    case 1:  // SetColourMapEntries
-        Util.Debug("SetColourMapEntries");
-        ws.rQshift8();  // Padding
-        first_colour = ws.rQshift16(); // First colour
-        num_colours = ws.rQshift16();
-        if (ws.rQwait("SetColourMapEntries", num_colours*6, 6)) { return false; }
-        
-        for (c=0; c < num_colours; c+=1) { 
-            red = ws.rQshift16();
-            //Util.Debug("red before: " + red);
-            red = parseInt(red / 256, 10);
-            //Util.Debug("red after: " + red);
-            green = parseInt(ws.rQshift16() / 256, 10);
-            blue = parseInt(ws.rQshift16() / 256, 10);
-            display.set_colourMap([blue, green, red], first_colour + c);
-        }
-        Util.Debug("colourMap: " + display.get_colourMap());
-        Util.Info("Registered " + num_colours + " colourMap entries");
-        //Util.Debug("colourMap: " + display.get_colourMap());
-        break;
-    case 2:  // Bell
-        Util.Debug("Bell");
-        conf.onBell(that);
-        break;
-    case 3:  // ServerCutText
-        Util.Debug("ServerCutText");
-        if (ws.rQwait("ServerCutText header", 7, 1)) { return false; }
-        ws.rQshiftBytes(3);  // Padding
-        length = ws.rQshift32();
-        if (ws.rQwait("ServerCutText", length, 8)) { return false; }
-
-        text = ws.rQshiftStr(length);
-        conf.clipboardReceive(that, text); // Obsolete
-        conf.onClipboard(that, text);
-        break;
-    case 250:  // XVP
-        ws.rQshift8();  // Padding
-        xvp_ver = ws.rQshift8();
-        xvp_msg = ws.rQshift8();
-        switch (xvp_msg) {
-        case 0:  // XVP_FAIL
-            updateState(rfb_state, "Operation failed");
-            break;
-        case 1:  // XVP_INIT
-            rfb_xvp_ver = xvp_ver;
-            Util.Info("XVP extensions enabled (version " + rfb_xvp_ver + ")");
-            conf.onXvpInit(rfb_xvp_ver);
-            break;
-        default:
-            fail("Disconnected: illegal server XVP message " + xvp_msg);
-            break;
-        }
-        break;
-    default:
-        fail("Disconnected: illegal server message type " + msg_type);
-        Util.Debug("ws.rQslice(0,30):" + ws.rQslice(0,30));
-        break;
-    }
-    //Util.Debug("<< normal_msg");
-    return ret;
-};
-
-framebufferUpdate = function() {
-    var now, hdr, fbu_rt_diff, ret = true;
-
-    if (FBU.rects === 0) {
-        //Util.Debug("New FBU: ws.rQslice(0,20): " + ws.rQslice(0,20));
-        if (ws.rQwait("FBU header", 3)) {
-            ws.rQunshift8(0);  // FBU msg_type
-            return false;
-        }
-        ws.rQshift8();  // padding
-        FBU.rects = ws.rQshift16();
-        //Util.Debug("FramebufferUpdate, rects:" + FBU.rects);
-        FBU.bytes = 0;
-        timing.cur_fbu = 0;
-        if (timing.fbu_rt_start > 0) {
-            now = (new Date()).getTime();
-            Util.Info("First FBU latency: " + (now - timing.fbu_rt_start));
-        }
-    }
+    Util.make_properties(RFB, [
+        ['target', 'wo', 'dom'],                // VNC display rendering Canvas object
+        ['focusContainer', 'wo', 'dom'],        // DOM element that captures keyboard input
+        ['encrypt', 'rw', 'bool'],              // Use TLS/SSL/wss encryption
+        ['true_color', 'rw', 'bool'],           // Request true color pixel data
+        ['local_cursor', 'rw', 'bool'],         // Request locally rendered cursor
+        ['shared', 'rw', 'bool'],               // Request shared mode
+        ['view_only', 'rw', 'bool'],            // Disable client mouse/keyboard
+        ['xvp_password_sep', 'rw', 'str'],      // Separator for XVP password fields
+        ['disconnectTimeout', 'rw', 'int'],     // Time (s) to wait for disconnection
+        ['wsProtocols', 'rw', 'arr'],           // Protocols to use in the WebSocket connection
+        ['repeaterID', 'rw', 'str'],            // [UltraVNC] RepeaterID to connect to
+        ['viewportDrag', 'rw', 'bool'],         // Move the viewport on mouse drags
+
+        // Callback functions
+        ['onUpdateState', 'rw', 'func'],        // onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change
+        ['onPasswordRequired', 'rw', 'func'],   // onPasswordRequired(rfb): VNC password is required
+        ['onClipboard', 'rw', 'func'],          // onClipboard(rfb, text): RFB clipboard contents received
+        ['onBell', 'rw', 'func'],               // onBell(rfb): RFB Bell message received
+        ['onFBUReceive', 'rw', 'func'],         // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed
+        ['onFBUComplete', 'rw', 'func'],        // onFBUComplete(rfb, fbu): RFB FBU received and processed
+        ['onFBResize', 'rw', 'func'],           // onFBResize(rfb, width, height): frame buffer resized
+        ['onDesktopName', 'rw', 'func'],        // onDesktopName(rfb, name): desktop name received
+        ['onXvpInit', 'rw', 'func'],            // onXvpInit(version): XVP extensions active for this connection
+    ]);
 
-    while (FBU.rects > 0) {
-        if (rfb_state !== "normal") {
-            return false;
-        }
-        if (ws.rQwait("FBU", FBU.bytes)) { return false; }
-        if (FBU.bytes === 0) {
-            if (ws.rQwait("rect header", 12)) { return false; }
-            /* New FramebufferUpdate */
-
-            hdr = ws.rQshiftBytes(12);
-            FBU.x      = (hdr[0] << 8) + hdr[1];
-            FBU.y      = (hdr[2] << 8) + hdr[3];
-            FBU.width  = (hdr[4] << 8) + hdr[5];
-            FBU.height = (hdr[6] << 8) + hdr[7];
-            FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) +
-                                    (hdr[10] << 8) +  hdr[11], 10);
-
-            conf.onFBUReceive(that,
-                    {'x': FBU.x, 'y': FBU.y,
-                     'width': FBU.width, 'height': FBU.height,
-                     'encoding': FBU.encoding,
-                     'encodingName': encNames[FBU.encoding]});
-
-            if (encNames[FBU.encoding]) {
-                // Debug:
-                /*
-                var msg =  "FramebufferUpdate rects:" + FBU.rects;
-                msg += " x: " + FBU.x + " y: " + FBU.y;
-                msg += " width: " + FBU.width + " height: " + FBU.height;
-                msg += " encoding:" + FBU.encoding;
-                msg += "(" + encNames[FBU.encoding] + ")";
-                msg += ", ws.rQlen(): " + ws.rQlen();
-                Util.Debug(msg);
-                */
+    RFB.prototype.set_local_cursor = function (cursor) {
+        if (!cursor || (cursor in {'0': 1, 'no': 1, 'false': 1})) {
+            this._local_cursor = false;
+        } else {
+            if (this._display.get_cursor_uri()) {
+                this._local_cursor = true;
             } else {
-                fail("Disconnected: unsupported encoding " +
-                    FBU.encoding);
-                return false;
+                Util.Warn("Browser does not support local cursor");
             }
         }
+    };
 
-        timing.last_fbu = (new Date()).getTime();
+    RFB.prototype.get_display = function () { return this._display; };
+    RFB.prototype.get_keyboard = function () { return this._keyboard; };
+    RFB.prototype.get_mouse = function () { return this._mouse; };
+
+    // Class Methods
+    RFB.messages = {
+        keyEvent: function (keysym, down) {
+            var arr = [4];
+            arr.push8(down);
+            arr.push16(0);
+            arr.push32(keysym);
+            return arr;
+        },
+
+        pointerEvent: function (x, y, mask) {
+            var arr = [5];  // msg-type
+            arr.push8(mask);
+            arr.push16(x);
+            arr.push16(y);
+            return arr;
+        },
+
+        // TODO(directxman12): make this unicode compatible?
+        clientCutText: function (text) {
+            var arr = [6];  // msg-type
+            arr.push8(0);   // padding
+            arr.push8(0);   // padding
+            arr.push8(0);   // padding
+            arr.push32(text.length);
+            var n = text.length;
+            for (var i = 0; i < n; i++) {
+                arr.push(text.charCodeAt(i));
+            }
 
-        ret = encHandlers[FBU.encoding]();
+            return arr;
+        },
+
+        pixelFormat: function (bpp, depth, true_color) {
+            var arr = [0]; // msg-type
+            arr.push8(0);  // padding
+            arr.push8(0);  // padding
+            arr.push8(0);  // padding
+
+            arr.push8(bpp * 8); // bits-per-pixel
+            arr.push8(depth * 8); // depth
+            arr.push8(0);  // little-endian
+            arr.push8(true_color ? 1 : 0);  // true-color
+
+            arr.push16(255);  // red-max
+            arr.push16(255);  // green-max
+            arr.push16(255);  // blue-max
+            arr.push8(16);    // red-shift
+            arr.push8(8);     // green-shift
+            arr.push8(0);     // blue-shift
+
+            arr.push8(0);     // padding
+            arr.push8(0);     // padding
+            arr.push8(0);     // padding
+            return arr;
+        },
+
+        clientEncodings: function (encodings, local_cursor, true_color) {
+            var i, encList = [];
+
+            for (i = 0; i < encodings.length; i++) {
+                if (encodings[i][0] === "Cursor" && !local_cursor) {
+                    Util.Debug("Skipping Cursor pseudo-encoding");
+                } else if (encodings[i][0] === "TIGHT" && !true_color) {
+                    // TODO: remove this when we have tight+non-true-color
+                    Util.Warn("Skipping tight as it is only supported with true color");
+                } else {
+                    encList.push(encodings[i][1]);
+                }
+            }
 
-        now = (new Date()).getTime();
-        timing.cur_fbu += (now - timing.last_fbu);
+            var arr = [2];  // msg-type
+            arr.push8(0);   // padding
 
-        if (ret) {
-            encStats[FBU.encoding][0] += 1;
-            encStats[FBU.encoding][1] += 1;
-            timing.pixels += FBU.width * FBU.height;
-        }
+            arr.push16(encList.length);  // encoding count
+            for (i = 0; i < encList.length; i++) {
+                arr.push32(encList[i]);
+            }
+
+            return arr;
+        },
+
+        fbUpdateRequests: function (cleanDirty, fb_width, fb_height) {
+            var arr = [];
 
-        if (timing.pixels >= (fb_width * fb_height)) {
-            if (((FBU.width === fb_width) &&
-                        (FBU.height === fb_height)) ||
-                    (timing.fbu_rt_start > 0)) {
-                timing.full_fbu_total += timing.cur_fbu;
-                timing.full_fbu_cnt += 1;
-                Util.Info("Timing of full FBU, cur: " +
-                          timing.cur_fbu + ", total: " +
-                          timing.full_fbu_total + ", cnt: " +
-                          timing.full_fbu_cnt + ", avg: " +
-                          (timing.full_fbu_total /
-                              timing.full_fbu_cnt));
-            }
-            if (timing.fbu_rt_start > 0) {
-                fbu_rt_diff = now - timing.fbu_rt_start;
-                timing.fbu_rt_total += fbu_rt_diff;
-                timing.fbu_rt_cnt += 1;
-                Util.Info("full FBU round-trip, cur: " +
-                          fbu_rt_diff + ", total: " +
-                          timing.fbu_rt_total + ", cnt: " +
-                          timing.fbu_rt_cnt + ", avg: " +
-                          (timing.fbu_rt_total /
-                              timing.fbu_rt_cnt));
-                timing.fbu_rt_start = 0;
+            var cb = cleanDirty.cleanBox;
+            var w, h;
+            if (cb.w > 0 && cb.h > 0) {
+                w = typeof cb.w === "undefined" ? fb_width : cb.w;
+                h = typeof cb.h === "undefined" ? fb_height : cb.h;
+                // Request incremental for clean box
+                arr = arr.concat(RFB.messages.fbUpdateRequest(1, cb.x, cb.y, w, h));
             }
+
+            for (var i = 0; i < cleanDirty.dirtyBoxes.length; i++) {
+                var db = cleanDirty.dirtyBoxes[i];
+                // Force all (non-incremental) for dirty box
+                w = typeof db.w === "undefined" ? fb_width : db.w;
+                h = typeof db.h === "undefined" ? fb_height : db.h;
+                arr = arr.concat(RFB.messages.fbUpdateRequest(0, db.x, db.y, w, h));
+            }
+
+            return arr;
+        },
+
+        fbUpdateRequest: function (incremental, x, y, w, h) {
+            if (typeof(x) === "undefined") { x = 0; }
+            if (typeof(y) === "undefined") { y = 0; }
+
+            var arr = [3];  // msg-type
+            arr.push8(incremental);
+            arr.push16(x);
+            arr.push16(y);
+            arr.push16(w);
+            arr.push16(h);
+
+            return arr;
         }
-        if (! ret) {
-            return ret; // false ret means need more data
-        }
-    }
-
-    conf.onFBUComplete(that,
-            {'x': FBU.x, 'y': FBU.y,
-                'width': FBU.width, 'height': FBU.height,
-                'encoding': FBU.encoding,
-                'encodingName': encNames[FBU.encoding]});
-
-    return true; // We finished this FBU
-};
-
-//
-// FramebufferUpdate encodings
-//
-
-encHandlers.RAW = function display_raw() {
-    //Util.Debug(">> display_raw (" + ws.rQlen() + " bytes)");
-
-    var cur_y, cur_height;
-
-    if (FBU.lines === 0) {
-        FBU.lines = FBU.height;
-    }
-    FBU.bytes = FBU.width * fb_Bpp; // At least a line
-    if (ws.rQwait("RAW", FBU.bytes)) { return false; }
-    cur_y = FBU.y + (FBU.height - FBU.lines);
-    cur_height = Math.min(FBU.lines,
-                          Math.floor(ws.rQlen()/(FBU.width * fb_Bpp)));
-    display.blitImage(FBU.x, cur_y, FBU.width, cur_height,
-            ws.get_rQ(), ws.get_rQi());
-    ws.rQshiftBytes(FBU.width * cur_height * fb_Bpp);
-    FBU.lines -= cur_height;
-
-    if (FBU.lines > 0) {
-        FBU.bytes = FBU.width * fb_Bpp; // At least another line
-    } else {
-        FBU.rects -= 1;
-        FBU.bytes = 0;
-    }
-    //Util.Debug("<< display_raw (" + ws.rQlen() + " bytes)");
-    return true;
-};
-
-encHandlers.COPYRECT = function display_copy_rect() {
-    //Util.Debug(">> display_copy_rect");
-
-    var old_x, old_y;
-
-    FBU.bytes = 4;
-    if (ws.rQwait("COPYRECT", 4)) { return false; }
-    display.renderQ_push({
-            'type': 'copy',
-            'old_x': ws.rQshift16(),
-            'old_y': ws.rQshift16(),
-            'x': FBU.x,
-            'y': FBU.y,
-            'width': FBU.width,
-            'height': FBU.height});
-    FBU.rects -= 1;
-    FBU.bytes = 0;
-    return true;
-};
-
-encHandlers.RRE = function display_rre() {
-    //Util.Debug(">> display_rre (" + ws.rQlen() + " bytes)");
-    var color, x, y, width, height, chunk;
-
-    if (FBU.subrects === 0) {
-        FBU.bytes = 4+fb_Bpp;
-        if (ws.rQwait("RRE", 4+fb_Bpp)) { return false; }
-        FBU.subrects = ws.rQshift32();
-        color = ws.rQshiftBytes(fb_Bpp); // Background
-        display.fillRect(FBU.x, FBU.y, FBU.width, FBU.height, color);
-    }
-    while ((FBU.subrects > 0) && (ws.rQlen() >= (fb_Bpp + 8))) {
-        color = ws.rQshiftBytes(fb_Bpp);
-        x = ws.rQshift16();
-        y = ws.rQshift16();
-        width = ws.rQshift16();
-        height = ws.rQshift16();
-        display.fillRect(FBU.x + x, FBU.y + y, width, height, color);
-        FBU.subrects -= 1;
-    }
-    //Util.Debug("   display_rre: rects: " + FBU.rects +
-    //           ", FBU.subrects: " + FBU.subrects);
-
-    if (FBU.subrects > 0) {
-        chunk = Math.min(rre_chunk_sz, FBU.subrects);
-        FBU.bytes = (fb_Bpp + 8) * chunk;
-    } else {
-        FBU.rects -= 1;
-        FBU.bytes = 0;
-    }
-    //Util.Debug("<< display_rre, FBU.bytes: " + FBU.bytes);
-    return true;
-};
-
-encHandlers.HEXTILE = function display_hextile() {
-    //Util.Debug(">> display_hextile");
-    var subencoding, subrects, color, cur_tile,
-        tile_x, x, w, tile_y, y, h, xy, s, sx, sy, wh, sw, sh,
-        rQ = ws.get_rQ(), rQi = ws.get_rQi(); 
-
-    if (FBU.tiles === 0) {
-        FBU.tiles_x = Math.ceil(FBU.width/16);
-        FBU.tiles_y = Math.ceil(FBU.height/16);
-        FBU.total_tiles = FBU.tiles_x * FBU.tiles_y;
-        FBU.tiles = FBU.total_tiles;
-    }
-
-    /* FBU.bytes comes in as 1, ws.rQlen() at least 1 */
-    while (FBU.tiles > 0) {
-        FBU.bytes = 1;
-        if (ws.rQwait("HEXTILE subencoding", FBU.bytes)) { return false; }
-        subencoding = rQ[rQi];  // Peek
-        if (subencoding > 30) { // Raw
-            fail("Disconnected: illegal hextile subencoding " + subencoding);
-            //Util.Debug("ws.rQslice(0,30):" + ws.rQslice(0,30));
-            return false;
+    };
+
+    RFB.genDES = function (password, challenge) {
+        var passwd = [];
+        for (var i = 0; i < password.length; i++) {
+            passwd.push(password.charCodeAt(i));
         }
-        subrects = 0;
-        cur_tile = FBU.total_tiles - FBU.tiles;
-        tile_x = cur_tile % FBU.tiles_x;
-        tile_y = Math.floor(cur_tile / FBU.tiles_x);
-        x = FBU.x + tile_x * 16;
-        y = FBU.y + tile_y * 16;
-        w = Math.min(16, (FBU.x + FBU.width) - x);
-        h = Math.min(16, (FBU.y + FBU.height) - y);
-
-        /* Figure out how much we are expecting */
-        if (subencoding & 0x01) { // Raw
-            //Util.Debug("   Raw subencoding");
-            FBU.bytes += w * h * fb_Bpp;
-        } else {
-            if (subencoding & 0x02) { // Background
-                FBU.bytes += fb_Bpp;
-            }
-            if (subencoding & 0x04) { // Foreground
-                FBU.bytes += fb_Bpp;
-            }
-            if (subencoding & 0x08) { // AnySubrects
-                FBU.bytes += 1;   // Since we aren't shifting it off
-                if (ws.rQwait("hextile subrects header", FBU.bytes)) { return false; }
-                subrects = rQ[rQi + FBU.bytes-1]; // Peek
-                if (subencoding & 0x10) { // SubrectsColoured
-                    FBU.bytes += subrects * (fb_Bpp + 2);
-                } else {
-                    FBU.bytes += subrects * 2;
-                }
+        return (new DES(passwd)).encrypt(challenge);
+    };
+
+    RFB.extract_data_uri = function (arr) {
+        return ";base64," + Base64.encode(arr);
+    };
+
+    RFB.encodingHandlers = {
+        RAW: function () {
+            if (this._FBU.lines === 0) {
+                this._FBU.lines = this._FBU.height;
             }
-        }
 
-        /*
-        Util.Debug("   tile:" + cur_tile + "/" + (FBU.total_tiles - 1) +
-              " (" + tile_x + "," + tile_y + ")" +
-              " [" + x + "," + y + "]@" + w + "x" + h +
-              ", subenc:" + subencoding +
-              "(last: " + FBU.lastsubencoding + "), subrects:" +
-              subrects +
-              ", ws.rQlen():" + ws.rQlen() + ", FBU.bytes:" + FBU.bytes +
-              " last:" + ws.rQslice(FBU.bytes-10, FBU.bytes) +
-              " next:" + ws.rQslice(FBU.bytes-1, FBU.bytes+10));
-        */
-        if (ws.rQwait("hextile", FBU.bytes)) { return false; }
-
-        /* We know the encoding and have a whole tile */
-        FBU.subencoding = rQ[rQi];
-        rQi += 1;
-        if (FBU.subencoding === 0) {
-            if (FBU.lastsubencoding & 0x01) {
-                /* Weird: ignore blanks after RAW */
-                Util.Debug("     Ignoring blank after RAW");
+            this._FBU.bytes = this._FBU.width * this._fb_Bpp;  // at least a line
+            if (this._sock.rQwait("RAW", this._FBU.bytes)) { return false; }
+            var cur_y = this._FBU.y + (this._FBU.height - this._FBU.lines);
+            var curr_height = Math.min(this._FBU.lines,
+                                       Math.floor(this._sock.rQlen() / (this._FBU.width * this._fb_Bpp)));
+            this._display.blitImage(this._FBU.x, cur_y, this._FBU.width,
+                                    curr_height, this._sock.get_rQ(),
+                                    this._sock.get_rQi());
+            this._sock.rQskipBytes(this._FBU.width * curr_height * this._fb_Bpp);
+            this._FBU.lines -= curr_height;
+
+            if (this._FBU.lines > 0) {
+                this._FBU.bytes = this._FBU.width * this._fb_Bpp;  // At least another line
             } else {
-                display.fillRect(x, y, w, h, FBU.background);
+                this._FBU.rects--;
+                this._FBU.bytes = 0;
             }
-        } else if (FBU.subencoding & 0x01) { // Raw
-            display.blitImage(x, y, w, h, rQ, rQi);
-            rQi += FBU.bytes - 1;
-        } else {
-            if (FBU.subencoding & 0x02) { // Background
-                FBU.background = rQ.slice(rQi, rQi + fb_Bpp);
-                rQi += fb_Bpp;
-            }
-            if (FBU.subencoding & 0x04) { // Foreground
-                FBU.foreground = rQ.slice(rQi, rQi + fb_Bpp);
-                rQi += fb_Bpp;
-            }
-
-            display.startTile(x, y, w, h, FBU.background);
-            if (FBU.subencoding & 0x08) { // AnySubrects
-                subrects = rQ[rQi];
-                rQi += 1;
-                for (s = 0; s < subrects; s += 1) {
-                    if (FBU.subencoding & 0x10) { // SubrectsColoured
-                        color = rQ.slice(rQi, rQi + fb_Bpp);
-                        rQi += fb_Bpp;
+
+            return true;
+        },
+
+        COPYRECT: function () {
+            this._FBU.bytes = 4;
+            if (this._sock.rQwait("COPYRECT", 4)) { return false; }
+            this._display.renderQ_push({
+                'type': 'copy',
+                'old_x': this._sock.rQshift16(),
+                'old_y': this._sock.rQshift16(),
+                'x': this._FBU.x,
+                'y': this._FBU.y,
+                'width': this._FBU.width,
+                'height': this._FBU.height
+            });
+            this._FBU.rects--;
+            this._FBU.bytes = 0;
+            return true;
+        },
+
+        RRE: function () {
+            var color;
+            if (this._FBU.subrects === 0) {
+                this._FBU.bytes = 4 + this._fb_Bpp;
+                if (this._sock.rQwait("RRE", 4 + this._fb_Bpp)) { return false; }
+                this._FBU.subrects = this._sock.rQshift32();
+                color = this._sock.rQshiftBytes(this._fb_Bpp);  // Background
+                this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, color);
+            }
+
+            while (this._FBU.subrects > 0 && this._sock.rQlen() >= (this._fb_Bpp + 8)) {
+                color = this._sock.rQshiftBytes(this._fb_Bpp);
+                var x = this._sock.rQshift16();
+                var y = this._sock.rQshift16();
+                var width = this._sock.rQshift16();
+                var height = this._sock.rQshift16();
+                this._display.fillRect(this._FBU.x + x, this._FBU.y + y, width, height, color);
+                this._FBU.subrects--;
+            }
+
+            if (this._FBU.subrects > 0) {
+                var chunk = Math.min(this._rre_chunk_sz, this._FBU.subrects);
+                this._FBU.bytes = (this._fb_Bpp + 8) * chunk;
+            } else {
+                this._FBU.rects--;
+                this._FBU.bytes = 0;
+            }
+
+            return true;
+        },
+
+        HEXTILE: function () {
+            var rQ = this._sock.get_rQ();
+            var rQi = this._sock.get_rQi();
+
+            if (this._FBU.tiles === 0) {
+                this._FBU.tiles_x = Math.ceil(this._FBU.width / 16);
+                this._FBU.tiles_y = Math.ceil(this._FBU.height / 16);
+                this._FBU.total_tiles = this._FBU.tiles_x * this._FBU.tiles_y;
+                this._FBU.tiles = this._FBU.total_tiles;
+            }
+
+            while (this._FBU.tiles > 0) {
+                this._FBU.bytes = 1;
+                if (this._sock.rQwait("HEXTILE subencoding", this._FBU.bytes)) { return false; }
+                var subencoding = rQ[rQi];  // Peek
+                if (subencoding > 30) {  // Raw
+                    this._fail("Disconnected: illegal hextile subencoding " + subencoding);
+                    return false;
+                }
+
+                var subrects = 0;
+                var curr_tile = this._FBU.total_tiles - this._FBU.tiles;
+                var tile_x = curr_tile % this._FBU.tiles_x;
+                var tile_y = Math.floor(curr_tile / this._FBU.tiles_x);
+                var x = this._FBU.x + tile_x * 16;
+                var y = this._FBU.y + tile_y * 16;
+                var w = Math.min(16, (this._FBU.x + this._FBU.width) - x);
+                var h = Math.min(16, (this._FBU.y + this._FBU.height) - y);
+
+                // Figure out how much we are expecting
+                if (subencoding & 0x01) {  // Raw
+                    this._FBU.bytes += w * h * this._fb_Bpp;
+                } else {
+                    if (subencoding & 0x02) {  // Background
+                        this._FBU.bytes += this._fb_Bpp;
+                    }
+                    if (subencoding & 0x04) {  // Foreground
+                        this._FBU.bytes += this._fb_Bpp;
+                    }
+                    if (subencoding & 0x08) {  // AnySubrects
+                        this._FBU.bytes++;  // Since we aren't shifting it off
+                        if (this._sock.rQwait("hextile subrects header", this._FBU.bytes)) { return false; }
+                        subrects = rQ[rQi + this._FBU.bytes - 1];  // Peek
+                        if (subencoding & 0x10) {  // SubrectsColoured
+                            this._FBU.bytes += subrects * (this._fb_Bpp + 2);
+                        } else {
+                            this._FBU.bytes += subrects * 2;
+                        }
+                    }
+                }
+
+                if (this._sock.rQwait("hextile", this._FBU.bytes)) { return false; }
+
+                // We know the encoding and have a whole tile
+                this._FBU.subencoding = rQ[rQi];
+                rQi++;
+                if (this._FBU.subencoding === 0) {
+                    if (this._FBU.lastsubencoding & 0x01) {
+                        // Weird: ignore blanks are RAW
+                        Util.Debug("     Ignoring blank after RAW");
                     } else {
-                        color = FBU.foreground;
+                        this._display.fillRect(x, y, w, h, rQ, rQi);
+                        rQi += this._FBU.bytes - 1;
+                    }
+                } else if (this._FBU.subencoding & 0x01) {  // Raw
+                    this._display.blitImage(x, y, w, h, rQ, rQi);
+                    rQi += this._FBU.bytes - 1;
+                } else {
+                    if (this._FBU.subencoding & 0x02) {  // Background
+                        this._FBU.background = rQ.slice(rQi, rQi + this._fb_Bpp);
+                        rQi += this._fb_Bpp;
+                    }
+                    if (this._FBU.subencoding & 0x04) {  // Foreground
+                        this._FBU.foreground = rQ.slice(rQi, rQi + this._fb_Bpp);
+                        rQi += this._fb_Bpp;
                     }
-                    xy = rQ[rQi];
-                    rQi += 1;
-                    sx = (xy >> 4);
-                    sy = (xy & 0x0f);
 
-                    wh = rQ[rQi];
-                    rQi += 1;
-                    sw = (wh >> 4)   + 1;
-                    sh = (wh & 0x0f) + 1;
+                    this._display.startTile(x, y, w, h, this._FBU.background);
+                    if (this._FBU.subencoding & 0x08) {  // AnySubrects
+                        subrects = rQ[rQi];
+                        rQi++;
+
+                        for (var s = 0; s < subrects; s++) {
+                            var color;
+                            if (this._FBU.subencoding & 0x10) {  // SubrectsColoured
+                                color = rQ.slice(rQi, rQi + this._fb_Bpp);
+                                rQi += this._fb_Bpp;
+                            } else {
+                                color = this._FBU.foreground;
+                            }
+                            var xy = rQ[rQi];
+                            rQi++;
+                            var sx = (xy >> 4);
+                            var sy = (xy & 0x0f);
+
+                            var wh = rQ[rQi];
+                            rQi++;
+                            var sw = (wh >> 4) + 1;
+                            var sh = (wh & 0x0f) + 1;
+
+                            this._display.subTile(sx, sy, sw, sh, color);
+                        }
+                    }
+                    this._display.finishTile();
+                }
+                this._sock.set_rQi(rQi);
+                this._FBU.lastsubencoding = this._FBU.subencoding;
+                this._FBU.bytes = 0;
+                this._FBU.tiles--;
+            }
+
+            if (this._FBU.tiles === 0) {
+                this._FBU.rects--;
+            }
 
-                    display.subTile(sx, sy, sw, sh, color);
+            return true;
+        },
+
+        getTightCLength: function (arr) {
+            var header = 1, data = 0;
+            data += arr[0] & 0x7f;
+            if (arr[0] & 0x80) {
+                header++;
+                data += (arr[1] & 0x7f) << 7;
+                if (arr[1] & 0x80) {
+                    header++;
+                    data += arr[2] << 14;
                 }
             }
-            display.finishTile();
-        }
-        ws.set_rQi(rQi);
-        FBU.lastsubencoding = FBU.subencoding;
-        FBU.bytes = 0;
-        FBU.tiles -= 1;
-    }
-
-    if (FBU.tiles === 0) {
-        FBU.rects -= 1;
-    }
-
-    //Util.Debug("<< display_hextile");
-    return true;
-};
-
-
-// Get 'compact length' header and data size
-getTightCLength = function (arr) {
-    var header = 1, data = 0;
-    data += arr[0] & 0x7f;
-    if (arr[0] & 0x80) {
-        header += 1;
-        data += (arr[1] & 0x7f) << 7;
-        if (arr[1] & 0x80) {
-            header += 1;
-            data += arr[2] << 14;
-        }
-    }
-    return [header, data];
-};
+            return [header, data];
+        },
 
-function display_tight(isTightPNG) {
-    //Util.Debug(">> display_tight");
+        display_tight: function (isTightPNG) {
+            if (this._fb_depth === 1) {
+                this._fail("Tight protocol handler only implements true color mode");
+            }
 
-    if (fb_depth === 1) {
-        fail("Tight protocol handler only implements true color mode");
-    }
+            this._FBU.bytes = 1;  // compression-control byte
+            if (this._sock.rQwait("TIGHT compression-control", this._FBU.bytes)) { return false; }
 
-    var ctl, cmode, clength, color, img, data;
-    var filterId = -1, resetStreams = 0, streamId = -1;
-    var rQ = ws.get_rQ(), rQi = ws.get_rQi(); 
+            var checksum = function (data) {
+                var sum = 0;
+                for (var i = 0; i < data.length; i++) {
+                    sum += data[i];
+                    if (sum > 65536) sum -= 65536;
+                }
+                return sum;
+            };
+
+            var resetStreams = 0;
+            var streamId = -1;
+            var decompress = function (data) {
+                for (var i = 0; i < 4; i++) {
+                    if ((resetStreams >> i) & 1) {
+                        this._FBU.zlibs[i].reset();
+                        Util.Info("Reset zlib stream " + i);
+                    }
+                }
 
-    FBU.bytes = 1; // compression-control byte
-    if (ws.rQwait("TIGHT compression-control", FBU.bytes)) { return false; }
+                var uncompressed = this._FBU.zlibs[streamId].uncompress(data, 0);
+                if (uncompressed.status !== 0) {
+                    Util.Error("Invalid data in zlib stream");
+                }
 
-    var checksum = function(data) {
-        var sum=0, i;
-        for (i=0; i<data.length;i++) {
-            sum += data[i];
-            if (sum > 65536) sum -= 65536;
-        }
-        return sum;
-    }
+                return uncompressed.data;
+            }.bind(this);
+
+            var indexedToRGB = function (data, numColors, palette, width, height) {
+                // Convert indexed (palette based) image data to RGB
+                // TODO: reduce number of calculations inside loop
+                var dest = [];
+                var x, y, dp, sp;
+                if (numColors === 2) {
+                    var w = Math.floor((width + 7) / 8);
+                    var w1 = Math.floor(width / 8);
+
+                    for (y = 0; y < height; y++) {
+                        var b;
+                        for (x = 0; x < w1; x++) {
+                            for (b = 7; b >= 0; b--) {
+                                dp = (y * width + x * 8 + 7 - b) * 3;
+                                sp = (data[y * w + x] >> b & 1) * 3;
+                                dest[dp] = palette[sp];
+                                dest[dp + 1] = palette[sp + 1];
+                                dest[dp + 2] = palette[sp + 2];
+                            }
+                        }
 
-    var decompress = function(data) {
-        for (var i=0; i<4; i++) {
-            if ((resetStreams >> i) & 1) {
-                FBU.zlibs[i].reset();
-                Util.Info("Reset zlib stream " + i);
-            }
-        }
-        var uncompressed = FBU.zlibs[streamId].uncompress(data, 0);
-        if (uncompressed.status !== 0) {
-            Util.Error("Invalid data in zlib stream");
-        }
-        //Util.Warn("Decompressed " + data.length + " to " +
-        //    uncompressed.data.length + " checksums " +
-        //    checksum(data) + ":" + checksum(uncompressed.data));
-
-        return uncompressed.data;
-    }
-
-    var indexedToRGB = function (data, numColors, palette, width, height) {
-        // Convert indexed (palette based) image data to RGB
-        // TODO: reduce number of calculations inside loop
-        var dest = [];
-        var x, y, b, w, w1, dp, sp;
-        if (numColors === 2) {
-            w = Math.floor((width + 7) / 8);
-            w1 = Math.floor(width / 8);
-            for (y = 0; y < height; y++) {
-                for (x = 0; x < w1; x++) {
-                    for (b = 7; b >= 0; b--) {
-                        dp = (y*width + x*8 + 7-b) * 3;
-                        sp = (data[y*w + x] >> b & 1) * 3;
-                        dest[dp  ] = palette[sp  ];
-                        dest[dp+1] = palette[sp+1];
-                        dest[dp+2] = palette[sp+2];
+                        for (b = 7; b >= 8 - width % 8; b--) {
+                            dp = (y * width + x * 8 + 7 - b) * 3;
+                            sp = (data[y * w + x] >> b & 1) * 3;
+                            dest[dp] = palette[sp];
+                            dest[dp + 1] = palette[sp + 1];
+                            dest[dp + 2] = palette[sp + 2];
+                        }
+                    }
+                } else {
+                    for (y = 0; y < height; y++) {
+                        for (x = 0; x < width; x++) {
+                            dp = (y * width + x) * 3;
+                            sp = data[y * width + x] * 3;
+                            dest[dp] = palette[sp];
+                            dest[dp + 1] = palette[sp + 1];
+                            dest[dp + 2] = palette[sp + 2];
+                        }
                     }
                 }
-                for (b = 7; b >= 8 - width % 8; b--) {
-                    dp = (y*width + x*8 + 7-b) * 3;
-                    sp = (data[y*w + x] >> b & 1) * 3;
-                    dest[dp  ] = palette[sp  ];
-                    dest[dp+1] = palette[sp+1];
-                    dest[dp+2] = palette[sp+2];
+
+                return dest;
+            }.bind(this);
+
+            var rQ = this._sock.get_rQ();
+            var rQi = this._sock.get_rQi();
+            var cmode, clength, data;
+
+            var handlePalette = function () {
+                var numColors = rQ[rQi + 2] + 1;
+                var paletteSize = numColors * this._fb_depth;
+                this._FBU.bytes += paletteSize;
+                if (this._sock.rQwait("TIGHT palette " + cmode, this._FBU.bytes)) { return false; }
+
+                var bpp = (numColors <= 2) ? 1 : 8;
+                var rowSize = Math.floor((this._FBU.width * bpp + 7) / 8);
+                var raw = false;
+                if (rowSize * this._FBU.height < 12) {
+                    raw = true;
+                    clength = [0, rowSize * this._FBU.height];
+                } else {
+                    clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(3 + paletteSize,
+                                                                                      3 + paletteSize + 3));
                 }
-            }
-        } else {
-            for (y = 0; y < height; y++) {
-                for (x = 0; x < width; x++) {
-                    dp = (y*width + x) * 3;
-                    sp = data[y*width + x] * 3;
-                    dest[dp  ] = palette[sp  ];
-                    dest[dp+1] = palette[sp+1];
-                    dest[dp+2] = palette[sp+2];
+
+                this._FBU.bytes += clength[0] + clength[1];
+                if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; }
+
+                // Shift ctl, filter id, num colors, palette entries, and clength off
+                this._sock.rQskipBytes(3);
+                var palette = this._sock.rQshiftBytes(paletteSize);
+                this._sock.rQskipBytes(clength[0]);
+
+                if (raw) {
+                    data = this._sock.rQshiftBytes(clength[1]);
+                } else {
+                    data = decompress(this._sock.rQshiftBytes(clength[1]));
                 }
+
+                // Convert indexed (palette based) image data to RGB
+                var rgb = indexedToRGB(data, numColors, palette, this._FBU.width, this._FBU.height);
+
+                this._display.renderQ_push({
+                    'type': 'blitRgb',
+                    'data': rgb,
+                    'x': this._FBU.x,
+                    'y': this._FBU.y,
+                    'width': this._FBU.width,
+                    'height': this._FBU.height
+                });
+
+                return true;
+            }.bind(this);
+
+            var handleCopy = function () {
+                var raw = false;
+                var uncompressedSize = this._FBU.width * this._FBU.height * this._fb_depth;
+                if (uncompressedSize < 12) {
+                    raw = true;
+                    clength = [0, uncompressedSize];
+                } else {
+                    clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(1, 4));
+                }
+                this._FBU.bytes = 1 + clength[0] + clength[1];
+                if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; }
+
+                // Shift ctl, clength off
+                this._sock.rQshiftBytes(1 + clength[0]);
+
+                if (raw) {
+                    data = this._sock.rQshiftBytes(clength[1]);
+                } else {
+                    data = decompress(this._sock.rQshiftBytes(clength[1]));
+                }
+
+                this._display.renderQ_push({
+                    'type': 'blitRgb',
+                    'data': data,
+                    'x': this._FBU.x,
+                    'y': this._FBU.y,
+                    'width': this._FBU.width,
+                    'height': this._FBU.height
+                });
+
+                return true;
+            }.bind(this);
+
+            var ctl = this._sock.rQpeek8();
+
+            // Keep tight reset bits
+            resetStreams = ctl & 0xF;
+
+            // Figure out filter
+            ctl = ctl >> 4;
+            streamId = ctl & 0x3;
+
+            if (ctl === 0x08)       cmode = "fill";
+            else if (ctl === 0x09)  cmode = "jpeg";
+            else if (ctl === 0x0A)  cmode = "png";
+            else if (ctl & 0x04)    cmode = "filter";
+            else if (ctl < 0x04)    cmode = "copy";
+            else return this._fail("Illegal tight compression received, ctl: " + ctl);
+
+            if (isTightPNG && (cmode === "filter" || cmode === "copy")) {
+                return this._fail("filter/copy received in tightPNG mode");
             }
-        }
-        return dest;
-    };
-    var handlePalette = function() {
-        var numColors = rQ[rQi + 2] + 1;
-        var paletteSize = numColors * fb_depth; 
-        FBU.bytes += paletteSize;
-        if (ws.rQwait("TIGHT palette " + cmode, FBU.bytes)) { return false; }
-
-        var bpp = (numColors <= 2) ? 1 : 8;
-        var rowSize = Math.floor((FBU.width * bpp + 7) / 8);
-        var raw = false;
-        if (rowSize * FBU.height < 12) {
-            raw = true;
-            clength = [0, rowSize * FBU.height];
-        } else {
-            clength = getTightCLength(ws.rQslice(3 + paletteSize,
-                                                 3 + paletteSize + 3));
-        }
-        FBU.bytes += clength[0] + clength[1];
-        if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; }
 
-        // Shift ctl, filter id, num colors, palette entries, and clength off
-        ws.rQshiftBytes(3); 
-        var palette = ws.rQshiftBytes(paletteSize);
-        ws.rQshiftBytes(clength[0]);
+            switch (cmode) {
+                // fill use fb_depth because TPIXELs drop the padding byte
+                case "fill":  // TPIXEL
+                    this._FBU.bytes += this._fb_depth;
+                    break;
+                case "jpeg":  // max clength
+                    this._FBU.bytes += 3;
+                    break;
+                case "png":  // max clength
+                    this._FBU.bytes += 3;
+                    break;
+                case "filter":  // filter id + num colors if palette
+                    this._FBU.bytes += 2;
+                    break;
+                case "copy":
+                    break;
+            }
 
-        if (raw) {
-            data = ws.rQshiftBytes(clength[1]);
-        } else {
-            data = decompress(ws.rQshiftBytes(clength[1]));
-        }
+            if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; }
+
+            // Determine FBU.bytes
+            switch (cmode) {
+                case "fill":
+                    this._sock.rQskip8();  // shift off ctl
+                    var color = this._sock.rQshiftBytes(this._fb_depth);
+                    this._display.renderQ_push({
+                        'type': 'fill',
+                        'x': this._FBU.x,
+                        'y': this._FBU.y,
+                        'width': this._FBU.width,
+                        'height': this._FBU.height,
+                        'color': [color[2], color[1], color[0]]
+                    });
+                    break;
+                case "png":
+                case "jpeg":
+                    clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(1, 4));
+                    this._FBU.bytes = 1 + clength[0] + clength[1];  // ctl + clength size + jpeg-data
+                    if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; }
+
+                    // We have everything, render it
+                    this._sock.rQskipBytes(1 + clength[0]);  // shift off clt + compact length
+                    var img = new Image();
+                    img.src = "data: image/" + cmode +
+                        RFB.extract_data_uri(this._sock.rQshiftBytes(clength[1]));
+                    this._display.renderQ_push({
+                        'type': 'img',
+                        'img': img,
+                        'x': this._FBU.x,
+                        'y': this._FBU.y
+                    });
+                    img = null;
+                    break;
+                case "filter":
+                    var filterId = rQ[rQi + 1];
+                    if (filterId === 1) {
+                        if (!handlePalette()) { return false; }
+                    } else {
+                        // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter
+                        // Filter 2, Gradient is valid but not use if jpeg is enabled
+                        // TODO(directxman12): why aren't we just calling '_fail' here
+                        throw new Error("Unsupported tight subencoding received, filter: " + filterId);
+                    }
+                    break;
+                case "copy":
+                    if (!handleCopy()) { return false; }
+                    break;
+            }
 
-        // Convert indexed (palette based) image data to RGB
-        var rgb = indexedToRGB(data, numColors, palette, FBU.width, FBU.height);
-
-        // Add it to the render queue
-        display.renderQ_push({
-                'type': 'blitRgb',
-                'data': rgb,
-                'x': FBU.x,
-                'y': FBU.y,
-                'width': FBU.width,
-                'height': FBU.height});
-        return true;
-    }
-
-    var handleCopy = function() {
-        var raw = false;
-        var uncompressedSize = FBU.width * FBU.height * fb_depth;
-        if (uncompressedSize < 12) {
-            raw = true;
-            clength = [0, uncompressedSize];
-        } else {
-            clength = getTightCLength(ws.rQslice(1, 4));
-        }
-        FBU.bytes = 1 + clength[0] + clength[1];
-        if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; }
 
-        // Shift ctl, clength off
-        ws.rQshiftBytes(1 + clength[0]);
+            this._FBU.bytes = 0;
+            this._FBU.rects--;
 
-        if (raw) {
-            data = ws.rQshiftBytes(clength[1]);
-        } else {
-            data = decompress(ws.rQshiftBytes(clength[1]));
-        }
+            return true;
+        },
 
-        display.renderQ_push({
-                'type': 'blitRgb',
-                'data': data,
-                'x': FBU.x,
-                'y': FBU.y,
-                'width': FBU.width,
-                'height': FBU.height});
-        return true;
-    }
-
-    ctl = ws.rQpeek8();
-
-    // Keep tight reset bits
-    resetStreams = ctl & 0xF;
-
-    // Figure out filter
-    ctl = ctl >> 4; 
-    streamId = ctl & 0x3;
-
-    if (ctl === 0x08)      cmode = "fill";
-    else if (ctl === 0x09) cmode = "jpeg";
-    else if (ctl === 0x0A) cmode = "png";
-    else if (ctl & 0x04)   cmode = "filter";
-    else if (ctl < 0x04)   cmode = "copy";
-    else return fail("Illegal tight compression received, ctl: " + ctl);
-
-    if (isTightPNG && (cmode === "filter" || cmode === "copy")) {
-        return fail("filter/copy received in tightPNG mode");
-    }
-
-    switch (cmode) {
-        // fill uses fb_depth because TPIXELs drop the padding byte
-        case "fill":   FBU.bytes += fb_depth; break; // TPIXEL
-        case "jpeg":   FBU.bytes += 3;        break; // max clength
-        case "png":    FBU.bytes += 3;        break; // max clength
-        case "filter": FBU.bytes += 2;        break; // filter id + num colors if palette
-        case "copy":                          break;
-    }
-
-    if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; }
-
-    //Util.Debug("   ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")");
-    //Util.Debug("   cmode: " + cmode);
-
-    // Determine FBU.bytes
-    switch (cmode) {
-    case "fill":
-        ws.rQshift8(); // shift off ctl
-        color = ws.rQshiftBytes(fb_depth);
-        display.renderQ_push({
-                'type': 'fill',
-                'x': FBU.x,
-                'y': FBU.y,
-                'width': FBU.width,
-                'height': FBU.height,
-                'color': [color[2], color[1], color[0]] });
-        break;
-    case "png":
-    case "jpeg":
-        clength = getTightCLength(ws.rQslice(1, 4));
-        FBU.bytes = 1 + clength[0] + clength[1]; // ctl + clength size + jpeg-data
-        if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; }
-
-        // We have everything, render it
-        //Util.Debug("   jpeg, ws.rQlen(): " + ws.rQlen() + ", clength[0]: " +
-        //           clength[0] + ", clength[1]: " + clength[1]);
-        ws.rQshiftBytes(1 + clength[0]); // shift off ctl + compact length
-        img = new Image();
-        img.src = "data:image/" + cmode +
-            extract_data_uri(ws.rQshiftBytes(clength[1]));
-        display.renderQ_push({
-                'type': 'img',
-                'img': img,
-                'x': FBU.x,
-                'y': FBU.y});
-        img = null;
-        break;
-    case "filter":
-        filterId = rQ[rQi + 1];
-        if (filterId === 1) {
-            if (!handlePalette()) { return false; }
-        } else {
-            // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter
-            // Filter 2, Gradient is valid but not used if jpeg is enabled
-            throw("Unsupported tight subencoding received, filter: " + filterId);
-        }
-        break;
-    case "copy":
-        if (!handleCopy()) { return false; }
-        break;
-    }
-
-    FBU.bytes = 0;
-    FBU.rects -= 1;
-    //Util.Debug("   ending ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")");
-    //Util.Debug("<< display_tight_png");
-    return true;
-}
-
-extract_data_uri = function(arr) {
-    //var i, stra = [];
-    //for (i=0; i< arr.length; i += 1) {
-    //    stra.push(String.fromCharCode(arr[i]));
-    //}
-    //return "," + escape(stra.join(''));
-    return ";base64," + Base64.encode(arr);
-};
-
-encHandlers.TIGHT = function () { return display_tight(false); };
-encHandlers.TIGHT_PNG = function () { return display_tight(true); };
-
-encHandlers.last_rect = function last_rect() {
-    //Util.Debug(">> last_rect");
-    FBU.rects = 0;
-    //Util.Debug("<< last_rect");
-    return true;
-};
-
-encHandlers.DesktopSize = function set_desktopsize() {
-    Util.Debug(">> set_desktopsize");
-    fb_width = FBU.width;
-    fb_height = FBU.height;
-    conf.onFBResize(that, fb_width, fb_height);
-    display.resize(fb_width, fb_height);
-    timing.fbu_rt_start = (new Date()).getTime();
-
-    FBU.bytes = 0;
-    FBU.rects -= 1;
-
-    Util.Debug("<< set_desktopsize");
-    return true;
-};
-
-encHandlers.Cursor = function set_cursor() {
-    var x, y, w, h, pixelslength, masklength;
-    Util.Debug(">> set_cursor");
-    x = FBU.x;  // hotspot-x
-    y = FBU.y;  // hotspot-y
-    w = FBU.width;
-    h = FBU.height;
-
-    pixelslength = w * h * fb_Bpp;
-    masklength = Math.floor((w + 7) / 8) * h;
-
-    FBU.bytes = pixelslength + masklength;
-    if (ws.rQwait("cursor encoding", FBU.bytes)) { return false; }
-
-    //Util.Debug("   set_cursor, x: " + x + ", y: " + y + ", w: " + w + ", h: " + h);
-
-    display.changeCursor(ws.rQshiftBytes(pixelslength),
-                            ws.rQshiftBytes(masklength),
-                            x, y, w, h);
-
-    FBU.bytes = 0;
-    FBU.rects -= 1;
-
-    Util.Debug("<< set_cursor");
-    return true;
-};
-
-encHandlers.JPEG_quality_lo = function set_jpeg_quality() {
-    Util.Error("Server sent jpeg_quality pseudo-encoding");
-};
-
-encHandlers.compress_lo = function set_compress_level() {
-    Util.Error("Server sent compress level pseudo-encoding");
-};
+        TIGHT: function () { return this._encHandlers.display_tight(false); },
+        TIGHT_PNG: function () { return this._encHandlers.display_tight(true); },
 
-/*
- * Client message routines
- */
+        last_rect: function () {
+            this._FBU.rects = 0;
+            return true;
+        },
 
-pixelFormat = function() {
-    //Util.Debug(">> pixelFormat");
-    var arr;
-    arr = [0];     // msg-type
-    arr.push8(0);  // padding
-    arr.push8(0);  // padding
-    arr.push8(0);  // padding
-
-    arr.push8(fb_Bpp * 8); // bits-per-pixel
-    arr.push8(fb_depth * 8); // depth
-    arr.push8(0);  // little-endian
-    arr.push8(conf.true_color ? 1 : 0);  // true-color
-
-    arr.push16(255);  // red-max
-    arr.push16(255);  // green-max
-    arr.push16(255);  // blue-max
-    arr.push8(16);    // red-shift
-    arr.push8(8);     // green-shift
-    arr.push8(0);     // blue-shift
-
-    arr.push8(0);     // padding
-    arr.push8(0);     // padding
-    arr.push8(0);     // padding
-    //Util.Debug("<< pixelFormat");
-    return arr;
-};
-
-clientEncodings = function() {
-    //Util.Debug(">> clientEncodings");
-    var arr, i, encList = [];
-
-    for (i=0; i<encodings.length; i += 1) {
-        if ((encodings[i][0] === "Cursor") &&
-            (! conf.local_cursor)) {
-            Util.Debug("Skipping Cursor pseudo-encoding");
-
-        // TODO: remove this when we have tight+non-true-color
-        } else if ((encodings[i][0] === "TIGHT") && 
-                   (! conf.true_color)) {
-            Util.Warn("Skipping tight, only support with true color");
-        } else {
-            //Util.Debug("Adding encoding: " + encodings[i][0]);
-            encList.push(encodings[i][1]);
-        }
-    }
-
-    arr = [2];     // msg-type
-    arr.push8(0);  // padding
-
-    arr.push16(encList.length); // encoding count
-    for (i=0; i < encList.length; i += 1) {
-        arr.push32(encList[i]);
-    }
-    //Util.Debug("<< clientEncodings: " + arr);
-    return arr;
-};
-
-fbUpdateRequest = function(incremental, x, y, xw, yw) {
-    //Util.Debug(">> fbUpdateRequest");
-    if (typeof(x) === "undefined") { x = 0; }
-    if (typeof(y) === "undefined") { y = 0; }
-    if (typeof(xw) === "undefined") { xw = fb_width; }
-    if (typeof(yw) === "undefined") { yw = fb_height; }
-    var arr;
-    arr = [3];  // msg-type
-    arr.push8(incremental);
-    arr.push16(x);
-    arr.push16(y);
-    arr.push16(xw);
-    arr.push16(yw);
-    //Util.Debug("<< fbUpdateRequest");
-    return arr;
-};
-
-// Based on clean/dirty areas, generate requests to send
-fbUpdateRequests = function() {
-    var cleanDirty = display.getCleanDirtyReset(),
-        arr = [], i, cb, db;
-
-    cb = cleanDirty.cleanBox;
-    if (cb.w > 0 && cb.h > 0) {
-        // Request incremental for clean box
-        arr = arr.concat(fbUpdateRequest(1, cb.x, cb.y, cb.w, cb.h));
-    }
-    for (i = 0; i < cleanDirty.dirtyBoxes.length; i++) {
-        db = cleanDirty.dirtyBoxes[i];
-        // Force all (non-incremental for dirty box
-        arr = arr.concat(fbUpdateRequest(0, db.x, db.y, db.w, db.h));
-    }
-    return arr;
-};
-
-
-
-keyEvent = function(keysym, down) {
-    //Util.Debug(">> keyEvent, keysym: " + keysym + ", down: " + down);
-    var arr;
-    arr = [4];  // msg-type
-    arr.push8(down);
-    arr.push16(0);
-    arr.push32(keysym);
-    //Util.Debug("<< keyEvent");
-    return arr;
-};
-
-pointerEvent = function(x, y) {
-    //Util.Debug(">> pointerEvent, x,y: " + x + "," + y +
-    //           " , mask: " + mouse_buttonMask);
-    var arr;
-    arr = [5];  // msg-type
-    arr.push8(mouse_buttonMask);
-    arr.push16(x);
-    arr.push16(y);
-    //Util.Debug("<< pointerEvent");
-    return arr;
-};
-
-clientCutText = function(text) {
-    //Util.Debug(">> clientCutText");
-    var arr, i, n;
-    arr = [6];     // msg-type
-    arr.push8(0);  // padding
-    arr.push8(0);  // padding
-    arr.push8(0);  // padding
-    arr.push32(text.length);
-    n = text.length;
-    for (i=0; i < n; i+=1) {
-        arr.push(text.charCodeAt(i));
-    }
-    //Util.Debug("<< clientCutText:" + arr);
-    return arr;
-};
-
-
-
-//
-// Public API interface functions
-//
-
-that.connect = function(host, port, password, path) {
-    //Util.Debug(">> connect");
-
-    rfb_host       = host;
-    rfb_port       = port;
-    rfb_password   = (password !== undefined)   ? password : "";
-    rfb_path       = (path !== undefined) ? path : "";
-
-    if ((!rfb_host) || (!rfb_port)) {
-        return fail("Must set host and port");
-    }
-
-    updateState('connect');
-    //Util.Debug("<< connect");
-
-};
-
-that.disconnect = function() {
-    //Util.Debug(">> disconnect");
-    updateState('disconnect', 'Disconnecting');
-    //Util.Debug("<< disconnect");
-};
-
-that.sendPassword = function(passwd) {
-    rfb_password = passwd;
-    rfb_state = "Authentication";
-    setTimeout(init_msg, 1);
-};
-
-that.sendCtrlAltDel = function() {
-    if (rfb_state !== "normal" || conf.view_only) { return false; }
-    Util.Info("Sending Ctrl-Alt-Del");
-    var arr = [];
-    arr = arr.concat(keyEvent(0xFFE3, 1)); // Control
-    arr = arr.concat(keyEvent(0xFFE9, 1)); // Alt
-    arr = arr.concat(keyEvent(0xFFFF, 1)); // Delete
-    arr = arr.concat(keyEvent(0xFFFF, 0)); // Delete
-    arr = arr.concat(keyEvent(0xFFE9, 0)); // Alt
-    arr = arr.concat(keyEvent(0xFFE3, 0)); // Control
-    ws.send(arr);
-};
-
-that.xvpOp = function(ver, op) {
-    if (rfb_xvp_ver < ver) { return false; }
-    Util.Info("Sending XVP operation " + op + " (version " + ver + ")")
-    ws.send_string("\xFA\x00" + String.fromCharCode(ver) + String.fromCharCode(op));
-    return true;
-};
-
-that.xvpShutdown = function() {
-    return that.xvpOp(1, 2);
-};
-
-that.xvpReboot = function() {
-    return that.xvpOp(1, 3);
-};
-
-that.xvpReset = function() {
-    return that.xvpOp(1, 4);
-};
-
-// Send a key press. If 'down' is not specified then send a down key
-// followed by an up key.
-that.sendKey = function(code, down) {
-    if (rfb_state !== "normal" || conf.view_only) { return false; }
-    var arr = [];
-    if (typeof down !== 'undefined') {
-        Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code);
-        arr = arr.concat(keyEvent(code, down ? 1 : 0));
-    } else {
-        Util.Info("Sending key code (down + up): " + code);
-        arr = arr.concat(keyEvent(code, 1));
-        arr = arr.concat(keyEvent(code, 0));
-    }
-    ws.send(arr);
-};
-
-that.clipboardPasteFrom = function(text) {
-    if (rfb_state !== "normal") { return; }
-    //Util.Debug(">> clipboardPasteFrom: " + text.substr(0,40) + "...");
-    ws.send(clientCutText(text));
-    //Util.Debug("<< clipboardPasteFrom");
-};
-
-// Override internal functions for testing
-that.testMode = function(override_send, data_mode) {
-    test_mode = true;
-    that.recv_message = ws.testMode(override_send, data_mode);
-
-    checkEvents = function () { /* Stub Out */ };
-    that.connect = function(host, port, password) {
-            rfb_host = host;
-            rfb_port = port;
-            rfb_password = password;
-            init_vars();
-            updateState('ProtocolVersion', "Starting VNC handshake");
-        };
-};
+        DesktopSize: function () {
+            Util.Debug(">> set_desktopsize");
+            this._fb_width = this._FBU.width;
+            this._fb_height = this._FBU.height;
+            this._onFBResize(this, this._fb_width, this._fb_height);
+            this._display.resize(this._fb_width, this._fb_height);
+            this._timing.fbu_rt_start = (new Date()).getTime();
+
+            this._FBU.bytes = 0;
+            this._FBU.rects--;
+
+            Util.Debug("<< set_desktopsize");
+            return true;
+        },
+
+        Cursor: function () {
+            Util.Debug(">> set_cursor");
+            var x = this._FBU.x;  // hotspot-x
+            var y = this._FBU.y;  // hotspot-y
+            var w = this._FBU.width;
+            var h = this._FBU.height;
+
+            var pixelslength = w * h * this._fb_Bpp;
+            var masklength = Math.floor((w + 7) / 8) * h;
 
+            this._FBU.bytes = pixelslength + masklength;
+            if (this._sock.rQwait("cursor encoding", this._FBU.bytes)) { return false; }
 
-return constructor();  // Return the public API interface
+            this._display.changeCursor(this._sock.rQshiftBytes(pixelslength),
+                                       this._sock.rQshiftBytes(masklength),
+                                       x, y, w, h);
 
-}  // End of RFB()
+            this._FBU.bytes = 0;
+            this._FBU.rects--;
+
+            Util.Debug("<< set_cursor");
+            return true;
+        },
+
+        JPEG_quality_lo: function () {
+            Util.Error("Server sent jpeg_quality pseudo-encoding");
+        },
+
+        compress_lo: function () {
+            Util.Error("Server sent compress level pseudo-encoding");
+        }
+    };
+})();

+ 952 - 976
include/ui.js

@@ -7,997 +7,973 @@
  * See README.md for usage and integration instructions.
  */
 
-"use strict";
-/*jslint white: false, browser: true */
-/*global window, $D, Util, WebUtil, RFB, Display */
-
-// Load supporting scripts
-window.onscriptsload = function () { UI.load(); };
-window.onload = function () { UI.keyboardinputReset(); };
-Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
-                   "keysymdef.js", "keyboard.js", "input.js", "display.js",
-                   "jsunzip.js", "rfb.js", "keysym.js"]);
-
-var UI = {
-
-rfb_state : 'loaded',
-settingsOpen : false,
-connSettingsOpen : false,
-popupStatusOpen : false,
-clipboardOpen: false,
-keyboardVisible: false,
-hideKeyboardTimeout: null,
-lastKeyboardinput: null,
-defaultKeyboardinputLen: 100,
-extraKeysVisible: false,
-ctrlOn: false,
-altOn: false,
-isTouchDevice: false,
-
-// Setup rfb object, load settings from browser storage, then call
-// UI.init to setup the UI/menus
-load: function (callback) {
-    WebUtil.initSettings(UI.start, callback);
-},
-
-// Render default UI and initialize settings menu
-start: function(callback) {
-    var html = '', i, sheet, sheets, llevels, port, autoconnect;
-
-    UI.isTouchDevice = 'ontouchstart' in document.documentElement;
-
-    // Stylesheet selection dropdown
-    sheet = WebUtil.selectStylesheet();
-    sheets = WebUtil.getStylesheets();
-    for (i = 0; i < sheets.length; i += 1) {
-        UI.addOption($D('noVNC_stylesheet'),sheets[i].title, sheets[i].title);
-    }
-
-    // Logging selection dropdown
-    llevels = ['error', 'warn', 'info', 'debug'];
-    for (i = 0; i < llevels.length; i += 1) {
-        UI.addOption($D('noVNC_logging'),llevels[i], llevels[i]);
-    }
-
-    // Settings with immediate effects
-    UI.initSetting('logging', 'warn');
-    WebUtil.init_logging(UI.getSetting('logging'));
-
-    UI.initSetting('stylesheet', 'default');
-    WebUtil.selectStylesheet(null);
-    // call twice to get around webkit bug
-    WebUtil.selectStylesheet(UI.getSetting('stylesheet'));
-
-    // if port == 80 (or 443) then it won't be present and should be
-    // set manually
-    port = window.location.port;
-    if (!port) {
-        if (window.location.protocol.substring(0,5) == 'https') {            
-            port = 443;
-        }
-        else if (window.location.protocol.substring(0,4) == 'http') {            
-            port = 80;
-        }
-    }
-
-    /* Populate the controls if defaults are provided in the URL */
-    UI.initSetting('host', window.location.hostname);
-    UI.initSetting('port', port);
-    UI.initSetting('password', '');
-    UI.initSetting('encrypt', (window.location.protocol === "https:"));
-    UI.initSetting('true_color', true);
-    UI.initSetting('cursor', !UI.isTouchDevice);
-    UI.initSetting('shared', true);
-    UI.initSetting('view_only', false);
-    UI.initSetting('path', 'websockify');
-    UI.initSetting('repeaterID', '');
-
-    UI.rfb = RFB({'target': $D('noVNC_canvas'),
-                  'onUpdateState': UI.updateState,
-                  'onXvpInit': UI.updateXvpVisualState,
-                  'onClipboard': UI.clipReceive,
-                  'onDesktopName': UI.updateDocumentTitle});
-
-    autoconnect = WebUtil.getQueryVar('autoconnect', false);
-    if (autoconnect === 'true' || autoconnect == '1') {
-        autoconnect = true;
-        UI.connect();
-    } else {
-        autoconnect = false;
-    }
-
-    UI.updateVisualState();
-
-    // Unfocus clipboard when over the VNC area
-    //$D('VNC_screen').onmousemove = function () {
-    //         var keyboard = UI.rfb.get_keyboard();
-    //        if ((! keyboard) || (! keyboard.get_focused())) {
-    //            $D('VNC_clipboard_text').blur();
-    //         }
-    //    };
-
-    // Show mouse selector buttons on touch screen devices
-    if (UI.isTouchDevice) {
-        // Show mobile buttons
-        $D('noVNC_mobile_buttons').style.display = "inline";
-        UI.setMouseButton();
-        // Remove the address bar
-        setTimeout(function() { window.scrollTo(0, 1); }, 100);
-        UI.forceSetting('clip', true);
-        $D('noVNC_clip').disabled = true;
-    } else {
-        UI.initSetting('clip', false);
-    }
-
-    //iOS Safari does not support CSS position:fixed.
-    //This detects iOS devices and enables javascript workaround.
-    if ((navigator.userAgent.match(/iPhone/i)) ||
-        (navigator.userAgent.match(/iPod/i)) ||
-        (navigator.userAgent.match(/iPad/i))) {
-        //UI.setOnscroll();
-        //UI.setResize();
-    }
-    UI.setBarPosition();
-
-    $D('noVNC_host').focus();
-
-    UI.setViewClip();
-    Util.addEvent(window, 'resize', UI.setViewClip);
-
-    Util.addEvent(window, 'beforeunload', function () {
-        if (UI.rfb_state === 'normal') {
-            return "You are currently connected.";
-        }
-    } );
-
-    // Show description by default when hosted at for kanaka.github.com
-    if (location.host === "kanaka.github.io") {
-        // Open the description dialog
-        $D('noVNC_description').style.display = "block";
-    } else {
-        // Show the connect panel on first load unless autoconnecting
-        if (autoconnect === UI.connSettingsOpen) {
+/* jslint white: false, browser: true */
+/* global window, $D, Util, WebUtil, RFB, Display */
+
+var UI;
+
+(function () {
+    "use strict";
+
+    // Load supporting scripts
+    window.onscriptsload = function () { UI.load(); };
+    window.onload = function () { UI.keyboardinputReset(); };
+    Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
+                       "keysymdef.js", "keyboard.js", "input.js", "display.js",
+                       "jsunzip.js", "rfb.js", "keysym.js"]);
+
+    var UI = {
+
+        rfb_state : 'loaded',
+        settingsOpen : false,
+        connSettingsOpen : false,
+        popupStatusOpen : false,
+        clipboardOpen: false,
+        keyboardVisible: false,
+        hideKeyboardTimeout: null,
+        lastKeyboardinput: null,
+        defaultKeyboardinputLen: 100,
+        extraKeysVisible: false,
+        ctrlOn: false,
+        altOn: false,
+        isTouchDevice: false,
+
+        // Setup rfb object, load settings from browser storage, then call
+        // UI.init to setup the UI/menus
+        load: function (callback) {
+            WebUtil.initSettings(UI.start, callback);
+        },
+
+        // Render default UI and initialize settings menu
+        start: function(callback) {
+            UI.isTouchDevice = 'ontouchstart' in document.documentElement;
+
+            // Stylesheet selection dropdown
+            var sheet = WebUtil.selectStylesheet();
+            var sheets = WebUtil.getStylesheets();
+            var i;
+            for (i = 0; i < sheets.length; i += 1) {
+                UI.addOption($D('noVNC_stylesheet'),sheets[i].title, sheets[i].title);
+            }
+
+            // Logging selection dropdown
+            var llevels = ['error', 'warn', 'info', 'debug'];
+            for (i = 0; i < llevels.length; i += 1) {
+                UI.addOption($D('noVNC_logging'),llevels[i], llevels[i]);
+            }
+
+            // Settings with immediate effects
+            UI.initSetting('logging', 'warn');
+            WebUtil.init_logging(UI.getSetting('logging'));
+
+            UI.initSetting('stylesheet', 'default');
+            WebUtil.selectStylesheet(null);
+            // call twice to get around webkit bug
+            WebUtil.selectStylesheet(UI.getSetting('stylesheet'));
+
+            // if port == 80 (or 443) then it won't be present and should be
+            // set manually
+            var port = window.location.port;
+            if (!port) {
+                if (window.location.protocol.substring(0,5) == 'https') {
+                    port = 443;
+                }
+                else if (window.location.protocol.substring(0,4) == 'http') {
+                    port = 80;
+                }
+            }
+
+            /* Populate the controls if defaults are provided in the URL */
+            UI.initSetting('host', window.location.hostname);
+            UI.initSetting('port', port);
+            UI.initSetting('password', '');
+            UI.initSetting('encrypt', (window.location.protocol === "https:"));
+            UI.initSetting('true_color', true);
+            UI.initSetting('cursor', !UI.isTouchDevice);
+            UI.initSetting('shared', true);
+            UI.initSetting('view_only', false);
+            UI.initSetting('path', 'websockify');
+            UI.initSetting('repeaterID', '');
+
+            UI.rfb = new RFB({'target': $D('noVNC_canvas'),
+                              'onUpdateState': UI.updateState,
+                              'onXvpInit': UI.updateXvpVisualState,
+                              'onClipboard': UI.clipReceive,
+                              'onDesktopName': UI.updateDocumentTitle});
+
+            var autoconnect = WebUtil.getQueryVar('autoconnect', false);
+            if (autoconnect === 'true' || autoconnect == '1') {
+                autoconnect = true;
+                UI.connect();
+            } else {
+                autoconnect = false;
+            }
+
+            UI.updateVisualState();
+
+            // Show mouse selector buttons on touch screen devices
+            if (UI.isTouchDevice) {
+                // Show mobile buttons
+                $D('noVNC_mobile_buttons').style.display = "inline";
+                UI.setMouseButton();
+                // Remove the address bar
+                setTimeout(function() { window.scrollTo(0, 1); }, 100);
+                UI.forceSetting('clip', true);
+                $D('noVNC_clip').disabled = true;
+            } else {
+                UI.initSetting('clip', false);
+            }
+
+            //iOS Safari does not support CSS position:fixed.
+            //This detects iOS devices and enables javascript workaround.
+            if ((navigator.userAgent.match(/iPhone/i)) ||
+                (navigator.userAgent.match(/iPod/i)) ||
+                (navigator.userAgent.match(/iPad/i))) {
+                //UI.setOnscroll();
+                //UI.setResize();
+            }
+            UI.setBarPosition();
+
+            $D('noVNC_host').focus();
+
+            UI.setViewClip();
+            Util.addEvent(window, 'resize', UI.setViewClip);
+
+            Util.addEvent(window, 'beforeunload', function () {
+                if (UI.rfb_state === 'normal') {
+                    return "You are currently connected.";
+                }
+            } );
+
+            // Show description by default when hosted at for kanaka.github.com
+            if (location.host === "kanaka.github.io") {
+                // Open the description dialog
+                $D('noVNC_description').style.display = "block";
+            } else {
+                // Show the connect panel on first load unless autoconnecting
+                if (autoconnect === UI.connSettingsOpen) {
+                    UI.toggleConnectPanel();
+                }
+            }
+
+            // Add mouse event click/focus/blur event handlers to the UI
+            UI.addMouseHandlers();
+
+            if (typeof callback === "function") {
+                callback(UI.rfb);
+            }
+        },
+
+        addMouseHandlers: function() {
+            // Setup interface handlers that can't be inline
+            $D("noVNC_view_drag_button").onclick = UI.setViewDrag;
+            $D("noVNC_mouse_button0").onclick = function () { UI.setMouseButton(1); };
+            $D("noVNC_mouse_button1").onclick = function () { UI.setMouseButton(2); };
+            $D("noVNC_mouse_button2").onclick = function () { UI.setMouseButton(4); };
+            $D("noVNC_mouse_button4").onclick = function () { UI.setMouseButton(0); };
+            $D("showKeyboard").onclick = UI.showKeyboard;
+
+            $D("keyboardinput").oninput = UI.keyInput;
+            $D("keyboardinput").onblur = UI.keyInputBlur;
+
+            $D("showExtraKeysButton").onclick = UI.showExtraKeys;
+            $D("toggleCtrlButton").onclick = UI.toggleCtrl;
+            $D("toggleAltButton").onclick = UI.toggleAlt;
+            $D("sendTabButton").onclick = UI.sendTab;
+            $D("sendEscButton").onclick = UI.sendEsc;
+
+            $D("sendCtrlAltDelButton").onclick = UI.sendCtrlAltDel;
+            $D("xvpShutdownButton").onclick = UI.xvpShutdown;
+            $D("xvpRebootButton").onclick = UI.xvpReboot;
+            $D("xvpResetButton").onclick = UI.xvpReset;
+            $D("noVNC_status").onclick = UI.togglePopupStatusPanel;
+            $D("noVNC_popup_status_panel").onclick = UI.togglePopupStatusPanel;
+            $D("xvpButton").onclick = UI.toggleXvpPanel;
+            $D("clipboardButton").onclick = UI.toggleClipboardPanel;
+            $D("settingsButton").onclick = UI.toggleSettingsPanel;
+            $D("connectButton").onclick = UI.toggleConnectPanel;
+            $D("disconnectButton").onclick = UI.disconnect;
+            $D("descriptionButton").onclick = UI.toggleConnectPanel;
+
+            $D("noVNC_clipboard_text").onfocus = UI.displayBlur;
+            $D("noVNC_clipboard_text").onblur = UI.displayFocus;
+            $D("noVNC_clipboard_text").onchange = UI.clipSend;
+            $D("noVNC_clipboard_clear_button").onclick = UI.clipClear;
+
+            $D("noVNC_settings_menu").onmouseover = UI.displayBlur;
+            $D("noVNC_settings_menu").onmouseover = UI.displayFocus;
+            $D("noVNC_apply").onclick = UI.settingsApply;
+
+            $D("noVNC_connect_button").onclick = UI.connect;
+        },
+
+        // Read form control compatible setting from cookie
+        getSetting: function(name) {
+            var ctrl = $D('noVNC_' + name);
+            var val = WebUtil.readSetting(name);
+            if (val !== null && ctrl.type === 'checkbox') {
+                if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) {
+                    val = false;
+                } else {
+                    val = true;
+                }
+            }
+            return val;
+        },
+
+        // Update cookie and form control setting. If value is not set, then
+        // updates from control to current cookie setting.
+        updateSetting: function(name, value) {
+
+            // Save the cookie for this session
+            if (typeof value !== 'undefined') {
+                WebUtil.writeSetting(name, value);
+            }
+
+            // Update the settings control
+            value = UI.getSetting(name);
+
+            var ctrl = $D('noVNC_' + name);
+            if (ctrl.type === 'checkbox') {
+                ctrl.checked = value;
+
+            } else if (typeof ctrl.options !== 'undefined') {
+                for (var i = 0; i < ctrl.options.length; i += 1) {
+                    if (ctrl.options[i].value === value) {
+                        ctrl.selectedIndex = i;
+                        break;
+                    }
+                }
+            } else {
+                /*Weird IE9 error leads to 'null' appearring
+                in textboxes instead of ''.*/
+                if (value === null) {
+                    value = "";
+                }
+                ctrl.value = value;
+            }
+        },
+
+        // Save control setting to cookie
+        saveSetting: function(name) {
+            var val, ctrl = $D('noVNC_' + name);
+            if (ctrl.type === 'checkbox') {
+                val = ctrl.checked;
+            } else if (typeof ctrl.options !== 'undefined') {
+                val = ctrl.options[ctrl.selectedIndex].value;
+            } else {
+                val = ctrl.value;
+            }
+            WebUtil.writeSetting(name, val);
+            //Util.Debug("Setting saved '" + name + "=" + val + "'");
+            return val;
+        },
+
+        // Initial page load read/initialization of settings
+        initSetting: function(name, defVal) {
+            // Check Query string followed by cookie
+            var val = WebUtil.getQueryVar(name);
+            if (val === null) {
+                val = WebUtil.readSetting(name, defVal);
+            }
+            UI.updateSetting(name, val);
+            return val;
+        },
+
+        // Force a setting to be a certain value
+        forceSetting: function(name, val) {
+            UI.updateSetting(name, val);
+            return val;
+        },
+
+
+        // Show the popup status panel
+        togglePopupStatusPanel: function() {
+            var psp = $D('noVNC_popup_status_panel');
+            if (UI.popupStatusOpen === true) {
+                psp.style.display = "none";
+                UI.popupStatusOpen = false;
+            } else {
+                psp.innerHTML = $D('noVNC_status').innerHTML;
+                psp.style.display = "block";
+                psp.style.left = window.innerWidth/2 -
+                    parseInt(window.getComputedStyle(psp, false).width)/2 -30 + "px";
+                UI.popupStatusOpen = true;
+            }
+        },
+
+        // Show the XVP panel
+        toggleXvpPanel: function() {
+            // Close the description panel
+            $D('noVNC_description').style.display = "none";
+            // Close settings if open
+            if (UI.settingsOpen === true) {
+                UI.settingsApply();
+                UI.closeSettingsMenu();
+            }
+            // Close connection settings if open
+            if (UI.connSettingsOpen === true) {
+                UI.toggleConnectPanel();
+            }
+            // Close popup status panel if open
+            if (UI.popupStatusOpen === true) {
+                UI.togglePopupStatusPanel();
+            }
+            // Close clipboard panel if open
+            if (UI.clipboardOpen === true) {
+                UI.toggleClipboardPanel();
+            }
+            // Toggle XVP panel
+            if (UI.xvpOpen === true) {
+                $D('noVNC_xvp').style.display = "none";
+                $D('xvpButton').className = "noVNC_status_button";
+                UI.xvpOpen = false;
+            } else {
+                $D('noVNC_xvp').style.display = "block";
+                $D('xvpButton').className = "noVNC_status_button_selected";
+                UI.xvpOpen = true;
+            }
+        },
+
+        // Show the clipboard panel
+        toggleClipboardPanel: function() {
+            // Close the description panel
+            $D('noVNC_description').style.display = "none";
+            // Close settings if open
+            if (UI.settingsOpen === true) {
+                UI.settingsApply();
+                UI.closeSettingsMenu();
+            }
+            // Close connection settings if open
+            if (UI.connSettingsOpen === true) {
+                UI.toggleConnectPanel();
+            }
+            // Close popup status panel if open
+            if (UI.popupStatusOpen === true) {
+                UI.togglePopupStatusPanel();
+            }
+            // Close XVP panel if open
+            if (UI.xvpOpen === true) {
+                UI.toggleXvpPanel();
+            }
+            // Toggle Clipboard Panel
+            if (UI.clipboardOpen === true) {
+                $D('noVNC_clipboard').style.display = "none";
+                $D('clipboardButton').className = "noVNC_status_button";
+                UI.clipboardOpen = false;
+            } else {
+                $D('noVNC_clipboard').style.display = "block";
+                $D('clipboardButton').className = "noVNC_status_button_selected";
+                UI.clipboardOpen = true;
+            }
+        },
+
+        // Show the connection settings panel/menu
+        toggleConnectPanel: function() {
+            // Close the description panel
+            $D('noVNC_description').style.display = "none";
+            // Close connection settings if open
+            if (UI.settingsOpen === true) {
+                UI.settingsApply();
+                UI.closeSettingsMenu();
+                $D('connectButton').className = "noVNC_status_button";
+            }
+            // Close clipboard panel if open
+            if (UI.clipboardOpen === true) {
+                UI.toggleClipboardPanel();
+            }
+            // Close popup status panel if open
+            if (UI.popupStatusOpen === true) {
+                UI.togglePopupStatusPanel();
+            }
+            // Close XVP panel if open
+            if (UI.xvpOpen === true) {
+                UI.toggleXvpPanel();
+            }
+
+            // Toggle Connection Panel
+            if (UI.connSettingsOpen === true) {
+                $D('noVNC_controls').style.display = "none";
+                $D('connectButton').className = "noVNC_status_button";
+                UI.connSettingsOpen = false;
+                UI.saveSetting('host');
+                UI.saveSetting('port');
+                //UI.saveSetting('password');
+            } else {
+                $D('noVNC_controls').style.display = "block";
+                $D('connectButton').className = "noVNC_status_button_selected";
+                UI.connSettingsOpen = true;
+                $D('noVNC_host').focus();
+            }
+        },
+
+        // Toggle the settings menu:
+        //   On open, settings are refreshed from saved cookies.
+        //   On close, settings are applied
+        toggleSettingsPanel: function() {
+            // Close the description panel
+            $D('noVNC_description').style.display = "none";
+            if (UI.settingsOpen) {
+                UI.settingsApply();
+                UI.closeSettingsMenu();
+            } else {
+                UI.updateSetting('encrypt');
+                UI.updateSetting('true_color');
+                if (UI.rfb.get_display().get_cursor_uri()) {
+                    UI.updateSetting('cursor');
+                } else {
+                    UI.updateSetting('cursor', !UI.isTouchDevice);
+                    $D('noVNC_cursor').disabled = true;
+                }
+                UI.updateSetting('clip');
+                UI.updateSetting('shared');
+                UI.updateSetting('view_only');
+                UI.updateSetting('path');
+                UI.updateSetting('repeaterID');
+                UI.updateSetting('stylesheet');
+                UI.updateSetting('logging');
+
+                UI.openSettingsMenu();
+            }
+        },
+
+        // Open menu
+        openSettingsMenu: function() {
+            // Close the description panel
+            $D('noVNC_description').style.display = "none";
+            // Close clipboard panel if open
+            if (UI.clipboardOpen === true) {
+                UI.toggleClipboardPanel();
+            }
+            // Close connection settings if open
+            if (UI.connSettingsOpen === true) {
+                UI.toggleConnectPanel();
+            }
+            // Close popup status panel if open
+            if (UI.popupStatusOpen === true) {
+                UI.togglePopupStatusPanel();
+            }
+            // Close XVP panel if open
+            if (UI.xvpOpen === true) {
+                UI.toggleXvpPanel();
+            }
+            $D('noVNC_settings').style.display = "block";
+            $D('settingsButton').className = "noVNC_status_button_selected";
+            UI.settingsOpen = true;
+        },
+
+        // Close menu (without applying settings)
+        closeSettingsMenu: function() {
+            $D('noVNC_settings').style.display = "none";
+            $D('settingsButton').className = "noVNC_status_button";
+            UI.settingsOpen = false;
+        },
+
+        // Save/apply settings when 'Apply' button is pressed
+        settingsApply: function() {
+            //Util.Debug(">> settingsApply");
+            UI.saveSetting('encrypt');
+            UI.saveSetting('true_color');
+            if (UI.rfb.get_display().get_cursor_uri()) {
+                UI.saveSetting('cursor');
+            }
+            UI.saveSetting('clip');
+            UI.saveSetting('shared');
+            UI.saveSetting('view_only');
+            UI.saveSetting('path');
+            UI.saveSetting('repeaterID');
+            UI.saveSetting('stylesheet');
+            UI.saveSetting('logging');
+
+            // Settings with immediate (non-connected related) effect
+            WebUtil.selectStylesheet(UI.getSetting('stylesheet'));
+            WebUtil.init_logging(UI.getSetting('logging'));
+            UI.setViewClip();
+            UI.setViewDrag(UI.rfb.get_viewportDrag());
+            //Util.Debug("<< settingsApply");
+        },
+
+
+
+        setPassword: function() {
+            UI.rfb.sendPassword($D('noVNC_password').value);
+            //Reset connect button.
+            $D('noVNC_connect_button').value = "Connect";
+            $D('noVNC_connect_button').onclick = UI.Connect;
+            //Hide connection panel.
             UI.toggleConnectPanel();
-        }
-    }
-
-    // Add mouse event click/focus/blur event handlers to the UI
-    UI.addMouseHandlers();
-
-    if (typeof callback === "function") {
-        callback(UI.rfb);
-    }
-},
-
-addMouseHandlers: function() {
-    // Setup interface handlers that can't be inline
-    $D("noVNC_view_drag_button").onclick = UI.setViewDrag;
-    $D("noVNC_mouse_button0").onclick = function () { UI.setMouseButton(1); };
-    $D("noVNC_mouse_button1").onclick = function () { UI.setMouseButton(2); };
-    $D("noVNC_mouse_button2").onclick = function () { UI.setMouseButton(4); };
-    $D("noVNC_mouse_button4").onclick = function () { UI.setMouseButton(0); };
-    $D("showKeyboard").onclick = UI.showKeyboard;
-
-    $D("keyboardinput").oninput = UI.keyInput;
-    $D("keyboardinput").onblur = UI.keyInputBlur;
-
-    $D("showExtraKeysButton").onclick = UI.showExtraKeys;
-    $D("toggleCtrlButton").onclick = UI.toggleCtrl;
-    $D("toggleAltButton").onclick = UI.toggleAlt;
-    $D("sendTabButton").onclick = UI.sendTab;
-    $D("sendEscButton").onclick = UI.sendEsc;
-
-    $D("sendCtrlAltDelButton").onclick = UI.sendCtrlAltDel;
-    $D("xvpShutdownButton").onclick = UI.xvpShutdown;
-    $D("xvpRebootButton").onclick = UI.xvpReboot;
-    $D("xvpResetButton").onclick = UI.xvpReset;
-    $D("noVNC_status").onclick = UI.togglePopupStatusPanel;
-    $D("noVNC_popup_status_panel").onclick = UI.togglePopupStatusPanel;
-    $D("xvpButton").onclick = UI.toggleXvpPanel;
-    $D("clipboardButton").onclick = UI.toggleClipboardPanel;
-    $D("settingsButton").onclick = UI.toggleSettingsPanel;
-    $D("connectButton").onclick = UI.toggleConnectPanel;
-    $D("disconnectButton").onclick = UI.disconnect;
-    $D("descriptionButton").onclick = UI.toggleConnectPanel;
-
-    $D("noVNC_clipboard_text").onfocus = UI.displayBlur;
-    $D("noVNC_clipboard_text").onblur = UI.displayFocus;
-    $D("noVNC_clipboard_text").onchange = UI.clipSend;
-    $D("noVNC_clipboard_clear_button").onclick = UI.clipClear;
-
-    $D("noVNC_settings_menu").onmouseover = UI.displayBlur;
-    $D("noVNC_settings_menu").onmouseover = UI.displayFocus;
-    $D("noVNC_apply").onclick = UI.settingsApply;
-
-    $D("noVNC_connect_button").onclick = UI.connect;
-},
-
-// Read form control compatible setting from cookie
-getSetting: function(name) {
-    var val, ctrl = $D('noVNC_' + name);
-    val = WebUtil.readSetting(name);
-    if (val !== null && ctrl.type === 'checkbox') {
-        if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) {
-            val = false;
-        } else {
-            val = true;
-        }
-    }
-    return val;
-},
+            return false;
+        },
 
-// Update cookie and form control setting. If value is not set, then
-// updates from control to current cookie setting.
-updateSetting: function(name, value) {
+        sendCtrlAltDel: function() {
+            UI.rfb.sendCtrlAltDel();
+        },
 
-    var i, ctrl = $D('noVNC_' + name);
-    // Save the cookie for this session
-    if (typeof value !== 'undefined') {
-        WebUtil.writeSetting(name, value);
-    }
+        xvpShutdown: function() {
+            UI.rfb.xvpShutdown();
+        },
 
-    // Update the settings control
-    value = UI.getSetting(name);
+        xvpReboot: function() {
+            UI.rfb.xvpReboot();
+        },
 
-    if (ctrl.type === 'checkbox') {
-        ctrl.checked = value;
+        xvpReset: function() {
+            UI.rfb.xvpReset();
+        },
 
-    } else if (typeof ctrl.options !== 'undefined') {
-        for (i = 0; i < ctrl.options.length; i += 1) {
-            if (ctrl.options[i].value === value) {
-                ctrl.selectedIndex = i;
-                break;
+        setMouseButton: function(num) {
+            if (typeof num === 'undefined') {
+                // Disable mouse buttons
+                num = -1;
             }
-        }
-    } else {
-        /*Weird IE9 error leads to 'null' appearring
-        in textboxes instead of ''.*/
-        if (value === null) {
-            value = "";
-        }
-        ctrl.value = value;
-    }
-},
-
-// Save control setting to cookie
-saveSetting: function(name) {
-    var val, ctrl = $D('noVNC_' + name);
-    if (ctrl.type === 'checkbox') {
-        val = ctrl.checked;
-    } else if (typeof ctrl.options !== 'undefined') {
-        val = ctrl.options[ctrl.selectedIndex].value;
-    } else {
-        val = ctrl.value;
-    }
-    WebUtil.writeSetting(name, val);
-    //Util.Debug("Setting saved '" + name + "=" + val + "'");
-    return val;
-},
-
-// Initial page load read/initialization of settings
-initSetting: function(name, defVal) {
-    var val;
-
-    // Check Query string followed by cookie
-    val = WebUtil.getQueryVar(name);
-    if (val === null) {
-        val = WebUtil.readSetting(name, defVal);
-    }
-    UI.updateSetting(name, val);
- //Util.Debug("Setting '" + name + "' initialized to '" + val + "'");
-    return val;
-},
-
-// Force a setting to be a certain value
-forceSetting: function(name, val) {
-    UI.updateSetting(name, val);
-    return val;
-},
-
-
-// Show the popup status panel
-togglePopupStatusPanel: function() {
-    var psp = $D('noVNC_popup_status_panel');
-    if (UI.popupStatusOpen === true) {
-        psp.style.display = "none";
-        UI.popupStatusOpen = false;
-    } else {
-        psp.innerHTML = $D('noVNC_status').innerHTML;
-        psp.style.display = "block";
-        psp.style.left = window.innerWidth/2 - 
-            parseInt(window.getComputedStyle(psp, false).width)/2 -30 + "px";
-        UI.popupStatusOpen = true;
-    }
-},
-
-// Show the XVP panel
-toggleXvpPanel: function() {
-    // Close the description panel
-    $D('noVNC_description').style.display = "none";
-    // Close settings if open
-    if (UI.settingsOpen === true) {
-        UI.settingsApply();
-        UI.closeSettingsMenu();
-    }
-    // Close connection settings if open
-    if (UI.connSettingsOpen === true) {
-        UI.toggleConnectPanel();
-    }
-    // Close popup status panel if open
-    if (UI.popupStatusOpen === true) {
-        UI.togglePopupStatusPanel();
-    }
-    // Close clipboard panel if open
-    if (UI.clipboardOpen === true) {
-        UI.toggleClipboardPanel();
-    }
-    // Toggle XVP panel
-    if (UI.xvpOpen === true) {
-        $D('noVNC_xvp').style.display = "none";
-        $D('xvpButton').className = "noVNC_status_button";
-        UI.xvpOpen = false;
-    } else {
-        $D('noVNC_xvp').style.display = "block";
-        $D('xvpButton').className = "noVNC_status_button_selected";
-        UI.xvpOpen = true;
-    }
-},
-
-// Show the clipboard panel
-toggleClipboardPanel: function() {
-    // Close the description panel
-    $D('noVNC_description').style.display = "none";
-    // Close settings if open
-    if (UI.settingsOpen === true) {
-        UI.settingsApply();
-        UI.closeSettingsMenu();
-    }
-    // Close connection settings if open
-    if (UI.connSettingsOpen === true) {
-        UI.toggleConnectPanel();
-    }
-    // Close popup status panel if open
-    if (UI.popupStatusOpen === true) {
-        UI.togglePopupStatusPanel();
-    }
-    // Close XVP panel if open
-    if (UI.xvpOpen === true) {
-        UI.toggleXvpPanel();
-    }
-    // Toggle Clipboard Panel
-    if (UI.clipboardOpen === true) {
-        $D('noVNC_clipboard').style.display = "none";
-        $D('clipboardButton').className = "noVNC_status_button";
-        UI.clipboardOpen = false;
-    } else {
-        $D('noVNC_clipboard').style.display = "block";
-        $D('clipboardButton').className = "noVNC_status_button_selected";
-        UI.clipboardOpen = true;
-    }
-},
-
-// Show the connection settings panel/menu
-toggleConnectPanel: function() {
-    // Close the description panel
-    $D('noVNC_description').style.display = "none";
-    // Close connection settings if open
-    if (UI.settingsOpen === true) {
-        UI.settingsApply();
-        UI.closeSettingsMenu();
-        $D('connectButton').className = "noVNC_status_button";
-    }
-    // Close clipboard panel if open
-    if (UI.clipboardOpen === true) {
-        UI.toggleClipboardPanel();
-    }
-    // Close popup status panel if open
-    if (UI.popupStatusOpen === true) {
-        UI.togglePopupStatusPanel();
-    }
-    // Close XVP panel if open
-    if (UI.xvpOpen === true) {
-        UI.toggleXvpPanel();
-    }
-
-    // Toggle Connection Panel
-    if (UI.connSettingsOpen === true) {
-        $D('noVNC_controls').style.display = "none";
-        $D('connectButton').className = "noVNC_status_button";
-        UI.connSettingsOpen = false;
-        UI.saveSetting('host');
-        UI.saveSetting('port');
-        //UI.saveSetting('password');
-    } else {
-        $D('noVNC_controls').style.display = "block";
-        $D('connectButton').className = "noVNC_status_button_selected";
-        UI.connSettingsOpen = true;
-        $D('noVNC_host').focus();
-    }
-},
-
-// Toggle the settings menu:
-//   On open, settings are refreshed from saved cookies.
-//   On close, settings are applied
-toggleSettingsPanel: function() {
-    // Close the description panel
-    $D('noVNC_description').style.display = "none";
-    if (UI.settingsOpen) {
-        UI.settingsApply();
-        UI.closeSettingsMenu();
-    } else {
-        UI.updateSetting('encrypt');
-        UI.updateSetting('true_color');
-        if (UI.rfb.get_display().get_cursor_uri()) {
-            UI.updateSetting('cursor');
-        } else {
-            UI.updateSetting('cursor', !UI.isTouchDevice);
-            $D('noVNC_cursor').disabled = true;
-        }
-        UI.updateSetting('clip');
-        UI.updateSetting('shared');
-        UI.updateSetting('view_only');
-        UI.updateSetting('path');
-        UI.updateSetting('repeaterID');
-        UI.updateSetting('stylesheet');
-        UI.updateSetting('logging');
-
-        UI.openSettingsMenu();
-    }
-},
-
-// Open menu
-openSettingsMenu: function() {
-    // Close the description panel
-    $D('noVNC_description').style.display = "none";
-    // Close clipboard panel if open
-    if (UI.clipboardOpen === true) {
-        UI.toggleClipboardPanel();
-    }
-    // Close connection settings if open
-    if (UI.connSettingsOpen === true) {
-        UI.toggleConnectPanel();
-    }
-    // Close popup status panel if open
-    if (UI.popupStatusOpen === true) {
-        UI.togglePopupStatusPanel();
-    }
-    // Close XVP panel if open
-    if (UI.xvpOpen === true) {
-        UI.toggleXvpPanel();
-    }
-    $D('noVNC_settings').style.display = "block";
-    $D('settingsButton').className = "noVNC_status_button_selected";
-    UI.settingsOpen = true;
-},
-
-// Close menu (without applying settings)
-closeSettingsMenu: function() {
-    $D('noVNC_settings').style.display = "none";
-    $D('settingsButton').className = "noVNC_status_button";
-    UI.settingsOpen = false;
-},
-
-// Save/apply settings when 'Apply' button is pressed
-settingsApply: function() {
-    //Util.Debug(">> settingsApply");
-    UI.saveSetting('encrypt');
-    UI.saveSetting('true_color');
-    if (UI.rfb.get_display().get_cursor_uri()) {
-        UI.saveSetting('cursor');
-    }
-    UI.saveSetting('clip');
-    UI.saveSetting('shared');
-    UI.saveSetting('view_only');
-    UI.saveSetting('path');
-    UI.saveSetting('repeaterID');
-    UI.saveSetting('stylesheet');
-    UI.saveSetting('logging');
-
-    // Settings with immediate (non-connected related) effect
-    WebUtil.selectStylesheet(UI.getSetting('stylesheet'));
-    WebUtil.init_logging(UI.getSetting('logging'));
-    UI.setViewClip();
-    UI.setViewDrag(UI.rfb.get_viewportDrag());
-    //Util.Debug("<< settingsApply");
-},
-
-
-
-setPassword: function() {
-    UI.rfb.sendPassword($D('noVNC_password').value);
-    //Reset connect button.
-    $D('noVNC_connect_button').value = "Connect";
-    $D('noVNC_connect_button').onclick = UI.Connect;
-    //Hide connection panel.
-    UI.toggleConnectPanel();
-    return false;
-},
-
-sendCtrlAltDel: function() {
-    UI.rfb.sendCtrlAltDel();
-},
-
-xvpShutdown: function() {
-    UI.rfb.xvpShutdown();
-},
-
-xvpReboot: function() {
-    UI.rfb.xvpReboot();
-},
-
-xvpReset: function() {
-    UI.rfb.xvpReset();
-},
-
-setMouseButton: function(num) {
-    var b, blist = [0, 1,2,4], button;
-
-    if (typeof num === 'undefined') {
-        // Disable mouse buttons
-        num = -1;
-    }
-    if (UI.rfb) {
-        UI.rfb.get_mouse().set_touchButton(num);
-    }
-
-    for (b = 0; b < blist.length; b++) {
-        button = $D('noVNC_mouse_button' + blist[b]);
-        if (blist[b] === num) {
-            button.style.display = "";
-        } else {
-            button.style.display = "none";
-            /*
-            button.style.backgroundColor = "black";
-            button.style.color = "lightgray";
-            button.style.backgroundColor = "";
-            button.style.color = "";
-            */
-        }
-    }
-},
-
-updateState: function(rfb, state, oldstate, msg) {
-    var s, sb, c, d, cad, vd, klass;
-    UI.rfb_state = state;
-    switch (state) {
-        case 'failed':
-        case 'fatal':
-            klass = "noVNC_status_error";
-            break;
-        case 'normal':
-            klass = "noVNC_status_normal";
-            break;
-        case 'disconnected':
-            $D('noVNC_logo').style.display = "block";
-            // Fall through
-        case 'loaded':
-            klass = "noVNC_status_normal";
-            break;
-        case 'password':
+            if (UI.rfb) {
+                UI.rfb.get_mouse().set_touchButton(num);
+            }
+
+            var blist = [0, 1,2,4];
+            for (var b = 0; b < blist.length; b++) {
+                var button = $D('noVNC_mouse_button' + blist[b]);
+                if (blist[b] === num) {
+                    button.style.display = "";
+                } else {
+                    button.style.display = "none";
+                }
+            }
+        },
+
+        updateState: function(rfb, state, oldstate, msg) {
+            UI.rfb_state = state;
+            var klass;
+            switch (state) {
+                case 'failed':
+                case 'fatal':
+                    klass = "noVNC_status_error";
+                    break;
+                case 'normal':
+                    klass = "noVNC_status_normal";
+                    break;
+                case 'disconnected':
+                    $D('noVNC_logo').style.display = "block";
+                    /* falls through */
+                case 'loaded':
+                    klass = "noVNC_status_normal";
+                    break;
+                case 'password':
+                    UI.toggleConnectPanel();
+
+                    $D('noVNC_connect_button').value = "Send Password";
+                    $D('noVNC_connect_button').onclick = UI.setPassword;
+                    $D('noVNC_password').focus();
+
+                    klass = "noVNC_status_warn";
+                    break;
+                default:
+                    klass = "noVNC_status_warn";
+                    break;
+            }
+
+            if (typeof(msg) !== 'undefined') {
+                $D('noVNC-control-bar').setAttribute("class", klass);
+                $D('noVNC_status').innerHTML = msg;
+            }
+
+            UI.updateVisualState();
+        },
+
+        // Disable/enable controls depending on connection state
+        updateVisualState: function() {
+            var connected = UI.rfb_state === 'normal' ? true : false;
+
+            //Util.Debug(">> updateVisualState");
+            $D('noVNC_encrypt').disabled = connected;
+            $D('noVNC_true_color').disabled = connected;
+            if (UI.rfb && UI.rfb.get_display() &&
+                UI.rfb.get_display().get_cursor_uri()) {
+                $D('noVNC_cursor').disabled = connected;
+            } else {
+                UI.updateSetting('cursor', !UI.isTouchDevice);
+                $D('noVNC_cursor').disabled = true;
+            }
+            $D('noVNC_shared').disabled = connected;
+            $D('noVNC_view_only').disabled = connected;
+            $D('noVNC_path').disabled = connected;
+            $D('noVNC_repeaterID').disabled = connected;
+
+            if (connected) {
+                UI.setViewClip();
+                UI.setMouseButton(1);
+                $D('clipboardButton').style.display = "inline";
+                $D('showKeyboard').style.display = "inline";
+                $D('noVNC_extra_keys').style.display = "";
+                $D('sendCtrlAltDelButton').style.display = "inline";
+            } else {
+                UI.setMouseButton();
+                $D('clipboardButton').style.display = "none";
+                $D('showKeyboard').style.display = "none";
+                $D('noVNC_extra_keys').style.display = "none";
+                $D('sendCtrlAltDelButton').style.display = "none";
+                UI.updateXvpVisualState(0);
+            }
+
+            // State change disables viewport dragging.
+            // It is enabled (toggled) by direct click on the button
+            UI.setViewDrag(false);
+
+            switch (UI.rfb_state) {
+                case 'fatal':
+                case 'failed':
+                case 'loaded':
+                case 'disconnected':
+                    $D('connectButton').style.display = "";
+                    $D('disconnectButton').style.display = "none";
+                    break;
+                default:
+                    $D('connectButton').style.display = "none";
+                    $D('disconnectButton').style.display = "";
+                    break;
+            }
+
+            //Util.Debug("<< updateVisualState");
+        },
+
+        // Disable/enable XVP button
+        updateXvpVisualState: function(ver) {
+            if (ver >= 1) {
+                $D('xvpButton').style.display = 'inline';
+            } else {
+                $D('xvpButton').style.display = 'none';
+                // Close XVP panel if open
+                if (UI.xvpOpen === true) {
+                    UI.toggleXvpPanel();
+                }
+            }
+        },
+
+        // Display the desktop name in the document title
+        updateDocumentTitle: function(rfb, name) {
+            document.title = name + " - noVNC";
+        },
+
+        clipReceive: function(rfb, text) {
+            Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "...");
+            $D('noVNC_clipboard_text').value = text;
+            Util.Debug("<< UI.clipReceive");
+        },
+
+        connect: function() {
+            UI.closeSettingsMenu();
             UI.toggleConnectPanel();
 
-            $D('noVNC_connect_button').value = "Send Password";
-            $D('noVNC_connect_button').onclick = UI.setPassword;
-            $D('noVNC_password').focus();
-
-            klass = "noVNC_status_warn";
-            break;
-        default:
-            klass = "noVNC_status_warn";
-            break;
-    }
-
-    if (typeof(msg) !== 'undefined') {
-        $D('noVNC-control-bar').setAttribute("class", klass);
-        $D('noVNC_status').innerHTML = msg;
-    }
-
-    UI.updateVisualState();
-},
-
-// Disable/enable controls depending on connection state
-updateVisualState: function() {
-    var connected = UI.rfb_state === 'normal' ? true : false;
-
-    //Util.Debug(">> updateVisualState");
-    $D('noVNC_encrypt').disabled = connected;
-    $D('noVNC_true_color').disabled = connected;
-    if (UI.rfb && UI.rfb.get_display() &&
-        UI.rfb.get_display().get_cursor_uri()) {
-        $D('noVNC_cursor').disabled = connected;
-    } else {
-        UI.updateSetting('cursor', !UI.isTouchDevice);
-        $D('noVNC_cursor').disabled = true;
-    }
-    $D('noVNC_shared').disabled = connected;
-    $D('noVNC_view_only').disabled = connected;
-    $D('noVNC_path').disabled = connected;
-    $D('noVNC_repeaterID').disabled = connected;
-
-    if (connected) {
-        UI.setViewClip();
-        UI.setMouseButton(1);
-        $D('clipboardButton').style.display = "inline";
-        $D('showKeyboard').style.display = "inline";
-        $D('noVNC_extra_keys').style.display = "";
-        $D('sendCtrlAltDelButton').style.display = "inline";
-    } else {
-        UI.setMouseButton();
-        $D('clipboardButton').style.display = "none";
-        $D('showKeyboard').style.display = "none";
-        $D('noVNC_extra_keys').style.display = "none";
-        $D('sendCtrlAltDelButton').style.display = "none";
-        UI.updateXvpVisualState(0);
-    }
-    
-    // State change disables viewport dragging.
-    // It is enabled (toggled) by direct click on the button
-    UI.setViewDrag(false);
-
-    switch (UI.rfb_state) {
-        case 'fatal':
-        case 'failed':
-        case 'loaded':
-        case 'disconnected':
-            $D('connectButton').style.display = "";
-            $D('disconnectButton').style.display = "none";
-            break;
-        default:
-            $D('connectButton').style.display = "none";
-            $D('disconnectButton').style.display = "";
-            break;
-    }
-
-    //Util.Debug("<< updateVisualState");
-},
-
-// Disable/enable XVP button
-updateXvpVisualState: function(ver) {
-    if (ver >= 1) {
-        $D('xvpButton').style.display = 'inline';
-    } else {
-        $D('xvpButton').style.display = 'none';
-        // Close XVP panel if open
-        if (UI.xvpOpen === true) {
-            UI.toggleXvpPanel();
-        }
-    }
-},
-
-
-// Display the desktop name in the document title
-updateDocumentTitle: function(rfb, name) {
-    document.title = name + " - noVNC";
-},
-
-
-clipReceive: function(rfb, text) {
-    Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "...");
-    $D('noVNC_clipboard_text').value = text;
-    Util.Debug("<< UI.clipReceive");
-},
-
-
-connect: function() {
-    var host, port, password, path;
-
-    UI.closeSettingsMenu();
-    UI.toggleConnectPanel();
-
-    host = $D('noVNC_host').value;
-    port = $D('noVNC_port').value;
-    password = $D('noVNC_password').value;
-    path = $D('noVNC_path').value;
-    if ((!host) || (!port)) {
-        throw("Must set host and port");
-    }
-
-    UI.rfb.set_encrypt(UI.getSetting('encrypt'));
-    UI.rfb.set_true_color(UI.getSetting('true_color'));
-    UI.rfb.set_local_cursor(UI.getSetting('cursor'));
-    UI.rfb.set_shared(UI.getSetting('shared'));
-    UI.rfb.set_view_only(UI.getSetting('view_only'));
-    UI.rfb.set_repeaterID(UI.getSetting('repeaterID'));
-
-    UI.rfb.connect(host, port, password, path);
-
-    //Close dialog.
-    setTimeout(UI.setBarPosition, 100);
-    $D('noVNC_logo').style.display = "none";
-},
-
-disconnect: function() {
-    UI.closeSettingsMenu();
-    UI.rfb.disconnect();
-
-    $D('noVNC_logo').style.display = "block";
-    UI.connSettingsOpen = false;
-    UI.toggleConnectPanel();
-},
-
-displayBlur: function() {
-    UI.rfb.get_keyboard().set_focused(false);
-    UI.rfb.get_mouse().set_focused(false);
-},
-
-displayFocus: function() {
-    UI.rfb.get_keyboard().set_focused(true);
-    UI.rfb.get_mouse().set_focused(true);
-},
-
-clipClear: function() {
-    $D('noVNC_clipboard_text').value = "";
-    UI.rfb.clipboardPasteFrom("");
-},
-
-clipSend: function() {
-    var text = $D('noVNC_clipboard_text').value;
-    Util.Debug(">> UI.clipSend: " + text.substr(0,40) + "...");
-    UI.rfb.clipboardPasteFrom(text);
-    Util.Debug("<< UI.clipSend");
-},
-
-
-// Enable/disable and configure viewport clipping
-setViewClip: function(clip) {
-    var display, cur_clip, pos, new_w, new_h;
-
-    if (UI.rfb) {
-        display = UI.rfb.get_display();
-    } else {
-        return;
-    }
-
-    cur_clip = display.get_viewport();
-
-    if (typeof(clip) !== 'boolean') {
-        // Use current setting
-        clip = UI.getSetting('clip');
-    }
-
-    if (clip && !cur_clip) {
-        // Turn clipping on
-        UI.updateSetting('clip', true);
-    } else if (!clip && cur_clip) {
-        // Turn clipping off
-        UI.updateSetting('clip', false);
-        display.set_viewport(false);
-        $D('noVNC_canvas').style.position = 'static';
-        display.viewportChange();
-    }
-    if (UI.getSetting('clip')) {
-        // If clipping, update clipping settings
-        $D('noVNC_canvas').style.position = 'absolute';
-        pos = Util.getPosition($D('noVNC_canvas'));
-        new_w = window.innerWidth - pos.x;
-        new_h = window.innerHeight - pos.y;
-        display.set_viewport(true);
-        display.viewportChange(0, 0, new_w, new_h);
-    }
-},
-
-// Toggle/set/unset the viewport drag/move button
-setViewDrag: function(drag) {
-    var vmb = $D('noVNC_view_drag_button');
-    if (!UI.rfb) { return; }
-
-    if (UI.rfb_state === 'normal' &&
-        UI.rfb.get_display().get_viewport()) {
-        vmb.style.display = "inline";
-    } else {
-        vmb.style.display = "none";
-    }
-
-    if (typeof(drag) === "undefined" ||
-        typeof(drag) === "object") {
-        // If not specified, then toggle
-        drag = !UI.rfb.get_viewportDrag();
-    }
-    if (drag) {
-        vmb.className = "noVNC_status_button_selected";
-        UI.rfb.set_viewportDrag(true);
-    } else {
-        vmb.className = "noVNC_status_button";
-        UI.rfb.set_viewportDrag(false);
-    }
-},
-
-// On touch devices, show the OS keyboard
-showKeyboard: function() {
-    var kbi, skb, l;
-    kbi = $D('keyboardinput');
-    skb = $D('showKeyboard');
-    l = kbi.value.length;
-    if(UI.keyboardVisible === false) {
-        kbi.focus();
-        try { kbi.setSelectionRange(l, l); } // Move the caret to the end
-        catch (err) {} // setSelectionRange is undefined in Google Chrome
-        UI.keyboardVisible = true;
-        skb.className = "noVNC_status_button_selected";
-    } else if(UI.keyboardVisible === true) {
-        kbi.blur();
-        skb.className = "noVNC_status_button";
-        UI.keyboardVisible = false;
-    }
-},
-
-keepKeyboard: function() {
-    clearTimeout(UI.hideKeyboardTimeout);
-    if(UI.keyboardVisible === true) {
-        $D('keyboardinput').focus();
-        $D('showKeyboard').className = "noVNC_status_button_selected";
-    } else if(UI.keyboardVisible === false) {
-        $D('keyboardinput').blur();
-        $D('showKeyboard').className = "noVNC_status_button";
-    }
-},
-
-keyboardinputReset: function() {
-    var kbi = $D('keyboardinput');
-    kbi.value = Array(UI.defaultKeyboardinputLen).join("_");
-    UI.lastKeyboardinput = kbi.value;
-},
-
-// When normal keyboard events are left uncought, use the input events from
-// the keyboardinput element instead and generate the corresponding key events.
-// This code is required since some browsers on Android are inconsistent in
-// sending keyCodes in the normal keyboard events when using on screen keyboards.
-keyInput: function(event) {
-    var newValue, oldValue, newLen, oldLen;
-    newValue = event.target.value;
-    oldValue = UI.lastKeyboardinput;
-
-    try {
-        // Try to check caret position since whitespace at the end
-        // will not be considered by value.length in some browsers
-        newLen = Math.max(event.target.selectionStart, newValue.length);
-    } catch (err) {
-        // selectionStart is undefined in Google Chrome
-        newLen = newValue.length;
-    }
-    oldLen = oldValue.length;
-
-    var backspaces;
-    var inputs = newLen - oldLen;
-    if (inputs < 0)
-        backspaces = -inputs;
-    else
-        backspaces = 0;
-
-    // Compare the old string with the new to account for
-    // text-corrections or other input that modify existing text
-    for (var i = 0; i < Math.min(oldLen, newLen); i++) {
-        if (newValue.charAt(i) != oldValue.charAt(i)) {
-            inputs = newLen - i;
-            backspaces = oldLen - i;
-            break;
-        }
-    }
-
-    // Send the key events
-    for (var i = 0; i < backspaces; i++)
-        UI.rfb.sendKey(XK_BackSpace);
-    for (var i = newLen - inputs; i < newLen; i++)
-        UI.rfb.sendKey(newValue.charCodeAt(i));
-
-    // Control the text content length in the keyboardinput element
-    if (newLen > 2 * UI.defaultKeyboardinputLen) {
-        UI.keyboardinputReset();
-    } else if (newLen < 1) {
-        // There always have to be some text in the keyboardinput
-        // element with which backspace can interact.
-        UI.keyboardinputReset();
-        // This sometimes causes the keyboard to disappear for a second
-        // but it is required for the android keyboard to recognize that
-        // text has been added to the field
-        event.target.blur();
-        // This has to be ran outside of the input handler in order to work
-        setTimeout(function() { UI.keepKeyboard(); }, 0);
-
-    } else {
-        UI.lastKeyboardinput = newValue;
-    }
-},
-
-keyInputBlur: function() {
-    $D('showKeyboard').className = "noVNC_status_button";
-    //Weird bug in iOS if you change keyboardVisible
-    //here it does not actually occur so next time
-    //you click keyboard icon it doesnt work.
-    UI.hideKeyboardTimeout = setTimeout(function() { UI.setKeyboard(); },100);
-},
-
-showExtraKeys: function() {
-    UI.keepKeyboard();
-    if(UI.extraKeysVisible === false) {
-        $D('toggleCtrlButton').style.display = "inline";
-        $D('toggleAltButton').style.display = "inline";
-        $D('sendTabButton').style.display = "inline";
-        $D('sendEscButton').style.display = "inline";
-        $D('showExtraKeysButton').className = "noVNC_status_button_selected";
-        UI.extraKeysVisible = true;
-    } else if(UI.extraKeysVisible === true) {
-        $D('toggleCtrlButton').style.display = "";
-        $D('toggleAltButton').style.display = "";
-        $D('sendTabButton').style.display = "";
-        $D('sendEscButton').style.display = "";
-        $D('showExtraKeysButton').className = "noVNC_status_button";
-        UI.extraKeysVisible = false;
-    }
-},
-
-toggleCtrl: function() {
-    UI.keepKeyboard();
-    if(UI.ctrlOn === false) {
-        UI.rfb.sendKey(XK_Control_L, true);
-        $D('toggleCtrlButton').className = "noVNC_status_button_selected";
-        UI.ctrlOn = true;
-    } else if(UI.ctrlOn === true) {
-        UI.rfb.sendKey(XK_Control_L, false);
-        $D('toggleCtrlButton').className = "noVNC_status_button";
-        UI.ctrlOn = false;
-    }
-},
-
-toggleAlt: function() {
-    UI.keepKeyboard();
-    if(UI.altOn === false) {
-        UI.rfb.sendKey(XK_Alt_L, true);
-        $D('toggleAltButton').className = "noVNC_status_button_selected";
-        UI.altOn = true;
-    } else if(UI.altOn === true) {
-        UI.rfb.sendKey(XK_Alt_L, false);
-        $D('toggleAltButton').className = "noVNC_status_button";
-        UI.altOn = false;
-    }
-},
-
-sendTab: function() {
-    UI.keepKeyboard();
-    UI.rfb.sendKey(XK_Tab);
-},
-
-sendEsc: function() {
-    UI.keepKeyboard();
-    UI.rfb.sendKey(XK_Escape);
-},
-
-setKeyboard: function() {
-    UI.keyboardVisible = false;
-},
-
-// iOS < Version 5 does not support position fixed. Javascript workaround:
-setOnscroll: function() {
-    window.onscroll = function() {
-        UI.setBarPosition();
-    };
-},
+            var host = $D('noVNC_host').value;
+            var port = $D('noVNC_port').value;
+            var password = $D('noVNC_password').value;
+            var path = $D('noVNC_path').value;
+            if ((!host) || (!port)) {
+                throw new Error("Must set host and port");
+            }
 
-setResize: function () {
-    window.onResize = function() {
-        UI.setBarPosition();
-    };
-},
+            UI.rfb.set_encrypt(UI.getSetting('encrypt'));
+            UI.rfb.set_true_color(UI.getSetting('true_color'));
+            UI.rfb.set_local_cursor(UI.getSetting('cursor'));
+            UI.rfb.set_shared(UI.getSetting('shared'));
+            UI.rfb.set_view_only(UI.getSetting('view_only'));
+            UI.rfb.set_repeaterID(UI.getSetting('repeaterID'));
 
-//Helper to add options to dropdown.
-addOption: function(selectbox,text,value )
-{
-    var optn = document.createElement("OPTION");
-    optn.text = text;
-    optn.value = value;
-    selectbox.options.add(optn);
-},
+            UI.rfb.connect(host, port, password, path);
 
-setBarPosition: function() {
-    $D('noVNC-control-bar').style.top = (window.pageYOffset) + 'px';
-    $D('noVNC_mobile_buttons').style.left = (window.pageXOffset) + 'px';
+            //Close dialog.
+            setTimeout(UI.setBarPosition, 100);
+            $D('noVNC_logo').style.display = "none";
+        },
 
-    var vncwidth = $D('noVNC_screen').style.offsetWidth;
-    $D('noVNC-control-bar').style.width = vncwidth + 'px';
-}
+        disconnect: function() {
+            UI.closeSettingsMenu();
+            UI.rfb.disconnect();
 
-};
+            $D('noVNC_logo').style.display = "block";
+            UI.connSettingsOpen = false;
+            UI.toggleConnectPanel();
+        },
+
+        displayBlur: function() {
+            UI.rfb.get_keyboard().set_focused(false);
+            UI.rfb.get_mouse().set_focused(false);
+        },
+
+        displayFocus: function() {
+            UI.rfb.get_keyboard().set_focused(true);
+            UI.rfb.get_mouse().set_focused(true);
+        },
+
+        clipClear: function() {
+            $D('noVNC_clipboard_text').value = "";
+            UI.rfb.clipboardPasteFrom("");
+        },
+
+        clipSend: function() {
+            var text = $D('noVNC_clipboard_text').value;
+            Util.Debug(">> UI.clipSend: " + text.substr(0,40) + "...");
+            UI.rfb.clipboardPasteFrom(text);
+            Util.Debug("<< UI.clipSend");
+        },
+
+        // Enable/disable and configure viewport clipping
+        setViewClip: function(clip) {
+            var display;
+            if (UI.rfb) {
+                display = UI.rfb.get_display();
+            } else {
+                return;
+            }
 
+            var cur_clip = display.get_viewport();
+
+            if (typeof(clip) !== 'boolean') {
+                // Use current setting
+                clip = UI.getSetting('clip');
+            }
 
+            if (clip && !cur_clip) {
+                // Turn clipping on
+                UI.updateSetting('clip', true);
+            } else if (!clip && cur_clip) {
+                // Turn clipping off
+                UI.updateSetting('clip', false);
+                display.set_viewport(false);
+                $D('noVNC_canvas').style.position = 'static';
+                display.viewportChange();
+            }
+            if (UI.getSetting('clip')) {
+                // If clipping, update clipping settings
+                $D('noVNC_canvas').style.position = 'absolute';
+                var pos = Util.getPosition($D('noVNC_canvas'));
+                var new_w = window.innerWidth - pos.x;
+                var new_h = window.innerHeight - pos.y;
+                display.set_viewport(true);
+                display.viewportChange(0, 0, new_w, new_h);
+            }
+        },
+
+        // Toggle/set/unset the viewport drag/move button
+        setViewDrag: function(drag) {
+            var vmb = $D('noVNC_view_drag_button');
+            if (!UI.rfb) { return; }
+
+            if (UI.rfb_state === 'normal' &&
+                UI.rfb.get_display().get_viewport()) {
+                vmb.style.display = "inline";
+            } else {
+                vmb.style.display = "none";
+            }
 
+            if (typeof(drag) === "undefined" ||
+                typeof(drag) === "object") {
+                // If not specified, then toggle
+                drag = !UI.rfb.get_viewportDrag();
+            }
+            if (drag) {
+                vmb.className = "noVNC_status_button_selected";
+                UI.rfb.set_viewportDrag(true);
+            } else {
+                vmb.className = "noVNC_status_button";
+                UI.rfb.set_viewportDrag(false);
+            }
+        },
+
+        // On touch devices, show the OS keyboard
+        showKeyboard: function() {
+            var kbi = $D('keyboardinput');
+            var skb = $D('showKeyboard');
+            var l = kbi.value.length;
+            if(UI.keyboardVisible === false) {
+                kbi.focus();
+                try { kbi.setSelectionRange(l, l); } // Move the caret to the end
+                catch (err) {} // setSelectionRange is undefined in Google Chrome
+                UI.keyboardVisible = true;
+                skb.className = "noVNC_status_button_selected";
+            } else if(UI.keyboardVisible === true) {
+                kbi.blur();
+                skb.className = "noVNC_status_button";
+                UI.keyboardVisible = false;
+            }
+        },
+
+        keepKeyboard: function() {
+            clearTimeout(UI.hideKeyboardTimeout);
+            if(UI.keyboardVisible === true) {
+                $D('keyboardinput').focus();
+                $D('showKeyboard').className = "noVNC_status_button_selected";
+            } else if(UI.keyboardVisible === false) {
+                $D('keyboardinput').blur();
+                $D('showKeyboard').className = "noVNC_status_button";
+            }
+        },
+
+        keyboardinputReset: function() {
+            var kbi = $D('keyboardinput');
+            kbi.value = new Array(UI.defaultKeyboardinputLen).join("_");
+            UI.lastKeyboardinput = kbi.value;
+        },
+
+        // When normal keyboard events are left uncought, use the input events from
+        // the keyboardinput element instead and generate the corresponding key events.
+        // This code is required since some browsers on Android are inconsistent in
+        // sending keyCodes in the normal keyboard events when using on screen keyboards.
+        keyInput: function(event) {
+            var newValue = event.target.value;
+            var oldValue = UI.lastKeyboardinput;
+
+            var newLen;
+            try {
+                // Try to check caret position since whitespace at the end
+                // will not be considered by value.length in some browsers
+                newLen = Math.max(event.target.selectionStart, newValue.length);
+            } catch (err) {
+                // selectionStart is undefined in Google Chrome
+                newLen = newValue.length;
+            }
+            var oldLen = oldValue.length;
+
+            var backspaces;
+            var inputs = newLen - oldLen;
+            if (inputs < 0) {
+                backspaces = -inputs;
+            } else {
+                backspaces = 0;
+            }
 
+            // Compare the old string with the new to account for
+            // text-corrections or other input that modify existing text
+            var i;
+            for (i = 0; i < Math.min(oldLen, newLen); i++) {
+                if (newValue.charAt(i) != oldValue.charAt(i)) {
+                    inputs = newLen - i;
+                    backspaces = oldLen - i;
+                    break;
+                }
+            }
+
+            // Send the key events
+            for (i = 0; i < backspaces; i++) {
+                UI.rfb.sendKey(XK_BackSpace);
+            }
+            for (i = newLen - inputs; i < newLen; i++) {
+                UI.rfb.sendKey(newValue.charCodeAt(i));
+            }
+
+            // Control the text content length in the keyboardinput element
+            if (newLen > 2 * UI.defaultKeyboardinputLen) {
+                UI.keyboardinputReset();
+            } else if (newLen < 1) {
+                // There always have to be some text in the keyboardinput
+                // element with which backspace can interact.
+                UI.keyboardinputReset();
+                // This sometimes causes the keyboard to disappear for a second
+                // but it is required for the android keyboard to recognize that
+                // text has been added to the field
+                event.target.blur();
+                // This has to be ran outside of the input handler in order to work
+                setTimeout(function() { UI.keepKeyboard(); }, 0);
+            } else {
+                UI.lastKeyboardinput = newValue;
+            }
+        },
+
+        keyInputBlur: function() {
+            $D('showKeyboard').className = "noVNC_status_button";
+            //Weird bug in iOS if you change keyboardVisible
+            //here it does not actually occur so next time
+            //you click keyboard icon it doesnt work.
+            UI.hideKeyboardTimeout = setTimeout(function() { UI.setKeyboard(); },100);
+        },
+
+        showExtraKeys: function() {
+            UI.keepKeyboard();
+            if(UI.extraKeysVisible === false) {
+                $D('toggleCtrlButton').style.display = "inline";
+                $D('toggleAltButton').style.display = "inline";
+                $D('sendTabButton').style.display = "inline";
+                $D('sendEscButton').style.display = "inline";
+                $D('showExtraKeysButton').className = "noVNC_status_button_selected";
+                UI.extraKeysVisible = true;
+            } else if(UI.extraKeysVisible === true) {
+                $D('toggleCtrlButton').style.display = "";
+                $D('toggleAltButton').style.display = "";
+                $D('sendTabButton').style.display = "";
+                $D('sendEscButton').style.display = "";
+                $D('showExtraKeysButton').className = "noVNC_status_button";
+                UI.extraKeysVisible = false;
+            }
+        },
+
+        toggleCtrl: function() {
+            UI.keepKeyboard();
+            if(UI.ctrlOn === false) {
+                UI.rfb.sendKey(XK_Control_L, true);
+                $D('toggleCtrlButton').className = "noVNC_status_button_selected";
+                UI.ctrlOn = true;
+            } else if(UI.ctrlOn === true) {
+                UI.rfb.sendKey(XK_Control_L, false);
+                $D('toggleCtrlButton').className = "noVNC_status_button";
+                UI.ctrlOn = false;
+            }
+        },
+
+        toggleAlt: function() {
+            UI.keepKeyboard();
+            if(UI.altOn === false) {
+                UI.rfb.sendKey(XK_Alt_L, true);
+                $D('toggleAltButton').className = "noVNC_status_button_selected";
+                UI.altOn = true;
+            } else if(UI.altOn === true) {
+                UI.rfb.sendKey(XK_Alt_L, false);
+                $D('toggleAltButton').className = "noVNC_status_button";
+                UI.altOn = false;
+            }
+        },
+
+        sendTab: function() {
+            UI.keepKeyboard();
+            UI.rfb.sendKey(XK_Tab);
+        },
+
+        sendEsc: function() {
+            UI.keepKeyboard();
+            UI.rfb.sendKey(XK_Escape);
+        },
+
+        setKeyboard: function() {
+            UI.keyboardVisible = false;
+        },
+
+        // iOS < Version 5 does not support position fixed. Javascript workaround:
+        setOnscroll: function() {
+            window.onscroll = function() {
+                UI.setBarPosition();
+            };
+        },
+
+        setResize: function () {
+            window.onResize = function() {
+                UI.setBarPosition();
+            };
+        },
+
+        //Helper to add options to dropdown.
+        addOption: function(selectbox, text, value) {
+            var optn = document.createElement("OPTION");
+            optn.text = text;
+            optn.value = value;
+            selectbox.options.add(optn);
+        },
+
+        setBarPosition: function() {
+            $D('noVNC-control-bar').style.top = (window.pageYOffset) + 'px';
+            $D('noVNC_mobile_buttons').style.left = (window.pageXOffset) + 'px';
+
+            var vncwidth = $D('noVNC_screen').style.offsetWidth;
+            $D('noVNC-control-bar').style.width = vncwidth + 'px';
+        }
+
+    };
+})();

+ 375 - 197
include/util.js

@@ -6,9 +6,8 @@
  * See README.md for usage and integration instructions.
  */
 
-"use strict";
-/*jslint bitwise: false, white: false */
-/*global window, console, document, navigator, ActiveXObject */
+/* jshint white: false, nonstandard: true */
+/*global window, console, document, navigator, ActiveXObject, INCLUDE_URI */
 
 // Globals defined here
 var Util = {};
@@ -19,88 +18,161 @@ var Util = {};
  */
 
 Array.prototype.push8 = function (num) {
+    "use strict";
     this.push(num & 0xFF);
 };
 
 Array.prototype.push16 = function (num) {
+    "use strict";
     this.push((num >> 8) & 0xFF,
-              (num     ) & 0xFF  );
+              num & 0xFF);
 };
 Array.prototype.push32 = function (num) {
+    "use strict";
     this.push((num >> 24) & 0xFF,
               (num >> 16) & 0xFF,
               (num >>  8) & 0xFF,
-              (num      ) & 0xFF  );
+              num & 0xFF);
 };
 
 // IE does not support map (even in IE9)
 //This prototype is provided by the Mozilla foundation and
 //is distributed under the MIT license.
 //http://www.ibiblio.org/pub/Linux/LICENSES/mit.license
-if (!Array.prototype.map)
-{
-  Array.prototype.map = function(fun /*, thisp*/)
-  {
-    var len = this.length;
-    if (typeof fun != "function")
-      throw new TypeError();
-
-    var res = new Array(len);
-    var thisp = arguments[1];
-    for (var i = 0; i < len; i++)
-    {
-      if (i in this)
-        res[i] = fun.call(thisp, this[i], i, this);
-    }
+if (!Array.prototype.map) {
+    Array.prototype.map = function (fun /*, thisp*/) {
+        "use strict";
+        var len = this.length;
+        if (typeof fun != "function") {
+            throw new TypeError();
+        }
+
+        var res = new Array(len);
+        var thisp = arguments[1];
+        for (var i = 0; i < len; i++) {
+            if (i in this) {
+                res[i] = fun.call(thisp, this[i], i, this);
+            }
+        }
 
-    return res;
-  };
+        return res;
+    };
 }
 
 // IE <9 does not support indexOf
 //This prototype is provided by the Mozilla foundation and
 //is distributed under the MIT license.
 //http://www.ibiblio.org/pub/Linux/LICENSES/mit.license
-if (!Array.prototype.indexOf)
-{
-  Array.prototype.indexOf = function(elt /*, from*/)
-  {
-    var len = this.length >>> 0;
-
-    var from = Number(arguments[1]) || 0;
-    from = (from < 0)
-         ? Math.ceil(from)
-         : Math.floor(from);
-    if (from < 0)
-      from += len;
-
-    for (; from < len; from++)
-    {
-      if (from in this &&
-          this[from] === elt)
-        return from;
-    }
-    return -1;
-  };
+if (!Array.prototype.indexOf) {
+    Array.prototype.indexOf = function (elt /*, from*/) {
+        "use strict";
+        var len = this.length >>> 0;
+
+        var from = Number(arguments[1]) || 0;
+        from = (from < 0) ? Math.ceil(from) : Math.floor(from);
+        if (from < 0) {
+            from += len;
+        }
+
+        for (; from < len; from++) {
+            if (from in this &&
+                    this[from] === elt) {
+                return from;
+            }
+        }
+        return -1;
+    };
 }
 
+// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
+if (!Object.keys) {
+    Object.keys = (function () {
+        'use strict';
+        var hasOwnProperty = Object.prototype.hasOwnProperty,
+            hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
+            dontEnums = [
+                'toString',
+                'toLocaleString',
+                'valueOf',
+                'hasOwnProperty',
+                'isPrototypeOf',
+                'propertyIsEnumerable',
+                'constructor'
+            ],
+            dontEnumsLength = dontEnums.length;
+
+        return function (obj) {
+            if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) {
+                throw new TypeError('Object.keys called on non-object');
+            }
+
+            var result = [], prop, i;
+
+            for (prop in obj) {
+                if (hasOwnProperty.call(obj, prop)) {
+                    result.push(prop);
+                }
+            }
+
+            if (hasDontEnumBug) {
+                for (i = 0; i < dontEnumsLength; i++) {
+                    if (hasOwnProperty.call(obj, dontEnums[i])) {
+                        result.push(dontEnums[i]);
+                    }
+                }
+            }
+            return result;
+        };
+    })();
+}
+
+// PhantomJS 1.x doesn't support bind,
+// so leave this in until PhantomJS 2.0 is released
+//This prototype is provided by the Mozilla foundation and
+//is distributed under the MIT license.
+//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license
+if (!Function.prototype.bind) {
+    Function.prototype.bind = function (oThis) {
+        if (typeof this !== "function") {
+            // closest thing possible to the ECMAScript 5
+            // internal IsCallable function
+            throw new TypeError("Function.prototype.bind - " +
+                                "what is trying to be bound is not callable");
+        }
+
+        var aArgs = Array.prototype.slice.call(arguments, 1),
+                fToBind = this,
+                fNOP = function () {},
+                fBound = function () {
+                    return fToBind.apply(this instanceof fNOP && oThis ? this
+                                                                       : oThis,
+                                         aArgs.concat(Array.prototype.slice.call(arguments)));
+                };
 
-// 
+        fNOP.prototype = this.prototype;
+        fBound.prototype = new fNOP();
+
+        return fBound;
+    };
+}
+
+//
 // requestAnimationFrame shim with setTimeout fallback
 //
 
-window.requestAnimFrame = (function(){
-    return  window.requestAnimationFrame       || 
-            window.webkitRequestAnimationFrame || 
-            window.mozRequestAnimationFrame    || 
-            window.oRequestAnimationFrame      || 
-            window.msRequestAnimationFrame     || 
-            function(callback){
+window.requestAnimFrame = (function () {
+    "use strict";
+    return  window.requestAnimationFrame       ||
+            window.webkitRequestAnimationFrame ||
+            window.mozRequestAnimationFrame    ||
+            window.oRequestAnimationFrame      ||
+            window.msRequestAnimationFrame     ||
+            function (callback) {
                 window.setTimeout(callback, 1000 / 60);
             };
 })();
 
-/* 
+/*
  * ------------------------------------------------------
  * Namespaced in Util
  * ------------------------------------------------------
@@ -112,6 +184,7 @@ window.requestAnimFrame = (function(){
 
 Util._log_level = 'warn';
 Util.init_logging = function (level) {
+    "use strict";
     if (typeof level === 'undefined') {
         level = Util._log_level;
     } else {
@@ -122,26 +195,34 @@ Util.init_logging = function (level) {
             window.console = {
                 'log'  : window.opera.postError,
                 'warn' : window.opera.postError,
-                'error': window.opera.postError };
+                'error': window.opera.postError
+            };
         } else {
             window.console = {
-                'log'  : function(m) {},
-                'warn' : function(m) {},
-                'error': function(m) {}};
+                'log'  : function (m) {},
+                'warn' : function (m) {},
+                'error': function (m) {}
+            };
         }
     }
 
     Util.Debug = Util.Info = Util.Warn = Util.Error = function (msg) {};
+    /* jshint -W086 */
     switch (level) {
-        case 'debug': Util.Debug = function (msg) { console.log(msg); };
-        case 'info':  Util.Info  = function (msg) { console.log(msg); };
-        case 'warn':  Util.Warn  = function (msg) { console.warn(msg); };
-        case 'error': Util.Error = function (msg) { console.error(msg); };
+        case 'debug':
+            Util.Debug = function (msg) { console.log(msg); };
+        case 'info':
+            Util.Info  = function (msg) { console.log(msg); };
+        case 'warn':
+            Util.Warn  = function (msg) { console.warn(msg); };
+        case 'error':
+            Util.Error = function (msg) { console.error(msg); };
         case 'none':
             break;
         default:
-            throw("invalid logging type '" + level + "'");
+            throw new Error("invalid logging type '" + level + "'");
     }
+    /* jshint +W086 */
 };
 Util.get_logging = function () {
     return Util._log_level;
@@ -149,93 +230,133 @@ Util.get_logging = function () {
 // Initialize logging level
 Util.init_logging();
 
+Util.make_property = function (proto, name, mode, type) {
+    "use strict";
 
-// Set configuration default for Crockford style function namespaces
-Util.conf_default = function(cfg, api, defaults, v, mode, type, defval, desc) {
-    var getter, setter;
+    var getter;
+    if (type === 'arr') {
+        getter = function (idx) {
+            if (typeof idx !== 'undefined') {
+                return this['_' + name][idx];
+            } else {
+                return this['_' + name];
+            }
+        };
+    } else {
+        getter = function () {
+            return this['_' + name];
+        };
+    }
 
-    // Default getter function
-    getter = function (idx) {
-        if ((type in {'arr':1, 'array':1}) &&
-            (typeof idx !== 'undefined')) {
-            return cfg[v][idx];
+    var make_setter = function (process_val) {
+        if (process_val) {
+            return function (val, idx) {
+                if (typeof idx !== 'undefined') {
+                    this['_' + name][idx] = process_val(val);
+                } else {
+                    this['_' + name] = process_val(val);
+                }
+            };
         } else {
-            return cfg[v];
+            return function (val, idx) {
+                if (typeof idx !== 'undefined') {
+                    this['_' + name][idx] = val;
+                } else {
+                    this['_' + name] = val;
+                }
+            };
         }
     };
 
-    // Default setter function
-    setter = function (val, idx) {
-        if (type in {'boolean':1, 'bool':1}) {
-            if ((!val) || (val in {'0':1, 'no':1, 'false':1})) {
-                val = false;
+    var setter;
+    if (type === 'bool') {
+        setter = make_setter(function (val) {
+            if (!val || (val in {'0': 1, 'no': 1, 'false': 1})) {
+                return false;
             } else {
-                val = true;
+                return true;
             }
-        } else if (type in {'integer':1, 'int':1}) {
-            val = parseInt(val, 10);
-        } else if (type === 'str') {
-            val = String(val);
-        } else if (type === 'func') {
+        });
+    } else if (type === 'int') {
+        setter = make_setter(function (val) { return parseInt(val, 10); });
+    } else if (type === 'float') {
+        setter = make_setter(parseFloat);
+    } else if (type === 'str') {
+        setter = make_setter(String);
+    } else if (type === 'func') {
+        setter = make_setter(function (val) {
             if (!val) {
-                val = function () {};
+                return function () {};
+            } else {
+                return val;
             }
-        }
-        if (typeof idx !== 'undefined') {
-            cfg[v][idx] = val;
-        } else {
-            cfg[v] = val;
-        }
-    };
-
-    // Set the description
-    api[v + '_description'] = desc;
+        });
+    } else if (type === 'arr' || type === 'dom' || type == 'raw') {
+        setter = make_setter();
+    } else {
+        throw new Error('Unknown property type ' + type);  // some sanity checking
+    }
 
-    // Set the getter function
-    if (typeof api['get_' + v] === 'undefined') {
-        api['get_' + v] = getter;
+    // set the getter
+    if (typeof proto['get_' + name] === 'undefined') {
+        proto['get_' + name] = getter;
     }
 
-    // Set the setter function with extra sanity checks
-    if (typeof api['set_' + v] === 'undefined') {
-        api['set_' + v] = function (val, idx) {
-            if (mode in {'RO':1, 'ro':1}) {
-                throw(v + " is read-only");
-            } else if ((mode in {'WO':1, 'wo':1}) &&
-                       (typeof cfg[v] !== 'undefined')) {
-                throw(v + " can only be set once");
-            }
-            setter(val, idx);
-        };
+    // set the setter if needed
+    if (typeof proto['set_' + name] === 'undefined') {
+        if (mode === 'rw') {
+            proto['set_' + name] = setter;
+        } else if (mode === 'wo') {
+            proto['set_' + name] = function (val, idx) {
+                if (typeof this['_' + name] !== 'undefined') {
+                    throw new Error(name + " can only be set once");
+                }
+                setter.call(this, val, idx);
+            };
+        }
     }
 
-    // Set the default value
-    if (typeof defaults[v] !== 'undefined') {
-        defval = defaults[v];
-    } else if ((type in {'arr':1, 'array':1}) &&
-            (! (defval instanceof Array))) {
-        defval = [];
+    // make a special setter that we can use in set defaults
+    proto['_raw_set_' + name] = function (val, idx) {
+        setter.call(this, val, idx);
+        //delete this['_init_set_' + name];  // remove it after use
+    };
+};
+
+Util.make_properties = function (constructor, arr) {
+    "use strict";
+    for (var i = 0; i < arr.length; i++) {
+        Util.make_property(constructor.prototype, arr[i][0], arr[i][1], arr[i][2]);
     }
-    // Coerce existing setting to the right type
-    //Util.Debug("v: " + v + ", defval: " + defval + ", defaults[v]: " + defaults[v]);
-    setter(defval);
 };
 
-// Set group of configuration defaults
-Util.conf_defaults = function(cfg, api, defaults, arr) {
+Util.set_defaults = function (obj, conf, defaults) {
+    var defaults_keys = Object.keys(defaults);
+    var conf_keys = Object.keys(conf);
+    var keys_obj = {};
     var i;
-    for (i = 0; i < arr.length; i++) {
-        Util.conf_default(cfg, api, defaults, arr[i][0], arr[i][1],
-                arr[i][2], arr[i][3], arr[i][4]);
+    for (i = 0; i < defaults_keys.length; i++) { keys_obj[defaults_keys[i]] = 1; }
+    for (i = 0; i < conf_keys.length; i++) { keys_obj[conf_keys[i]] = 1; }
+    var keys = Object.keys(keys_obj);
+
+    for (i = 0; i < keys.length; i++) {
+        var setter = obj['_raw_set_' + keys[i]];
+
+        if (conf[keys[i]]) {
+            setter.call(obj, conf[keys[i]]);
+        } else {
+            setter.call(obj, defaults[keys[i]]);
+        }
     }
 };
 
 /*
  * Decode from UTF-8
  */
-Util.decodeUTF8 = function(utf8string) {
+Util.decodeUTF8 = function (utf8string) {
+    "use strict";
     return decodeURIComponent(escape(utf8string));
-}
+};
 
 
 
@@ -250,42 +371,46 @@ Util.decodeUTF8 = function(utf8string) {
 // Handles the case where load_scripts is invoked from a script that
 // itself is loaded via load_scripts. Once all scripts are loaded the
 // window.onscriptsloaded handler is called (if set).
-Util.get_include_uri = function() {
+Util.get_include_uri = function () {
     return (typeof INCLUDE_URI !== "undefined") ? INCLUDE_URI : "include/";
-}
+};
 Util._loading_scripts = [];
 Util._pending_scripts = [];
-Util.load_scripts = function(files) {
+Util.load_scripts = function (files) {
+    "use strict";
     var head = document.getElementsByTagName('head')[0], script,
         ls = Util._loading_scripts, ps = Util._pending_scripts;
-    for (var f=0; f<files.length; f++) {
+
+    var loadFunc = function (e) {
+        while (ls.length > 0 && (ls[0].readyState === 'loaded' ||
+                                 ls[0].readyState === 'complete')) {
+            // For IE, append the script to trigger execution
+            var s = ls.shift();
+            //console.log("loaded script: " + s.src);
+            head.appendChild(s);
+        }
+        if (!this.readyState ||
+            (Util.Engine.presto && this.readyState === 'loaded') ||
+            this.readyState === 'complete') {
+            if (ps.indexOf(this) >= 0) {
+                this.onload = this.onreadystatechange = null;
+                //console.log("completed script: " + this.src);
+                ps.splice(ps.indexOf(this), 1);
+
+                // Call window.onscriptsload after last script loads
+                if (ps.length === 0 && window.onscriptsload) {
+                    window.onscriptsload();
+                }
+            }
+        }
+    };
+
+    for (var f = 0; f < files.length; f++) {
         script = document.createElement('script');
         script.type = 'text/javascript';
         script.src = Util.get_include_uri() + files[f];
         //console.log("loading script: " + script.src);
-        script.onload = script.onreadystatechange = function (e) {
-            while (ls.length > 0 && (ls[0].readyState === 'loaded' ||
-                                     ls[0].readyState === 'complete')) {
-                // For IE, append the script to trigger execution
-                var s = ls.shift();
-                //console.log("loaded script: " + s.src);
-                head.appendChild(s);
-            }
-            if (!this.readyState ||
-                (Util.Engine.presto && this.readyState === 'loaded') ||
-                this.readyState === 'complete') {
-                if (ps.indexOf(this) >= 0) {
-                    this.onload = this.onreadystatechange = null;
-                    //console.log("completed script: " + this.src);
-                    ps.splice(ps.indexOf(this), 1);
-
-                    // Call window.onscriptsload after last script loads
-                    if (ps.length === 0 && window.onscriptsload) {
-                        window.onscriptsload();
-                    }
-                }
-            }
-        };
+        script.onload = script.onreadystatechange = loadFunc;
         // In-order script execution tricks
         if (Util.Engine.trident) {
             // For IE wait until readyState is 'loaded' before
@@ -300,20 +425,22 @@ Util.load_scripts = function(files) {
         }
         ps.push(script);
     }
-}
+};
 
 
 // Get DOM element position on page
 //  This solution is based based on http://www.greywyvern.com/?post=331
 //  Thanks to Brian Huisman AKA GreyWyvern!
-Util.getPosition = (function() {
+Util.getPosition = (function () {
+    "use strict";
     function getStyle(obj, styleProp) {
+        var y;
         if (obj.currentStyle) {
-            var y = obj.currentStyle[styleProp];
+            y = obj.currentStyle[styleProp];
         } else if (window.getComputedStyle)
-            var y = window.getComputedStyle(obj, null)[styleProp];
+            y = window.getComputedStyle(obj, null)[styleProp];
         return y;
-    };
+    }
 
     function scrollDist() {
         var myScrollTop = 0, myScrollLeft = 0;
@@ -342,7 +469,7 @@ Util.getPosition = (function() {
         }
 
         return [myScrollLeft, myScrollTop];
-    };
+    }
 
     return function (obj) {
         var curleft = 0, curtop = 0, scr = obj, fixed = false;
@@ -362,7 +489,7 @@ Util.getPosition = (function() {
         do {
             curleft += obj.offsetLeft;
             curtop += obj.offsetTop;
-        } while (obj = obj.offsetParent);
+        } while ((obj = obj.offsetParent));
 
         return {'x': curleft, 'y': curtop};
     };
@@ -371,6 +498,7 @@ Util.getPosition = (function() {
 
 // Get mouse event position in DOM element
 Util.getEventPosition = function (e, obj, scale) {
+    "use strict";
     var evt, docX, docY, pos;
     //if (!e) evt = window.event;
     evt = (e ? e : window.event);
@@ -390,38 +518,41 @@ Util.getEventPosition = function (e, obj, scale) {
     }
     var realx = docX - pos.x;
     var realy = docY - pos.y;
-    var x = Math.max(Math.min(realx, obj.width-1), 0);
-    var y = Math.max(Math.min(realy, obj.height-1), 0);
+    var x = Math.max(Math.min(realx, obj.width - 1), 0);
+    var y = Math.max(Math.min(realy, obj.height - 1), 0);
     return {'x': x / scale, 'y': y / scale, 'realx': realx / scale, 'realy': realy / scale};
 };
 
 
 // Event registration. Based on: http://www.scottandrew.com/weblog/articles/cbs-events
-Util.addEvent = function (obj, evType, fn){
-    if (obj.attachEvent){
-        var r = obj.attachEvent("on"+evType, fn);
+Util.addEvent = function (obj, evType, fn) {
+    "use strict";
+    if (obj.attachEvent) {
+        var r = obj.attachEvent("on" + evType, fn);
         return r;
-    } else if (obj.addEventListener){
-        obj.addEventListener(evType, fn, false); 
+    } else if (obj.addEventListener) {
+        obj.addEventListener(evType, fn, false);
         return true;
     } else {
-        throw("Handler could not be attached");
+        throw new Error("Handler could not be attached");
     }
 };
 
-Util.removeEvent = function(obj, evType, fn){
-    if (obj.detachEvent){
-        var r = obj.detachEvent("on"+evType, fn);
+Util.removeEvent = function (obj, evType, fn) {
+    "use strict";
+    if (obj.detachEvent) {
+        var r = obj.detachEvent("on" + evType, fn);
         return r;
-    } else if (obj.removeEventListener){
+    } else if (obj.removeEventListener) {
         obj.removeEventListener(evType, fn, false);
         return true;
     } else {
-        throw("Handler could not be removed");
+        throw new Error("Handler could not be removed");
     }
 };
 
-Util.stopEvent = function(e) {
+Util.stopEvent = function (e) {
+    "use strict";
     if (e.stopPropagation) { e.stopPropagation(); }
     else                   { e.cancelBubble = true; }
 
@@ -433,41 +564,88 @@ Util.stopEvent = function(e) {
 // Set browser engine versions. Based on mootools.
 Util.Features = {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)};
 
-Util.Engine = {
-    // Version detection break in Opera 11.60 (errors on arguments.callee.caller reference)
-    //'presto': (function() {
-    //         return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()),
-    'presto': (function() { return (!window.opera) ? false : true; }()),
-
-    'trident': (function() {
-            return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4); }()),
-    'webkit': (function() {
-            try { return (navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); } catch (e) { return false; } }()),
-    //'webkit': (function() {
-    //        return ((typeof navigator.taintEnabled !== "unknown") && navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); }()),
-    'gecko': (function() {
-            return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19 : 18); }())
-};
-if (Util.Engine.webkit) {
-    // Extract actual webkit version if available
-    Util.Engine.webkit = (function(v) {
-            var re = new RegExp('WebKit/([0-9\.]*) ');
-            v = (navigator.userAgent.match(re) || ['', v])[1];
-            return parseFloat(v, 10);
-        })(Util.Engine.webkit);
-}
+(function () {
+    "use strict";
+    // 'presto': (function () { return (!window.opera) ? false : true; }()),
+    var detectPresto = function () {
+        return !!window.opera;
+    };
+
+    // 'trident': (function () { return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4);
+    var detectTrident = function () {
+        if (!window.ActiveXObject) {
+            return false;
+        } else {
+            if (window.XMLHttpRequest) {
+                return (document.querySelectorAll) ? 6 : 5;
+            } else {
+                return 4;
+            }
+        }
+    };
+
+    // 'webkit': (function () { try { return (navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); } catch (e) { return false; } }()),
+    var detectInitialWebkit = function () {
+        try {
+            if (navigator.taintEnabled) {
+                return false;
+            } else {
+                if (Util.Features.xpath) {
+                    return (Util.Features.query) ? 525 : 420;
+                } else {
+                    return 419;
+                }
+            }
+        } catch (e) {
+            return false;
+        }
+    };
+
+    var detectActualWebkit = function (initial_ver) {
+        var re = /WebKit\/([0-9\.]*) /;
+        var str_ver = (navigator.userAgent.match(re) || ['', initial_ver])[1];
+        return parseFloat(str_ver, 10);
+    };
+
+    // 'gecko': (function () { return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19ssName) ? 19 : 18 : 18); }())
+    var detectGecko = function () {
+        /* jshint -W041 */
+        if (!document.getBoxObjectFor && window.mozInnerScreenX == null) {
+            return false;
+        } else {
+            return (document.getElementsByClassName) ? 19 : 18;
+        }
+        /* jshint +W041 */
+    };
+
+    Util.Engine = {
+        // Version detection break in Opera 11.60 (errors on arguments.callee.caller reference)
+        //'presto': (function() {
+        //         return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()),
+        'presto': detectPresto(),
+        'trident': detectTrident(),
+        'webkit': detectInitialWebkit(),
+        'gecko': detectGecko(),
+    };
+
+    if (Util.Engine.webkit) {
+        // Extract actual webkit version if available
+        Util.Engine.webkit = detectActualWebkit(Util.Engine.webkit);
+    }
+})();
 
-Util.Flash = (function(){
+Util.Flash = (function () {
+    "use strict";
     var v, version;
     try {
         v = navigator.plugins['Shockwave Flash'].description;
-    } catch(err1) {
+    } catch (err1) {
         try {
             v = new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version');
-        } catch(err2) {
+        } catch (err2) {
             v = '0 r0';
         }
     }
     version = v.match(/\d+/g);
     return {version: parseInt(version[0] || 0 + '.' + version[1], 10) || 0, build: parseInt(version[2], 10) || 0};
-}()); 
+}());

+ 309 - 349
include/websock.js

@@ -14,7 +14,7 @@
  * read binary data off of the receive queue.
  */
 
-/*jslint browser: true, bitwise: false, plusplus: false */
+/*jslint browser: true, bitwise: true */
 /*global Util, Base64 */
 
 
@@ -43,382 +43,342 @@ if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) {
         }
         Util.load_scripts(["web-socket-js/swfobject.js",
                            "web-socket-js/web_socket.js"]);
-    }());
+    })();
 }
 
 
 function Websock() {
-"use strict";
-
-var api = {},         // Public API
-    websocket = null, // WebSocket object
-    mode = 'base64',  // Current WebSocket mode: 'binary', 'base64'
-    rQ = [],          // Receive queue
-    rQi = 0,          // Receive queue index
-    rQmax = 10000,    // Max receive queue size before compacting
-    sQ = [],          // Send queue
-
-    eventHandlers = {
-        'message' : function() {},
-        'open'    : function() {},
-        'close'   : function() {},
-        'error'   : function() {}
-    },
-
-    test_mode = false;
-
-
-//
-// Queue public functions
-//
-
-function get_sQ() {
-    return sQ;
+    "use strict";
+
+    this._websocket = null;  // WebSocket object
+    this._rQ = [];           // Receive queue
+    this._rQi = 0;           // Receive queue index
+    this._rQmax = 10000;     // Max receive queue size before compacting
+    this._sQ = [];           // Send queue
+
+    this._mode = 'base64';    // Current WebSocket mode: 'binary', 'base64'
+    this.maxBufferedAmount = 200;
+
+    this._eventHandlers = {
+        'message': function () {},
+        'open': function () {},
+        'close': function () {},
+        'error': function () {}
+    };
 }
 
-function get_rQ() {
-    return rQ;
-}
-function get_rQi() {
-    return rQi;
-}
-function set_rQi(val) {
-    rQi = val;
-}
+(function () {
+    "use strict";
+    Websock.prototype = {
+        // Getters and Setters
+        get_sQ: function () {
+            return this._sQ;
+        },
+
+        get_rQ: function () {
+            return this._rQ;
+        },
+
+        get_rQi: function () {
+            return this._rQi;
+        },
+
+        set_rQi: function (val) {
+            this._rQi = val;
+        },
+
+        // Receive Queue
+        rQlen: function () {
+            return this._rQ.length - this._rQi;
+        },
+
+        rQpeek8: function () {
+            return this._rQ[this._rQi];
+        },
+
+        rQshift8: function () {
+            return this._rQ[this._rQi++];
+        },
+
+        rQskip8: function () {
+            this._rQi++;
+        },
+
+        rQskipBytes: function (num) {
+            this._rQi += num;
+        },
+
+        rQunshift8: function (num) {
+            if (this._rQi === 0) {
+                this._rQ.unshift(num);
+            } else {
+                this._rQi--;
+                this._rQ[this._rQi] = num;
+            }
+        },
+
+        rQshift16: function () {
+            return (this._rQ[this._rQi++] << 8) +
+                   this._rQ[this._rQi++];
+        },
+
+        rQshift32: function () {
+            return (this._rQ[this._rQi++] << 24) +
+                   (this._rQ[this._rQi++] << 16) +
+                   (this._rQ[this._rQi++] << 8) +
+                   this._rQ[this._rQi++];
+        },
+
+        rQshiftStr: function (len) {
+            if (typeof(len) === 'undefined') { len = this.rQlen(); }
+            var arr = this._rQ.slice(this._rQi, this._rQi + len);
+            this._rQi += len;
+            return String.fromCharCode.apply(null, arr);
+        },
+
+        rQshiftBytes: function (len) {
+            if (typeof(len) === 'undefined') { len = this.rQlen(); }
+            this._rQi += len;
+            return this._rQ.slice(this._rQi - len, this._rQi);
+        },
+
+        rQslice: function (start, end) {
+            if (end) {
+                return this._rQ.slice(this._rQi + start, this._rQi + end);
+            } else {
+                return this._rQ.slice(this._rQi + start);
+            }
+        },
+
+        // Check to see if we must wait for 'num' bytes (default to FBU.bytes)
+        // to be available in the receive queue. Return true if we need to
+        // wait (and possibly print a debug message), otherwise false.
+        rQwait: function (msg, num, goback) {
+            var rQlen = this._rQ.length - this._rQi; // Skip rQlen() function call
+            if (rQlen < num) {
+                if (goback) {
+                    if (this._rQi < goback) {
+                        throw new Error("rQwait cannot backup " + goback + " bytes");
+                    }
+                    this._rQi -= goback;
+                }
+                return true; // true means need more data
+            }
+            return false;
+        },
 
-function rQlen() {
-    return rQ.length - rQi;
-}
+        // Send Queue
 
-function rQpeek8() {
-    return (rQ[rQi]      );
-}
-function rQshift8() {
-    return (rQ[rQi++]      );
-}
-function rQunshift8(num) {
-    if (rQi === 0) {
-        rQ.unshift(num);
-    } else {
-        rQi -= 1;
-        rQ[rQi] = num;
-    }
+        flush: function () {
+            if (this._websocket.bufferedAmount !== 0) {
+                Util.Debug("bufferedAmount: " + this._websocket.bufferedAmount);
+            }
 
-}
-function rQshift16() {
-    return (rQ[rQi++] <<  8) +
-           (rQ[rQi++]      );
-}
-function rQshift32() {
-    return (rQ[rQi++] << 24) +
-           (rQ[rQi++] << 16) +
-           (rQ[rQi++] <<  8) +
-           (rQ[rQi++]      );
-}
-function rQshiftStr(len) {
-    if (typeof(len) === 'undefined') { len = rQlen(); }
-    var arr = rQ.slice(rQi, rQi + len);
-    rQi += len;
-    return String.fromCharCode.apply(null, arr);
-}
-function rQshiftBytes(len) {
-    if (typeof(len) === 'undefined') { len = rQlen(); }
-    rQi += len;
-    return rQ.slice(rQi-len, rQi);
-}
+            if (this._websocket.bufferedAmount < this.maxBufferedAmount) {
+                if (this._sQ.length > 0) {
+                    this._websocket.send(this._encode_message());
+                    this._sQ = [];
+                }
 
-function rQslice(start, end) {
-    if (end) {
-        return rQ.slice(rQi + start, rQi + end);
-    } else {
-        return rQ.slice(rQi + start);
-    }
-}
+                return true;
+            } else {
+                Util.Info("Delaying send, bufferedAmount: " +
+                        this._websocket.bufferedAmount);
+                return false;
+            }
+        },
+
+        send: function (arr) {
+           this._sQ = this._sQ.concat(arr);
+           return this.flush();
+        },
+
+        send_string: function (str) {
+            this.send(str.split('').map(function (chr) {
+                return chr.charCodeAt(0);
+            }));
+        },
+
+        // Event Handlers
+        on: function (evt, handler) {
+            this._eventHandlers[evt] = handler;
+        },
+
+        init: function (protocols, ws_schema) {
+            this._rQ = [];
+            this._rQi = 0;
+            this._sQ = [];
+            this._websocket = null;
+
+            // Check for full typed array support
+            var bt = false;
+            if (('Uint8Array' in window) &&
+                    ('set' in Uint8Array.prototype)) {
+                bt = true;
+            }
 
-// Check to see if we must wait for 'num' bytes (default to FBU.bytes)
-// to be available in the receive queue. Return true if we need to
-// wait (and possibly print a debug message), otherwise false.
-function rQwait(msg, num, goback) {
-    var rQlen = rQ.length - rQi; // Skip rQlen() function call
-    if (rQlen < num) {
-        if (goback) {
-            if (rQi < goback) {
-                throw("rQwait cannot backup " + goback + " bytes");
+            // Check for full binary type support in WebSockets
+            // Inspired by:
+            // https://github.com/Modernizr/Modernizr/issues/370
+            // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js
+            var wsbt = false;
+            try {
+                if (bt && ('binaryType' in WebSocket.prototype ||
+                           !!(new WebSocket(ws_schema + '://.').binaryType))) {
+                    Util.Info("Detected binaryType support in WebSockets");
+                    wsbt = true;
+                }
+            } catch (exc) {
+                // Just ignore failed test localhost connection
             }
-            rQi -= goback;
-        }
-        //Util.Debug("   waiting for " + (num-rQlen) +
-        //           " " + msg + " byte(s)");
-        return true;  // true means need more data
-    }
-    return false;
-}
 
-//
-// Private utility routines
-//
-
-function encode_message() {
-    if (mode === 'binary') {
-        // Put in a binary arraybuffer
-        return (new Uint8Array(sQ)).buffer;
-    } else {
-        // base64 encode
-        return Base64.encode(sQ);
-    }
-}
+            // Default protocols if not specified
+            if (typeof(protocols) === "undefined") {
+                if (wsbt) {
+                    protocols = ['binary', 'base64'];
+                } else {
+                    protocols = 'base64';
+                }
+            }
 
-function decode_message(data) {
-    //Util.Debug(">> decode_message: " + data);
-    if (mode === 'binary') {
-        // push arraybuffer values onto the end
-        var u8 = new Uint8Array(data);
-        for (var i = 0; i < u8.length; i++) {
-            rQ.push(u8[i]);
-        }
-    } else {
-        // base64 decode and concat to the end
-        rQ = rQ.concat(Base64.decode(data, 0));
-    }
-    //Util.Debug(">> decode_message, rQ: " + rQ);
-}
+            if (!wsbt) {
+                if (protocols === 'binary') {
+                    throw new Error('WebSocket binary sub-protocol requested but not supported');
+                }
 
+                if (typeof(protocols) === 'object') {
+                    var new_protocols = [];
+
+                    for (var i = 0; i < protocols.length; i++) {
+                        if (protocols[i] === 'binary') {
+                            Util.Error('Skipping unsupported WebSocket binary sub-protocol');
+                        } else {
+                            new_protocols.push(protocols[i]);
+                        }
+                    }
+
+                    if (new_protocols.length > 0) {
+                        protocols = new_protocols;
+                    } else {
+                        throw new Error("Only WebSocket binary sub-protocol was requested and is not supported.");
+                    }
+                }
+            }
 
-//
-// Public Send functions
-//
-
-function flush() {
-    if (websocket.bufferedAmount !== 0) {
-        Util.Debug("bufferedAmount: " + websocket.bufferedAmount);
-    }
-    if (websocket.bufferedAmount < api.maxBufferedAmount) {
-        //Util.Debug("arr: " + arr);
-        //Util.Debug("sQ: " + sQ);
-        if (sQ.length > 0) {
-            websocket.send(encode_message(sQ));
-            sQ = [];
-        }
-        return true;
-    } else {
-        Util.Info("Delaying send, bufferedAmount: " +
-                websocket.bufferedAmount);
-        return false;
-    }
-}
+            return protocols;
+        },
 
-// overridable for testing
-function send(arr) {
-    //Util.Debug(">> send_array: " + arr);
-    sQ = sQ.concat(arr);
-    return flush();
-}
+        open: function (uri, protocols) {
+            var ws_schema = uri.match(/^([a-z]+):\/\//)[1];
+            protocols = this.init(protocols, ws_schema);
 
-function send_string(str) {
-    //Util.Debug(">> send_string: " + str);
-    api.send(str.split('').map(
-        function (chr) { return chr.charCodeAt(0); } ) );
-}
+            this._websocket = new WebSocket(uri, protocols);
 
-//
-// Other public functions
-
-function recv_message(e) {
-    //Util.Debug(">> recv_message: " + e.data.length);
-
-    try {
-        decode_message(e.data);
-        if (rQlen() > 0) {
-            eventHandlers.message();
-            // Compact the receive queue
-            if (rQ.length > rQmax) {
-                //Util.Debug("Compacting receive queue");
-                rQ = rQ.slice(rQi);
-                rQi = 0;
+            if (protocols.indexOf('binary') >= 0) {
+                this._websocket.binaryType = 'arraybuffer';
             }
-        } else {
-            Util.Debug("Ignoring empty message");
-        }
-    } catch (exc) {
-        if (typeof exc.stack !== 'undefined') {
-            Util.Warn("recv_message, caught exception: " + exc.stack);
-        } else if (typeof exc.description !== 'undefined') {
-            Util.Warn("recv_message, caught exception: " + exc.description);
-        } else {
-            Util.Warn("recv_message, caught exception:" + exc);
-        }
-        if (typeof exc.name !== 'undefined') {
-            eventHandlers.error(exc.name + ": " + exc.message);
-        } else {
-            eventHandlers.error(exc);
-        }
-    }
-    //Util.Debug("<< recv_message");
-}
-
 
-// Set event handlers
-function on(evt, handler) { 
-    eventHandlers[evt] = handler;
-}
-
-function init(protocols, ws_schema) {
-    rQ         = [];
-    rQi        = 0;
-    sQ         = [];
-    websocket  = null;
-
-    var bt = false,
-        wsbt = false,
-        try_binary = false;
-
-    // Check for full typed array support
-    if (('Uint8Array' in window) &&
-        ('set' in Uint8Array.prototype)) {
-        bt = true;
-    }
-    // Check for full binary type support in WebSocket
-    // Inspired by:
-    // https://github.com/Modernizr/Modernizr/issues/370
-    // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js
-    try {
-        if (bt && ('binaryType' in WebSocket.prototype ||
-                   !!(new WebSocket(ws_schema + '://.').binaryType))) {
-            Util.Info("Detected binaryType support in WebSockets");
-            wsbt = true;
-        }
-    } catch (exc) {
-        // Just ignore failed test localhost connections
-    }
-
-    // Default protocols if not specified
-    if (typeof(protocols) === "undefined") {
-        if (wsbt) {
-            protocols = ['binary', 'base64'];
-        } else {
-            protocols = 'base64';
-        }
-    }
-
-    // If no binary support, make sure it was not requested
-    if (!wsbt) {
-        if (protocols === 'binary') {
-            throw("WebSocket binary sub-protocol requested but not supported");
-        }
-        if (typeof(protocols) === "object") {
-            var new_protocols = [];
-            for (var i = 0; i < protocols.length; i++) {
-                if (protocols[i] === 'binary') {
-                    Util.Error("Skipping unsupported WebSocket binary sub-protocol");
+            this._websocket.onmessage = this._recv_message.bind(this);
+            this._websocket.onopen = (function () {
+                Util.Debug('>> WebSock.onopen');
+                if (this._websocket.protocol) {
+                    this._mode = this._websocket.protocol;
+                    Util.Info("Server choose sub-protocol: " + this._websocket.protocol);
                 } else {
-                    new_protocols.push(protocols[i]);
+                    this._mode = 'base64';
+                    Util.Error('Server select no sub-protocol!: ' + this._websocket.protocol);
                 }
+                this._eventHandlers.open();
+                Util.Debug("<< WebSock.onopen");
+            }).bind(this);
+            this._websocket.onclose = (function (e) {
+                Util.Debug(">> WebSock.onclose");
+                this._eventHandlers.close(e);
+                Util.Debug("<< WebSock.onclose");
+            }).bind(this);
+            this._websocket.onerror = (function (e) {
+                Util.Debug(">> WebSock.onerror: " + e);
+                this._eventHandlers.error(e);
+                Util.Debug("<< WebSock.onerror: " + e);
+            }).bind(this);
+        },
+
+        close: function () {
+            if (this._websocket) {
+                if ((this._websocket.readyState === WebSocket.OPEN) ||
+                        (this._websocket.readyState === WebSocket.CONNECTING)) {
+                    Util.Info("Closing WebSocket connection");
+                    this._websocket.close();
+                }
+
+                this._websocket.onmessage = function (e) { return; };
             }
-            if (new_protocols.length > 0) {
-                protocols = new_protocols;
+        },
+
+        // private methods
+        _encode_message: function () {
+            if (this._mode === 'binary') {
+                // Put in a binary arraybuffer
+                return (new Uint8Array(this._sQ)).buffer;
             } else {
-                throw("Only WebSocket binary sub-protocol was requested and not supported.");
+                // base64 encode
+                return Base64.encode(this._sQ);
             }
-        }
-    }
+        },
+
+        _decode_message: function (data) {
+            if (this._mode === 'binary') {
+                // push arraybuffer values onto the end
+                var u8 = new Uint8Array(data);
+                for (var i = 0; i < u8.length; i++) {
+                    this._rQ.push(u8[i]);
+                }
+            } else {
+                // base64 decode and concat to end
+                this._rQ = this._rQ.concat(Base64.decode(data, 0));
+            }
+        },
+
+        _recv_message: function (e) {
+            try {
+                this._decode_message(e.data);
+                if (this.rQlen() > 0) {
+                    this._eventHandlers.message();
+                    // Compact the receive queue
+                    if (this._rQ.length > this._rQmax) {
+                        this._rQ = this._rQ.slice(this._rQi);
+                        this._rQi = 0;
+                    }
+                } else {
+                    Util.Debug("Ignoring empty message");
+                }
+            } catch (exc) {
+                var exception_str = "";
+                if (exc.name) {
+                    exception_str += "\n    name: " + exc.name + "\n";
+                    exception_str += "    message: " + exc.message + "\n";
+                }
 
-    return protocols;
-}
+                if (typeof exc.description !== 'undefined') {
+                    exception_str += "    description: " + exc.description + "\n";
+                }
 
-function open(uri, protocols) {
-    var ws_schema = uri.match(/^([a-z]+):\/\//)[1];
-    protocols = init(protocols, ws_schema);
+                if (typeof exc.stack !== 'undefined') {
+                    exception_str += exc.stack;
+                }
 
-    if (test_mode) {
-        websocket = {};
-    } else {
-        websocket = new WebSocket(uri, protocols);
-        if (protocols.indexOf('binary') >= 0) {
-            websocket.binaryType = 'arraybuffer';
-        }
-    }
-
-    websocket.onmessage = recv_message;
-    websocket.onopen = function() {
-        Util.Debug(">> WebSock.onopen");
-        if (websocket.protocol) {
-            mode = websocket.protocol;
-            Util.Info("Server chose sub-protocol: " + websocket.protocol);
-        } else {
-            mode = 'base64';
-            Util.Error("Server select no sub-protocol!: " + websocket.protocol);
-        }
-        eventHandlers.open();
-        Util.Debug("<< WebSock.onopen");
-    };
-    websocket.onclose = function(e) {
-        Util.Debug(">> WebSock.onclose");
-        eventHandlers.close(e);
-        Util.Debug("<< WebSock.onclose");
-    };
-    websocket.onerror = function(e) {
-        Util.Debug(">> WebSock.onerror: " + e);
-        eventHandlers.error(e);
-        Util.Debug("<< WebSock.onerror");
-    };
-}
+                if (exception_str.length > 0) {
+                    Util.Error("recv_message, caught exception: " + exception_str);
+                } else {
+                    Util.Error("recv_message, caught exception: " + exc);
+                }
 
-function close() {
-    if (websocket) {
-        if ((websocket.readyState === WebSocket.OPEN) ||
-            (websocket.readyState === WebSocket.CONNECTING)) {
-            Util.Info("Closing WebSocket connection");
-            websocket.close();
+                if (typeof exc.name !== 'undefined') {
+                    this._eventHandlers.error(exc.name + ": " + exc.message);
+                } else {
+                    this._eventHandlers.error(exc);
+                }
+            }
         }
-        websocket.onmessage = function (e) { return; };
-    }
-}
-
-// Override internal functions for testing
-// Takes a send function, returns reference to recv function
-function testMode(override_send, data_mode) {
-    test_mode = true;
-    mode = data_mode;
-    api.send = override_send;
-    api.close = function () {};
-    return recv_message;
-}
-
-function constructor() {
-    // Configuration settings
-    api.maxBufferedAmount = 200;
-
-    // Direct access to send and receive queues
-    api.get_sQ       = get_sQ;
-    api.get_rQ       = get_rQ;
-    api.get_rQi      = get_rQi;
-    api.set_rQi      = set_rQi;
-
-    // Routines to read from the receive queue
-    api.rQlen        = rQlen;
-    api.rQpeek8      = rQpeek8;
-    api.rQshift8     = rQshift8;
-    api.rQunshift8   = rQunshift8;
-    api.rQshift16    = rQshift16;
-    api.rQshift32    = rQshift32;
-    api.rQshiftStr   = rQshiftStr;
-    api.rQshiftBytes = rQshiftBytes;
-    api.rQslice      = rQslice;
-    api.rQwait       = rQwait;
-
-    api.flush        = flush;
-    api.send         = send;
-    api.send_string  = send_string;
-
-    api.on           = on;
-    api.init         = init;
-    api.open         = open;
-    api.close        = close;
-    api.testMode     = testMode;
-
-    return api;
-}
-
-return constructor();
-
-}
+    };
+})();

+ 61 - 43
include/webutil.js

@@ -7,8 +7,7 @@
  * See README.md for usage and integration instructions.
  */
 
-"use strict";
-/*jslint bitwise: false, white: false */
+/*jslint bitwise: false, white: false, browser: true, devel: true */
 /*global Util, window, document */
 
 // Globals defined here
@@ -31,45 +30,47 @@ if (!window.$D) {
 }
 
 
-/* 
+/*
  * ------------------------------------------------------
  * Namespaced in WebUtil
  * ------------------------------------------------------
  */
 
 // init log level reading the logging HTTP param
-WebUtil.init_logging = function(level) {
+WebUtil.init_logging = function (level) {
+    "use strict";
     if (typeof level !== "undefined") {
         Util._log_level = level;
     } else {
-        Util._log_level = (document.location.href.match(
-            /logging=([A-Za-z0-9\._\-]*)/) ||
-            ['', Util._log_level])[1];
+        var param = document.location.href.match(/logging=([A-Za-z0-9\._\-]*)/);
+        Util._log_level = (param || ['', Util._log_level])[1];
     }
     Util.init_logging();
 };
 
 
 WebUtil.dirObj = function (obj, depth, parent) {
-    var i, msg = "", val = "";
-    if (! depth) { depth=2; }
-    if (! parent) { parent= ""; }
-
-    // Print the properties of the passed-in object 
-    for (i in obj) {
-        if ((depth > 1) && (typeof obj[i] === "object")) { 
+    "use strict";
+    if (! depth) { depth = 2; }
+    if (! parent) { parent = ""; }
+
+    // Print the properties of the passed-in object
+    var msg = "";
+    for (var i in obj) {
+        if ((depth > 1) && (typeof obj[i] === "object")) {
             // Recurse attributes that are objects
-            msg += WebUtil.dirObj(obj[i], depth-1, parent + "." + i);
+            msg += WebUtil.dirObj(obj[i], depth - 1, parent + "." + i);
         } else {
             //val = new String(obj[i]).replace("\n", " ");
+            var val = "";
             if (typeof(obj[i]) === "undefined") {
                 val = "undefined";
             } else {
                 val = obj[i].toString().replace("\n", " ");
             }
             if (val.length > 30) {
-                val = val.substr(0,30) + "...";
-            } 
+                val = val.substr(0, 30) + "...";
+            }
             msg += parent + "." + i + ": " + val + "\n";
         }
     }
@@ -77,7 +78,8 @@ WebUtil.dirObj = function (obj, depth, parent) {
 };
 
 // Read a query string variable
-WebUtil.getQueryVar = function(name, defVal) {
+WebUtil.getQueryVar = function (name, defVal) {
+    "use strict";
     var re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
         match = document.location.href.match(re);
     if (typeof defVal === 'undefined') { defVal = null; }
@@ -94,42 +96,50 @@ WebUtil.getQueryVar = function(name, defVal) {
  */
 
 // No days means only for this browser session
-WebUtil.createCookie = function(name,value,days) {
-    var date, expires, secure;
+WebUtil.createCookie = function (name, value, days) {
+    "use strict";
+    var date, expires;
     if (days) {
         date = new Date();
-        date.setTime(date.getTime()+(days*24*60*60*1000));
-        expires = "; expires="+date.toGMTString();
+        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
+        expires = "; expires=" + date.toGMTString();
     } else {
         expires = "";
     }
+
+    var secure;
     if (document.location.protocol === "https:") {
         secure = "; secure";
     } else {
         secure = "";
     }
-    document.cookie = name+"="+value+expires+"; path=/"+secure;
+    document.cookie = name + "=" + value + expires + "; path=/" + secure;
 };
 
-WebUtil.readCookie = function(name, defaultValue) {
-    var i, c, nameEQ = name + "=", ca = document.cookie.split(';');
-    for(i=0; i < ca.length; i += 1) {
-        c = ca[i];
-        while (c.charAt(0) === ' ') { c = c.substring(1,c.length); }
-        if (c.indexOf(nameEQ) === 0) { return c.substring(nameEQ.length,c.length); }
+WebUtil.readCookie = function (name, defaultValue) {
+    "use strict";
+    var nameEQ = name + "=",
+        ca = document.cookie.split(';');
+
+    for (var i = 0; i < ca.length; i += 1) {
+        var c = ca[i];
+        while (c.charAt(0) === ' ') { c = c.substring(1, c.length); }
+        if (c.indexOf(nameEQ) === 0) { return c.substring(nameEQ.length, c.length); }
     }
     return (typeof defaultValue !== 'undefined') ? defaultValue : null;
 };
 
-WebUtil.eraseCookie = function(name) {
-    WebUtil.createCookie(name,"",-1);
+WebUtil.eraseCookie = function (name) {
+    "use strict";
+    WebUtil.createCookie(name, "", -1);
 };
 
 /*
  * Setting handling.
  */
 
-WebUtil.initSettings = function(callback) {
+WebUtil.initSettings = function (callback /*, ...callbackArgs */) {
+    "use strict";
     var callbackArgs = Array.prototype.slice.call(arguments, 1);
     if (window.chrome && window.chrome.storage) {
         window.chrome.storage.sync.get(function (cfg) {
@@ -148,7 +158,8 @@ WebUtil.initSettings = function(callback) {
 };
 
 // No days means only for this browser session
-WebUtil.writeSetting = function(name, value) {
+WebUtil.writeSetting = function (name, value) {
+    "use strict";
     if (window.chrome && window.chrome.storage) {
         //console.log("writeSetting:", name, value);
         if (WebUtil.settings[name] !== value) {
@@ -160,7 +171,8 @@ WebUtil.writeSetting = function(name, value) {
     }
 };
 
-WebUtil.readSetting = function(name, defaultValue) {
+WebUtil.readSetting = function (name, defaultValue) {
+    "use strict";
     var value;
     if (window.chrome && window.chrome.storage) {
         value = WebUtil.settings[name];
@@ -177,7 +189,8 @@ WebUtil.readSetting = function(name, defaultValue) {
     }
 };
 
-WebUtil.eraseSetting = function(name) {
+WebUtil.eraseSetting = function (name) {
+    "use strict";
     if (window.chrome && window.chrome.storage) {
         window.chrome.storage.sync.remove(name);
         delete WebUtil.settings[name];
@@ -189,9 +202,12 @@ WebUtil.eraseSetting = function(name) {
 /*
  * Alternate stylesheet selection
  */
-WebUtil.getStylesheets = function() { var i, links, sheets = [];
-    links = document.getElementsByTagName("link");
-    for (i = 0; i < links.length; i += 1) {
+WebUtil.getStylesheets = function () {
+    "use strict";
+    var links = document.getElementsByTagName("link");
+    var sheets = [];
+
+    for (var i = 0; i < links.length; i += 1) {
         if (links[i].title &&
             links[i].rel.toUpperCase().indexOf("STYLESHEET") > -1) {
             sheets.push(links[i]);
@@ -202,14 +218,16 @@ WebUtil.getStylesheets = function() { var i, links, sheets = [];
 
 // No sheet means try and use value from cookie, null sheet used to
 // clear all alternates.
-WebUtil.selectStylesheet = function(sheet) {
-    var i, link, sheets = WebUtil.getStylesheets();
+WebUtil.selectStylesheet = function (sheet) {
+    "use strict";
     if (typeof sheet === 'undefined') {
         sheet = 'default';
     }
-    for (i=0; i < sheets.length; i += 1) {
-        link = sheets[i];
-        if (link.title === sheet) {    
+
+    var sheets = WebUtil.getStylesheets();
+    for (var i = 0; i < sheets.length; i += 1) {
+        var link = sheets[i];
+        if (link.title === sheet) {
             Util.Debug("Using stylesheet " + sheet);
             link.disabled = false;
         } else {

+ 191 - 0
karma.conf.js

@@ -0,0 +1,191 @@
+// Karma configuration
+
+module.exports = function(config) {
+  /*var customLaunchers = {
+    sl_chrome_win7: {
+      base: 'SauceLabs',
+      browserName: 'chrome',
+      platform: 'Windows 7'
+    },
+
+    sl_firefox30_linux: {
+      base: 'SauceLabs',
+      browserName: 'firefox',
+      version: '30',
+      platform: 'Linux'
+    },
+
+    sl_firefox26_linux: {
+      base: 'SauceLabs',
+      browserName: 'firefox',
+      version: 26,
+      platform: 'Linux'
+    },
+
+    sl_windows7_ie10: {
+      base: 'SauceLabs',
+      browserName: 'internet explorer',
+      platform: 'Windows 7',
+      version: '10'
+    },
+
+    sl_windows81_ie11: {
+      base: 'SauceLabs',
+      browserName: 'internet explorer',
+      platform: 'Windows 8.1',
+      version: '11'
+    },
+
+    sl_osxmavericks_safari7: {
+      base: 'SauceLabs',
+      browserName: 'safari',
+      platform: 'OS X 10.9',
+      version: '7'
+    },
+
+    sl_osxmtnlion_safari6: {
+      base: 'SauceLabs',
+      browserName: 'safari',
+      platform: 'OS X 10.8',
+      version: '6'
+    }
+  };*/
+
+  var customLaunchers = {};
+  var browsers = [];
+  var useSauce = false;
+
+  if (process.env.SAUCE_USERNAME && process.env.SAUCE_ACCESS_KEY) {
+    useSauce = true;
+  }
+
+  if (useSauce && process.env.TEST_BROWSER_NAME && process.env.TEST_BROWSER_NAME != 'PhantomJS') {
+    var names = process.env.TEST_BROWSER_NAME.split(',');
+    var platforms = process.env.TEST_BROWSER_OS.split(',');
+    var versions = [];
+    if (process.env.TEST_BROWSER_VERSION) {
+      versions = process.env.TEST_BROWSER_VERSION.split(',');
+    } else {
+      versions = [null];
+    }
+
+    for (var i = 0; i < names.length; i++) {
+      for (var j = 0; j < platforms.length; j++) {
+        for (var k = 0; k < versions.length; k++) {
+          var launcher_name = 'sl_' + platforms[j].replace(/[^a-zA-Z0-9]/g, '') + '_' + names[i];
+          if (versions[k]) {
+            launcher_name += '_' + versions[k];
+          }
+
+          customLaunchers[launcher_name] = {
+            base: 'SauceLabs',
+            browserName: names[i],
+            platform: platforms[j],
+          };
+
+          if (versions[i]) {
+            customLaunchers[launcher_name].version = versions[k];
+          }
+        }
+      }
+    }
+
+    browsers = Object.keys(customLaunchers);
+  } else {
+    useSauce = false;
+    browsers = ['PhantomJS'];
+  }
+
+  var my_conf = {
+
+    // base path that will be used to resolve all patterns (eg. files, exclude)
+    basePath: '',
+
+    // frameworks to use
+    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+    frameworks: ['mocha', 'sinon', 'chai', 'sinon-chai'],
+
+
+    // list of files / patterns to load in the browser (loaded in order)
+    files: [
+      'tests/fake.*.js',
+      'include/util.js',  // load first to avoid issues, since methods are called immediately
+      //'../include/*.js',
+      'include/base64.js',
+      'include/keysym.js',
+      'include/keysymdef.js',
+      'include/keyboard.js',
+      'include/input.js',
+      'include/websock.js',
+      'include/rfb.js',
+      'include/jsunzip.js',
+      'include/des.js',
+      'include/display.js',
+      'tests/test.*.js'
+    ],
+
+    client: {
+      mocha: {
+        'ui': 'bdd'
+      }
+    },
+
+    // list of files to exclude
+    exclude: [
+      '../include/playback.js',
+      '../include/ui.js'
+    ],
+
+    customLaunchers: customLaunchers,
+
+    // start these browsers
+    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+    browsers: browsers,
+
+    // preprocess matching files before serving them to the browser
+    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+    preprocessors: {
+
+    },
+
+
+    // test results reporter to use
+    // possible values: 'dots', 'progress'
+    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+    reporters: ['mocha', 'saucelabs'],
+
+
+    // web server port
+    port: 9876,
+
+
+    // enable / disable colors in the output (reporters and logs)
+    colors: true,
+
+
+    // level of logging
+    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+    logLevel: config.LOG_INFO,
+
+
+    // enable / disable watching file and executing tests whenever any file changes
+    autoWatch: false,
+
+    // Continuous Integration mode
+    // if true, Karma captures browsers, runs the tests and exits
+    singleRun: true,
+
+    // Increase timeout in case connection is slow/we run more browsers than possible
+    // (we currently get 3 for free, and we try to run 7, so it can take a while)
+    captureTimeout: 240000
+  };
+
+  if (useSauce) {
+    my_conf.sauceLabs = {
+      testName: 'noVNC Tests (all)',
+      startConnect: true,
+    };
+  }
+
+  config.set(my_conf);
+};

+ 50 - 0
package.json

@@ -0,0 +1,50 @@
+{
+  "name": "noVNC",
+  "version": "0.5.0",
+  "description": "An HTML5 VNC client",
+  "main": "karma.conf.js",
+  "directories": {
+    "doc": "docs",
+    "test": "tests"
+  },
+  "scripts": {
+    "test": "karma start karma.conf.js"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/kanaka/noVNC.git"
+  },
+  "author": "Joel Martin <github@martintribe.org> (https://github.com/kanaka)",
+  "contributors": [
+    "Solly Ross <sross@redhat.com> (https://github.com/directxman12)",
+    "Peter Åstrand <astrand@cendio.se> (https://github.com/astrand)",
+    "Samuel Mannehed <samuel@cendio.se> (https://github.com/samhed)"
+  ],
+  "license": "MPL 2.0",
+  "bugs": {
+    "url": "https://github.com/kanaka/noVNC/issues"
+  },
+  "homepage": "https://github.com/kanaka/noVNC",
+  "devDependencies": {
+    "ansi": "^0.3.0",
+    "casperjs": "^1.1.0-beta3",
+    "chai": "^1.9.1",
+    "commander": "^2.2.0",
+    "karma": "^0.12.16",
+    "karma-chai": "^0.1.0",
+    "karma-mocha": "^0.1.4",
+    "karma-mocha-reporter": "^0.2.5",
+    "karma-phantomjs-launcher": "^0.1.4",
+    "karma-sauce-launcher": "^0.2.8",
+    "karma-sinon": "^1.0.3",
+    "karma-sinon-chai": "^0.1.6",
+    "mocha": "^1.20.1",
+    "open": "0.0.5",
+    "phantom": "^0.6.3",
+    "phantomjs": "^1.9.7-9",
+    "sinon": "^1.10.2",
+    "sinon-chai": "^2.5.0",
+    "spooky": "^0.2.4",
+    "temp": "^0.8.0"
+  }
+}

+ 96 - 0
tests/fake.websocket.js

@@ -0,0 +1,96 @@
+var FakeWebSocket;
+
+(function () {
+    // PhantomJS can't create Event objects directly, so we need to use this
+    function make_event(name, props) {
+        var evt = document.createEvent('Event');
+        evt.initEvent(name, true, true);
+        if (props) {
+            for (var prop in props) {
+                evt[prop] = props[prop];
+            }
+        }
+        return evt;
+    }
+
+    FakeWebSocket = function (uri, protocols) {
+        this.url = uri;
+        this.binaryType = "arraybuffer";
+        this.extensions = "";
+
+        if (!protocols || typeof protocols === 'string') {
+            this.protocol = protocols;
+        } else {
+            this.protocol = protocols[0];
+        }
+
+        this._send_queue = new Uint8Array(20000);
+
+        this.readyState = FakeWebSocket.CONNECTING;
+        this.bufferedAmount = 0;
+
+        this.__is_fake = true;
+    };
+
+    FakeWebSocket.prototype = {
+        close: function (code, reason) {
+            this.readyState = FakeWebSocket.CLOSED;
+            if (this.onclose) {
+                this.onclose(make_event("close", { 'code': code, 'reason': reason, 'wasClean': true }));
+            }
+        },
+
+        send: function (data) {
+            if (this.protocol == 'base64') {
+                data = Base64.decode(data);
+            } else {
+                data = new Uint8Array(data);
+            }
+            this._send_queue.set(data, this.bufferedAmount);
+            this.bufferedAmount += data.length;
+        },
+
+        _get_sent_data: function () {
+            var arr = [];
+            for (var i = 0; i < this.bufferedAmount; i++) {
+                arr[i] = this._send_queue[i];
+            }
+
+            this.bufferedAmount = 0;
+
+            return arr;
+        },
+
+        _open: function (data) {
+            this.readyState = FakeWebSocket.OPEN;
+            if (this.onopen) {
+                this.onopen(make_event('open'));
+            }
+        },
+
+        _receive_data: function (data) {
+            this.onmessage(make_event("message", { 'data': data }));
+        }
+    };
+
+    FakeWebSocket.OPEN = WebSocket.OPEN;
+    FakeWebSocket.CONNECTING = WebSocket.CONNECTING;
+    FakeWebSocket.CLOSING = WebSocket.CLOSING;
+    FakeWebSocket.CLOSED = WebSocket.CLOSED;
+
+    FakeWebSocket.__is_fake = true;
+
+    FakeWebSocket.replace = function () {
+        if (!WebSocket.__is_fake) {
+            var real_version = WebSocket;
+            WebSocket = FakeWebSocket;
+            FakeWebSocket.__real_version = real_version;
+        }
+    };
+
+    FakeWebSocket.restore = function () {
+        if (WebSocket.__is_fake) {
+            WebSocket = WebSocket.__real_version;
+        }
+    };
+})();

+ 1 - 1
tests/run_from_console.casper.js

@@ -2,7 +2,7 @@ var Spooky = require('spooky');
 var path = require('path');
 
 var phantom_path = require('phantomjs').path;
-var casper_path = path.resolve(__dirname, 'node_modules/casperjs/bin/casperjs');
+var casper_path = path.resolve(__dirname, '../node_modules/casperjs/bin/casperjs');
 process.env.PHANTOMJS_EXECUTABLE = phantom_path;
 var casper_opts = {
   child: {

+ 6 - 6
tests/run_from_console.js

@@ -67,16 +67,16 @@ if (program.autoInject) {
   temp.track();
 
   var template = {
-    header: "<html>\n<head>\n<meta charset='utf-8' />\n<link rel='stylesheet' href='" + path.resolve(__dirname, 'node_modules/mocha/mocha.css') + "'/>\n</head>\n<body><div id='mocha'></div>",
+    header: "<html>\n<head>\n<meta charset='utf-8' />\n<link rel='stylesheet' href='" + path.resolve(__dirname, '../node_modules/mocha/mocha.css') + "'/>\n</head>\n<body><div id='mocha'></div>",
     script_tag: function(p) { return "<script src='" + p + "'></script>"; },
     footer: "<script>\nmocha.checkLeaks();\nmocha.globals(['navigator', 'create', 'ClientUtils', '__utils__']);\nmocha.run(function () { window.__mocha_done = true; });\n</script>\n</body>\n</html>"
   };
 
-  template.header += "\n" + template.script_tag(path.resolve(__dirname, 'node_modules/chai/chai.js'));
-  template.header += "\n" + template.script_tag(path.resolve(__dirname, 'node_modules/mocha/mocha.js'));
-  template.header += "\n" + template.script_tag(path.resolve(__dirname, 'node_modules/sinon/pkg/sinon.js'));
-  template.header += "\n" + template.script_tag(path.resolve(__dirname, 'node_modules/sinon-chai/lib/sinon-chai.js'));
-  template.header += "\n" + template.script_tag(path.resolve(__dirname, 'node_modules/sinon-chai/lib/sinon-chai.js'));
+  template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/chai/chai.js'));
+  template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/mocha/mocha.js'));
+  template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/sinon/pkg/sinon.js'));
+  template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/sinon-chai/lib/sinon-chai.js'));
+  template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/sinon-chai/lib/sinon-chai.js'));
   template.header += "\n<script>mocha.setup('bdd');</script>";
 
 

+ 33 - 0
tests/test.base64.js

@@ -0,0 +1,33 @@
+// requires local modules: base64
+var assert = chai.assert;
+var expect = chai.expect;
+
+describe('Base64 Tools', function() {
+    "use strict";
+
+    var BIN_ARR = new Array(256);
+    for (var i = 0; i < 256; i++) {
+        BIN_ARR[i] = i;
+    }
+    
+    var B64_STR = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==";
+
+
+    describe('encode', function() {
+        it('should encode a binary string into Base64', function() {
+            var encoded = Base64.encode(BIN_ARR);
+            expect(encoded).to.equal(B64_STR);
+        });
+    });
+
+    describe('decode', function() {
+        it('should decode a Base64 string into a normal string', function() {
+            var decoded = Base64.decode(B64_STR);
+            expect(decoded).to.deep.equal(BIN_ARR);
+        });
+
+        it('should throw an error if we have extra characters at the end of the string', function() {
+            expect(function () { Base64.decode(B64_STR+'abcdef'); }).to.throw(Error);
+        });
+    });
+});

+ 332 - 0
tests/test.display.js

@@ -0,0 +1,332 @@
+// requires local modules: util, base64, display
+/* jshint expr: true */
+var expect = chai.expect;
+
+chai.use(function (_chai, utils) {
+    _chai.Assertion.addMethod('displayed', function (target_data) {
+        var obj = this._obj;
+        var data_cl = obj._drawCtx.getImageData(0, 0, obj._viewportLoc.w, obj._viewportLoc.h).data;
+        // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray, so work around that
+        var data = new Uint8Array(data_cl);
+        this.assert(utils.eql(data, target_data),
+            "expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}",
+            "expected #{this} not to have displayed the image #{act}",
+            target_data,
+            data);
+    });
+});
+
+describe('Display/Canvas Helper', function () {
+    var checked_data = [
+        0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
+        0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
+        0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
+        0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
+    ];
+    checked_data = new Uint8Array(checked_data);
+
+    var basic_data = [0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0xff, 0xff, 0xff, 255];
+    basic_data = new Uint8Array(basic_data);
+
+    function make_image_canvas (input_data) {
+        var canvas = document.createElement('canvas');
+        canvas.width = 4;
+        canvas.height = 4;
+        var ctx = canvas.getContext('2d');
+        var data = ctx.createImageData(4, 4);
+        for (var i = 0; i < checked_data.length; i++) { data.data[i] = input_data[i]; }
+        ctx.putImageData(data, 0, 0);
+        return canvas;
+    }
+
+    describe('viewport handling', function () {
+        var display;
+        beforeEach(function () {
+            display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true });
+            display.resize(5, 5);
+            display.viewportChange(1, 1, 3, 3);
+            display.getCleanDirtyReset();
+        });
+
+        it('should take viewport location into consideration when drawing images', function () {
+            display.resize(4, 4);
+            display.viewportChange(0, 0, 2, 2);
+            display.drawImage(make_image_canvas(basic_data), 1, 1);
+
+            var expected = new Uint8Array(16);
+            var i;
+            for (i = 0; i < 8; i++) { expected[i] = basic_data[i]; }
+            for (i = 8; i < 16; i++) { expected[i] = 0; }
+            expect(display).to.have.displayed(expected);
+        });
+
+        it('should redraw the left side when shifted left', function () {
+            display.viewportChange(-1, 0, 3, 3);
+            var cdr = display.getCleanDirtyReset();
+            expect(cdr.cleanBox).to.deep.equal({ x: 1, y: 1, w: 2, h: 3 });
+            expect(cdr.dirtyBoxes).to.have.length(1);
+            expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 0, y: 1, w: 2, h: 3 });
+        });
+
+        it('should redraw the right side when shifted right', function () {
+            display.viewportChange(1, 0, 3, 3);
+            var cdr = display.getCleanDirtyReset();
+            expect(cdr.cleanBox).to.deep.equal({ x: 2, y: 1, w: 2, h: 3 });
+            expect(cdr.dirtyBoxes).to.have.length(1);
+            expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 4, y: 1, w: 1, h: 3 });
+        });
+
+        it('should redraw the top part when shifted up', function () {
+            display.viewportChange(0, -1, 3, 3);
+            var cdr = display.getCleanDirtyReset();
+            expect(cdr.cleanBox).to.deep.equal({ x: 1, y: 1, w: 3, h: 2 });
+            expect(cdr.dirtyBoxes).to.have.length(1);
+            expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 1, y: 0, w: 3, h: 1 });
+        });
+
+        it('should redraw the bottom part when shifted down', function () {
+            display.viewportChange(0, 1, 3, 3);
+            var cdr = display.getCleanDirtyReset();
+            expect(cdr.cleanBox).to.deep.equal({ x: 1, y: 2, w: 3, h: 2 });
+            expect(cdr.dirtyBoxes).to.have.length(1);
+            expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 1, y: 4, w: 3, h: 1 });
+        });
+
+        it('should reset the entire viewport to being clean after calculating the clean/dirty boxes', function () {
+            display.viewportChange(0, 1, 3, 3);
+            var cdr1 = display.getCleanDirtyReset();
+            var cdr2 = display.getCleanDirtyReset();
+            expect(cdr1).to.not.deep.equal(cdr2);
+            expect(cdr2.cleanBox).to.deep.equal({ x: 1, y: 2, w: 3, h: 3 });
+            expect(cdr2.dirtyBoxes).to.be.empty;
+        });
+
+        it('should simply mark the whole display area as dirty if not using viewports', function () {
+            display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: false });
+            display.resize(5, 5);
+            var cdr = display.getCleanDirtyReset();
+            expect(cdr.cleanBox).to.deep.equal({ x: 0, y: 0, w: 0, h: 0 });
+            expect(cdr.dirtyBoxes).to.have.length(1);
+            expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 0, y: 0, w: 5, h: 5 });
+        });
+    });
+
+    describe('resizing', function () {
+        var display;
+        beforeEach(function () {
+            display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true });
+            display.resize(4, 3);
+        });
+
+        it('should change the size of the logical canvas', function () {
+            display.resize(5, 7);
+            expect(display._fb_width).to.equal(5);
+            expect(display._fb_height).to.equal(7);
+        });
+
+        it('should update the viewport dimensions', function () {
+            sinon.spy(display, 'viewportChange');
+            display.resize(2, 2);
+            expect(display.viewportChange).to.have.been.calledOnce;
+        });
+    });
+
+    describe('drawing', function () {
+
+        // TODO(directxman12): improve the tests for each of the drawing functions to cover more than just the
+        //                     basic cases
+        function drawing_tests (pref_js) {
+            var display;
+            beforeEach(function () {
+                display = new Display({ target: document.createElement('canvas'), prefer_js: pref_js });
+                display.resize(4, 4);
+            });
+
+            it('should clear the screen on #clear without a logo set', function () {
+                display.fillRect(0, 0, 4, 4, [0x00, 0x00, 0xff]);
+                display._logo = null;
+                display.clear();
+                display.resize(4, 4);
+                var empty = [];
+                for (var i = 0; i < 4 * display._fb_width * display._fb_height; i++) { empty[i] = 0; }
+                expect(display).to.have.displayed(new Uint8Array(empty));
+            });
+
+            it('should draw the logo on #clear with a logo set', function (done) {
+                display._logo = { width: 4, height: 4, data: make_image_canvas(checked_data).toDataURL() };
+                display._drawCtx._act_drawImg = display._drawCtx.drawImage;
+                display._drawCtx.drawImage = function (img, x, y) {
+                    this._act_drawImg(img, x, y);
+                    expect(display).to.have.displayed(checked_data);
+                    done();
+                };
+                display.clear();
+                expect(display._fb_width).to.equal(4);
+                expect(display._fb_height).to.equal(4);
+            });
+
+            it('should support filling a rectangle with particular color via #fillRect', function () {
+                display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);
+                display.fillRect(0, 0, 2, 2, [0xff, 0, 0]);
+                display.fillRect(2, 2, 2, 2, [0xff, 0, 0]);
+                expect(display).to.have.displayed(checked_data);
+            });
+
+            it('should support copying an portion of the canvas via #copyImage', function () {
+                display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);
+                display.fillRect(0, 0, 2, 2, [0xff, 0, 0x00]);
+                display.copyImage(0, 0, 2, 2, 2, 2);
+                expect(display).to.have.displayed(checked_data);
+            });
+
+            it('should support drawing tile data with a background color and sub tiles', function () {
+                display.startTile(0, 0, 4, 4, [0, 0xff, 0]);
+                display.subTile(0, 0, 2, 2, [0xff, 0, 0]);
+                display.subTile(2, 2, 2, 2, [0xff, 0, 0]);
+                display.finishTile();
+                expect(display).to.have.displayed(checked_data);
+            });
+
+            it('should support drawing BGRX blit images with true color via #blitImage', function () {
+                var data = [];
+                for (var i = 0; i < 16; i++) {
+                    data[i * 4] = checked_data[i * 4 + 2];
+                    data[i * 4 + 1] = checked_data[i * 4 + 1];
+                    data[i * 4 + 2] = checked_data[i * 4];
+                    data[i * 4 + 3] = checked_data[i * 4 + 3];
+                }
+                display.blitImage(0, 0, 4, 4, data, 0);
+                expect(display).to.have.displayed(checked_data);
+            });
+
+            it('should support drawing RGB blit images with true color via #blitRgbImage', function () {
+                var data = [];
+                for (var i = 0; i < 16; i++) {
+                    data[i * 3] = checked_data[i * 4];
+                    data[i * 3 + 1] = checked_data[i * 4 + 1];
+                    data[i * 3 + 2] = checked_data[i * 4 + 2];
+                }
+                display.blitRgbImage(0, 0, 4, 4, data, 0);
+                expect(display).to.have.displayed(checked_data);
+            });
+
+            it('should support drawing blit images from a data URL via #blitStringImage', function (done) {
+                var img_url = make_image_canvas(checked_data).toDataURL();
+                display._drawCtx._act_drawImg = display._drawCtx.drawImage;
+                display._drawCtx.drawImage = function (img, x, y) {
+                    this._act_drawImg(img, x, y);
+                    expect(display).to.have.displayed(checked_data);
+                    done();
+                };
+                display.blitStringImage(img_url, 0, 0);
+            });
+
+            it('should support drawing solid colors with color maps', function () {
+                display._true_color = false;
+                display.set_colourMap({ 0: [0xff, 0, 0], 1: [0, 0xff, 0] });
+                display.fillRect(0, 0, 4, 4, [1]);
+                display.fillRect(0, 0, 2, 2, [0]);
+                display.fillRect(2, 2, 2, 2, [0]);
+                expect(display).to.have.displayed(checked_data);
+            });
+
+            it('should support drawing blit images with color maps', function () {
+                display._true_color = false;
+                display.set_colourMap({ 1: [0xff, 0, 0], 0: [0, 0xff, 0] });
+                var data = [1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1].map(function (elem) { return [elem]; });
+                display.blitImage(0, 0, 4, 4, data, 0);
+                expect(display).to.have.displayed(checked_data);
+            });
+
+            it('should support drawing an image object via #drawImage', function () {
+                var img = make_image_canvas(checked_data);
+                display.drawImage(img, 0, 0);
+                expect(display).to.have.displayed(checked_data);
+            });
+        }
+
+        describe('(prefering native methods)', function () { drawing_tests.call(this, false); });
+        describe('(prefering JavaScript)', function () { drawing_tests.call(this, true); });
+    });
+
+    describe('the render queue processor', function () {
+        var display;
+        beforeEach(function () {
+            display = new Display({ target: document.createElement('canvas'), prefer_js: false });
+            display.resize(4, 4);
+            sinon.spy(display, '_scan_renderQ');
+            this.old_requestAnimFrame = window.requestAnimFrame;
+            window.requestAnimFrame = function (cb) {
+                this.next_frame_cb = cb;
+            }.bind(this);
+            this.next_frame = function () { this.next_frame_cb(); };
+        });
+
+        afterEach(function () {
+            window.requestAnimFrame = this.old_requestAnimFrame;
+        });
+
+        it('should try to process an item when it is pushed on, if nothing else is on the queue', function () {
+            display.renderQ_push({ type: 'noop' });  // does nothing
+            expect(display._scan_renderQ).to.have.been.calledOnce;
+        });
+
+        it('should not try to process an item when it is pushed on if we are waiting for other items', function () {
+            display._renderQ.length = 2;
+            display.renderQ_push({ type: 'noop' });
+            expect(display._scan_renderQ).to.not.have.been.called;
+        });
+
+        it('should wait until an image is loaded to attempt to draw it and the rest of the queue', function () {
+            var img = { complete: false };
+            display._renderQ = [{ type: 'img', x: 3, y: 4, img: img },
+                                { type: 'fill', x: 1, y: 2, width: 3, height: 4, color: 5 }];
+            display.drawImage = sinon.spy();
+            display.fillRect = sinon.spy();
+
+            display._scan_renderQ();
+            expect(display.drawImage).to.not.have.been.called;
+            expect(display.fillRect).to.not.have.been.called;
+
+            display._renderQ[0].img.complete = true;
+            this.next_frame();
+            expect(display.drawImage).to.have.been.calledOnce;
+            expect(display.fillRect).to.have.been.calledOnce;
+        });
+
+        it('should draw a blit image on type "blit"', function () {
+            display.blitImage = sinon.spy();
+            display.renderQ_push({ type: 'blit', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] });
+            expect(display.blitImage).to.have.been.calledOnce;
+            expect(display.blitImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0);
+        });
+
+        it('should draw a blit RGB image on type "blitRgb"', function () {
+            display.blitRgbImage = sinon.spy();
+            display.renderQ_push({ type: 'blitRgb', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] });
+            expect(display.blitRgbImage).to.have.been.calledOnce;
+            expect(display.blitRgbImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0);
+        });
+
+        it('should copy a region on type "copy"', function () {
+            display.copyImage = sinon.spy();
+            display.renderQ_push({ type: 'copy', x: 3, y: 4, width: 5, height: 6, old_x: 7, old_y: 8 });
+            expect(display.copyImage).to.have.been.calledOnce;
+            expect(display.copyImage).to.have.been.calledWith(7, 8, 3, 4, 5, 6);
+        });
+
+        it('should fill a rect with a given color on type "fill"', function () {
+            display.fillRect = sinon.spy();
+            display.renderQ_push({ type: 'fill', x: 3, y: 4, width: 5, height: 6, color: [7, 8, 9]});
+            expect(display.fillRect).to.have.been.calledOnce;
+            expect(display.fillRect).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9]);
+        });
+
+        it('should draw an image from an image object on type "img" (if complete)', function () {
+            display.drawImage = sinon.spy();
+            display.renderQ_push({ type: 'img', x: 3, y: 4, img: { complete: true } });
+            expect(display.drawImage).to.have.been.calledOnce;
+            expect(display.drawImage).to.have.been.calledWith({ complete: true }, 3, 4);
+        });
+    });
+});

+ 3 - 1
tests/test.helper.js

@@ -1,4 +1,6 @@
-var assert = chai.assert;
+// requires local modules: keysym, keysymdef, keyboard
+
+var assert = chai.assert;
 var expect = chai.expect;
 
 describe('Helpers', function() {

+ 3 - 2
tests/test.keyboard.js

@@ -1,7 +1,8 @@
+// requires local modules: input, keyboard, keysymdef
 var assert = chai.assert;
 var expect = chai.expect;
 
-
+/* jshint newcap: false, expr: true */
 describe('Key Event Pipeline Stages', function() {
     "use strict";
     describe('Decode Keyboard Events', function() {
@@ -50,7 +51,7 @@ describe('Key Event Pipeline Stages', function() {
             KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
                 expect(evt).to.be.deep.equal({keyId: 0x41, type: 'keydown'});
                 done();
-            }).keydown({keyCode: 0x41})
+            }).keydown({keyCode: 0x41});
         });
         it('should forward keyup events with the right type', function(done) {
             KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {

+ 1696 - 0
tests/test.rfb.js

@@ -0,0 +1,1696 @@
+// requires local modules: util, base64, websock, rfb, keyboard, keysym, keysymdef, input, jsunzip, des, display
+// requires test modules: fake.websocket
+/* jshint expr: true */
+var assert = chai.assert;
+var expect = chai.expect;
+
+function make_rfb (extra_opts) {
+    if (!extra_opts) {
+        extra_opts = {};
+    }
+
+    extra_opts.target = extra_opts.target || document.createElement('canvas');
+    return new RFB(extra_opts);
+}
+
+// some useful assertions for noVNC
+chai.use(function (_chai, utils) {
+    _chai.Assertion.addMethod('displayed', function (target_data) {
+        var obj = this._obj;
+        var data_cl = obj._drawCtx.getImageData(0, 0, obj._fb_width, obj._fb_height).data;
+        // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray, so work around that
+        var data = new Uint8Array(data_cl);
+        this.assert(utils.eql(data, target_data),
+            "expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}",
+            "expected #{this} not to have displayed the image #{act}",
+            target_data,
+            data);
+    });
+
+    _chai.Assertion.addMethod('sent', function (target_data) {
+        var obj = this._obj;
+        var data = obj._websocket._get_sent_data();
+        this.assert(utils.eql(data, target_data),
+            "expected #{this} to have sent the data #{exp}, but it actually sent #{act}",
+            "expected #{this} not to have sent the data #{act}",
+            target_data,
+            data);
+    });
+});
+
+describe('Remote Frame Buffer Protocol Client', function() {
+    "use strict";
+    before(FakeWebSocket.replace);
+    after(FakeWebSocket.restore);
+
+    describe('Public API Basic Behavior', function () {
+        var client;
+        beforeEach(function () {
+            client = make_rfb();
+        });
+
+        describe('#connect', function () {
+            beforeEach(function () { client._updateState = sinon.spy(); });
+
+            it('should set the current state to "connect"', function () {
+                client.connect('host', 8675);
+                expect(client._updateState).to.have.been.calledOnce;
+                expect(client._updateState).to.have.been.calledWith('connect');
+            });
+
+            it('should fail if we are missing a host', function () {
+                sinon.spy(client, '_fail');
+                client.connect(undefined, 8675);
+                expect(client._fail).to.have.been.calledOnce;
+            });
+
+            it('should fail if we are missing a port', function () {
+                sinon.spy(client, '_fail');
+                client.connect('abc');
+                expect(client._fail).to.have.been.calledOnce;
+            });
+
+            it('should not update the state if we are missing a host or port', function () {
+                sinon.spy(client, '_fail');
+                client.connect('abc');
+                expect(client._fail).to.have.been.calledOnce;
+                expect(client._updateState).to.have.been.calledOnce;
+                expect(client._updateState).to.have.been.calledWith('failed');
+            });
+        });
+
+        describe('#disconnect', function () {
+            beforeEach(function () { client._updateState = sinon.spy(); });
+
+            it('should set the current state to "disconnect"', function () {
+                client.disconnect();
+                expect(client._updateState).to.have.been.calledOnce;
+                expect(client._updateState).to.have.been.calledWith('disconnect');
+            });
+        });
+
+        describe('#sendPassword', function () {
+            beforeEach(function () { this.clock = sinon.useFakeTimers(); });
+            afterEach(function () { this.clock.restore(); });
+
+            it('should set the state to "Authentication"', function () {
+                client._rfb_state = "blah";
+                client.sendPassword('pass');
+                expect(client._rfb_state).to.equal('Authentication');
+            });
+
+            it('should call init_msg "soon"', function () {
+                client._init_msg = sinon.spy();
+                client.sendPassword('pass');
+                this.clock.tick(5);
+                expect(client._init_msg).to.have.been.calledOnce;
+            });
+        });
+
+        describe('#sendCtrlAlDel', function () {
+            beforeEach(function () {
+                client._sock = new Websock();
+                client._sock.open('ws://', 'binary');
+                client._sock._websocket._open();
+                sinon.spy(client._sock, 'send');
+                client._rfb_state = "normal";
+                client._view_only = false;
+            });
+
+            it('should sent ctrl[down]-alt[down]-del[down] then del[up]-alt[up]-ctrl[up]', function () {
+                var expected = [];
+                expected = expected.concat(RFB.messages.keyEvent(0xFFE3, 1));
+                expected = expected.concat(RFB.messages.keyEvent(0xFFE9, 1));
+                expected = expected.concat(RFB.messages.keyEvent(0xFFFF, 1));
+                expected = expected.concat(RFB.messages.keyEvent(0xFFFF, 0));
+                expected = expected.concat(RFB.messages.keyEvent(0xFFE9, 0));
+                expected = expected.concat(RFB.messages.keyEvent(0xFFE3, 0));
+
+                client.sendCtrlAltDel();
+                expect(client._sock).to.have.sent(expected);
+            });
+
+            it('should not send the keys if we are not in a normal state', function () {
+                client._rfb_state = "broken";
+                client.sendCtrlAltDel();
+                expect(client._sock.send).to.not.have.been.called;
+            });
+
+            it('should not send the keys if we are set as view_only', function () {
+                client._view_only = true;
+                client.sendCtrlAltDel();
+                expect(client._sock.send).to.not.have.been.called;
+            });
+        });
+
+        describe('#sendKey', function () {
+            beforeEach(function () {
+                client._sock = new Websock();
+                client._sock.open('ws://', 'binary');
+                client._sock._websocket._open();
+                sinon.spy(client._sock, 'send');
+                client._rfb_state = "normal";
+                client._view_only = false;
+            });
+
+            it('should send a single key with the given code and state (down = true)', function () {
+                var expected = RFB.messages.keyEvent(123, 1);
+                client.sendKey(123, true);
+                expect(client._sock).to.have.sent(expected);
+            });
+
+            it('should send both a down and up event if the state is not specified', function () {
+                var expected = RFB.messages.keyEvent(123, 1);
+                expected = expected.concat(RFB.messages.keyEvent(123, 0));
+                client.sendKey(123);
+                expect(client._sock).to.have.sent(expected);
+            });
+
+            it('should not send the key if we are not in a normal state', function () {
+                client._rfb_state = "broken";
+                client.sendKey(123);
+                expect(client._sock.send).to.not.have.been.called;
+            });
+
+            it('should not send the key if we are set as view_only', function () {
+                client._view_only = true;
+                client.sendKey(123);
+                expect(client._sock.send).to.not.have.been.called;
+            });
+        });
+
+        describe('#clipboardPasteFrom', function () {
+            beforeEach(function () {
+                client._sock = new Websock();
+                client._sock.open('ws://', 'binary');
+                client._sock._websocket._open();
+                sinon.spy(client._sock, 'send');
+                client._rfb_state = "normal";
+                client._view_only = false;
+            });
+
+            it('should send the given text in a paste event', function () {
+                var expected = RFB.messages.clientCutText('abc');
+                client.clipboardPasteFrom('abc');
+                expect(client._sock).to.have.sent(expected);
+            });
+
+            it('should not send the text if we are not in a normal state', function () {
+                client._rfb_state = "broken";
+                client.clipboardPasteFrom('abc');
+                expect(client._sock.send).to.not.have.been.called;
+            });
+        });
+
+        describe("XVP operations", function () {
+            beforeEach(function () {
+                client._sock = new Websock();
+                client._sock.open('ws://', 'binary');
+                client._sock._websocket._open();
+                sinon.spy(client._sock, 'send');
+                client._rfb_state = "normal";
+                client._view_only = false;
+                client._rfb_xvp_ver = 1;
+            });
+
+            it('should send the shutdown signal on #xvpShutdown', function () {
+                client.xvpShutdown();
+                expect(client._sock).to.have.sent([0xFA, 0x00, 0x01, 0x02]);
+            });
+
+            it('should send the reboot signal on #xvpReboot', function () {
+                client.xvpReboot();
+                expect(client._sock).to.have.sent([0xFA, 0x00, 0x01, 0x03]);
+            });
+
+            it('should send the reset signal on #xvpReset', function () {
+                client.xvpReset();
+                expect(client._sock).to.have.sent([0xFA, 0x00, 0x01, 0x04]);
+            });
+
+            it('should support sending arbitrary XVP operations via #xvpOp', function () {
+                client.xvpOp(1, 7);
+                expect(client._sock).to.have.sent([0xFA, 0x00, 0x01, 0x07]);
+            });
+
+            it('should not send XVP operations with higher versions than we support', function () {
+                expect(client.xvpOp(2, 7)).to.be.false;
+                expect(client._sock.send).to.not.have.been.called;
+            });
+        });
+    });
+
+    describe('Misc Internals', function () {
+        describe('#_updateState', function () {
+            var client;
+            beforeEach(function () {
+                this.clock = sinon.useFakeTimers();
+                client = make_rfb();
+            });
+
+            afterEach(function () {
+                this.clock.restore();
+            });
+
+            it('should clear the disconnect timer if the state is not disconnect', function () {
+                var spy = sinon.spy();
+                client._disconnTimer = setTimeout(spy, 50);
+                client._updateState('normal');
+                this.clock.tick(51);
+                expect(spy).to.not.have.been.called;
+                expect(client._disconnTimer).to.be.null;
+            });
+        });
+    });
+
+    describe('Page States', function () {
+        describe('loaded', function () {
+            var client;
+            beforeEach(function () { client = make_rfb(); });
+
+            it('should close any open WebSocket connection', function () {
+                sinon.spy(client._sock, 'close');
+                client._updateState('loaded');
+                expect(client._sock.close).to.have.been.calledOnce;
+            });
+        });
+
+        describe('disconnected', function () {
+            var client;
+            beforeEach(function () { client = make_rfb(); });
+
+            it('should close any open WebSocket connection', function () {
+                sinon.spy(client._sock, 'close');
+                client._updateState('disconnected');
+                expect(client._sock.close).to.have.been.calledOnce;
+            });
+        });
+
+        describe('connect', function () {
+            var client;
+            beforeEach(function () { client = make_rfb(); });
+
+            it('should reset the variable states', function () {
+                sinon.spy(client, '_init_vars');
+                client._updateState('connect');
+                expect(client._init_vars).to.have.been.calledOnce;
+            });
+
+            it('should actually connect to the websocket', function () {
+                sinon.spy(client._sock, 'open');
+                client._updateState('connect');
+                expect(client._sock.open).to.have.been.calledOnce;
+            });
+
+            it('should use wss:// to connect if encryption is enabled', function () {
+                sinon.spy(client._sock, 'open');
+                client.set_encrypt(true);
+                client._updateState('connect');
+                expect(client._sock.open.args[0][0]).to.contain('wss://');
+            });
+
+            it('should use ws:// to connect if encryption is not enabled', function () {
+                sinon.spy(client._sock, 'open');
+                client.set_encrypt(true);
+                client._updateState('connect');
+                expect(client._sock.open.args[0][0]).to.contain('wss://');
+            });
+
+            it('should use a uri with the host, port, and path specified to connect', function () {
+                sinon.spy(client._sock, 'open');
+                client.set_encrypt(false);
+                client._rfb_host = 'HOST';
+                client._rfb_port = 8675;
+                client._rfb_path = 'PATH';
+                client._updateState('connect');
+                expect(client._sock.open).to.have.been.calledWith('ws://HOST:8675/PATH');
+            });
+
+            it('should attempt to close the websocket before we open an new one', function () {
+                sinon.spy(client._sock, 'close');
+                client._updateState('connect');
+                expect(client._sock.close).to.have.been.calledOnce;
+            });
+        });
+
+        describe('disconnect', function () {
+            var client;
+            beforeEach(function () {
+                this.clock = sinon.useFakeTimers();
+                client = make_rfb();
+                client.connect('host', 8675);
+            });
+
+            afterEach(function () {
+                this.clock.restore();
+            });
+
+            it('should fail if we do not call Websock.onclose within the disconnection timeout', function () {
+                client._sock._websocket.close = function () {};  // explicitly don't call onclose
+                client._updateState('disconnect');
+                this.clock.tick(client.get_disconnectTimeout() * 1000);
+                expect(client._rfb_state).to.equal('failed');
+            });
+
+            it('should not fail if Websock.onclose gets called within the disconnection timeout', function () {
+                client._updateState('disconnect');
+                this.clock.tick(client.get_disconnectTimeout() * 500);
+                client._sock._websocket.close();
+                this.clock.tick(client.get_disconnectTimeout() * 500 + 1);
+                expect(client._rfb_state).to.equal('disconnected');
+            });
+
+            it('should close the WebSocket connection', function () {
+                sinon.spy(client._sock, 'close');
+                client._updateState('disconnect');
+                expect(client._sock.close).to.have.been.calledTwice; // once on loaded, once on disconnect
+            });
+        });
+
+        describe('failed', function () {
+            var client;
+            beforeEach(function () {
+                this.clock = sinon.useFakeTimers();
+                client = make_rfb();
+                client.connect('host', 8675);
+            });
+
+            afterEach(function () {
+                this.clock.restore();
+            });
+
+            it('should close the WebSocket connection', function () {
+                sinon.spy(client._sock, 'close');
+                client._updateState('failed');
+                expect(client._sock.close).to.have.been.called;
+            });
+
+            it('should transition to disconnected but stay in failed state', function () {
+                client.set_onUpdateState(sinon.spy());
+                client._updateState('failed');
+                this.clock.tick(50);
+                expect(client._rfb_state).to.equal('failed');
+
+                var onUpdateState = client.get_onUpdateState();
+                expect(onUpdateState).to.have.been.called;
+                // it should be specifically the last call
+                expect(onUpdateState.args[onUpdateState.args.length - 1][1]).to.equal('disconnected');
+                expect(onUpdateState.args[onUpdateState.args.length - 1][2]).to.equal('failed');
+            });
+
+        });
+
+        describe('fatal', function () {
+            var client;
+            beforeEach(function () { client = make_rfb(); });
+
+            it('should close any open WebSocket connection', function () {
+                sinon.spy(client._sock, 'close');
+                client._updateState('fatal');
+                expect(client._sock.close).to.have.been.calledOnce;
+            });
+        });
+
+        // NB(directxman12): Normal does *nothing* in updateState
+    });
+
+    describe('Protocol Initialization States', function () {
+        describe('ProtocolVersion', function () {
+            beforeEach(function () {
+                this.clock = sinon.useFakeTimers();
+            });
+
+            afterEach(function () {
+                this.clock.restore();
+            });
+
+            function send_ver (ver, client) {
+                var arr = new Uint8Array(12);
+                for (var i = 0; i < ver.length; i++) {
+                    arr[i+4] = ver.charCodeAt(i);
+                }
+                arr[0] = 'R'; arr[1] = 'F'; arr[2] = 'B'; arr[3] = ' ';
+                arr[11] = '\n';
+                client._sock._websocket._receive_data(arr);
+            }
+
+            describe('version parsing', function () {
+                var client;
+                beforeEach(function () {
+                    client = make_rfb();
+                    client.connect('host', 8675);
+                    client._sock._websocket._open();
+                });
+
+                it('should interpret version 000.000 as a repeater', function () {
+                    client._repeaterID = '\x01\x02\x03\x04\x05';
+                    send_ver('000.000', client);
+                    expect(client._rfb_version).to.equal(0);
+
+                    var sent_data = client._sock._websocket._get_sent_data();
+                    expect(sent_data.slice(0, 5)).to.deep.equal([1, 2, 3, 4, 5]);
+                });
+
+                it('should interpret version 003.003 as version 3.3', function () {
+                    send_ver('003.003', client);
+                    expect(client._rfb_version).to.equal(3.3);
+                });
+
+                it('should interpret version 003.006 as version 3.3', function () {
+                    send_ver('003.006', client);
+                    expect(client._rfb_version).to.equal(3.3);
+                });
+
+                it('should interpret version 003.889 as version 3.3', function () {
+                    send_ver('003.889', client);
+                    expect(client._rfb_version).to.equal(3.3);
+                });
+
+                it('should interpret version 003.007 as version 3.7', function () {
+                    send_ver('003.007', client);
+                    expect(client._rfb_version).to.equal(3.7);
+                });
+
+                it('should interpret version 003.008 as version 3.8', function () {
+                    send_ver('003.008', client);
+                    expect(client._rfb_version).to.equal(3.8);
+                });
+
+                it('should interpret version 004.000 as version 3.8', function () {
+                    send_ver('004.000', client);
+                    expect(client._rfb_version).to.equal(3.8);
+                });
+
+                it('should interpret version 004.001 as version 3.8', function () {
+                    send_ver('004.001', client);
+                    expect(client._rfb_version).to.equal(3.8);
+                });
+
+                it('should fail on an invalid version', function () {
+                    send_ver('002.000', client);
+                    expect(client._rfb_state).to.equal('failed');
+                });
+            });
+
+            var client;
+            beforeEach(function () {
+                client = make_rfb();
+                client.connect('host', 8675);
+                client._sock._websocket._open();
+            });
+
+            it('should handle two step repeater negotiation', function () {
+                client._repeaterID = '\x01\x02\x03\x04\x05';
+
+                send_ver('000.000', client);
+                expect(client._rfb_version).to.equal(0);
+                var sent_data = client._sock._websocket._get_sent_data();
+                expect(sent_data.slice(0, 5)).to.deep.equal([1, 2, 3, 4, 5]);
+                expect(sent_data).to.have.length(250);
+
+                send_ver('003.008', client);
+                expect(client._rfb_version).to.equal(3.8);
+            });
+
+            it('should initialize the flush interval', function () {
+                client._sock.flush = sinon.spy();
+                send_ver('003.008', client);
+                this.clock.tick(100);
+                expect(client._sock.flush).to.have.been.calledThrice;
+            });
+
+            it('should send back the interpreted version', function () {
+                send_ver('004.000', client);
+
+                var expected_str = 'RFB 003.008\n';
+                var expected = [];
+                for (var i = 0; i < expected_str.length; i++) {
+                    expected[i] = expected_str.charCodeAt(i);
+                }
+
+                expect(client._sock).to.have.sent(expected);
+            });
+
+            it('should transition to the Security state on successful negotiation', function () {
+                send_ver('003.008', client);
+                expect(client._rfb_state).to.equal('Security');
+            });
+        });
+
+        describe('Security', function () {
+            var client;
+
+            beforeEach(function () {
+                client = make_rfb();
+                client.connect('host', 8675);
+                client._sock._websocket._open();
+                client._rfb_state = 'Security';
+            });
+
+            it('should simply receive the auth scheme when for versions < 3.7', function () {
+                client._rfb_version = 3.6;
+                var auth_scheme_raw = [1, 2, 3, 4];
+                var auth_scheme = (auth_scheme_raw[0] << 24) + (auth_scheme_raw[1] << 16) +
+                                  (auth_scheme_raw[2] << 8) + auth_scheme_raw[3];
+                client._sock._websocket._receive_data(auth_scheme_raw);
+                expect(client._rfb_auth_scheme).to.equal(auth_scheme);
+            });
+
+            it('should choose for the most prefered scheme possible for versions >= 3.7', function () {
+                client._rfb_version = 3.7;
+                var auth_schemes = [2, 1, 2];
+                client._sock._websocket._receive_data(auth_schemes);
+                expect(client._rfb_auth_scheme).to.equal(2);
+                expect(client._sock).to.have.sent([2]);
+            });
+
+            it('should fail if there are no supported schemes for versions >= 3.7', function () {
+                client._rfb_version = 3.7;
+                var auth_schemes = [1, 32];
+                client._sock._websocket._receive_data(auth_schemes);
+                expect(client._rfb_state).to.equal('failed');
+            });
+
+            it('should fail with the appropriate message if no types are sent for versions >= 3.7', function () {
+                client._rfb_version = 3.7;
+                var failure_data = [0, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115];
+                sinon.spy(client, '_fail');
+                client._sock._websocket._receive_data(failure_data);
+
+                expect(client._fail).to.have.been.calledTwice;
+                expect(client._fail).to.have.been.calledWith('Security failure: whoops');
+            });
+
+            it('should transition to the Authentication state and continue on successful negotiation', function () {
+                client._rfb_version = 3.7;
+                var auth_schemes = [1, 1];
+                client._negotiate_authentication = sinon.spy();
+                client._sock._websocket._receive_data(auth_schemes);
+                expect(client._rfb_state).to.equal('Authentication');
+                expect(client._negotiate_authentication).to.have.been.calledOnce;
+            });
+        });
+
+        describe('Authentication', function () {
+            var client;
+
+            beforeEach(function () {
+                client = make_rfb();
+                client.connect('host', 8675);
+                client._sock._websocket._open();
+                client._rfb_state = 'Security';
+            });
+
+            function send_security(type, cl) {
+                cl._sock._websocket._receive_data(new Uint8Array([1, type]));
+            }
+
+            it('should fail on auth scheme 0 (pre 3.7) with the given message', function () {
+                client._rfb_version = 3.6;
+                var err_msg = "Whoopsies";
+                var data = [0, 0, 0, 0];
+                var err_len = err_msg.length;
+                data.push32(err_len);
+                for (var i = 0; i < err_len; i++) {
+                    data.push(err_msg.charCodeAt(i));
+                }
+
+                sinon.spy(client, '_fail');
+                client._sock._websocket._receive_data(new Uint8Array(data));
+                expect(client._rfb_state).to.equal('failed');
+                expect(client._fail).to.have.been.calledWith('Auth failure: Whoopsies');
+            });
+
+            it('should transition straight to SecurityResult on "no auth" (1) for versions >= 3.8', function () {
+                client._rfb_version = 3.8;
+                send_security(1, client);
+                expect(client._rfb_state).to.equal('SecurityResult');
+            });
+
+            it('should transition straight to ClientInitialisation on "no auth" for versions < 3.8', function () {
+                client._rfb_version = 3.7;
+                sinon.spy(client, '_updateState');
+                send_security(1, client);
+                expect(client._updateState).to.have.been.calledWith('ClientInitialisation');
+                expect(client._rfb_state).to.equal('ServerInitialisation');
+            });
+
+            it('should fail on an unknown auth scheme', function () {
+                client._rfb_version = 3.8;
+                send_security(57, client);
+                expect(client._rfb_state).to.equal('failed');
+            });
+
+            describe('VNC Authentication (type 2) Handler', function () {
+                var client;
+
+                beforeEach(function () {
+                    client = make_rfb();
+                    client.connect('host', 8675);
+                    client._sock._websocket._open();
+                    client._rfb_state = 'Security';
+                    client._rfb_version = 3.8;
+                });
+
+                it('should transition to the "password" state if missing a password', function () {
+                    send_security(2, client);
+                    expect(client._rfb_state).to.equal('password');
+                });
+
+                it('should encrypt the password with DES and then send it back', function () {
+                    client._rfb_password = 'passwd';
+                    send_security(2, client);
+                    client._sock._websocket._get_sent_data(); // skip the choice of auth reply
+
+                    var challenge = [];
+                    for (var i = 0; i < 16; i++) { challenge[i] = i; }
+                    client._sock._websocket._receive_data(new Uint8Array(challenge));
+
+                    var des_pass = RFB.genDES('passwd', challenge);
+                    expect(client._sock).to.have.sent(des_pass);
+                });
+
+                it('should transition to SecurityResult immediately after sending the password', function () {
+                    client._rfb_password = 'passwd';
+                    send_security(2, client);
+
+                    var challenge = [];
+                    for (var i = 0; i < 16; i++) { challenge[i] = i; }
+                    client._sock._websocket._receive_data(new Uint8Array(challenge));
+
+                    expect(client._rfb_state).to.equal('SecurityResult');
+                });
+            });
+
+            describe('XVP Authentication (type 22) Handler', function () {
+                var client;
+
+                beforeEach(function () {
+                    client = make_rfb();
+                    client.connect('host', 8675);
+                    client._sock._websocket._open();
+                    client._rfb_state = 'Security';
+                    client._rfb_version = 3.8;
+                });
+
+                it('should fall through to standard VNC authentication upon completion', function () {
+                    client.set_xvp_password_sep('#');
+                    client._rfb_password = 'user#target#password';
+                    client._negotiate_std_vnc_auth = sinon.spy();
+                    send_security(22, client);
+                    expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce;
+                });
+
+                it('should transition to the "password" state if the passwords is missing', function() {
+                    send_security(22, client);
+                    expect(client._rfb_state).to.equal('password');
+                });
+
+                it('should transition to the "password" state if the passwords is improperly formatted', function() {
+                    client._rfb_password = 'user@target';
+                    send_security(22, client);
+                    expect(client._rfb_state).to.equal('password');
+                });
+
+                it('should split the password, send the first two parts, and pass on the last part', function () {
+                    client.set_xvp_password_sep('#');
+                    client._rfb_password = 'user#target#password';
+                    client._negotiate_std_vnc_auth = sinon.spy();
+
+                    send_security(22, client);
+
+                    expect(client._rfb_password).to.equal('password');
+
+                    var expected = [22, 4, 6]; // auth selection, len user, len target
+                    for (var i = 0; i < 10; i++) { expected[i+3] = 'usertarget'.charCodeAt(i); }
+
+                    expect(client._sock).to.have.sent(expected);
+                });
+            });
+
+            describe('TightVNC Authentication (type 16) Handler', function () {
+                var client;
+
+                beforeEach(function () {
+                    client = make_rfb();
+                    client.connect('host', 8675);
+                    client._sock._websocket._open();
+                    client._rfb_state = 'Security';
+                    client._rfb_version = 3.8;
+                    send_security(16, client);
+                    client._sock._websocket._get_sent_data();  // skip the security reply
+                });
+
+                function send_num_str_pairs(pairs, client) {
+                    var pairs_len = pairs.length;
+                    var data = [];
+                    data.push32(pairs_len);
+
+                    for (var i = 0; i < pairs_len; i++) {
+                        data.push32(pairs[i][0]);
+                        var j;
+                        for (j = 0; j < 4; j++) {
+                            data.push(pairs[i][1].charCodeAt(j));
+                        }
+                        for (j = 0; j < 8; j++) {
+                            data.push(pairs[i][2].charCodeAt(j));
+                        }
+                    }
+
+                    client._sock._websocket._receive_data(new Uint8Array(data));
+                }
+
+                it('should skip tunnel negotiation if no tunnels are requested', function () {
+                    client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0]));
+                    expect(client._rfb_tightvnc).to.be.true;
+                });
+
+                it('should fail if no supported tunnels are listed', function () {
+                    send_num_str_pairs([[123, 'OTHR', 'SOMETHNG']], client);
+                    expect(client._rfb_state).to.equal('failed');
+                });
+
+                it('should choose the notunnel tunnel type', function () {
+                    send_num_str_pairs([[0, 'TGHT', 'NOTUNNEL'], [123, 'OTHR', 'SOMETHNG']], client);
+                    expect(client._sock).to.have.sent([0, 0, 0, 0]);
+                });
+
+                it('should continue to sub-auth negotiation after tunnel negotiation', function () {
+                    send_num_str_pairs([[0, 'TGHT', 'NOTUNNEL']], client);
+                    client._sock._websocket._get_sent_data();  // skip the tunnel choice here
+                    send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client);
+                    expect(client._sock).to.have.sent([0, 0, 0, 1]);
+                    expect(client._rfb_state).to.equal('SecurityResult');
+                });
+
+                /*it('should attempt to use VNC auth over no auth when possible', function () {
+                    client._rfb_tightvnc = true;
+                    client._negotiate_std_vnc_auth = sinon.spy();
+                    send_num_str_pairs([[1, 'STDV', 'NOAUTH__'], [2, 'STDV', 'VNCAUTH_']], client);
+                    expect(client._sock).to.have.sent([0, 0, 0, 1]);
+                    expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce;
+                    expect(client._rfb_auth_scheme).to.equal(2);
+                });*/ // while this would make sense, the original code doesn't actually do this
+
+                it('should accept the "no auth" auth type and transition to SecurityResult', function () {
+                    client._rfb_tightvnc = true;
+                    send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client);
+                    expect(client._sock).to.have.sent([0, 0, 0, 1]);
+                    expect(client._rfb_state).to.equal('SecurityResult');
+                });
+
+                it('should accept VNC authentication and transition to that', function () {
+                    client._rfb_tightvnc = true;
+                    client._negotiate_std_vnc_auth = sinon.spy();
+                    send_num_str_pairs([[2, 'STDV', 'VNCAUTH__']], client);
+                    expect(client._sock).to.have.sent([0, 0, 0, 2]);
+                    expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce;
+                    expect(client._rfb_auth_scheme).to.equal(2);
+                });
+
+                it('should fail if there are no supported auth types', function () {
+                    client._rfb_tightvnc = true;
+                    send_num_str_pairs([[23, 'stdv', 'badval__']], client);
+                    expect(client._rfb_state).to.equal('failed');
+                });
+            });
+        });
+
+        describe('SecurityResult', function () {
+            var client;
+
+            beforeEach(function () {
+                client = make_rfb();
+                client.connect('host', 8675);
+                client._sock._websocket._open();
+                client._rfb_state = 'SecurityResult';
+            });
+
+            it('should fall through to ClientInitialisation on a response code of 0', function () {
+                client._updateState = sinon.spy();
+                client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0]));
+                expect(client._updateState).to.have.been.calledOnce;
+                expect(client._updateState).to.have.been.calledWith('ClientInitialisation');
+            });
+
+            it('should fail on an error code of 1 with the given message for versions >= 3.8', function () {
+                client._rfb_version = 3.8;
+                sinon.spy(client, '_fail');
+                var failure_data = [0, 0, 0, 1, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115];
+                client._sock._websocket._receive_data(new Uint8Array(failure_data));
+                expect(client._rfb_state).to.equal('failed');
+                expect(client._fail).to.have.been.calledWith('whoops');
+            });
+
+            it('should fail on an error code of 1 with a standard message for version < 3.8', function () {
+                client._rfb_version = 3.7;
+                client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 1]));
+                expect(client._rfb_state).to.equal('failed');
+            });
+        });
+
+        describe('ClientInitialisation', function () {
+            var client;
+
+            beforeEach(function () {
+                client = make_rfb();
+                client.connect('host', 8675);
+                client._sock._websocket._open();
+                client._rfb_state = 'SecurityResult';
+            });
+
+            it('should transition to the ServerInitialisation state', function () {
+                client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0]));
+                expect(client._rfb_state).to.equal('ServerInitialisation');
+            });
+
+            it('should send 1 if we are in shared mode', function () {
+                client.set_shared(true);
+                client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0]));
+                expect(client._sock).to.have.sent([1]);
+            });
+
+            it('should send 0 if we are not in shared mode', function () {
+                client.set_shared(false);
+                client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0]));
+                expect(client._sock).to.have.sent([0]);
+            });
+        });
+
+        describe('ServerInitialisation', function () {
+            var client;
+
+            beforeEach(function () {
+                client = make_rfb();
+                client.connect('host', 8675);
+                client._sock._websocket._open();
+                client._rfb_state = 'ServerInitialisation';
+            });
+
+            function send_server_init(opts, client) {
+                var full_opts = { width: 10, height: 12, bpp: 24, depth: 24, big_endian: 0,
+                                  true_color: 1, red_max: 255, green_max: 255, blue_max: 255,
+                                  red_shift: 16, green_shift: 8, blue_shift: 0, name: 'a name' };
+                for (var opt in opts) {
+                    full_opts[opt] = opts[opt];
+                }
+                var data = [];
+
+                data.push16(full_opts.width);
+                data.push16(full_opts.height);
+
+                data.push(full_opts.bpp);
+                data.push(full_opts.depth);
+                data.push(full_opts.big_endian);
+                data.push(full_opts.true_color);
+
+                data.push16(full_opts.red_max);
+                data.push16(full_opts.green_max);
+                data.push16(full_opts.blue_max);
+                data.push8(full_opts.red_shift);
+                data.push8(full_opts.green_shift);
+                data.push8(full_opts.blue_shift);
+
+                // padding
+                data.push8(0);
+                data.push8(0);
+                data.push8(0);
+
+                client._sock._websocket._receive_data(new Uint8Array(data));
+
+                var name_data = [];
+                name_data.push32(full_opts.name.length);
+                for (var i = 0; i < full_opts.name.length; i++) {
+                    name_data.push(full_opts.name.charCodeAt(i));
+                }
+                client._sock._websocket._receive_data(new Uint8Array(name_data));
+            }
+
+            it('should set the framebuffer width and height', function () {
+                send_server_init({ width: 32, height: 84 }, client);
+                expect(client._fb_width).to.equal(32);
+                expect(client._fb_height).to.equal(84);
+            });
+
+            // NB(sross): we just warn, not fail, for endian-ness and shifts, so we don't test them
+
+            it('should set the framebuffer name and call the callback', function () {
+                client.set_onDesktopName(sinon.spy());
+                send_server_init({ name: 'some name' }, client);
+
+                var spy = client.get_onDesktopName();
+                expect(client._fb_name).to.equal('some name');
+                expect(spy).to.have.been.calledOnce;
+                expect(spy.args[0][1]).to.equal('some name');
+            });
+
+            it('should handle the extended init message of the tight encoding', function () {
+                // NB(sross): we don't actually do anything with it, so just test that we can
+                //            read it w/o throwing an error
+                client._rfb_tightvnc = true;
+                send_server_init({}, client);
+
+                var tight_data = [];
+                tight_data.push16(1);
+                tight_data.push16(2);
+                tight_data.push16(3);
+                tight_data.push16(0);
+                for (var i = 0; i < 16 + 32 + 48; i++) {
+                    tight_data.push(i);
+                }
+                client._sock._websocket._receive_data(tight_data);
+
+                expect(client._rfb_state).to.equal('normal');
+            });
+
+            it('should set the true color mode on the display to the configuration variable', function () {
+                client.set_true_color(false);
+                sinon.spy(client._display, 'set_true_color');
+                send_server_init({ true_color: 1 }, client);
+                expect(client._display.set_true_color).to.have.been.calledOnce;
+                expect(client._display.set_true_color).to.have.been.calledWith(false);
+            });
+
+            it('should call the resize callback and resize the display', function () {
+                client.set_onFBResize(sinon.spy());
+                sinon.spy(client._display, 'resize');
+                send_server_init({ width: 27, height: 32 }, client);
+
+                var spy = client.get_onFBResize();
+                expect(client._display.resize).to.have.been.calledOnce;
+                expect(client._display.resize).to.have.been.calledWith(27, 32);
+                expect(spy).to.have.been.calledOnce;
+                expect(spy.args[0][1]).to.equal(27);
+                expect(spy.args[0][2]).to.equal(32);
+            });
+
+            it('should grab the mouse and keyboard', function () {
+                sinon.spy(client._keyboard, 'grab');
+                sinon.spy(client._mouse, 'grab');
+                send_server_init({}, client);
+                expect(client._keyboard.grab).to.have.been.calledOnce;
+                expect(client._mouse.grab).to.have.been.calledOnce;
+            });
+
+            it('should set the BPP and depth to 4 and 3 respectively if in true color mode', function () {
+                client.set_true_color(true);
+                send_server_init({}, client);
+                expect(client._fb_Bpp).to.equal(4);
+                expect(client._fb_depth).to.equal(3);
+            });
+
+            it('should set the BPP and depth to 1 and 1 respectively if not in true color mode', function () {
+                client.set_true_color(false);
+                send_server_init({}, client);
+                expect(client._fb_Bpp).to.equal(1);
+                expect(client._fb_depth).to.equal(1);
+            });
+
+            // TODO(directxman12): test the various options in this configuration matrix
+            it('should reply with the pixel format, client encodings, and initial update request', function () {
+                client.set_true_color(true);
+                client.set_local_cursor(false);
+                var expected = RFB.messages.pixelFormat(4, 3, true);
+                expected = expected.concat(RFB.messages.clientEncodings(client._encodings, false, true));
+                var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 },
+                                     dirtyBoxes: [ { x: 0, y: 0, w: 27, h: 32 } ] };
+                expected = expected.concat(RFB.messages.fbUpdateRequests(expected_cdr, 27, 32));
+
+                send_server_init({ width: 27, height: 32 }, client);
+                expect(client._sock).to.have.sent(expected);
+            });
+
+            it('should check for sending mouse events', function () {
+                // be lazy with our checking so we don't have to check through the whole sent buffer
+                sinon.spy(client, '_checkEvents');
+                send_server_init({}, client);
+                expect(client._checkEvents).to.have.been.calledOnce;
+            });
+
+            it('should transition to the "normal" state', function () {
+                send_server_init({}, client);
+                expect(client._rfb_state).to.equal('normal');
+            });
+        });
+    });
+
+    describe('Protocol Message Processing After Completing Initialization', function () {
+        var client;
+
+        beforeEach(function () {
+            client = make_rfb();
+            client.connect('host', 8675);
+            client._sock._websocket._open();
+            client._rfb_state = 'normal';
+            client._fb_name = 'some device';
+            client._fb_width = 640;
+            client._fb_height = 20;
+        });
+
+        describe('Framebuffer Update Handling', function () {
+            var client;
+
+            beforeEach(function () {
+                client = make_rfb();
+                client.connect('host', 8675);
+                client._sock._websocket._open();
+                client._rfb_state = 'normal';
+                client._fb_name = 'some device';
+                client._fb_width = 640;
+                client._fb_height = 20;
+            });
+
+            var target_data_arr = [
+                0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
+                0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
+                0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255,
+                0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255
+            ];
+            var target_data;
+
+            var target_data_check_arr = [
+                0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
+                0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
+                0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
+                0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
+            ];
+            var target_data_check;
+
+            before(function () {
+                // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray
+                target_data = new Uint8Array(target_data_arr);
+                target_data_check = new Uint8Array(target_data_check_arr);
+            });
+
+            function send_fbu_msg (rect_info, rect_data, client, rect_cnt) {
+                var data = [];
+
+                if (!rect_cnt || rect_cnt > -1) {
+                    // header
+                    data.push(0);  // msg type
+                    data.push(0);  // padding
+                    data.push16(rect_cnt || rect_data.length);
+                }
+
+                for (var i = 0; i < rect_data.length; i++) {
+                    if (rect_info[i]) {
+                        data.push16(rect_info[i].x);
+                        data.push16(rect_info[i].y);
+                        data.push16(rect_info[i].width);
+                        data.push16(rect_info[i].height);
+                        data.push32(rect_info[i].encoding);
+                    }
+                    data = data.concat(rect_data[i]);
+                }
+
+                client._sock._websocket._receive_data(new Uint8Array(data));
+            }
+
+            it('should send an update request if there is sufficient data', function () {
+                var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 },
+                                     dirtyBoxes: [ { x: 0, y: 0, w: 640, h: 20 } ] };
+                var expected_msg = RFB.messages.fbUpdateRequests(expected_cdr, 640, 20);
+
+                client._framebufferUpdate = function () { return true; };
+                client._sock._websocket._receive_data(new Uint8Array([0]));
+
+                expect(client._sock).to.have.sent(expected_msg);
+            });
+
+            it('should not send an update request if we need more data', function () {
+                client._sock._websocket._receive_data(new Uint8Array([0]));
+                expect(client._sock._websocket._get_sent_data()).to.have.length(0);
+            });
+
+            it('should resume receiving an update if we previously did not have enough data', function () {
+                var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 },
+                                     dirtyBoxes: [ { x: 0, y: 0, w: 640, h: 20 } ] };
+                var expected_msg = RFB.messages.fbUpdateRequests(expected_cdr, 640, 20);
+
+                // just enough to set FBU.rects
+                client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 3]));
+                expect(client._sock._websocket._get_sent_data()).to.have.length(0);
+
+                client._framebufferUpdate = function () { return true; };  // we magically have enough data
+                // 247 should *not* be used as the message type here
+                client._sock._websocket._receive_data(new Uint8Array([247]));
+                expect(client._sock).to.have.sent(expected_msg);
+            });
+
+            it('should parse out information from a header before any actual data comes in', function () {
+                client.set_onFBUReceive(sinon.spy());
+                var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 0x02, encodingName: 'RRE' };
+                send_fbu_msg([rect_info], [[]], client);
+
+                var spy = client.get_onFBUReceive();
+                expect(spy).to.have.been.calledOnce;
+                expect(spy).to.have.been.calledWith(sinon.match.any, rect_info);
+            });
+
+            it('should fire onFBUComplete when the update is complete', function () {
+                client.set_onFBUComplete(sinon.spy());
+                var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: -224, encodingName: 'last_rect' };
+                send_fbu_msg([rect_info], [[]], client);  // last_rect
+
+                var spy = client.get_onFBUComplete();
+                expect(spy).to.have.been.calledOnce;
+                expect(spy).to.have.been.calledWith(sinon.match.any, rect_info);
+            });
+
+            it('should not fire onFBUComplete if we have not finished processing the update', function () {
+                client.set_onFBUComplete(sinon.spy());
+                var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 0x00, encodingName: 'RAW' };
+                send_fbu_msg([rect_info], [[]], client);
+                expect(client.get_onFBUComplete()).to.not.have.been.called;
+            });
+
+            it('should call the appropriate encoding handler', function () {
+                client._encHandlers[0x02] = sinon.spy();
+                var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 0x02 };
+                send_fbu_msg([rect_info], [[]], client);
+                expect(client._encHandlers[0x02]).to.have.been.calledOnce;
+            });
+
+            it('should fail on an unsupported encoding', function () {
+                client.set_onFBUReceive(sinon.spy());
+                var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 234 };
+                send_fbu_msg([rect_info], [[]], client);
+                expect(client._rfb_state).to.equal('failed');
+            });
+
+            it('should be able to pause and resume receiving rects if not enought data', function () {
+                // seed some initial data to copy
+                client._fb_width = 4;
+                client._fb_height = 4;
+                client._display.resize(4, 4);
+                var initial_data = client._display._drawCtx.createImageData(4, 2);
+                var initial_data_arr = target_data_check_arr.slice(0, 32);
+                for (var i = 0; i < 32; i++) { initial_data.data[i] = initial_data_arr[i]; }
+                client._display._drawCtx.putImageData(initial_data, 0, 0);
+
+                var info = [{ x: 0, y: 2, width: 2, height: 2, encoding: 0x01},
+                            { x: 2, y: 2, width: 2, height: 2, encoding: 0x01}];
+                // data says [{ old_x: 0, old_y: 0 }, { old_x: 0, old_y: 0 }]
+                var rects = [[0, 2, 0, 0], [0, 0, 0, 0]];
+                send_fbu_msg([info[0]], [rects[0]], client, 2);
+                send_fbu_msg([info[1]], [rects[1]], client, -1);
+                expect(client._display).to.have.displayed(target_data_check);
+            });
+
+            describe('Message Encoding Handlers', function () {
+                var client;
+
+                beforeEach(function () {
+                    client = make_rfb();
+                    client.connect('host', 8675);
+                    client._sock._websocket._open();
+                    client._rfb_state = 'normal';
+                    client._fb_name = 'some device';
+                    // a really small frame
+                    client._fb_width = 4;
+                    client._fb_height = 4;
+                    client._display._fb_width = 4;
+                    client._display._fb_height = 4;
+                    client._fb_Bpp = 4;
+                });
+
+                it('should handle the RAW encoding', function () {
+                    var info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 },
+                                { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 },
+                                { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 },
+                                { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }];
+                    // data is in bgrx
+                    var rects = [
+                        [0x00, 0x00, 0xff, 0, 0x00, 0xff, 0x00, 0, 0x00, 0xff, 0x00, 0, 0x00, 0x00, 0xff, 0],
+                        [0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0],
+                        [0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0],
+                        [0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0]];
+                    send_fbu_msg(info, rects, client);
+                    expect(client._display).to.have.displayed(target_data);
+                });
+
+                it('should handle the COPYRECT encoding', function () {
+                    // seed some initial data to copy
+                    var initial_data = client._display._drawCtx.createImageData(4, 2);
+                    var initial_data_arr = target_data_check_arr.slice(0, 32);
+                    for (var i = 0; i < 32; i++) { initial_data.data[i] = initial_data_arr[i]; }
+                    client._display._drawCtx.putImageData(initial_data, 0, 0);
+
+                    var info = [{ x: 0, y: 2, width: 2, height: 2, encoding: 0x01},
+                                { x: 2, y: 2, width: 2, height: 2, encoding: 0x01}];
+                    // data says [{ old_x: 0, old_y: 0 }, { old_x: 0, old_y: 0 }]
+                    var rects = [[0, 2, 0, 0], [0, 0, 0, 0]];
+                    send_fbu_msg(info, rects, client);
+                    expect(client._display).to.have.displayed(target_data_check);
+                });
+
+                // TODO(directxman12): for encodings with subrects, test resuming on partial send?
+                // TODO(directxman12): test rre_chunk_sz (related to above about subrects)?
+
+                it('should handle the RRE encoding', function () {
+                    var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x02 }];
+                    var rect = [];
+                    rect.push32(2); // 2 subrects
+                    rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color
+                    rect.push(0xff); // becomes ff0000ff --> #0000FF color
+                    rect.push(0x00);
+                    rect.push(0x00);
+                    rect.push(0xff);
+                    rect.push16(0); // x: 0
+                    rect.push16(0); // y: 0
+                    rect.push16(2); // width: 2
+                    rect.push16(2); // height: 2
+                    rect.push(0xff); // becomes ff0000ff --> #0000FF color
+                    rect.push(0x00);
+                    rect.push(0x00);
+                    rect.push(0xff);
+                    rect.push16(2); // x: 2
+                    rect.push16(2); // y: 2
+                    rect.push16(2); // width: 2
+                    rect.push16(2); // height: 2
+
+                    send_fbu_msg(info, [rect], client);
+                    expect(client._display).to.have.displayed(target_data_check);
+                });
+
+                describe('the HEXTILE encoding handler', function () {
+                    var client;
+                    beforeEach(function () {
+                        client = make_rfb();
+                        client.connect('host', 8675);
+                        client._sock._websocket._open();
+                        client._rfb_state = 'normal';
+                        client._fb_name = 'some device';
+                        // a really small frame
+                        client._fb_width = 4;
+                        client._fb_height = 4;
+                        client._display._fb_width = 4;
+                        client._display._fb_height = 4;
+                        client._fb_Bpp = 4;
+                    });
+
+                    it('should handle a tile with fg, bg specified, normal subrects', function () {
+                        var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }];
+                        var rect = [];
+                        rect.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects
+                        rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color
+                        rect.push(0xff); // becomes ff0000ff --> #0000FF fg color
+                        rect.push(0x00);
+                        rect.push(0x00);
+                        rect.push(0xff);
+                        rect.push(2); // 2 subrects
+                        rect.push(0); // x: 0, y: 0
+                        rect.push(1 | (1 << 4)); // width: 2, height: 2
+                        rect.push(2 | (2 << 4)); // x: 2, y: 2
+                        rect.push(1 | (1 << 4)); // width: 2, height: 2
+                        send_fbu_msg(info, [rect], client);
+                        expect(client._display).to.have.displayed(target_data_check);
+                    });
+
+                    it('should handle a raw tile', function () {
+                        var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }];
+                        var rect = [];
+                        rect.push(0x01); // raw
+                        for (var i = 0; i < target_data.length; i += 4) {
+                            rect.push(target_data[i + 2]);
+                            rect.push(target_data[i + 1]);
+                            rect.push(target_data[i]);
+                            rect.push(target_data[i + 3]);
+                        }
+                        send_fbu_msg(info, [rect], client);
+                        expect(client._display).to.have.displayed(target_data);
+                    });
+
+                    it('should handle a tile with only bg specified (solid bg)', function () {
+                        var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }];
+                        var rect = [];
+                        rect.push(0x02);
+                        rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color
+                        send_fbu_msg(info, [rect], client);
+
+                        var expected = [];
+                        for (var i = 0; i < 16; i++) { expected.push32(0xff00ff); }
+                        expect(client._display).to.have.displayed(new Uint8Array(expected));
+                    });
+
+                    it('should handle a tile with bg and coloured subrects', function () {
+                        var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }];
+                        var rect = [];
+                        rect.push(0x02 | 0x08 | 0x10); // bg spec, anysubrects, colouredsubrects
+                        rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color
+                        rect.push(2); // 2 subrects
+                        rect.push(0xff); // becomes ff0000ff --> #0000FF fg color
+                        rect.push(0x00);
+                        rect.push(0x00);
+                        rect.push(0xff);
+                        rect.push(0); // x: 0, y: 0
+                        rect.push(1 | (1 << 4)); // width: 2, height: 2
+                        rect.push(0xff); // becomes ff0000ff --> #0000FF fg color
+                        rect.push(0x00);
+                        rect.push(0x00);
+                        rect.push(0xff);
+                        rect.push(2 | (2 << 4)); // x: 2, y: 2
+                        rect.push(1 | (1 << 4)); // width: 2, height: 2
+                        send_fbu_msg(info, [rect], client);
+                        expect(client._display).to.have.displayed(target_data_check);
+                    });
+
+                    it('should carry over fg and bg colors from the previous tile if not specified', function () {
+                        client._fb_width = 4;
+                        client._fb_height = 17;
+                        client._display.resize(4, 17);
+
+                        var info = [{ x: 0, y: 0, width: 4, height: 17, encoding: 0x05}];
+                        var rect = [];
+                        rect.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects
+                        rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color
+                        rect.push(0xff); // becomes ff0000ff --> #0000FF fg color
+                        rect.push(0x00);
+                        rect.push(0x00);
+                        rect.push(0xff);
+                        rect.push(8); // 8 subrects
+                        var i;
+                        for (i = 0; i < 4; i++) {
+                            rect.push((0 << 4) | (i * 4)); // x: 0, y: i*4
+                            rect.push(1 | (1 << 4)); // width: 2, height: 2
+                            rect.push((2 << 4) | (i * 4 + 2)); // x: 2, y: i * 4 + 2
+                            rect.push(1 | (1 << 4)); // width: 2, height: 2
+                        }
+                        rect.push(0x08); // anysubrects
+                        rect.push(1); // 1 subrect
+                        rect.push(0); // x: 0, y: 0
+                        rect.push(1 | (1 << 4)); // width: 2, height: 2
+                        send_fbu_msg(info, [rect], client);
+
+                        var expected = [];
+                        for (i = 0; i < 4; i++) { expected = expected.concat(target_data_check_arr); }
+                        expected = expected.concat(target_data_check_arr.slice(0, 16));
+                        expect(client._display).to.have.displayed(new Uint8Array(expected));
+                    });
+
+                    it('should fail on an invalid subencoding', function () {
+                        var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }];
+                        var rects = [[45]];  // an invalid subencoding
+                        send_fbu_msg(info, rects, client);
+                        expect(client._rfb_state).to.equal('failed');
+                    });
+                });
+
+                it.skip('should handle the TIGHT encoding', function () {
+                    // TODO(directxman12): test this
+                });
+
+                it.skip('should handle the TIGHT_PNG encoding', function () {
+                    // TODO(directxman12): test this
+                });
+
+                it('should handle the DesktopSize pseduo-encoding', function () {
+                    client.set_onFBResize(sinon.spy());
+                    sinon.spy(client._display, 'resize');
+                    send_fbu_msg([{ x: 0, y: 0, width: 20, height: 50, encoding: -223 }], [[]], client);
+
+                    var spy = client.get_onFBResize();
+                    expect(spy).to.have.been.calledOnce;
+                    expect(spy).to.have.been.calledWith(sinon.match.any, 20, 50);
+
+                    expect(client._fb_width).to.equal(20);
+                    expect(client._fb_height).to.equal(50);
+
+                    expect(client._display.resize).to.have.been.calledOnce;
+                    expect(client._display.resize).to.have.been.calledWith(20, 50);
+                });
+
+                it.skip('should handle the Cursor pseudo-encoding', function () {
+                    // TODO(directxman12): test
+                });
+
+                it('should handle the last_rect pseudo-encoding', function () {
+                    client.set_onFBUReceive(sinon.spy());
+                    send_fbu_msg([{ x: 0, y: 0, width: 0, height: 0, encoding: -224}], [[]], client, 100);
+                    expect(client._FBU.rects).to.equal(0);
+                    expect(client.get_onFBUReceive()).to.have.been.calledOnce;
+                });
+            });
+        });
+
+        it('should set the colour map on the display on SetColourMapEntries', function () {
+            var expected_cm = [];
+            var data = [1, 0, 0, 1, 0, 4];
+            var i;
+            for (i = 0; i < 4; i++) {
+                expected_cm[i + 1] = [i * 10, i * 10 + 1, i * 10 + 2];
+                data.push16(expected_cm[i + 1][2] << 8);
+                data.push16(expected_cm[i + 1][1] << 8);
+                data.push16(expected_cm[i + 1][0] << 8);
+            }
+
+            client._sock._websocket._receive_data(new Uint8Array(data));
+            expect(client._display.get_colourMap()).to.deep.equal(expected_cm);
+        });
+
+        describe('XVP Message Handling', function () {
+            beforeEach(function () {
+                client = make_rfb();
+                client.connect('host', 8675);
+                client._sock._websocket._open();
+                client._rfb_state = 'normal';
+                client._fb_name = 'some device';
+                client._fb_width = 27;
+                client._fb_height = 32;
+            });
+
+            it('should call updateState with a message on XVP_FAIL, but keep the same state', function () {
+                client._updateState = sinon.spy();
+                client._sock._websocket._receive_data(new Uint8Array([250, 0, 10, 0]));
+                expect(client._updateState).to.have.been.calledOnce;
+                expect(client._updateState).to.have.been.calledWith('normal', 'Operation Failed');
+            });
+
+            it('should set the XVP version and fire the callback with the version on XVP_INIT', function () {
+                client.set_onXvpInit(sinon.spy());
+                client._sock._websocket._receive_data(new Uint8Array([250, 0, 10, 1]));
+                expect(client._rfb_xvp_ver).to.equal(10);
+                expect(client.get_onXvpInit()).to.have.been.calledOnce;
+                expect(client.get_onXvpInit()).to.have.been.calledWith(10);
+            });
+
+            it('should fail on unknown XVP message types', function () {
+                client._sock._websocket._receive_data(new Uint8Array([250, 0, 10, 237]));
+                expect(client._rfb_state).to.equal('failed');
+            });
+        });
+
+        it('should fire the clipboard callback with the retrieved text on ServerCutText', function () {
+            var expected_str = 'cheese!';
+            var data = [3, 0, 0, 0];
+            data.push32(expected_str.length);
+            for (var i = 0; i < expected_str.length; i++) { data.push(expected_str.charCodeAt(i)); }
+            client.set_onClipboard(sinon.spy());
+
+            client._sock._websocket._receive_data(new Uint8Array(data));
+            var spy = client.get_onClipboard();
+            expect(spy).to.have.been.calledOnce;
+            expect(spy.args[0][1]).to.equal(expected_str);
+        });
+
+        it('should fire the bell callback on Bell', function () {
+            client.set_onBell(sinon.spy());
+            client._sock._websocket._receive_data(new Uint8Array([2]));
+            expect(client.get_onBell()).to.have.been.calledOnce;
+        });
+
+        it('should fail on an unknown message type', function () {
+            client._sock._websocket._receive_data(new Uint8Array([87]));
+            expect(client._rfb_state).to.equal('failed');
+        });
+    });
+
+    describe('Asynchronous Events', function () {
+        describe('Mouse event handlers', function () {
+            var client;
+            beforeEach(function () {
+                client = make_rfb();
+                client._sock.send = sinon.spy();
+                client._rfb_state = 'normal';
+            });
+
+            it('should not send button messages in view-only mode', function () {
+                client._view_only = true;
+                client._mouse._onMouseButton(0, 0, 1, 0x001);
+                expect(client._sock.send).to.not.have.been.called;
+            });
+
+            it('should not send movement messages in view-only mode', function () {
+                client._view_only = true;
+                client._mouse._onMouseMove(0, 0);
+                expect(client._sock.send).to.not.have.been.called;
+            });
+
+            it('should send a pointer event on mouse button presses', function () {
+                client._mouse._onMouseButton(10, 12, 1, 0x001);
+                expect(client._sock.send).to.have.been.calledOnce;
+                var pointer_msg = RFB.messages.pointerEvent(10, 12, 0x001);
+                expect(client._sock.send).to.have.been.calledWith(pointer_msg);
+            });
+
+            it('should send a pointer event on mouse movement', function () {
+                client._mouse._onMouseMove(10, 12);
+                expect(client._sock.send).to.have.been.calledOnce;
+                var pointer_msg = RFB.messages.pointerEvent(10, 12, 0);
+                expect(client._sock.send).to.have.been.calledWith(pointer_msg);
+            });
+
+            it('should set the button mask so that future mouse movements use it', function () {
+                client._mouse._onMouseButton(10, 12, 1, 0x010);
+                client._sock.send = sinon.spy();
+                client._mouse._onMouseMove(13, 9);
+                expect(client._sock.send).to.have.been.calledOnce;
+                var pointer_msg = RFB.messages.pointerEvent(13, 9, 0x010);
+                expect(client._sock.send).to.have.been.calledWith(pointer_msg);
+            });
+
+            // NB(directxman12): we don't need to test not sending messages in
+            //                   non-normal modes, since we haven't grabbed input
+            //                   yet (grabbing input should be checked in the lifecycle tests).
+
+            it('should not send movement messages when viewport dragging', function () {
+                client._viewportDragging = true;
+                client._display.viewportChange = sinon.spy();
+                client._mouse._onMouseMove(13, 9);
+                expect(client._sock.send).to.not.have.been.called;
+            });
+
+            it('should not send button messages when initiating viewport dragging', function () {
+                client._viewportDrag = true;
+                client._mouse._onMouseButton(13, 9, 0x001);
+                expect(client._sock.send).to.not.have.been.called;
+            });
+
+            it('should be initiate viewport dragging on a button down event, if enabled', function () {
+                client._viewportDrag = true;
+                client._mouse._onMouseButton(13, 9, 0x001);
+                expect(client._viewportDragging).to.be.true;
+                expect(client._viewportDragPos).to.deep.equal({ x: 13, y: 9 });
+            });
+
+            it('should terminate viewport dragging on a button up event, if enabled', function () {
+                client._viewportDrag = true;
+                client._viewportDragging = true;
+                client._mouse._onMouseButton(13, 9, 0x000);
+                expect(client._viewportDragging).to.be.false;
+            });
+
+            it('if enabled, viewportDragging should occur on mouse movement while a button is down', function () {
+                client._viewportDrag = true;
+                client._viewportDragging = true;
+                client._viewportDragPos = { x: 13, y: 9 };
+                client._display.viewportChange = sinon.spy();
+
+                client._mouse._onMouseMove(10, 4);
+
+                expect(client._viewportDragging).to.be.true;
+                expect(client._viewportDragPos).to.deep.equal({ x: 10, y: 4 });
+                expect(client._display.viewportChange).to.have.been.calledOnce;
+                expect(client._display.viewportChange).to.have.been.calledWith(3, 5);
+            });
+        });
+
+        describe('Keyboard Event Handlers', function () {
+            var client;
+            beforeEach(function () {
+                client = make_rfb();
+                client._sock.send = sinon.spy();
+            });
+
+            it('should send a key message on a key press', function () {
+                client._keyboard._onKeyPress(1234, 1);
+                expect(client._sock.send).to.have.been.calledOnce;
+                var key_msg = RFB.messages.keyEvent(1234, 1);
+                expect(client._sock.send).to.have.been.calledWith(key_msg);
+            });
+
+            it('should not send messages in view-only mode', function () {
+                client._view_only = true;
+                client._keyboard._onKeyPress(1234, 1);
+                expect(client._sock.send).to.not.have.been.called;
+            });
+        });
+
+        describe('WebSocket event handlers', function () {
+            var client;
+            beforeEach(function () {
+                client = make_rfb();
+                this.clock = sinon.useFakeTimers();
+            });
+
+            afterEach(function () { this.clock.restore(); });
+
+            // message events
+            it ('should do nothing if we receive an empty message and have nothing in the queue', function () {
+                client.connect('host', 8675);
+                client._rfb_state = 'normal';
+                client._normal_msg = sinon.spy();
+                client._sock._websocket._receive_data(Base64.encode([]));
+                expect(client._normal_msg).to.not.have.been.called;
+            });
+
+            it('should handle a message in the normal state as a normal message', function () {
+                client.connect('host', 8675);
+                client._rfb_state = 'normal';
+                client._normal_msg = sinon.spy();
+                client._sock._websocket._receive_data(Base64.encode([1, 2, 3]));
+                expect(client._normal_msg).to.have.been.calledOnce;
+            });
+
+            it('should handle a message in any non-disconnected/failed state like an init message', function () {
+                client.connect('host', 8675);
+                client._rfb_state = 'ProtocolVersion';
+                client._init_msg = sinon.spy();
+                client._sock._websocket._receive_data(Base64.encode([1, 2, 3]));
+                expect(client._init_msg).to.have.been.calledOnce;
+            });
+
+            it('should split up the handling of muplitle normal messages across 10ms intervals', function () {
+                client.connect('host', 8675);
+                client._sock._websocket._open();
+                client._rfb_state = 'normal';
+                client.set_onBell(sinon.spy());
+                client._sock._websocket._receive_data(new Uint8Array([0x02, 0x02]));
+                expect(client.get_onBell()).to.have.been.calledOnce;
+                this.clock.tick(20);
+                expect(client.get_onBell()).to.have.been.calledTwice;
+            });
+
+            // open events
+            it('should update the state to ProtocolVersion on open (if the state is "connect")', function () {
+                client.connect('host', 8675);
+                client._sock._websocket._open();
+                expect(client._rfb_state).to.equal('ProtocolVersion');
+            });
+
+            it('should fail if we are not currently ready to connect and we get an "open" event', function () {
+                client.connect('host', 8675);
+                client._rfb_state = 'some_other_state';
+                client._sock._websocket._open();
+                expect(client._rfb_state).to.equal('failed');
+            });
+
+            // close events
+            it('should transition to "disconnected" from "disconnect" on a close event', function () {
+                client.connect('host', 8675);
+                client._rfb_state = 'disconnect';
+                client._sock._websocket.close();
+                expect(client._rfb_state).to.equal('disconnected');
+            });
+
+            it('should transition to failed if we get a close event from any non-"disconnection" state', function () {
+                client.connect('host', 8675);
+                client._rfb_state = 'normal';
+                client._sock._websocket.close();
+                expect(client._rfb_state).to.equal('failed');
+            });
+
+            // error events do nothing
+        });
+    });
+});

+ 105 - 0
tests/test.util.js

@@ -0,0 +1,105 @@
+// requires local modules: util
+/* jshint expr: true */
+
+var assert = chai.assert;
+var expect = chai.expect;
+
+describe('Utils', function() {
+    "use strict";
+
+    describe('Array instance methods', function () {
+        describe('push8', function () {
+            it('should push a byte on to the array', function () {
+                var arr = [1];
+                arr.push8(128);
+                expect(arr).to.deep.equal([1, 128]);
+            });
+
+            it('should only use the least significant byte of any number passed in', function () {
+                var arr = [1];
+                arr.push8(0xABCD);
+                expect(arr).to.deep.equal([1, 0xCD]);
+            });
+        });
+
+        describe('push16', function () {
+            it('should push two bytes on to the array', function () {
+                var arr = [1];
+                arr.push16(0xABCD);
+                expect(arr).to.deep.equal([1, 0xAB, 0xCD]);
+            });
+
+            it('should only use the two least significant bytes of any number passed in', function () {
+                var arr = [1];
+                arr.push16(0xABCDEF);
+                expect(arr).to.deep.equal([1, 0xCD, 0xEF]);
+            });
+        });
+
+        describe('push32', function () {
+            it('should push four bytes on to the array', function () {
+                var arr = [1];
+                arr.push32(0xABCDEF12);
+                expect(arr).to.deep.equal([1, 0xAB, 0xCD, 0xEF, 0x12]);
+            });
+
+            it('should only use the four least significant bytes of any number passed in', function () {
+                var arr = [1];
+                arr.push32(0xABCDEF1234);
+                expect(arr).to.deep.equal([1, 0xCD, 0xEF, 0x12, 0x34]);
+            });
+        });
+    });
+
+    describe('logging functions', function () {
+        beforeEach(function () {
+            sinon.spy(console, 'log');
+            sinon.spy(console, 'warn');
+            sinon.spy(console, 'error');
+        });
+
+        afterEach(function () {
+           console.log.restore();
+           console.warn.restore();
+           console.error.restore();
+        });
+
+        it('should use noop for levels lower than the min level', function () {
+            Util.init_logging('warn');
+            Util.Debug('hi');
+            Util.Info('hello');
+            expect(console.log).to.not.have.been.called;
+        });
+
+        it('should use console.log for Debug and Info', function () {
+            Util.init_logging('debug');
+            Util.Debug('dbg');
+            Util.Info('inf');
+            expect(console.log).to.have.been.calledWith('dbg');
+            expect(console.log).to.have.been.calledWith('inf');
+        });
+
+        it('should use console.warn for Warn', function () {
+            Util.init_logging('warn');
+            Util.Warn('wrn');
+            expect(console.warn).to.have.been.called;
+            expect(console.warn).to.have.been.calledWith('wrn');
+        });
+
+        it('should use console.error for Error', function () {
+            Util.init_logging('error');
+            Util.Error('err');
+            expect(console.error).to.have.been.called;
+            expect(console.error).to.have.been.calledWith('err');
+        });
+    });
+
+    // TODO(directxman12): test the conf_default and conf_defaults methods
+    // TODO(directxman12): test decodeUTF8
+    // TODO(directxman12): test the event methods (addEvent, removeEvent, stopEvent)
+    // TODO(directxman12): figure out a good way to test getPosition and getEventPosition
+    // TODO(directxman12): figure out how to test the browser detection functions properly
+    //                     (we can't really test them against the browsers, except for Gecko
+    //                     via PhantomJS, the default test driver)
+    // TODO(directxman12): figure out how to test Util.Flash
+});

+ 480 - 0
tests/test.websock.js

@@ -0,0 +1,480 @@
+// requires local modules: websock, base64, util
+// requires test modules: fake.websocket
+/* jshint expr: true */
+var assert = chai.assert;
+var expect = chai.expect;
+
+describe('Websock', function() {
+    "use strict";
+
+    describe('Queue methods', function () {
+        var sock;
+        var RQ_TEMPLATE = [0, 1, 2, 3, 4, 5, 6, 7];
+
+        beforeEach(function () {
+            sock = new Websock();
+            for (var i = RQ_TEMPLATE.length - 1; i >= 0; i--) {
+               sock.rQunshift8(RQ_TEMPLATE[i]);
+            }
+        });
+        describe('rQlen', function () {
+            it('should return the length of the receive queue', function () {
+               sock.set_rQi(0);
+
+               expect(sock.rQlen()).to.equal(RQ_TEMPLATE.length);
+            });
+
+            it("should return the proper length if we read some from the receive queue", function () {
+                sock.set_rQi(1);
+
+                expect(sock.rQlen()).to.equal(RQ_TEMPLATE.length - 1);
+            });
+        });
+
+        describe('rQpeek8', function () {
+            it('should peek at the next byte without poping it off the queue', function () {
+                var bef_len = sock.rQlen();
+                var peek = sock.rQpeek8();
+                expect(sock.rQpeek8()).to.equal(peek);
+                expect(sock.rQlen()).to.equal(bef_len);
+            });
+        });
+
+        describe('rQshift8', function () {
+            it('should pop a single byte from the receive queue', function () {
+                var peek = sock.rQpeek8();
+                var bef_len = sock.rQlen();
+                expect(sock.rQshift8()).to.equal(peek);
+                expect(sock.rQlen()).to.equal(bef_len - 1);
+            });
+        });
+
+        describe('rQunshift8', function () {
+            it('should place a byte at the front of the queue', function () {
+                sock.rQunshift8(255);
+                expect(sock.rQpeek8()).to.equal(255);
+                expect(sock.rQlen()).to.equal(RQ_TEMPLATE.length + 1);
+            });
+        });
+
+        describe('rQshift16', function () {
+            it('should pop two bytes from the receive queue and return a single number', function () {
+                var bef_len = sock.rQlen();
+                var expected = (RQ_TEMPLATE[0] << 8) + RQ_TEMPLATE[1];
+                expect(sock.rQshift16()).to.equal(expected);
+                expect(sock.rQlen()).to.equal(bef_len - 2);
+            });
+        });
+
+        describe('rQshift32', function () {
+            it('should pop four bytes from the receive queue and return a single number', function () {
+                var bef_len = sock.rQlen();
+                var expected = (RQ_TEMPLATE[0] << 24) +
+                               (RQ_TEMPLATE[1] << 16) +
+                               (RQ_TEMPLATE[2] << 8) +
+                               RQ_TEMPLATE[3];
+                expect(sock.rQshift32()).to.equal(expected);
+                expect(sock.rQlen()).to.equal(bef_len - 4);
+            });
+        });
+
+        describe('rQshiftStr', function () {
+            it('should shift the given number of bytes off of the receive queue and return a string', function () {
+                var bef_len = sock.rQlen();
+                var bef_rQi = sock.get_rQi();
+                var shifted = sock.rQshiftStr(3);
+                expect(shifted).to.be.a('string');
+                expect(shifted).to.equal(String.fromCharCode.apply(null, RQ_TEMPLATE.slice(bef_rQi, bef_rQi + 3)));
+                expect(sock.rQlen()).to.equal(bef_len - 3);
+            });
+
+            it('should shift the entire rest of the queue off if no length is given', function () {
+                sock.rQshiftStr();
+                expect(sock.rQlen()).to.equal(0);
+            });
+        });
+
+        describe('rQshiftBytes', function () {
+            it('should shift the given number of bytes of the receive queue and return an array', function () {
+                var bef_len = sock.rQlen();
+                var bef_rQi = sock.get_rQi();
+                var shifted = sock.rQshiftBytes(3);
+                expect(shifted).to.be.an.instanceof(Array);
+                expect(shifted).to.deep.equal(RQ_TEMPLATE.slice(bef_rQi, bef_rQi + 3));
+                expect(sock.rQlen()).to.equal(bef_len - 3);
+            });
+
+            it('should shift the entire rest of the queue off if no length is given', function () {
+                sock.rQshiftBytes();
+                expect(sock.rQlen()).to.equal(0);
+            });
+        });
+
+        describe('rQslice', function () {
+            beforeEach(function () {
+                sock.set_rQi(0);
+            });
+
+            it('should not modify the receive queue', function () {
+                var bef_len = sock.rQlen();
+                sock.rQslice(0, 2);
+                expect(sock.rQlen()).to.equal(bef_len);
+            });
+
+            it('should return an array containing the given slice of the receive queue', function () {
+                var sl = sock.rQslice(0, 2);
+                expect(sl).to.be.an.instanceof(Array);
+                expect(sl).to.deep.equal(RQ_TEMPLATE.slice(0, 2));
+            });
+
+            it('should use the rest of the receive queue if no end is given', function () {
+                var sl = sock.rQslice(1);
+                expect(sl).to.have.length(RQ_TEMPLATE.length - 1);
+                expect(sl).to.deep.equal(RQ_TEMPLATE.slice(1));
+            });
+
+            it('should take the current rQi in to account', function () {
+                sock.set_rQi(1);
+                expect(sock.rQslice(0, 2)).to.deep.equal(RQ_TEMPLATE.slice(1, 3));
+            });
+        });
+
+        describe('rQwait', function () {
+            beforeEach(function () {
+                sock.set_rQi(0);
+            });
+
+            it('should return true if there are not enough bytes in the receive queue', function () {
+                expect(sock.rQwait('hi', RQ_TEMPLATE.length + 1)).to.be.true;
+            });
+
+            it('should return false if there are enough bytes in the receive queue', function () {
+                expect(sock.rQwait('hi', RQ_TEMPLATE.length)).to.be.false;
+            });
+
+            it('should return true and reduce rQi by "goback" if there are not enough bytes', function () {
+                sock.set_rQi(5);
+                expect(sock.rQwait('hi', RQ_TEMPLATE.length, 4)).to.be.true;
+                expect(sock.get_rQi()).to.equal(1);
+            });
+
+            it('should raise an error if we try to go back more than possible', function () {
+                sock.set_rQi(5);
+                expect(function () { sock.rQwait('hi', RQ_TEMPLATE.length, 6); }).to.throw(Error);
+            });
+
+            it('should not reduce rQi if there are enough bytes', function () {
+                sock.set_rQi(5);
+                sock.rQwait('hi', 1, 6);
+                expect(sock.get_rQi()).to.equal(5);
+            });
+        });
+
+        describe('flush', function () {
+            beforeEach(function () {
+                sock._websocket = {
+                    send: sinon.spy()
+                };
+            });
+
+            it('should actually send on the websocket if the websocket does not have too much buffered', function () {
+                sock.maxBufferedAmount = 10;
+                sock._websocket.bufferedAmount = 8;
+                sock._sQ = [1, 2, 3];
+                var encoded = sock._encode_message();
+
+                sock.flush();
+                expect(sock._websocket.send).to.have.been.calledOnce;
+                expect(sock._websocket.send).to.have.been.calledWith(encoded);
+            });
+
+            it('should return true if the websocket did not have too much buffered', function () {
+                sock.maxBufferedAmount = 10;
+                sock._websocket.bufferedAmount = 8;
+
+                expect(sock.flush()).to.be.true;
+            });
+
+            it('should not call send if we do not have anything queued up', function () {
+                sock._sQ = [];
+                sock.maxBufferedAmount = 10;
+                sock._websocket.bufferedAmount = 8;
+
+                sock.flush();
+
+                expect(sock._websocket.send).not.to.have.been.called;
+            });
+
+            it('should not send and return false if the websocket has too much buffered', function () {
+                sock.maxBufferedAmount = 10;
+                sock._websocket.bufferedAmount = 12;
+
+                expect(sock.flush()).to.be.false;
+                expect(sock._websocket.send).to.not.have.been.called;
+            });
+        });
+
+        describe('send', function () {
+            beforeEach(function () {
+                sock.flush = sinon.spy();
+            });
+
+            it('should add to the send queue', function () {
+                sock.send([1, 2, 3]);
+                var sq = sock.get_sQ();
+                expect(sock.get_sQ().slice(sq.length - 3)).to.deep.equal([1, 2, 3]);
+            });
+
+            it('should call flush', function () {
+                sock.send([1, 2, 3]);
+                expect(sock.flush).to.have.been.calledOnce;
+            });
+        });
+
+        describe('send_string', function () {
+            beforeEach(function () {
+                sock.send = sinon.spy();
+            });
+
+            it('should call send after converting the string to an array', function () {
+                sock.send_string("\x01\x02\x03");
+                expect(sock.send).to.have.been.calledWith([1, 2, 3]);
+            });
+        });
+    });
+
+    describe('lifecycle methods', function () {
+        var old_WS;
+        before(function () {
+           old_WS = WebSocket;
+        });
+
+        var sock;
+        beforeEach(function () {
+           sock = new Websock();
+           WebSocket = sinon.spy();
+           WebSocket.OPEN = old_WS.OPEN;
+           WebSocket.CONNECTING = old_WS.CONNECTING;
+           WebSocket.CLOSING = old_WS.CLOSING;
+           WebSocket.CLOSED = old_WS.CLOSED;
+        });
+
+        describe('opening', function () {
+            it('should pick the correct protocols if none are given' , function () {
+
+            });
+
+            it('should open the actual websocket', function () {
+                sock.open('ws://localhost:8675', 'base64');
+                expect(WebSocket).to.have.been.calledWith('ws://localhost:8675', 'base64');
+            });
+
+            it('should fail if we try to use binary but do not support it', function () {
+                expect(function () { sock.open('ws:///', 'binary'); }).to.throw(Error);
+            });
+
+            it('should fail if we specified an array with only binary and we do not support it', function () {
+                expect(function () { sock.open('ws:///', ['binary']); }).to.throw(Error);
+            });
+
+            it('should skip binary if we have multiple options for encoding and do not support binary', function () {
+                sock.open('ws:///', ['binary', 'base64']);
+                expect(WebSocket).to.have.been.calledWith('ws:///', ['base64']);
+            });
+            // it('should initialize the event handlers')?
+        });
+
+        describe('closing', function () {
+            beforeEach(function () {
+                sock.open('ws://');
+                sock._websocket.close = sinon.spy();
+            });
+
+            it('should close the actual websocket if it is open', function () {
+                sock._websocket.readyState = WebSocket.OPEN;
+                sock.close();
+                expect(sock._websocket.close).to.have.been.calledOnce;
+            });
+
+            it('should close the actual websocket if it is connecting', function () {
+                sock._websocket.readyState = WebSocket.CONNECTING;
+                sock.close();
+                expect(sock._websocket.close).to.have.been.calledOnce;
+            });
+
+            it('should not try to close the actual websocket if closing', function () {
+                sock._websocket.readyState = WebSocket.CLOSING;
+                sock.close();
+                expect(sock._websocket.close).not.to.have.been.called;
+            });
+
+            it('should not try to close the actual websocket if closed', function () {
+                sock._websocket.readyState = WebSocket.CLOSED;
+                sock.close();
+                expect(sock._websocket.close).not.to.have.been.called;
+            });
+
+            it('should reset onmessage to not call _recv_message', function () {
+                sinon.spy(sock, '_recv_message');
+                sock.close();
+                sock._websocket.onmessage(null);
+                try {
+                    expect(sock._recv_message).not.to.have.been.called;
+                } finally {
+                    sock._recv_message.restore();
+                }
+            });
+        });
+
+        describe('event handlers', function () {
+            beforeEach(function () {
+                sock._recv_message = sinon.spy();
+                sock.on('open', sinon.spy());
+                sock.on('close', sinon.spy());
+                sock.on('error', sinon.spy());
+                sock.open('ws://');
+            });
+
+            it('should call _recv_message on a message', function () {
+                sock._websocket.onmessage(null);
+                expect(sock._recv_message).to.have.been.calledOnce;
+            });
+
+            it('should copy the mode over upon opening', function () {
+                sock._websocket.protocol = 'cheese';
+                sock._websocket.onopen();
+                expect(sock._mode).to.equal('cheese');
+            });
+
+            it('should assume base64 if no protocol was available on opening', function () {
+                sock._websocket.protocol = null;
+                sock._websocket.onopen();
+                expect(sock._mode).to.equal('base64');
+            });
+
+            it('should call the open event handler on opening', function () {
+                sock._websocket.onopen();
+                expect(sock._eventHandlers.open).to.have.been.calledOnce;
+            });
+
+            it('should call the close event handler on closing', function () {
+                sock._websocket.onclose();
+                expect(sock._eventHandlers.close).to.have.been.calledOnce;
+            });
+
+            it('should call the error event handler on error', function () {
+                sock._websocket.onerror();
+                expect(sock._eventHandlers.error).to.have.been.calledOnce;
+            });
+        });
+
+        after(function () {
+            WebSocket = old_WS;
+        });
+    });
+
+    describe('WebSocket Receiving', function () {
+        var sock;
+        beforeEach(function () {
+           sock = new Websock();
+        });
+
+        it('should support decoding base64 string data to add it to the receive queue', function () {
+            var msg = { data: Base64.encode([1, 2, 3]) };
+            sock._mode = 'base64';
+            sock._recv_message(msg);
+            expect(sock.rQshiftStr(3)).to.equal('\x01\x02\x03');
+        });
+
+        it('should support adding binary Uint8Array data to the receive queue', function () {
+            var msg = { data: new Uint8Array([1, 2, 3]) };
+            sock._mode = 'binary';
+            sock._recv_message(msg);
+            expect(sock.rQshiftStr(3)).to.equal('\x01\x02\x03');
+        });
+
+        it('should call the message event handler if present', function () {
+            sock._eventHandlers.message = sinon.spy();
+            var msg = { data: Base64.encode([1, 2, 3]) };
+            sock._mode = 'base64';
+            sock._recv_message(msg);
+            expect(sock._eventHandlers.message).to.have.been.calledOnce;
+        });
+
+        it('should not call the message event handler if there is nothing in the receive queue', function () {
+            sock._eventHandlers.message = sinon.spy();
+            var msg = { data: Base64.encode([]) };
+            sock._mode = 'base64';
+            sock._recv_message(msg);
+            expect(sock._eventHandlers.message).not.to.have.been.called;
+        });
+
+        it('should compact the receive queue', function () {
+            // NB(sross): while this is an internal implementation detail, it's important to
+            //            test, otherwise the receive queue could become very large very quickly
+            sock._rQ = [0, 1, 2, 3, 4, 5];
+            sock.set_rQi(6);
+            sock._rQmax = 3;
+            var msg = { data: Base64.encode([1, 2, 3]) };
+            sock._mode = 'base64';
+            sock._recv_message(msg);
+            expect(sock._rQ.length).to.equal(3);
+            expect(sock.get_rQi()).to.equal(0);
+        });
+
+        it('should call the error event handler on an exception', function () {
+            sock._eventHandlers.error = sinon.spy();
+            sock._eventHandlers.message = sinon.stub().throws();
+            var msg = { data: Base64.encode([1, 2, 3]) };
+            sock._mode = 'base64';
+            sock._recv_message(msg);
+            expect(sock._eventHandlers.error).to.have.been.calledOnce;
+        });
+    });
+
+    describe('Data encoding', function () {
+        before(function () { FakeWebSocket.replace(); });
+        after(function () { FakeWebSocket.restore(); });
+
+        describe('as binary data', function () {
+            var sock;
+            beforeEach(function () {
+                sock = new Websock();
+                sock.open('ws://', 'binary');
+                sock._websocket._open();
+            });
+
+            it('should convert the send queue into an ArrayBuffer', function () {
+                sock._sQ = [1, 2, 3];
+                var res = sock._encode_message();  // An ArrayBuffer
+                expect(new Uint8Array(res)).to.deep.equal(new Uint8Array(res));
+            });
+
+            it('should properly pass the encoded data off to the actual WebSocket', function () {
+                sock.send([1, 2, 3]);
+                expect(sock._websocket._get_sent_data()).to.deep.equal([1, 2, 3]);
+            });
+        });
+
+        describe('as Base64 data', function () {
+            var sock;
+            beforeEach(function () {
+                sock = new Websock();
+                sock.open('ws://', 'base64');
+                sock._websocket._open();
+            });
+
+            it('should convert the send queue into a Base64-encoded string', function () {
+                sock._sQ = [1, 2, 3];
+                expect(sock._encode_message()).to.equal(Base64.encode([1, 2, 3]));
+            });
+
+            it('should properly pass the encoded data off to the actual WebSocket', function () {
+                sock.send([1, 2, 3]);
+                expect(sock._websocket._get_sent_data()).to.deep.equal([1, 2, 3]);
+            });
+
+        });
+
+    });
+});