├── tutorials ├── bin │ └── .gitkeep ├── advanced_single_binary_distribution │ ├── provisioner │ │ ├── .gitignore │ │ └── single_binary_thrust_provisioner.go │ ├── .gitignore │ └── advanced_single_binary_distribution.go ├── provisioner │ └── tutorial_provisioner.go ├── basic_multiple_windows │ └── basic_multiple_windows.go ├── advanced_window_devtools │ └── advanced_window_devtools.go ├── basic_window │ └── basic_window.go ├── basic_frameless_window │ └── basic_frameless_window.go ├── basic_session │ └── basic_session.go ├── basic_webserver_app │ └── basic_webserver_app.go ├── basic_menu │ └── basic_menu.go ├── basic_remote_messaging │ └── basic_remote_messaging.go ├── basic_menu_events │ └── basic_menu_events.go ├── advanced_session │ └── advanced_session.go └── basic_window_events │ └── basic_window_events.go ├── examples ├── chat │ ├── .gitignore │ ├── client │ │ ├── run_client │ │ └── client.go │ ├── server │ │ ├── run_server │ │ ├── chat.html │ │ ├── chat.css │ │ ├── chat.html.go │ │ ├── chat.js │ │ ├── server.go │ │ ├── chat.css.go │ │ └── chat.js.go │ ├── README.md │ └── LICENSE └── jankybrowser │ ├── .gitignore │ ├── run │ ├── package.json │ ├── _README.md │ ├── browser.go │ ├── index.js │ ├── browser.html │ └── browser.html.go ├── Gomfile ├── .travis.yml ├── lib ├── common │ └── common.go ├── spawn │ ├── provisioner.go │ ├── unzip.go │ ├── download.go │ ├── helper_linux.go │ ├── helper_windows.go │ ├── spawn_process.go │ └── helper_darwin.go ├── bindings │ ├── menu │ │ ├── menu_sync.go │ │ ├── menuitem.go │ │ └── menu.go │ ├── session │ │ ├── session_cookie.go │ │ ├── session_invokable.go │ │ ├── dummy_session.go │ │ └── session.go │ └── window │ │ └── window.go ├── events │ └── eventhandler.go ├── dispatcher │ └── dispatcher.go ├── connection │ └── connection.go └── commands │ └── commands.go ├── CHANGELOG.md ├── main.go ├── LICENSE ├── .gitignore ├── thrust ├── thrust_test.go └── thrust.go ├── TODO.md └── README.md /tutorials/bin/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/chat/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/jankybrowser/.gitignore: -------------------------------------------------------------------------------- 1 | jankybrowser -------------------------------------------------------------------------------- /Gomfile: -------------------------------------------------------------------------------- 1 | gom 'github.com/miketheprogrammer/go-thrust' 2 | -------------------------------------------------------------------------------- /tutorials/advanced_single_binary_distribution/provisioner/.gitignore: -------------------------------------------------------------------------------- 1 | vendor.go 2 | -------------------------------------------------------------------------------- /tutorials/advanced_single_binary_distribution/.gitignore: -------------------------------------------------------------------------------- 1 | advanced_single_binary_distribution 2 | -------------------------------------------------------------------------------- /examples/chat/client/run_client: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | 5 | exec go run client.go "$@" 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - tip 4 | before_install: 5 | - go get github.com/mattn/gom 6 | script: 7 | - $HOME/gopath/bin/gom install 8 | - $HOME/gopath/bin/gom test ./_vendor/src/github.com/miketheprogrammer/go-thrust/ 9 | - $HOME/gopath/bin/gom test ./_vendor/src/github.com/miketheprogrammer/go-thrust/lib/... 10 | -------------------------------------------------------------------------------- /examples/chat/server/run_server: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cd -- "$(dirname -- "$0")" 5 | 6 | declare -a ASSETS=(chat.html chat.js chat.css) 7 | 8 | for asset in "${ASSETS[@]}"; do 9 | var="$(printf "$asset" | tr -c 'a-zA-Z' '_')" 10 | bin2go -pkg=main -in="$asset" -out="$asset.go" "$var" 11 | done 12 | exec go run server.go "${ASSETS[@]/%/.go}" "$@" 13 | -------------------------------------------------------------------------------- /examples/jankybrowser/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cd -- "$(dirname -- "$0")" 5 | 6 | declare -a ASSETS=(browser.html) 7 | 8 | for asset in "${ASSETS[@]}"; do 9 | var="$(printf "$asset" | tr -c 'a-zA-Z' '_')" 10 | bin2go -pkg=main -in="$asset" -out="$asset.go" "$var" 11 | done 12 | echo go run browser.go "${ASSETS[@]/%/.go}" "$@" 13 | exec go run browser.go "${ASSETS[@]/%/.go}" "$@" 14 | -------------------------------------------------------------------------------- /examples/jankybrowser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jankybrowser", 3 | "version": "1.2.0", 4 | "description": "", 5 | "bin": { 6 | "jankybrowser": "jankybrowser" 7 | }, 8 | "main": "index.js", 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "async": "^0.9.0", 16 | "minimist": "^1.1.0", 17 | "node-thrust": "^0.7.5" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tutorials/provisioner/tutorial_provisioner.go: -------------------------------------------------------------------------------- 1 | package tutorial 2 | 3 | import "github.com/miketheprogrammer/go-thrust/lib/spawn" 4 | 5 | /* 6 | Default implementation of Provisioner 7 | */ 8 | type TutorialProvisioner struct{} 9 | 10 | func NewTutorialProvisioner() TutorialProvisioner { 11 | return TutorialProvisioner{} 12 | } 13 | 14 | func (tp TutorialProvisioner) Provision() error { 15 | spawn.SetBaseDirectory("") // Means use the users home directory 16 | return spawn.Bootstrap() 17 | 18 | } 19 | -------------------------------------------------------------------------------- /examples/jankybrowser/_README.md: -------------------------------------------------------------------------------- 1 | JankyBrowser 2 | ============ 3 | 4 | The only cross-platform browser that fits in a Gist! 5 | 6 | One line install. Works on Linux, MacOSX and Windows. 7 | 8 | #### Local Install 9 | ``` 10 | $> npm install http://gist.github.com/morganrallen/f07f59802884bcdcad4a/download 11 | $> node node_modules/jankybrowser 12 | ``` 13 | 14 | #### Global Install 15 | ``` 16 | $> npm install -g http://gist.github.com/morganrallen/f07f59802884bcdcad4a/download 17 | $> jankybrowser 18 | ``` 19 | 20 | JankyBrowser is based on [Thrust](https://github.com/breach/thrust) 21 | -------------------------------------------------------------------------------- /lib/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | //Global ID tracking for Commands 11 | //Could probably move this to a factory function 12 | var ActionId uint = 0 13 | var LogLevel string = "enabled" 14 | var Log *log.Logger = log.New(ioutil.Discard, "Go-Thrust:", 3) 15 | 16 | func InitLogger(level string) { 17 | LogLevel = strings.ToLower(level) 18 | switch LogLevel { 19 | case "none": 20 | Log = log.New(ioutil.Discard, "Go-Thrust:", 3) 21 | default: 22 | Log = log.New(os.Stdout, "Go-Thrust:", 3) 23 | } 24 | Log.Print("Thrust Client:: Initializing") 25 | } 26 | -------------------------------------------------------------------------------- /lib/spawn/provisioner.go: -------------------------------------------------------------------------------- 1 | package spawn 2 | 3 | type Provisioner interface { 4 | Provision() error 5 | } 6 | 7 | // Package global provisioner, use SetProvisioner to set this variable 8 | var provisioner Provisioner 9 | 10 | // Sets the current provision to use during spawn.Run() 11 | func SetProvisioner(p Provisioner) { 12 | provisioner = p 13 | } 14 | 15 | /* 16 | Default implementation of Provisioner 17 | */ 18 | type ThrustProvisioner struct{} 19 | 20 | func NewThrustProvisioner() ThrustProvisioner { 21 | return ThrustProvisioner{} 22 | } 23 | 24 | func (tp ThrustProvisioner) Provision() error { 25 | return Bootstrap() 26 | } 27 | -------------------------------------------------------------------------------- /examples/chat/client/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "github.com/miketheprogrammer/go-thrust/thrust" 8 | ) 9 | 10 | var ( 11 | host = flag.String("host", "0.0.0.0", "IP address to bind to") 12 | port = flag.Int("port", 8000, "TCP port to listen on") 13 | ) 14 | 15 | func main() { 16 | flag.Parse() 17 | thrust.InitLogger() 18 | thrust.Start() 19 | 20 | thrustWindow := thrust.NewWindow(thrust.WindowOptions{ 21 | RootUrl: fmt.Sprintf("http://127.0.0.1:%d", *port), 22 | }) 23 | thrustWindow.Show() 24 | thrustWindow.Focus() 25 | // BLOCKING - Dont run before youve excuted all commands you want first. 26 | thrust.LockThread() 27 | } 28 | -------------------------------------------------------------------------------- /examples/chat/server/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |

Welcome to the Go-Thrust Chat Demo.

