[
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI Pipeline\non: [push, pull_request]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v1\n      - name: Install Chrome\n        run: |\n          sudo apt-get update\n          sudo apt-get install -yqq google-chrome-stable\n      - name: Install Go\n        uses: actions/setup-go@v2\n        with:\n          go-version: '1.16'\n      - name: Run tests\n        run: go test -v -race ./...\n      - name: Build examples\n        env:\n          CGO_ENABLED: 0\n        run: |\n          go build -o example-hello ./examples/hello\n          go build -o example-stopwatch ./examples/stopwatch\n          go build -o example-counter ./examples/counter\n"
  },
  {
    "path": ".gitignore",
    "content": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\nexample/Example.app\n\n# Test binary, build with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n## JetBrains Idea and Goland\n/.idea/**/*.*\n/.idea/\\$CACHE_FILE\\$\n/.idea/dataSources/\n!/.idea/inspectionProfiles/Project_Default.xml\n!/.idea/dictionaries/*.xml\n!/.idea/go.xml\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Serge Zaitsev\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Lorca\n\n[![Build Status](https://img.shields.io/github/workflow/status/zserge/lorca/CI%20Pipeline)](https://github.com/zserge/lorca)\n[![GoDoc](https://godoc.org/github.com/zserge/lorca?status.svg)](https://godoc.org/github.com/zserge/lorca)\n[![Go Report Card](https://goreportcard.com/badge/github.com/zserge/lorca)](https://goreportcard.com/report/github.com/zserge/lorca)\n\n<div>\n<img align=\"left\" src=\"https://raw.githubusercontent.com/zserge/lorca/master/lorca.png\" alt=\"Lorca\" width=\"128px\" height=\"128px\" />\n<br/>\n<p>\n\tA very small library to build modern HTML5 desktop apps in Go. It uses Chrome\n\tbrowser as a UI layer. Unlike Electron it doesn't bundle Chrome into the app\n\tpackage, but rather reuses the one that is already installed. Lorca\n\testablishes a connection to the browser window and allows calling Go code\n\tfrom the UI and manipulating UI from Go in a seamless manner.\n</p>\n<br/>\n</div>\n\n\n## Features\n\n* Pure Go library (no cgo) with a very simple API\n* Small application size (normally 5-10MB)\n* Best of both worlds - the whole power of HTML/CSS to make your UI look\n\tgood, combined with Go performance and ease of development\n* Expose Go functions/methods and call them from JavaScript\n* Call arbitrary JavaScript code from Go\n* Asynchronous flow between UI and main app in both languages (async/await and Goroutines)\n* Supports loading web UI from the local web server or via data URL\n* Supports testing your app with the UI in the headless mode\n* Supports multiple app windows\n* Supports packaging and branding (e.g. custom app icons). Packaging for all\n\tthree OS can be done on a single machine using GOOS and GOARCH variables.\n\nAlso, limitations by design:\n\n* Requires Chrome/Chromium >= 70 to be installed.\n* No control over the Chrome window yet (e.g. you can't remove border, make it\n\ttransparent, control position or size).\n* No window menu (tray menus and native OS dialogs are still possible via\n\t3rd-party libraries)\n\nIf you want to have more control of the browser window - consider using\n[webview](https://github.com/zserge/webview) library with a similar API, so\nmigration would be smooth.\n\n## Example\n\n```go\nui, _ := lorca.New(\"\", \"\", 480, 320)\ndefer ui.Close()\n\n// Bind Go function to be available in JS. Go function may be long-running and\n// blocking - in JS it's represented with a Promise.\nui.Bind(\"add\", func(a, b int) int { return a + b })\n\n// Call JS function from Go. Functions may be asynchronous, i.e. return promises\nn := ui.Eval(`Math.random()`).Float()\nfmt.Println(n)\n\n// Call JS that calls Go and so on and so on...\nm := ui.Eval(`add(2, 3)`).Int()\nfmt.Println(m)\n\n// Wait for the browser window to be closed\n<-ui.Done()\n```\n\n<p align=\"center\"><img src=\"examples/counter/counter.gif\" /></p>\n\nAlso, see [examples](examples) for more details about binding functions and packaging binaries.\n\n## Hello World\n\nHere are the steps to run the hello world example.\n\n```\ncd examples/counter\ngo get\ngo run ./\n```\n\n## How it works\n\nUnder the hood Lorca uses [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) to instrument on a Chrome instance. First Lorca tries to locate your installed Chrome, starts a remote debugging instance binding to an ephemeral port and reads from `stderr` for the actual WebSocket endpoint. Then Lorca opens a new client connection to the WebSocket server, and instruments Chrome by sending JSON messages of Chrome DevTools Protocol methods via WebSocket. JavaScript functions are evaluated in Chrome, while Go functions actually run in Go runtime and returned values are sent to Chrome.\n\n## What's in a name?\n\n> There is kind of a legend, that before his execution Garcia Lorca have seen a\n> sunrise over the heads of the soldiers and he said \"And yet, the sun rises...\".\n> Probably it was the beginning of a poem. (J. Brodsky)\n\nLorca is an anagram of [Carlo](https://github.com/GoogleChromeLabs/carlo/), a\nproject with a similar goal for Node.js.\n\n## License\n\nCode is distributed under MIT license, feel free to use it in your proprietary\nprojects as well.\n\n"
  },
  {
    "path": "chrome.go",
    "content": "package lorca\n\nimport (\n\t\"bufio\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"golang.org/x/net/websocket\"\n)\n\ntype h = map[string]interface{}\n\n// Result is a struct for the resulting value of the JS expression or an error.\ntype result struct {\n\tValue json.RawMessage\n\tErr   error\n}\n\ntype bindingFunc func(args []json.RawMessage) (interface{}, error)\n\n// Msg is a struct for incoming messages (results and async events)\ntype msg struct {\n\tID     int             `json:\"id\"`\n\tResult json.RawMessage `json:\"result\"`\n\tError  json.RawMessage `json:\"error\"`\n\tMethod string          `json:\"method\"`\n\tParams json.RawMessage `json:\"params\"`\n}\n\ntype chrome struct {\n\tsync.Mutex\n\tcmd      *exec.Cmd\n\tws       *websocket.Conn\n\tid       int32\n\ttarget   string\n\tsession  string\n\twindow   int\n\tpending  map[int]chan result\n\tbindings map[string]bindingFunc\n}\n\nfunc newChromeWithArgs(chromeBinary string, args ...string) (*chrome, error) {\n\t// The first two IDs are used internally during the initialization\n\tc := &chrome{\n\t\tid:       2,\n\t\tpending:  map[int]chan result{},\n\t\tbindings: map[string]bindingFunc{},\n\t}\n\n\t// Start chrome process\n\tc.cmd = exec.Command(chromeBinary, args...)\n\tpipe, err := c.cmd.StderrPipe()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := c.cmd.Start(); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Wait for websocket address to be printed to stderr\n\tre := regexp.MustCompile(`^DevTools listening on (ws://.*?)\\r?\\n$`)\n\tm, err := readUntilMatch(pipe, re)\n\tif err != nil {\n\t\tc.kill()\n\t\treturn nil, err\n\t}\n\twsURL := m[1]\n\n\t// Open a websocket\n\tc.ws, err = websocket.Dial(wsURL, \"\", \"http://127.0.0.1\")\n\tif err != nil {\n\t\tc.kill()\n\t\treturn nil, err\n\t}\n\n\t// Find target and initialize session\n\tc.target, err = c.findTarget()\n\tif err != nil {\n\t\tc.kill()\n\t\treturn nil, err\n\t}\n\n\tc.session, err = c.startSession(c.target)\n\tif err != nil {\n\t\tc.kill()\n\t\treturn nil, err\n\t}\n\tgo c.readLoop()\n\tfor method, args := range map[string]h{\n\t\t\"Page.enable\":          nil,\n\t\t\"Target.setAutoAttach\": {\"autoAttach\": true, \"waitForDebuggerOnStart\": false},\n\t\t\"Network.enable\":       nil,\n\t\t\"Runtime.enable\":       nil,\n\t\t\"Security.enable\":      nil,\n\t\t\"Performance.enable\":   nil,\n\t\t\"Log.enable\":           nil,\n\t} {\n\t\tif _, err := c.send(method, args); err != nil {\n\t\t\tc.kill()\n\t\t\tc.cmd.Wait()\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif !contains(args, \"--headless\") {\n\t\twin, err := c.getWindowForTarget(c.target)\n\t\tif err != nil {\n\t\t\tc.kill()\n\t\t\treturn nil, err\n\t\t}\n\t\tc.window = win.WindowID\n\t}\n\n\treturn c, nil\n}\n\nfunc (c *chrome) findTarget() (string, error) {\n\terr := websocket.JSON.Send(c.ws, h{\n\t\t\"id\": 0, \"method\": \"Target.setDiscoverTargets\", \"params\": h{\"discover\": true},\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfor {\n\t\tm := msg{}\n\t\tif err = websocket.JSON.Receive(c.ws, &m); err != nil {\n\t\t\treturn \"\", err\n\t\t} else if m.Method == \"Target.targetCreated\" {\n\t\t\ttarget := struct {\n\t\t\t\tTargetInfo struct {\n\t\t\t\t\tType string `json:\"type\"`\n\t\t\t\t\tID   string `json:\"targetId\"`\n\t\t\t\t} `json:\"targetInfo\"`\n\t\t\t}{}\n\t\t\tif err := json.Unmarshal(m.Params, &target); err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t} else if target.TargetInfo.Type == \"page\" {\n\t\t\t\treturn target.TargetInfo.ID, nil\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (c *chrome) startSession(target string) (string, error) {\n\terr := websocket.JSON.Send(c.ws, h{\n\t\t\"id\": 1, \"method\": \"Target.attachToTarget\", \"params\": h{\"targetId\": target},\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfor {\n\t\tm := msg{}\n\t\tif err = websocket.JSON.Receive(c.ws, &m); err != nil {\n\t\t\treturn \"\", err\n\t\t} else if m.ID == 1 {\n\t\t\tif m.Error != nil {\n\t\t\t\treturn \"\", errors.New(\"Target error: \" + string(m.Error))\n\t\t\t}\n\t\t\tsession := struct {\n\t\t\t\tID string `json:\"sessionId\"`\n\t\t\t}{}\n\t\t\tif err := json.Unmarshal(m.Result, &session); err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\treturn session.ID, nil\n\t\t}\n\t}\n}\n\n// WindowState defines the state of the Chrome window, possible values are\n// \"normal\", \"maximized\", \"minimized\" and \"fullscreen\".\ntype WindowState string\n\nconst (\n\t// WindowStateNormal defines a normal state of the browser window\n\tWindowStateNormal WindowState = \"normal\"\n\t// WindowStateMaximized defines a maximized state of the browser window\n\tWindowStateMaximized WindowState = \"maximized\"\n\t// WindowStateMinimized defines a minimized state of the browser window\n\tWindowStateMinimized WindowState = \"minimized\"\n\t// WindowStateFullscreen defines a fullscreen state of the browser window\n\tWindowStateFullscreen WindowState = \"fullscreen\"\n)\n\n// Bounds defines settable window properties.\ntype Bounds struct {\n\tLeft        int         `json:\"left\"`\n\tTop         int         `json:\"top\"`\n\tWidth       int         `json:\"width\"`\n\tHeight      int         `json:\"height\"`\n\tWindowState WindowState `json:\"windowState\"`\n}\n\ntype windowTargetMessage struct {\n\tWindowID int    `json:\"windowId\"`\n\tBounds   Bounds `json:\"bounds\"`\n}\n\nfunc (c *chrome) getWindowForTarget(target string) (windowTargetMessage, error) {\n\tvar m windowTargetMessage\n\tmsg, err := c.send(\"Browser.getWindowForTarget\", h{\"targetId\": target})\n\tif err != nil {\n\t\treturn m, err\n\t}\n\terr = json.Unmarshal(msg, &m)\n\treturn m, err\n}\n\ntype targetMessageTemplate struct {\n\tID     int    `json:\"id\"`\n\tMethod string `json:\"method\"`\n\tParams struct {\n\t\tName    string `json:\"name\"`\n\t\tPayload string `json:\"payload\"`\n\t\tID      int    `json:\"executionContextId\"`\n\t\tArgs    []struct {\n\t\t\tType  string      `json:\"type\"`\n\t\t\tValue interface{} `json:\"value\"`\n\t\t} `json:\"args\"`\n\t} `json:\"params\"`\n\tError struct {\n\t\tMessage string `json:\"message\"`\n\t} `json:\"error\"`\n\tResult json.RawMessage `json:\"result\"`\n}\n\ntype targetMessage struct {\n\ttargetMessageTemplate\n\tResult struct {\n\t\tResult struct {\n\t\t\tType        string          `json:\"type\"`\n\t\t\tSubtype     string          `json:\"subtype\"`\n\t\t\tDescription string          `json:\"description\"`\n\t\t\tValue       json.RawMessage `json:\"value\"`\n\t\t\tObjectID    string          `json:\"objectId\"`\n\t\t} `json:\"result\"`\n\t\tException struct {\n\t\t\tException struct {\n\t\t\t\tValue json.RawMessage `json:\"value\"`\n\t\t\t} `json:\"exception\"`\n\t\t} `json:\"exceptionDetails\"`\n\t} `json:\"result\"`\n}\n\nfunc (c *chrome) readLoop() {\n\tfor {\n\t\tm := msg{}\n\t\tif err := websocket.JSON.Receive(c.ws, &m); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tif m.Method == \"Target.receivedMessageFromTarget\" {\n\t\t\tparams := struct {\n\t\t\t\tSessionID string `json:\"sessionId\"`\n\t\t\t\tMessage   string `json:\"message\"`\n\t\t\t}{}\n\t\t\tjson.Unmarshal(m.Params, &params)\n\t\t\tif params.SessionID != c.session {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tres := targetMessage{}\n\t\t\tjson.Unmarshal([]byte(params.Message), &res)\n\n\t\t\tif res.ID == 0 && res.Method == \"Runtime.consoleAPICalled\" || res.Method == \"Runtime.exceptionThrown\" {\n\t\t\t\tlog.Println(params.Message)\n\t\t\t} else if res.ID == 0 && res.Method == \"Runtime.bindingCalled\" {\n\t\t\t\tpayload := struct {\n\t\t\t\t\tName string            `json:\"name\"`\n\t\t\t\t\tSeq  int               `json:\"seq\"`\n\t\t\t\t\tArgs []json.RawMessage `json:\"args\"`\n\t\t\t\t}{}\n\t\t\t\tjson.Unmarshal([]byte(res.Params.Payload), &payload)\n\n\t\t\t\tc.Lock()\n\t\t\t\tbinding, ok := c.bindings[res.Params.Name]\n\t\t\t\tc.Unlock()\n\t\t\t\tif ok {\n\t\t\t\t\tjsString := func(v interface{}) string { b, _ := json.Marshal(v); return string(b) }\n\t\t\t\t\tgo func() {\n\t\t\t\t\t\tresult, error := \"\", `\"\"`\n\t\t\t\t\t\tif r, err := binding(payload.Args); err != nil {\n\t\t\t\t\t\t\terror = jsString(err.Error())\n\t\t\t\t\t\t} else if b, err := json.Marshal(r); err != nil {\n\t\t\t\t\t\t\terror = jsString(err.Error())\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tresult = string(b)\n\t\t\t\t\t\t}\n\t\t\t\t\t\texpr := fmt.Sprintf(`\n\t\t\t\t\t\t\tif (%[4]s) {\n\t\t\t\t\t\t\t\twindow['%[1]s']['errors'].get(%[2]d)(%[4]s);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\twindow['%[1]s']['callbacks'].get(%[2]d)(%[3]s);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\twindow['%[1]s']['callbacks'].delete(%[2]d);\n\t\t\t\t\t\t\twindow['%[1]s']['errors'].delete(%[2]d);\n\t\t\t\t\t\t\t`, payload.Name, payload.Seq, result, error)\n\t\t\t\t\t\tc.send(\"Runtime.evaluate\", h{\"expression\": expr, \"contextId\": res.Params.ID})\n\t\t\t\t\t}()\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tc.Lock()\n\t\t\tresc, ok := c.pending[res.ID]\n\t\t\tdelete(c.pending, res.ID)\n\t\t\tc.Unlock()\n\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif res.Error.Message != \"\" {\n\t\t\t\tresc <- result{Err: errors.New(res.Error.Message)}\n\t\t\t} else if res.Result.Exception.Exception.Value != nil {\n\t\t\t\tresc <- result{Err: errors.New(string(res.Result.Exception.Exception.Value))}\n\t\t\t} else if res.Result.Result.Type == \"object\" && res.Result.Result.Subtype == \"error\" {\n\t\t\t\tresc <- result{Err: errors.New(res.Result.Result.Description)}\n\t\t\t} else if res.Result.Result.Type != \"\" {\n\t\t\t\tresc <- result{Value: res.Result.Result.Value}\n\t\t\t} else {\n\t\t\t\tres := targetMessageTemplate{}\n\t\t\t\tjson.Unmarshal([]byte(params.Message), &res)\n\t\t\t\tresc <- result{Value: res.Result}\n\t\t\t}\n\t\t} else if m.Method == \"Target.targetDestroyed\" {\n\t\t\tparams := struct {\n\t\t\t\tTargetID string `json:\"targetId\"`\n\t\t\t}{}\n\t\t\tjson.Unmarshal(m.Params, &params)\n\t\t\tif params.TargetID == c.target {\n\t\t\t\tc.kill()\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (c *chrome) send(method string, params h) (json.RawMessage, error) {\n\tid := atomic.AddInt32(&c.id, 1)\n\tb, err := json.Marshal(h{\"id\": int(id), \"method\": method, \"params\": params})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresc := make(chan result)\n\tc.Lock()\n\tc.pending[int(id)] = resc\n\tc.Unlock()\n\n\tif err := websocket.JSON.Send(c.ws, h{\n\t\t\"id\":     int(id),\n\t\t\"method\": \"Target.sendMessageToTarget\",\n\t\t\"params\": h{\"message\": string(b), \"sessionId\": c.session},\n\t}); err != nil {\n\t\treturn nil, err\n\t}\n\tres := <-resc\n\treturn res.Value, res.Err\n}\n\nfunc (c *chrome) load(url string) error {\n\t_, err := c.send(\"Page.navigate\", h{\"url\": url})\n\treturn err\n}\n\nfunc (c *chrome) eval(expr string) (json.RawMessage, error) {\n\treturn c.send(\"Runtime.evaluate\", h{\"expression\": expr, \"awaitPromise\": true, \"returnByValue\": true})\n}\n\nfunc (c *chrome) bind(name string, f bindingFunc) error {\n\tc.Lock()\n\t// check if binding already exists\n\t_, exists := c.bindings[name]\n\n\tc.bindings[name] = f\n\tc.Unlock()\n\n\tif exists {\n\t\t// Just replace callback and return, as the binding was already added to js\n\t\t// and adding it again would break it.\n\t\treturn nil\n\t}\n\n\tif _, err := c.send(\"Runtime.addBinding\", h{\"name\": name}); err != nil {\n\t\treturn err\n\t}\n\tscript := fmt.Sprintf(`(() => {\n\tconst bindingName = '%s';\n\tconst binding = window[bindingName];\n\twindow[bindingName] = async (...args) => {\n\t\tconst me = window[bindingName];\n\t\tlet errors = me['errors'];\n\t\tlet callbacks = me['callbacks'];\n\t\tif (!callbacks) {\n\t\t\tcallbacks = new Map();\n\t\t\tme['callbacks'] = callbacks;\n\t\t}\n\t\tif (!errors) {\n\t\t\terrors = new Map();\n\t\t\tme['errors'] = errors;\n\t\t}\n\t\tconst seq = (me['lastSeq'] || 0) + 1;\n\t\tme['lastSeq'] = seq;\n\t\tconst promise = new Promise((resolve, reject) => {\n\t\t\tcallbacks.set(seq, resolve);\n\t\t\terrors.set(seq, reject);\n\t\t});\n\t\tbinding(JSON.stringify({name: bindingName, seq, args}));\n\t\treturn promise;\n\t}})();\n\t`, name)\n\t_, err := c.send(\"Page.addScriptToEvaluateOnNewDocument\", h{\"source\": script})\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = c.eval(script)\n\treturn err\n}\n\nfunc (c *chrome) setBounds(b Bounds) error {\n\tif b.WindowState == \"\" {\n\t\tb.WindowState = WindowStateNormal\n\t}\n\tparam := h{\"windowId\": c.window, \"bounds\": b}\n\tif b.WindowState != WindowStateNormal {\n\t\tparam[\"bounds\"] = h{\"windowState\": b.WindowState}\n\t}\n\t_, err := c.send(\"Browser.setWindowBounds\", param)\n\treturn err\n}\n\nfunc (c *chrome) bounds() (Bounds, error) {\n\tresult, err := c.send(\"Browser.getWindowBounds\", h{\"windowId\": c.window})\n\tif err != nil {\n\t\treturn Bounds{}, err\n\t}\n\tbounds := struct {\n\t\tBounds Bounds `json:\"bounds\"`\n\t}{}\n\terr = json.Unmarshal(result, &bounds)\n\treturn bounds.Bounds, err\n}\n\nfunc (c *chrome) pdf(width, height int) ([]byte, error) {\n\tresult, err := c.send(\"Page.printToPDF\", h{\n\t\t\"paperWidth\":  float32(width) / 96,\n\t\t\"paperHeight\": float32(height) / 96,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpdf := struct {\n\t\tData []byte `json:\"data\"`\n\t}{}\n\terr = json.Unmarshal(result, &pdf)\n\treturn pdf.Data, err\n}\n\nfunc (c *chrome) png(x, y, width, height int, bg uint32, scale float32) ([]byte, error) {\n\tif x == 0 && y == 0 && width == 0 && height == 0 {\n\t\t// By default either use SVG size if it's an SVG, or use A4 page size\n\t\tbounds, err := c.eval(`document.rootElement ? [document.rootElement.x.baseVal.value, document.rootElement.y.baseVal.value, document.rootElement.width.baseVal.value, document.rootElement.height.baseVal.value] : [0,0,816,1056]`)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trect := make([]int, 4)\n\t\tif err := json.Unmarshal(bounds, &rect); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tx, y, width, height = rect[0], rect[1], rect[2], rect[3]\n\t}\n\n\t_, err := c.send(\"Emulation.setDefaultBackgroundColorOverride\", h{\n\t\t\"color\": h{\n\t\t\t\"r\": (bg >> 16) & 0xff,\n\t\t\t\"g\": (bg >> 8) & 0xff,\n\t\t\t\"b\": bg & 0xff,\n\t\t\t\"a\": (bg >> 24) & 0xff,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult, err := c.send(\"Page.captureScreenshot\", h{\n\t\t\"clip\": h{\n\t\t\t\"x\": x, \"y\": y, \"width\": width, \"height\": height, \"scale\": scale,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpdf := struct {\n\t\tData []byte `json:\"data\"`\n\t}{}\n\terr = json.Unmarshal(result, &pdf)\n\treturn pdf.Data, err\n}\n\nfunc (c *chrome) kill() error {\n\tif c.ws != nil {\n\t\tif err := c.ws.Close(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t// TODO: cancel all pending requests\n\tif state := c.cmd.ProcessState; state == nil || !state.Exited() {\n\t\treturn c.cmd.Process.Kill()\n\t}\n\treturn nil\n}\n\nfunc readUntilMatch(r io.ReadCloser, re *regexp.Regexp) ([]string, error) {\n\tbr := bufio.NewReader(r)\n\tfor {\n\t\tif line, err := br.ReadString('\\n'); err != nil {\n\t\t\tr.Close()\n\t\t\treturn nil, err\n\t\t} else if m := re.FindStringSubmatch(line); m != nil {\n\t\t\tgo io.Copy(ioutil.Discard, br)\n\t\t\treturn m, nil\n\t\t}\n\t}\n}\n\nfunc contains(arr []string, x string) bool {\n\tfor _, n := range arr {\n\t\tif x == n {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "chrome_test.go",
    "content": "package lorca\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n)\n\nfunc TestChromeEval(t *testing.T) {\n\tc, err := newChromeWithArgs(ChromeExecutable(), \"--user-data-dir=/tmp\", \"--headless\", \"--remote-debugging-port=0\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer c.kill()\n\n\tfor _, test := range []struct {\n\t\tExpr   string\n\t\tResult string\n\t\tError  string\n\t}{\n\t\t{Expr: ``, Result: ``},\n\t\t{Expr: `42`, Result: `42`},\n\t\t{Expr: `2+3`, Result: `5`},\n\t\t{Expr: `(() => ({x: 5, y: 7}))()`, Result: `{\"x\":5,\"y\":7}`},\n\t\t{Expr: `(() => ([1,'foo',false]))()`, Result: `[1,\"foo\",false]`},\n\t\t{Expr: `((a, b) => a*b)(3, 7)`, Result: `21`},\n\t\t{Expr: `Promise.resolve(42)`, Result: `42`},\n\t\t{Expr: `Promise.reject('foo')`, Error: `\"foo\"`},\n\t\t{Expr: `throw \"bar\"`, Error: `\"bar\"`},\n\t\t{Expr: `2+`, Error: `SyntaxError: Unexpected end of input`},\n\t} {\n\t\tresult, err := c.eval(test.Expr)\n\t\tif err != nil {\n\t\t\tif err.Error() != test.Error {\n\t\t\t\tt.Fatal(test.Expr, err, test.Error)\n\t\t\t}\n\t\t} else if string(result) != test.Result {\n\t\t\tt.Fatal(test.Expr, string(result), test.Result)\n\t\t}\n\t}\n}\n\nfunc TestChromeLoad(t *testing.T) {\n\tc, err := newChromeWithArgs(ChromeExecutable(), \"--user-data-dir=/tmp\", \"--headless\", \"--remote-debugging-port=0\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer c.kill()\n\tif err := c.load(\"data:text/html,<html><body>Hello</body></html>\"); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tfor i := 0; i < 10; i++ {\n\t\turl, err := c.eval(`window.location.href`)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif strings.HasPrefix(string(url), `\"data:text/html,`) {\n\t\t\tbreak\n\t\t}\n\t}\n\tif res, err := c.eval(`document.body ? document.body.innerText :\n\t\t\tnew Promise(res => window.onload = () => res(document.body.innerText))`); err != nil {\n\t\tt.Fatal(err)\n\t} else if string(res) != `\"Hello\"` {\n\t\tt.Fatal(res)\n\t}\n}\n\nfunc TestChromeBind(t *testing.T) {\n\tc, err := newChromeWithArgs(ChromeExecutable(), \"--user-data-dir=/tmp\", \"--headless\", \"--remote-debugging-port=0\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer c.kill()\n\n\tif err := c.bind(\"add\", func(args []json.RawMessage) (interface{}, error) {\n\t\ta, b := 0, 0\n\t\tif len(args) != 2 {\n\t\t\treturn nil, errors.New(\"2 arguments expected\")\n\t\t}\n\t\tif err := json.Unmarshal(args[0], &a); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := json.Unmarshal(args[1], &b); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn a + b, nil\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif res, err := c.eval(`window.add(2, 3)`); err != nil {\n\t\tt.Fatal(err)\n\t} else if string(res) != `5` {\n\t\tt.Fatal(string(res))\n\t}\n\n\tif res, err := c.eval(`window.add(\"foo\", \"bar\")`); err == nil {\n\t\tt.Fatal(string(res), err)\n\t}\n\tif res, err := c.eval(`window.add(1, 2, 3)`); err == nil {\n\t\tt.Fatal(res, err)\n\t}\n}\n\nfunc TestChromeAsync(t *testing.T) {\n\tc, err := newChromeWithArgs(ChromeExecutable(), \"--user-data-dir=/tmp\", \"--headless\", \"--remote-debugging-port=0\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer c.kill()\n\n\tif err := c.bind(\"len\", func(args []json.RawMessage) (interface{}, error) {\n\t\treturn len(args[0]), nil\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\twg := &sync.WaitGroup{}\n\tn := 10\n\tfailed := int32(0)\n\twg.Add(n)\n\tfor i := 0; i < n; i++ {\n\t\tgo func(i int) {\n\t\t\tdefer wg.Done()\n\t\t\tv, err := c.eval(\"len('hello')\")\n\t\t\tif string(v) != `7` {\n\t\t\t\tatomic.StoreInt32(&failed, 1)\n\t\t\t} else if err != nil {\n\t\t\t\tatomic.StoreInt32(&failed, 2)\n\t\t\t}\n\t\t}(i)\n\t}\n\twg.Wait()\n\n\tif status := atomic.LoadInt32(&failed); status != 0 {\n\t\tt.Fatal()\n\t}\n}\n"
  },
  {
    "path": "examples/counter/build-linux.sh",
    "content": "#!/bin/sh\n\nAPP=lorca-example\nAPPDIR=${APP}_1.0.0\n\nmkdir -p $APPDIR/usr/bin\nmkdir -p $APPDIR/usr/share/applications\nmkdir -p $APPDIR/usr/share/icons/hicolor/1024x1024/apps\nmkdir -p $APPDIR/usr/share/icons/hicolor/256x256/apps\nmkdir -p $APPDIR/DEBIAN\n\ngo build -o $APPDIR/usr/bin/$APP\n\ncp icons/icon.png $APPDIR/usr/share/icons/hicolor/1024x1024/apps/${APP}.png\ncp icons/icon.png $APPDIR/usr/share/icons/hicolor/256x256/apps/${APP}.png\n\ncat > $APPDIR/usr/share/applications/${APP}.desktop << EOF\n[Desktop Entry]\nVersion=1.0\nType=Application\nName=$APP\nExec=$APP\nIcon=$APP\nTerminal=false\nStartupWMClass=Lorca\nEOF\n\ncat > $APPDIR/DEBIAN/control << EOF\nPackage: ${APP}\nVersion: 1.0-0\nSection: base\nPriority: optional\nArchitecture: amd64\nMaintainer: Serge Zaitsev <zaitsev.serge@gmail.com>\nDescription: Example for Lorca GUI toolkit\nEOF\n\ndpkg-deb --build $APPDIR\n"
  },
  {
    "path": "examples/counter/build-macos.sh",
    "content": "#!/bin/sh\n\nAPP=\"Example.app\"\nmkdir -p $APP/Contents/{MacOS,Resources}\ngo build -o $APP/Contents/MacOS/lorca-example\ncat > $APP/Contents/Info.plist << EOF\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleExecutable</key>\n\t<string>lorca-example</string>\n\t<key>CFBundleIconFile</key>\n\t<string>icon.icns</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>com.zserge.lorca.example</string>\n</dict>\n</plist>\nEOF\ncp icons/icon.icns $APP/Contents/Resources/icon.icns\nfind $APP\n"
  },
  {
    "path": "examples/counter/build-windows.bat",
    "content": "@echo off\r\ngo generate\r\ngo build -ldflags \"-H windowsgui\" -o lorca-example.exe\r\n"
  },
  {
    "path": "examples/counter/main.go",
    "content": "package main\n\nimport (\n\t\"embed\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"runtime\"\n\t\"sync\"\n\n\t\"github.com/zserge/lorca\"\n)\n\n//go:embed www\nvar fs embed.FS\n\n// Go types that are bound to the UI must be thread-safe, because each binding\n// is executed in its own goroutine. In this simple case we may use atomic\n// operations, but for more complex cases one should use proper synchronization.\ntype counter struct {\n\tsync.Mutex\n\tcount int\n}\n\nfunc (c *counter) Add(n int) {\n\tc.Lock()\n\tdefer c.Unlock()\n\tc.count = c.count + n\n}\n\nfunc (c *counter) Value() int {\n\tc.Lock()\n\tdefer c.Unlock()\n\treturn c.count\n}\n\nfunc main() {\n\targs := []string{}\n\tif runtime.GOOS == \"linux\" {\n\t\targs = append(args, \"--class=Lorca\")\n\t}\n\tui, err := lorca.New(\"\", \"\", 480, 320, args...)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer ui.Close()\n\n\t// A simple way to know when UI is ready (uses body.onload event in JS)\n\tui.Bind(\"start\", func() {\n\t\tlog.Println(\"UI is ready\")\n\t})\n\n\t// Create and bind Go object to the UI\n\tc := &counter{}\n\tui.Bind(\"counterAdd\", c.Add)\n\tui.Bind(\"counterValue\", c.Value)\n\n\t// Load HTML.\n\t// You may also use `data:text/html,<base64>` approach to load initial HTML,\n\t// e.g: ui.Load(\"data:text/html,\" + url.PathEscape(html))\n\n\tln, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer ln.Close()\n\tgo http.Serve(ln, http.FileServer(http.FS(fs)))\n\tui.Load(fmt.Sprintf(\"http://%s/www\", ln.Addr()))\n\n\t// You may use console.log to debug your JS code, it will be printed via\n\t// log.Println(). Also exceptions are printed in a similar manner.\n\tui.Eval(`\n\t\tconsole.log(\"Hello, world!\");\n\t\tconsole.log('Multiple values:', [1, false, {\"x\":5}]);\n\t`)\n\n\t// Wait until the interrupt signal arrives or browser window is closed\n\tsigc := make(chan os.Signal)\n\tsignal.Notify(sigc, os.Interrupt)\n\tselect {\n\tcase <-sigc:\n\tcase <-ui.Done():\n\t}\n\n\tlog.Println(\"exiting...\")\n}\n"
  },
  {
    "path": "examples/counter/www/index.html",
    "content": "<!doctype html>\n<html>\n\t<head>\n\t\t<title>Counter</title>\n\t\t<link rel=\"shortcut icon\" href=\"favicon.png\">\n\t\t<style>\n\t\t* { margin: 0; padding: 0; box-sizing: border-box; user-select: none; }\n\t\tbody { height: 100vh; display: flex; align-items: center; justify-content: center; background-color: #f1c40f; font-family: 'Helvetika Neue', Arial, sans-serif; font-size: 28px; }\n\t\t.counter-container { display: flex; flex-direction: column; align-items: center; }\n\t\t.counter { text-transform: uppercase; color: #fff; font-weight: bold; font-size: 3rem; }\n\t\t.btn-row { display: flex; align-items: center; margin: 1rem; }\n\t\t.btn { cursor: pointer; min-width: 4em; padding: 1em; border-radius: 5px; text-align: center; margin: 0 1rem; box-shadow: 0 6px #8b5e00; color: white; background-color: #E4B702; position: relative; font-weight: bold; }\n\t\t.btn:hover { box-shadow: 0 4px #8b5e00; top: 2px; }\n\t\t.btn:active{ box-shadow: 0 1px #8b5e00; top: 5px; }\n\t\t</style>\n\t</head>\n\t<body onload=start()>\n\t\t<!-- UI layout -->\n\t\t<div class=\"counter-container\">\n\t\t\t<div class=\"counter\"></div>\n\t\t\t<div class=\"btn-row\">\n\t\t\t\t<div class=\"btn btn-incr\">+1</div>\n\t\t\t\t<div class=\"btn btn-decr\">-1</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Connect UI actions to Go functions -->\n\t\t<script>\n\t\t\tconst counter = document.querySelector('.counter');\n\t\t\tconst btnIncr = document.querySelector('.btn-incr');\n\t\t\tconst btnDecr = document.querySelector('.btn-decr');\n\n\t\t\t// We use async/await because Go functions are asynchronous\n\t\t\tconst render = async () => {\n\t\t\t\tcounter.innerText = `Count: ${await window.counterValue()}`;\n\t\t\t};\n\n\t\t\tbtnIncr.addEventListener('click', async () => {\n\t\t\t\tawait counterAdd(1); // Call Go function\n\t\t\t\trender();\n\t\t\t});\n\n\t\t\tbtnDecr.addEventListener('click', async () => {\n\t\t\t\tawait counterAdd(-1); // Call Go function\n\t\t\t\trender();\n\t\t\t});\n\n\t\t\trender();\n\t\t</script>\n\t</body>\n</html>\n"
  },
  {
    "path": "examples/hello/main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"net/url\"\n\n\t\"github.com/zserge/lorca\"\n)\n\nfunc main() {\n\t// Create UI with basic HTML passed via data URI\n\tui, err := lorca.New(\"data:text/html,\"+url.PathEscape(`\n\t<html>\n\t\t<head><title>Hello</title></head>\n\t\t<body><h1>Hello, world!</h1></body>\n\t</html>\n\t`), \"\", 480, 320)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer ui.Close()\n\t// Wait until UI window is closed\n\t<-ui.Done()\n}\n"
  },
  {
    "path": "examples/stopwatch/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/url\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/zserge/lorca\"\n)\n\nfunc main() {\n\tui, err := lorca.New(\"\", \"\", 480, 320)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer ui.Close()\n\n\t// Data model: number of ticks\n\tticks := uint32(0)\n\t// Channel to connect UI events with the background ticking goroutine\n\ttogglec := make(chan bool)\n\t// Bind Go functions to JS\n\tui.Bind(\"toggle\", func() { togglec <- true })\n\tui.Bind(\"reset\", func() {\n\t\tatomic.StoreUint32(&ticks, 0)\n\t\tui.Eval(`document.querySelector('.timer').innerText = '0'`)\n\t})\n\n\t// Load HTML after Go functions are bound to JS\n\tui.Load(\"data:text/html,\" + url.PathEscape(`\n\t<html>\n\t\t<body>\n\t\t\t<!-- toggle() and reset() are Go functions wrapped into JS -->\n\t\t\t<div class=\"timer\" onclick=\"toggle()\"></div>\n\t\t\t<button onclick=\"reset()\">Reset</button>\n\t\t</body>\n\t</html>\n\t`))\n\n\t// Start ticker goroutine\n\tgo func() {\n\t\tt := time.NewTicker(100 * time.Millisecond)\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-t.C: // Every 100ms increate number of ticks and update UI\n\t\t\t\tui.Eval(fmt.Sprintf(`document.querySelector('.timer').innerText = 0.1*%d`,\n\t\t\t\t\tatomic.AddUint32(&ticks, 1)))\n\t\t\tcase <-togglec: // If paused - wait for another toggle event to unpause\n\t\t\t\t<-togglec\n\t\t\t}\n\t\t}\n\t}()\n\t<-ui.Done()\n}\n"
  },
  {
    "path": "export.go",
    "content": "package lorca\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n)\n\nconst (\n\t// PageA4Width is a width of an A4 page in pixels at 96dpi\n\tPageA4Width = 816\n\t// PageA4Height is a height of an A4 page in pixels at 96dpi\n\tPageA4Height = 1056\n)\n\n// PDF converts a given URL (may be a local file) to a PDF file. Script is\n// evaluated before the page is printed to PDF, you may modify the contents of\n// the page there of wait until the page is fully rendered. Width and height\n// are page bounds in pixels. PDF by default uses 96dpi density. For A4 page\n// you may use PageA4Width and PageA4Height constants.\nfunc PDF(url, script string, width, height int) ([]byte, error) {\n\treturn doHeadless(url, func(c *chrome) ([]byte, error) {\n\t\tif _, err := c.eval(script); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn c.pdf(width, height)\n\t})\n}\n\n// PNG converts a given URL (may be a local file) to a PNG image. Script is\n// evaluated before the \"screenshot\" is taken, so you can modify the contents\n// of a URL there. Image bounds are provides in pixels. Background is in ARGB\n// format, the default value of zero keeps the background transparent. Scale\n// allows zooming the page in and out.\n//\n// This function is most convenient to convert SVG to PNG of different sizes,\n// for example when preparing Lorca app icons.\nfunc PNG(url, script string, x, y, width, height int, bg uint32, scale float32) ([]byte, error) {\n\treturn doHeadless(url, func(c *chrome) ([]byte, error) {\n\t\tif _, err := c.eval(script); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn c.png(x, y, width, height, bg, scale)\n\t})\n}\n\nfunc doHeadless(url string, f func(c *chrome) ([]byte, error)) ([]byte, error) {\n\tdir, err := ioutil.TempDir(\"\", \"lorca\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer os.RemoveAll(dir)\n\targs := append(defaultChromeArgs, fmt.Sprintf(\"--user-data-dir=%s\", dir), \"--remote-debugging-port=0\", \"--headless\", url)\n\tchrome, err := newChromeWithArgs(ChromeExecutable(), args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer chrome.kill()\n\treturn f(chrome)\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/zserge/lorca\n\ngo 1.16\n\nrequire golang.org/x/net v0.0.0-20200222125558-5a598a2470a0\n"
  },
  {
    "path": "go.sum",
    "content": "golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\n"
  },
  {
    "path": "locate.go",
    "content": "package lorca\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"strings\"\n)\n\n// ChromeExecutable returns a string which points to the preferred Chrome\n// executable file.\nvar ChromeExecutable = LocateChrome\n\n// LocateChrome returns a path to the Chrome binary, or an empty string if\n// Chrome installation is not found.\nfunc LocateChrome() string {\n\n\t// If env variable \"LORCACHROME\" specified and it exists\n\tif path, ok := os.LookupEnv(\"LORCACHROME\"); ok {\n\t\tif _, err := os.Stat(path); err == nil {\n\t\t\treturn path\n\t\t}\n\t}\n\n\tvar paths []string\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tpaths = []string{\n\t\t\t\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\",\n\t\t\t\"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary\",\n\t\t\t\"/Applications/Chromium.app/Contents/MacOS/Chromium\",\n\t\t\t\"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge\",\n\t\t\t\"/usr/bin/google-chrome-stable\",\n\t\t\t\"/usr/bin/google-chrome\",\n\t\t\t\"/usr/bin/chromium\",\n\t\t\t\"/usr/bin/chromium-browser\",\n\t\t}\n\tcase \"windows\":\n\t\tpaths = []string{\n\t\t\tos.Getenv(\"LocalAppData\") + \"/Google/Chrome/Application/chrome.exe\",\n\t\t\tos.Getenv(\"ProgramFiles\") + \"/Google/Chrome/Application/chrome.exe\",\n\t\t\tos.Getenv(\"ProgramFiles(x86)\") + \"/Google/Chrome/Application/chrome.exe\",\n\t\t\tos.Getenv(\"LocalAppData\") + \"/Chromium/Application/chrome.exe\",\n\t\t\tos.Getenv(\"ProgramFiles\") + \"/Chromium/Application/chrome.exe\",\n\t\t\tos.Getenv(\"ProgramFiles(x86)\") + \"/Chromium/Application/chrome.exe\",\n\t\t\tos.Getenv(\"ProgramFiles(x86)\") + \"/Microsoft/Edge/Application/msedge.exe\",\n\t\t\tos.Getenv(\"ProgramFiles\") + \"/Microsoft/Edge/Application/msedge.exe\",\n\t\t}\n\tdefault:\n\t\tpaths = []string{\n\t\t\t\"/usr/bin/google-chrome-stable\",\n\t\t\t\"/usr/bin/google-chrome\",\n\t\t\t\"/usr/bin/chromium\",\n\t\t\t\"/usr/bin/chromium-browser\",\n\t\t\t\"/snap/bin/chromium\",\n\t\t}\n\t}\n\n\tfor _, path := range paths {\n\t\tif _, err := os.Stat(path); os.IsNotExist(err) {\n\t\t\tcontinue\n\t\t}\n\t\treturn path\n\t}\n\treturn \"\"\n}\n\n// PromptDownload asks user if he wants to download and install Chrome, and\n// opens a download web page if the user agrees.\nfunc PromptDownload() {\n\ttitle := \"Chrome not found\"\n\ttext := \"No Chrome/Chromium installation was found. Would you like to download and install it now?\"\n\n\t// Ask user for confirmation\n\tif !messageBox(title, text) {\n\t\treturn\n\t}\n\n\t// Open download page\n\turl := \"https://www.google.com/chrome/\"\n\tswitch runtime.GOOS {\n\tcase \"linux\":\n\t\texec.Command(\"xdg-open\", url).Run()\n\tcase \"darwin\":\n\t\texec.Command(\"open\", url).Run()\n\tcase \"windows\":\n\t\tr := strings.NewReplacer(\"&\", \"^&\")\n\t\texec.Command(\"cmd\", \"/c\", \"start\", r.Replace(url)).Run()\n\t}\n}\n"
  },
  {
    "path": "locate_test.go",
    "content": "package lorca\n\nimport (\n\t\"os/exec\"\n\t\"testing\"\n)\n\nfunc TestLocate(t *testing.T) {\n\tif exe := ChromeExecutable(); exe == \"\" {\n\t\tt.Fatal()\n\t} else {\n\t\tt.Log(exe)\n\t\tb, err := exec.Command(exe, \"--version\").CombinedOutput()\n\t\tt.Log(string(b))\n\t\tt.Log(err)\n\t}\n}\n"
  },
  {
    "path": "messagebox.go",
    "content": "//+build !windows\n\npackage lorca\n\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"strings\"\n\t\"syscall\"\n)\n\nfunc messageBox(title, text string) bool {\n\tif runtime.GOOS == \"linux\" {\n\t\terr := exec.Command(\"zenity\", \"--question\", \"--title\", title, \"--text\", text).Run()\n\t\tif err != nil {\n\t\t\tif exitError, ok := err.(*exec.ExitError); ok {\n\t\t\t\treturn exitError.Sys().(syscall.WaitStatus).ExitStatus() == 0\n\t\t\t}\n\t\t}\n\t} else if runtime.GOOS == \"darwin\" {\n\t\tscript := `set T to button returned of ` +\n\t\t\t`(display dialog \"%s\" with title \"%s\" buttons {\"No\", \"Yes\"} default button \"Yes\")`\n\t\tout, err := exec.Command(\"osascript\", \"-e\", fmt.Sprintf(script, text, title)).Output()\n\t\tif err != nil {\n\t\t\tif exitError, ok := err.(*exec.ExitError); ok {\n\t\t\t\treturn exitError.Sys().(syscall.WaitStatus).ExitStatus() == 0\n\t\t\t}\n\t\t}\n\t\treturn strings.TrimSpace(string(out)) == \"Yes\"\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "messagebox_windows.go",
    "content": "//+build windows\n\npackage lorca\n\nimport (\n\t\"syscall\"\n\t\"unsafe\"\n)\n\nfunc messageBox(title, text string) bool {\n\tuser32 := syscall.NewLazyDLL(\"user32.dll\")\n\tmessageBoxW := user32.NewProc(\"MessageBoxW\")\n\tmbYesNo := 0x00000004\n\tmbIconQuestion := 0x00000020\n\tidYes := 6\n\tret, _, _ := messageBoxW.Call(0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(text))),\n\t\tuintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))), uintptr(uint(mbYesNo|mbIconQuestion)))\n\treturn int(ret) == idYes\n}\n"
  },
  {
    "path": "ui.go",
    "content": "package lorca\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"reflect\"\n)\n\n// UI interface allows talking to the HTML5 UI from Go.\ntype UI interface {\n\tLoad(url string) error\n\tBounds() (Bounds, error)\n\tSetBounds(Bounds) error\n\tBind(name string, f interface{}) error\n\tEval(js string) Value\n\tDone() <-chan struct{}\n\tClose() error\n}\n\ntype ui struct {\n\tchrome *chrome\n\tdone   chan struct{}\n\ttmpDir string\n}\n\nvar defaultChromeArgs = []string{\n\t\"--disable-background-networking\",\n\t\"--disable-background-timer-throttling\",\n\t\"--disable-backgrounding-occluded-windows\",\n\t\"--disable-breakpad\",\n\t\"--disable-client-side-phishing-detection\",\n\t\"--disable-default-apps\",\n\t\"--disable-dev-shm-usage\",\n\t\"--disable-infobars\",\n\t\"--disable-extensions\",\n\t\"--disable-features=site-per-process\",\n\t\"--disable-hang-monitor\",\n\t\"--disable-ipc-flooding-protection\",\n\t\"--disable-popup-blocking\",\n\t\"--disable-prompt-on-repost\",\n\t\"--disable-renderer-backgrounding\",\n\t\"--disable-sync\",\n\t\"--disable-translate\",\n\t\"--disable-windows10-custom-titlebar\",\n\t\"--metrics-recording-only\",\n\t\"--no-first-run\",\n\t\"--no-default-browser-check\",\n\t\"--safebrowsing-disable-auto-update\",\n\t\"--enable-automation\",\n\t\"--password-store=basic\",\n\t\"--use-mock-keychain\",\n\t\"--remote-allow-origins=*\",\n}\n\n// New returns a new HTML5 UI for the given URL, user profile directory, window\n// size and other options passed to the browser engine. If URL is an empty\n// string - a blank page is displayed. If user profile directory is an empty\n// string - a temporary directory is created and it will be removed on\n// ui.Close(). You might want to use \"--headless\" custom CLI argument to test\n// your UI code.\nfunc New(url, dir string, width, height int, customArgs ...string) (UI, error) {\n\tif url == \"\" {\n\t\turl = \"data:text/html,<html></html>\"\n\t}\n\ttmpDir := \"\"\n\tif dir == \"\" {\n\t\tname, err := ioutil.TempDir(\"\", \"lorca\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdir, tmpDir = name, name\n\t}\n\targs := append(defaultChromeArgs, fmt.Sprintf(\"--app=%s\", url))\n\targs = append(args, fmt.Sprintf(\"--user-data-dir=%s\", dir))\n\targs = append(args, fmt.Sprintf(\"--window-size=%d,%d\", width, height))\n\targs = append(args, customArgs...)\n\targs = append(args, \"--remote-debugging-port=0\")\n\n\tchrome, err := newChromeWithArgs(ChromeExecutable(), args...)\n\tdone := make(chan struct{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tgo func() {\n\t\tchrome.cmd.Wait()\n\t\tclose(done)\n\t}()\n\treturn &ui{chrome: chrome, done: done, tmpDir: tmpDir}, nil\n}\n\nfunc (u *ui) Done() <-chan struct{} {\n\treturn u.done\n}\n\nfunc (u *ui) Close() error {\n\t// ignore err, as the chrome process might be already dead, when user close the window.\n\tu.chrome.kill()\n\t<-u.done\n\tif u.tmpDir != \"\" {\n\t\tif err := os.RemoveAll(u.tmpDir); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (u *ui) Load(url string) error { return u.chrome.load(url) }\n\nfunc (u *ui) Bind(name string, f interface{}) error {\n\tv := reflect.ValueOf(f)\n\t// f must be a function\n\tif v.Kind() != reflect.Func {\n\t\treturn errors.New(\"only functions can be bound\")\n\t}\n\t// f must return either value and error or just error\n\tif n := v.Type().NumOut(); n > 2 {\n\t\treturn errors.New(\"function may only return a value or a value+error\")\n\t}\n\n\treturn u.chrome.bind(name, func(raw []json.RawMessage) (interface{}, error) {\n\t\tif len(raw) != v.Type().NumIn() {\n\t\t\treturn nil, errors.New(\"function arguments mismatch\")\n\t\t}\n\t\targs := []reflect.Value{}\n\t\tfor i := range raw {\n\t\t\targ := reflect.New(v.Type().In(i))\n\t\t\tif err := json.Unmarshal(raw[i], arg.Interface()); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\targs = append(args, arg.Elem())\n\t\t}\n\t\terrorType := reflect.TypeOf((*error)(nil)).Elem()\n\t\tres := v.Call(args)\n\t\tswitch len(res) {\n\t\tcase 0:\n\t\t\t// No results from the function, just return nil\n\t\t\treturn nil, nil\n\t\tcase 1:\n\t\t\t// One result may be a value, or an error\n\t\t\tif res[0].Type().Implements(errorType) {\n\t\t\t\tif res[0].Interface() != nil {\n\t\t\t\t\treturn nil, res[0].Interface().(error)\n\t\t\t\t}\n\t\t\t\treturn nil, nil\n\t\t\t}\n\t\t\treturn res[0].Interface(), nil\n\t\tcase 2:\n\t\t\t// Two results: first one is value, second is error\n\t\t\tif !res[1].Type().Implements(errorType) {\n\t\t\t\treturn nil, errors.New(\"second return value must be an error\")\n\t\t\t}\n\t\t\tif res[1].Interface() == nil {\n\t\t\t\treturn res[0].Interface(), nil\n\t\t\t}\n\t\t\treturn res[0].Interface(), res[1].Interface().(error)\n\t\tdefault:\n\t\t\treturn nil, errors.New(\"unexpected number of return values\")\n\t\t}\n\t})\n}\n\nfunc (u *ui) Eval(js string) Value {\n\tv, err := u.chrome.eval(js)\n\treturn value{err: err, raw: v}\n}\n\nfunc (u *ui) SetBounds(b Bounds) error {\n\treturn u.chrome.setBounds(b)\n}\n\nfunc (u *ui) Bounds() (Bounds, error) {\n\treturn u.chrome.bounds()\n}\n"
  },
  {
    "path": "ui_test.go",
    "content": "package lorca\n\nimport (\n\t\"errors\"\n\t\"math/rand\"\n\t\"strconv\"\n\t\"testing\"\n)\n\nfunc TestEval(t *testing.T) {\n\tui, err := New(\"\", \"\", 480, 320, \"--headless\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer ui.Close()\n\n\tif n := ui.Eval(`2+3`).Int(); n != 5 {\n\t\tt.Fatal(n)\n\t}\n\n\tif s := ui.Eval(`\"foo\" + \"bar\"`).String(); s != \"foobar\" {\n\t\tt.Fatal(s)\n\t}\n\n\tif a := ui.Eval(`[1,2,3].map(n => n *2)`).Array(); a[0].Int() != 2 || a[1].Int() != 4 || a[2].Int() != 6 {\n\t\tt.Fatal(a)\n\t}\n\n\t// XXX this probably should be unquoted?\n\tif err := ui.Eval(`throw \"fail\"`).Err(); err.Error() != `\"fail\"` {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestBind(t *testing.T) {\n\tui, err := New(\"\", \"\", 480, 320, \"--headless\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer ui.Close()\n\n\tif err := ui.Bind(\"add\", func(a, b int) int { return a + b }); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := ui.Bind(\"rand\", func() int { return rand.Int() }); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := ui.Bind(\"strlen\", func(s string) int { return len(s) }); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := ui.Bind(\"atoi\", func(s string) (int, error) { return strconv.Atoi(s) }); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := ui.Bind(\"shouldFail\", \"hello\"); err == nil {\n\t\tt.Fail()\n\t}\n\n\tif n := ui.Eval(`add(2,3)`); n.Int() != 5 {\n\t\tt.Fatal(n)\n\t}\n\tif n := ui.Eval(`add(2,3,4)`); n.Err() == nil {\n\t\tt.Fatal(n)\n\t}\n\tif n := ui.Eval(`add(2)`); n.Err() == nil {\n\t\tt.Fatal(n)\n\t}\n\tif n := ui.Eval(`add(\"hello\", \"world\")`); n.Err() == nil {\n\t\tt.Fatal(n)\n\t}\n\tif n := ui.Eval(`rand()`); n.Err() != nil {\n\t\tt.Fatal(n)\n\t}\n\tif n := ui.Eval(`rand(100)`); n.Err() == nil {\n\t\tt.Fatal(n)\n\t}\n\tif n := ui.Eval(`strlen('foo')`); n.Int() != 3 {\n\t\tt.Fatal(n)\n\t}\n\tif n := ui.Eval(`strlen(123)`); n.Err() == nil {\n\t\tt.Fatal(n)\n\t}\n\tif n := ui.Eval(`atoi('123')`); n.Int() != 123 {\n\t\tt.Fatal(n)\n\t}\n\tif n := ui.Eval(`atoi('hello')`); n.Err() == nil {\n\t\tt.Fatal(n)\n\t}\n}\n\nfunc TestFunctionReturnTypes(t *testing.T) {\n\tui, err := New(\"\", \"\", 480, 320, \"--headless\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer ui.Close()\n\n\tif err := ui.Bind(\"noResults\", func() { return }); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := ui.Bind(\"oneNonNilResult\", func() interface{} { return 1 }); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := ui.Bind(\"oneNilResult\", func() interface{} { return nil }); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := ui.Bind(\"oneNonNilErrorResult\", func() error { return errors.New(\"error\") }); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := ui.Bind(\"oneNilErrorResult\", func() error { return nil }); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := ui.Bind(\"twoResultsNonNilError\", func() (interface{}, error) { return nil, errors.New(\"error\") }); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := ui.Bind(\"twoResultsNilError\", func() (interface{}, error) { return 1, nil }); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := ui.Bind(\"twoResultsBothNonNil\", func() (interface{}, error) { return 1, errors.New(\"error\") }); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := ui.Bind(\"twoResultsBothNil\", func() (interface{}, error) { return nil, nil }); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif v := ui.Eval(`noResults()`); v.Err() != nil {\n\t\tt.Fatal(v)\n\t}\n\tif v := ui.Eval(`oneNonNilResult()`); v.Int() != 1 {\n\t\tt.Fatal(v)\n\t}\n\tif v := ui.Eval(`oneNilResult()`); v.Err() != nil {\n\t\tt.Fatal(v)\n\t}\n\tif v := ui.Eval(`oneNonNilErrorResult()`); v.Err() == nil {\n\t\tt.Fatal(v)\n\t}\n\tif v := ui.Eval(`oneNilErrorResult()`); v.Err() != nil {\n\t\tt.Fatal(v)\n\t}\n\tif v := ui.Eval(`twoResultsNonNilError()`); v.Err() == nil {\n\t\tt.Fatal(v)\n\t}\n\tif v := ui.Eval(`twoResultsNilError()`); v.Err() != nil || v.Int() != 1 {\n\t\tt.Fatal(v)\n\t}\n\tif v := ui.Eval(`twoResultsBothNonNil()`); v.Err() == nil {\n\t\tt.Fatal(v)\n\t}\n\tif v := ui.Eval(`twoResultsBothNil()`); v.Err() != nil {\n\t\tt.Fatal(v)\n\t}\n}\n"
  },
  {
    "path": "value.go",
    "content": "package lorca\n\nimport \"encoding/json\"\n\n// Value is a generic type of a JSON value (primitive, object, array) and\n// optionally an error value.\ntype Value interface {\n\tErr() error\n\tTo(interface{}) error\n\tFloat() float32\n\tInt() int\n\tString() string\n\tBool() bool\n\tObject() map[string]Value\n\tArray() []Value\n\tBytes() []byte\n}\n\ntype value struct {\n\terr error\n\traw json.RawMessage\n}\n\nfunc (v value) Err() error             { return v.err }\nfunc (v value) Bytes() []byte          { return v.raw }\nfunc (v value) To(x interface{}) error { return json.Unmarshal(v.raw, x) }\nfunc (v value) Float() (f float32)     { v.To(&f); return f }\nfunc (v value) Int() (i int)           { v.To(&i); return i }\nfunc (v value) String() (s string)     { v.To(&s); return s }\nfunc (v value) Bool() (b bool)         { v.To(&b); return b }\nfunc (v value) Array() (values []Value) {\n\tarray := []json.RawMessage{}\n\tv.To(&array)\n\tfor _, el := range array {\n\t\tvalues = append(values, value{raw: el})\n\t}\n\treturn values\n}\nfunc (v value) Object() (object map[string]Value) {\n\tobject = map[string]Value{}\n\tkv := map[string]json.RawMessage{}\n\tv.To(&kv)\n\tfor k, v := range kv {\n\t\tobject[k] = value{raw: v}\n\t}\n\treturn object\n}\n"
  },
  {
    "path": "value_test.go",
    "content": "package lorca\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"testing\"\n)\n\nvar errTest = errors.New(\"fail\")\n\nfunc TestValueError(t *testing.T) {\n\tv := value{err: errTest}\n\tif v.Err() != errTest {\n\t\tt.Fail()\n\t}\n\n\tv = value{raw: json.RawMessage(`\"hello\"`)}\n\tif v.Err() != nil {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestValuePrimitive(t *testing.T) {\n\tv := value{raw: json.RawMessage(`42`)}\n\tif v.Int() != 42 {\n\t\tt.Fail()\n\t}\n\tv = value{raw: json.RawMessage(`\"hello\"`)}\n\tif v.Int() != 0 || v.String() != \"hello\" {\n\t\tt.Fail()\n\t}\n\tv = value{err: errTest}\n\tif v.Int() != 0 || v.String() != \"\" {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestValueComplex(t *testing.T) {\n\tv := value{raw: json.RawMessage(`[\"foo\", 42.3, {\"x\": 5}]`)}\n\tif len(v.Array()) != 3 {\n\t\tt.Fail()\n\t}\n\tif v.Array()[0].String() != \"foo\" {\n\t\tt.Fail()\n\t}\n\tif v.Array()[1].Float() != 42.3 {\n\t\tt.Fail()\n\t}\n\tif v.Array()[2].Object()[\"x\"].Int() != 5 {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestRawValue(t *testing.T) {\n\tvar v Value\n\n\tv = value{raw: json.RawMessage(nil)}\n\tif v.Bytes() != nil {\n\t\tt.Fail()\n\t}\n\n\tv = value{raw: json.RawMessage(`\"hello\"`)}\n\tif !bytes.Equal(v.Bytes(), []byte(`\"hello\"`)) {\n\t\tt.Fail()\n\t}\n}\n"
  }
]