First draft of rewrite to Webextension and Rust

This proves that frames can be generated on Firefox using the canvas and
a Tree Walker to examine text nodes. Already with little optimisation
frames don't ever take longer than 200ms to render.

Chrome has a MediaStream of the viewport, hopefully that will prove
performant as well.

This doesn't have functioning text colour detection or text occlusion
support. But early research suggests this will possible by comparing 2
screenshots: one with and the other without rendered text.
This commit is contained in:
Thomas Buckley-Houston 2017-12-30 21:37:28 +08:00
parent 6524500745
commit d00361f87d
37 changed files with 5980 additions and 980 deletions

20
.eslintrc Normal file
View File

@ -0,0 +1,20 @@
{
"env" : {
"node" : true,
"browser" : true,
"webextensions": true,
"mocha": true
},
"globals": {
"BUILD_ENV": true
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"extends": "eslint:recommended",
"rules": {
"no-console": 0,
"no-unused-vars": [2, {"args": "all", "argsIgnorePattern": "^_"}]
}
}

4
.gitignore vendored
View File

@ -2,4 +2,6 @@ build/
*.log
*.out
blank.png
interfacer/interfacer
interfacer/target
webext/web-ext-artifacts
webext/node_modules

672
interfacer/Cargo.lock generated Normal file
View File

@ -0,0 +1,672 @@
[root]
name = "browsh"
version = "0.1.0"
dependencies = [
"termsize 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"websocket 0.20.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "advapi32-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "atty"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
"termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "base64"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "base64"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "bitflags"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "bitflags"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "byteorder"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "bytes"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"iovec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "cc"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "cfg-if"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "core-foundation"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "core-foundation-sys"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "crypt32-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "fuchsia-zircon"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"fuchsia-zircon-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "fuchsia-zircon-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "futures"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "httparse"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "hyper"
version = "0.10.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"base64 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"httparse 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)",
"traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "idna"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "iovec"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "kernel32-sys"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "language-tags"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "lazy_static"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "lazycell"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "libc"
version = "0.2.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "log"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "matches"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "mime"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "mio"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"fuchsia-zircon 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"fuchsia-zircon-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"iovec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazycell 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"net2 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)",
"slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "miow"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"net2 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "native-tls"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"openssl 0.9.23 (registry+https://github.com/rust-lang/crates.io-index)",
"schannel 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
"security-framework 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
"security-framework-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
"tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "net2"
version = "0.2.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num_cpus"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "openssl"
version = "0.9.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-sys 0.9.23 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "openssl-sys"
version = "0.9.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cc 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "percent-encoding"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "pkg-config"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "rand"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"fuchsia-zircon 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "redox_syscall"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "redox_termios"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"redox_syscall 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "safemem"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "schannel"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"advapi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"crypt32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"secur32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "scoped-tls"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "secur32-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "security-framework"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"core-foundation 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
"security-framework-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "security-framework-sys"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "sha1"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "slab"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "slab"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "tempdir"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"rand 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "termion"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "termsize"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"atty 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
"termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "time"
version = "0.1.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "tokio-core"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bytes 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"iovec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"mio 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
"scoped-tls 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"slab 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "tokio-io"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bytes 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "tokio-tls"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "traitobject"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "typeable"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "unicase"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "unicode-bidi"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "unicode-normalization"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "url"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "vcpkg"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "version_check"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "websocket"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"bytes 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)",
"sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "winapi"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "winapi"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi-i686-pc-windows-gnu 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-x86_64-pc-windows-gnu 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "winapi-build"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "ws2_32-sys"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[metadata]
"checksum advapi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e06588080cb19d0acb6739808aafa5f26bfb2ca015b2b6370028b44cf7cb8a9a"
"checksum atty 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "8352656fd42c30a0c3c89d26dea01e3b77c0ab2af18230835c15e2e13cd51859"
"checksum base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "30e93c03064e7590d0466209155251b90c22e37fab1daf2771582598b5827557"
"checksum base64 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "96434f987501f0ed4eb336a411e0631ecd1afa11574fe148587adc4ff96143c9"
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5"
"checksum byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "652805b7e73fada9d85e9a6682a4abd490cb52d96aeecc12e33a0de34dfd0d23"
"checksum bytes 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d828f97b58cc5de3e40c421d0cf2132d6b2da4ee0e11b8632fa838f0f9333ad6"
"checksum cc 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a9b13a57efd6b30ecd6598ebdb302cca617930b5470647570468a65d12ef9719"
"checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de"
"checksum core-foundation 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "25bfd746d203017f7d5cbd31ee5d8e17f94b6521c7af77ece6c9e4b2d4b16c67"
"checksum core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "065a5d7ffdcbc8fa145d6f0746f3555025b9097a9e9cda59f7467abae670c78d"
"checksum crypt32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e34988f7e069e0b2f3bfc064295161e489b2d4e04a2e4248fb94360cdf00b4ec"
"checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
"checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
"checksum fuchsia-zircon 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f6c0581a4e363262e52b87f59ee2afe3415361c6ec35e665924eb08afe8ff159"
"checksum fuchsia-zircon-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "43f3795b4bae048dc6123a6b972cadde2e676f9ded08aef6bb77f5f157684a82"
"checksum futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "118b49cac82e04121117cbd3121ede3147e885627d82c4546b87c702debb90c1"
"checksum httparse 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "af2f2dd97457e8fb1ae7c5a420db346af389926e36f43768b96f101546b04a07"
"checksum hyper 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)" = "368cb56b2740ebf4230520e2b90ebb0461e69034d85d1945febd9b3971426db2"
"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d"
"checksum iovec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b6e8b9c2247fcf6c6a1151f1156932be5606c9fd6f55a2d7f9fc1cb29386b2f7"
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
"checksum lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c8f31047daa365f19be14b47c29df4f7c3b581832407daabe6ae77397619237d"
"checksum lazycell 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3b585b7a6811fb03aa10e74b278a0f00f8dd9b45dc681f148bb29fa5cb61859b"
"checksum libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)" = "36fbc8a8929c632868295d0178dd8f63fc423fd7537ad0738372bd010b3ac9b0"
"checksum log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "880f77541efa6e5cc74e76910c9884d9859683118839d6a1dc3b11e63512565b"
"checksum matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "100aabe6b8ff4e4a7e32c1c13523379802df0772b82466207ac25b013f193376"
"checksum mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0"
"checksum mio 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)" = "0e8411968194c7b139e9105bc4ae7db0bae232af087147e72f0616ebf5fdb9cb"
"checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919"
"checksum native-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "04b781c9134a954c84f0594b9ab3f5606abc516030388e8511887ef4c204a1e5"
"checksum net2 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)" = "3a80f842784ef6c9a958b68b7516bc7e35883c614004dd94959a4dca1b716c09"
"checksum num_cpus 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "514f0d73e64be53ff320680ca671b64fe3fb91da01e1ae2ddc99eb51d453b20d"
"checksum openssl 0.9.23 (registry+https://github.com/rust-lang/crates.io-index)" = "169a4b9160baf9b9b1ab975418c673686638995ba921683a7f1e01470dcb8854"
"checksum openssl-sys 0.9.23 (registry+https://github.com/rust-lang/crates.io-index)" = "2200ffec628e3f14c39fc0131a301db214f1a7d584e36507ee8700b0c7fb7a46"
"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
"checksum pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3a8b4c6b8165cd1a1cd4b9b120978131389f64bdaf456435caa41e630edba903"
"checksum rand 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)" = "6475140dfd8655aeb72e1fd4b7a1cc1c202be65d71669476e392fe62532b9edd"
"checksum redox_syscall 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "ab105df655884ede59d45b7070c8a65002d921461ee813a024558ca16030eea0"
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
"checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f"
"checksum schannel 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "4330c2e874379fbd28fa67ba43239dbe8c7fb00662ceb1078bd37474f08bf5ce"
"checksum scoped-tls 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f417c22df063e9450888a7561788e9bd46d3bb3c1466435b4eccb903807f147d"
"checksum secur32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3f412dfa83308d893101dd59c10d6fda8283465976c28c287c5c855bf8d216bc"
"checksum security-framework 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "dfa44ee9c54ce5eecc9de7d5acbad112ee58755239381f687e564004ba4a2332"
"checksum security-framework-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "5421621e836278a0b139268f36eee0dc7e389b784dc3f79d8f11aabadf41bead"
"checksum sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c"
"checksum slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23"
"checksum slab 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fdeff4cd9ecff59ec7e3744cbca73dfe5ac35c2aedb2cfba8a1c715a18912e9d"
"checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6"
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
"checksum termsize 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4e3cde4e7c68acfef795660eba99817550b0d7b6d79ebb8e75439358ca5b680f"
"checksum time 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)" = "d5d788d3aa77bc0ef3e9621256885555368b47bd495c13dd2e7413c89f845520"
"checksum tokio-core 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c87c27560184212c9dc45cd8f38623f37918248aad5b58fb65303b5d07a98c6e"
"checksum tokio-io 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "514aae203178929dbf03318ad7c683126672d4d96eccb77b29603d33c9e25743"
"checksum tokio-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d88e411cac1c87e405e4090be004493c5d8072a370661033b1a64ea205ec2e13"
"checksum traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079"
"checksum typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887"
"checksum unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33"
"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
"checksum unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "51ccda9ef9efa3f7ef5d91e8f9b83bbe6955f9bf86aec89d5cce2c874625920f"
"checksum url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fa35e768d4daf1d85733418a49fb42e10d7f633e394fccab4ab7aba897053fe2"
"checksum vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9e0a7d8bed3178a8fb112199d466eeca9ed09a14ba8ad67718179b4fd5487d0b"
"checksum version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6b772017e347561807c1aa192438c5fd74242a670a6cffacc40f2defd1dc069d"
"checksum websocket 0.20.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eb277e7f4c23dc49176f74ae200e77651764efb2c25f56ad2d22623b63826369"
"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
"checksum winapi 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "890b38836c01d72fdb636d15c9cfc52ec7fd783b330abc93cd1686f4308dfccc"
"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
"checksum winapi-i686-pc-windows-gnu 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ca38ad0b1b8fcebdf9a7906e368ce3a760ffa638c4c4f0cb57f3c26ad34cc86e"
"checksum winapi-x86_64-pc-windows-gnu 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a824ed89cff621305f4ae379551c65e1fca1eadb1d4a25e3a4e8989080a2d337"
"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"

13
interfacer/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "browsh"
version = "0.1.0"
authors = ["Tom Buckley-Houston <tom@tombh.co.uk>"]
[dependencies]
termsize = "0.1.5"
websocket = "0.20.2"
[[bin]]
name = "browsh"
path = "ws-server.rs"

View File

@ -1,633 +0,0 @@
package main
import (
"fmt"
"github.com/tombh/termbox-go"
"math"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
)
// Import the xzoom C code that creates an X window that zooms
// and pans the desktop.
// It's written in C because it borrows from the original xzoom
// program: http://git.r-36.net/xzoom/
// NB: The following comments are parsed by `go build` ...
// #cgo LDFLAGS: -lXext -lX11 -lXt
// #include "../xzoom/xzoom.h"
import "C"
var logfile string
var current string
var curev termbox.Event
var lastMouseButton string
var hipWidth int
var hipHeight int
var envDesktopWidth int
var envDesktopHeight int
var desktopWidth float32
var desktopHeight float32
var desktopXFloat float32
var desktopYFloat float32
var roundedDesktopX int
var roundedDesktopY int
// Channels to control the background xzoom go routine
var stopXZoomChannel = make(chan struct{})
var xZoomStoppedChannel = make(chan struct{})
var panNeedsSetup bool
var panStartingX float32
var panStartingY float32
// Keyboard mode is for interacting with the desktop with the keyboard
// instead of the mouse.
var keyboardMode = false
var kbCursorX int
var kbCursorY int
var char string
var debugMode = parseENVVar("DEBUG") == 1
func initialise() {
setupLogging()
log("Starting...")
setupTermbox()
setupDimensions()
if !debugMode {
C.xzoom_init()
xzoomBackground()
}
}
func parseENVVar(variable string) int {
value, err := strconv.Atoi(os.Getenv(variable))
if err != nil {
return 0
}
return value
}
func setupDimensions() {
if debugMode {
hipWidth, hipHeight = termbox.Size()
envDesktopWidth = 1200
envDesktopHeight = 900
} else {
hipWidth = parseENVVar("TTY_WIDTH")
hipHeight = parseENVVar("TTY_HEIGHT")
envDesktopWidth = parseENVVar("DESKTOP_WIDTH")
envDesktopHeight = parseENVVar("DESKTOP_HEIGHT")
}
kbCursorX = int(hipWidth / 2)
kbCursorY = int(hipHeight / 2)
C.desktop_width = C.int(envDesktopWidth)
C.width[C.SRC] = C.desktop_width
C.width[C.DST] = C.desktop_width
C.desktop_height = C.int(envDesktopHeight)
C.height[C.SRC] = C.desktop_height
C.height[C.DST] = C.desktop_height
desktopWidth = float32(envDesktopWidth)
desktopHeight = float32(envDesktopHeight)
log(fmt.Sprintf("Desktop dimensions: W: %d, H: %d", envDesktopWidth, envDesktopHeight))
log(fmt.Sprintf("Term dimensions: W: %d, H: %d", hipWidth, hipHeight))
}
func setupTermbox() {
err := termbox.Init()
if err != nil {
panic(err)
}
termbox.SetInputMode(termbox.InputAlt | termbox.InputMouse)
}
func setupLogging() {
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
panic(err)
}
os.Mkdir(filepath.Join(dir, "..", "logs"), os.ModePerm)
logfile = fmt.Sprintf(filepath.Join(dir, "..", "logs", "input.log"))
if _, err := os.Stat(logfile); err == nil {
os.Truncate(logfile, 0)
}
}
// Render text. This doesn't play very nice with hiptext, it can cause
// random tearing and flickering :( I suspect there are ways to overcome
// this for rendering outside of hiptext's area. But to render over hiptext
// it will probably mean patching hiptext.
func printXY(x, y int, s string, force bool) {
for _, r := range s {
// It seems termbox keeps an internal representation of the TTY
// and won't try updating the cell unless it thinks it has changed.
// This is only relevant because we're in competition with the
// rapid screen updates from hiptext. This means that unless we
// "remove -> flush -> redraw" everything hiptext removes whatever
// we render.
if force {
// 32 is the space character
termbox.SetCell(x, y, 32, termbox.ColorWhite, termbox.ColorDefault)
termbox.Flush()
}
termbox.SetCell(x, y, r, termbox.ColorWhite, termbox.ColorDefault)
x++
}
termbox.Flush()
}
func log(msg string) {
f, oErr := os.OpenFile(logfile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if oErr != nil {
panic(oErr)
}
defer f.Close()
msg = msg + "\n"
if _, wErr := f.WriteString(msg); wErr != nil {
panic(wErr)
}
if debugMode {
printXY(0, hipHeight - 1, msg, true)
}
}
func min(a float32, b float32) float32 {
if a < b {
return a
}
return b
}
func getXGrab() int {
return int(C.xgrab)
}
func getYGrab() int {
return int(C.ygrab)
}
// Issue an xdotool command to simulate mouse and keyboard input
func xdotool(args ...string) {
log(strings.Join(args, " "))
if debugMode {
return
}
if args[0] == "noop" {
return
}
if err := exec.Command("xdotool", args...).Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func roundToInt(value32 float32) int {
var rounded float64
value := float64(value32)
if value < 0 {
rounded = math.Ceil(value - 0.5)
}
rounded = math.Floor(value + 0.5)
return int(rounded)
}
// Whether the current input event includes a depressed CTRL key.
// Waiting for this PR: https://github.com/nsf/termbox-go/pull/126
func ctrlPressed() bool {
return curev.Mod&termbox.ModCtrl != 0
}
func altPressed() bool {
return curev.Mod&termbox.ModAlt != 0
}
// Whether the mouse is moving
func mouseMotion() bool {
return curev.Mod&termbox.ModMotion != 0
}
// Convert Termbox symbols to xdotool arguments
func mouseButtonStr() []string {
switch curev.Key {
case termbox.MouseLeft:
lastMouseButton = "1"
return []string{"mousedown", lastMouseButton}
case termbox.MouseMiddle:
lastMouseButton = "2"
return []string{"mousedown", lastMouseButton}
case termbox.MouseRight:
lastMouseButton = "3"
return []string{"mousedown", lastMouseButton}
case termbox.MouseRelease:
return []string{"mouseup", lastMouseButton}
case termbox.MouseWheelUp:
if ctrlPressed() {
zoom("in")
return []string{"noop"}
}
return []string{"click", "4"}
case termbox.MouseWheelDown:
if ctrlPressed() {
zoom("out")
return []string{"noop"}
}
return []string{"click", "5"}
}
return []string{""}
}
func zoom(direction string) {
oldZoom := C.magnification
// The actual zoom
if direction == "in" {
C.magnification++
} else {
if C.magnification > 1 {
C.magnification--
}
}
C.width[C.SRC] = (C.desktop_width + C.magnification - 1) / C.magnification
C.height[C.SRC] = (C.desktop_height + C.magnification - 1) / C.magnification
moveViewportForZoom(oldZoom)
keepViewportInDesktop()
}
// Move the viewport so that the mouse is still over the same part of
// the desktop.
func moveViewportForZoom(oldZoom C.int) {
factor := float32(oldZoom) / float32(C.magnification)
magnifiedRelativeX := factor * (desktopXFloat - float32(C.xgrab))
magnifiedRelativeY := factor * (desktopYFloat - float32(C.ygrab))
C.xgrab = C.int(desktopXFloat - magnifiedRelativeX)
C.ygrab = C.int(desktopYFloat - magnifiedRelativeY)
}
func keepViewportInDesktop() {
manageViewportSize()
manageViewportPosition()
}
func manageViewportSize() {
if C.width[C.SRC] < 1 {
C.width[C.SRC] = 1
}
if C.width[C.SRC] > C.desktop_width {
C.width[C.SRC] = C.desktop_width
}
if C.height[C.SRC] < 1 {
C.height[C.SRC] = 1
}
if C.height[C.SRC] > C.desktop_height {
C.height[C.SRC] = C.desktop_height
}
}
func manageViewportPosition() {
if C.xgrab > (C.desktop_width - C.width[C.SRC]) {
C.xgrab = C.desktop_width - C.width[C.SRC]
}
if C.xgrab < 0 {
C.xgrab = 0
}
if C.ygrab > (C.desktop_height - C.height[C.SRC]) {
C.ygrab = C.desktop_height - C.height[C.SRC]
}
if C.ygrab < 0 {
C.ygrab = 0
}
}
// Auxillary data. Whether the mouse was moving or a mod key like CTRL
// is being pressed at the same time.
func modStr(m termbox.Modifier) string {
var out []string
if mouseMotion() {
out = append(out, "Motion")
}
if ctrlPressed() {
out = append(out, "Ctrl")
}
return strings.Join(out, " ")
}
func isPanning() bool {
mousePanning := ctrlPressed() && mouseMotion() && lastMouseButton == "1"
kbPanning := (char == "U" || char == "K" || char == "N" || char == "H") && keyboardMode
return mousePanning || kbPanning
}
func mouseEvent() {
// Figure out where the mouse is on the actual real desktop.
// Note that the zomming and panning code effectively keep the mouse in the exact same position relative
// to the desktop, so mousemove *should* have no effect.
setCurrentDesktopCoords()
// Always move the mouse first so that button presses are correct. This is because we're not constantly
// updating the mouse position, *unless* a drag event is happening. This saves bandwidth. Also, mouse
// movement isn't supported on all terminals.
xdotool("mousemove", fmt.Sprintf("%d", roundedDesktopX), fmt.Sprintf("%d", roundedDesktopY))
if isPanning() {
pan()
} else {
panNeedsSetup = true
// Pressing of CTRL indicates that the user is panning or zooming, so there is no need to send
// button presses.
// TODO: What about CTRL+leftbutton to open new tab!?
if !keyboardMode && !ctrlPressed() {
xdotool(mouseButtonStr()...)
}
}
}
func pan() {
if panNeedsSetup {
panStartingX = desktopXFloat
panStartingY = desktopYFloat
panNeedsSetup = false
}
C.xgrab = C.int(float32(C.xgrab) + panStartingX - desktopXFloat)
C.ygrab = C.int(float32(C.ygrab) + panStartingY - desktopYFloat)
keepViewportInDesktop()
}
// Convert terminal coords into desktop coords
func setCurrentDesktopCoords() {
hipWidthFloat := float32(hipWidth)
hipHeightFloat := float32(hipHeight)
eventX := float32(getCursorX())
eventY := float32(getCursorY())
width := float32(C.width[C.SRC])
height := float32(C.height[C.SRC])
xOffset := float32(C.xgrab)
yOffset := float32(C.ygrab)
desktopXFloat = (eventX * (width / hipWidthFloat)) + xOffset
desktopYFloat = (eventY * (height / hipHeightFloat)) + yOffset
roundedDesktopX = roundToInt(desktopXFloat)
roundedDesktopY = roundToInt(desktopYFloat)
log(
fmt.Sprintf(
"setCurrentDesktopCoords: tw: %d, th: %d, dx: %d, dy: %d, mag: %d",
hipHeightFloat, hipWidthFloat, desktopXFloat, desktopYFloat, C.magnification))
}
func getCursorX() int {
if keyboardMode {
return kbCursorX
}
return curev.MouseX
}
func getCursorY() int {
if keyboardMode {
return kbCursorY
}
return curev.MouseY
}
// Convert a keyboard event into an xdotool command
// See: http://wiki.linuxquestions.org/wiki/List_of_Keysyms_Recognised_by_Xmodmap
func keyEvent() {
var command string
log(fmt.Sprintf("EventKey: k: %d, c: %c, mod: %s", curev.Key, curev.Ch, modStr(curev.Mod)))
key := getSpecialKeyPress()
if curev.Key == 0 {
key = fmt.Sprintf("%c", curev.Ch)
char = key
command = "type"
} else {
command = "key"
}
// What is this? It always appears when the program starts :/
badkey := fmt.Sprintf("%s", curev.Ch) == "%!s(int32=0)" && curev.Key == 0
if key == "" || badkey {
log(fmt.Sprintf("No key found for keycode: %d"))
return
}
if (curev.Key == termbox.KeyCtrlM) && altPressed() {
keyboardMode = !keyboardMode
printXY(0, hipHeight, " ", true)
}
if keyboardMode {
mouseEvent()
handleKeyboardMode(key)
renderCursor()
printXY(0, hipHeight, "KB ON ", true)
return
}
xdotool(command, key)
}
func handleKeyboardMode(key string) {
switch key {
case "u", "U":
kbCursorY--
if kbCursorY < 0 {
kbCursorY = 0
}
case "n", "N":
kbCursorY++
if kbCursorY > hipHeight {
kbCursorY = hipHeight
}
case "h", "H":
kbCursorX--
if kbCursorX < 0 {
kbCursorX = 0
}
case "k", "K":
kbCursorX++
if kbCursorX > hipWidth {
kbCursorX = hipWidth
}
case "ctrl+u":
zoom("in")
case "ctrl+n":
zoom("out")
case "j":
xdotool("click", "1")
case "r":
xdotool("click", "2")
case "t":
xdotool("click", "3")
}
}
func renderCursor() {
printXY(kbCursorX, kbCursorY, "+", false)
}
func getSpecialKeyPress() string {
var key string
switch curev.Key {
case termbox.KeyEnter:
key = "Return"
case termbox.KeyBackspace, termbox.KeyBackspace2:
key = "BackSpace"
case termbox.KeySpace:
key = "space"
case termbox.KeyF1:
key = "F1"
case termbox.KeyF2:
key = "F2"
case termbox.KeyF3:
key = "F3"
case termbox.KeyF4:
key = "F4"
case termbox.KeyF5:
key = "F5"
case termbox.KeyF6:
key = "F6"
case termbox.KeyF7:
key = "F7"
case termbox.KeyF8:
key = "F8"
case termbox.KeyF9:
key = "F9"
case termbox.KeyF10:
key = "F10"
case termbox.KeyF11:
key = "F11"
case termbox.KeyF12:
key = "F12"
case termbox.KeyInsert:
key = "Insert"
case termbox.KeyDelete:
key = "Delete"
case termbox.KeyHome:
key = "Home"
case termbox.KeyEnd:
key = "End"
case termbox.KeyPgup:
key = "Prior"
case termbox.KeyPgdn:
key = "Next"
case termbox.KeyArrowUp:
key = "Up"
case termbox.KeyArrowDown:
key = "Down"
case termbox.KeyArrowLeft:
key = "Left"
case termbox.KeyArrowRight:
key = "Right"
case termbox.KeyCtrlU:
key = "ctrl+u"
case termbox.KeyCtrlL:
key = "ctrl+l"
case termbox.KeyCtrlN:
key = "ctrl+n"
}
return key
}
func parseInput() {
switch curev.Type {
case termbox.EventKey:
keyEvent()
case termbox.EventMouse:
log(
fmt.Sprintf(
"EventMouse: x: %d, y: %d, b: %s, mod: %s",
getCursorX(), getCursorY(), mouseButtonStr(), modStr(curev.Mod)))
mouseEvent()
case termbox.EventNone:
log("EventNone")
}
}
// Run the xzoom window in a background go routine
func xzoomBackground() {
go func() {
defer close(xZoomStoppedChannel)
for {
select {
default:
C.do_iteration()
time.Sleep(40 * time.Millisecond) // 25fps
case <-stopXZoomChannel:
// Gracefully close the xzoom go routine
return
}
}
}()
}
func needToExit() bool {
// CTRL+ALT+Q
if (curev.Key == termbox.KeyCtrlQ) && altPressed() {
return true
}
return false
}
func teardown() {
termbox.Close()
if !debugMode {
close(stopXZoomChannel)
<-xZoomStoppedChannel
}
}
// I'm afraid I don't understand most of what this does :/
// TODO: if anyone can shed some light on this. Add some comments, refactor it...
func mainLoop() {
data := make([]byte, 0, 64)
for {
if cap(data)-len(data) < 32 {
newdata := make([]byte, len(data), len(data)+32)
copy(newdata, data)
data = newdata
}
beg := len(data)
d := data[beg : beg+32]
switch ev := termbox.PollRawEvent(d); ev.Type {
case termbox.EventRaw:
data = data[:beg+ev.N]
current = fmt.Sprintf("%q", data)
for {
ev := termbox.ParseEvent(data)
if ev.N == 0 {
break
}
curev = ev
if needToExit() {
log("Exit requested by user")
return
}
copy(data, data[curev.N:])
data = data[:len(data)-curev.N]
}
case termbox.EventError:
panic(ev.Err)
}
parseInput()
}
}
func main() {
initialise()
defer func() {
teardown()
}()
mainLoop()
}

18
interfacer/marionette.py Normal file
View File

@ -0,0 +1,18 @@
# The protocal is actually quite simple:
# message_size:[command_type, message_id, command, params]
# Eg; the following starts a new session:
# `o=JSON.dump([0,2,"newSession",{}]); puts "#{o.length}:#{o}"`
# So write this up in rust :D
from marionette_driver.marionette import Marionette
from marionette_driver.addons import Addons
client = Marionette('localhost', port=2828)
client.start_session()
print("session started")
# addons = Addons(client)
# addons.install("/home/tombh/Workspace/texttop/webext/web-ext-artifacts/browsh-0.1-an+fx.xpi")
client.navigate('http://media.giphy.com/media/3o6Zt4FQZaiDpAVu1O/giphy.gif')
print(client.get_url())

View File

@ -1,99 +0,0 @@
package main
import (
"github.com/tombh/termbox-go"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"testing"
"os"
)
func TestMouseInput(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Mouse Input Suite")
}
var _ = Describe("Mouse Input", func() {
BeforeEach(func() {
os.Setenv("DESKTOP_WIDTH", "1600")
os.Setenv("DESKTOP_HEIGHT", "1200")
os.Setenv("TTY_WIDTH", "90")
os.Setenv("TTY_HEIGHT", "30")
setupLogging()
termbox.Init()
setupDimensions()
setCurrentDesktopCoords()
})
AfterEach(func() {
termbox.Close()
})
Describe("Mouse position", func() {
It("Should work in the top left", func() {
curev.MouseX = 30
curev.MouseY = 10
setCurrentDesktopCoords()
Expect(roundedDesktopX).To(Equal(533))
Expect(roundedDesktopY).To(Equal(400))
})
It("Should work in the middle", func() {
curev.MouseX = 45
curev.MouseY = 15
setCurrentDesktopCoords()
Expect(roundedDesktopX).To(Equal(800))
Expect(roundedDesktopY).To(Equal(600))
})
It("Should work in the bottom right", func() {
curev.MouseX = 60
curev.MouseY = 20
setCurrentDesktopCoords()
Expect(roundedDesktopX).To(Equal(1067))
Expect(roundedDesktopY).To(Equal(800))
})
})
Describe("Zooming", func() {
BeforeEach(func() {
curev.MouseX = 30
curev.MouseY = 10
setCurrentDesktopCoords()
})
It("Should zoom in once", func() {
Expect(getXGrab()).To(Equal(0))
Expect(getYGrab()).To(Equal(0))
Expect(roundedDesktopX).To(Equal(533))
Expect(roundedDesktopY).To(Equal(400))
zoom("in")
setCurrentDesktopCoords()
Expect(getXGrab()).To(Equal(266))
Expect(getYGrab()).To(Equal(200))
Expect(roundedDesktopX).To(Equal(533))
Expect(roundedDesktopY).To(Equal(400))
})
It("Should zoom in then out", func() {
zoom("in")
setCurrentDesktopCoords()
zoom("out")
// Shouldn't need to do this a second time, but this test helped me
// figure out a different bug, so I'm leaving it like this for now.
zoom("out")
setCurrentDesktopCoords()
Expect(getXGrab()).To(Equal(0))
Expect(getYGrab()).To(Equal(0))
Expect(roundedDesktopX).To(Equal(533))
Expect(roundedDesktopY).To(Equal(400))
})
It("Should zoom near an edge without breaking out", func() {
curev.MouseX = 0
curev.MouseY = 0
setCurrentDesktopCoords()
zoom("in")
Expect(getXGrab()).To(Equal(0))
Expect(getYGrab()).To(Equal(0))
Expect(roundedDesktopX).To(Equal(0))
Expect(roundedDesktopY).To(Equal(0))
})
})
})

17
interfacer/ws-server.py Normal file
View File

@ -0,0 +1,17 @@
from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket
class SimpleEcho(WebSocket):
def handleMessage(self):
# Send cursor to 0,0
print("\x1b[H")
print(self.data)
def handleConnected(self):
print(self.address, 'connected')
def handleClose(self):
print(self.address, 'closed')
server = SimpleWebSocketServer('', 2794, SimpleEcho)
server.serveforever()

65
interfacer/ws-server.rs Normal file
View File

@ -0,0 +1,65 @@
extern crate termsize;
extern crate websocket;
use std::thread;
use std::str;
use std::io::{self, Write};
use websocket::OwnedMessage;
use websocket::sync::Server;
fn main() {
let server = Server::bind("127.0.0.1:2794").unwrap();
//println!("Starting");
for request in server.filter_map(Result::ok) {
// Spawn a new thread for each connection.
thread::spawn(move || {
let mut client = request.use_protocol("rust-websocket").accept().unwrap();
//let ip = client.peer_addr().unwrap();
//println!("Connection from {}", ip);
// Clear terminal
print!("\x1b[2J");
termsize::get().map(|size| {
let size = format!("/tty_size,{},{}", size.cols, size.rows);
let message = OwnedMessage::Text(size);
client.send_message(&message).unwrap();
});
let (mut receiver, mut sender) = client.split().unwrap();
for message in receiver.incoming_messages() {
let message = message.unwrap();
match message {
OwnedMessage::Close(_) => {
let message = OwnedMessage::Close(None);
sender.send_message(&message).unwrap();
//println!("Client {} disconnected", ip);
return;
}
OwnedMessage::Ping(ping) => {
let message = OwnedMessage::Pong(ping);
sender.send_message(&message).unwrap();
}
OwnedMessage::Text(text) => {
let output = match str::from_utf8(text.as_bytes()) {
Ok(v) => v,
Err(e) => panic!("Invalid UTF-8 sequence: {}", e),
};
print!("\x1b[H");
print!("{}", output);
io::stdout().flush().expect("Could not flush stdout");
}
_ => {
println!("Uknown data: {:?}", &message);
return;
}
}
}
});
}
}

3
webext/.web-extension-id Normal file
View File

@ -0,0 +1,3 @@
# This file was created by https://github.com/mozilla/web-ext
# Your auto-generated extension ID for addons.mozilla.org is:
{8ff2d753-2dc8-46de-a837-fa28331d9fcf}

81
webext/background.js Normal file
View File

@ -0,0 +1,81 @@
let tty_width, tty_height;
// Create WebSocket connection.
const socket = new WebSocket('ws://localhost:2794');
// Connection opened
socket.addEventListener('open', function (_event) {
console.log('Connected to Browsh client');
});
// Listen for messages
socket.addEventListener('message', (event) => {
console.log('Message from server ', event.data);
const parts = event.data.split(',');
const command = parts[0];
if (command === '/tty_size') {
tty_width = parts[1];
tty_height = parts[2];
}
});
let active_tab_id;
function handleActivated(active_info) {
console.log(active_info);
active_tab_id = active_info.id
}
browser.tabs.onActivated.addListener(handleActivated);
function handleRegistration(_request, sender, sendResponse) {
console.log('tab registered');
sendResponse(sender.tab);
if (sender.tab.active) active_tab_id = sender.tab.id;
}
browser.runtime.onMessage.addListener(handleRegistration);
let tabs = {};
function connected(channel) {
console.log('tab connected');
tabs[channel.name] = channel;
resizeBrowserWindow();
// Currently tabs will only ever send screen output over their channel
channel.onMessage.addListener(function(screen) {
socket.send(screen);
});
}
browser.runtime.onConnect.addListener(connected);
setInterval(() => {
if (tabs[active_tab_id] === undefined) return;
tabs[active_tab_id].postMessage(`/send_frame,${tty_width},${tty_height}`);
}, 1000);
function resizeBrowserWindow() {
const width = tty_width * 9;
const height = tty_height * 19.5; // this is actually line-height
var getting = browser.windows.getCurrent();
getting.then(
(windowInfo) => {
console.log('resizing window', windowInfo, width, height);
const updating = browser.windows.update(
windowInfo.id,
{
width: width,
height: height
}
);
updating(
(info) => {
console.log(info);
},
(error) => {
console.error(error);
}
);
},
(error) => {
console.error(error);
}
);
}

4
webext/content.js Normal file
View File

@ -0,0 +1,4 @@
import FrameBuilder from 'frame_builder';
new FrameBuilder();

View File

View File

29
webext/manifest.json Normal file
View File

@ -0,0 +1,29 @@
{
"manifest_version": 2,
"name": "Browsh",
"version": "0.1",
"description": "Renders the browser as realtime, interactive, TTY-compatible text",
"icons": {
"48": "icons/browsh-48.png",
"96": "icons/browsh-96.png"
},
"background": {
"scripts": ["dist/background.js"]
},
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["dist/content.js"],
"css": ["src/styles.css"],
"run_at": "document_start"
}
],
"permissions": [
"<all_urls>"
]
}

4153
webext/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
webext/package.json Normal file
View File

@ -0,0 +1,19 @@
{
"scripts": {
"test": "NODE_PATH=src:test mocha"
},
"babel": {
"presets": ["es2015"]
},
"devDependencies": {
"babel-preset-es2015": "^6.24.1",
"babel-register": "^6.26.0",
"chai": "^4.1.2",
"mocha": "^4.1.0",
"sinon": "^4.1.3",
"webpack": "^3.10.0"
},
"dependencies": {
"lodash": "^4.17.4"
}
}

View File

@ -0,0 +1,17 @@
export default class BaseBuilder {
logPerformance(work, reference) {
let start = performance.now();
work();
let end = performance.now();
this.firstFrameLog(reference, end - start);
}
// If you're logging large objects and using a high-ish FPS (<1000ms) then you might
// crash the browser. So use this function instead.
firstFrameLog(...logs) {
if (this.is_first_frame_finished) return;
if (BUILD_ENV === 'development') {
console.log(logs);
}
}
}

222
webext/src/frame_builder.js Normal file
View File

@ -0,0 +1,222 @@
import BaseBuilder from 'base_builder';
import GraphicsBuilder from 'graphics_builder';
import TextBuilder from 'text_builder';
// Takes the graphics and text from the current viewport, combines them, then
// sends it to the background process where the the rest of the UI, like tabs,
// address bar, etc will be added.
export default class FrameBuilder extends BaseBuilder{
constructor() {
super();
// ID for element we place in the DOM to measure the size of a single monospace
// character.
this.measuring_box_id = 'browsh_em_measuring_box';
this.graphics_builder = new GraphicsBuilder();
this.text_builder = new TextBuilder();
document.addEventListener("DOMContentLoaded", () => {
this.init();
}, false);
// Whilst developing this webextension the auto reload only reloads this code,
// not the page, so we don't get the `DOMContentLoaded` event to kick everything off.
if (this.isWindowAlreadyLoaded()) this.init(100);
}
isWindowAlreadyLoaded() {
return !!this.findMeasuringBox();
}
init(delay = 0) {
console.log('Browsh init()');
this.calculateMonospaceDimensions();
// When the webext devtools auto reloads this code the background process
// can sometimes still be loading, in which case we need to wait.
setTimeout(() => this.registerWithBackground(), delay);
}
registerWithBackground() {
let sending = browser.runtime.sendMessage('/register');
sending.then(
(r) => this.registrationSuccess(r),
(e) => this.registrationError(e)
);
}
// The background process tells us when it wants a frame.
registrationSuccess(registered) {
this.channel = browser.runtime.connect({
// We need to give ourselves a unique channel name, so the background
// process can identify us amongst other tabs.
name: registered.id.toString()
});
this.channel.onMessage.addListener((message) => {
const parts = message.split(',');
const command = parts[0];
const tty_width = parseInt(parts[1]);
const tty_height = parseInt(parts[2]);
if (command === '/send_frame') {
this.sendFrame(tty_width, tty_height);
}
});
}
registrationError(error) {
console.error(error);
}
// This is critical in order for the terminal to match the browser as closely as possible.
// Ideally we want the browser's window size to be exactly multiples of the terminal's
// dimensions. So if the terminal is 80x40 and the font-size is 12px (12x6 pixels), then
// the window should be 480x480. Also knowing the precise font-size helps the text builder
// map un-snapped text to the best grid cells - grid cells that represent the terminal's
// character positions.
// The reason that we can't just do some basic maths on the CSS `font-size` value we enforce
// is that there are various factors that can skew the actual font dimensions on the page.
// For instance, you can't guarantee that a browser is using exactly the same version of
// a named monospace font. Also different browser families and even different versions of
// the same browser may have subtle differences in how they render text. Furthermore we can
// actually get floating point accuracy if we use `Element.getBoundingClientRect()` which
// further helps as calculations are compounded during our rendering processes.
calculateMonospaceDimensions() {
const element = this.getOrCreateMeasuringBox();
const dom_rect = element.getBoundingClientRect();
this.char_width = dom_rect.width;
this.char_height = dom_rect.height;
this.text_builder.char_width = this.char_width;
this.text_builder.char_height = this.char_height;
console.log('char dimensions', this.char_width, this.char_height);
}
getOrCreateMeasuringBox() {
let measuring_box = this.findMeasuringBox();
if (measuring_box) return measuring_box;
measuring_box = document.createElement('span');
measuring_box.id = this.measuring_box_id;
measuring_box.style.visibility = 'hidden';
// Back when printing was done by physical stamps, it was convention to measure the
// font-size using the letter 'M', thus where we get the unit 'em' from. Not that it
// should make any difference to us, but it's nice to keep a tradition.
var M = document.createTextNode('M');
measuring_box.appendChild(M);
document.body.appendChild(measuring_box);
return measuring_box;
}
findMeasuringBox() {
return document.getElementById(this.measuring_box_id);
}
sendFrame(tty_width, tty_height) {
this.setupDimensions(tty_width, tty_height);
this.compileFrame();
this.logPerformance(() => {
this.buildFrame();
}, 'build frame');
this.channel.postMessage(this.screen);
this.is_first_frame_finished = true;
}
setupDimensions(tty_width, tty_height) {
this.tty_width = tty_width;
this.tty_height = tty_height;
this.frame_width = tty_width;
// A frame is 'taller' than the TTY because of the special UTF8 half-block
// trick.
this.frame_height = tty_height * 2;
}
compileFrame() {
this.logPerformance(() => {
this.pixels_with_text = this.graphics_builder.getSnapshotWithText(
this.frame_width,
this.frame_height
);
}, 'get snapshot with text');
this.logPerformance(() => {
this.pixels_without_text = this.graphics_builder.getSnapshotWithoutText(
this.frame_width,
this.frame_height
);
}, 'get snapshot without text');
this.formatted_text = this.text_builder.getFormattedText(
this,
this.graphics_builder
);
}
buildFrame() {
this.screen = "";
this.bg_row = [];
this.fg_row = [];
for (let y = 0; y < this.frame_height; y++) {
for (let x = 0; x < this.frame_width; x++) {
this.buildPixel(x, y);
}
}
}
// Note how we have to keep track of 2 rows of pixels in order to create 1 row of
// the terminal.
buildPixel(x, y) {
let pixel_data_start, r, g, b;
pixel_data_start = y * (this.frame_width * 4) + (x * 4);
r = this.pixels_without_text[pixel_data_start + 0];
g = this.pixels_without_text[pixel_data_start + 1];
b = this.pixels_without_text[pixel_data_start + 2];
if (this.bg_row.length < this.frame_width) {
this.bg_row.push([r, g, b]);
} else {
this.fg_row.push([r, g, b]);
}
if (this.fg_row.length === this.frame_width) {
this.screen += this.buildTtyRow(this.bg_row, this.fg_row, y);
this.bg_row = [];
this.fg_row = [];
}
}
// This is where we implement the UTF8 half-block trick.
// This is a half-black: ▄ Notice how it takes up precisely half a text cell. This
// means that we can get 2 pixel colours from it, the top pixel comes from setting
// the background colour and the bottom pixel comes from setting the foreground
// colour, namely the colour of the text.
// However we can't just write random pixels to a TTY screen, we must collate 2 rows
// of native pixels for every row of the terminal.
buildTtyRow(bg_row, fg_row, y) {
let tty_index, char;
let row = "";
const tty_row = parseInt(y / 2);
for (let x = 0; x < this.frame_width; x++) {
tty_index = (tty_row * this.frame_width) + x;
if (this.doesCellHaveACharacter(tty_index)) {
char = this.formatted_text[tty_index];
row += this.ttyPixel(char[1], char[2], char[0]);
} else {
row += this.ttyPixel(fg_row[x], bg_row[x], '▄');
}
}
if (tty_row + 1 < this.tty_height) {
row += "\n";
}
return row;
}
// We need to know this because we want all empty cells to be 'transparent'
doesCellHaveACharacter(index) {
if (this.formatted_text[index] === undefined) return false;
const char = this.formatted_text[index][0];
const is_undefined = char === undefined;
const is_empty = char === '';
const is_space = /^\s+$/.test(char);
const is_not_worth_printing = is_empty || is_space || is_undefined;
return !is_not_worth_printing;
}
// Display a single character in true colour
ttyPixel(fg, bg, character) {
let fg_code = `\x1b[38;2;${fg[0]};${fg[1]};${fg[2]}m`;
let bg_code = `\x1b[48;2;${bg[0]};${bg[1]};${bg[2]}m`;
return `${fg_code}${bg_code}${character}`;
}
}

View File

@ -0,0 +1,92 @@
import BaseBuilder from 'base_builder';
// Converts an instance of the viewport into a scaled array of pixel values.
// Note, that it does this both with and without the text visible in order
// to aid in a clean seperation of the graphics and text in the final frame
// renderd in the terminal.
export default class GraphicsBuilder extends BaseBuilder {
// Note that `frame_height` is twice the height of our TTY's height
// because of the special UTF8 half-block trick that gets us 2 'pixels'
// per character cell.
constructor() {
super();
this.off_screen_canvas = document.createElement('canvas');
this.ctx = this.off_screen_canvas.getContext('2d');
this.updateCurrentViewportDimensions();
}
getSnapshotWithoutText(frame_width, frame_height) {
this.frame_width = frame_width;
this.frame_height = frame_height;
this.hideText();
let snapshot = this.getSnapshot();
this.showText();
return snapshot;
}
hideText() {
this.styles = document.createElement("style");
document.head.appendChild(this.styles);
this.styles.sheet.insertRule(
'html * { color: transparent !important; }'
);
}
showText() {
this.styles.parentNode.removeChild(this.styles);
}
getSnapshotWithText(frame_width, frame_height) {
this.frame_width = frame_width;
this.frame_height = frame_height;
return this.getSnapshot();
}
getSnapshot() {
this.updateCurrentViewportDimensions()
this.scaleCanvas();
let pixel_data = this.getPixelData();
this.is_first_frame_finished = true;
return pixel_data;
}
// Perhaps the window has been resized to better accomodate text-sizing, or to try
// to trigger some mobile responsive CSS.
// And of course also the page may have been scrolled.
updateCurrentViewportDimensions() {
this.viewport_width = window.innerWidth;
this.viewport_height = window.innerHeight;
this.page_x_position = window.scrollX;
this.page_y_position = window.scrollY;
// Resize our canvas to match the viewport. I guess this makes for efficient
// use of memory?
this.off_screen_canvas.width = this.viewport_width;
this.off_screen_canvas.height = this.viewport_height;
}
// Scale the screenshot so that 1 pixel approximates one TTY cell.
// TODO: Allow one of these to be manually adjusted in realtime through the Browsh client
// in order for the user to set the correct aspect ratio for their particular terminal
// setup.
scaleCanvas() {
let scale_x = this.frame_width / this.viewport_width;
let scale_y = this.frame_height / this.viewport_height;
this.ctx.scale(scale_x, scale_y);
}
// Get an array of RGB values.
// This is Firefox-only. Chrome has a nicer MediaStream for this.
getPixelData() {
let background_colour = 'rgb(255,255,255)';
this.ctx.drawWindow(
window,
this.page_x_position,
this.page_y_position,
this.viewport_width,
this.viewport_height,
background_colour
);
return this.ctx.getImageData(0, 0, this.frame_width, this.frame_height).data;
}
}

9
webext/src/styles.css Normal file
View File

@ -0,0 +1,9 @@
html * {
font-family: monospace !important;
font-size: 15px !important;
line-height: 20px !important;
}
sup, sub {
vertical-align: baseline !important;
}

286
webext/src/text_builder.js Normal file
View File

@ -0,0 +1,286 @@
import _ from 'lodash';
import BaseBuilder from 'base_builder';
// Convert the text on the page into a snapped 2-dimensional grid to be displayed directly
// in the terminal.
export default class TextBuillder extends BaseBuilder {
constructor() {
super();
this.tty_grid = [];
this.parse_started_elements = [];
}
getFormattedText(frame_builder, graphics_builder) {
this.tty_width = frame_builder.tty_width;
this.tty_height = frame_builder.ty_height;
this.char_width = frame_builder.char_width;
this.char_height = frame_builder.char_height;
this.pixels_with_text = frame_builder.pixels_with_text;
this.pixels_without_text = frame_builder.pixels_without_text;
this.logPerformance(() => {
// This is relatively cheap: around 50ms for a 13,000 word Wikipedia page.
this.getTextNodes();
}, 'Tree Walker');
this.setViewportDimensions(graphics_builder);
this.logPerformance(() => {
// This should be around 125ms for a largish Wikipedia page of 13,000 words.
this.positionTextNodes();
}, 'position text nodes');
this.is_first_frame_finished = true;
return this.tty_grid;
}
setViewportDimensions(graphics_builder) {
this.viewport = {
width: graphics_builder.viewport_width,
height: graphics_builder.viewport_height
}
}
// Search through every node in the DOM looking for displayable text.
getTextNodes() {
this.text_nodes = [];
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{ acceptNode: this.isRelevantTextNode },
false
);
while(walker.nextNode()) this.text_nodes.push(walker.currentNode);
}
// Does the node contain text that we want to display?
// TODO: Exclude text outside of viewport
isRelevantTextNode(node) {
// Ignore nodes with only whitespace
if (/^\s+$/.test(node.textContent) || node.textContent === '') {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
// This is the key to being able to display formatted text within the strict confines
// of a TTY. DOM Rectangles are closely related to selection ranges (like when you click
// and drag the mouse cursor over text). Think of an individual DOM rectangle as a single
// bar of highlighted selection. So that, for example, a 3 line paragraph will have 3
// DOM rectangles. Fortunately DOMRect coordinates and dimensions are precisely defined.
// Although do note that, unlike selection ranges, sub-selections can appear seemingly
// inside other selections for things like italics or anchor tags.
positionTextNodes() {
let range = document.createRange();
let bounding_box;
for (const node of this.text_nodes) {
range.selectNode(node);
bounding_box = range.getBoundingClientRect();
if (this.isOutsideViewport(bounding_box)) continue;
this.fixJustifiedText(node);
this.formatTextForTTYGrid(
this.normaliseWhitespace(node.textContent, node.parentElement),
range.getClientRects()
);
}
}
// If even a single pixel is inside the viewport we need to check it
isOutsideViewport(bounding_box) {
const is_top_in =
bounding_box.top >= 0 &&
bounding_box.top < this.viewport.height;
const is_bottom_in =
bounding_box.bottom >= 0 &&
bounding_box.bottom < this.viewport.height;
const is_left_in =
bounding_box.left >= 0 &&
bounding_box.left < this.viewport.width;
const is_right_in =
bounding_box.right >= 0 &&
bounding_box.right < this.viewport.width;
return !((is_top_in || is_bottom_in) && (is_left_in || is_right_in));
}
// Justified text uses the space between words to stretch a line to perfectly fit from
// end to end. That'd be ok if it only stretched by exact units of monospace width, but
// it doesn't, which messes with our fragile grid system.
// TODO:
// * It'd be nice to detect right-justified text so we can keep it. Just need to be
// careful with things like traversing parents up the DOM, or using `computedStyle()`
// because they can be expensive.
// * Another approach could be to explore how a global use of `pre` styling renders
// pages.
// * Also, is it possible and/or faster to do this once in the main style sheet? Or
// even by a find-replace on all occurrences of 'justify'?
// * Yet another thing, the style change doesn't actually get picked up until the
// next frame.
fixJustifiedText(node) {
node.parentElement.style.textAlign = 'left';
}
// The need for this wasn't immediately obvious to me. The fact is that the DOM stores
// text nodes _as they are written in the HTML doc_. Therefore, if you've written some
// nicely indented HTML, then the text node will actually contain those as something like
// `\n text starts here`
// It's just that the way CSS works most of the time means that whitespace is collapsed
// so viewers never notice.
//
// TODO:
// The normalisation here of course destroys the formatting of `white-space: pre`
// styling, like code snippets for example. So hopefully we can detect the node's
// `white-space` setting and skip this function if necessary?
normaliseWhitespace(text, parent) {
text = text.replace(/[\t\n\r ]+/g, " ");
if (this.isFirstParseInElement(parent)) {
if (text.charAt(0) === " ") text = text.substring(1, text.length);
}
if (text.charAt(text.length - 1) === " ") text = text.substring(0, text.length - 1);
return text;
}
// An element may contain many text nodes. For example a `<p>` element may contain a
// starting text node followed by a `<a>` tag, finishing with another plain text node. We
// only want to remove leading whitespace from the text at the _beginning_ of a line.
// Usually we can do this just by checking if a DOM rectangle's position is further down
// the page than the previous one - but of course there is nothing to compare the first
// DOM rectangle to. What's more, DOM rects are grouped per _text node_, NOT per element
// and we are not guaranteed to iterate through elements in the order that text flows.
// Therefore we need to make the assumption that plain text nodes flow within their shared
// parent element. There is a possible caveat here for elements starting with another
// element (like a link), where that sub-element contains leading whitespace.
isFirstParseInElement(element) {
const is_parse_started = _.includes(this.parse_started_elements, element);
if (is_parse_started) {
return false
} else {
this.parse_started_elements.push(element);
return true
}
}
// Here is where we actually make use of the rather strict monospaced and fixed font size
// CSS rules enforced by the webextension. Of course the CSS is never going to be able to
// perfectly snap characters onto a grid, so we force it here instead. At least we can be
// fairly certain that every character at least takes up the same space as a TTY cell, it
// just might not be perfectly aligned. So here we just round down all coordinates to force
// the snapping.
formatTextForTTYGrid(text, dom_rects) {
let col, tty_box, step, character, previous_box;
let character_index = 0;
for (const box of dom_rects) {
if (this.isNewLine(previous_box, box)) {
character = text.charAt(character_index);
if (/[\t\n\r ]+/.test(character)) character_index++;
}
tty_box = this.convertBoxToTTYUnits(box);
col = tty_box.col_start;
for (step = 0; step < tty_box.width; step++) {
character = text.charAt(character_index);
this.placeCharacterOnTTYGrid(
col,
tty_box.row,
character
);
col++;
character_index++;
}
previous_box = box;
}
}
isNewLine(previous_box, current_box) {
if (previous_box === undefined) return false;
return current_box.top > previous_box.top
}
convertBoxToTTYUnits(viewport_dom_rect) {
return {
col_start: Math.round(viewport_dom_rect.left / this.char_width),
row: Math.round(viewport_dom_rect.top / this.char_height),
width: Math.round(viewport_dom_rect.width / this.char_width),
}
}
placeCharacterOnTTYGrid(col, row, character) {
//let pixel_data_start;
//let fg = [];
//let bg = [];
const index = (row * this.tty_width) + col;
if (this.isCharOutsideGrid(col, row)) return;
//if (this.isTextObscured(index)) return;
// Don't clobber for now. TODO: Use `getComputedStyles()` and save for whole element.
if (this.tty_grid[index] === undefined) {
//pixel_data_start = ((row * 2) + 1) * (this.tty_width * 4) + (col * 4);
//fg[0] = this.pixels_with_text[pixel_data_start + 0];
//fg[1] = this.pixels_with_text[pixel_data_start + 1];
//fg[2] = this.pixels_with_text[pixel_data_start + 2];
//bg[0] = this.pixels_without_text[pixel_data_start + 0];
//bg[1] = this.pixels_without_text[pixel_data_start + 1];
//bg[2] = this.pixels_without_text[pixel_data_start + 2];
this.tty_grid[index] = [
character,
[0,0,0],
[255,255,255]
];
}
}
// Theoretically this should only be needed for DOM rectangles that _straddle_ the
// viewport.
isCharOutsideGrid(col, row) {
return col + 1 >= this.tty_width || row + 1 >= this.tty_height;
}
// This is somewhat of a, hopefully elegant, hack. So, imagine that situation where you're
// browsing a web page and a popup appears; perhaps just a select box, or menu, or worst
// of all a dreaded full-page overlay. Now, DOM rectangles don't take into account whether
// they are the uppermost visible element, so we're left in a bit of a pickle. The only JS
// way to know if an element is visible is to use `Document.elementFromPoint(x, y)`, where
// you compare the returned element with the element whose visibility you're checking.
// This is has a number of problems. Firstly, it only checks one coordinate in the element
// for visibility, which of course isn't going to be 100% reliably speak for all the
// characters in the element. Secondly, even ignoring the first caveat, running
// `elementFromPoint()` for every character is very expensive, around 25ms for an average
// DOM. So it's basically a no-go. So instead we take advantage of the fact that we're
// working with a very scaled down version of the webpage's pixels. In fact not just any
// scale, but the scale such that every pixel represents a single character. As such it's
// fairly safe to assume that if you make the text transparent and a pixel's colour
// doesn't change then that character must be obscured by something.
// There are of course some potential edge cases with this. What if, as a result of a
// significant scaling a '.', or a ',' becomes so small that it doesn't actually register
// any colour change whatsoever? Or what if we get a false positive, where a character is
// obscured _by another character_? Hopefully in such a case we can work with `z-index`
// so that characters justifiably overwrite each other in the TTY grid.
// TODO: compare top and bottom pixel of text row.
isTextObscured(index) {
return _.isEqual(
this.pixels_with_text[index],
this.pixels_without_text[index]
);
}
// Purely for debugging. Draws a red border around all the DOMClientRect nodes.
// Based on code from the MDN docs site.
addClientRectsOverlay(dom_rects, normalised_text) {
// Don't draw on every frame
if (this.first_call_finished) return;
// Absolutely position a div over each client rect so that its border width
// is the same as the rectangle's width.
// Note: the overlays will be out of place if the user resizes or zooms.
for (const rect of dom_rects) {
let tableRectDiv = document.createElement('div');
// A DOMClientRect object only contains dimensions, so there's no way to identify it
// to a node, so let's put its text as an attribute so we can cross-check if needs be.
tableRectDiv.setAttribute('browsh-text', normalised_text);
tableRectDiv.style.position = 'absolute';
tableRectDiv.style.border = '1px solid red';
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
let scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
tableRectDiv.style.margin = tableRectDiv.style.padding = '0';
tableRectDiv.style.top = (rect.top + scrollTop) + 'px';
tableRectDiv.style.left = (rect.left + scrollLeft) + 'px';
// We want rect.width to be the border width, so content width is 2px less.
tableRectDiv.style.width = (rect.width - 2) + 'px';
tableRectDiv.style.height = (rect.height - 2) + 'px';
document.body.appendChild(tableRectDiv);
}
}
}

8
webext/test/fixtures/canvas_pixels.js vendored Normal file
View File

@ -0,0 +1,8 @@
let base = [
255,255,255,0, 0 ,0 ,0 ,0, 255,255,255,0,
0 ,0 ,0 ,0, 255,255,255,0, 0 ,0 ,0 ,0,
255,255,255,0, 0 ,0 ,0 ,0, 255,255,255,0,
0 ,0 ,0 ,0, 255,255,255,0, 0 ,0 , 0,0
];
export default base;

18
webext/test/fixtures/text_grid.js vendored Normal file
View File

@ -0,0 +1,18 @@
let base = build(
[
'', 'x', '',
'x', '', 'x'
],
[111, 111, 111],
[222, 222, 222]
);
export default base;
function build(text, fg_colour, bg_colour) {
let grid = [];
for(const character of text) {
grid.push([character, fg_colour, bg_colour]);
}
return grid;
}

26
webext/test/fixtures/text_nodes.js vendored Normal file
View File

@ -0,0 +1,26 @@
// It'd be nice to somehow automate the creation of the coordinates here
let base = {
textContent: "\n testing nodes",
parentElement: {
style: {}
},
bounding_box: {
top: 10.1,
bottom: 50.1,
left: 10.1,
right: 80.1
},
dom_rects: [{
top: 10.1,
left: 10.1,
width: 70.1
},
{
left: 10.1,
top: 30.1,
width: 50.1
}]
};
export default [base];

View File

@ -0,0 +1,36 @@
import 'helper';
import sinon from 'sinon';
import {expect} from 'chai';
import FrameBuilder from 'frame_builder';
import GraphicsBuilder from 'graphics_builder';
import TextBuilder from 'text_builder';
import canvas_pixels from 'fixtures/canvas_pixels';
import text_grid from 'fixtures/text_grid';
describe('Frame Builder', ()=> {
let frame_builder;
beforeEach(()=> {
sinon.stub(GraphicsBuilder.prototype, 'getSnapshotWithText').returns(canvas_pixels);
sinon.stub(GraphicsBuilder.prototype, 'getSnapshotWithoutText').returns(canvas_pixels);
sinon.stub(TextBuilder.prototype, 'getFormattedText').returns(text_grid);
frame_builder = new FrameBuilder();
frame_builder.channel = {
postMessage: () => {}
}
});
it('should merge pixels and text into ANSI true colour syntax', ()=> {
frame_builder.sendFrame(3, 2);
const screen = frame_builder.screen.replace(/\u001b\[/g, 'ESC');
expect(screen).to.eq(
'ESC38;2;0;0;0mESC48;2;255;255;255m▄' +
'ESC38;2;111;111;111mESC48;2;222;222;222mx' +
'ESC38;2;0;0;0mESC48;2;255;255;255m▄\n' +
'ESC38;2;111;111;111mESC48;2;222;222;222mx' +
'ESC38;2;255;255;255mESC48;2;0;0;0m▄' +
'ESC38;2;111;111;111mESC48;2;222;222;222mx'
);
});
});

20
webext/test/helper.js Normal file
View File

@ -0,0 +1,20 @@
import MockRange from 'mocks/range'
global.document = {
addEventListener: () => {},
getElementById: () => {},
createRange: () => {
return new MockRange()
},
createElement: () => {
return {
getContext: () => {}
}
}
};
global.BUILD_ENV = {};
global.window = {};
global.performance = {
now: () => {}
}

1
webext/test/mocha.opts Normal file
View File

@ -0,0 +1 @@
--require babel-register

View File

@ -0,0 +1,11 @@
export default class MockRange {
selectNode(node) {
this.node = node;
}
getBoundingClientRect() {
return this.node.bounding_box;
}
getClientRects() {
return this.node.dom_rects;
}
}

View File

@ -0,0 +1,18 @@
#content {
width: 1000px;
margin: auto;
}
h1 {
text-align: center;
}
.left_col {
width: 45%;
float: left;
}
.right_col {
width: 45%;
float: right;
}

View File

@ -0,0 +1,32 @@
/* Animation */
@-webkit-keyframes spinner {
to { -webkit-transform: rotate(360deg); }
}
@-moz-keyframes spinner {
to { -moz-transform: rotate(360deg); }
}
@-ms-keyframes spinner {
to { -ms-transform: rotate(360deg); }
}
@keyframes spinner {
to { transform: rotate(360deg); }
}
/* Loader (*/
#spinner {
margin: auto;
width: 200px;
height: 200px;
border-radius: 50%;
background-image: linear-gradient(bottom, #FF00FF 50%, #00FFFF 50%);
background-image: -o-linear-gradient(bottom, #FF00FF 50%, #00FFFF 50%);
background-image: -moz-linear-gradient(bottom, #FF00FF 50%, #00FFFF 50%);
background-image: -webkit-linear-gradient(bottom, #FF00FF 50%, #00FFFF 50%);
background-image: -ms-linear-gradient(bottom, #FF00FF 50%, #00FFFF 50%);
-webkit-animation: spinner 30s infinite linear;
-moz-animation: spinner 30s infinite linear;
-ms-animation: spinner 30s infinite linear;
animation: spinner 30s infinite linear;
}

View File

@ -0,0 +1,26 @@
<html>
<head>
<meta charset="utf-8">
<title>Smörgåsbord</title>
<link href="css/main.css" rel="stylesheet" type="text/css" />
<link href="css/spinner.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="content">
<div id="spinner"></div>
<h1>Smörgåsbord</h1>
<div class="left_col">
Smörgåsbord (Swedish: [ˈsmœrɡɔsˌbuːɖ]) is a type of Scandinavian meal,
originating in Sweden, served buffet-style with multiple hot and cold
dishes of various foods on a table.
</div>
<div class="right_col">
The <a href="/"?>Swedish</a> word smörgåsbord consists of the words smörgås (sandwich,
usually open-faced) and bord (table). Smörgås in turn consists of the
words smör (butter, cognate with English smear) and gås. Gås literally
means goose, but later referred to the small pieces of butter that
formed and floated to the surface of cream while it was churned.
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,36 @@
import 'helper';
import _ from 'lodash';
import {expect} from 'chai';
import TextBuilder from 'text_builder';
import text_nodes from 'fixtures/text_nodes';
const defaults = {
tty_width: 10,
tty_height: 5,
char_width: 10,
char_height: 20,
viewport: {
height: 100,
width: 100
},
text_nodes: text_nodes
}
describe('Text Builder', ()=> {
let text_builder;
beforeEach(()=> {
text_builder = new TextBuilder();
_.merge(text_builder, defaults);
});
it('should convert text nodes to a grid', ()=> {
text_builder.positionTextNodes();
expect(text_builder.tty_grid[0]).to.be.undefined;
expect(text_builder.tty_grid[11]).to.deep.equal(['t', [0, 0, 0], [255, 255, 255]]);
expect(text_builder.tty_grid[17][0]).to.eq('g');
expect(text_builder.tty_grid[21][0]).to.eq('n');
expect(text_builder.tty_grid[25][0]).to.eq('s');
});
});

25
webext/webpack.config.js Normal file
View File

@ -0,0 +1,25 @@
const webpack = require('webpack');
const path = require('path');
module.exports = {
entry: {
content: './content.js',
background: './background.js'
},
output: {
path: __dirname,
filename: 'dist/[name].js',
},
resolve: {
modules: [
path.resolve(__dirname, './src'),
'node_modules'
]
},
plugins: [
new webpack.DefinePlugin({
// TODO: For production use a different webpack.config.js
BUILD_ENV: 'development'
})
]
};

View File

@ -1,25 +0,0 @@
Although the xzoom code has been heavily modified, I include the original license.
---
This package was maintained by tony mancill <tmancill@debian.org>, for a
while, and then picked up by Junichi Uekawa <dancer@debian.org>. Since Sun, 15 Jun 2008 maintained by Anibal Avelar <aavelar@cofradia.org>.
This package was put together by Joey Hess <joeyh@debian.org>, using
sources from:
ftp://sunsite.unc.edu/pub/linux/libs/X/xzoom-0.3.tgz
Copyright: 1995-2008 Itai Nahshon
License:
This program is distributed with no warranty.
Source files for this program may be distributed freely.
Modifications to this file are okay as long as:
a. This copyright notice and comment are preserved and
left at the top of the file.
b. The man page is fixed to reflect the change.
c. The author of this change adds his name and change
description to the list of changes below.
Executable files may be distributed with sources, or with
exact location where the source code can be obtained.

View File

@ -1,57 +0,0 @@
/* scale image from SRC to DST - parameterized by type T */
/* get pixel address of point (x,y) in image t */
#define getP(t, x, y) \
(T *)(&ximage[t]->data[(ximage[t]->xoffset + (x)) * sizeof(T) + \
(y) * ximage[t]->bytes_per_line])
{
int i, j, k;
/* copy scaled lines from SRC to DST */
j = height[SRC] - 1;
do {
T *p1;
T *p2;
int p2step;
T *p1_save;
/* p1 point to begining of scanline j*magnification in DST */
p1 = getP(DST, 0, j * magnification);
p1_save = p1;
/* p2 point to begining of scanline j in SRC */
p2 = getP(SRC, 0, j);
i = width[SRC];
do {
T c = *p2++;
k = magnification;
do *p1++ = c; while (--k > 0);
} while (--i > 0);
/* duplicate that line as needed */
if (magnification > 1)
{
/* p1 point to begining of scanline j*magnification in DST */
p1 = p1_save;
/* p2 points to begining of next line */
p2 = p1;
p2step = ximage[DST]->bytes_per_line / sizeof(T);
i = width[DST] * sizeof(T);
k = magnification - 1;
do {
p2 += p2step;
memcpy(p2, p1, i);
} while (--k > 0);
}
} while (--j >= 0);
}
#undef getP

View File

@ -1,165 +0,0 @@
/*
This code largely comes from Itai Nahshon's xzoom, see:
http://git.r-36.net/xzoom
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#include <X11/cursorfont.h>
#include <X11/keysym.h>
Display *dpy;
Screen *scr;
Window win, root, child;
Status status;
int winX, winY;
unsigned int mask;
GC gc;
#define SRC 0
#define DST 1
int desktop_width;
int desktop_height;
// The top left of the area that we zoom in on
int xgrab = 0;
int ygrab = 0;
int magnification = 1;
int old_magnification = 1;
int width[2];
int height[2];
unsigned depth = 0;
XImage *ximage[2];
int created_images = False;
void scale16(void)
{
#define T unsigned short
#include "scale.h"
#undef T
}
void scale32(void)
{
#define T unsigned int
#include "scale.h"
#undef T
}
void allocate_images(void) {
int i;
for (i = 0; i < 2; i++) {
char *data;
data = malloc(BitmapUnit(dpy) / 8 * width[i] * height[i]);
ximage[i] = XCreateImage(dpy,
DefaultVisualOfScreen(scr),
DefaultDepthOfScreen(scr),
ZPixmap, 0, data,
width[i], height[i], 32, 0);
if (ximage[i] == NULL) {
perror("XCreateImage");
exit(-1);
}
}
created_images = True;
}
void destroy_images(void) {
int i;
if (!created_images) return;
for (i = 0; i < 2; i++) {
free(ximage[i]->data);
ximage[i]->data = NULL;
XDestroyImage(ximage[i]);
}
created_images = False;
}
// Update the zoom window with the current state of the desktop.
// Happens at 25fps.
void update_zoom_window_with_desktop() {
// Get a snapshot of the desktop, or a portion of the desktop
XGetSubImage(dpy, RootWindowOfScreen(scr),
xgrab, ygrab, width[SRC], height[SRC], AllPlanes,
ZPixmap, ximage[SRC], 0, 0);
// Zoom in on that snapshot
if (depth <= 8 * sizeof(short)) scale16();
else if (depth <= 8 * sizeof(int)) scale32();
// Put the snapshot into the xzoom window
XPutImage(dpy, win, gc, ximage[DST], 0, 0, 0, 0, width[DST], height[DST]);
XSync(dpy, 0);
}
void recreate_images_on_zoom() {
if (old_magnification != magnification) {
destroy_images();
allocate_images();
}
old_magnification = magnification;
}
int xzoom_init() {
XSetWindowAttributes xswa;
XGCValues gcv;
char *dpyname = ":0";
atexit(destroy_images);
if (!(dpy = XOpenDisplay(dpyname))) {
perror("Cannot open display");
exit(-1);
}
scr = DefaultScreenOfDisplay(dpy);
depth = DefaultDepthOfScreen(scr);
win = XCreateWindow(dpy, RootWindowOfScreen(scr),
desktop_width, 0, width[DST], height[DST], 0,
DefaultDepthOfScreen(scr), InputOutput,
DefaultVisualOfScreen(scr),
CWEventMask | CWBackPixel, &xswa);
status = XMapWindow(dpy, win);
gcv.plane_mask = AllPlanes;
gcv.subwindow_mode = IncludeInferiors;
gcv.function = GXcopy;
gcv.foreground = WhitePixelOfScreen(scr);
gcv.background = BlackPixelOfScreen(scr);
gc = XCreateGC(dpy, RootWindowOfScreen(scr),
GCFunction |
GCPlaneMask |
GCSubwindowMode |
GCForeground |
GCBackground,
&gcv);
allocate_images();
}
void do_iteration() {
recreate_images_on_zoom();
update_zoom_window_with_desktop();
}