11 |
12 |
13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /lib/bindings/menu/menu_sync.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import "github.com/miketheprogrammer/go-thrust/lib/commands" 4 | 5 | type ChildCommand struct { 6 | Command *commands.Command 7 | Child *Menu 8 | } 9 | 10 | type MenuSync struct { 11 | /* Channels for singaling queues */ 12 | ReadyChan chan bool 13 | DisplayedChan chan bool 14 | ChildStableChan chan uint 15 | TreeStableChan chan bool 16 | 17 | /*Queues for preserving command order and Priority*/ 18 | ReadyQueue []*commands.Command 19 | DisplayedQueue []*commands.Command 20 | // Not Exactly a Queue, more of a priority queue. Send out the first child that is stable. 21 | ChildStableQueue []*ChildCommand 22 | TreeStableQueue []*commands.Command 23 | 24 | /* Channels for control */ 25 | QuitChan chan bool 26 | } 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | NOV132014 (Version 0.3.3): 2 | Improved CPU Usage - More battery power for all. 3 | Added basic chat client/server example. 4 | 5 | NOV132014 (Version 0.3.2): 6 | 7 | Important Changes: 8 | - Upgraded Thrust Core to version 0.7.5 9 | - Implemented Session and SessionInvokable interface. 10 | - Windows Support. 11 | - Added optional auto download to spawn.Run() 12 | - Remove return values from spawn.Run() 13 | 14 | Trivial Changes: 15 | - Added several tutorials 16 | 17 | NOV102014 (Version 0.2.4): 18 | 19 | Important Changes: 20 | - Upgraded Thrust Core to version 0.7.4 21 | - Removed Menu.AttachToWindow 22 | - Added Menu.Popup 23 | - Support added for Webview. 24 | 25 | Trivial Changes: 26 | - demo.go now opens public/index.html 27 | - public/index.html now has an example of webview 28 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/codegangsta/cli" 8 | "github.com/miketheprogrammer/go-thrust/lib/spawn" 9 | ) 10 | 11 | func install(c *cli.Context) { 12 | spawn.SetBaseDirectory("") // Default to usr.homedir. 13 | tp := spawn.NewThrustProvisioner() 14 | if err := tp.Provision(); err != nil { 15 | panic(err) 16 | } 17 | fmt.Println("Thrust installed") 18 | } 19 | 20 | func main() { 21 | app := cli.NewApp() 22 | app.Name = "go-thrust" 23 | app.Usage = "Tools to developp Thrust applications in Go" 24 | 25 | app.Commands = []cli.Command{ 26 | { 27 | Name: "install", 28 | ShortName: "i", 29 | Usage: "Install Thrust", 30 | Action: func(c *cli.Context) { 31 | install(c) 32 | }, 33 | }, 34 | } 35 | 36 | app.Run(os.Args) 37 | } 38 | -------------------------------------------------------------------------------- /examples/chat/README.md: -------------------------------------------------------------------------------- 1 | Browser-to-browser chat example 2 | =============================== 3 | 4 | This example uses https://github.com/nsf/bin2go to embed assets. To 5 | make sure it's installed, run 6 | 7 | go get github.com/nsf/bin2go 8 | 9 | Other neccessary packages 10 | 11 | go get -v github.com/gorilla/websocket 12 | go get -v github.com/tv42/birpc 13 | go get -v github.com/tv42/topic 14 | 15 | and put the resulting bin2go in `PATH`. 16 | 17 | To run the example: 18 | 19 | ./run 20 | 21 | or, to select another port: 22 | 23 | ./run -port=8888 24 | 25 | Then open two browser windows to http://localhost:8000/ , change your 26 | name from *J. Doe* to something more distinct, and type messages. See 27 | how they are transmitted to the other browser window. 28 | 29 | See the browser's Javascript console for a debug log of incoming and 30 | outgoing messages. 31 | -------------------------------------------------------------------------------- /lib/bindings/session/session_cookie.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | /* 4 | Cookie 5 | source the source url 6 | name the cookie name 7 | value the cookie value 8 | domain the cookie domain 9 | path the cookie path 10 | creation the creation date 11 | expiry the expiration date 12 | last_access the last time the cookie was accessed 13 | secure is the cookie secure 14 | http_only is the cookie only valid for HTTP 15 | priority internal priority information 16 | */ 17 | type Cookie struct { 18 | Source string `json:"source"` 19 | Name string `json:"name"` 20 | Value string `json:"value"` 21 | Domain string `json:"domain"` 22 | Path string `json:"path"` 23 | Creation int64 `json:"creation"` 24 | Expiry int64 `json:"expiry"` 25 | LastAccess int64 `json:"last_access"` 26 | Secure uint `json:"secure"` 27 | HttpOnly bool `json:"http_only"` 28 | Priority uint `json:"priority"` 29 | } 30 | -------------------------------------------------------------------------------- /tutorials/basic_multiple_windows/basic_multiple_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/miketheprogrammer/go-thrust/thrust" 5 | "github.com/miketheprogrammer/go-thrust/tutorials/provisioner" 6 | ) 7 | 8 | func main() { 9 | thrust.InitLogger() 10 | // Set any Custom Provisioners before Start 11 | thrust.SetProvisioner(tutorial.NewTutorialProvisioner()) 12 | // thrust.Start() must always come before any bindings are created. 13 | thrust.Start() 14 | 15 | thrustWindow := thrust.NewWindow(thrust.WindowOptions{ 16 | RootUrl: "http://breach.cc/", 17 | }) 18 | thrustWindow.Show() 19 | thrustWindow.Maximize() 20 | thrustWindow.Focus() 21 | 22 | thrustWindow2 := thrust.NewWindow(thrust.WindowOptions{ 23 | RootUrl: "http://google.com/", 24 | }) 25 | thrustWindow2.Show() 26 | thrustWindow2.Focus() 27 | 28 | // BLOCKING - Dont run before youve excuted all commands you want first. 29 | thrust.LockThread() 30 | } 31 | -------------------------------------------------------------------------------- /tutorials/advanced_window_devtools/advanced_window_devtools.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/miketheprogrammer/go-thrust/thrust" 7 | "github.com/miketheprogrammer/go-thrust/tutorials/provisioner" 8 | ) 9 | 10 | func main() { 11 | thrust.InitLogger() 12 | // Set any Custom Provisioners before Start 13 | thrust.SetProvisioner(tutorial.NewTutorialProvisioner()) 14 | // thrust.Start() must always come before any bindings are created. 15 | thrust.Start() 16 | thrustWindow := thrust.NewWindow(thrust.WindowOptions{ 17 | RootUrl: "http://breach.cc/", 18 | }) 19 | thrustWindow.Show() 20 | thrustWindow.Maximize() 21 | thrustWindow.Focus() 22 | 23 | thrustWindow.OpenDevtools() 24 | // Lets do a window timeout 25 | go func() { 26 | <-time.After(time.Second * 10) 27 | thrustWindow.CloseDevtools() 28 | }() 29 | // BLOCKING - Dont run before youve excuted all commands you want first 30 | thrust.LockThread() 31 | } 32 | -------------------------------------------------------------------------------- /tutorials/basic_window/basic_window.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/miketheprogrammer/go-thrust/thrust" 7 | "github.com/miketheprogrammer/go-thrust/tutorials/provisioner" 8 | ) 9 | 10 | func main() { 11 | 12 | thrust.InitLogger() 13 | // Set any Custom Provisioners before Start 14 | thrust.SetProvisioner(tutorial.NewTutorialProvisioner()) 15 | // thrust.Start() must always come before any bindings are created. 16 | thrust.Start() 17 | 18 | thrustWindow := thrust.NewWindow(thrust.WindowOptions{ 19 | RootUrl: "http://breach.cc/", 20 | }) 21 | thrustWindow.Show() 22 | thrustWindow.Maximize() 23 | thrustWindow.Focus() 24 | 25 | // Lets do a window timeout 26 | go func() { 27 | <-time.After(time.Second * 5) 28 | thrustWindow.Close() 29 | thrust.Exit() 30 | }() 31 | 32 | // In lieu of something like an http server, we need to lock this thread 33 | // in order to keep it open, and keep the process running. 34 | // Dont worry we use runtime.Gosched :) 35 | thrust.LockThread() 36 | } 37 | -------------------------------------------------------------------------------- /tutorials/basic_frameless_window/basic_frameless_window.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/miketheprogrammer/go-thrust/thrust" 5 | "github.com/miketheprogrammer/go-thrust/tutorials/provisioner" 6 | ) 7 | 8 | func main() { 9 | 10 | thrust.InitLogger() 11 | // Set any Custom Provisioners before Start 12 | thrust.SetProvisioner(tutorial.NewTutorialProvisioner()) 13 | // thrust.Start() must always come before any bindings are created. 14 | thrust.Start() 15 | 16 | thrustWindow := thrust.NewWindow(thrust.WindowOptions{ 17 | RootUrl: "http://breach.cc/", 18 | HasFrame: true, 19 | }) 20 | thrustWindow.Show() 21 | thrustWindow.Focus() 22 | 23 | // Lets do a window timeout 24 | go func() { 25 | // <-time.After(time.Second * 5) 26 | // thrustWindow.Close() 27 | // thrust.Exit() 28 | }() 29 | 30 | // In lieu of something like an http server, we need to lock this thread 31 | // in order to keep it open, and keep the process running. 32 | // Dont worry we use runtime.Gosched :) 33 | thrust.LockThread() 34 | } 35 | -------------------------------------------------------------------------------- /lib/spawn/unzip.go: -------------------------------------------------------------------------------- 1 | package spawn 2 | 3 | import ( 4 | "archive/zip" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | func unzip(src, dest string) error { 12 | r, err := zip.OpenReader(src) 13 | if err != nil { 14 | return err 15 | } 16 | defer r.Close() 17 | fmt.Println("Unzipping", src, "to", dest) 18 | 19 | for _, f := range r.File { 20 | rc, err := f.Open() 21 | if err != nil { 22 | return err 23 | } 24 | defer rc.Close() 25 | 26 | filePath := filepath.Join(dest, f.Name) 27 | if f.FileInfo().IsDir() { 28 | if err := os.MkdirAll(filePath, 0775); err != nil { 29 | return err 30 | } 31 | } else { 32 | if err := os.MkdirAll(filepath.Dir(filePath), 0775); err != nil { 33 | return err 34 | } 35 | file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.FileInfo().Mode()) 36 | if err != nil { 37 | return err 38 | } 39 | defer file.Close() 40 | 41 | _, err = io.Copy(file, rc) 42 | if err != nil { 43 | return err 44 | } 45 | } 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /tutorials/basic_session/basic_session.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/miketheprogrammer/go-thrust/thrust" 5 | "github.com/miketheprogrammer/go-thrust/tutorials/provisioner" 6 | ) 7 | 8 | func main() { 9 | /* 10 | use basic setup 11 | */ 12 | thrust.InitLogger() 13 | // Set any Custom Provisioners before Start 14 | thrust.SetProvisioner(tutorial.NewTutorialProvisioner()) 15 | // thrust.Start() must always come before any bindings are created. 16 | thrust.Start() 17 | 18 | /* 19 | Start of Basic Session Tutorial area 20 | */ 21 | // arguments (incognito, useDisk) 22 | mysession := thrust.NewSession(false, false, "./cache") 23 | //mysession.SetInvokable(*session.NewDummySession()) 24 | /* 25 | Modified basic_window, where we provide, a session argument 26 | to NewWindow. 27 | */ 28 | thrustWindow := thrust.NewWindow(thrust.WindowOptions{ 29 | RootUrl: "http://breach.cc/", 30 | Session: mysession, 31 | }) 32 | thrustWindow.Show() 33 | thrustWindow.Maximize() 34 | thrustWindow.Focus() 35 | 36 | // BLOCKING - Dont run before youve excuted all commands you want first. 37 | thrust.LockThread() 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Michael Hernandez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /tutorials/advanced_single_binary_distribution/advanced_single_binary_distribution.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/miketheprogrammer/go-thrust/thrust" 5 | "github.com/miketheprogrammer/go-thrust/tutorials/advanced_single_binary_distribution/provisioner" 6 | "github.com/pkg/profile" 7 | ) 8 | 9 | func main() { 10 | 11 | thrust.InitLogger() 12 | // Set any Custom Provisioners before Start 13 | thrust.SetProvisioner(provisioner.NewSingleBinaryThrustProvisioner()) 14 | // thrust.Start() must always come before any bindings are created. 15 | thrust.Start() 16 | 17 | thrustWindow := thrust.NewWindow(thrust.WindowOptions{ 18 | RootUrl: "http://breach.cc/", 19 | HasFrame: true, 20 | }) 21 | thrustWindow.Show() 22 | thrustWindow.Focus() 23 | 24 | // Lets do a window timeout 25 | go func() { 26 | // <-time.After(time.Second * 5) 27 | // thrustWindow.Close() 28 | // thrust.Exit() 29 | }() 30 | 31 | // In lieu of something like an http server, we need to lock this thread 32 | // in order to keep it open, and keep the process running. 33 | // Dont worry we use runtime.Gosched :) 34 | defer profile.Start().Stop() 35 | thrust.LockThread() 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | _vendor 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | 27 | thrust 28 | demo 29 | 30 | ThrustShell.app 31 | thrust-v0.7.0-linux-x64 32 | 33 | vendor/linux/x64/content_shell.pak 34 | vendor/linux/x64/icudtl.dat 35 | vendor/linux/x64/libchromiumcontent.so 36 | vendor/linux/x64/libffmpegsumo.so 37 | vendor/linux/x64/thrust_shell 38 | 39 | vendor/darwin/x64/ThrustShell.app 40 | 41 | release/go-thrust/vendor/linux/x64/content_shell.pak 42 | release/go-thrust/vendor/linux/x64/icudtl.dat 43 | release/go-thrust/vendor/linux/x64/libchromiumcontent.so 44 | release/go-thrust/vendor/linux/x64/libffmpegsumo.so 45 | release/go-thrust/go-thrust/vendor/linux/x64/thrust_shell 46 | 47 | release/vendor/darwin/x64/ThrustShell.app 48 | 49 | dist/* 50 | 51 | tutorial/bin/* 52 | 53 | release 54 | 55 | vendor 56 | 57 | dummy_session 58 | 59 | cache/* 60 | 61 | *cache* -------------------------------------------------------------------------------- /examples/jankybrowser/browser.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "html/template" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/miketheprogrammer/go-thrust/thrust" 11 | ) 12 | 13 | var ( 14 | port = flag.Int("port", 8000, "TCP port to listen on") 15 | ) 16 | var html *template.Template = template.New("main") 17 | 18 | func init() { 19 | template.Must(html.New("browser.html").Parse(string(browser_html))) 20 | 21 | } 22 | 23 | func index(w http.ResponseWriter, req *http.Request) { 24 | w.Header().Set("Content-Type", "text/html") 25 | w.WriteHeader(http.StatusOK) 26 | err := html.ExecuteTemplate(w, "browser.html", nil) 27 | if err != nil { 28 | log.Printf("Template error: %v", err) 29 | } 30 | } 31 | 32 | func main() { 33 | flag.Parse() 34 | thrust.InitLogger() 35 | thrust.Start() 36 | 37 | thrustWindow := thrust.NewWindow(thrust.WindowOptions{ 38 | RootUrl: fmt.Sprintf("http://127.0.0.1:%d", *port), 39 | }) 40 | thrustWindow.Show() 41 | thrustWindow.Focus() 42 | 43 | addr := fmt.Sprintf("127.0.0.1:%d", *port) 44 | http.Handle("/", http.HandlerFunc(index)) 45 | err := http.ListenAndServe(addr, nil) 46 | 47 | if err != nil { 48 | panic(err) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/chat/server/chat.css: -------------------------------------------------------------------------------- 1 | #msg { 2 | border: 1px solid gray; 3 | padding: 0.5em; 4 | max-height: 80%; 5 | } 6 | 7 | #msg p { 8 | margin: 0.1em; 9 | } 10 | 11 | #msg span { 12 | margin-right: 0.5em; 13 | } 14 | 15 | #msg .time { 16 | font-family: monospace; 17 | font-size: 80%; 18 | color: #404040; 19 | float: right; 20 | } 21 | 22 | #msg .from { 23 | color: blue; 24 | } 25 | 26 | #msg .from:after { 27 | content: ":"; 28 | } 29 | 30 | #msg .message { 31 | } 32 | 33 | form[name="msgform"] { 34 | max-width: 95%; 35 | display: -webkit-flex; 36 | display: -moz-flex; 37 | display: -ms-flex; 38 | display: flex; 39 | -webkit-flex-flow: row nowrap; 40 | -moz-flex-flow: row nowrap; 41 | -ms-flex-flow: row nowrap; 42 | -ms-flex-direction: row; 43 | -ms-flex-wrap: nowrap; 44 | flex-flow: row nowrap; 45 | } 46 | 47 | form[name="msgform"] input { 48 | -webkit-flex: 1 auto; 49 | -moz-flex: 1 auto; 50 | -ms-flex: 1 auto; 51 | flex: 1 auto; 52 | } 53 | 54 | form[name="msgform"] input[name="name"] { 55 | max-width: 8em; 56 | } 57 | 58 | form[name="msgform"] input[type="submit"] { 59 | -webkit-flex: 0 0 auto; 60 | -moz-flex: 0 0 auto; 61 | -ms-flex: 0 0 auto; 62 | flex: 0 0 auto; 63 | } 64 | -------------------------------------------------------------------------------- /tutorials/advanced_single_binary_distribution/provisioner/single_binary_thrust_provisioner.go: -------------------------------------------------------------------------------- 1 | package provisioner 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/miketheprogrammer/go-thrust/lib/spawn" 9 | ) 10 | 11 | /* 12 | Single Binary Distribution 13 | */ 14 | type SingleBinaryThrustProvisioner struct{} 15 | 16 | /* 17 | NewSingleBinaryThrustProvisioner instantiates an SingleBinaryThrustProvisioner 18 | 19 | go:generate go-bindata -pkg $GOPACKAGE -o vendor.go vendor/ 20 | */ 21 | func NewSingleBinaryThrustProvisioner() SingleBinaryThrustProvisioner { 22 | return SingleBinaryThrustProvisioner{} 23 | } 24 | 25 | /* 26 | Provisions a thrust environment based on settings. 27 | */ 28 | func (sbtp SingleBinaryThrustProvisioner) Provision() error { 29 | err := sbtp.extractToPath(spawn.GetDownloadPath()) 30 | if err != nil { 31 | return err 32 | } 33 | return spawn.Bootstrap() 34 | } 35 | 36 | func (sbtp SingleBinaryThrustProvisioner) extractToPath(filepath string) error { 37 | data, err := Asset("vendor/thrust-v0.7.6-darwin-x64.zip") 38 | if err != nil { 39 | fmt.Println("Error accessing thrust bindata") 40 | return err 41 | } 42 | fmt.Println("No error accessing thrust bindata") 43 | fmt.Println("Writing to", filepath) 44 | err = ioutil.WriteFile(filepath, data, os.ModePerm) 45 | if nil != err { 46 | return err 47 | } 48 | return nil 49 | // return spawn.UnzipExecutable(filepath) 50 | } 51 | -------------------------------------------------------------------------------- /lib/bindings/menu/menuitem.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import . "github.com/miketheprogrammer/go-thrust/lib/common" 4 | 5 | type MenuItem struct { 6 | CommandID uint `json:"command_id,omitempty"` 7 | Label string `json:"label,omitempty"` 8 | GroupID uint `json:"group_id,omitempty"` 9 | SubMenu *Menu `json:"submenu,omitempty"` 10 | Type string `json:"type,omitempty"` 11 | Checked bool `json:"checked"` 12 | Enabled bool `json:"enabled"` 13 | Visible bool `json:"visible"` 14 | Parent *Menu `json:"-"` 15 | } 16 | 17 | func NewMenuItem() *MenuItem { 18 | return &MenuItem{} 19 | } 20 | 21 | func (mi MenuItem) IsSubMenu() bool { 22 | return mi.SubMenu != nil 23 | } 24 | 25 | func (mi MenuItem) IsCheckItem() bool { 26 | return mi.Type == "check" 27 | } 28 | 29 | func (mi MenuItem) IsRadioItem() bool { 30 | return mi.Type == "radio" 31 | } 32 | 33 | func (mi MenuItem) IsGroupID(groupID uint) bool { 34 | return mi.GroupID == groupID 35 | } 36 | 37 | func (mi MenuItem) IsCommandID(commandID uint) bool { 38 | return mi.CommandID == commandID 39 | } 40 | 41 | func (mi MenuItem) HandleEvent() { 42 | Log.Print("EventType", mi.Type) 43 | switch mi.Type { 44 | case "check": 45 | Log.Print("Toggling Checked(", mi.Checked, ")", "to", "checked(", !mi.Checked, ")") 46 | mi.Parent.SetChecked(mi.CommandID, !mi.Checked) 47 | case "radio": 48 | Log.Print("Toggling RadioChecked(", mi.Checked, ")", "to", "checked(", !mi.Checked, ")") 49 | mi.Parent.ToggleRadio(mi.CommandID, mi.GroupID, !mi.Checked) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /examples/jankybrowser/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var argv = require('minimist')(process.argv.slice(2)); 4 | 5 | var async = require('async'); 6 | 7 | var api = null; 8 | var window = null; 9 | 10 | var url = __dirname + "/test.html"; 11 | 12 | var fs = require("fs"); 13 | var http = require("http"); 14 | var port = 21024; 15 | 16 | 17 | async.series([ 18 | function(cb) { 19 | var server = http.createServer(function(req, res) { 20 | if(req.url === "/") { 21 | req.url = "/browser.html"; 22 | } 23 | 24 | req.url = __dirname + req.url; 25 | console.log("attempting to load %s", req.url); 26 | fs.createReadStream(req.url).pipe(res); 27 | }); 28 | 29 | server.listen(port, '127.0.0.1', function() { 30 | console.log("listening on %s", port); 31 | cb(null); 32 | }); 33 | }, 34 | function(cb_) { 35 | require('node-thrust')(function(err, a) { 36 | api = a; 37 | return cb_(err); 38 | }, argv.thrust_path || null); 39 | }, 40 | function(cb_) { 41 | var root = "http://127.0.0.1:" + port; 42 | 43 | if(argv.ext) root += ("/" + argv.ext); 44 | 45 | window = api.window({ 46 | root_url: root, 47 | size: { 48 | width: 1024, 49 | height: 768 50 | } 51 | }); 52 | 53 | window.on("closed", function() { 54 | process.exit(0); 55 | }); 56 | 57 | return cb_(); 58 | }, 59 | function(cb_) { 60 | window.show(cb_); 61 | } 62 | ], function(err) { 63 | if(err) { 64 | console.log('FAILED'); 65 | console.error(err); 66 | } 67 | console.log('OK'); 68 | }); 69 | -------------------------------------------------------------------------------- /tutorials/basic_webserver_app/basic_webserver_app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/miketheprogrammer/go-thrust/thrust" 8 | "github.com/miketheprogrammer/go-thrust/tutorials/provisioner" 9 | ) 10 | 11 | func handler(w http.ResponseWriter, r *http.Request) { 12 | fmt.Fprint(w, htmlIndex) 13 | } 14 | 15 | func main() { 16 | http.HandleFunc("/", handler) 17 | thrust.InitLogger() 18 | // Set any Custom Provisioners before Start 19 | thrust.SetProvisioner(tutorial.NewTutorialProvisioner()) 20 | // thrust.Start() must always come before any bindings are created. 21 | thrust.Start() 22 | 23 | mysession := thrust.NewSession(false, false, "cache") 24 | 25 | thrustWindow := thrust.NewWindow(thrust.WindowOptions{ 26 | RootUrl: "http://localhost:8080/", 27 | Session: mysession, 28 | }) 29 | thrustWindow.Show() 30 | thrustWindow.Maximize() 31 | thrustWindow.Focus() 32 | 33 | // See, we dont use thrust.LockThread() because we now have something holding the process open 34 | http.ListenAndServe(":8080", nil) 35 | } 36 | 37 | var htmlIndex string = ` 38 | 39 | 40 |

Welcome to Go-Thrust

41 | 42 | 52 | 53 | 54 | ` 55 | -------------------------------------------------------------------------------- /lib/bindings/session/session_invokable.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import "github.com/miketheprogrammer/go-thrust/lib/commands" 4 | 5 | /* 6 | Methods prefixed with Invoke are methods that can be called by ThrustCore, this differs to our 7 | standard call/reply, or event actions, since we are now the responder. 8 | */ 9 | /* 10 | SessionInvokable is an interface designed to allow you to create your own Session Store. 11 | Simple build a structure that supports these methods, and call session.SetInvokable(myInvokable) 12 | 13 | ?? What is the best and most simple value to return from these methods. Assume most of them work 14 | on the principle of single value return. Do we worry about different types or do we use a CommandResponse object 15 | If we use different types, we will just have to add them to a CommandResponse from the Caller anyway. 16 | I would say different types will keep the user from shooting himself in the foot. 17 | */ 18 | 19 | type SessionInvokable interface { 20 | InvokeCookiesLoad(args *commands.CommandResponseArguments, session *Session) (cookies []Cookie) 21 | InvokeCookiesLoadForKey(args *commands.CommandResponseArguments, session *Session) (cookies []Cookie) 22 | InvokeCookiesFlush(args *commands.CommandResponseArguments, session *Session) bool 23 | InvokeCookiesAdd(args *commands.CommandResponseArguments, session *Session) bool 24 | InvokeCookiesUpdateAccessTime(args *commands.CommandResponseArguments, session *Session) bool 25 | InvokeCookiesDelete(args *commands.CommandResponseArguments, session *Session) bool 26 | InvokeCookieForceKeepSessionState(args *commands.CommandResponseArguments, session *Session) 27 | } 28 | -------------------------------------------------------------------------------- /lib/events/eventhandler.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/miketheprogrammer/go-thrust/lib/commands" 7 | "github.com/miketheprogrammer/go-thrust/lib/dispatcher" 8 | ) 9 | 10 | /* 11 | Create a new EventHandler for a give event. 12 | */ 13 | func NewHandler(event string, fn interface{}) (ThrustEventHandler, error) { 14 | h := ThrustEventHandler{} 15 | h.Event = event 16 | h.Type = "event" 17 | err := h.SetHandleFunc(fn) 18 | dispatcher.RegisterHandler(h) 19 | return h, err 20 | } 21 | 22 | /** 23 | Begin Thrust Handler Code. 24 | **/ 25 | type Handler interface { 26 | Handle(cr commands.CommandResponse) 27 | Register() 28 | SetHandleFunc(fn interface{}) 29 | } 30 | 31 | type ThrustEventHandler struct { 32 | Type string 33 | Event string 34 | Handler interface{} 35 | } 36 | 37 | func (teh ThrustEventHandler) Handle(cr commands.CommandResponse) { 38 | if cr.Action != "event" { 39 | return 40 | } 41 | if cr.Type != teh.Event && teh.Event != "*" { 42 | return 43 | } 44 | cr.Event.Type = cr.Type 45 | if fn, ok := teh.Handler.(func(commands.CommandResponse)); ok == true { 46 | fn(cr) 47 | return 48 | } 49 | if fn, ok := teh.Handler.(func(commands.EventResult)); ok == true { 50 | fn(cr.Event) 51 | return 52 | } 53 | } 54 | 55 | func (teh *ThrustEventHandler) SetHandleFunc(fn interface{}) error { 56 | if fn, ok := fn.(func(commands.CommandResponse)); ok == true { 57 | teh.Handler = fn 58 | return nil 59 | } 60 | if fn, ok := fn.(func(commands.EventResult)); ok == true { 61 | teh.Handler = fn 62 | return nil 63 | } 64 | 65 | return errors.New("Invalid Handler Definition") 66 | } 67 | -------------------------------------------------------------------------------- /lib/spawn/download.go: -------------------------------------------------------------------------------- 1 | package spawn 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/cheggaaa/pb" 13 | 14 | . "github.com/miketheprogrammer/go-thrust/lib/common" 15 | ) 16 | 17 | func downloadFromUrl(url, filepath, version string) (fileName string, err error) { 18 | url = strings.Replace(url, "$V", version, 2) 19 | fileName = strings.Replace(filepath, "$V", version, 1) 20 | if PathNotExist(fileName) == false { 21 | fmt.Println("Thrust already exists on filesystem .... skipping") 22 | return 23 | } 24 | fmt.Println("Extract directory was", GetDownloadPath()) 25 | fmt.Println("Downloading", url, "to", fileName) 26 | 27 | output, err := os.Create(fileName) 28 | if err != nil { 29 | Log.Print("Error while creating", fileName, "-", err) 30 | return 31 | } 32 | defer output.Close() 33 | 34 | response, err := http.Get(url) 35 | if err != nil { 36 | fmt.Println("Error while downloading", url, "-", err) 37 | return 38 | } 39 | defer response.Body.Close() 40 | if response.StatusCode != http.StatusOK { 41 | err = errors.New(fmt.Sprintf("Server return non-200 status: %v", response.Status)) 42 | fmt.Println(err) 43 | return 44 | } 45 | 46 | // create bar 47 | bar := pb.New(int(response.ContentLength)).SetUnits(pb.U_BYTES).SetRefreshRate(time.Millisecond * 10) 48 | bar.ShowSpeed = true 49 | bar.Start() 50 | defer bar.Finish() 51 | 52 | // create multi writer 53 | writer := io.MultiWriter(output, bar) 54 | 55 | _, err = io.Copy(writer, response.Body) 56 | if err != nil { 57 | Log.Print("Error while downloading", url, "-", err) 58 | return 59 | } 60 | 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /lib/dispatcher/dispatcher.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "runtime" 5 | 6 | "github.com/miketheprogrammer/go-thrust/lib/commands" 7 | "github.com/miketheprogrammer/go-thrust/lib/connection" 8 | ) 9 | 10 | type HandleFunc func(commands.CommandResponse) 11 | type Handler interface { 12 | Handle(commands.CommandResponse) 13 | } 14 | 15 | var registry []interface{} 16 | 17 | /* 18 | RegisterHandler registers a HandleFunc f to receive a CommandResponse when one is sent to the system. 19 | */ 20 | func RegisterHandler(h interface{}) { 21 | registry = append(registry, h) 22 | } 23 | 24 | /* 25 | Dispatch dispatches a CommandResponse to every handler in the registry 26 | */ 27 | func Dispatch(command commands.CommandResponse) { 28 | for _, f := range registry { 29 | if fn, ok := f.(func(cr commands.CommandResponse)); ok == true { 30 | go fn(command) 31 | } 32 | if handler, ok := f.(Handler); ok == true { 33 | go handler.Handle(command) 34 | } 35 | } 36 | } 37 | 38 | /* 39 | RunLoop starts a loop that receives CommandResponses and dispatches them. 40 | This is a helper method, but you could just implement your own, if you only 41 | need this loop to be the blocking loop. 42 | For Instance, in a HTTP Server setting, you might want to run this as a 43 | goroutine and then let the servers blocking handler keep the process open. 44 | As long as there are commands in the channel, this loop will dispatch as fast 45 | as possible 46 | */ 47 | func RunLoop() { 48 | outChannels := connection.GetOutputChannels() 49 | defer connection.Clean() 50 | 51 | for { 52 | Run(outChannels) 53 | runtime.Gosched() 54 | } 55 | 56 | } 57 | 58 | func Run(outChannels *connection.Out) { 59 | response := <-outChannels.CommandResponses 60 | Dispatch(response) 61 | } 62 | -------------------------------------------------------------------------------- /thrust/thrust_test.go: -------------------------------------------------------------------------------- 1 | package thrust 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/miketheprogrammer/go-thrust/lib/commands" 7 | ) 8 | 9 | func TestNewEventHandler(t *testing.T) { 10 | handler, err := NewEventHandler("focus", func(cr commands.CommandResponse) { 11 | 12 | }) 13 | 14 | if err != nil { 15 | t.Fail() 16 | } 17 | if handler.Type != "event" { 18 | t.Fail() 19 | } 20 | if handler.Event != "focus" { 21 | t.Fail() 22 | } 23 | 24 | handler, err = NewEventHandler("focus", func(rr commands.ReplyResult) { 25 | 26 | }) 27 | 28 | if err == nil { 29 | t.Fail() 30 | } 31 | 32 | handler, err = NewEventHandler("focus", func(er commands.EventResult) { 33 | 34 | }) 35 | 36 | if err != nil { 37 | t.Fail() 38 | } 39 | if handler.Type != "event" { 40 | t.Fail() 41 | } 42 | if handler.Event != "focus" { 43 | t.Fail() 44 | } 45 | } 46 | 47 | func TestHandlerHandle(t *testing.T) { 48 | var responses int 49 | 50 | handler, err := NewEventHandler("focus", func(cr commands.CommandResponse) { 51 | t.Log("Woot") 52 | t.Log(cr) 53 | responses += 1 54 | }) 55 | 56 | if err != nil { 57 | t.Fail() 58 | } 59 | 60 | handler.Handle(commands.CommandResponse{ 61 | Action: "event", 62 | ID: 1, 63 | Type: "focus", 64 | Event: commands.EventResult{ 65 | Type: "focus", 66 | }, 67 | }) 68 | 69 | handler, err = NewEventHandler("focus", func(er commands.EventResult) { 70 | t.Log("Woot") 71 | t.Log(er) 72 | responses += 1 73 | }) 74 | 75 | if err != nil { 76 | t.Fail() 77 | } 78 | 79 | handler.Handle(commands.CommandResponse{ 80 | Action: "event", 81 | ID: 1, 82 | Type: "focus", 83 | Event: commands.EventResult{ 84 | Type: "focus", 85 | }, 86 | }) 87 | 88 | if responses != 2 { 89 | t.Fail() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/bindings/session/dummy_session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "github.com/miketheprogrammer/go-thrust/lib/commands" 5 | "github.com/miketheprogrammer/go-thrust/lib/common" 6 | ) 7 | 8 | type DummySession struct{} 9 | 10 | func NewDummySession() (dummy *DummySession) { 11 | return &DummySession{} 12 | } 13 | 14 | /* 15 | For Simplicity type declarations 16 | */ 17 | func (ds DummySession) InvokeCookiesLoad(args *commands.CommandResponseArguments, session *Session) (cookies []Cookie) { 18 | common.Log.Print("InvokeCookiesLoad") 19 | cookies = make([]Cookie, 0) 20 | 21 | return cookies 22 | } 23 | 24 | func (ds DummySession) InvokeCookiesLoadForKey(args *commands.CommandResponseArguments, session *Session) (cookies []Cookie) { 25 | common.Log.Print("InvokeCookiesLoadForKey") 26 | cookies = make([]Cookie, 0) 27 | 28 | return cookies 29 | } 30 | 31 | func (ds DummySession) InvokeCookiesFlush(args *commands.CommandResponseArguments, session *Session) bool { 32 | common.Log.Print("InvokeCookiesFlush") 33 | return false 34 | } 35 | 36 | func (ds DummySession) InvokeCookiesAdd(args *commands.CommandResponseArguments, session *Session) bool { 37 | common.Log.Print("InvokeCookiesAdd") 38 | return false 39 | } 40 | 41 | func (ds DummySession) InvokeCookiesUpdateAccessTime(args *commands.CommandResponseArguments, session *Session) bool { 42 | common.Log.Print("InvokeCookiesUpdateAccessTime") 43 | return false 44 | } 45 | 46 | func (ds DummySession) InvokeCookiesDelete(args *commands.CommandResponseArguments, session *Session) bool { 47 | common.Log.Print("InvokeCookiesDelete") 48 | return false 49 | } 50 | 51 | func (ds DummySession) InvokeCookieForceKeepSessionState(args *commands.CommandResponseArguments, session *Session) { 52 | common.Log.Print("InvokeCookieForceKeepSessionState") 53 | } 54 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | Roadmap to v1.0 : 2 | ================ 3 | Please note Complete Support *may* not be toggled until Thrust core is stable. 4 | 5 | - [ ] Add kiosk support (core.0.7.3) 6 | - [X] Queue Requests prior to Object being created to matain a synchronous looking API without the need for alot of state checks 7 | - [ ] Remove overuse of pointers in structs where modification will not take place 8 | - [ ] Add Window Support 9 | - [X] Basic Support 10 | - [X] Refactor Connection usage 11 | - [ ] Complete Support 12 | - Accessors (core.0.7.3) 13 | - Events (core.0.7.3) 14 | 15 | - [ ] Add Menu Support 16 | - [X] Basic Support 17 | - [X] Refactor Connection usage 18 | - [ ] Complete Support 19 | 20 | - [ ] Add Session Support 21 | - [X] Basic Support 22 | - [ ] Complete Support 23 | 24 | - [X] Implement Package Connection 25 | 26 | - [x] Seperate out in to packages other than main 27 | - [X] Package Window 28 | - [X] Package Menu 29 | - [X] Package Commands 30 | - [X] Package Spawn 31 | 32 | - [ ] Remove func Main as this is a Library 33 | - [ ] Should use Tests instead 34 | 35 | - [X] Refactor how Dispatching occurs 36 | - [X] We should not have to manually dispatch, there should be a registration method 37 | 38 | - [ ] Refactor menu.SetChecked to accept a nillable menu item pointer, so we dont have to waste resources finding the item in the Tree 39 | 40 | - [X] Refactor CallWhen* methods, Due to the nature of using GoRoutines, there is the chance that calls will execute out of the original order they were intended. 41 | 42 | - [X] Create a script to autodownload binaries 43 | 44 | - [X] Refactor Logging 45 | 46 | - [ ] SubMenus need order preservation 47 | 48 | - [X] vendor folders need versioning 49 | 50 | - [X] Need to fix Pathing for autoinstall and autorun. Relative paths will not work for most use cases. -------------------------------------------------------------------------------- /lib/spawn/helper_linux.go: -------------------------------------------------------------------------------- 1 | package spawn 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | /* 10 | GetThrustDirectory returns the Directory where the unzipped thrust contents are. 11 | Differs between builds based on OS 12 | */ 13 | func GetThrustDirectory() string { 14 | return filepath.Join(base, "vendor", "linux", "x64", thrustVersion) 15 | } 16 | 17 | /* 18 | GetDownloadDirectory gets the download or extract directory for Thrust 19 | */ 20 | func GetDownloadPath() string { 21 | return strings.Replace(filepath.Join(base, "$V"), "$V", thrustVersion, 1) 22 | } 23 | 24 | /* 25 | GetExecutablePath returns the path to the Thrust Executable 26 | Differs between builds based on OS 27 | */ 28 | func GetExecutablePath() string { 29 | return GetThrustDirectory() + "/thrust_shell" 30 | } 31 | 32 | /* 33 | GetDownloadURL returns the interpolatable version of the Thrust download url 34 | Differs between builds based on OS 35 | */ 36 | func GetDownloadURL() string { 37 | return "https://github.com/breach/thrust/releases/download/v$V/thrust-v$V-linux-x64.zip" 38 | } 39 | 40 | /* 41 | Bootstrap executes the default bootstrapping plan for this system and returns an error if failed 42 | */ 43 | func Bootstrap() error { 44 | if executableNotExist() == true { 45 | return prepareExecutable() 46 | } 47 | return nil 48 | } 49 | 50 | func executableNotExist() bool { 51 | _, err := os.Stat(GetExecutablePath()) 52 | return os.IsNotExist(err) 53 | } 54 | 55 | func prepareExecutable() error { 56 | path, err := downloadFromUrl(GetDownloadURL(), base+"/$V", thrustVersion) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | return UnzipExecutable(path) 62 | } 63 | 64 | func UnzipExecutable(path string) error { 65 | return unzip(path, GetThrustDirectory()) 66 | } 67 | 68 | func PathNotExist(path string) bool { 69 | _, err := os.Stat(path) 70 | return os.IsNotExist(err) 71 | } 72 | -------------------------------------------------------------------------------- /lib/spawn/helper_windows.go: -------------------------------------------------------------------------------- 1 | package spawn 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | /* 10 | GetThrustDirectory returns the Directory where the unzipped thrust contents are. 11 | Differs between builds based on OS 12 | */ 13 | func GetThrustDirectory() string { 14 | return filepath.Join(base, "vendor", "windows", "ia32", thrustVersion) 15 | } 16 | 17 | /* 18 | GetDownloadDirectory gets the download or extract directory for Thrust 19 | */ 20 | func GetDownloadPath() string { 21 | return strings.Replace(filepath.Join(base, "$V"), "$V", thrustVersion, 1) 22 | } 23 | 24 | /* 25 | GetExecutablePath returns the path to the Thrust Executable 26 | Differs between builds based on OS 27 | */ 28 | func GetExecutablePath() string { 29 | return filepath.Join(GetThrustDirectory(), "thrust_shell.exe") 30 | } 31 | 32 | /* 33 | GetDownloadURL returns the interpolatable version of the Thrust download url 34 | Differs between builds based on OS 35 | */ 36 | func GetDownloadURL() string { 37 | //https://github.com/breach/thrust/releases/download/v0.7.5/thrust-v0.7.5-win32-ia32.zip 38 | return "https://github.com/breach/thrust/releases/download/v$V/thrust-v$V-win32-ia32.zip" 39 | } 40 | 41 | func Bootstrap() error { 42 | if executableNotExist() == true { 43 | if err := prepareExecutable(); err != nil { 44 | return err 45 | } 46 | return nil 47 | } 48 | return nil 49 | } 50 | 51 | func executableNotExist() bool { 52 | _, err := os.Stat(GetExecutablePath()) 53 | return os.IsNotExist(err) 54 | } 55 | 56 | func prepareExecutable() error { 57 | path, err := downloadFromUrl(GetDownloadURL(), base+"\\$V", thrustVersion) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | return UnzipExecutable(path) 63 | } 64 | 65 | func UnzipExecutable(path string) error { 66 | return unzip(path, GetThrustDirectory()) 67 | } 68 | 69 | func PathNotExist(path string) bool { 70 | _, err := os.Stat(path) 71 | return os.IsNotExist(err) 72 | } 73 | -------------------------------------------------------------------------------- /tutorials/basic_menu/basic_menu.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/miketheprogrammer/go-thrust/thrust" 5 | "github.com/miketheprogrammer/go-thrust/tutorials/provisioner" 6 | ) 7 | 8 | func main() { 9 | thrust.InitLogger() 10 | // Set any Custom Provisioners before Start 11 | thrust.SetProvisioner(tutorial.NewTutorialProvisioner()) 12 | // thrust.Start() must always come before any bindings are created. 13 | thrust.Start() 14 | thrustWindow := thrust.NewWindow(thrust.WindowOptions{ 15 | RootUrl: "http://breach.cc/", 16 | }) 17 | thrustWindow.Show() 18 | thrustWindow.Maximize() 19 | thrustWindow.Focus() 20 | // make our top menus 21 | //applicationMenu, is essentially the menu bar 22 | applicationMenu := thrust.NewMenu() 23 | //applicationMenuRoot is the first menu, on darwin this is always named the name of your application. 24 | applicationMenuRoot := thrust.NewMenu() 25 | //File menu is our second menu 26 | fileMenu := thrust.NewMenu() 27 | 28 | // Lets build our root menu. 29 | // the first argument to AddItem is a CommandID 30 | // A CommandID is used by Thrust Core to communicate back results and events. 31 | applicationMenuRoot.AddItem(1, "About") 32 | // Now for the File menu 33 | fileMenu.AddItem(2, "Open") 34 | fileMenu.AddItem(3, "Edit") 35 | fileMenu.AddSeparator() 36 | fileMenu.AddItem(4, "Close") 37 | 38 | // Now we just need to plumb our menus together any way we want. 39 | 40 | applicationMenu.AddSubmenu(5, "Application", applicationMenuRoot) 41 | applicationMenu.AddSubmenu(6, "File", fileMenu) 42 | 43 | // Remember how in basic_browser, Window automatically self registered with the dispatcher. 44 | // unfortunately we have no such luck here. 45 | // I suppose this method could be added as an effect of SetApplicationMenu, but the effects of that need to be 46 | // Ironed out. 47 | // However, as least we only need to register the top level menu for events, all sub menus will delegate for the top menu. 48 | 49 | // Now we set it as our application Menu 50 | applicationMenu.SetApplicationMenu() 51 | // BLOCKING - Dont run before youve excuted all commands you want first. 52 | thrust.LockThread() 53 | } 54 | -------------------------------------------------------------------------------- /tutorials/basic_remote_messaging/basic_remote_messaging.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/miketheprogrammer/go-thrust/lib/bindings/window" 8 | "github.com/miketheprogrammer/go-thrust/lib/commands" 9 | "github.com/miketheprogrammer/go-thrust/thrust" 10 | "github.com/miketheprogrammer/go-thrust/tutorials/provisioner" 11 | ) 12 | 13 | func handler(w http.ResponseWriter, r *http.Request) { 14 | fmt.Fprint(w, htmlIndex) 15 | } 16 | 17 | func main() { 18 | http.HandleFunc("/", handler) 19 | thrust.InitLogger() 20 | // Set any Custom Provisioners before Start 21 | thrust.SetProvisioner(tutorial.NewTutorialProvisioner()) 22 | // thrust.Start() must always come before any bindings are created. 23 | thrust.Start() 24 | 25 | thrustWindow := thrust.NewWindow(thrust.WindowOptions{ 26 | RootUrl: "http://localhost:8080/", 27 | }) 28 | thrustWindow.Show() 29 | thrustWindow.Maximize() 30 | thrustWindow.Focus() 31 | thrustWindow.OpenDevtools() 32 | _, err := thrustWindow.HandleRemote(func(er commands.EventResult, this *window.Window) { 33 | fmt.Println("RemoteMessage Recieved:", er.Message.Payload) 34 | // Keep in mind once we have the message, lets say its json of some new type we made, 35 | // We can unmarshal it to that type. 36 | // Same goes for the other way around. 37 | this.SendRemoteMessage("boop") 38 | }) 39 | if err != nil { 40 | fmt.Println(err) 41 | thrust.Exit() 42 | } 43 | // See, we dont use thrust.LockThread() because we now have something holding the process open 44 | http.ListenAndServe(":8080", nil) 45 | } 46 | 47 | var htmlIndex string = ` 48 | 49 | 50 | 68 | 69 | 70 |

71 | 72 | 73 | ` 74 | -------------------------------------------------------------------------------- /examples/chat/LICENSE: -------------------------------------------------------------------------------- 1 | # The primary Copyright holder of most of the code is Tommi Virtanen 2 | 3 | Copyright (c) 2013 Tommi Virtanen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | # The Secondary Copyright holder is Michael Hernandez, whom has made this into a derivative work. 24 | 25 | Copyright (c) 2014 Michael Hernandez 26 | 27 | Permission is hereby granted, free of charge, to any person obtaining a copy 28 | of this software and associated documentation files (the "Software"), to deal 29 | in the Software without restriction, including without limitation the rights 30 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 31 | copies of the Software, and to permit persons to whom the Software is 32 | furnished to do so, subject to the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be included in 35 | all copies or substantial portions of the Software. 36 | 37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 38 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 39 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 40 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 41 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 42 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 43 | THE SOFTWARE. -------------------------------------------------------------------------------- /tutorials/basic_menu_events/basic_menu_events.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/miketheprogrammer/go-thrust/lib/bindings/menu" 7 | "github.com/miketheprogrammer/go-thrust/lib/commands" 8 | "github.com/miketheprogrammer/go-thrust/thrust" 9 | "github.com/miketheprogrammer/go-thrust/tutorials/provisioner" 10 | ) 11 | 12 | func main() { 13 | thrust.InitLogger() 14 | // Set any Custom Provisioners before Start 15 | thrust.SetProvisioner(tutorial.NewTutorialProvisioner()) 16 | // thrust.Start() must always come before any bindings are created. 17 | thrust.Start() 18 | thrustWindow := thrust.NewWindow(thrust.WindowOptions{ 19 | RootUrl: "http://breach.cc/", 20 | }) 21 | thrustWindow.Show() 22 | thrustWindow.Maximize() 23 | thrustWindow.Focus() 24 | 25 | // make our top menus 26 | //applicationMenu, is essentially the menu bar 27 | applicationMenu := thrust.NewMenu() 28 | //applicationMenuRoot is the first menu, on darwin this is always named the name of your application. 29 | applicationMenuRoot := thrust.NewMenu() 30 | //File menu is our second menu 31 | fileMenu := thrust.NewMenu() 32 | 33 | // Lets build our root menu. 34 | // the first argument to AddItem is a CommandID 35 | // A CommandID is used by Thrust Core to communicate back results and events. 36 | applicationMenuRoot.AddItem(1, "About") 37 | applicationMenuRoot.RegisterEventHandlerByCommandID(1, 38 | func(reply commands.CommandResponse, item *menu.MenuItem) { 39 | fmt.Println("About Handled") 40 | }) 41 | // Now for the File menu 42 | fileMenu.AddItem(2, "Open") 43 | fileMenu.RegisterEventHandlerByCommandID(2, 44 | func(reply commands.CommandResponse, item *menu.MenuItem) { 45 | fmt.Println("Open Handled") 46 | }) 47 | fileMenu.AddItem(3, "Edit") 48 | fileMenu.AddSeparator() 49 | fileMenu.AddItem(4, "Close") 50 | fileMenu.RegisterEventHandlerByCommandID(4, 51 | func(reply commands.CommandResponse, item *menu.MenuItem) { 52 | fmt.Println("Close Event Handled") 53 | thrust.Exit() 54 | }) 55 | // Now we just need to plumb our menus together any way we want. 56 | 57 | applicationMenu.AddSubmenu(5, "Application", applicationMenuRoot) 58 | applicationMenu.AddSubmenu(6, "File", fileMenu) 59 | 60 | // Remember how in basic_browser, Window automatically self registered with the dispatcher. 61 | // unfortunately we have no such luck here. 62 | // I suppose this method could be added as an effect of SetApplicationMenu, but the effects of that need to be 63 | // Ironed out. 64 | // However, as least we only need to register the top level menu for events, all sub menus will delegate for the top menu. 65 | 66 | // Now we set it as our application Menu 67 | applicationMenu.SetApplicationMenu() 68 | // BLOCKING - Dont run before youve excuted all commands you want first. 69 | thrust.LockThread() 70 | } 71 | -------------------------------------------------------------------------------- /thrust/thrust.go: -------------------------------------------------------------------------------- 1 | package thrust 2 | 3 | import ( 4 | "runtime" 5 | "time" 6 | 7 | "github.com/miketheprogrammer/go-thrust/lib/bindings/menu" 8 | "github.com/miketheprogrammer/go-thrust/lib/bindings/session" 9 | "github.com/miketheprogrammer/go-thrust/lib/bindings/window" 10 | "github.com/miketheprogrammer/go-thrust/lib/common" 11 | "github.com/miketheprogrammer/go-thrust/lib/connection" 12 | "github.com/miketheprogrammer/go-thrust/lib/dispatcher" 13 | "github.com/miketheprogrammer/go-thrust/lib/events" 14 | "github.com/miketheprogrammer/go-thrust/lib/spawn" 15 | ) 16 | 17 | /* 18 | Begin Generic Access and Binding Management Section. 19 | */ 20 | 21 | /* 22 | Bindings 23 | */ 24 | 25 | type WindowOptions window.Options 26 | 27 | /* NewWindow creates a new Window Binding */ 28 | func NewWindow(options WindowOptions) *window.Window { 29 | return window.NewWindow(window.Options(options)) 30 | } 31 | 32 | /* NewSession creates a new Session Binding */ 33 | func NewSession(incognito, overrideDefaultSession bool, path string) *session.Session { 34 | return session.NewSession(incognito, overrideDefaultSession, path) 35 | } 36 | 37 | /* NewMenu creates a new Menu Binding */ 38 | func NewMenu() *menu.Menu { 39 | return menu.NewMenu() 40 | } 41 | 42 | /* 43 | Start spawns the thrust core executable, and begins the dispatcher loop in a go routine 44 | */ 45 | func Start() { 46 | spawn.Run() 47 | go dispatcher.RunLoop() 48 | } 49 | 50 | /* 51 | SetProvisioner overrides the default Provisioner, the default provisioner downloads 52 | Thrust-Core if Thrust-Core is not found. 53 | It also does some other nifty things to configure your install (on darwin) for the ApplicationName you choose. 54 | */ 55 | func SetProvisioner(p spawn.Provisioner) { 56 | spawn.SetProvisioner(p) 57 | } 58 | 59 | /* 60 | Use LockThread on the main thread in lieue of a webserver or some other service that holds the thread 61 | This is primarily used when Thrust and just Thrust is what you are using, in that case lock the thread. 62 | Otherwise, why dont you start an http server, and expose some websockets. 63 | */ 64 | func LockThread() { 65 | for { 66 | time.Sleep(1 * time.Second) 67 | runtime.Gosched() 68 | } 69 | } 70 | 71 | /* 72 | Initialize and Enable the internal *log.Logger. 73 | */ 74 | func InitLogger() { 75 | common.InitLogger("") 76 | } 77 | 78 | /* 79 | Disable the internal *log.Logger instance 80 | */ 81 | func DisableLogger() { 82 | common.InitLogger("none") 83 | } 84 | 85 | /* 86 | ALWAYS use this method instead of os.Exit() 87 | This method will handle destroying the child process, and exiting as cleanly as possible. 88 | */ 89 | func Exit() { 90 | connection.CleanExit() 91 | } 92 | 93 | /* 94 | Sets the Application Name 95 | */ 96 | func SetApplicationName(name string) { 97 | spawn.ApplicationName = name 98 | } 99 | 100 | /* 101 | Create a new EventHandler for a give event. 102 | */ 103 | func NewEventHandler(event string, fn interface{}) (events.ThrustEventHandler, error) { 104 | return events.NewHandler(event, fn) 105 | } 106 | -------------------------------------------------------------------------------- /lib/spawn/spawn_process.go: -------------------------------------------------------------------------------- 1 | package spawn 2 | 3 | /* 4 | Package spawn implements methods and interfaces used in downloading and spawning the underlying thrust core binary. 5 | */ 6 | import ( 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "os/user" 11 | "path/filepath" 12 | "runtime" 13 | 14 | . "github.com/miketheprogrammer/go-thrust/lib/common" 15 | "github.com/miketheprogrammer/go-thrust/lib/connection" 16 | ) 17 | 18 | const ( 19 | thrustVersion = "0.7.6" 20 | ) 21 | 22 | var ( 23 | // ApplicationName only functionally applies to OSX builds, otherwise it is only cosmetic 24 | ApplicationName = "Go Thrust" 25 | // base directory for storing the executable 26 | base = "" 27 | ) 28 | 29 | /* 30 | SetBaseDirectory sets the base directory used in the other helper methods 31 | */ 32 | func SetBaseDirectory(dir string) error { 33 | if len(dir) == 0 { 34 | usr, err := user.Current() 35 | if err != nil { 36 | fmt.Println(err) 37 | } 38 | dir = usr.HomeDir 39 | } 40 | dir, err := filepath.Abs(dir) 41 | if err != nil { 42 | fmt.Println("Could not calculate absolute path", err) 43 | return err 44 | } 45 | base = dir 46 | 47 | return nil 48 | } 49 | 50 | /* 51 | The SpawnThrustCore method is a bootstrap and run method. 52 | It will try to detect an installation of thrust, if it cannot find it 53 | it will download the version of Thrust detailed in the "common" package. 54 | Once downloaded, it will launch a process. 55 | Go-Thrust and all *-Thrust packages communicate with Thrust Core via Stdin/Stdout. 56 | using -log=debug as a command switch will give you the most information about what is going on. -log=info will give you notices that stuff is happening. 57 | Any log level higher than that will output nothing. 58 | */ 59 | func Run() { 60 | if Log == nil { 61 | InitLogger("debug") 62 | } 63 | if base == "" { 64 | SetBaseDirectory("") // Default to usr.homedir. 65 | } 66 | 67 | thrustExecPath := GetExecutablePath() 68 | if len(thrustExecPath) > 0 { 69 | 70 | if provisioner == nil { 71 | SetProvisioner(NewThrustProvisioner()) 72 | } 73 | if err := provisioner.Provision(); err != nil { 74 | panic(err) 75 | } 76 | 77 | thrustExecPath = GetExecutablePath() 78 | 79 | Log.Print("Attempting to start Thrust Core") 80 | Log.Print("CMD:", thrustExecPath) 81 | cmd := exec.Command(thrustExecPath) 82 | cmdIn, e1 := cmd.StdinPipe() 83 | cmdOut, e2 := cmd.StdoutPipe() 84 | 85 | if e1 != nil { 86 | fmt.Println(e1) 87 | os.Exit(2) // need to improve exit codes 88 | } 89 | 90 | if e2 != nil { 91 | fmt.Println(e2) 92 | os.Exit(2) 93 | } 94 | 95 | if LogLevel != "none" { 96 | cmd.Stderr = os.Stdout 97 | } 98 | 99 | if err := cmd.Start(); err != nil { 100 | Log.Panic("Thrust Core not started.") 101 | } 102 | 103 | Log.Print("Thrust Core started.") 104 | 105 | // Setup our Connection. 106 | connection.Stdout = cmdOut 107 | connection.Stdin = cmdIn 108 | connection.ExecCommand = cmd 109 | connection.InitializeThreads() 110 | return 111 | } else { 112 | fmt.Println("===============WARNING================") 113 | fmt.Println("Current operating system not supported", runtime.GOOS) 114 | fmt.Println("===============END====================") 115 | } 116 | return 117 | } 118 | -------------------------------------------------------------------------------- /tutorials/advanced_session/advanced_session.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/miketheprogrammer/go-thrust/lib/bindings/session" 5 | "github.com/miketheprogrammer/go-thrust/lib/commands" 6 | "github.com/miketheprogrammer/go-thrust/lib/common" 7 | "github.com/miketheprogrammer/go-thrust/thrust" 8 | "github.com/miketheprogrammer/go-thrust/tutorials/provisioner" 9 | ) 10 | 11 | func main() { 12 | /* 13 | use basic setup 14 | */ 15 | thrust.InitLogger() 16 | // Set any Custom Provisioners before Start 17 | thrust.SetProvisioner(tutorial.NewTutorialProvisioner()) 18 | // thrust.Start() must always come before any bindings are created. 19 | thrust.Start() 20 | 21 | /* 22 | Start of Advanced Session Tutorial. 23 | We are going to set the Override value to true as ooposed to the false 24 | used in basic_session. This will cause ThrustCore to try to invoke methods from us. 25 | 26 | Look down below func main to find our session class. 27 | */ 28 | mysession := thrust.NewSession(false, true, "session_cache") 29 | 30 | mysession.SetInvokable(NewSimpleSession()) 31 | /* 32 | Modified basic_window, where we provide, a session argument 33 | to NewWindow. 34 | */ 35 | thrustWindow := thrust.NewWindow(thrust.WindowOptions{ 36 | RootUrl: "http://breach.cc/", 37 | Session: mysession, 38 | }) 39 | thrustWindow.Show() 40 | thrustWindow.Maximize() 41 | thrustWindow.Focus() 42 | 43 | // In lieu of something like an http server, we need to lock this thread 44 | // in order to keep it open, and keep the process running. 45 | // Dont worry we use runtime.Gosched :) 46 | thrust.LockThread() 47 | } 48 | 49 | // SimpleSession must subscribe to the contract set forth by SessionInvokable interface 50 | 51 | type SimpleSession struct { 52 | cookieStore map[string][]session.Cookie 53 | } 54 | 55 | func NewSimpleSession() (ss SimpleSession) { 56 | return ss 57 | } 58 | 59 | /* 60 | For Simplicity type declarations 61 | */ 62 | func (ss SimpleSession) InvokeCookiesLoad(args *commands.CommandResponseArguments, s *session.Session) (cookies []session.Cookie) { 63 | common.Log.Print("InvokeCookiesLoad") 64 | cookies = make([]session.Cookie, 0) 65 | 66 | return cookies 67 | } 68 | 69 | func (ss SimpleSession) InvokeCookiesLoadForKey(args *commands.CommandResponseArguments, s *session.Session) (cookies []session.Cookie) { 70 | common.Log.Print("InvokeCookiesLoadForKey") 71 | cookies = make([]session.Cookie, 0) 72 | 73 | return cookies 74 | } 75 | 76 | func (ss SimpleSession) InvokeCookiesFlush(args *commands.CommandResponseArguments, s *session.Session) bool { 77 | common.Log.Print("InvokeCookiesFlush") 78 | return false 79 | } 80 | 81 | func (ss SimpleSession) InvokeCookiesAdd(args *commands.CommandResponseArguments, s *session.Session) bool { 82 | common.Log.Print("InvokeCookiesAdd") 83 | return false 84 | } 85 | 86 | func (ss SimpleSession) InvokeCookiesUpdateAccessTime(args *commands.CommandResponseArguments, s *session.Session) bool { 87 | common.Log.Print("InvokeCookiesUpdateAccessTime") 88 | return false 89 | } 90 | 91 | func (ss SimpleSession) InvokeCookiesDelete(args *commands.CommandResponseArguments, s *session.Session) bool { 92 | common.Log.Print("InvokeCookiesDelete") 93 | return false 94 | } 95 | 96 | func (ss SimpleSession) InvokeCookieForceKeepSessionState(args *commands.CommandResponseArguments, s *session.Session) { 97 | common.Log.Print("InvokeCookieForceKeepSessionState") 98 | } 99 | -------------------------------------------------------------------------------- /tutorials/basic_window_events/basic_window_events.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/miketheprogrammer/go-thrust/lib/commands" 9 | "github.com/miketheprogrammer/go-thrust/lib/connection" 10 | "github.com/miketheprogrammer/go-thrust/thrust" 11 | "github.com/miketheprogrammer/go-thrust/tutorials/provisioner" 12 | ) 13 | 14 | /* 15 | This tutorial teaches how to handle global events. 16 | You can even use this to track menu/window/session etc. events, 17 | if you store your bindings somewhere and track the ids. 18 | Check package thrust for the acceptable handler definitions. 19 | */ 20 | func main() { 21 | thrust.InitLogger() 22 | // Set any Custom Provisioners before Start 23 | thrust.SetProvisioner(tutorial.NewTutorialProvisioner()) 24 | // thrust.Start() must always come before any bindings are created. 25 | thrust.Start() 26 | 27 | thrustWindow := thrust.NewWindow(thrust.WindowOptions{ 28 | RootUrl: "http://breach.cc/", 29 | }) 30 | thrustWindow.Show() 31 | 32 | /* 33 | Here we use an EventResult Callback, it provides us a little less data than a ComandResponse cb 34 | however, its good for if we know we dont need to worry about targetids. Like understanding which window 35 | was focuses, just that there is a window, that was focused. 36 | */ 37 | onfocus, err := thrust.NewEventHandler("focus", func(er commands.EventResult) { 38 | fmt.Println("Focus Event Occured") 39 | }) 40 | fmt.Println(onfocus) 41 | if err != nil { 42 | fmt.Println(err) 43 | connection.CleanExit() 44 | } 45 | 46 | /* 47 | Note blur does not seem to be triggered by unfocus 48 | only by actually clicking the window, and then clicking somewhere else. 49 | */ 50 | onblur, err := thrust.NewEventHandler("blur", func(er commands.EventResult) { 51 | fmt.Println("Blur Event Occured") 52 | }) 53 | fmt.Println(onblur) 54 | if err != nil { 55 | fmt.Println(err) 56 | connection.CleanExit() 57 | } 58 | 59 | /* 60 | Here we use a CommandResponse callback just because we can, it provides us more data 61 | than the EventResult callback 62 | */ 63 | onclose, err := thrust.NewEventHandler("closed", func(cr commands.EventResult) { 64 | fmt.Println("Close Event Occured") 65 | }) 66 | fmt.Println(onclose) 67 | if err != nil { 68 | fmt.Println(err) 69 | connection.CleanExit() 70 | } 71 | 72 | /* 73 | Lets say we just want to log all events 74 | */ 75 | onanything, err := thrust.NewEventHandler("*", func(cr commands.CommandResponse) { 76 | cr_marshaled, err := json.Marshal(cr) 77 | if err != nil { 78 | fmt.Println(err) 79 | } else { 80 | fmt.Println(fmt.Sprintf("Event(%s) - Signaled by Command (%s)", cr.Type, cr_marshaled)) 81 | } 82 | }) 83 | 84 | fmt.Println(onanything) 85 | if err != nil { 86 | fmt.Println(err) 87 | connection.CleanExit() 88 | } 89 | 90 | time.AfterFunc(time.Second, func() { 91 | thrustWindow.Focus() 92 | }) 93 | 94 | time.AfterFunc(time.Second*2, func() { 95 | thrustWindow.UnFocus() 96 | }) 97 | 98 | time.AfterFunc(time.Second*3, func() { 99 | thrustWindow.Close() 100 | }) 101 | 102 | // Lets do a window timeout 103 | go func() { 104 | <-time.After(time.Second * 5) 105 | connection.CleanExit() 106 | }() 107 | // BLOCKING - Dont run before youve excuted all commands you want first 108 | thrust.LockThread() 109 | 110 | } 111 | -------------------------------------------------------------------------------- /examples/chat/server/chat.html.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var chat_html = []byte{ 4 | 0x3c, 0x21, 0x44, 0x4f, 0x43, 0x54, 0x59, 0x50, 0x45, 0x20, 0x68, 0x74, 5 | 0x6d, 0x6c, 0x3e, 0x0a, 0x3c, 0x68, 0x74, 0x6d, 0x6c, 0x3e, 0x0a, 0x20, 6 | 0x20, 0x3c, 0x68, 0x65, 0x61, 0x64, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 7 | 0x3c, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3e, 0x7b, 0x7b, 0x74, 0x65, 8 | 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x20, 0x22, 0x63, 0x68, 0x61, 0x74, 9 | 0x2e, 0x6a, 0x73, 0x22, 0x7d, 0x7d, 0x3c, 0x2f, 0x73, 0x63, 0x72, 0x69, 10 | 0x70, 0x74, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73, 0x74, 0x79, 11 | 0x6c, 0x65, 0x3e, 0x7b, 0x7b, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 12 | 0x65, 0x20, 0x22, 0x63, 0x68, 0x61, 0x74, 0x2e, 0x63, 0x73, 0x73, 0x22, 13 | 0x7d, 0x7d, 0x3c, 0x2f, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3e, 0x0a, 0x20, 14 | 0x20, 0x3c, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x3e, 0x0a, 0x20, 0x20, 0x3c, 15 | 0x62, 0x6f, 0x64, 0x79, 0x20, 0x6f, 0x6e, 0x4c, 0x6f, 0x61, 0x64, 0x3d, 16 | 0x22, 0x69, 0x6e, 0x69, 0x74, 0x28, 0x29, 0x3b, 0x22, 0x20, 0x68, 0x65, 17 | 0x69, 0x67, 0x68, 0x74, 0x3d, 0x22, 0x31, 0x30, 0x30, 0x25, 0x22, 0x3e, 18 | 0x0a, 0x20, 0x20, 0x3c, 0x69, 0x6d, 0x67, 0x20, 0x68, 0x65, 0x69, 0x67, 19 | 0x68, 0x74, 0x3d, 0x22, 0x36, 0x30, 0x70, 0x78, 0x22, 0x20, 0x77, 0x69, 20 | 0x64, 0x74, 0x68, 0x3d, 0x22, 0x32, 0x30, 0x30, 0x70, 0x78, 0x22, 0x20, 21 | 0x73, 0x72, 0x63, 0x3d, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 22 | 0x69, 0x2e, 0x69, 0x6d, 0x67, 0x75, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 23 | 0x44, 0x77, 0x46, 0x4b, 0x49, 0x30, 0x4a, 0x2e, 0x70, 0x6e, 0x67, 0x22, 24 | 0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x3c, 0x62, 0x72, 0x2f, 0x3e, 0x0a, 0x20, 25 | 0x20, 0x3c, 0x68, 0x33, 0x3e, 0x57, 0x65, 0x6c, 0x63, 0x6f, 0x6d, 0x65, 26 | 0x20, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x47, 0x6f, 0x2d, 0x54, 27 | 0x68, 0x72, 0x75, 0x73, 0x74, 0x20, 0x43, 0x68, 0x61, 0x74, 0x20, 0x44, 28 | 0x65, 0x6d, 0x6f, 0x2e, 0x3c, 0x2f, 0x68, 0x33, 0x3e, 0x0a, 0x20, 0x20, 29 | 0x20, 0x20, 0x3c, 0x64, 0x69, 0x76, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x6d, 30 | 0x73, 0x67, 0x22, 0x3e, 0x3c, 0x2f, 0x64, 0x69, 0x76, 0x3e, 0x0a, 0x20, 31 | 0x20, 0x20, 0x20, 0x3c, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x6e, 0x61, 0x6d, 32 | 0x65, 0x3d, 0x22, 0x6d, 0x73, 0x67, 0x66, 0x6f, 0x72, 0x6d, 0x22, 0x20, 33 | 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x3d, 0x22, 0x23, 0x22, 0x20, 0x6f, 34 | 0x6e, 0x73, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x3d, 0x22, 0x72, 0x65, 0x74, 35 | 0x75, 0x72, 0x6e, 0x20, 0x73, 0x65, 0x6e, 0x64, 0x28, 0x29, 0x3b, 0x22, 36 | 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x69, 0x6e, 0x70, 37 | 0x75, 0x74, 0x20, 0x74, 0x79, 0x70, 0x65, 0x3d, 0x22, 0x74, 0x65, 0x78, 38 | 0x74, 0x22, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x3d, 0x22, 0x6e, 0x61, 0x6d, 39 | 0x65, 0x22, 0x20, 0x73, 0x69, 0x7a, 0x65, 0x3d, 0x22, 0x30, 0x22, 0x20, 40 | 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3d, 0x22, 0x4a, 0x2e, 0x20, 0x44, 0x6f, 41 | 0x65, 0x22, 0x20, 0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 42 | 0x3c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, 0x74, 0x79, 0x70, 0x65, 0x3d, 43 | 0x22, 0x74, 0x65, 0x78, 0x74, 0x22, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x3d, 44 | 0x22, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x20, 0x73, 0x69, 45 | 0x7a, 0x65, 0x3d, 0x22, 0x30, 0x22, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 46 | 0x3d, 0x22, 0x22, 0x20, 0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 47 | 0x20, 0x3c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, 0x74, 0x79, 0x70, 0x65, 48 | 0x3d, 0x22, 0x73, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x22, 0x20, 0x76, 0x61, 49 | 0x6c, 0x75, 0x65, 0x3d, 0x22, 0x73, 0x65, 0x6e, 0x64, 0x22, 0x20, 0x2f, 50 | 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 51 | 0x3e, 0x0a, 0x20, 0x20, 0x3c, 0x2f, 0x62, 0x6f, 0x64, 0x79, 0x3e, 0x0a, 52 | 0x3c, 0x2f, 0x68, 0x74, 0x6d, 0x6c, 0x3e, 0x0a, 53 | } 54 | -------------------------------------------------------------------------------- /lib/connection/connection.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "os/signal" 11 | "strings" 12 | "syscall" 13 | 14 | "github.com/miketheprogrammer/go-thrust/lib/commands" 15 | . "github.com/miketheprogrammer/go-thrust/lib/common" 16 | ) 17 | 18 | const ( 19 | SOCKET_BOUNDARY = "--(Foo)++__THRUST_SHELL_BOUNDARY__++(Bar)--" 20 | ) 21 | 22 | // Single Connection 23 | //var conn net.Conn 24 | var Stdin io.WriteCloser 25 | var Stdout io.ReadCloser 26 | var ExecCommand *exec.Cmd 27 | 28 | type In struct { 29 | Commands chan *commands.Command 30 | CommandResponses chan *commands.CommandResponse 31 | Quit chan int 32 | } 33 | type Out struct { 34 | CommandResponses chan commands.CommandResponse 35 | Errors chan error 36 | } 37 | 38 | var in In 39 | var out Out 40 | 41 | /* 42 | Initializes threads with Channel Structs 43 | Opens Connection 44 | */ 45 | func InitializeThreads() { 46 | //c, err := net.Dial(proto, address) 47 | //conn = c 48 | 49 | in = In{ 50 | Commands: make(chan *commands.Command), 51 | CommandResponses: make(chan *commands.CommandResponse), 52 | Quit: make(chan int), 53 | } 54 | 55 | out = Out{ 56 | CommandResponses: make(chan commands.CommandResponse), 57 | Errors: make(chan error), 58 | } 59 | 60 | go Reader(&out, &in) 61 | go Writer(&out, &in) 62 | 63 | go func() { 64 | fmt.Println("Registering signals") 65 | p := []os.Signal{syscall.SIGINT} 66 | c := make(chan os.Signal, len(p)) 67 | signal.Notify(c, p...) 68 | 69 | for s := range c { 70 | fmt.Println("Getting signal ", s) 71 | if s == os.Interrupt || s == os.Kill { 72 | fmt.Println("Finishing clean up quiting") 73 | CleanExit() 74 | return 75 | } 76 | } 77 | }() 78 | return 79 | } 80 | 81 | func GetOutputChannels() *Out { 82 | return &out 83 | } 84 | 85 | func GetInputChannels() *In { 86 | return &in 87 | } 88 | 89 | func GetCommunicationChannels() (*Out, *In) { 90 | return GetOutputChannels(), GetInputChannels() 91 | } 92 | 93 | func Clean() { 94 | Log.Print("Killing Thrust Core") 95 | if err := ExecCommand.Process.Kill(); err != nil { 96 | Log.Print("failed to kill: ", err) 97 | } 98 | } 99 | 100 | func CleanExit() { 101 | Clean() 102 | os.Exit(0) 103 | } 104 | 105 | func Reader(out *Out, in *In) { 106 | 107 | reader := bufio.NewReader(Stdout) 108 | defer Stdin.Close() 109 | for { 110 | line, err := reader.ReadString(byte('\n')) 111 | if err != nil { 112 | fmt.Println(err) 113 | // For now lets just force cleanup and exit 114 | CleanExit() 115 | return 116 | } 117 | 118 | //Log.Print("SOCKET::Line", line) 119 | if !strings.Contains(line, SOCKET_BOUNDARY) { 120 | response := commands.CommandResponse{} 121 | json.Unmarshal([]byte(line), &response) 122 | out.CommandResponses <- response 123 | } 124 | } 125 | } 126 | 127 | func Writer(out *Out, in *In) { 128 | for { 129 | select { 130 | case response := <-in.CommandResponses: 131 | cmd, _ := json.Marshal(response) 132 | //Log.Print("Writing RESPONSE", string(cmd), "\n", SOCKET_BOUNDARY) 133 | 134 | Stdin.Write(cmd) 135 | Stdin.Write([]byte("\n")) 136 | Stdin.Write([]byte(SOCKET_BOUNDARY)) 137 | Stdin.Write([]byte("\n")) 138 | case command := <-in.Commands: 139 | ActionId += 1 140 | command.ID = ActionId 141 | 142 | //fmt.Println(command) 143 | cmd, _ := json.Marshal(command) 144 | //Log.Print("Writing", string(cmd), "\n", SOCKET_BOUNDARY) 145 | 146 | Stdin.Write(cmd) 147 | Stdin.Write([]byte("\n")) 148 | Stdin.Write([]byte(SOCKET_BOUNDARY)) 149 | Stdin.Write([]byte("\n")) 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /examples/chat/server/chat.js: -------------------------------------------------------------------------------- 1 | var backoff_attempts = 0; 2 | function backoff_ok() { 3 | backoff_attempts = 0; 4 | } 5 | function backoff_compute() { 6 | var SECOND = 1000; 7 | var backoff_initial = 0.5 * SECOND; 8 | var backoff_max = 60 * SECOND; 9 | var backoff_multiplier = 1.5; 10 | var backoff_randomization = 0.5; 11 | 12 | var retry = backoff_initial * Math.pow(backoff_multiplier, backoff_attempts); 13 | var limited = Math.min(retry, backoff_max); 14 | 15 | // randomness is backoff_randomization wide centered around 1.0 16 | var rand = Math.random() * backoff_randomization; 17 | rand = rand + 1 - backoff_randomization/2; 18 | var delay = limited * rand; 19 | console.log({retry: retry, limited: limited, rand: rand, delay: delay}); 20 | 21 | backoff_attempts += 1; 22 | var pretty = delay/1000; 23 | console.log("Retrying websocket in " + pretty.toFixed(2) + "s..."); 24 | return delay; 25 | } 26 | 27 | var ws; 28 | function connect() { 29 | var div = document.getElementById("msg"); 30 | var line = document.createElement("p"); 31 | line.appendChild(document.createTextNode("Connecting...")); 32 | div.appendChild(line); 33 | 34 | if (ws != null) { 35 | ws.close(); 36 | ws = null; 37 | } 38 | var scheme = "ws"; 39 | if (window.location.protocol == "https") { 40 | scheme = "wss"; 41 | } 42 | ws = new WebSocket(scheme + "://" + window.location.host + "/sock"); 43 | // Initializing these event handlers is not racy as long as we do 44 | // it before we let the execution here return to the event loop. 45 | // https://www.w3.org/Bugs/Public/show_bug.cgi?id=12510 46 | ws.onopen = function () { 47 | backoff_ok(); 48 | var line = document.createElement("p"); 49 | line.appendChild(document.createTextNode("Opened.")); 50 | div.appendChild(line); 51 | }; 52 | ws.onmessage = function (e) { 53 | // TODO can throw SyntaxError 54 | var rpc = JSON.parse(e.data); 55 | console.log("rpc in:", rpc); 56 | 57 | if (rpc.fn === undefined 58 | || rpc.fn == "") { 59 | // responses; we're currently ignoring errors 60 | return; 61 | } 62 | 63 | // kludge dispatch for now 64 | if (rpc.fn != "Chat.Message") { 65 | rpc.error = {rpc: "No such function."}; 66 | delete rpc.fn; 67 | delete rpc.args; 68 | delete rpc.result; 69 | ws.send(rpc) 70 | return; 71 | } 72 | 73 | // TODO catch exceptions 74 | 75 | var line = document.createElement("p"); 76 | 77 | var time = document.createElement("span"); 78 | time.setAttribute("class", "time"); 79 | time.appendChild(document.createTextNode(rpc.args.time)); 80 | line.appendChild(time); 81 | 82 | var from = document.createElement("span"); 83 | from.setAttribute("class", "from"); 84 | from.appendChild(document.createTextNode(rpc.args.from)); 85 | line.appendChild(from); 86 | 87 | var text = document.createElement("span"); 88 | text.setAttribute("class", "message"); 89 | text.appendChild(document.createTextNode(rpc.args.message)); 90 | line.appendChild(text); 91 | 92 | div.appendChild(line); 93 | 94 | delete rpc.fn; 95 | delete rpc.args; 96 | rpc.result = {}; 97 | delete rpc.error; 98 | console.log("rpc out:", rpc); 99 | ws.send(JSON.stringify(rpc)); 100 | }; 101 | ws.onclose = function (e) { 102 | var line = document.createElement("p"); 103 | line.appendChild(document.createTextNode("Closed.")); 104 | div.appendChild(line); 105 | 106 | // If this websocket is still the main one, reconnect; 107 | // otherwise, let it die. This allows easy fiddling from js 108 | // console with just ``connect()``. Ordering of the status 109 | // messages gets weird, though. 110 | if (this === ws) { 111 | var delay = backoff_compute(); 112 | setTimeout(connect, delay); 113 | } 114 | }; 115 | } 116 | function init() { 117 | document.msgform.message.focus(); 118 | connect(); 119 | }; 120 | 121 | function send() { 122 | var rpc = { 123 | // TODO 124 | id: "0", 125 | fn: "Chat.Message", 126 | args: { 127 | from: document.msgform.name.value, 128 | message: document.msgform.message.value 129 | } 130 | }; 131 | console.log("rpc out:", rpc); 132 | ws.send(JSON.stringify(rpc)); 133 | document.msgform.message.value = ""; 134 | return false; 135 | }; 136 | -------------------------------------------------------------------------------- /lib/commands/commands.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import "encoding/json" 4 | 5 | /* 6 | commands package contains structures for working with JSON RPC 7 | Calls to ThrustCore 8 | */ 9 | 10 | /* 11 | Command defines the structure used in send json rpc messages to ThrustCore 12 | */ 13 | type Command struct { 14 | ID uint `json:"_id"` 15 | Action string `json:"_action"` 16 | ObjectType string `json:"_type,omitempty"` 17 | Method string `json:"_method"` 18 | TargetID uint `json:"_target,omitempty"` 19 | Args CommandArguments `json:"_args"` 20 | } 21 | 22 | /* 23 | CommandArguments defines the structure used in providing arguments 24 | to Command's when talking to ThrustCore 25 | Covers all possible argument combinations. 26 | Makes use of omit empty to adapt to different use cases 27 | */ 28 | type CommandArguments struct { 29 | RootUrl string `json:"root_url,omitempty"` 30 | Title string `json:"title,omitempty"` 31 | Size SizeHW `json:"size,omitempty"` 32 | Position PositionXY `json:"position,omitempty"` 33 | X int `json:"x,omitempty"` 34 | Y int `json:"y,omitempty"` 35 | Width uint `json:"width,omitempty"` 36 | Height uint `json:"height,omitempty"` 37 | CommandID uint `json:"command_id,omitempty"` 38 | Label string `json:"label,omitempty"` 39 | MenuID uint `json:"menu_id,omitempty"` // this should never be 0 anyway 40 | WindowID uint `json:"window_id,omitempty"` 41 | SessionID uint `json:"session_id,omitempty"` 42 | GroupID uint `json:"group_id,omitempty"` 43 | Value bool `json:"value"` 44 | CookieStore bool `json:"cookie_store"` 45 | OffTheRecord bool `json:"off_the_record"` 46 | Fullscreen bool `json:"fullscreen"` 47 | Kiosk bool `json:"kiosk"` 48 | Focus bool `json:"focus"` 49 | HasFrame bool `json:"has_frame"` 50 | IconPath string `json:"icon_path,omitempty"` 51 | Path string `json:"path,omitempty"` 52 | Message RemoteMessage `json:"message,omitempty"` 53 | } 54 | 55 | /* 56 | SizeHW is a simple struct defining Height and Width 57 | */ 58 | type SizeHW struct { 59 | Width uint `json:"width,omitempty"` 60 | Height uint `json:"height,omitempty"` 61 | } 62 | 63 | type PositionXY struct { 64 | X int `json:"x,omitempty"` 65 | Y int `json:"y,omitempty"` 66 | } 67 | 68 | /* 69 | ReplyResult is used in CommandResponse's of Type Reply 70 | */ 71 | type ReplyResult struct { 72 | TargetID uint `json:"_target,omitempty"` 73 | Cookies json.RawMessage `json:"cookies"` 74 | } 75 | 76 | /* 77 | EventResult is used in CommandResponse's of Type Event 78 | */ 79 | type EventResult struct { 80 | CommandID uint `json:"command_id,omitempty"` 81 | EventFlags int `json:"event_flags,omitempty"` 82 | Message RemoteMessage `json:"message,omitempty"` 83 | Type string `json:"-"` 84 | } 85 | 86 | /* 87 | Remote Message 88 | */ 89 | type RemoteMessage struct { 90 | Payload string `json:"payload"` 91 | } 92 | 93 | /* 94 | CommandResponse defines the structure of a response 95 | from a Command sent to ThrustCore 96 | */ 97 | type CommandResponse struct { 98 | Action string `json:"_action,omitempty"` 99 | Error string `json:"_error,omitempty"` 100 | ID uint `json:"_id,omitempty"` 101 | Result ReplyResult `json:"_result,omitempty"` 102 | Event EventResult `json:"_event,omitempty"` 103 | Args CommandResponseArguments `json:"_args,omitempty"` 104 | TargetID uint `json:"_target,omitempty"` 105 | Method string `json:"_method,omitempty"` 106 | Type string `json:"_type,omitempty"` 107 | } 108 | 109 | /* 110 | Command Response Arguments covers all possible cases of CommandResponses that contain an _args parameter. 111 | This is often the case with methods on the Session Object that are Invoked from the core. 112 | */ 113 | type CommandResponseArguments struct { 114 | key string `json:"key,omitempty"` 115 | } 116 | -------------------------------------------------------------------------------- /examples/chat/server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "html/template" 7 | "log" 8 | "net" 9 | "net/http" 10 | "os" 11 | "path" 12 | "time" 13 | 14 | "github.com/gorilla/websocket" 15 | "github.com/miketheprogrammer/go-thrust/thrust" 16 | "github.com/tv42/birpc" 17 | "github.com/tv42/birpc/wetsock" 18 | "github.com/tv42/topic" 19 | ) 20 | 21 | var ( 22 | host = flag.String("host", "0.0.0.0", "IP address to bind to") 23 | port = flag.Int("port", 8000, "TCP port to listen on") 24 | ) 25 | 26 | var html *template.Template = template.New("main") 27 | 28 | func init() { 29 | template.Must(html.New("chat.html").Parse(string(chat_html))) 30 | template.Must(html.New("chat.css").Parse(string(chat_css))) 31 | template.Must(html.New("chat.js").Parse(string(chat_js))) 32 | } 33 | 34 | func Usage() { 35 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 36 | flag.PrintDefaults() 37 | } 38 | 39 | type Incoming struct { 40 | From string 41 | Message string 42 | } 43 | 44 | type Outgoing struct { 45 | Time time.Time `json:"time"` 46 | From string `json:"from"` 47 | Message string `json:"message"` 48 | } 49 | 50 | func index(w http.ResponseWriter, req *http.Request) { 51 | w.Header().Set("Content-Type", "text/html") 52 | w.WriteHeader(http.StatusOK) 53 | err := html.ExecuteTemplate(w, "chat.html", nil) 54 | if err != nil { 55 | log.Printf("Template error: %v", err) 56 | } 57 | } 58 | 59 | type Chat struct { 60 | broadcast *topic.Topic 61 | registry *birpc.Registry 62 | } 63 | 64 | // Closing the socket from another goroutine causes a concurrent 65 | // blocking read to return a net.errClosing error. It's pointless to 66 | // spam logs with it. The error is not exported, so checking for it is 67 | // ugly. 68 | // 69 | // Background: https://code.google.com/p/go/issues/detail?id=4373 70 | func isErrClosing(err error) bool { 71 | switch err2 := err.(type) { 72 | case *net.OpError: 73 | err = err2.Err 74 | } 75 | return err.Error() == "use of closed network connection" 76 | } 77 | 78 | type nothing struct{} 79 | 80 | func (c *Chat) Message(msg *Incoming, _ *nothing, ws *websocket.Conn) error { 81 | log.Printf("recv from %v:%#v\n", ws.RemoteAddr(), msg) 82 | 83 | c.broadcast.Broadcast <- Outgoing{ 84 | Time: time.Now(), 85 | From: msg.From, 86 | Message: msg.Message, 87 | } 88 | return nil 89 | } 90 | 91 | func main() { 92 | prog := path.Base(os.Args[0]) 93 | log.SetFlags(0) 94 | log.SetPrefix(prog + ": ") 95 | 96 | flag.Usage = Usage 97 | flag.Parse() 98 | 99 | if flag.NArg() > 0 { 100 | Usage() 101 | os.Exit(1) 102 | } 103 | 104 | log.Printf("Serving at http://%s:%d/", *host, *port) 105 | 106 | chat := Chat{} 107 | chat.broadcast = topic.New() 108 | chat.registry = birpc.NewRegistry() 109 | chat.registry.RegisterService(&chat) 110 | defer close(chat.broadcast.Broadcast) 111 | upgrader := websocket.Upgrader{} 112 | 113 | serve := func(w http.ResponseWriter, req *http.Request) { 114 | ws, err := upgrader.Upgrade(w, req, nil) 115 | if err != nil { 116 | log.Println(err) 117 | return 118 | } 119 | endpoint := wetsock.NewEndpoint(chat.registry, ws) 120 | messages := make(chan interface{}, 10) 121 | chat.broadcast.Register(messages) 122 | go func() { 123 | defer chat.broadcast.Unregister(messages) 124 | for i := range messages { 125 | msg := i.(Outgoing) 126 | // Fire-and-forget. 127 | // TODO use .Notify when it exists 128 | _ = endpoint.Go("Chat.Message", msg, nil, nil) 129 | } 130 | // broadcast topic kicked us out for being too slow; 131 | // probably a hung TCP connection. let client 132 | // re-establish. 133 | log.Printf("Kicking slow client: %v", ws.RemoteAddr()) 134 | ws.Close() 135 | }() 136 | 137 | if err := endpoint.Serve(); err != nil { 138 | log.Printf("websocket error from %v: %v", ws.RemoteAddr(), err) 139 | } 140 | } 141 | 142 | http.HandleFunc("/sock", serve) 143 | http.Handle("/", http.HandlerFunc(index)) 144 | addr := fmt.Sprintf("%s:%d", *host, *port) 145 | 146 | thrust.InitLogger() 147 | thrust.Start() 148 | thrustWindow := thrust.NewWindow(thrust.WindowOptions{ 149 | RootUrl: fmt.Sprintf("http://127.0.0.1:%d", *port), 150 | }) 151 | thrustWindow.Show() 152 | thrustWindow.Focus() 153 | 154 | err := http.ListenAndServe(addr, nil) 155 | if err != nil { 156 | log.Fatal(err) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /lib/bindings/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | /* 4 | Pacage Session contains an API Binding, and 5 | interfaces that will assist in accessing the 6 | default Session or implementing your own Custom Session. 7 | */ 8 | import ( 9 | "encoding/json" 10 | 11 | . "github.com/miketheprogrammer/go-thrust/lib/commands" 12 | . "github.com/miketheprogrammer/go-thrust/lib/common" 13 | "github.com/miketheprogrammer/go-thrust/lib/connection" 14 | "github.com/miketheprogrammer/go-thrust/lib/dispatcher" 15 | ) 16 | 17 | /* 18 | Session is the core API Binding object used to communicate with Thrust. 19 | */ 20 | type Session struct { 21 | TargetID uint 22 | CookieStore bool 23 | OffTheRecord bool 24 | Path string 25 | Ready bool 26 | CommandHistory []*Command 27 | ResponseHistory []*CommandResponse 28 | WaitingResponses []*Command 29 | CommandQueue []*Command 30 | SendChannel *connection.In 31 | SessionOverrideInterface SessionInvokable 32 | } 33 | 34 | /* 35 | NewSession is a constructor that takes 3 arguments, 36 | incognito which is a boolean, meaning dont persist session state 37 | after close. 38 | overrideDefaultSession which is a boolean that till tell thrust core 39 | to try to invoke session methods from us. 40 | path string is the path to store session data. 41 | */ 42 | func NewSession(incognito, overrideDefaultSession bool, path string) *Session { 43 | session := Session{ 44 | CookieStore: overrideDefaultSession, 45 | OffTheRecord: incognito, 46 | Path: path, 47 | } 48 | if overrideDefaultSession == true { 49 | session.SetInvokable(*NewDummySession()) 50 | } 51 | command := Command{ 52 | Action: "create", 53 | ObjectType: "session", 54 | Args: CommandArguments{ 55 | CookieStore: session.CookieStore, 56 | OffTheRecord: session.OffTheRecord, 57 | Path: session.Path, 58 | }, 59 | } 60 | session.SendChannel = connection.GetInputChannels() 61 | session.WaitingResponses = append(session.WaitingResponses, &command) 62 | session.Send(&command) 63 | dispatcher.RegisterHandler(session.DispatchResponse) 64 | return &session 65 | } 66 | 67 | func (session *Session) HandleInvoke(reply CommandResponse) { 68 | if reply.TargetID == session.TargetID { 69 | response := &CommandResponse{ 70 | Action: "reply", 71 | ID: reply.ID, 72 | Result: ReplyResult{}, 73 | } 74 | switch reply.Method { 75 | case "cookies_load": 76 | cookies := session.SessionOverrideInterface.InvokeCookiesLoad(&reply.Args, session) 77 | marshaledCookies, err := json.Marshal(cookies) 78 | if err != nil { 79 | Log.Print(err) 80 | } else { 81 | response.Result.Cookies = marshaledCookies 82 | } 83 | case "cookies_load_for_key": 84 | cookies := session.SessionOverrideInterface.InvokeCookiesLoadForKey(&reply.Args, session) 85 | marshaledCookies, err := json.Marshal(cookies) 86 | if err != nil { 87 | Log.Print(err) 88 | } else { 89 | response.Result.Cookies = marshaledCookies 90 | } 91 | case "cookies_flush": 92 | session.SessionOverrideInterface.InvokeCookiesFlush(&reply.Args, session) 93 | case "cookies_add": 94 | session.SessionOverrideInterface.InvokeCookiesAdd(&reply.Args, session) 95 | case "cookies_delete": 96 | session.SessionOverrideInterface.InvokeCookiesDelete(&reply.Args, session) 97 | case "cookies_update_access_time": 98 | session.SessionOverrideInterface.InvokeCookiesUpdateAccessTime(&reply.Args, session) 99 | case "cookies_force_keep_session_state": 100 | session.SessionOverrideInterface.InvokeCookieForceKeepSessionState(&reply.Args, session) 101 | } 102 | Log.Print("Sending Response to Invoke") 103 | session.SendChannel.CommandResponses <- response 104 | } 105 | } 106 | 107 | func (session *Session) HandleReply(reply CommandResponse) { 108 | Log.Print(reply) 109 | for k, command := range session.WaitingResponses { 110 | if command.ID != reply.ID { 111 | continue 112 | } 113 | if command.ID == reply.ID { 114 | Log.Print("Window(", session.TargetID, ")::Handling Reply::", reply) 115 | if len(session.WaitingResponses) > 1 { 116 | // Remove the element at index k 117 | session.WaitingResponses = session.WaitingResponses[:k+copy(session.WaitingResponses[k:], session.WaitingResponses[k+1:])] 118 | } else { 119 | // Just initialize to empty splice literal 120 | session.WaitingResponses = []*Command{} 121 | } 122 | Log.Print("session", session.TargetID, command.Action, reply.Result.TargetID) 123 | if session.TargetID == 0 && command.Action == "create" { 124 | if reply.Result.TargetID != 0 { 125 | session.TargetID = reply.Result.TargetID 126 | Log.Print("Session:: Received TargetID(", session.TargetID, ") :: Setting Ready State") 127 | session.Ready = true 128 | } 129 | } 130 | } 131 | } 132 | } 133 | 134 | func (session *Session) DispatchResponse(response CommandResponse) { 135 | switch response.Action { 136 | case "invoke": 137 | session.HandleInvoke(response) 138 | case "reply": 139 | session.HandleReply(response) 140 | 141 | } 142 | } 143 | 144 | func (session *Session) Send(command *Command) { 145 | session.SendChannel.Commands <- command 146 | } 147 | 148 | func (session *Session) SetInvokable(si SessionInvokable) { 149 | session.SessionOverrideInterface = si 150 | } 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-thrust 2 | ========= 3 | 4 | ALERT - Breaking Changes. 5 | Please See Examples, and Tutorial Files, however Tutorial Readmes have not yet been updated.. 6 | Basic Breakdown is, alot of intializers are now wrapped in the `thrust` package. 7 | i.e. window.NewWindow is now thrust.NewWindow. 8 | Import Paths have changed, please see the lib/ folder. 9 | 10 | Cross Platform UI Kit powered by Blink/V8/Chromium Content Lib 11 | 12 | Current Thrust Version 0.7.5 13 | 14 | ```bash 15 | # Fetch go-thrust (lib and command) 16 | go get -v github.com/miketheprogrammer/go-thrust 17 | 18 | # Install Thrust 19 | go-thrust install 20 | ``` 21 | 22 | ![Logo Thrust](http://i.imgur.com/DwFKI0J.png) 23 | Official GoLang Thrust Client for Thrust (https://github.com/breach/thrust) 24 | 25 | Chat with us 26 | ================== 27 | irc: #breach on freenode - this is the entire breach/thrust community 28 | gitter.im : https://gitter.im/miketheprogrammer/go-thrust - focused on go-thrust. 29 | [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/miketheprogrammer/go-thrust?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 30 | 31 | Contributors 32 | =================== 33 | Michael Hernandez (miketheprogrammer) 34 | 35 | Francis Bouvier 36 | 37 | tehbilly 38 | 39 | gronpipmaster 40 | 41 | ShionRyuu 42 | 43 | Toorop 44 | 45 | Quick Start, see it in action 46 | ================== 47 | On Linux/Darwin 48 | 49 | Start Go Thrust using source and GoLang runtime 50 | ```bash 51 | go run tutorials/basic_window/basic_window.go 52 | ``` 53 | 54 | For Single Binary Install please use the tutorial 55 | ```bash 56 | cd tutorials/advanced_single_binary_distribution 57 | mkdir vendor 58 | cd vendor 59 | wget https://github.com/breach/thrust/releases/download/v0.7.6/thrust-v0.7.6-darwin-x64.zip 60 | cd .. 61 | go-bindata -pkg provisioner -o provisioner/vendor.go vendor/ 62 | go run advanced_single_binary_distribution.go 63 | 64 | ``` 65 | 66 | Forward: 67 | ================== 68 | Go-Thrust is a cross platform GUI Application development base. It aims to provide the 69 | essentials to create a cross platform application using web technologies such as, (HTML,CSS,JS), 70 | as well as new technologies like Websockets, Webview, etcetera. Go-Thrust is the supported Go Library for accessing the underlying technology Thrust. Thrust builds upon the efforts of predecessors like ExoBrowser, Node-Webkit, and Atom-Shell to create an easily buildable Webbrowser base. Thrust consists essentially of a C/C++ implementation of the Chromium Content Library (Blink/V8) and abstracts away most of the nitty gritty platform specific implementations. 71 | 72 | While Go Thrust and Thrust are in their infancy, they already provide some pretty nice features. 73 | Expect many more to come. 74 | 75 | Thrust exposes all of this beautifully over an Stdin/Stdout pipe speaking a JSONRPC protocol. 76 | 77 | DOCUMENTATION (Docs and Tutorials) 78 | ================ 79 | * [Tutorials](https://github.com/miketheprogrammer/go-thrust/tree/master/tutorials) 80 | * [Examples](https://github.com/miketheprogrammer/go-thrust/tree/master/examples) 81 | * [GoDoc](http://godoc.org/github.com/miketheprogrammer/go-thrust) 82 | 83 | 84 | Current Platform Specific Cases: 85 | ================ 86 | Currently Darwin handles Application Menus different from other systems. 87 | The "Root" menu is always named the project name. However on linux, the name is whatever you provide. This can be seen in demo.go, and can be alleviated by trying to use the same name, or by using different logic for both cases. I make no attempt to unify it at the library level, because the user deserves the freedom to do this themselves. 88 | 89 | This thrust client exposes enough Methods to be fairly forwards compatible even without adding new helper methods. The beauty of working with a stable JSON RPC Protocol is that most methods are just helpers around build that data structure. 90 | 91 | Helper methods receive the UpperCamelCase version of their relative names in Thrust. 92 | 93 | i.e. insert_item_at == InsertItemAt 94 | 95 | Please note that the intended use case of Application Menus is to only support 96 | OSX and Unity/X11 global menu bars. This means that you should implement most menus in html and javascript, using IPC/RPC to communicate with the host application. The side effect is primarily that Windows, and certain unix/linux systems will not load ApplicationMenus 97 | 98 | 99 | Using Go-Thrust as a Library and bundling your final release 100 | ========================== 101 | To use go-thrust as a library, simple use the code in the same way you would use any GoLang library. 102 | 103 | - More info to come on Prepping releases. 104 | 105 | Current Platform Targets 106 | ================ 107 | Linux(x64), Darwin(x64), Windows(ia32). - These targets depend on Thrust Core, obviously the go library will run on any target, but the core may not. 108 | 109 | Unfortunately because Thrust is built around C and Go its not exactly portable to Android and IOS, this may be possible in the future, but not yet. However, since we are primarily building a web app, we may be able to do something at a later time. For instance, there is work being done on allowing java to run go libraries. This would allow us to hopefully covert our application menus, to maybe just an html menu at the top, and just serve the page, instead of using Thrust. Thats all for the future however, for now, enjoy CPD for Linux,Darwin,Windows. 110 | 111 | 112 | The Future of Go-Thrust 113 | ================ 114 | Any user of Go-Thrust should feel free to contribute ideas, code, anything that can help move this project in the right direction. The ideal goal is to stay as much out of the way, but still provide a useful system. 115 | ``` 116 | -------------------------------------------------------------------------------- /examples/chat/server/chat.css.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var chat_css = []byte{ 4 | 0x23, 0x6d, 0x73, 0x67, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 5 | 0x6f, 0x72, 0x64, 0x65, 0x72, 0x3a, 0x20, 0x31, 0x70, 0x78, 0x20, 0x73, 6 | 0x6f, 0x6c, 0x69, 0x64, 0x20, 0x67, 0x72, 0x61, 0x79, 0x3b, 0x0a, 0x20, 7 | 0x20, 0x20, 0x20, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x3a, 0x20, 8 | 0x30, 0x2e, 0x35, 0x65, 0x6d, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 9 | 0x61, 0x78, 0x2d, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3a, 0x20, 0x38, 10 | 0x30, 0x25, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x23, 0x6d, 0x73, 0x67, 0x20, 11 | 0x70, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x72, 0x67, 12 | 0x69, 0x6e, 0x3a, 0x20, 0x30, 0x2e, 0x31, 0x65, 0x6d, 0x3b, 0x0a, 0x7d, 13 | 0x0a, 0x0a, 0x23, 0x6d, 0x73, 0x67, 0x20, 0x73, 0x70, 0x61, 0x6e, 0x20, 14 | 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x72, 0x67, 0x69, 0x6e, 15 | 0x2d, 0x72, 0x69, 0x67, 0x68, 0x74, 0x3a, 0x20, 0x30, 0x2e, 0x35, 0x65, 16 | 0x6d, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x23, 0x6d, 0x73, 0x67, 0x20, 0x2e, 17 | 0x74, 0x69, 0x6d, 0x65, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x66, 18 | 0x6f, 0x6e, 0x74, 0x2d, 0x66, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x3a, 0x20, 19 | 0x6d, 0x6f, 0x6e, 0x6f, 0x73, 0x70, 0x61, 0x63, 0x65, 0x3b, 0x0a, 0x20, 20 | 0x20, 0x20, 0x20, 0x66, 0x6f, 0x6e, 0x74, 0x2d, 0x73, 0x69, 0x7a, 0x65, 21 | 0x3a, 0x20, 0x38, 0x30, 0x25, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x63, 22 | 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x23, 0x34, 0x30, 0x34, 0x30, 0x34, 23 | 0x30, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 24 | 0x3a, 0x20, 0x72, 0x69, 0x67, 0x68, 0x74, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 25 | 0x23, 0x6d, 0x73, 0x67, 0x20, 0x2e, 0x66, 0x72, 0x6f, 0x6d, 0x20, 0x7b, 26 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 27 | 0x62, 0x6c, 0x75, 0x65, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x23, 0x6d, 0x73, 28 | 0x67, 0x20, 0x2e, 0x66, 0x72, 0x6f, 0x6d, 0x3a, 0x61, 0x66, 0x74, 0x65, 29 | 0x72, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6e, 0x74, 30 | 0x65, 0x6e, 0x74, 0x3a, 0x20, 0x22, 0x3a, 0x22, 0x3b, 0x0a, 0x7d, 0x0a, 31 | 0x0a, 0x23, 0x6d, 0x73, 0x67, 0x20, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 32 | 0x67, 0x65, 0x20, 0x7b, 0x0a, 0x7d, 0x0a, 0x0a, 0x66, 0x6f, 0x72, 0x6d, 33 | 0x5b, 0x6e, 0x61, 0x6d, 0x65, 0x3d, 0x22, 0x6d, 0x73, 0x67, 0x66, 0x6f, 34 | 0x72, 0x6d, 0x22, 0x5d, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 35 | 0x61, 0x78, 0x2d, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3a, 0x20, 0x39, 0x35, 36 | 0x25, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x64, 0x69, 0x73, 0x70, 0x6c, 37 | 0x61, 0x79, 0x3a, 0x20, 0x2d, 0x77, 0x65, 0x62, 0x6b, 0x69, 0x74, 0x2d, 38 | 0x66, 0x6c, 0x65, 0x78, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x64, 0x69, 39 | 0x73, 0x70, 0x6c, 0x61, 0x79, 0x3a, 0x20, 0x2d, 0x6d, 0x6f, 0x7a, 0x2d, 40 | 0x66, 0x6c, 0x65, 0x78, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x64, 0x69, 41 | 0x73, 0x70, 0x6c, 0x61, 0x79, 0x3a, 0x20, 0x2d, 0x6d, 0x73, 0x2d, 0x66, 42 | 0x6c, 0x65, 0x78, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x64, 0x69, 0x73, 43 | 0x70, 0x6c, 0x61, 0x79, 0x3a, 0x20, 0x66, 0x6c, 0x65, 0x78, 0x3b, 0x0a, 44 | 0x20, 0x20, 0x20, 0x20, 0x2d, 0x77, 0x65, 0x62, 0x6b, 0x69, 0x74, 0x2d, 45 | 0x66, 0x6c, 0x65, 0x78, 0x2d, 0x66, 0x6c, 0x6f, 0x77, 0x3a, 0x20, 0x72, 46 | 0x6f, 0x77, 0x20, 0x6e, 0x6f, 0x77, 0x72, 0x61, 0x70, 0x3b, 0x0a, 0x20, 47 | 0x20, 0x20, 0x20, 0x2d, 0x6d, 0x6f, 0x7a, 0x2d, 0x66, 0x6c, 0x65, 0x78, 48 | 0x2d, 0x66, 0x6c, 0x6f, 0x77, 0x3a, 0x20, 0x72, 0x6f, 0x77, 0x20, 0x6e, 49 | 0x6f, 0x77, 0x72, 0x61, 0x70, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2d, 50 | 0x6d, 0x73, 0x2d, 0x66, 0x6c, 0x65, 0x78, 0x2d, 0x66, 0x6c, 0x6f, 0x77, 51 | 0x3a, 0x20, 0x72, 0x6f, 0x77, 0x20, 0x6e, 0x6f, 0x77, 0x72, 0x61, 0x70, 52 | 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2d, 0x6d, 0x73, 0x2d, 0x66, 0x6c, 53 | 0x65, 0x78, 0x2d, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 54 | 0x3a, 0x20, 0x72, 0x6f, 0x77, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2d, 55 | 0x6d, 0x73, 0x2d, 0x66, 0x6c, 0x65, 0x78, 0x2d, 0x77, 0x72, 0x61, 0x70, 56 | 0x3a, 0x20, 0x6e, 0x6f, 0x77, 0x72, 0x61, 0x70, 0x3b, 0x0a, 0x20, 0x20, 57 | 0x20, 0x20, 0x66, 0x6c, 0x65, 0x78, 0x2d, 0x66, 0x6c, 0x6f, 0x77, 0x3a, 58 | 0x20, 0x72, 0x6f, 0x77, 0x20, 0x6e, 0x6f, 0x77, 0x72, 0x61, 0x70, 0x3b, 59 | 0x0a, 0x7d, 0x0a, 0x0a, 0x66, 0x6f, 0x72, 0x6d, 0x5b, 0x6e, 0x61, 0x6d, 60 | 0x65, 0x3d, 0x22, 0x6d, 0x73, 0x67, 0x66, 0x6f, 0x72, 0x6d, 0x22, 0x5d, 61 | 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 62 | 0x20, 0x2d, 0x77, 0x65, 0x62, 0x6b, 0x69, 0x74, 0x2d, 0x66, 0x6c, 0x65, 63 | 0x78, 0x3a, 0x20, 0x31, 0x20, 0x61, 0x75, 0x74, 0x6f, 0x3b, 0x0a, 0x20, 64 | 0x20, 0x20, 0x20, 0x2d, 0x6d, 0x6f, 0x7a, 0x2d, 0x66, 0x6c, 0x65, 0x78, 65 | 0x3a, 0x20, 0x31, 0x20, 0x61, 0x75, 0x74, 0x6f, 0x3b, 0x0a, 0x20, 0x20, 66 | 0x20, 0x20, 0x2d, 0x6d, 0x73, 0x2d, 0x66, 0x6c, 0x65, 0x78, 0x3a, 0x20, 67 | 0x31, 0x20, 0x61, 0x75, 0x74, 0x6f, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 68 | 0x66, 0x6c, 0x65, 0x78, 0x3a, 0x20, 0x31, 0x20, 0x61, 0x75, 0x74, 0x6f, 69 | 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x66, 0x6f, 0x72, 0x6d, 0x5b, 0x6e, 0x61, 70 | 0x6d, 0x65, 0x3d, 0x22, 0x6d, 0x73, 0x67, 0x66, 0x6f, 0x72, 0x6d, 0x22, 71 | 0x5d, 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5b, 0x6e, 0x61, 0x6d, 0x65, 72 | 0x3d, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x5d, 0x20, 0x7b, 0x0a, 0x20, 73 | 0x20, 0x20, 0x20, 0x6d, 0x61, 0x78, 0x2d, 0x77, 0x69, 0x64, 0x74, 0x68, 74 | 0x3a, 0x20, 0x38, 0x65, 0x6d, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x66, 0x6f, 75 | 0x72, 0x6d, 0x5b, 0x6e, 0x61, 0x6d, 0x65, 0x3d, 0x22, 0x6d, 0x73, 0x67, 76 | 0x66, 0x6f, 0x72, 0x6d, 0x22, 0x5d, 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 77 | 0x5b, 0x74, 0x79, 0x70, 0x65, 0x3d, 0x22, 0x73, 0x75, 0x62, 0x6d, 0x69, 78 | 0x74, 0x22, 0x5d, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2d, 0x77, 79 | 0x65, 0x62, 0x6b, 0x69, 0x74, 0x2d, 0x66, 0x6c, 0x65, 0x78, 0x3a, 0x20, 80 | 0x30, 0x20, 0x30, 0x20, 0x61, 0x75, 0x74, 0x6f, 0x3b, 0x0a, 0x20, 0x20, 81 | 0x20, 0x20, 0x2d, 0x6d, 0x6f, 0x7a, 0x2d, 0x66, 0x6c, 0x65, 0x78, 0x3a, 82 | 0x20, 0x30, 0x20, 0x30, 0x20, 0x61, 0x75, 0x74, 0x6f, 0x3b, 0x0a, 0x20, 83 | 0x20, 0x20, 0x20, 0x2d, 0x6d, 0x73, 0x2d, 0x66, 0x6c, 0x65, 0x78, 0x3a, 84 | 0x20, 0x30, 0x20, 0x30, 0x20, 0x61, 0x75, 0x74, 0x6f, 0x3b, 0x0a, 0x20, 85 | 0x20, 0x20, 0x20, 0x66, 0x6c, 0x65, 0x78, 0x3a, 0x20, 0x30, 0x20, 0x30, 86 | 0x20, 0x61, 0x75, 0x74, 0x6f, 0x3b, 0x0a, 0x7d, 0x0a, 87 | } 88 | -------------------------------------------------------------------------------- /examples/jankybrowser/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 108 | 237 | 238 | 239 |
240 | 246 |
247 |
    248 |
    +
    249 |
    250 |
    251 |
    252 | 253 | 254 | -------------------------------------------------------------------------------- /lib/spawn/helper_darwin.go: -------------------------------------------------------------------------------- 1 | package spawn 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | /* 12 | GetThrustDirectory returns the Directory where the unzipped thrust contents are. 13 | Differs between builds based on OS 14 | */ 15 | func GetThrustDirectory() string { 16 | return base + "/vendor/darwin/x64/v" + thrustVersion 17 | } 18 | 19 | /* 20 | GetDownloadPath gets the download or extract directory for Thrust 21 | */ 22 | func GetDownloadPath() string { 23 | return strings.Replace(filepath.Join(base, "$V"), "$V", thrustVersion, 1) 24 | } 25 | 26 | /* 27 | Get the path to the .app directory (only OSX) 28 | */ 29 | func getAppDirectory() string { 30 | return base + "/vendor/darwin/x64/v" + thrustVersion + "/" + ApplicationName + ".app" 31 | } 32 | 33 | /* 34 | GetExecutablePath returns the path to the Thrust Executable 35 | Differs between builds based on OS 36 | */ 37 | func GetExecutablePath() string { 38 | return GetThrustDirectory() + "/" + ApplicationName + ".app/Contents/MacOS/" + ApplicationName 39 | } 40 | 41 | /* 42 | GetDownloadURL returns the interpolatable version of the Thrust download url 43 | Differs between builds based on OS 44 | */ 45 | func GetDownloadURL() string { 46 | return "https://github.com/breach/thrust/releases/download/v$V/thrust-v$V-darwin-x64.zip" 47 | } 48 | 49 | /* 50 | SetThrustApplicationTitle sets the title in the Info.plist. This method only exists on Darwin. 51 | */ 52 | func Bootstrap() error { 53 | if ExecutableNotExist() == true { 54 | var err error 55 | if err = prepareExecutable(); err != nil { 56 | return err 57 | } 58 | if err = prepareInfoPropertiesListTemplate(); err != nil { 59 | return err 60 | } 61 | 62 | return writeInfoPropertiesList() 63 | } 64 | 65 | return nil 66 | } 67 | 68 | /* 69 | ExecutableNotExist checks if the executable does not exist 70 | */ 71 | func ExecutableNotExist() bool { 72 | _, err := os.Stat(GetExecutablePath()) 73 | fmt.Println(err) 74 | return os.IsNotExist(err) 75 | } 76 | 77 | func PathNotExist(path string) bool { 78 | _, err := os.Stat(path) 79 | return os.IsNotExist(err) 80 | } 81 | 82 | /* 83 | prepareExecutable dowloads, unzips and does alot of other magic to prepare our thrust core build. 84 | */ 85 | func prepareExecutable() error { 86 | path, err := downloadFromUrl(GetDownloadURL(), base+"/$V", thrustVersion) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | return UnzipExecutable(path) 92 | } 93 | 94 | func UnzipExecutable(path string) error { 95 | if err := unzip(path, GetThrustDirectory()); err != nil { 96 | return err 97 | } 98 | os.Rename(GetThrustDirectory()+"/ThrustShell.app/Contents/MacOS/ThrustShell", GetThrustDirectory()+"/ThrustShell.app/Contents/MacOS/"+ApplicationName) 99 | os.Rename(GetThrustDirectory()+"/ThrustShell.app", GetThrustDirectory()+"/"+ApplicationName+".app") 100 | 101 | if err := applySymlinks(); err != nil { 102 | panic(err) 103 | } 104 | 105 | return nil 106 | } 107 | 108 | /* 109 | ApplySymLinks exists because our unzip utility does not respect deferred symlinks. It applies all the neccessary symlinks to make the thrust core exe connect to the thrust core libs. 110 | */ 111 | func applySymlinks() error { 112 | fmt.Println("Removing bad symlinks") 113 | var err error 114 | if PathNotExist(getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Versions/Current") == false { 115 | if err = os.Remove(getAppDirectory() + "/Contents/Frameworks/ThrustShell Framework.framework/Versions/Current"); err != nil { 116 | return err 117 | } 118 | } 119 | 120 | if PathNotExist(getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Frameworks") == false { 121 | if err = os.Remove(getAppDirectory() + "/Contents/Frameworks/ThrustShell Framework.framework/Frameworks"); err != nil { 122 | return err 123 | } 124 | } 125 | 126 | if PathNotExist(getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Libraries") == false { 127 | if err = os.Remove(getAppDirectory() + "/Contents/Frameworks/ThrustShell Framework.framework/Libraries"); err != nil { 128 | return err 129 | } 130 | } 131 | 132 | if PathNotExist(getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Resources") == false { 133 | if err = os.Remove(getAppDirectory() + "/Contents/Frameworks/ThrustShell Framework.framework/Resources"); err != nil { 134 | return err 135 | } 136 | } 137 | 138 | if PathNotExist(getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/ThrustShell Framework") == false { 139 | if err = os.Remove(getAppDirectory() + "/Contents/Frameworks/ThrustShell Framework.framework/ThrustShell Framework"); err != nil { 140 | return err 141 | } 142 | } 143 | 144 | if PathNotExist(getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Versions/Current/Libraries") == false { 145 | if err = os.Remove(getAppDirectory() + "/Contents/Frameworks/ThrustShell Framework.framework/Versions/Current/Libraries"); err != nil { 146 | return err 147 | } 148 | } 149 | 150 | fmt.Println("Applying Symlinks") 151 | 152 | if PathNotExist(getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Versions/Current") == true { 153 | if err = os.Symlink( 154 | getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Versions/A", 155 | getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Versions/Current"); err != nil { 156 | return err 157 | } 158 | } 159 | 160 | if PathNotExist(getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Frameworks") == true { 161 | if err = os.Symlink( 162 | getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Versions/Current/Frameworks", 163 | getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Frameworks"); err != nil { 164 | return err 165 | } 166 | } 167 | 168 | if PathNotExist(getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Libraries") == true { 169 | if err = os.Symlink( 170 | getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Versions/Current/Libraries", 171 | getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Libraries"); err != nil { 172 | return err 173 | } 174 | } 175 | 176 | if PathNotExist(getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Resources") == true { 177 | if err = os.Symlink( 178 | getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Versions/Current/Resources", 179 | getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Resources"); err != nil { 180 | return err 181 | } 182 | } 183 | 184 | if PathNotExist(getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/ThrustShell Framework") == true { 185 | if err = os.Symlink( 186 | getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Versions/Current/ThrustShell Framework", 187 | getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/ThrustShell Framework"); err != nil { 188 | return err 189 | } 190 | } 191 | 192 | if PathNotExist(getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Versions/Current/Libraries") == true { 193 | if err = os.Symlink( 194 | getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Versions/A/Libraries/Libraries", 195 | getAppDirectory()+"/Contents/Frameworks/ThrustShell Framework.framework/Versions/Current/Libraries"); err != nil { 196 | return err 197 | } 198 | } 199 | 200 | return nil 201 | } 202 | 203 | func prepareInfoPropertiesListTemplate() error { 204 | plistPath := getInfoPropertiesListDirectory() + "/Info.plist" 205 | 206 | // Not an OSX user, but perhaps we should build this on each invocation anyways? 207 | // Might help prevent stale issues if the plist gets out of date/sync, although 208 | // I'm not entirely sure how important that is. 209 | if _, err := os.Stat(plistPath + ".tmpl"); os.IsNotExist(err) { 210 | plist, err := ioutil.ReadFile(plistPath) 211 | 212 | if err != nil { 213 | fmt.Println(err) 214 | return err 215 | } 216 | 217 | plistTmpl := strings.Replace(string(plist), "ThrustShell", "$$", -1) 218 | plistTmpl = strings.Replace(plistTmpl, "org.breach.$$", "org.breach.ThrustShell", 1) 219 | plistTmpl = strings.Replace(plistTmpl, "$$Application", "ThrustShellApplication", 1) 220 | //func WriteFile(filename string, data []byte, perm os.FileMode) error 221 | 222 | return ioutil.WriteFile(plistPath+".tmpl", []byte(plistTmpl), 0775) 223 | } 224 | 225 | return nil 226 | } 227 | 228 | func writeInfoPropertiesList() error { 229 | plistPath := getInfoPropertiesListDirectory() + "/Info.plist" 230 | if err := prepareInfoPropertiesListTemplate(); err == nil { 231 | plistTmpl, err := ioutil.ReadFile(plistPath + ".tmpl") 232 | 233 | if err != nil { 234 | fmt.Println(err) 235 | return err 236 | } 237 | 238 | plist := strings.Replace(string(plistTmpl), "$$", ApplicationName, -1) 239 | 240 | err = ioutil.WriteFile(plistPath, []byte(plist), 0775) 241 | if err != nil { 242 | return err 243 | } 244 | } else { 245 | fmt.Println("Could not change title") 246 | return nil 247 | } 248 | return nil 249 | } 250 | 251 | func getInfoPropertiesListDirectory() string { 252 | return GetThrustDirectory() + "/" + ApplicationName + ".app/Contents" 253 | } 254 | -------------------------------------------------------------------------------- /lib/bindings/window/window.go: -------------------------------------------------------------------------------- 1 | package window 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "path/filepath" 8 | "time" 9 | 10 | "github.com/miketheprogrammer/go-thrust/lib/bindings/session" 11 | . "github.com/miketheprogrammer/go-thrust/lib/commands" 12 | . "github.com/miketheprogrammer/go-thrust/lib/common" 13 | "github.com/miketheprogrammer/go-thrust/lib/connection" 14 | "github.com/miketheprogrammer/go-thrust/lib/dispatcher" 15 | "github.com/miketheprogrammer/go-thrust/lib/events" 16 | "github.com/miketheprogrammer/go-thrust/lib/spawn" 17 | ) 18 | 19 | type Window struct { 20 | TargetID uint 21 | CommandHistory []*Command 22 | ResponseHistory []*CommandResponse 23 | WaitingResponses []*Command 24 | CommandQueue []*Command 25 | Url string 26 | Title string 27 | Ready bool 28 | Displayed bool 29 | SendChannel *connection.In `json:"-"` 30 | } 31 | 32 | type Options struct { 33 | RootUrl string 34 | Size SizeHW 35 | Title string 36 | IconPath string 37 | HasFrame bool 38 | Session *session.Session 39 | } 40 | 41 | func checkUrl(s string) (string, error) { 42 | u, err := url.Parse(s) 43 | if err != nil { 44 | return s, err 45 | } 46 | if u.Scheme == "" { 47 | p, err := filepath.Abs(s) 48 | if err != nil { 49 | return s, err 50 | } 51 | u = &url.URL{ 52 | Scheme: "file", 53 | Path: p, 54 | } 55 | } 56 | return u.String(), err 57 | } 58 | 59 | func NewWindow(options Options) *Window { 60 | w := Window{} 61 | w.setOptions(options) 62 | _, sendChannel := connection.GetCommunicationChannels() 63 | 64 | size := options.Size 65 | if options.Size == (SizeHW{}) { 66 | size = SizeHW{ 67 | Width: 1024, 68 | Height: 768, 69 | } 70 | } 71 | 72 | windowCreate := Command{ 73 | Action: "create", 74 | ObjectType: "window", 75 | Args: CommandArguments{ 76 | RootUrl: w.Url, 77 | Title: options.Title, // Should be using the options.Title, not spawn.ApplicationName 78 | Size: size, 79 | HasFrame: options.HasFrame, // This was reversed, HasFrame behavior should be 'true' (has frame) and 'false' (no frame) 80 | IconPath: options.IconPath, 81 | }, 82 | } 83 | dispatcher.RegisterHandler(w.DispatchResponse) 84 | if options.Session == nil { 85 | w.SetSendChannel(sendChannel) 86 | w.WaitingResponses = append(w.WaitingResponses, &windowCreate) 87 | w.Send(&windowCreate) 88 | } else { 89 | go func() { 90 | for { 91 | if options.Session.TargetID != 0 { 92 | fmt.Println("sess", options.Session.TargetID) 93 | windowCreate.Args.SessionID = options.Session.TargetID 94 | w.SetSendChannel(sendChannel) 95 | w.WaitingResponses = append(w.WaitingResponses, &windowCreate) 96 | w.Send(&windowCreate) 97 | return 98 | } 99 | time.Sleep(time.Microsecond * 10) 100 | } 101 | }() 102 | } 103 | return &w 104 | } 105 | 106 | func (w *Window) setOptions(options Options) { 107 | u, _ := checkUrl(options.RootUrl) 108 | w.Url = u 109 | if len(w.Url) == 0 { 110 | w.Url = "http://google.com" 111 | } 112 | 113 | w.Title = options.Title 114 | if len(w.Title) == 0 { 115 | w.Title = spawn.ApplicationName 116 | } 117 | } 118 | 119 | func (w *Window) SetSendChannel(sendChannel *connection.In) { 120 | w.SendChannel = sendChannel 121 | } 122 | 123 | func (w *Window) IsTarget(targetId uint) bool { 124 | return targetId == w.TargetID 125 | } 126 | 127 | func (w *Window) HandleError(reply CommandResponse) { 128 | 129 | } 130 | 131 | func (w *Window) HandleReply(reply CommandResponse) { 132 | for k, v := range w.WaitingResponses { 133 | if v.ID != reply.ID { 134 | continue 135 | } 136 | Log.Print("Window(", w.TargetID, ")::Handling Reply::", reply) 137 | if len(w.WaitingResponses) > 1 { 138 | // Remove the element at index k 139 | w.WaitingResponses = w.WaitingResponses[:k+copy(w.WaitingResponses[k:], w.WaitingResponses[k+1:])] 140 | } else { 141 | // Just initialize to empty splice literal 142 | w.WaitingResponses = []*Command{} 143 | } 144 | 145 | // If we dont already have a TargetID then we accept a create action 146 | if w.TargetID == 0 && v.Action == "create" { 147 | if reply.Result.TargetID != 0 { 148 | w.TargetID = reply.Result.TargetID 149 | Log.Print("Received TargetID", "\nSetting Ready State") 150 | w.Ready = true 151 | } 152 | 153 | for i, _ := range w.CommandQueue { 154 | w.CommandQueue[i].TargetID = w.TargetID 155 | w.Send(w.CommandQueue[i]) 156 | } 157 | // Reinitialize empty command queue, and allow gc. 158 | w.CommandQueue = []*Command{} 159 | 160 | return 161 | } 162 | 163 | if v.Action == "call" && v.Method == "show" { 164 | w.Displayed = true 165 | } 166 | 167 | } 168 | } 169 | 170 | func (w *Window) DispatchResponse(reply CommandResponse) { 171 | 172 | switch reply.Action { 173 | case "reply": 174 | w.HandleReply(reply) 175 | } 176 | 177 | } 178 | func (w *Window) Send(command *Command) { 179 | 180 | w.SendChannel.Commands <- command 181 | } 182 | 183 | func (w *Window) Call(command *Command) { 184 | command.Action = "call" 185 | command.TargetID = w.TargetID 186 | if w.Ready == false { 187 | w.CommandQueue = append(w.CommandQueue, command) 188 | return 189 | } 190 | w.Send(command) 191 | } 192 | 193 | func (w *Window) CallWhenReady(command *Command) { 194 | w.WaitingResponses = append(w.WaitingResponses, command) 195 | go func() { 196 | for { 197 | if w.Ready { 198 | w.Call(command) 199 | return 200 | } 201 | time.Sleep(time.Microsecond * 100) 202 | } 203 | }() 204 | } 205 | 206 | func (w *Window) CallWhenDisplayed(command *Command) { 207 | w.WaitingResponses = append(w.WaitingResponses, command) 208 | go func() { 209 | for { 210 | if w.Displayed { 211 | w.Call(command) 212 | return 213 | } 214 | time.Sleep(time.Microsecond * 100) 215 | } 216 | }() 217 | } 218 | 219 | func (w *Window) Show() { 220 | command := Command{ 221 | Method: "show", 222 | } 223 | 224 | w.CallWhenReady(&command) 225 | } 226 | 227 | func (w *Window) SetTitle(title string) { 228 | command := Command{ 229 | Method: "set_title", 230 | Args: CommandArguments{ 231 | Title: title, 232 | }, 233 | } 234 | 235 | w.CallWhenDisplayed(&command) 236 | } 237 | 238 | func (w *Window) Maximize() { 239 | command := Command{ 240 | Method: "maximize", 241 | } 242 | w.CallWhenDisplayed(&command) 243 | } 244 | 245 | func (w *Window) UnMaximize() { 246 | command := Command{ 247 | Method: "unmaximize", 248 | } 249 | w.CallWhenDisplayed(&command) 250 | } 251 | 252 | func (w *Window) Minimize() { 253 | command := Command{ 254 | Method: "minimize", 255 | } 256 | w.CallWhenDisplayed(&command) 257 | } 258 | 259 | func (w *Window) Restore() { 260 | command := Command{ 261 | Method: "restore", 262 | } 263 | w.CallWhenDisplayed(&command) 264 | } 265 | 266 | func (w *Window) Focus() { 267 | command := Command{ 268 | Method: "focus", 269 | Args: CommandArguments{ 270 | Focus: true, 271 | }, 272 | } 273 | 274 | w.CallWhenDisplayed(&command) 275 | } 276 | 277 | func (w *Window) UnFocus() { 278 | command := Command{ 279 | Method: "show", 280 | Args: CommandArguments{ 281 | Focus: false, 282 | }, 283 | } 284 | 285 | w.CallWhenDisplayed(&command) 286 | } 287 | 288 | func (w *Window) Fullscreen(fullscreen bool) { 289 | command := Command{ 290 | Method: "set_fullscreen", 291 | Args: CommandArguments{ 292 | Fullscreen: fullscreen, 293 | }, 294 | } 295 | 296 | w.CallWhenDisplayed(&command) 297 | } 298 | 299 | func (w *Window) Kiosk(kiosk bool) { 300 | command := Command{ 301 | Method: "set_kiosk", 302 | Args: CommandArguments{ 303 | Kiosk: kiosk, 304 | }, 305 | } 306 | 307 | w.CallWhenDisplayed(&command) 308 | } 309 | 310 | func (w *Window) Close() { 311 | command := Command{ 312 | Method: "close", 313 | Args: CommandArguments{}, 314 | } 315 | 316 | w.CallWhenDisplayed(&command) 317 | } 318 | 319 | func (w *Window) OpenDevtools() { 320 | command := Command{ 321 | Method: "open_devtools", 322 | Args: CommandArguments{}, 323 | } 324 | 325 | w.CallWhenDisplayed(&command) 326 | } 327 | 328 | func (w *Window) CloseDevtools() { 329 | command := Command{ 330 | Method: "close_devtools", 331 | Args: CommandArguments{}, 332 | } 333 | 334 | w.CallWhenDisplayed(&command) 335 | } 336 | 337 | func (w *Window) Move(x, y int) { 338 | command := Command{ 339 | Method: "move", 340 | Args: CommandArguments{ 341 | X: x, 342 | Y: y, 343 | }, 344 | } 345 | 346 | w.CallWhenDisplayed(&command) 347 | } 348 | 349 | func (w *Window) Resize(width, height uint) { 350 | command := Command{ 351 | Method: "resize", 352 | Args: CommandArguments{ 353 | Width: width, 354 | Height: height, 355 | }, 356 | } 357 | 358 | w.CallWhenReady(&command) 359 | } 360 | 361 | func (w *Window) Position(x, y int) { 362 | command := Command{ 363 | Method: "position", 364 | Args: CommandArguments{ 365 | Position: PositionXY{ 366 | X: x, 367 | Y: y, 368 | }, 369 | }, 370 | } 371 | 372 | w.CallWhenReady(&command) 373 | } 374 | 375 | func (w *Window) SendRemoteMessage(msg string) { 376 | command := Command{ 377 | Method: "remote", 378 | Args: CommandArguments{ 379 | Message: RemoteMessage{ 380 | Payload: msg, 381 | }, 382 | }, 383 | } 384 | 385 | // We dont use call, because messages are of variable size, and we definitely, 386 | // do not want to store a reference to them. So we use .Send 387 | go func() { 388 | for { 389 | if w.Displayed { 390 | command.Action = "call" 391 | command.TargetID = w.TargetID 392 | w.Send(&command) 393 | return 394 | } 395 | time.Sleep(time.Microsecond * 100) 396 | } 397 | }() 398 | } 399 | 400 | /* 401 | CRorER means commands.CommandResponse or commands.EventResult 402 | */ 403 | type WindowEventHandler func(CRorER interface{}, window *Window) 404 | 405 | /* 406 | Binding Event Handlers are a bit different than global thrust handlers. 407 | The Signature of the function you pass in is WindowEventHandler 408 | */ 409 | func (w *Window) HandleEvent(event string, fn interface{}) (events.ThrustEventHandler, error) { 410 | if fn, ok := fn.(func(CommandResponse, *Window)); ok == true { 411 | return events.NewHandler(event, func(cr CommandResponse) { 412 | fn(cr, w) 413 | }) 414 | } 415 | if fn, ok := fn.(func(EventResult, *Window)); ok == true { 416 | return events.NewHandler(event, func(er EventResult) { 417 | fn(er, w) 418 | }) 419 | } 420 | return events.ThrustEventHandler{}, errors.New("Function Signature Invalid") 421 | } 422 | 423 | func (w *Window) HandleBlur(fn interface{}) (events.ThrustEventHandler, error) { 424 | return w.HandleEvent("blur", fn) 425 | } 426 | 427 | func (w *Window) HandleRemote(fn interface{}) (events.ThrustEventHandler, error) { 428 | return w.HandleEvent("remote", fn) 429 | 430 | } 431 | -------------------------------------------------------------------------------- /lib/bindings/menu/menu.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | /* 4 | Package menu: 5 | Provides common objects and methods for working with menus in 6 | Thrust. Unfortunately, we have had to use GoRoutines in this and other packages to ensure that information is passed to GoLang in the proper order. 7 | Please be aware of this when using this package in your own library. 8 | The reason this is unfortunate is that as library writers it is best to leave GoRoutines out of your library and let the user decide when to use them. However with I/O you typically need to use GoRoutines in some way or another. 9 | */ 10 | import ( 11 | "time" 12 | 13 | "github.com/miketheprogrammer/go-thrust/lib/bindings/window" 14 | . "github.com/miketheprogrammer/go-thrust/lib/commands" 15 | . "github.com/miketheprogrammer/go-thrust/lib/common" 16 | "github.com/miketheprogrammer/go-thrust/lib/connection" 17 | "github.com/miketheprogrammer/go-thrust/lib/dispatcher" 18 | ) 19 | 20 | /* 21 | Menu is the basic object for creating and working with Menu's 22 | Provides all the necessary attributes and methods to work with asynchronous calls to the menu API. 23 | The TargetID is assigned by ThrustCore, so on init of this object, there is no TargetID. A Goroutine is dispatched to get the targetID. 24 | */ 25 | type Menu struct { 26 | TargetID uint `json:"target_id,omitempty"` 27 | WaitingResponses []*Command `json:"awaiting_responses,omitempty"` 28 | CommandQueue []*Command `json:"command_queue,omitempty"` 29 | Ready bool `json:"ready"` 30 | Displayed bool `json:"displayed"` 31 | Parent *Menu `json:"-"` 32 | Children []*Menu `json:"-"` 33 | Items []*MenuItem `json:"items,omitempty"` 34 | EventRegistry []uint `json:"events,omitempty"` 35 | SendChannel *connection.In `json:"-"` 36 | Sync MenuSync `jons:"-"` 37 | ReplyHandlers map[uint]func(reply CommandResponse, item *MenuItem) `json:"_"` 38 | } 39 | 40 | /* 41 | Create a new menu object. 42 | Dispatches a call to ThrustCore to generate the object and return the new 43 | TargetID in a reply. 44 | */ 45 | func NewMenu() *Menu { 46 | menu := Menu{} 47 | menuCreate := Command{ 48 | Action: "create", 49 | ObjectType: "menu", 50 | } 51 | menu.Sync = MenuSync{ 52 | ReadyChan: make(chan bool), 53 | DisplayedChan: make(chan bool), 54 | ChildStableChan: make(chan uint), 55 | TreeStableChan: make(chan bool), 56 | ReadyQueue: make([]*Command, 0), 57 | DisplayedQueue: make([]*Command, 0), 58 | ChildStableQueue: make([]*ChildCommand, 0), 59 | TreeStableQueue: make([]*Command, 0), 60 | } 61 | menu.ReplyHandlers = make(map[uint]func(reply CommandResponse, item *MenuItem)) 62 | menu.SetSendChannel(connection.GetInputChannels()) 63 | menu.WaitingResponses = append(menu.WaitingResponses, &menuCreate) 64 | dispatcher.RegisterHandler(menu.DispatchResponse) 65 | 66 | go menu.SendThread() 67 | menu.Send(&menuCreate) 68 | 69 | return &menu 70 | } 71 | 72 | /* 73 | SetSendChannel is a helper Setter for SendChannel, in case we make it private in the future. 74 | Use this for full forwards compatibility. 75 | */ 76 | func (menu *Menu) SetSendChannel(sendChannel *connection.In) { 77 | menu.SendChannel = sendChannel 78 | } 79 | 80 | /* 81 | IsTarget checks if the current menu is the menu we are looking for. 82 | */ 83 | func (menu *Menu) IsTarget(targetId uint) bool { 84 | return targetId == menu.TargetID 85 | } 86 | 87 | /* 88 | HandleError is a handler for Error responses from ThrustCore 89 | This should be changed to private as soon as API stabilizes. 90 | */ 91 | func (menu *Menu) HandleError(reply CommandResponse) { 92 | 93 | } 94 | 95 | /* 96 | HandleEvent is a handler for Event responses from ThrustCore 97 | This should be changed to private as soon as API stabilizes. 98 | */ 99 | func (menu *Menu) HandleEvent(reply CommandResponse) { 100 | for _, item := range menu.Items { 101 | if reply.Event.CommandID == item.CommandID { 102 | Log.Print("Menu(", menu.TargetID, "):: Handling Event", item.CommandID, "::Handled With Flags", reply.Event.EventFlags, "With Type", item.Type) 103 | handler, ok := menu.ReplyHandlers[item.CommandID] 104 | if ok { 105 | handler(reply, item) 106 | } else { 107 | item.HandleEvent() 108 | } 109 | return 110 | } 111 | } 112 | } 113 | 114 | /* 115 | HandleReply is a handler for Reply responses from ThrustCore 116 | This should be changed to private as soon as API stabilizes. 117 | */ 118 | func (menu *Menu) HandleReply(reply CommandResponse) { 119 | 120 | for k, v := range menu.WaitingResponses { 121 | if v.ID != reply.ID { 122 | continue 123 | } 124 | Log.Print("MENU(", menu.TargetID, ")::Handling Reply", reply) 125 | removeAt := func(k int) { 126 | if len(menu.WaitingResponses) > 1 { 127 | menu.WaitingResponses = menu.WaitingResponses[:k+copy(menu.WaitingResponses[k:], menu.WaitingResponses[k+1:])] 128 | } else { 129 | menu.WaitingResponses = []*Command{} 130 | } 131 | } 132 | defer removeAt(k) 133 | 134 | if menu.TargetID == 0 && v.Action == "create" { 135 | //Assume we have a reply to action:create 136 | if reply.Result.TargetID != 0 { 137 | menu.TargetID = reply.Result.TargetID 138 | Log.Print("Received TargetID", "\nSetting Ready State") 139 | menu.Ready = true 140 | } 141 | for i, _ := range menu.CommandQueue { 142 | menu.CommandQueue[i].TargetID = menu.TargetID 143 | menu.Send(menu.CommandQueue[i]) 144 | } 145 | // Reinitialize empty command queue, and allow gc. 146 | menu.CommandQueue = []*Command{} 147 | return 148 | } 149 | 150 | if v.Action == "call" && v.Method == "set_application_menu" { 151 | Log.Print("Received reply to set_application_menu", "Setting Menu Displayed to True") 152 | menu.setDisplayed(true) 153 | } 154 | 155 | } 156 | } 157 | 158 | /* 159 | setDisplayed sets the menu Displayed attribute, this is a tracking attribute and has no effect on ThrustCore or UI Layer. Thus is is private. 160 | */ 161 | func (menu *Menu) setDisplayed(displayed bool) { 162 | menu.Displayed = displayed 163 | 164 | for _, child := range menu.Items { 165 | if child.IsSubMenu() { 166 | child.SubMenu.setDisplayed(displayed) 167 | } 168 | } 169 | } 170 | 171 | /* 172 | DispatchResponse dispatches CommandResponses to the proper delegates (Error, Event, Reply) 173 | */ 174 | func (menu *Menu) DispatchResponse(reply CommandResponse) { 175 | switch reply.Action { 176 | case "event": 177 | menu.HandleEvent(reply) 178 | case "reply": 179 | menu.HandleReply(reply) 180 | } 181 | 182 | // for _, child := range menu.Items { 183 | // if child.IsSubMenu() { 184 | // child.SubMenu.DispatchResponse(reply) 185 | // } 186 | // } 187 | } 188 | 189 | /* 190 | SendThread is a Thread for Sending Commands based on current state of the Menu. 191 | Some commands require other events in the system to have already taken place. 192 | This thread ensures that you can run almost any command at anytime, and have it take place in the correct order. This further insures that the underlying ThrustCore api does not crash, do to improper api knowledge. 193 | */ 194 | func (menu *Menu) SendThread() { 195 | //removeItemAt for []ChildCommand 196 | CCremoveItemAt := func(a []*ChildCommand, i int) []*ChildCommand { 197 | copy(a[i:], a[i+1:]) 198 | a[len(a)-1] = nil // or the zero value of T 199 | a = a[:len(a)-1] 200 | return a 201 | } 202 | //removeItemAt for []*Command 203 | CremoveItemAt := func(a []*Command, i int) []*Command { 204 | copy(a[i:], a[i+1:]) 205 | a[len(a)-1] = nil // or the zero value of T 206 | a = a[:len(a)-1] 207 | return a 208 | } 209 | go func() { 210 | for { 211 | if menu.Ready { 212 | menu.Sync.ReadyChan <- true 213 | } 214 | if menu.Displayed { 215 | menu.Sync.DisplayedChan <- true 216 | } 217 | for _, child := range menu.Items { 218 | if child.IsSubMenu() { 219 | if child.SubMenu.IsStable() { 220 | menu.Sync.ChildStableChan <- child.SubMenu.TargetID 221 | } 222 | } 223 | } 224 | if menu.IsTreeStable() { 225 | menu.Sync.TreeStableChan <- true 226 | } 227 | time.Sleep(time.Microsecond * 100) 228 | } 229 | }() 230 | 231 | go func() { 232 | for { 233 | select { 234 | case ready := <-menu.Sync.ReadyChan: 235 | if len(menu.Sync.ReadyQueue) == 0 || ready == false { 236 | break 237 | } 238 | command := menu.Sync.ReadyQueue[0] 239 | menu.Sync.ReadyQueue = CremoveItemAt(menu.Sync.ReadyQueue, 0) 240 | menu.Call(command) 241 | case displayed := <-menu.Sync.DisplayedChan: 242 | if len(menu.Sync.DisplayedQueue) == 0 || displayed == false { 243 | break 244 | } 245 | command := menu.Sync.DisplayedQueue[0] 246 | menu.Sync.DisplayedQueue = CremoveItemAt(menu.Sync.DisplayedQueue, 0) 247 | menu.WaitingResponses = append(menu.WaitingResponses, command) 248 | menu.Call(command) 249 | case stableChildId := <-menu.Sync.ChildStableChan: 250 | if len(menu.Sync.ChildStableQueue) == 0 { 251 | break 252 | } 253 | for i, childCommand := range menu.Sync.ChildStableQueue { 254 | if childCommand.Child.TargetID != stableChildId { 255 | continue 256 | } 257 | 258 | childCommand.Command.Args.MenuID = childCommand.Child.TargetID 259 | menu.Sync.ChildStableQueue = CCremoveItemAt(menu.Sync.ChildStableQueue, i) 260 | menu.Call(childCommand.Command) 261 | break 262 | 263 | } 264 | 265 | case treeStable := <-menu.Sync.TreeStableChan: 266 | if len(menu.Sync.TreeStableQueue) == 0 || treeStable == false { 267 | break 268 | } 269 | command := menu.Sync.TreeStableQueue[0] 270 | command.Args.MenuID = menu.TargetID 271 | menu.WaitingResponses = append(menu.WaitingResponses, command) 272 | menu.Sync.TreeStableQueue = CremoveItemAt(menu.Sync.TreeStableQueue, 0) 273 | menu.Call(command) 274 | } 275 | } 276 | }() 277 | } 278 | 279 | /* 280 | Send emits a Command over the Command SendChannel to be delivered to ThrustCore 281 | */ 282 | func (menu *Menu) Send(command *Command) { 283 | menu.SendChannel.Commands <- command 284 | } 285 | 286 | /* 287 | Call turns a Command into an action:call, there are two main types of Actions for outgoing commands, create/call. There may be more added later. 288 | */ 289 | func (menu *Menu) Call(command *Command) { 290 | command.Action = "call" 291 | command.TargetID = menu.TargetID 292 | 293 | if menu.Ready == false { 294 | menu.CommandQueue = append(menu.CommandQueue, command) 295 | return 296 | } 297 | menu.Send(command) 298 | } 299 | 300 | /* 301 | CallWhenReady queues up "Calls" to go out only when the Menu State is "Ready" 302 | */ 303 | func (menu *Menu) CallWhenReady(command *Command) { 304 | menu.WaitingResponses = append(menu.WaitingResponses, command) 305 | menu.Sync.ReadyQueue = append(menu.Sync.ReadyQueue, command) 306 | } 307 | 308 | /* 309 | CallWhenChildStable queues up "Calls" to go out only when the state of the Child is Stable. Stable means that the child is Ready and has no AwaitingResponses 310 | */ 311 | func (menu *Menu) CallWhenChildStable(command *Command, child *Menu) { 312 | menu.WaitingResponses = append(menu.WaitingResponses, command) 313 | menu.Sync.ChildStableQueue = append(menu.Sync.ChildStableQueue, &ChildCommand{ 314 | Command: command, 315 | Child: child, 316 | }) 317 | } 318 | 319 | /* 320 | CallWhenTreeStable queues up "Calls" to go out only when the state of the menu is Stable. Stable means that the menu is Ready and has no AwaitingResponses 321 | */ 322 | func (menu *Menu) CallWhenTreeStable(command *Command) { 323 | menu.Sync.TreeStableQueue = append(menu.Sync.TreeStableQueue, command) 324 | } 325 | 326 | /* 327 | CallWhenDisplayed queues up "Calls" to go out only when 328 | the menu is Displayed 329 | */ 330 | func (menu *Menu) CallWhenDisplayed(command *Command) { 331 | menu.Sync.DisplayedQueue = append(menu.Sync.DisplayedQueue, command) 332 | } 333 | 334 | /* 335 | AddItem adds a MenuItem to both the internal representation of menu and the external representation of menu 336 | */ 337 | func (menu *Menu) AddItem(commandID uint, label string) { 338 | command := Command{ 339 | Method: "add_item", 340 | Args: CommandArguments{ 341 | CommandID: commandID, 342 | Label: label, 343 | }, 344 | } 345 | menuItem := MenuItem{ 346 | CommandID: commandID, 347 | Label: label, 348 | Parent: menu, 349 | Type: "item", 350 | } 351 | menu.Items = append(menu.Items, &menuItem) 352 | 353 | menu.CallWhenReady(&command) 354 | } 355 | 356 | /* 357 | AddCheckItem adds a CheckItem to both the internal representation of menu and the external representation of menu 358 | */ 359 | func (menu *Menu) AddCheckItem(commandID uint, label string) { 360 | command := Command{ 361 | Method: "add_check_item", 362 | Args: CommandArguments{ 363 | CommandID: commandID, 364 | Label: label, 365 | }, 366 | } 367 | menuItem := MenuItem{ 368 | CommandID: commandID, 369 | Label: label, 370 | Type: "check", 371 | Parent: menu, 372 | } 373 | menu.Items = append(menu.Items, &menuItem) 374 | menu.CallWhenReady(&command) 375 | } 376 | 377 | /* 378 | AddRadioItem adds a RadioItem to both the internal representation of menu and the external representation of menu 379 | */ 380 | func (menu *Menu) AddRadioItem(commandID uint, label string, groupID uint) { 381 | command := Command{ 382 | Method: "add_radio_item", 383 | Args: CommandArguments{ 384 | CommandID: commandID, 385 | Label: label, 386 | GroupID: groupID, 387 | }, 388 | } 389 | menuItem := MenuItem{ 390 | CommandID: commandID, 391 | Label: label, 392 | GroupID: groupID, 393 | Parent: menu, 394 | Type: "radio", 395 | } 396 | menu.Items = append(menu.Items, &menuItem) 397 | menu.CallWhenReady(&command) 398 | } 399 | 400 | /* 401 | AddSubmenu adds a SubMenu to both the internal representation of menu and the external representation of menu 402 | */ 403 | func (menu *Menu) AddSubmenu(commandID uint, label string, child *Menu) { 404 | command := Command{ 405 | Method: "add_submenu", 406 | Args: CommandArguments{ 407 | CommandID: commandID, 408 | Label: label, 409 | }, 410 | } 411 | 412 | // Assign Bidirectional navigation elements i.e. DoublyLinkedLists 413 | child.Parent = menu 414 | menuItem := MenuItem{ 415 | CommandID: commandID, 416 | Label: label, 417 | SubMenu: child, 418 | Parent: menu, 419 | } 420 | menu.Items = append(menu.Items, &menuItem) 421 | 422 | menu.CallWhenChildStable(&command, child) 423 | } 424 | 425 | /* 426 | SetChecked Checks or Unchecks a CheckItem in the UI 427 | */ 428 | func (menu *Menu) SetChecked(commandID uint, checked bool) { 429 | command := Command{ 430 | Method: "set_checked", 431 | Args: CommandArguments{ 432 | CommandID: commandID, 433 | Value: checked, 434 | }, 435 | } 436 | 437 | for _, item := range menu.Items { 438 | if item.IsCommandID(commandID) { 439 | item.Checked = checked 440 | } 441 | } 442 | menu.CallWhenDisplayed(&command) 443 | } 444 | 445 | /* 446 | ToggleRadio Checks or Unchecks a RadioItem in the UI. 447 | It is used by the default event handler to turn on the expected item, 448 | and turn of other items in the group. 449 | */ 450 | func (menu *Menu) ToggleRadio(commandID, groupID uint, checked bool) { 451 | for _, item := range menu.RadioGroupAtGroupID(groupID) { 452 | command := Command{ 453 | Method: "set_checked", 454 | Args: CommandArguments{ 455 | CommandID: item.CommandID, 456 | Value: checked, 457 | }, 458 | } 459 | if item.IsCommandID(commandID) { 460 | item.Checked = checked 461 | } else { 462 | item.Checked = false 463 | command.Args.Value = false 464 | } 465 | menu.CallWhenDisplayed(&command) 466 | } 467 | 468 | } 469 | 470 | /* 471 | SetEnabled sets whether or not a given item can receive 472 | actions via the UI. 473 | */ 474 | func (menu *Menu) SetEnabled(commandID uint, enabled bool) { 475 | command := Command{ 476 | Method: "set_enabled", 477 | Args: CommandArguments{ 478 | CommandID: commandID, 479 | Value: enabled, 480 | }, 481 | } 482 | 483 | for _, item := range menu.Items { 484 | if item.IsCommandID(commandID) { 485 | item.Enabled = enabled 486 | } 487 | } 488 | menu.CallWhenDisplayed(&command) 489 | } 490 | 491 | /* 492 | SetVisible sets a boolean visibility attribute in the UI for 493 | a menu item with the given commandID. 494 | */ 495 | func (menu *Menu) SetVisible(commandID uint, visible bool) { 496 | command := Command{ 497 | Method: "set_visible", 498 | Args: CommandArguments{ 499 | CommandID: commandID, 500 | Value: visible, 501 | }, 502 | } 503 | 504 | for _, item := range menu.Items { 505 | if item.IsCommandID(commandID) { 506 | item.Visible = visible 507 | } 508 | } 509 | menu.CallWhenDisplayed(&command) 510 | } 511 | 512 | /* 513 | AddSeperator adds a Seperator Item to both the internal representation of menu and the external representation of menu. 514 | */ 515 | func (menu *Menu) AddSeparator() { 516 | command := Command{ 517 | Method: "add_separator", 518 | } 519 | menuItem := MenuItem{ 520 | Type: "separator", 521 | Parent: menu, 522 | } 523 | menu.Items = append(menu.Items, &menuItem) 524 | menu.CallWhenReady(&command) 525 | } 526 | 527 | /* 528 | SetApplicationMenu sets the Application Menu on system that support global application level menus such as x11, unity, darwin 529 | */ 530 | func (menu *Menu) SetApplicationMenu() { 531 | command := Command{ 532 | Method: "set_application_menu", 533 | Args: CommandArguments{ 534 | MenuID: menu.TargetID, 535 | }, 536 | } 537 | 538 | // Thread to wait for Stable Menu State 539 | menu.CallWhenTreeStable(&command) 540 | } 541 | 542 | /* 543 | Popup creates a popup menu on the given window 544 | */ 545 | func (menu *Menu) Popup(w *window.Window) { 546 | go func() { 547 | for { 548 | if w.TargetID != 0 { 549 | command := Command{ 550 | Method: "popup", 551 | Args: CommandArguments{ 552 | WindowID: w.TargetID, 553 | }, 554 | } 555 | 556 | // Thread to wait for Stable Menu State 557 | menu.CallWhenTreeStable(&command) 558 | return 559 | } 560 | time.Sleep(time.Microsecond * 10) 561 | } 562 | 563 | }() 564 | } 565 | 566 | /* 567 | IsStable returns the a boolean value indicating that the menu is Ready and has no WaitingResponses 568 | */ 569 | func (menu *Menu) IsStable() bool { 570 | return menu.Ready && len(menu.WaitingResponses) == 0 571 | } 572 | 573 | /* 574 | A Menu Tree is considered stable if and only if its children nodes report that they are stable. 575 | Function is recursive, so factor that in to performance 576 | */ 577 | func (menu *Menu) IsTreeStable() bool { 578 | if !menu.IsStable() { 579 | return false 580 | } 581 | for _, child := range menu.Items { 582 | if child.IsSubMenu() { 583 | if !child.SubMenu.IsTreeStable() { 584 | return false 585 | } 586 | } 587 | } 588 | 589 | return true 590 | } 591 | 592 | /* 593 | ItemAtCommandID recursively searches the Menu Tree for an item with the commandID. Returns the first found match. A proper menu should not reuse 594 | commandID's 595 | */ 596 | func (menu *Menu) ItemAtCommandID(commandID uint) *MenuItem { 597 | for _, item := range menu.Items { 598 | if item.IsCommandID(commandID) { 599 | return item 600 | } 601 | if item.IsSubMenu() { 602 | result := item.SubMenu.ItemAtCommandID(commandID) 603 | if result != nil { 604 | return result 605 | } 606 | } 607 | } 608 | return nil 609 | } 610 | 611 | func (menu *Menu) RegisterEventHandlerByCommandID(commandID uint, handler func(reply CommandResponse, item *MenuItem)) { 612 | menu.ReplyHandlers[commandID] = func(reply CommandResponse, item *MenuItem) { 613 | handler(reply, item) 614 | } 615 | } 616 | 617 | /* 618 | Find all menu items that belong to group identified by groupID 619 | Not recursive, as a group should be identified at the same level. 620 | Since it is not recursive you can theoretically reuse a groupID but problems 621 | could creep up elsewhere, so please use unique groupID for radio items 622 | */ 623 | func (menu *Menu) RadioGroupAtGroupID(groupID uint) []*MenuItem { 624 | group := []*MenuItem{} 625 | for _, item := range menu.Items { 626 | if item.IsGroupID(groupID) { 627 | group = append(group, item) 628 | } 629 | } 630 | 631 | return group 632 | } 633 | 634 | /* 635 | DEBUG Functions 636 | */ 637 | func (menu Menu) PrintRecursiveWaitingResponses() { 638 | Log.Print("Scanning Menu(", menu.TargetID, ")") 639 | if len(menu.WaitingResponses) > 0 { 640 | for _, v := range menu.WaitingResponses { 641 | Log.Print("Waiting for", v.ID, v.Action, v.Method) 642 | } 643 | } 644 | 645 | for _, child := range menu.Items { 646 | if child.IsSubMenu() { 647 | child.SubMenu.PrintRecursiveWaitingResponses() 648 | } 649 | } 650 | } 651 | -------------------------------------------------------------------------------- /examples/chat/server/chat.js.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var chat_js = []byte{ 4 | 0x76, 0x61, 0x72, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x6f, 0x66, 0x66, 0x5f, 5 | 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73, 0x20, 0x3d, 0x20, 0x30, 6 | 0x3b, 0x0a, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x62, 7 | 0x61, 0x63, 0x6b, 0x6f, 0x66, 0x66, 0x5f, 0x6f, 0x6b, 0x28, 0x29, 0x20, 8 | 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x6f, 0x66, 9 | 0x66, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73, 0x20, 0x3d, 10 | 0x20, 0x30, 0x3b, 0x0a, 0x7d, 0x0a, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 11 | 0x6f, 0x6e, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x6f, 0x66, 0x66, 0x5f, 0x63, 12 | 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x28, 0x29, 0x20, 0x7b, 0x0a, 0x20, 13 | 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x53, 0x45, 0x43, 0x4f, 0x4e, 14 | 0x44, 0x20, 0x3d, 0x20, 0x31, 0x30, 0x30, 0x30, 0x3b, 0x0a, 0x20, 0x20, 15 | 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x6f, 0x66, 16 | 0x66, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x20, 0x3d, 0x20, 17 | 0x30, 0x2e, 0x35, 0x20, 0x2a, 0x20, 0x53, 0x45, 0x43, 0x4f, 0x4e, 0x44, 18 | 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x62, 0x61, 19 | 0x63, 0x6b, 0x6f, 0x66, 0x66, 0x5f, 0x6d, 0x61, 0x78, 0x20, 0x3d, 0x20, 20 | 0x36, 0x30, 0x20, 0x2a, 0x20, 0x53, 0x45, 0x43, 0x4f, 0x4e, 0x44, 0x3b, 21 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x62, 0x61, 0x63, 22 | 0x6b, 0x6f, 0x66, 0x66, 0x5f, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 23 | 0x69, 0x65, 0x72, 0x20, 0x3d, 0x20, 0x31, 0x2e, 0x35, 0x3b, 0x0a, 0x20, 24 | 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x6f, 25 | 0x66, 0x66, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x69, 0x7a, 0x61, 26 | 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x3d, 0x20, 0x30, 0x2e, 0x35, 0x3b, 0x0a, 27 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x72, 0x65, 0x74, 28 | 0x72, 0x79, 0x20, 0x3d, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x6f, 0x66, 0x66, 29 | 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x20, 0x2a, 0x20, 0x4d, 30 | 0x61, 0x74, 0x68, 0x2e, 0x70, 0x6f, 0x77, 0x28, 0x62, 0x61, 0x63, 0x6b, 31 | 0x6f, 0x66, 0x66, 0x5f, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 32 | 0x65, 0x72, 0x2c, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x6f, 0x66, 0x66, 0x5f, 33 | 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73, 0x29, 0x3b, 0x0a, 0x20, 34 | 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x6c, 0x69, 0x6d, 0x69, 0x74, 35 | 0x65, 0x64, 0x20, 0x3d, 0x20, 0x4d, 0x61, 0x74, 0x68, 0x2e, 0x6d, 0x69, 36 | 0x6e, 0x28, 0x72, 0x65, 0x74, 0x72, 0x79, 0x2c, 0x20, 0x62, 0x61, 0x63, 37 | 0x6b, 0x6f, 0x66, 0x66, 0x5f, 0x6d, 0x61, 0x78, 0x29, 0x3b, 0x0a, 0x0a, 38 | 0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 0x20, 0x72, 0x61, 0x6e, 0x64, 0x6f, 39 | 0x6d, 0x6e, 0x65, 0x73, 0x73, 0x20, 0x69, 0x73, 0x20, 0x62, 0x61, 0x63, 40 | 0x6b, 0x6f, 0x66, 0x66, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x69, 41 | 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x77, 0x69, 0x64, 0x65, 0x20, 42 | 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x65, 0x64, 0x20, 0x61, 0x72, 0x6f, 43 | 0x75, 0x6e, 0x64, 0x20, 0x31, 0x2e, 0x30, 0x0a, 0x20, 0x20, 0x20, 0x20, 44 | 0x76, 0x61, 0x72, 0x20, 0x72, 0x61, 0x6e, 0x64, 0x20, 0x3d, 0x20, 0x4d, 45 | 0x61, 0x74, 0x68, 0x2e, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x28, 0x29, 46 | 0x20, 0x2a, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x6f, 0x66, 0x66, 0x5f, 0x72, 47 | 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 48 | 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x61, 0x6e, 0x64, 0x20, 0x3d, 49 | 0x20, 0x72, 0x61, 0x6e, 0x64, 0x20, 0x2b, 0x20, 0x31, 0x20, 0x2d, 0x20, 50 | 0x62, 0x61, 0x63, 0x6b, 0x6f, 0x66, 0x66, 0x5f, 0x72, 0x61, 0x6e, 0x64, 51 | 0x6f, 0x6d, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x32, 0x3b, 52 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x64, 0x65, 0x6c, 53 | 0x61, 0x79, 0x20, 0x3d, 0x20, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x64, 54 | 0x20, 0x2a, 0x20, 0x72, 0x61, 0x6e, 0x64, 0x3b, 0x0a, 0x20, 0x20, 0x20, 55 | 0x20, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x6c, 0x6f, 0x67, 56 | 0x28, 0x7b, 0x72, 0x65, 0x74, 0x72, 0x79, 0x3a, 0x20, 0x72, 0x65, 0x74, 57 | 0x72, 0x79, 0x2c, 0x20, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x64, 0x3a, 58 | 0x20, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x64, 0x2c, 0x20, 0x72, 0x61, 59 | 0x6e, 0x64, 0x3a, 0x20, 0x72, 0x61, 0x6e, 0x64, 0x2c, 0x20, 0x64, 0x65, 60 | 0x6c, 0x61, 0x79, 0x3a, 0x20, 0x64, 0x65, 0x6c, 0x61, 0x79, 0x7d, 0x29, 61 | 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x6f, 62 | 0x66, 0x66, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73, 0x20, 63 | 0x2b, 0x3d, 0x20, 0x31, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 64 | 0x72, 0x20, 0x70, 0x72, 0x65, 0x74, 0x74, 0x79, 0x20, 0x3d, 0x20, 0x64, 65 | 0x65, 0x6c, 0x61, 0x79, 0x2f, 0x31, 0x30, 0x30, 0x30, 0x3b, 0x0a, 0x20, 66 | 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x6c, 67 | 0x6f, 0x67, 0x28, 0x22, 0x52, 0x65, 0x74, 0x72, 0x79, 0x69, 0x6e, 0x67, 68 | 0x20, 0x77, 0x65, 0x62, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x20, 0x69, 69 | 0x6e, 0x20, 0x22, 0x20, 0x2b, 0x20, 0x70, 0x72, 0x65, 0x74, 0x74, 0x79, 70 | 0x2e, 0x74, 0x6f, 0x46, 0x69, 0x78, 0x65, 0x64, 0x28, 0x32, 0x29, 0x20, 71 | 0x2b, 0x20, 0x22, 0x73, 0x2e, 0x2e, 0x2e, 0x22, 0x29, 0x3b, 0x0a, 0x20, 72 | 0x20, 0x20, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x64, 0x65, 73 | 0x6c, 0x61, 0x79, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x76, 0x61, 0x72, 0x20, 74 | 0x77, 0x73, 0x3b, 0x0a, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 75 | 0x20, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x28, 0x29, 0x20, 0x7b, 76 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x64, 0x69, 0x76, 77 | 0x20, 0x3d, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 78 | 0x67, 0x65, 0x74, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, 79 | 0x49, 0x64, 0x28, 0x22, 0x6d, 0x73, 0x67, 0x22, 0x29, 0x3b, 0x0a, 0x20, 80 | 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x20, 81 | 0x3d, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 82 | 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 83 | 0x28, 0x22, 0x70, 0x22, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 84 | 0x69, 0x6e, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x43, 0x68, 85 | 0x69, 0x6c, 0x64, 0x28, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 86 | 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x65, 0x78, 0x74, 0x4e, 87 | 0x6f, 0x64, 0x65, 0x28, 0x22, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 88 | 0x69, 0x6e, 0x67, 0x2e, 0x2e, 0x2e, 0x22, 0x29, 0x29, 0x3b, 0x0a, 0x20, 89 | 0x20, 0x20, 0x20, 0x64, 0x69, 0x76, 0x2e, 0x61, 0x70, 0x70, 0x65, 0x6e, 90 | 0x64, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x28, 0x6c, 0x69, 0x6e, 0x65, 0x29, 91 | 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x77, 92 | 0x73, 0x20, 0x21, 0x3d, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x29, 0x20, 0x7b, 93 | 0x0a, 0x09, 0x77, 0x73, 0x2e, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x28, 0x29, 94 | 0x3b, 0x0a, 0x09, 0x77, 0x73, 0x20, 0x3d, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 95 | 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 96 | 0x76, 0x61, 0x72, 0x20, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x20, 0x3d, 97 | 0x20, 0x22, 0x77, 0x73, 0x22, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 98 | 0x66, 0x20, 0x28, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x2e, 0x6c, 0x6f, 99 | 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 100 | 0x63, 0x6f, 0x6c, 0x20, 0x3d, 0x3d, 0x20, 0x22, 0x68, 0x74, 0x74, 0x70, 101 | 0x73, 0x22, 0x29, 0x20, 0x7b, 0x0a, 0x09, 0x73, 0x63, 0x68, 0x65, 0x6d, 102 | 0x65, 0x20, 0x3d, 0x20, 0x22, 0x77, 0x73, 0x73, 0x22, 0x3b, 0x0a, 0x20, 103 | 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x77, 0x73, 0x20, 104 | 0x3d, 0x20, 0x6e, 0x65, 0x77, 0x20, 0x57, 0x65, 0x62, 0x53, 0x6f, 0x63, 105 | 0x6b, 0x65, 0x74, 0x28, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x20, 0x2b, 106 | 0x20, 0x22, 0x3a, 0x2f, 0x2f, 0x22, 0x20, 0x2b, 0x20, 0x77, 0x69, 0x6e, 107 | 0x64, 0x6f, 0x77, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 108 | 0x2e, 0x68, 0x6f, 0x73, 0x74, 0x20, 0x2b, 0x20, 0x22, 0x2f, 0x73, 0x6f, 109 | 0x63, 0x6b, 0x22, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 110 | 0x20, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x69, 0x6e, 111 | 0x67, 0x20, 0x74, 0x68, 0x65, 0x73, 0x65, 0x20, 0x65, 0x76, 0x65, 0x6e, 112 | 0x74, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x73, 0x20, 0x69, 113 | 0x73, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x72, 0x61, 0x63, 0x79, 0x20, 0x61, 114 | 0x73, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x20, 0x61, 0x73, 0x20, 0x77, 0x65, 115 | 0x20, 0x64, 0x6f, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 0x20, 0x69, 116 | 0x74, 0x20, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x20, 0x77, 0x65, 0x20, 117 | 0x6c, 0x65, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x78, 0x65, 0x63, 118 | 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x68, 0x65, 0x72, 0x65, 0x20, 0x72, 119 | 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 120 | 0x20, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x20, 0x6c, 0x6f, 0x6f, 0x70, 0x2e, 121 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 0x20, 0x68, 0x74, 0x74, 0x70, 122 | 0x73, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x77, 0x33, 0x2e, 0x6f, 123 | 0x72, 0x67, 0x2f, 0x42, 0x75, 0x67, 0x73, 0x2f, 0x50, 0x75, 0x62, 0x6c, 124 | 0x69, 0x63, 0x2f, 0x73, 0x68, 0x6f, 0x77, 0x5f, 0x62, 0x75, 0x67, 0x2e, 125 | 0x63, 0x67, 0x69, 0x3f, 0x69, 0x64, 0x3d, 0x31, 0x32, 0x35, 0x31, 0x30, 126 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x77, 0x73, 0x2e, 0x6f, 0x6e, 0x6f, 0x70, 127 | 0x65, 0x6e, 0x20, 0x3d, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 128 | 0x6e, 0x20, 0x28, 0x29, 0x20, 0x7b, 0x0a, 0x09, 0x62, 0x61, 0x63, 0x6b, 129 | 0x6f, 0x66, 0x66, 0x5f, 0x6f, 0x6b, 0x28, 0x29, 0x3b, 0x0a, 0x09, 0x76, 130 | 0x61, 0x72, 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x20, 0x3d, 0x20, 0x64, 0x6f, 131 | 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 132 | 0x65, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x28, 0x22, 0x70, 0x22, 133 | 0x29, 0x3b, 0x0a, 0x09, 0x6c, 0x69, 0x6e, 0x65, 0x2e, 0x61, 0x70, 0x70, 134 | 0x65, 0x6e, 0x64, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x28, 0x64, 0x6f, 0x63, 135 | 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 136 | 0x54, 0x65, 0x78, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x28, 0x22, 0x4f, 0x70, 137 | 0x65, 0x6e, 0x65, 0x64, 0x2e, 0x22, 0x29, 0x29, 0x3b, 0x0a, 0x09, 0x64, 138 | 0x69, 0x76, 0x2e, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x43, 0x68, 0x69, 139 | 0x6c, 0x64, 0x28, 0x6c, 0x69, 0x6e, 0x65, 0x29, 0x3b, 0x0a, 0x20, 0x20, 140 | 0x20, 0x20, 0x7d, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x77, 0x73, 0x2e, 141 | 0x6f, 0x6e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x20, 0x3d, 0x20, 142 | 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x28, 0x65, 0x29, 143 | 0x20, 0x7b, 0x0a, 0x09, 0x2f, 0x2f, 0x20, 0x54, 0x4f, 0x44, 0x4f, 0x20, 144 | 0x63, 0x61, 0x6e, 0x20, 0x74, 0x68, 0x72, 0x6f, 0x77, 0x20, 0x53, 0x79, 145 | 0x6e, 0x74, 0x61, 0x78, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x0a, 0x09, 0x76, 146 | 0x61, 0x72, 0x20, 0x72, 0x70, 0x63, 0x20, 0x3d, 0x20, 0x4a, 0x53, 0x4f, 147 | 0x4e, 0x2e, 0x70, 0x61, 0x72, 0x73, 0x65, 0x28, 0x65, 0x2e, 0x64, 0x61, 148 | 0x74, 0x61, 0x29, 0x3b, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 149 | 0x65, 0x2e, 0x6c, 0x6f, 0x67, 0x28, 0x22, 0x72, 0x70, 0x63, 0x20, 0x69, 150 | 0x6e, 0x3a, 0x22, 0x2c, 0x20, 0x72, 0x70, 0x63, 0x29, 0x3b, 0x0a, 0x0a, 151 | 0x09, 0x69, 0x66, 0x20, 0x28, 0x72, 0x70, 0x63, 0x2e, 0x66, 0x6e, 0x20, 152 | 0x3d, 0x3d, 0x3d, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 153 | 0x64, 0x0a, 0x09, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x7c, 0x20, 0x72, 0x70, 154 | 0x63, 0x2e, 0x66, 0x6e, 0x20, 0x3d, 0x3d, 0x20, 0x22, 0x22, 0x29, 0x20, 155 | 0x7b, 0x0a, 0x09, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 0x20, 0x72, 0x65, 156 | 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x3b, 0x20, 0x77, 0x65, 0x27, 157 | 0x72, 0x65, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x6c, 0x79, 158 | 0x20, 0x69, 0x67, 0x6e, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x20, 0x65, 0x72, 159 | 0x72, 0x6f, 0x72, 0x73, 0x0a, 0x09, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 160 | 0x74, 0x75, 0x72, 0x6e, 0x3b, 0x0a, 0x09, 0x7d, 0x0a, 0x0a, 0x09, 0x2f, 161 | 0x2f, 0x20, 0x6b, 0x6c, 0x75, 0x64, 0x67, 0x65, 0x20, 0x64, 0x69, 0x73, 162 | 0x70, 0x61, 0x74, 0x63, 0x68, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x6e, 0x6f, 163 | 0x77, 0x0a, 0x09, 0x69, 0x66, 0x20, 0x28, 0x72, 0x70, 0x63, 0x2e, 0x66, 164 | 0x6e, 0x20, 0x21, 0x3d, 0x20, 0x22, 0x43, 0x68, 0x61, 0x74, 0x2e, 0x4d, 165 | 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x29, 0x20, 0x7b, 0x0a, 0x09, 166 | 0x20, 0x20, 0x20, 0x20, 0x72, 0x70, 0x63, 0x2e, 0x65, 0x72, 0x72, 0x6f, 167 | 0x72, 0x20, 0x3d, 0x20, 0x7b, 0x72, 0x70, 0x63, 0x3a, 0x20, 0x22, 0x4e, 168 | 0x6f, 0x20, 0x73, 0x75, 0x63, 0x68, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 169 | 0x69, 0x6f, 0x6e, 0x2e, 0x22, 0x7d, 0x3b, 0x0a, 0x09, 0x20, 0x20, 0x20, 170 | 0x20, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x20, 0x72, 0x70, 0x63, 0x2e, 171 | 0x66, 0x6e, 0x3b, 0x0a, 0x09, 0x20, 0x20, 0x20, 0x20, 0x64, 0x65, 0x6c, 172 | 0x65, 0x74, 0x65, 0x20, 0x72, 0x70, 0x63, 0x2e, 0x61, 0x72, 0x67, 0x73, 173 | 0x3b, 0x0a, 0x09, 0x20, 0x20, 0x20, 0x20, 0x64, 0x65, 0x6c, 0x65, 0x74, 174 | 0x65, 0x20, 0x72, 0x70, 0x63, 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 175 | 0x3b, 0x0a, 0x09, 0x20, 0x20, 0x20, 0x20, 0x77, 0x73, 0x2e, 0x73, 0x65, 176 | 0x6e, 0x64, 0x28, 0x72, 0x70, 0x63, 0x29, 0x0a, 0x09, 0x20, 0x20, 0x20, 177 | 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x3b, 0x0a, 0x09, 0x7d, 0x0a, 178 | 0x0a, 0x09, 0x2f, 0x2f, 0x20, 0x54, 0x4f, 0x44, 0x4f, 0x20, 0x63, 0x61, 179 | 0x74, 0x63, 0x68, 0x20, 0x65, 0x78, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 180 | 0x6e, 0x73, 0x0a, 0x0a, 0x09, 0x76, 0x61, 0x72, 0x20, 0x6c, 0x69, 0x6e, 181 | 0x65, 0x20, 0x3d, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 182 | 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6c, 0x65, 0x6d, 0x65, 183 | 0x6e, 0x74, 0x28, 0x22, 0x70, 0x22, 0x29, 0x3b, 0x0a, 0x0a, 0x09, 0x76, 184 | 0x61, 0x72, 0x20, 0x74, 0x69, 0x6d, 0x65, 0x20, 0x3d, 0x20, 0x64, 0x6f, 185 | 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 186 | 0x65, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x28, 0x22, 0x73, 0x70, 187 | 0x61, 0x6e, 0x22, 0x29, 0x3b, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x2e, 188 | 0x73, 0x65, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 189 | 0x28, 0x22, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x22, 0x2c, 0x20, 0x22, 0x74, 190 | 0x69, 0x6d, 0x65, 0x22, 0x29, 0x3b, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 191 | 0x2e, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x43, 0x68, 0x69, 0x6c, 0x64, 192 | 0x28, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x72, 193 | 0x65, 0x61, 0x74, 0x65, 0x54, 0x65, 0x78, 0x74, 0x4e, 0x6f, 0x64, 0x65, 194 | 0x28, 0x72, 0x70, 0x63, 0x2e, 0x61, 0x72, 0x67, 0x73, 0x2e, 0x74, 0x69, 195 | 0x6d, 0x65, 0x29, 0x29, 0x3b, 0x0a, 0x09, 0x6c, 0x69, 0x6e, 0x65, 0x2e, 196 | 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x28, 197 | 0x74, 0x69, 0x6d, 0x65, 0x29, 0x3b, 0x0a, 0x0a, 0x09, 0x76, 0x61, 0x72, 198 | 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20, 0x3d, 0x20, 0x64, 0x6f, 0x63, 0x75, 199 | 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 200 | 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x28, 0x22, 0x73, 0x70, 0x61, 0x6e, 201 | 0x22, 0x29, 0x3b, 0x0a, 0x09, 0x66, 0x72, 0x6f, 0x6d, 0x2e, 0x73, 0x65, 202 | 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x28, 0x22, 203 | 0x63, 0x6c, 0x61, 0x73, 0x73, 0x22, 0x2c, 0x20, 0x22, 0x66, 0x72, 0x6f, 204 | 0x6d, 0x22, 0x29, 0x3b, 0x0a, 0x09, 0x66, 0x72, 0x6f, 0x6d, 0x2e, 0x61, 205 | 0x70, 0x70, 0x65, 0x6e, 0x64, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x28, 0x64, 206 | 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 207 | 0x74, 0x65, 0x54, 0x65, 0x78, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x28, 0x72, 208 | 0x70, 0x63, 0x2e, 0x61, 0x72, 0x67, 0x73, 0x2e, 0x66, 0x72, 0x6f, 0x6d, 209 | 0x29, 0x29, 0x3b, 0x0a, 0x09, 0x6c, 0x69, 0x6e, 0x65, 0x2e, 0x61, 0x70, 210 | 0x70, 0x65, 0x6e, 0x64, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x28, 0x66, 0x72, 211 | 0x6f, 0x6d, 0x29, 0x3b, 0x0a, 0x0a, 0x09, 0x76, 0x61, 0x72, 0x20, 0x74, 212 | 0x65, 0x78, 0x74, 0x20, 0x3d, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 213 | 0x6e, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6c, 0x65, 214 | 0x6d, 0x65, 0x6e, 0x74, 0x28, 0x22, 0x73, 0x70, 0x61, 0x6e, 0x22, 0x29, 215 | 0x3b, 0x0a, 0x09, 0x74, 0x65, 0x78, 0x74, 0x2e, 0x73, 0x65, 0x74, 0x41, 216 | 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x28, 0x22, 0x63, 0x6c, 217 | 0x61, 0x73, 0x73, 0x22, 0x2c, 0x20, 0x22, 0x6d, 0x65, 0x73, 0x73, 0x61, 218 | 0x67, 0x65, 0x22, 0x29, 0x3b, 0x0a, 0x09, 0x74, 0x65, 0x78, 0x74, 0x2e, 219 | 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x28, 220 | 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x72, 0x65, 221 | 0x61, 0x74, 0x65, 0x54, 0x65, 0x78, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x28, 222 | 0x72, 0x70, 0x63, 0x2e, 0x61, 0x72, 0x67, 0x73, 0x2e, 0x6d, 0x65, 0x73, 223 | 0x73, 0x61, 0x67, 0x65, 0x29, 0x29, 0x3b, 0x0a, 0x09, 0x6c, 0x69, 0x6e, 224 | 0x65, 0x2e, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x43, 0x68, 0x69, 0x6c, 225 | 0x64, 0x28, 0x74, 0x65, 0x78, 0x74, 0x29, 0x3b, 0x0a, 0x0a, 0x09, 0x64, 226 | 0x69, 0x76, 0x2e, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x43, 0x68, 0x69, 227 | 0x6c, 0x64, 0x28, 0x6c, 0x69, 0x6e, 0x65, 0x29, 0x3b, 0x0a, 0x0a, 0x09, 228 | 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x20, 0x72, 0x70, 0x63, 0x2e, 0x66, 229 | 0x6e, 0x3b, 0x0a, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x20, 0x72, 230 | 0x70, 0x63, 0x2e, 0x61, 0x72, 0x67, 0x73, 0x3b, 0x0a, 0x09, 0x72, 0x70, 231 | 0x63, 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x20, 0x3d, 0x20, 0x7b, 232 | 0x7d, 0x3b, 0x0a, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x20, 0x72, 233 | 0x70, 0x63, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x3b, 0x0a, 0x09, 0x63, 234 | 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x6c, 0x6f, 0x67, 0x28, 0x22, 235 | 0x72, 0x70, 0x63, 0x20, 0x6f, 0x75, 0x74, 0x3a, 0x22, 0x2c, 0x20, 0x72, 236 | 0x70, 0x63, 0x29, 0x3b, 0x0a, 0x09, 0x77, 0x73, 0x2e, 0x73, 0x65, 0x6e, 237 | 0x64, 0x28, 0x4a, 0x53, 0x4f, 0x4e, 0x2e, 0x73, 0x74, 0x72, 0x69, 0x6e, 238 | 0x67, 0x69, 0x66, 0x79, 0x28, 0x72, 0x70, 0x63, 0x29, 0x29, 0x3b, 0x0a, 239 | 0x20, 0x20, 0x20, 0x20, 0x7d, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x77, 240 | 0x73, 0x2e, 0x6f, 0x6e, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x20, 0x3d, 0x20, 241 | 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x28, 0x65, 0x29, 242 | 0x20, 0x7b, 0x0a, 0x09, 0x76, 0x61, 0x72, 0x20, 0x6c, 0x69, 0x6e, 0x65, 243 | 0x20, 0x3d, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 244 | 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 245 | 0x74, 0x28, 0x22, 0x70, 0x22, 0x29, 0x3b, 0x0a, 0x09, 0x6c, 0x69, 0x6e, 246 | 0x65, 0x2e, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x43, 0x68, 0x69, 0x6c, 247 | 0x64, 0x28, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 248 | 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x65, 0x78, 0x74, 0x4e, 0x6f, 0x64, 249 | 0x65, 0x28, 0x22, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x64, 0x2e, 0x22, 0x29, 250 | 0x29, 0x3b, 0x0a, 0x09, 0x64, 0x69, 0x76, 0x2e, 0x61, 0x70, 0x70, 0x65, 251 | 0x6e, 0x64, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x28, 0x6c, 0x69, 0x6e, 0x65, 252 | 0x29, 0x3b, 0x0a, 0x0a, 0x09, 0x2f, 0x2f, 0x20, 0x49, 0x66, 0x20, 0x74, 253 | 0x68, 0x69, 0x73, 0x20, 0x77, 0x65, 0x62, 0x73, 0x6f, 0x63, 0x6b, 0x65, 254 | 0x74, 0x20, 0x69, 0x73, 0x20, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x20, 0x74, 255 | 0x68, 0x65, 0x20, 0x6d, 0x61, 0x69, 0x6e, 0x20, 0x6f, 0x6e, 0x65, 0x2c, 256 | 0x20, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x3b, 0x0a, 257 | 0x09, 0x2f, 0x2f, 0x20, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x77, 0x69, 0x73, 258 | 0x65, 0x2c, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x69, 0x74, 0x20, 0x64, 0x69, 259 | 0x65, 0x2e, 0x20, 0x54, 0x68, 0x69, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x6f, 260 | 0x77, 0x73, 0x20, 0x65, 0x61, 0x73, 0x79, 0x20, 0x66, 0x69, 0x64, 0x64, 261 | 0x6c, 0x69, 0x6e, 0x67, 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20, 0x6a, 0x73, 262 | 0x0a, 0x09, 0x2f, 0x2f, 0x20, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 263 | 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x6a, 0x75, 0x73, 0x74, 0x20, 0x60, 264 | 0x60, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x28, 0x29, 0x60, 0x60, 265 | 0x2e, 0x20, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x20, 0x6f, 266 | 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 267 | 0x0a, 0x09, 0x2f, 0x2f, 0x20, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 268 | 0x73, 0x20, 0x67, 0x65, 0x74, 0x73, 0x20, 0x77, 0x65, 0x69, 0x72, 0x64, 269 | 0x2c, 0x20, 0x74, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x2e, 0x0a, 0x09, 0x69, 270 | 0x66, 0x20, 0x28, 0x74, 0x68, 0x69, 0x73, 0x20, 0x3d, 0x3d, 0x3d, 0x20, 271 | 0x77, 0x73, 0x29, 0x20, 0x7b, 0x0a, 0x09, 0x20, 0x20, 0x20, 0x20, 0x76, 272 | 0x61, 0x72, 0x20, 0x64, 0x65, 0x6c, 0x61, 0x79, 0x20, 0x3d, 0x20, 0x62, 273 | 0x61, 0x63, 0x6b, 0x6f, 0x66, 0x66, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x75, 274 | 0x74, 0x65, 0x28, 0x29, 0x3b, 0x0a, 0x09, 0x20, 0x20, 0x20, 0x20, 0x73, 275 | 0x65, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x28, 0x63, 0x6f, 276 | 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2c, 0x20, 0x64, 0x65, 0x6c, 0x61, 0x79, 277 | 0x29, 0x3b, 0x0a, 0x09, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x3b, 278 | 0x0a, 0x7d, 0x0a, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 279 | 0x69, 0x6e, 0x69, 0x74, 0x28, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 280 | 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x6d, 0x73, 281 | 0x67, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 282 | 0x65, 0x2e, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x28, 0x29, 0x3b, 0x0a, 0x20, 283 | 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x28, 0x29, 284 | 0x3b, 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 285 | 0x6f, 0x6e, 0x20, 0x73, 0x65, 0x6e, 0x64, 0x28, 0x29, 0x20, 0x7b, 0x0a, 286 | 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x72, 0x70, 0x63, 0x20, 287 | 0x3d, 0x20, 0x7b, 0x0a, 0x09, 0x2f, 0x2f, 0x20, 0x54, 0x4f, 0x44, 0x4f, 288 | 0x0a, 0x09, 0x69, 0x64, 0x3a, 0x20, 0x22, 0x30, 0x22, 0x2c, 0x0a, 0x09, 289 | 0x66, 0x6e, 0x3a, 0x20, 0x22, 0x43, 0x68, 0x61, 0x74, 0x2e, 0x4d, 0x65, 290 | 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x2c, 0x0a, 0x09, 0x61, 0x72, 0x67, 291 | 0x73, 0x3a, 0x20, 0x7b, 0x0a, 0x09, 0x20, 0x20, 0x20, 0x20, 0x66, 0x72, 292 | 0x6f, 0x6d, 0x3a, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 293 | 0x2e, 0x6d, 0x73, 0x67, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x6e, 0x61, 0x6d, 294 | 0x65, 0x2e, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2c, 0x0a, 0x09, 0x20, 0x20, 295 | 0x20, 0x20, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x3a, 0x20, 0x64, 296 | 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x6d, 0x73, 0x67, 0x66, 297 | 0x6f, 0x72, 0x6d, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 298 | 0x76, 0x61, 0x6c, 0x75, 0x65, 0x0a, 0x09, 0x7d, 0x0a, 0x20, 0x20, 0x20, 299 | 0x20, 0x7d, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6e, 0x73, 300 | 0x6f, 0x6c, 0x65, 0x2e, 0x6c, 0x6f, 0x67, 0x28, 0x22, 0x72, 0x70, 0x63, 301 | 0x20, 0x6f, 0x75, 0x74, 0x3a, 0x22, 0x2c, 0x20, 0x72, 0x70, 0x63, 0x29, 302 | 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x77, 0x73, 0x2e, 0x73, 0x65, 0x6e, 303 | 0x64, 0x28, 0x4a, 0x53, 0x4f, 0x4e, 0x2e, 0x73, 0x74, 0x72, 0x69, 0x6e, 304 | 0x67, 0x69, 0x66, 0x79, 0x28, 0x72, 0x70, 0x63, 0x29, 0x29, 0x3b, 0x0a, 305 | 0x20, 0x20, 0x20, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 306 | 0x2e, 0x6d, 0x73, 0x67, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x6d, 0x65, 0x73, 307 | 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x20, 0x3d, 308 | 0x20, 0x22, 0x22, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x74, 309 | 0x75, 0x72, 0x6e, 0x20, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x3b, 0x0a, 0x7d, 310 | 0x3b, 0x0a, 311 | } 312 | -------------------------------------------------------------------------------- /examples/jankybrowser/browser.html.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var browser_html = []byte{ 4 | 0x3c, 0x68, 0x74, 0x6d, 0x6c, 0x3e, 0x0a, 0x20, 0x20, 0x3c, 0x68, 0x65, 5 | 0x61, 0x64, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73, 0x74, 0x79, 6 | 0x6c, 0x65, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x68, 0x74, 7 | 0x6d, 0x6c, 0x2c, 0x20, 0x62, 0x6f, 0x64, 0x79, 0x2c, 0x0a, 0x20, 0x20, 8 | 0x20, 0x20, 0x20, 0x20, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x20, 0x7b, 9 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6c, 0x65, 10 | 0x61, 0x72, 0x3a, 0x20, 0x62, 0x6f, 0x74, 0x68, 0x3b, 0x0a, 0x20, 0x20, 11 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x72, 0x67, 0x69, 0x6e, 12 | 0x3a, 0x20, 0x30, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 13 | 0x20, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x3a, 0x20, 0x30, 0x3b, 14 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x77, 0x69, 0x64, 15 | 0x74, 0x68, 0x3a, 0x20, 0x31, 0x30, 0x30, 0x25, 0x3b, 0x0a, 0x20, 0x20, 16 | 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 17 | 0x20, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x20, 0x7b, 0x0a, 0x20, 0x20, 18 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 19 | 0x2d, 0x62, 0x6f, 0x74, 0x74, 0x6f, 0x6d, 0x3a, 0x20, 0x31, 0x70, 0x78, 20 | 0x20, 0x73, 0x6f, 0x6c, 0x69, 0x64, 0x20, 0x67, 0x72, 0x61, 0x79, 0x3b, 21 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x0a, 0x20, 0x20, 22 | 0x20, 0x20, 0x20, 0x20, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x20, 0x69, 23 | 0x6e, 0x70, 0x75, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 24 | 0x20, 0x20, 0x20, 0x6d, 0x61, 0x72, 0x67, 0x69, 0x6e, 0x3a, 0x20, 0x38, 25 | 0x70, 0x78, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 26 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x23, 0x6e, 0x61, 0x76, 0x20, 27 | 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6c, 28 | 0x6f, 0x61, 0x74, 0x3a, 0x20, 0x6c, 0x65, 0x66, 0x74, 0x3b, 0x0a, 0x20, 29 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 30 | 0x20, 0x23, 0x6e, 0x61, 0x76, 0x20, 0x2e, 0x69, 0x6d, 0x61, 0x67, 0x65, 31 | 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 32 | 0x6c, 0x6f, 0x61, 0x74, 0x3a, 0x20, 0x6c, 0x65, 0x66, 0x74, 0x3b, 0x0a, 33 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x0a, 0x20, 0x20, 0x20, 34 | 0x20, 0x20, 0x20, 0x23, 0x6e, 0x61, 0x76, 0x20, 0x2e, 0x66, 0x6f, 0x72, 35 | 0x77, 0x61, 0x72, 0x64, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 36 | 0x23, 0x6e, 0x61, 0x76, 0x20, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x77, 0x61, 37 | 0x72, 0x64, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 38 | 0x20, 0x66, 0x6f, 0x6e, 0x74, 0x2d, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 39 | 0x3a, 0x20, 0x62, 0x6f, 0x6c, 0x64, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 40 | 0x20, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x3a, 0x20, 0x6c, 41 | 0x65, 0x66, 0x74, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 42 | 0x20, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x3a, 0x20, 0x38, 0x70, 43 | 0x78, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70, 44 | 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x2d, 0x74, 0x6f, 0x70, 0x3a, 0x20, 45 | 0x31, 0x30, 0x70, 0x78, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 46 | 0x7d, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x23, 0x74, 0x61, 47 | 0x62, 0x73, 0x2d, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x2c, 0x20, 48 | 0x23, 0x74, 0x61, 0x62, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 49 | 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x72, 0x67, 0x69, 0x6e, 0x3a, 0x20, 50 | 0x30, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70, 51 | 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x3a, 0x20, 0x30, 0x3b, 0x0a, 0x20, 52 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 53 | 0x20, 0x20, 0x23, 0x74, 0x61, 0x62, 0x73, 0x2d, 0x77, 0x72, 0x61, 0x70, 54 | 0x70, 0x65, 0x72, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 55 | 0x20, 0x20, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3a, 0x20, 0x34, 0x30, 56 | 0x70, 0x78, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 57 | 0x6f, 0x76, 0x65, 0x72, 0x66, 0x6c, 0x6f, 0x77, 0x3a, 0x20, 0x68, 0x69, 58 | 0x64, 0x64, 0x65, 0x6e, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 59 | 0x7d, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2e, 0x74, 0x61, 60 | 0x62, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 61 | 0x20, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3a, 0x20, 0x31, 0x30, 0x30, 62 | 0x25, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6c, 63 | 0x69, 0x73, 0x74, 0x2d, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3a, 0x20, 0x6e, 64 | 0x6f, 0x6e, 0x65, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 65 | 0x20, 0x6d, 0x61, 0x72, 0x67, 0x69, 0x6e, 0x3a, 0x20, 0x30, 0x3b, 0x0a, 66 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70, 0x61, 0x64, 0x64, 67 | 0x69, 0x6e, 0x67, 0x3a, 0x20, 0x30, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 68 | 0x20, 0x20, 0x20, 0x20, 0x6f, 0x76, 0x65, 0x72, 0x66, 0x6c, 0x6f, 0x77, 69 | 0x3a, 0x20, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x3b, 0x0a, 0x20, 0x20, 70 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3a, 71 | 0x20, 0x31, 0x30, 0x30, 0x25, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 72 | 0x20, 0x7d, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2e, 0x74, 73 | 0x61, 0x62, 0x73, 0x20, 0x6c, 0x69, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 74 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x3a, 0x20, 75 | 0x6c, 0x65, 0x66, 0x74, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 76 | 0x20, 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x2d, 0x68, 0x65, 0x69, 0x67, 0x68, 77 | 0x74, 0x3a, 0x20, 0x30, 0x2e, 0x35, 0x65, 0x6d, 0x3b, 0x0a, 0x20, 0x20, 78 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 79 | 0x67, 0x3a, 0x20, 0x31, 0x35, 0x70, 0x78, 0x3b, 0x0a, 0x20, 0x20, 0x20, 80 | 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 81 | 0x2e, 0x74, 0x61, 0x62, 0x73, 0x20, 0x6c, 0x69, 0x2e, 0x61, 0x63, 0x74, 82 | 0x69, 0x76, 0x65, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 83 | 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 84 | 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x67, 0x72, 0x61, 0x79, 85 | 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x0a, 0x20, 86 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x69, 0x2e, 0x74, 0x61, 0x62, 0x20, 87 | 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x62, 0x6f, 88 | 0x72, 0x64, 0x65, 0x72, 0x3a, 0x20, 0x31, 0x70, 0x78, 0x20, 0x73, 0x6f, 89 | 0x6c, 0x69, 0x64, 0x20, 0x62, 0x6c, 0x61, 0x63, 0x6b, 0x3b, 0x0a, 0x20, 90 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 91 | 0x20, 0x20, 0x2e, 0x74, 0x61, 0x62, 0x20, 0x2e, 0x74, 0x61, 0x62, 0x2d, 92 | 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 93 | 0x20, 0x20, 0x20, 0x20, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x3a, 0x20, 94 | 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x3b, 0x0a, 0x20, 0x20, 0x20, 95 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x3a, 0x20, 96 | 0x72, 0x69, 0x67, 0x68, 0x74, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 97 | 0x20, 0x20, 0x20, 0x66, 0x6f, 0x6e, 0x74, 0x2d, 0x73, 0x69, 0x7a, 0x65, 98 | 0x3a, 0x20, 0x30, 0x2e, 0x35, 0x65, 0x6d, 0x3b, 0x0a, 0x20, 0x20, 0x20, 99 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x66, 0x74, 0x3a, 0x20, 0x35, 100 | 0x70, 0x78, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 101 | 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x2d, 0x6c, 0x65, 0x66, 0x74, 102 | 0x3a, 0x20, 0x33, 0x70, 0x78, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 103 | 0x20, 0x20, 0x20, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 104 | 0x20, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x3b, 0x0a, 0x20, 105 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x74, 0x6f, 0x70, 0x3a, 0x20, 106 | 0x2d, 0x38, 0x70, 0x78, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 107 | 0x7d, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x23, 0x6e, 0x65, 108 | 0x77, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 109 | 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x3a, 0x20, 0x73, 0x6f, 0x6c, 0x69, 110 | 0x64, 0x20, 0x31, 0x70, 0x78, 0x20, 0x67, 0x72, 0x61, 0x79, 0x3b, 0x0a, 111 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x75, 0x72, 0x73, 112 | 0x6f, 0x72, 0x3a, 0x20, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x3b, 113 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 114 | 0x61, 0x74, 0x3a, 0x20, 0x72, 0x69, 0x67, 0x68, 0x74, 0x3b, 0x0a, 0x20, 115 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70, 0x6f, 0x73, 0x69, 0x74, 116 | 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x61, 0x62, 0x73, 0x6f, 0x6c, 0x75, 0x74, 117 | 0x65, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x72, 118 | 0x69, 0x67, 0x68, 0x74, 0x3a, 0x20, 0x31, 0x30, 0x70, 0x78, 0x3b, 0x0a, 119 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x74, 0x65, 0x78, 0x74, 120 | 0x2d, 0x61, 0x6c, 0x69, 0x67, 0x6e, 0x3a, 0x20, 0x63, 0x65, 0x6e, 0x74, 121 | 0x65, 0x72, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 122 | 0x74, 0x6f, 0x70, 0x3a, 0x20, 0x31, 0x30, 0x70, 0x78, 0x3b, 0x0a, 0x20, 123 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 124 | 0x3a, 0x20, 0x32, 0x30, 0x70, 0x78, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 125 | 0x20, 0x20, 0x20, 0x20, 0x2d, 0x77, 0x65, 0x62, 0x6b, 0x69, 0x74, 0x2d, 126 | 0x75, 0x73, 0x65, 0x72, 0x2d, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x3a, 127 | 0x20, 0x6e, 0x6f, 0x6e, 0x65, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 128 | 0x20, 0x20, 0x20, 0x7a, 0x2d, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x3a, 0x20, 129 | 0x31, 0x30, 0x30, 0x30, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 130 | 0x7d, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x23, 0x70, 0x61, 131 | 0x67, 0x65, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x20, 0x7b, 132 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x68, 0x65, 0x69, 133 | 0x67, 0x68, 0x74, 0x3a, 0x20, 0x31, 0x30, 0x30, 0x25, 0x3b, 0x0a, 0x20, 134 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 135 | 0x3a, 0x20, 0x31, 0x30, 0x30, 0x25, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 136 | 0x20, 0x20, 0x7d, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x23, 137 | 0x70, 0x61, 0x67, 0x65, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 138 | 0x20, 0x2e, 0x70, 0x61, 0x67, 0x65, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 139 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3a, 140 | 0x20, 0x31, 0x30, 0x30, 0x25, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 141 | 0x20, 0x20, 0x20, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 142 | 0x6e, 0x3a, 0x20, 0x6c, 0x65, 0x66, 0x74, 0x20, 0x30, 0x2e, 0x37, 0x73, 143 | 0x20, 0x65, 0x61, 0x73, 0x65, 0x2d, 0x69, 0x6e, 0x2d, 0x6f, 0x75, 0x74, 144 | 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70, 0x6f, 145 | 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x61, 0x62, 0x73, 0x6f, 146 | 0x6c, 0x75, 0x74, 0x65, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 147 | 0x20, 0x20, 0x6c, 0x65, 0x66, 0x74, 0x3a, 0x20, 0x2d, 0x31, 0x30, 0x30, 148 | 0x25, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x77, 149 | 0x69, 0x64, 0x74, 0x68, 0x3a, 0x20, 0x31, 0x30, 0x30, 0x25, 0x3b, 0x0a, 150 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x0a, 0x20, 0x20, 0x20, 151 | 0x20, 0x20, 0x20, 0x23, 0x70, 0x61, 0x67, 0x65, 0x2d, 0x63, 0x6f, 0x6e, 152 | 0x74, 0x65, 0x6e, 0x74, 0x20, 0x2e, 0x70, 0x61, 0x67, 0x65, 0x2e, 0x61, 153 | 0x63, 0x74, 0x69, 0x76, 0x65, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 154 | 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x66, 0x74, 0x3a, 0x20, 0x30, 0x3b, 155 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 156 | 0x20, 0x3c, 0x2f, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3e, 0x0a, 0x20, 0x20, 157 | 0x20, 0x20, 0x3c, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x20, 0x74, 0x79, 158 | 0x70, 0x65, 0x3d, 0x22, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x6a, 0x61, 0x76, 159 | 0x61, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x22, 0x3e, 0x0a, 0x0a, 0x66, 160 | 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x75, 0x72, 0x6c, 0x54, 161 | 0x6f, 0x54, 0x74, 0x69, 0x6c, 0x65, 0x28, 0x75, 0x72, 0x6c, 0x29, 0x20, 162 | 0x7b, 0x0a, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x61, 0x20, 0x3d, 0x20, 163 | 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x72, 0x65, 164 | 0x61, 0x74, 0x65, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x28, 0x22, 165 | 0x61, 0x22, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x61, 0x2e, 0x68, 0x72, 0x65, 166 | 0x66, 0x20, 0x3d, 0x20, 0x75, 0x72, 0x6c, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 167 | 0x76, 0x61, 0x72, 0x20, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 168 | 0x20, 0x3d, 0x20, 0x61, 0x2e, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 169 | 0x65, 0x3b, 0x0a, 0x20, 0x20, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 170 | 0x65, 0x20, 0x3d, 0x20, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 171 | 0x2e, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x28, 0x22, 0x2e, 0x22, 0x29, 0x2e, 172 | 0x73, 0x6c, 0x69, 0x63, 0x65, 0x28, 0x2d, 0x32, 0x29, 0x2e, 0x6a, 0x6f, 173 | 0x69, 0x6e, 0x28, 0x22, 0x2e, 0x22, 0x29, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 174 | 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x68, 0x6f, 0x73, 0x74, 0x6e, 175 | 0x61, 0x6d, 0x65, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 0x76, 0x61, 0x72, 0x20, 176 | 0x6c, 0x6f, 0x63, 0x3b, 0x0a, 0x76, 0x61, 0x72, 0x20, 0x74, 0x61, 0x62, 177 | 0x73, 0x3b, 0x0a, 0x76, 0x61, 0x72, 0x20, 0x6e, 0x65, 0x77, 0x54, 0x61, 178 | 0x62, 0x3b, 0x0a, 0x76, 0x61, 0x72, 0x20, 0x70, 0x61, 0x67, 0x65, 0x43, 179 | 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x3b, 0x0a, 0x76, 0x61, 0x72, 0x20, 180 | 0x6e, 0x61, 0x76, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x3b, 0x0a, 181 | 0x76, 0x61, 0x72, 0x20, 0x6e, 0x61, 0x76, 0x42, 0x61, 0x63, 0x6b, 0x77, 182 | 0x61, 0x72, 0x64, 0x3b, 0x0a, 0x0a, 0x76, 0x61, 0x72, 0x20, 0x61, 0x63, 183 | 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x67, 0x65, 0x3b, 0x0a, 0x76, 0x61, 184 | 0x72, 0x20, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x54, 0x61, 0x62, 0x3b, 185 | 0x0a, 0x76, 0x61, 0x72, 0x20, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x57, 186 | 0x56, 0x3b, 0x0a, 0x0a, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 187 | 0x20, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x28, 0x70, 0x61, 188 | 0x67, 0x65, 0x2c, 0x20, 0x74, 0x61, 0x62, 0x29, 0x20, 0x7b, 0x0a, 0x20, 189 | 0x20, 0x69, 0x66, 0x28, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 190 | 0x67, 0x65, 0x20, 0x26, 0x26, 0x20, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 191 | 0x50, 0x61, 0x67, 0x65, 0x20, 0x21, 0x3d, 0x3d, 0x20, 0x70, 0x61, 0x67, 192 | 0x65, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x61, 0x63, 0x74, 193 | 0x69, 0x76, 0x65, 0x50, 0x61, 0x67, 0x65, 0x2e, 0x63, 0x6c, 0x61, 0x73, 194 | 0x73, 0x4c, 0x69, 0x73, 0x74, 0x2e, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 195 | 0x28, 0x22, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x22, 0x29, 0x3b, 0x0a, 196 | 0x20, 0x20, 0x7d, 0x0a, 0x0a, 0x20, 0x20, 0x69, 0x66, 0x28, 0x61, 0x63, 197 | 0x74, 0x69, 0x76, 0x65, 0x54, 0x61, 0x62, 0x20, 0x26, 0x26, 0x20, 0x61, 198 | 0x63, 0x74, 0x69, 0x76, 0x65, 0x54, 0x61, 0x62, 0x20, 0x21, 0x3d, 0x3d, 199 | 0x20, 0x74, 0x61, 0x62, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 200 | 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x54, 0x61, 0x62, 0x2e, 0x63, 0x6c, 201 | 0x61, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x2e, 0x72, 0x65, 0x6d, 0x6f, 202 | 0x76, 0x65, 0x28, 0x22, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x22, 0x29, 203 | 0x3b, 0x0a, 0x20, 0x20, 0x7d, 0x0a, 0x0a, 0x20, 0x20, 0x61, 0x63, 0x74, 204 | 0x69, 0x76, 0x65, 0x50, 0x61, 0x67, 0x65, 0x20, 0x3d, 0x20, 0x70, 0x61, 205 | 0x67, 0x65, 0x3b, 0x0a, 0x20, 0x20, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 206 | 0x54, 0x61, 0x62, 0x20, 0x3d, 0x20, 0x74, 0x61, 0x62, 0x3b, 0x0a, 0x20, 207 | 0x20, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x57, 0x56, 0x20, 0x3d, 0x20, 208 | 0x70, 0x61, 0x67, 0x65, 0x2e, 0x66, 0x69, 0x72, 0x73, 0x74, 0x45, 0x6c, 209 | 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x3b, 0x0a, 210 | 0x0a, 0x20, 0x20, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x67, 211 | 0x65, 0x2e, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x2e, 212 | 0x61, 0x64, 0x64, 0x28, 0x22, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x22, 213 | 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x54, 214 | 0x61, 0x62, 0x2e, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 215 | 0x2e, 0x61, 0x64, 0x64, 0x28, 0x22, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 216 | 0x22, 0x29, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x6c, 0x6f, 0x63, 0x2e, 0x76, 217 | 0x61, 0x6c, 0x75, 0x65, 0x20, 0x3d, 0x20, 0x61, 0x63, 0x74, 0x69, 0x76, 218 | 0x65, 0x57, 0x56, 0x2e, 0x73, 0x72, 0x63, 0x3b, 0x0a, 0x7d, 0x0a, 0x0a, 219 | 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x2e, 0x6f, 0x6e, 0x6c, 0x6f, 0x61, 220 | 0x64, 0x20, 0x3d, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 221 | 0x28, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x24, 222 | 0x20, 0x3d, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 223 | 0x71, 0x75, 0x65, 0x72, 0x79, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 224 | 0x72, 0x2e, 0x62, 0x69, 0x6e, 0x64, 0x28, 0x64, 0x6f, 0x63, 0x75, 0x6d, 225 | 0x65, 0x6e, 0x74, 0x29, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x6c, 0x6f, 0x63, 226 | 0x20, 0x3d, 0x20, 0x24, 0x28, 0x22, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 227 | 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x22, 0x29, 0x3b, 0x0a, 0x20, 0x20, 228 | 0x74, 0x61, 0x62, 0x73, 0x20, 0x3d, 0x20, 0x24, 0x28, 0x22, 0x75, 0x6c, 229 | 0x2e, 0x74, 0x61, 0x62, 0x73, 0x22, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x6e, 230 | 0x65, 0x77, 0x54, 0x61, 0x62, 0x20, 0x3d, 0x20, 0x24, 0x28, 0x22, 0x23, 231 | 0x6e, 0x65, 0x77, 0x22, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x70, 0x61, 0x67, 232 | 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x20, 0x3d, 0x20, 0x24, 233 | 0x28, 0x22, 0x23, 0x70, 0x61, 0x67, 0x65, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 234 | 0x65, 0x6e, 0x74, 0x22, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x6e, 0x61, 0x76, 235 | 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x20, 0x3d, 0x20, 0x24, 0x28, 236 | 0x22, 0x23, 0x6e, 0x61, 0x76, 0x20, 0x2e, 0x66, 0x6f, 0x72, 0x77, 0x61, 237 | 0x72, 0x64, 0x22, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x6e, 0x61, 0x76, 0x42, 238 | 0x61, 0x63, 0x6b, 0x77, 0x61, 0x72, 0x64, 0x20, 0x3d, 0x20, 0x24, 0x28, 239 | 0x22, 0x23, 0x6e, 0x61, 0x76, 0x20, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x77, 240 | 0x61, 0x72, 0x64, 0x22, 0x29, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x66, 0x75, 241 | 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6f, 0x70, 0x65, 0x6e, 0x4e, 242 | 0x65, 0x77, 0x54, 0x61, 0x62, 0x28, 0x75, 0x72, 0x6c, 0x29, 0x20, 0x7b, 243 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x70, 0x61, 0x67, 244 | 0x65, 0x20, 0x3d, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 245 | 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6c, 0x65, 0x6d, 0x65, 246 | 0x6e, 0x74, 0x28, 0x22, 0x64, 0x69, 0x76, 0x22, 0x29, 0x3b, 0x0a, 0x20, 247 | 0x20, 0x20, 0x20, 0x70, 0x61, 0x67, 0x65, 0x2e, 0x63, 0x6c, 0x61, 0x73, 248 | 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x20, 0x3d, 0x20, 0x22, 0x70, 0x61, 0x67, 249 | 0x65, 0x22, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 250 | 0x20, 0x77, 0x65, 0x62, 0x76, 0x69, 0x65, 0x77, 0x20, 0x3d, 0x20, 0x64, 251 | 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 252 | 0x74, 0x65, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x28, 0x22, 0x77, 253 | 0x65, 0x62, 0x76, 0x69, 0x65, 0x77, 0x22, 0x29, 0x3b, 0x0a, 0x20, 0x20, 254 | 0x20, 0x20, 0x70, 0x61, 0x67, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x65, 0x6e, 255 | 0x64, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x28, 0x77, 0x65, 0x62, 0x76, 0x69, 256 | 0x65, 0x77, 0x29, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x70, 0x61, 257 | 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2e, 0x61, 0x70, 258 | 0x70, 0x65, 0x6e, 0x64, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x28, 0x70, 0x61, 259 | 0x67, 0x65, 0x29, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 260 | 0x28, 0x75, 0x72, 0x6c, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 261 | 0x20, 0x20, 0x77, 0x65, 0x62, 0x76, 0x69, 0x65, 0x77, 0x2e, 0x73, 0x72, 262 | 0x63, 0x20, 0x3d, 0x20, 0x75, 0x72, 0x6c, 0x3b, 0x0a, 0x20, 0x20, 0x20, 263 | 0x20, 0x7d, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 264 | 0x74, 0x61, 0x62, 0x20, 0x3d, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 265 | 0x6e, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6c, 0x65, 266 | 0x6d, 0x65, 0x6e, 0x74, 0x28, 0x22, 0x6c, 0x69, 0x22, 0x29, 0x3b, 0x0a, 267 | 0x20, 0x20, 0x20, 0x20, 0x74, 0x61, 0x62, 0x2e, 0x63, 0x6c, 0x61, 0x73, 268 | 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x20, 0x3d, 0x20, 0x22, 0x74, 0x61, 0x62, 269 | 0x22, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 270 | 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x20, 0x3d, 0x20, 0x64, 0x6f, 0x63, 0x75, 271 | 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 272 | 0x65, 0x78, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x28, 0x75, 0x72, 0x6c, 0x54, 273 | 0x6f, 0x54, 0x74, 0x69, 0x6c, 0x65, 0x28, 0x75, 0x72, 0x6c, 0x29, 0x29, 274 | 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74, 0x61, 0x62, 0x2e, 0x61, 0x70, 275 | 0x70, 0x65, 0x6e, 0x64, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x28, 0x6c, 0x61, 276 | 0x62, 0x65, 0x6c, 0x29, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 277 | 0x61, 0x72, 0x20, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x20, 0x3d, 0x20, 0x64, 278 | 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 279 | 0x74, 0x65, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x28, 0x22, 0x64, 280 | 0x69, 0x76, 0x22, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6c, 281 | 0x6f, 0x73, 0x65, 0x2e, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x4e, 0x61, 0x6d, 282 | 0x65, 0x20, 0x3d, 0x20, 0x22, 0x74, 0x61, 0x62, 0x2d, 0x63, 0x6c, 0x6f, 283 | 0x73, 0x65, 0x22, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6c, 0x6f, 284 | 0x73, 0x65, 0x2e, 0x74, 0x65, 0x78, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 285 | 0x6e, 0x74, 0x20, 0x3d, 0x20, 0x22, 0x58, 0x22, 0x3b, 0x0a, 0x20, 0x20, 286 | 0x20, 0x20, 0x74, 0x61, 0x62, 0x2e, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 287 | 0x43, 0x68, 0x69, 0x6c, 0x64, 0x28, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x29, 288 | 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74, 0x61, 0x62, 0x73, 0x2e, 289 | 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x28, 290 | 0x74, 0x61, 0x62, 0x29, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x66, 291 | 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x68, 0x61, 0x6e, 0x64, 292 | 0x6c, 0x65, 0x54, 0x61, 0x62, 0x43, 0x6c, 0x69, 0x63, 0x6b, 0x28, 0x29, 293 | 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6e, 294 | 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x6c, 0x6f, 0x67, 0x28, 0x22, 0x63, 0x6c, 295 | 0x69, 0x63, 0x6b, 0x69, 0x6e, 0x67, 0x20, 0x74, 0x61, 0x62, 0x22, 0x29, 296 | 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x61, 0x63, 0x74, 0x69, 297 | 0x76, 0x61, 0x74, 0x65, 0x28, 0x70, 0x61, 0x67, 0x65, 0x2c, 0x20, 0x74, 298 | 0x61, 0x62, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x0a, 299 | 0x20, 0x20, 0x20, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 300 | 0x20, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x43, 0x6c, 0x6f, 0x73, 0x65, 301 | 0x43, 0x6c, 0x69, 0x63, 0x6b, 0x28, 0x65, 0x76, 0x74, 0x29, 0x20, 0x7b, 302 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 303 | 0x6c, 0x65, 0x2e, 0x6c, 0x6f, 0x67, 0x28, 0x22, 0x63, 0x6c, 0x6f, 0x73, 304 | 0x69, 0x6e, 0x67, 0x20, 0x74, 0x61, 0x62, 0x22, 0x29, 0x3b, 0x0a, 0x20, 305 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x74, 0x61, 0x62, 0x2e, 0x72, 0x65, 0x6d, 306 | 0x6f, 0x76, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x69, 0x73, 0x74, 307 | 0x65, 0x6e, 0x65, 0x72, 0x28, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x54, 308 | 0x61, 0x62, 0x43, 0x6c, 0x69, 0x63, 0x6b, 0x29, 0x3b, 0x0a, 0x20, 0x20, 309 | 0x20, 0x20, 0x20, 0x20, 0x77, 0x65, 0x62, 0x76, 0x69, 0x65, 0x77, 0x2e, 310 | 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4c, 311 | 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x28, 0x68, 0x61, 0x6e, 0x64, 312 | 0x6c, 0x65, 0x54, 0x61, 0x62, 0x43, 0x6c, 0x69, 0x63, 0x6b, 0x29, 0x3b, 313 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6c, 0x6f, 0x73, 0x65, 314 | 0x2e, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 315 | 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x28, 0x68, 0x61, 0x6e, 316 | 0x64, 0x6c, 0x65, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6c, 0x69, 0x63, 317 | 0x6b, 0x29, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x74, 318 | 0x61, 0x62, 0x2e, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x4e, 0x6f, 0x64, 319 | 0x65, 0x2e, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x68, 0x69, 0x6c, 320 | 0x64, 0x28, 0x74, 0x61, 0x62, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 321 | 0x20, 0x20, 0x77, 0x65, 0x62, 0x76, 0x69, 0x65, 0x77, 0x2e, 0x70, 0x61, 322 | 0x72, 0x65, 0x6e, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x2e, 0x72, 0x65, 0x6d, 323 | 0x6f, 0x76, 0x65, 0x43, 0x68, 0x69, 0x6c, 0x64, 0x28, 0x77, 0x65, 0x62, 324 | 0x76, 0x69, 0x65, 0x77, 0x29, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 325 | 0x20, 0x20, 0x65, 0x76, 0x74, 0x2e, 0x73, 0x74, 0x6f, 0x70, 0x50, 0x72, 326 | 0x6f, 0x70, 0x61, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x29, 0x3b, 327 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 328 | 0x74, 0x61, 0x62, 0x2e, 0x61, 0x64, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 329 | 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x28, 0x22, 0x63, 0x6c, 330 | 0x69, 0x63, 0x6b, 0x22, 0x2c, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 331 | 0x54, 0x61, 0x62, 0x43, 0x6c, 0x69, 0x63, 0x6b, 0x2c, 0x20, 0x66, 0x61, 332 | 0x6c, 0x73, 0x65, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6c, 333 | 0x6f, 0x73, 0x65, 0x2e, 0x61, 0x64, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 334 | 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x28, 0x22, 0x63, 0x6c, 335 | 0x69, 0x63, 0x6b, 0x22, 0x2c, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 336 | 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6c, 0x69, 0x63, 0x6b, 0x2c, 0x20, 337 | 0x74, 0x72, 0x75, 0x65, 0x29, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 338 | 0x77, 0x65, 0x62, 0x76, 0x69, 0x65, 0x77, 0x2e, 0x61, 0x64, 0x64, 0x45, 339 | 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 340 | 0x28, 0x22, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x2d, 0x73, 0x65, 0x74, 0x22, 341 | 0x2c, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x65, 342 | 0x76, 0x74, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 343 | 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x2e, 0x74, 0x65, 0x78, 0x74, 0x43, 0x6f, 344 | 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x20, 0x3d, 0x20, 0x65, 0x76, 0x74, 0x2e, 345 | 0x74, 0x69, 0x74, 0x6c, 0x65, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 346 | 0x2c, 0x20, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x29, 0x3b, 0x0a, 0x0a, 0x20, 347 | 0x20, 0x20, 0x20, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x28, 348 | 0x70, 0x61, 0x67, 0x65, 0x2c, 0x20, 0x74, 0x61, 0x62, 0x29, 0x3b, 0x0a, 349 | 0x20, 0x20, 0x7d, 0x0a, 0x0a, 0x20, 0x20, 0x6c, 0x6f, 0x63, 0x2e, 0x61, 350 | 0x64, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x65, 351 | 0x6e, 0x65, 0x72, 0x28, 0x22, 0x6b, 0x65, 0x79, 0x70, 0x72, 0x65, 0x73, 352 | 0x73, 0x22, 0x2c, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 353 | 0x28, 0x65, 0x76, 0x74, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 354 | 0x69, 0x66, 0x28, 0x65, 0x76, 0x74, 0x2e, 0x6b, 0x65, 0x79, 0x43, 0x6f, 355 | 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x3d, 0x20, 0x31, 0x33, 0x29, 0x20, 0x7b, 356 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 357 | 0x6c, 0x65, 0x2e, 0x6c, 0x6f, 0x67, 0x28, 0x22, 0x63, 0x68, 0x61, 0x6e, 358 | 0x67, 0x69, 0x6e, 0x67, 0x20, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 359 | 0x6e, 0x20, 0x22, 0x20, 0x2b, 0x20, 0x6c, 0x6f, 0x63, 0x2e, 0x76, 0x61, 360 | 0x6c, 0x75, 0x65, 0x29, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 361 | 0x20, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x57, 0x56, 0x2e, 0x73, 0x72, 362 | 0x63, 0x20, 0x3d, 0x20, 0x6c, 0x6f, 0x63, 0x2e, 0x76, 0x61, 0x6c, 0x75, 363 | 0x65, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x7d, 364 | 0x2c, 0x20, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x29, 0x3b, 0x0a, 0x0a, 0x20, 365 | 0x20, 0x6e, 0x65, 0x77, 0x54, 0x61, 0x62, 0x2e, 0x61, 0x64, 0x64, 0x45, 366 | 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 367 | 0x28, 0x22, 0x63, 0x6c, 0x69, 0x63, 0x6b, 0x22, 0x2c, 0x20, 0x66, 0x75, 368 | 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x29, 0x20, 0x7b, 0x0a, 0x20, 369 | 0x20, 0x20, 0x20, 0x6f, 0x70, 0x65, 0x6e, 0x4e, 0x65, 0x77, 0x54, 0x61, 370 | 0x62, 0x28, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 371 | 0x77, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 372 | 0x22, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x7d, 0x2c, 0x20, 0x66, 0x61, 0x6c, 373 | 0x73, 0x65, 0x29, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x6e, 0x61, 0x76, 0x42, 374 | 0x61, 0x63, 0x6b, 0x77, 0x61, 0x72, 0x64, 0x2e, 0x61, 0x64, 0x64, 0x45, 375 | 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 376 | 0x28, 0x22, 0x63, 0x6c, 0x69, 0x63, 0x6b, 0x22, 0x2c, 0x20, 0x66, 0x75, 377 | 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x29, 0x20, 0x7b, 0x0a, 0x20, 378 | 0x20, 0x20, 0x20, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x57, 0x56, 0x2e, 379 | 0x62, 0x61, 0x63, 0x6b, 0x28, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x7d, 0x29, 380 | 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x6e, 0x61, 0x76, 0x46, 0x6f, 0x72, 0x77, 381 | 0x61, 0x72, 0x64, 0x2e, 0x61, 0x64, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 382 | 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x28, 0x22, 0x63, 0x6c, 383 | 0x69, 0x63, 0x6b, 0x22, 0x2c, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 384 | 0x6f, 0x6e, 0x28, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x61, 385 | 0x63, 0x74, 0x69, 0x76, 0x65, 0x57, 0x56, 0x2e, 0x66, 0x6f, 0x72, 0x77, 386 | 0x61, 0x72, 0x64, 0x28, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x7d, 0x29, 0x3b, 387 | 0x0a, 0x0a, 0x20, 0x20, 0x6f, 0x70, 0x65, 0x6e, 0x4e, 0x65, 0x77, 0x54, 388 | 0x61, 0x62, 0x28, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 389 | 0x77, 0x77, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 390 | 0x6d, 0x22, 0x29, 0x3b, 0x0a, 0x7d, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x20, 391 | 0x20, 0x3c, 0x2f, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3e, 0x0a, 0x20, 392 | 0x20, 0x3c, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x3e, 0x0a, 0x20, 0x20, 0x3c, 393 | 0x62, 0x6f, 0x64, 0x79, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x68, 394 | 0x65, 0x61, 0x64, 0x65, 0x72, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 395 | 0x20, 0x3c, 0x64, 0x69, 0x76, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x6e, 0x61, 396 | 0x76, 0x22, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 397 | 0x69, 0x6d, 0x67, 0x20, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3d, 0x22, 398 | 0x35, 0x30, 0x70, 0x78, 0x22, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3d, 399 | 0x22, 0x31, 0x32, 0x30, 0x70, 0x78, 0x22, 0x20, 0x63, 0x6c, 0x61, 0x73, 400 | 0x73, 0x3d, 0x22, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x22, 0x20, 0x73, 0x72, 401 | 0x63, 0x3d, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x69, 0x2e, 402 | 0x69, 0x6d, 0x67, 0x75, 0x72, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x44, 0x77, 403 | 0x46, 0x4b, 0x49, 0x30, 0x4a, 0x2e, 0x70, 0x6e, 0x67, 0x22, 0x2f, 0x3e, 404 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x64, 0x69, 405 | 0x76, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x62, 0x61, 0x63, 406 | 0x6b, 0x77, 0x61, 0x72, 0x64, 0x22, 0x3e, 0x26, 0x6c, 0x74, 0x3b, 0x3c, 407 | 0x2f, 0x64, 0x69, 0x76, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 408 | 0x20, 0x20, 0x3c, 0x64, 0x69, 0x76, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 409 | 0x3d, 0x22, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x22, 0x3e, 0x26, 410 | 0x67, 0x74, 0x3b, 0x3c, 0x2f, 0x64, 0x69, 0x76, 0x3e, 0x0a, 0x20, 0x20, 411 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x69, 0x6e, 0x70, 0x75, 0x74, 412 | 0x20, 0x69, 0x64, 0x3d, 0x22, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 413 | 0x6e, 0x22, 0x20, 0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 414 | 0x3c, 0x2f, 0x64, 0x69, 0x76, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 415 | 0x20, 0x3c, 0x64, 0x69, 0x76, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x74, 0x61, 416 | 0x62, 0x73, 0x2d, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x22, 0x3e, 417 | 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x75, 0x6c, 418 | 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x74, 0x61, 0x62, 0x73, 419 | 0x22, 0x3e, 0x3c, 0x2f, 0x75, 0x6c, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 420 | 0x20, 0x20, 0x20, 0x20, 0x3c, 0x64, 0x69, 0x76, 0x20, 0x69, 0x64, 0x3d, 421 | 0x22, 0x6e, 0x65, 0x77, 0x22, 0x3e, 0x2b, 0x3c, 0x2f, 0x64, 0x69, 0x76, 422 | 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x2f, 0x64, 0x69, 423 | 0x76, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x2f, 0x68, 0x65, 0x61, 424 | 0x64, 0x65, 0x72, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x64, 0x69, 425 | 0x76, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x70, 0x61, 0x67, 0x65, 0x2d, 0x63, 426 | 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x22, 0x3e, 0x3c, 0x2f, 0x64, 0x69, 427 | 0x76, 0x3e, 0x0a, 0x20, 0x20, 0x3c, 0x2f, 0x62, 0x6f, 0x64, 0x79, 0x3e, 428 | 0x0a, 0x3c, 0x2f, 0x68, 0x74, 0x6d, 0x6c, 0x3e, 0x0a, 429 | } 430 | --------------------------------------------------------------------------------