Full Code of zserge/lorca for AI

master 36a77caf0fc7 cached
25 files
46.7 KB
14.6k tokens
77 symbols
1 requests
Download .txt
Repository: zserge/lorca
Branch: master
Commit: 36a77caf0fc7
Files: 25
Total size: 46.7 KB

Directory structure:
gitextract_tqfsxivb/

├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── chrome.go
├── chrome_test.go
├── examples/
│   ├── counter/
│   │   ├── build-linux.sh
│   │   ├── build-macos.sh
│   │   ├── build-windows.bat
│   │   ├── icons/
│   │   │   └── icon.icns
│   │   ├── main.go
│   │   └── www/
│   │       └── index.html
│   ├── hello/
│   │   └── main.go
│   └── stopwatch/
│       └── main.go
├── export.go
├── go.mod
├── go.sum
├── locate.go
├── locate_test.go
├── messagebox.go
├── messagebox_windows.go
├── ui.go
├── ui_test.go
├── value.go
└── value_test.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/ci.yml
================================================
name: CI Pipeline
on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: Install Chrome
        run: |
          sudo apt-get update
          sudo apt-get install -yqq google-chrome-stable
      - name: Install Go
        uses: actions/setup-go@v2
        with:
          go-version: '1.16'
      - name: Run tests
        run: go test -v -race ./...
      - name: Build examples
        env:
          CGO_ENABLED: 0
        run: |
          go build -o example-hello ./examples/hello
          go build -o example-stopwatch ./examples/stopwatch
          go build -o example-counter ./examples/counter


================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
example/Example.app

# Test binary, build with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

## JetBrains Idea and Goland
/.idea/**/*.*
/.idea/\$CACHE_FILE\$
/.idea/dataSources/
!/.idea/inspectionProfiles/Project_Default.xml
!/.idea/dictionaries/*.xml
!/.idea/go.xml


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2018 Serge Zaitsev

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Lorca

[![Build Status](https://img.shields.io/github/workflow/status/zserge/lorca/CI%20Pipeline)](https://github.com/zserge/lorca)
[![GoDoc](https://godoc.org/github.com/zserge/lorca?status.svg)](https://godoc.org/github.com/zserge/lorca)
[![Go Report Card](https://goreportcard.com/badge/github.com/zserge/lorca)](https://goreportcard.com/report/github.com/zserge/lorca)

<div>
<img align="left" src="https://raw.githubusercontent.com/zserge/lorca/master/lorca.png" alt="Lorca" width="128px" height="128px" />
<br/>
<p>
	A very small library to build modern HTML5 desktop apps in Go. It uses Chrome
	browser as a UI layer. Unlike Electron it doesn't bundle Chrome into the app
	package, but rather reuses the one that is already installed. Lorca
	establishes a connection to the browser window and allows calling Go code
	from the UI and manipulating UI from Go in a seamless manner.
</p>
<br/>
</div>


## Features

* Pure Go library (no cgo) with a very simple API
* Small application size (normally 5-10MB)
* Best of both worlds - the whole power of HTML/CSS to make your UI look
	good, combined with Go performance and ease of development
* Expose Go functions/methods and call them from JavaScript
* Call arbitrary JavaScript code from Go
* Asynchronous flow between UI and main app in both languages (async/await and Goroutines)
* Supports loading web UI from the local web server or via data URL
* Supports testing your app with the UI in the headless mode
* Supports multiple app windows
* Supports packaging and branding (e.g. custom app icons). Packaging for all
	three OS can be done on a single machine using GOOS and GOARCH variables.

Also, limitations by design:

* Requires Chrome/Chromium >= 70 to be installed.
* No control over the Chrome window yet (e.g. you can't remove border, make it
	transparent, control position or size).
* No window menu (tray menus and native OS dialogs are still possible via
	3rd-party libraries)

If you want to have more control of the browser window - consider using
[webview](https://github.com/zserge/webview) library with a similar API, so
migration would be smooth.

## Example

```go
ui, _ := lorca.New("", "", 480, 320)
defer ui.Close()

// Bind Go function to be available in JS. Go function may be long-running and
// blocking - in JS it's represented with a Promise.
ui.Bind("add", func(a, b int) int { return a + b })

// Call JS function from Go. Functions may be asynchronous, i.e. return promises
n := ui.Eval(`Math.random()`).Float()
fmt.Println(n)

// Call JS that calls Go and so on and so on...
m := ui.Eval(`add(2, 3)`).Int()
fmt.Println(m)

// Wait for the browser window to be closed
<-ui.Done()
```

<p align="center"><img src="examples/counter/counter.gif" /></p>

Also, see [examples](examples) for more details about binding functions and packaging binaries.

## Hello World

Here are the steps to run the hello world example.

```
cd examples/counter
go get
go run ./
```

## How it works

Under 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.

## What's in a name?

> There is kind of a legend, that before his execution Garcia Lorca have seen a
> sunrise over the heads of the soldiers and he said "And yet, the sun rises...".
> Probably it was the beginning of a poem. (J. Brodsky)

Lorca is an anagram of [Carlo](https://github.com/GoogleChromeLabs/carlo/), a
project with a similar goal for Node.js.

## License

Code is distributed under MIT license, feel free to use it in your proprietary
projects as well.



================================================
FILE: chrome.go
================================================
package lorca

import (
	"bufio"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os/exec"
	"regexp"
	"sync"
	"sync/atomic"

	"golang.org/x/net/websocket"
)

type h = map[string]interface{}

// Result is a struct for the resulting value of the JS expression or an error.
type result struct {
	Value json.RawMessage
	Err   error
}

type bindingFunc func(args []json.RawMessage) (interface{}, error)

// Msg is a struct for incoming messages (results and async events)
type msg struct {
	ID     int             `json:"id"`
	Result json.RawMessage `json:"result"`
	Error  json.RawMessage `json:"error"`
	Method string          `json:"method"`
	Params json.RawMessage `json:"params"`
}

type chrome struct {
	sync.Mutex
	cmd      *exec.Cmd
	ws       *websocket.Conn
	id       int32
	target   string
	session  string
	window   int
	pending  map[int]chan result
	bindings map[string]bindingFunc
}

func newChromeWithArgs(chromeBinary string, args ...string) (*chrome, error) {
	// The first two IDs are used internally during the initialization
	c := &chrome{
		id:       2,
		pending:  map[int]chan result{},
		bindings: map[string]bindingFunc{},
	}

	// Start chrome process
	c.cmd = exec.Command(chromeBinary, args...)
	pipe, err := c.cmd.StderrPipe()
	if err != nil {
		return nil, err
	}
	if err := c.cmd.Start(); err != nil {
		return nil, err
	}

	// Wait for websocket address to be printed to stderr
	re := regexp.MustCompile(`^DevTools listening on (ws://.*?)\r?\n$`)
	m, err := readUntilMatch(pipe, re)
	if err != nil {
		c.kill()
		return nil, err
	}
	wsURL := m[1]

	// Open a websocket
	c.ws, err = websocket.Dial(wsURL, "", "http://127.0.0.1")
	if err != nil {
		c.kill()
		return nil, err
	}

	// Find target and initialize session
	c.target, err = c.findTarget()
	if err != nil {
		c.kill()
		return nil, err
	}

	c.session, err = c.startSession(c.target)
	if err != nil {
		c.kill()
		return nil, err
	}
	go c.readLoop()
	for method, args := range map[string]h{
		"Page.enable":          nil,
		"Target.setAutoAttach": {"autoAttach": true, "waitForDebuggerOnStart": false},
		"Network.enable":       nil,
		"Runtime.enable":       nil,
		"Security.enable":      nil,
		"Performance.enable":   nil,
		"Log.enable":           nil,
	} {
		if _, err := c.send(method, args); err != nil {
			c.kill()
			c.cmd.Wait()
			return nil, err
		}
	}

	if !contains(args, "--headless") {
		win, err := c.getWindowForTarget(c.target)
		if err != nil {
			c.kill()
			return nil, err
		}
		c.window = win.WindowID
	}

	return c, nil
}

func (c *chrome) findTarget() (string, error) {
	err := websocket.JSON.Send(c.ws, h{
		"id": 0, "method": "Target.setDiscoverTargets", "params": h{"discover": true},
	})
	if err != nil {
		return "", err
	}
	for {
		m := msg{}
		if err = websocket.JSON.Receive(c.ws, &m); err != nil {
			return "", err
		} else if m.Method == "Target.targetCreated" {
			target := struct {
				TargetInfo struct {
					Type string `json:"type"`
					ID   string `json:"targetId"`
				} `json:"targetInfo"`
			}{}
			if err := json.Unmarshal(m.Params, &target); err != nil {
				return "", err
			} else if target.TargetInfo.Type == "page" {
				return target.TargetInfo.ID, nil
			}
		}
	}
}

func (c *chrome) startSession(target string) (string, error) {
	err := websocket.JSON.Send(c.ws, h{
		"id": 1, "method": "Target.attachToTarget", "params": h{"targetId": target},
	})
	if err != nil {
		return "", err
	}
	for {
		m := msg{}
		if err = websocket.JSON.Receive(c.ws, &m); err != nil {
			return "", err
		} else if m.ID == 1 {
			if m.Error != nil {
				return "", errors.New("Target error: " + string(m.Error))
			}
			session := struct {
				ID string `json:"sessionId"`
			}{}
			if err := json.Unmarshal(m.Result, &session); err != nil {
				return "", err
			}
			return session.ID, nil
		}
	}
}

// WindowState defines the state of the Chrome window, possible values are
// "normal", "maximized", "minimized" and "fullscreen".
type WindowState string

const (
	// WindowStateNormal defines a normal state of the browser window
	WindowStateNormal WindowState = "normal"
	// WindowStateMaximized defines a maximized state of the browser window
	WindowStateMaximized WindowState = "maximized"
	// WindowStateMinimized defines a minimized state of the browser window
	WindowStateMinimized WindowState = "minimized"
	// WindowStateFullscreen defines a fullscreen state of the browser window
	WindowStateFullscreen WindowState = "fullscreen"
)

// Bounds defines settable window properties.
type Bounds struct {
	Left        int         `json:"left"`
	Top         int         `json:"top"`
	Width       int         `json:"width"`
	Height      int         `json:"height"`
	WindowState WindowState `json:"windowState"`
}

type windowTargetMessage struct {
	WindowID int    `json:"windowId"`
	Bounds   Bounds `json:"bounds"`
}

func (c *chrome) getWindowForTarget(target string) (windowTargetMessage, error) {
	var m windowTargetMessage
	msg, err := c.send("Browser.getWindowForTarget", h{"targetId": target})
	if err != nil {
		return m, err
	}
	err = json.Unmarshal(msg, &m)
	return m, err
}

type targetMessageTemplate struct {
	ID     int    `json:"id"`
	Method string `json:"method"`
	Params struct {
		Name    string `json:"name"`
		Payload string `json:"payload"`
		ID      int    `json:"executionContextId"`
		Args    []struct {
			Type  string      `json:"type"`
			Value interface{} `json:"value"`
		} `json:"args"`
	} `json:"params"`
	Error struct {
		Message string `json:"message"`
	} `json:"error"`
	Result json.RawMessage `json:"result"`
}

type targetMessage struct {
	targetMessageTemplate
	Result struct {
		Result struct {
			Type        string          `json:"type"`
			Subtype     string          `json:"subtype"`
			Description string          `json:"description"`
			Value       json.RawMessage `json:"value"`
			ObjectID    string          `json:"objectId"`
		} `json:"result"`
		Exception struct {
			Exception struct {
				Value json.RawMessage `json:"value"`
			} `json:"exception"`
		} `json:"exceptionDetails"`
	} `json:"result"`
}

func (c *chrome) readLoop() {
	for {
		m := msg{}
		if err := websocket.JSON.Receive(c.ws, &m); err != nil {
			return
		}

		if m.Method == "Target.receivedMessageFromTarget" {
			params := struct {
				SessionID string `json:"sessionId"`
				Message   string `json:"message"`
			}{}
			json.Unmarshal(m.Params, &params)
			if params.SessionID != c.session {
				continue
			}
			res := targetMessage{}
			json.Unmarshal([]byte(params.Message), &res)

			if res.ID == 0 && res.Method == "Runtime.consoleAPICalled" || res.Method == "Runtime.exceptionThrown" {
				log.Println(params.Message)
			} else if res.ID == 0 && res.Method == "Runtime.bindingCalled" {
				payload := struct {
					Name string            `json:"name"`
					Seq  int               `json:"seq"`
					Args []json.RawMessage `json:"args"`
				}{}
				json.Unmarshal([]byte(res.Params.Payload), &payload)

				c.Lock()
				binding, ok := c.bindings[res.Params.Name]
				c.Unlock()
				if ok {
					jsString := func(v interface{}) string { b, _ := json.Marshal(v); return string(b) }
					go func() {
						result, error := "", `""`
						if r, err := binding(payload.Args); err != nil {
							error = jsString(err.Error())
						} else if b, err := json.Marshal(r); err != nil {
							error = jsString(err.Error())
						} else {
							result = string(b)
						}
						expr := fmt.Sprintf(`
							if (%[4]s) {
								window['%[1]s']['errors'].get(%[2]d)(%[4]s);
							} else {
								window['%[1]s']['callbacks'].get(%[2]d)(%[3]s);
							}
							window['%[1]s']['callbacks'].delete(%[2]d);
							window['%[1]s']['errors'].delete(%[2]d);
							`, payload.Name, payload.Seq, result, error)
						c.send("Runtime.evaluate", h{"expression": expr, "contextId": res.Params.ID})
					}()
				}
				continue
			}

			c.Lock()
			resc, ok := c.pending[res.ID]
			delete(c.pending, res.ID)
			c.Unlock()

			if !ok {
				continue
			}

			if res.Error.Message != "" {
				resc <- result{Err: errors.New(res.Error.Message)}
			} else if res.Result.Exception.Exception.Value != nil {
				resc <- result{Err: errors.New(string(res.Result.Exception.Exception.Value))}
			} else if res.Result.Result.Type == "object" && res.Result.Result.Subtype == "error" {
				resc <- result{Err: errors.New(res.Result.Result.Description)}
			} else if res.Result.Result.Type != "" {
				resc <- result{Value: res.Result.Result.Value}
			} else {
				res := targetMessageTemplate{}
				json.Unmarshal([]byte(params.Message), &res)
				resc <- result{Value: res.Result}
			}
		} else if m.Method == "Target.targetDestroyed" {
			params := struct {
				TargetID string `json:"targetId"`
			}{}
			json.Unmarshal(m.Params, &params)
			if params.TargetID == c.target {
				c.kill()
				return
			}
		}
	}
}

func (c *chrome) send(method string, params h) (json.RawMessage, error) {
	id := atomic.AddInt32(&c.id, 1)
	b, err := json.Marshal(h{"id": int(id), "method": method, "params": params})
	if err != nil {
		return nil, err
	}
	resc := make(chan result)
	c.Lock()
	c.pending[int(id)] = resc
	c.Unlock()

	if err := websocket.JSON.Send(c.ws, h{
		"id":     int(id),
		"method": "Target.sendMessageToTarget",
		"params": h{"message": string(b), "sessionId": c.session},
	}); err != nil {
		return nil, err
	}
	res := <-resc
	return res.Value, res.Err
}

func (c *chrome) load(url string) error {
	_, err := c.send("Page.navigate", h{"url": url})
	return err
}

func (c *chrome) eval(expr string) (json.RawMessage, error) {
	return c.send("Runtime.evaluate", h{"expression": expr, "awaitPromise": true, "returnByValue": true})
}

func (c *chrome) bind(name string, f bindingFunc) error {
	c.Lock()
	// check if binding already exists
	_, exists := c.bindings[name]

	c.bindings[name] = f
	c.Unlock()

	if exists {
		// Just replace callback and return, as the binding was already added to js
		// and adding it again would break it.
		return nil
	}

	if _, err := c.send("Runtime.addBinding", h{"name": name}); err != nil {
		return err
	}
	script := fmt.Sprintf(`(() => {
	const bindingName = '%s';
	const binding = window[bindingName];
	window[bindingName] = async (...args) => {
		const me = window[bindingName];
		let errors = me['errors'];
		let callbacks = me['callbacks'];
		if (!callbacks) {
			callbacks = new Map();
			me['callbacks'] = callbacks;
		}
		if (!errors) {
			errors = new Map();
			me['errors'] = errors;
		}
		const seq = (me['lastSeq'] || 0) + 1;
		me['lastSeq'] = seq;
		const promise = new Promise((resolve, reject) => {
			callbacks.set(seq, resolve);
			errors.set(seq, reject);
		});
		binding(JSON.stringify({name: bindingName, seq, args}));
		return promise;
	}})();
	`, name)
	_, err := c.send("Page.addScriptToEvaluateOnNewDocument", h{"source": script})
	if err != nil {
		return err
	}
	_, err = c.eval(script)
	return err
}

func (c *chrome) setBounds(b Bounds) error {
	if b.WindowState == "" {
		b.WindowState = WindowStateNormal
	}
	param := h{"windowId": c.window, "bounds": b}
	if b.WindowState != WindowStateNormal {
		param["bounds"] = h{"windowState": b.WindowState}
	}
	_, err := c.send("Browser.setWindowBounds", param)
	return err
}

func (c *chrome) bounds() (Bounds, error) {
	result, err := c.send("Browser.getWindowBounds", h{"windowId": c.window})
	if err != nil {
		return Bounds{}, err
	}
	bounds := struct {
		Bounds Bounds `json:"bounds"`
	}{}
	err = json.Unmarshal(result, &bounds)
	return bounds.Bounds, err
}

func (c *chrome) pdf(width, height int) ([]byte, error) {
	result, err := c.send("Page.printToPDF", h{
		"paperWidth":  float32(width) / 96,
		"paperHeight": float32(height) / 96,
	})
	if err != nil {
		return nil, err
	}
	pdf := struct {
		Data []byte `json:"data"`
	}{}
	err = json.Unmarshal(result, &pdf)
	return pdf.Data, err
}

func (c *chrome) png(x, y, width, height int, bg uint32, scale float32) ([]byte, error) {
	if x == 0 && y == 0 && width == 0 && height == 0 {
		// By default either use SVG size if it's an SVG, or use A4 page size
		bounds, 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]`)
		if err != nil {
			return nil, err
		}
		rect := make([]int, 4)
		if err := json.Unmarshal(bounds, &rect); err != nil {
			return nil, err
		}
		x, y, width, height = rect[0], rect[1], rect[2], rect[3]
	}

	_, err := c.send("Emulation.setDefaultBackgroundColorOverride", h{
		"color": h{
			"r": (bg >> 16) & 0xff,
			"g": (bg >> 8) & 0xff,
			"b": bg & 0xff,
			"a": (bg >> 24) & 0xff,
		},
	})
	if err != nil {
		return nil, err
	}
	result, err := c.send("Page.captureScreenshot", h{
		"clip": h{
			"x": x, "y": y, "width": width, "height": height, "scale": scale,
		},
	})
	if err != nil {
		return nil, err
	}
	pdf := struct {
		Data []byte `json:"data"`
	}{}
	err = json.Unmarshal(result, &pdf)
	return pdf.Data, err
}

func (c *chrome) kill() error {
	if c.ws != nil {
		if err := c.ws.Close(); err != nil {
			return err
		}
	}
	// TODO: cancel all pending requests
	if state := c.cmd.ProcessState; state == nil || !state.Exited() {
		return c.cmd.Process.Kill()
	}
	return nil
}

func readUntilMatch(r io.ReadCloser, re *regexp.Regexp) ([]string, error) {
	br := bufio.NewReader(r)
	for {
		if line, err := br.ReadString('\n'); err != nil {
			r.Close()
			return nil, err
		} else if m := re.FindStringSubmatch(line); m != nil {
			go io.Copy(ioutil.Discard, br)
			return m, nil
		}
	}
}

func contains(arr []string, x string) bool {
	for _, n := range arr {
		if x == n {
			return true
		}
	}
	return false
}


================================================
FILE: chrome_test.go
================================================
package lorca

import (
	"encoding/json"
	"errors"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
)

func TestChromeEval(t *testing.T) {
	c, err := newChromeWithArgs(ChromeExecutable(), "--user-data-dir=/tmp", "--headless", "--remote-debugging-port=0")
	if err != nil {
		t.Fatal(err)
	}
	defer c.kill()

	for _, test := range []struct {
		Expr   string
		Result string
		Error  string
	}{
		{Expr: ``, Result: ``},
		{Expr: `42`, Result: `42`},
		{Expr: `2+3`, Result: `5`},
		{Expr: `(() => ({x: 5, y: 7}))()`, Result: `{"x":5,"y":7}`},
		{Expr: `(() => ([1,'foo',false]))()`, Result: `[1,"foo",false]`},
		{Expr: `((a, b) => a*b)(3, 7)`, Result: `21`},
		{Expr: `Promise.resolve(42)`, Result: `42`},
		{Expr: `Promise.reject('foo')`, Error: `"foo"`},
		{Expr: `throw "bar"`, Error: `"bar"`},
		{Expr: `2+`, Error: `SyntaxError: Unexpected end of input`},
	} {
		result, err := c.eval(test.Expr)
		if err != nil {
			if err.Error() != test.Error {
				t.Fatal(test.Expr, err, test.Error)
			}
		} else if string(result) != test.Result {
			t.Fatal(test.Expr, string(result), test.Result)
		}
	}
}

func TestChromeLoad(t *testing.T) {
	c, err := newChromeWithArgs(ChromeExecutable(), "--user-data-dir=/tmp", "--headless", "--remote-debugging-port=0")
	if err != nil {
		t.Fatal(err)
	}
	defer c.kill()
	if err := c.load("data:text/html,<html><body>Hello</body></html>"); err != nil {
		t.Fatal(err)
	}
	for i := 0; i < 10; i++ {
		url, err := c.eval(`window.location.href`)
		if err != nil {
			t.Fatal(err)
		}
		if strings.HasPrefix(string(url), `"data:text/html,`) {
			break
		}
	}
	if res, err := c.eval(`document.body ? document.body.innerText :
			new Promise(res => window.onload = () => res(document.body.innerText))`); err != nil {
		t.Fatal(err)
	} else if string(res) != `"Hello"` {
		t.Fatal(res)
	}
}

func TestChromeBind(t *testing.T) {
	c, err := newChromeWithArgs(ChromeExecutable(), "--user-data-dir=/tmp", "--headless", "--remote-debugging-port=0")
	if err != nil {
		t.Fatal(err)
	}
	defer c.kill()

	if err := c.bind("add", func(args []json.RawMessage) (interface{}, error) {
		a, b := 0, 0
		if len(args) != 2 {
			return nil, errors.New("2 arguments expected")
		}
		if err := json.Unmarshal(args[0], &a); err != nil {
			return nil, err
		}
		if err := json.Unmarshal(args[1], &b); err != nil {
			return nil, err
		}
		return a + b, nil
	}); err != nil {
		t.Fatal(err)
	}

	if res, err := c.eval(`window.add(2, 3)`); err != nil {
		t.Fatal(err)
	} else if string(res) != `5` {
		t.Fatal(string(res))
	}

	if res, err := c.eval(`window.add("foo", "bar")`); err == nil {
		t.Fatal(string(res), err)
	}
	if res, err := c.eval(`window.add(1, 2, 3)`); err == nil {
		t.Fatal(res, err)
	}
}

func TestChromeAsync(t *testing.T) {
	c, err := newChromeWithArgs(ChromeExecutable(), "--user-data-dir=/tmp", "--headless", "--remote-debugging-port=0")
	if err != nil {
		t.Fatal(err)
	}
	defer c.kill()

	if err := c.bind("len", func(args []json.RawMessage) (interface{}, error) {
		return len(args[0]), nil
	}); err != nil {
		t.Fatal(err)
	}

	wg := &sync.WaitGroup{}
	n := 10
	failed := int32(0)
	wg.Add(n)
	for i := 0; i < n; i++ {
		go func(i int) {
			defer wg.Done()
			v, err := c.eval("len('hello')")
			if string(v) != `7` {
				atomic.StoreInt32(&failed, 1)
			} else if err != nil {
				atomic.StoreInt32(&failed, 2)
			}
		}(i)
	}
	wg.Wait()

	if status := atomic.LoadInt32(&failed); status != 0 {
		t.Fatal()
	}
}


================================================
FILE: examples/counter/build-linux.sh
================================================
#!/bin/sh

APP=lorca-example
APPDIR=${APP}_1.0.0

mkdir -p $APPDIR/usr/bin
mkdir -p $APPDIR/usr/share/applications
mkdir -p $APPDIR/usr/share/icons/hicolor/1024x1024/apps
mkdir -p $APPDIR/usr/share/icons/hicolor/256x256/apps
mkdir -p $APPDIR/DEBIAN

go build -o $APPDIR/usr/bin/$APP

cp icons/icon.png $APPDIR/usr/share/icons/hicolor/1024x1024/apps/${APP}.png
cp icons/icon.png $APPDIR/usr/share/icons/hicolor/256x256/apps/${APP}.png

cat > $APPDIR/usr/share/applications/${APP}.desktop << EOF
[Desktop Entry]
Version=1.0
Type=Application
Name=$APP
Exec=$APP
Icon=$APP
Terminal=false
StartupWMClass=Lorca
EOF

cat > $APPDIR/DEBIAN/control << EOF
Package: ${APP}
Version: 1.0-0
Section: base
Priority: optional
Architecture: amd64
Maintainer: Serge Zaitsev <zaitsev.serge@gmail.com>
Description: Example for Lorca GUI toolkit
EOF

dpkg-deb --build $APPDIR


================================================
FILE: examples/counter/build-macos.sh
================================================
#!/bin/sh

APP="Example.app"
mkdir -p $APP/Contents/{MacOS,Resources}
go build -o $APP/Contents/MacOS/lorca-example
cat > $APP/Contents/Info.plist << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleExecutable</key>
	<string>lorca-example</string>
	<key>CFBundleIconFile</key>
	<string>icon.icns</string>
	<key>CFBundleIdentifier</key>
	<string>com.zserge.lorca.example</string>
</dict>
</plist>
EOF
cp icons/icon.icns $APP/Contents/Resources/icon.icns
find $APP


================================================
FILE: examples/counter/build-windows.bat
================================================
@echo off
go generate
go build -ldflags "-H windowsgui" -o lorca-example.exe


================================================
FILE: examples/counter/main.go
================================================
package main

import (
	"embed"
	"fmt"
	"log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"runtime"
	"sync"

	"github.com/zserge/lorca"
)

//go:embed www
var fs embed.FS

// Go types that are bound to the UI must be thread-safe, because each binding
// is executed in its own goroutine. In this simple case we may use atomic
// operations, but for more complex cases one should use proper synchronization.
type counter struct {
	sync.Mutex
	count int
}

func (c *counter) Add(n int) {
	c.Lock()
	defer c.Unlock()
	c.count = c.count + n
}

func (c *counter) Value() int {
	c.Lock()
	defer c.Unlock()
	return c.count
}

func main() {
	args := []string{}
	if runtime.GOOS == "linux" {
		args = append(args, "--class=Lorca")
	}
	ui, err := lorca.New("", "", 480, 320, args...)
	if err != nil {
		log.Fatal(err)
	}
	defer ui.Close()

	// A simple way to know when UI is ready (uses body.onload event in JS)
	ui.Bind("start", func() {
		log.Println("UI is ready")
	})

	// Create and bind Go object to the UI
	c := &counter{}
	ui.Bind("counterAdd", c.Add)
	ui.Bind("counterValue", c.Value)

	// Load HTML.
	// You may also use `data:text/html,<base64>` approach to load initial HTML,
	// e.g: ui.Load("data:text/html," + url.PathEscape(html))

	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		log.Fatal(err)
	}
	defer ln.Close()
	go http.Serve(ln, http.FileServer(http.FS(fs)))
	ui.Load(fmt.Sprintf("http://%s/www", ln.Addr()))

	// You may use console.log to debug your JS code, it will be printed via
	// log.Println(). Also exceptions are printed in a similar manner.
	ui.Eval(`
		console.log("Hello, world!");
		console.log('Multiple values:', [1, false, {"x":5}]);
	`)

	// Wait until the interrupt signal arrives or browser window is closed
	sigc := make(chan os.Signal)
	signal.Notify(sigc, os.Interrupt)
	select {
	case <-sigc:
	case <-ui.Done():
	}

	log.Println("exiting...")
}


================================================
FILE: examples/counter/www/index.html
================================================
<!doctype html>
<html>
	<head>
		<title>Counter</title>
		<link rel="shortcut icon" href="favicon.png">
		<style>
		* { margin: 0; padding: 0; box-sizing: border-box; user-select: none; }
		body { height: 100vh; display: flex; align-items: center; justify-content: center; background-color: #f1c40f; font-family: 'Helvetika Neue', Arial, sans-serif; font-size: 28px; }
		.counter-container { display: flex; flex-direction: column; align-items: center; }
		.counter { text-transform: uppercase; color: #fff; font-weight: bold; font-size: 3rem; }
		.btn-row { display: flex; align-items: center; margin: 1rem; }
		.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; }
		.btn:hover { box-shadow: 0 4px #8b5e00; top: 2px; }
		.btn:active{ box-shadow: 0 1px #8b5e00; top: 5px; }
		</style>
	</head>
	<body onload=start()>
		<!-- UI layout -->
		<div class="counter-container">
			<div class="counter"></div>
			<div class="btn-row">
				<div class="btn btn-incr">+1</div>
				<div class="btn btn-decr">-1</div>
			</div>
		</div>

		<!-- Connect UI actions to Go functions -->
		<script>
			const counter = document.querySelector('.counter');
			const btnIncr = document.querySelector('.btn-incr');
			const btnDecr = document.querySelector('.btn-decr');

			// We use async/await because Go functions are asynchronous
			const render = async () => {
				counter.innerText = `Count: ${await window.counterValue()}`;
			};

			btnIncr.addEventListener('click', async () => {
				await counterAdd(1); // Call Go function
				render();
			});

			btnDecr.addEventListener('click', async () => {
				await counterAdd(-1); // Call Go function
				render();
			});

			render();
		</script>
	</body>
</html>


================================================
FILE: examples/hello/main.go
================================================
package main

import (
	"log"
	"net/url"

	"github.com/zserge/lorca"
)

func main() {
	// Create UI with basic HTML passed via data URI
	ui, err := lorca.New("data:text/html,"+url.PathEscape(`
	<html>
		<head><title>Hello</title></head>
		<body><h1>Hello, world!</h1></body>
	</html>
	`), "", 480, 320)
	if err != nil {
		log.Fatal(err)
	}
	defer ui.Close()
	// Wait until UI window is closed
	<-ui.Done()
}


================================================
FILE: examples/stopwatch/main.go
================================================
package main

import (
	"fmt"
	"log"
	"net/url"
	"sync/atomic"
	"time"

	"github.com/zserge/lorca"
)

func main() {
	ui, err := lorca.New("", "", 480, 320)
	if err != nil {
		log.Fatal(err)
	}
	defer ui.Close()

	// Data model: number of ticks
	ticks := uint32(0)
	// Channel to connect UI events with the background ticking goroutine
	togglec := make(chan bool)
	// Bind Go functions to JS
	ui.Bind("toggle", func() { togglec <- true })
	ui.Bind("reset", func() {
		atomic.StoreUint32(&ticks, 0)
		ui.Eval(`document.querySelector('.timer').innerText = '0'`)
	})

	// Load HTML after Go functions are bound to JS
	ui.Load("data:text/html," + url.PathEscape(`
	<html>
		<body>
			<!-- toggle() and reset() are Go functions wrapped into JS -->
			<div class="timer" onclick="toggle()"></div>
			<button onclick="reset()">Reset</button>
		</body>
	</html>
	`))

	// Start ticker goroutine
	go func() {
		t := time.NewTicker(100 * time.Millisecond)
		for {
			select {
			case <-t.C: // Every 100ms increate number of ticks and update UI
				ui.Eval(fmt.Sprintf(`document.querySelector('.timer').innerText = 0.1*%d`,
					atomic.AddUint32(&ticks, 1)))
			case <-togglec: // If paused - wait for another toggle event to unpause
				<-togglec
			}
		}
	}()
	<-ui.Done()
}


================================================
FILE: export.go
================================================
package lorca

import (
	"fmt"
	"io/ioutil"
	"os"
)

const (
	// PageA4Width is a width of an A4 page in pixels at 96dpi
	PageA4Width = 816
	// PageA4Height is a height of an A4 page in pixels at 96dpi
	PageA4Height = 1056
)

// PDF converts a given URL (may be a local file) to a PDF file. Script is
// evaluated before the page is printed to PDF, you may modify the contents of
// the page there of wait until the page is fully rendered. Width and height
// are page bounds in pixels. PDF by default uses 96dpi density. For A4 page
// you may use PageA4Width and PageA4Height constants.
func PDF(url, script string, width, height int) ([]byte, error) {
	return doHeadless(url, func(c *chrome) ([]byte, error) {
		if _, err := c.eval(script); err != nil {
			return nil, err
		}
		return c.pdf(width, height)
	})
}

// PNG converts a given URL (may be a local file) to a PNG image. Script is
// evaluated before the "screenshot" is taken, so you can modify the contents
// of a URL there. Image bounds are provides in pixels. Background is in ARGB
// format, the default value of zero keeps the background transparent. Scale
// allows zooming the page in and out.
//
// This function is most convenient to convert SVG to PNG of different sizes,
// for example when preparing Lorca app icons.
func PNG(url, script string, x, y, width, height int, bg uint32, scale float32) ([]byte, error) {
	return doHeadless(url, func(c *chrome) ([]byte, error) {
		if _, err := c.eval(script); err != nil {
			return nil, err
		}
		return c.png(x, y, width, height, bg, scale)
	})
}

func doHeadless(url string, f func(c *chrome) ([]byte, error)) ([]byte, error) {
	dir, err := ioutil.TempDir("", "lorca")
	if err != nil {
		return nil, err
	}
	defer os.RemoveAll(dir)
	args := append(defaultChromeArgs, fmt.Sprintf("--user-data-dir=%s", dir), "--remote-debugging-port=0", "--headless", url)
	chrome, err := newChromeWithArgs(ChromeExecutable(), args...)
	if err != nil {
		return nil, err
	}
	defer chrome.kill()
	return f(chrome)
}


================================================
FILE: go.mod
================================================
module github.com/zserge/lorca

go 1.16

require golang.org/x/net v0.0.0-20200222125558-5a598a2470a0


================================================
FILE: go.sum
================================================
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=


================================================
FILE: locate.go
================================================
package lorca

import (
	"os"
	"os/exec"
	"runtime"
	"strings"
)

// ChromeExecutable returns a string which points to the preferred Chrome
// executable file.
var ChromeExecutable = LocateChrome

// LocateChrome returns a path to the Chrome binary, or an empty string if
// Chrome installation is not found.
func LocateChrome() string {

	// If env variable "LORCACHROME" specified and it exists
	if path, ok := os.LookupEnv("LORCACHROME"); ok {
		if _, err := os.Stat(path); err == nil {
			return path
		}
	}

	var paths []string
	switch runtime.GOOS {
	case "darwin":
		paths = []string{
			"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
			"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
			"/Applications/Chromium.app/Contents/MacOS/Chromium",
			"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
			"/usr/bin/google-chrome-stable",
			"/usr/bin/google-chrome",
			"/usr/bin/chromium",
			"/usr/bin/chromium-browser",
		}
	case "windows":
		paths = []string{
			os.Getenv("LocalAppData") + "/Google/Chrome/Application/chrome.exe",
			os.Getenv("ProgramFiles") + "/Google/Chrome/Application/chrome.exe",
			os.Getenv("ProgramFiles(x86)") + "/Google/Chrome/Application/chrome.exe",
			os.Getenv("LocalAppData") + "/Chromium/Application/chrome.exe",
			os.Getenv("ProgramFiles") + "/Chromium/Application/chrome.exe",
			os.Getenv("ProgramFiles(x86)") + "/Chromium/Application/chrome.exe",
			os.Getenv("ProgramFiles(x86)") + "/Microsoft/Edge/Application/msedge.exe",
			os.Getenv("ProgramFiles") + "/Microsoft/Edge/Application/msedge.exe",
		}
	default:
		paths = []string{
			"/usr/bin/google-chrome-stable",
			"/usr/bin/google-chrome",
			"/usr/bin/chromium",
			"/usr/bin/chromium-browser",
			"/snap/bin/chromium",
		}
	}

	for _, path := range paths {
		if _, err := os.Stat(path); os.IsNotExist(err) {
			continue
		}
		return path
	}
	return ""
}

// PromptDownload asks user if he wants to download and install Chrome, and
// opens a download web page if the user agrees.
func PromptDownload() {
	title := "Chrome not found"
	text := "No Chrome/Chromium installation was found. Would you like to download and install it now?"

	// Ask user for confirmation
	if !messageBox(title, text) {
		return
	}

	// Open download page
	url := "https://www.google.com/chrome/"
	switch runtime.GOOS {
	case "linux":
		exec.Command("xdg-open", url).Run()
	case "darwin":
		exec.Command("open", url).Run()
	case "windows":
		r := strings.NewReplacer("&", "^&")
		exec.Command("cmd", "/c", "start", r.Replace(url)).Run()
	}
}


================================================
FILE: locate_test.go
================================================
package lorca

import (
	"os/exec"
	"testing"
)

func TestLocate(t *testing.T) {
	if exe := ChromeExecutable(); exe == "" {
		t.Fatal()
	} else {
		t.Log(exe)
		b, err := exec.Command(exe, "--version").CombinedOutput()
		t.Log(string(b))
		t.Log(err)
	}
}


================================================
FILE: messagebox.go
================================================
//+build !windows

package lorca

import (
	"fmt"
	"os/exec"
	"runtime"
	"strings"
	"syscall"
)

func messageBox(title, text string) bool {
	if runtime.GOOS == "linux" {
		err := exec.Command("zenity", "--question", "--title", title, "--text", text).Run()
		if err != nil {
			if exitError, ok := err.(*exec.ExitError); ok {
				return exitError.Sys().(syscall.WaitStatus).ExitStatus() == 0
			}
		}
	} else if runtime.GOOS == "darwin" {
		script := `set T to button returned of ` +
			`(display dialog "%s" with title "%s" buttons {"No", "Yes"} default button "Yes")`
		out, err := exec.Command("osascript", "-e", fmt.Sprintf(script, text, title)).Output()
		if err != nil {
			if exitError, ok := err.(*exec.ExitError); ok {
				return exitError.Sys().(syscall.WaitStatus).ExitStatus() == 0
			}
		}
		return strings.TrimSpace(string(out)) == "Yes"
	}
	return false
}


================================================
FILE: messagebox_windows.go
================================================
//+build windows

package lorca

import (
	"syscall"
	"unsafe"
)

func messageBox(title, text string) bool {
	user32 := syscall.NewLazyDLL("user32.dll")
	messageBoxW := user32.NewProc("MessageBoxW")
	mbYesNo := 0x00000004
	mbIconQuestion := 0x00000020
	idYes := 6
	ret, _, _ := messageBoxW.Call(0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(text))),
		uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))), uintptr(uint(mbYesNo|mbIconQuestion)))
	return int(ret) == idYes
}


================================================
FILE: ui.go
================================================
package lorca

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"reflect"
)

// UI interface allows talking to the HTML5 UI from Go.
type UI interface {
	Load(url string) error
	Bounds() (Bounds, error)
	SetBounds(Bounds) error
	Bind(name string, f interface{}) error
	Eval(js string) Value
	Done() <-chan struct{}
	Close() error
}

type ui struct {
	chrome *chrome
	done   chan struct{}
	tmpDir string
}

var defaultChromeArgs = []string{
	"--disable-background-networking",
	"--disable-background-timer-throttling",
	"--disable-backgrounding-occluded-windows",
	"--disable-breakpad",
	"--disable-client-side-phishing-detection",
	"--disable-default-apps",
	"--disable-dev-shm-usage",
	"--disable-infobars",
	"--disable-extensions",
	"--disable-features=site-per-process",
	"--disable-hang-monitor",
	"--disable-ipc-flooding-protection",
	"--disable-popup-blocking",
	"--disable-prompt-on-repost",
	"--disable-renderer-backgrounding",
	"--disable-sync",
	"--disable-translate",
	"--disable-windows10-custom-titlebar",
	"--metrics-recording-only",
	"--no-first-run",
	"--no-default-browser-check",
	"--safebrowsing-disable-auto-update",
	"--enable-automation",
	"--password-store=basic",
	"--use-mock-keychain",
	"--remote-allow-origins=*",
}

// New returns a new HTML5 UI for the given URL, user profile directory, window
// size and other options passed to the browser engine. If URL is an empty
// string - a blank page is displayed. If user profile directory is an empty
// string - a temporary directory is created and it will be removed on
// ui.Close(). You might want to use "--headless" custom CLI argument to test
// your UI code.
func New(url, dir string, width, height int, customArgs ...string) (UI, error) {
	if url == "" {
		url = "data:text/html,<html></html>"
	}
	tmpDir := ""
	if dir == "" {
		name, err := ioutil.TempDir("", "lorca")
		if err != nil {
			return nil, err
		}
		dir, tmpDir = name, name
	}
	args := append(defaultChromeArgs, fmt.Sprintf("--app=%s", url))
	args = append(args, fmt.Sprintf("--user-data-dir=%s", dir))
	args = append(args, fmt.Sprintf("--window-size=%d,%d", width, height))
	args = append(args, customArgs...)
	args = append(args, "--remote-debugging-port=0")

	chrome, err := newChromeWithArgs(ChromeExecutable(), args...)
	done := make(chan struct{})
	if err != nil {
		return nil, err
	}

	go func() {
		chrome.cmd.Wait()
		close(done)
	}()
	return &ui{chrome: chrome, done: done, tmpDir: tmpDir}, nil
}

func (u *ui) Done() <-chan struct{} {
	return u.done
}

func (u *ui) Close() error {
	// ignore err, as the chrome process might be already dead, when user close the window.
	u.chrome.kill()
	<-u.done
	if u.tmpDir != "" {
		if err := os.RemoveAll(u.tmpDir); err != nil {
			return err
		}
	}
	return nil
}

func (u *ui) Load(url string) error { return u.chrome.load(url) }

func (u *ui) Bind(name string, f interface{}) error {
	v := reflect.ValueOf(f)
	// f must be a function
	if v.Kind() != reflect.Func {
		return errors.New("only functions can be bound")
	}
	// f must return either value and error or just error
	if n := v.Type().NumOut(); n > 2 {
		return errors.New("function may only return a value or a value+error")
	}

	return u.chrome.bind(name, func(raw []json.RawMessage) (interface{}, error) {
		if len(raw) != v.Type().NumIn() {
			return nil, errors.New("function arguments mismatch")
		}
		args := []reflect.Value{}
		for i := range raw {
			arg := reflect.New(v.Type().In(i))
			if err := json.Unmarshal(raw[i], arg.Interface()); err != nil {
				return nil, err
			}
			args = append(args, arg.Elem())
		}
		errorType := reflect.TypeOf((*error)(nil)).Elem()
		res := v.Call(args)
		switch len(res) {
		case 0:
			// No results from the function, just return nil
			return nil, nil
		case 1:
			// One result may be a value, or an error
			if res[0].Type().Implements(errorType) {
				if res[0].Interface() != nil {
					return nil, res[0].Interface().(error)
				}
				return nil, nil
			}
			return res[0].Interface(), nil
		case 2:
			// Two results: first one is value, second is error
			if !res[1].Type().Implements(errorType) {
				return nil, errors.New("second return value must be an error")
			}
			if res[1].Interface() == nil {
				return res[0].Interface(), nil
			}
			return res[0].Interface(), res[1].Interface().(error)
		default:
			return nil, errors.New("unexpected number of return values")
		}
	})
}

func (u *ui) Eval(js string) Value {
	v, err := u.chrome.eval(js)
	return value{err: err, raw: v}
}

func (u *ui) SetBounds(b Bounds) error {
	return u.chrome.setBounds(b)
}

func (u *ui) Bounds() (Bounds, error) {
	return u.chrome.bounds()
}


================================================
FILE: ui_test.go
================================================
package lorca

import (
	"errors"
	"math/rand"
	"strconv"
	"testing"
)

func TestEval(t *testing.T) {
	ui, err := New("", "", 480, 320, "--headless")
	if err != nil {
		t.Fatal(err)
	}
	defer ui.Close()

	if n := ui.Eval(`2+3`).Int(); n != 5 {
		t.Fatal(n)
	}

	if s := ui.Eval(`"foo" + "bar"`).String(); s != "foobar" {
		t.Fatal(s)
	}

	if a := ui.Eval(`[1,2,3].map(n => n *2)`).Array(); a[0].Int() != 2 || a[1].Int() != 4 || a[2].Int() != 6 {
		t.Fatal(a)
	}

	// XXX this probably should be unquoted?
	if err := ui.Eval(`throw "fail"`).Err(); err.Error() != `"fail"` {
		t.Fatal(err)
	}
}

func TestBind(t *testing.T) {
	ui, err := New("", "", 480, 320, "--headless")
	if err != nil {
		t.Fatal(err)
	}
	defer ui.Close()

	if err := ui.Bind("add", func(a, b int) int { return a + b }); err != nil {
		t.Fatal(err)
	}
	if err := ui.Bind("rand", func() int { return rand.Int() }); err != nil {
		t.Fatal(err)
	}
	if err := ui.Bind("strlen", func(s string) int { return len(s) }); err != nil {
		t.Fatal(err)
	}
	if err := ui.Bind("atoi", func(s string) (int, error) { return strconv.Atoi(s) }); err != nil {
		t.Fatal(err)
	}
	if err := ui.Bind("shouldFail", "hello"); err == nil {
		t.Fail()
	}

	if n := ui.Eval(`add(2,3)`); n.Int() != 5 {
		t.Fatal(n)
	}
	if n := ui.Eval(`add(2,3,4)`); n.Err() == nil {
		t.Fatal(n)
	}
	if n := ui.Eval(`add(2)`); n.Err() == nil {
		t.Fatal(n)
	}
	if n := ui.Eval(`add("hello", "world")`); n.Err() == nil {
		t.Fatal(n)
	}
	if n := ui.Eval(`rand()`); n.Err() != nil {
		t.Fatal(n)
	}
	if n := ui.Eval(`rand(100)`); n.Err() == nil {
		t.Fatal(n)
	}
	if n := ui.Eval(`strlen('foo')`); n.Int() != 3 {
		t.Fatal(n)
	}
	if n := ui.Eval(`strlen(123)`); n.Err() == nil {
		t.Fatal(n)
	}
	if n := ui.Eval(`atoi('123')`); n.Int() != 123 {
		t.Fatal(n)
	}
	if n := ui.Eval(`atoi('hello')`); n.Err() == nil {
		t.Fatal(n)
	}
}

func TestFunctionReturnTypes(t *testing.T) {
	ui, err := New("", "", 480, 320, "--headless")
	if err != nil {
		t.Fatal(err)
	}
	defer ui.Close()

	if err := ui.Bind("noResults", func() { return }); err != nil {
		t.Fatal(err)
	}
	if err := ui.Bind("oneNonNilResult", func() interface{} { return 1 }); err != nil {
		t.Fatal(err)
	}
	if err := ui.Bind("oneNilResult", func() interface{} { return nil }); err != nil {
		t.Fatal(err)
	}
	if err := ui.Bind("oneNonNilErrorResult", func() error { return errors.New("error") }); err != nil {
		t.Fatal(err)
	}
	if err := ui.Bind("oneNilErrorResult", func() error { return nil }); err != nil {
		t.Fatal(err)
	}
	if err := ui.Bind("twoResultsNonNilError", func() (interface{}, error) { return nil, errors.New("error") }); err != nil {
		t.Fatal(err)
	}
	if err := ui.Bind("twoResultsNilError", func() (interface{}, error) { return 1, nil }); err != nil {
		t.Fatal(err)
	}
	if err := ui.Bind("twoResultsBothNonNil", func() (interface{}, error) { return 1, errors.New("error") }); err != nil {
		t.Fatal(err)
	}
	if err := ui.Bind("twoResultsBothNil", func() (interface{}, error) { return nil, nil }); err != nil {
		t.Fatal(err)
	}

	if v := ui.Eval(`noResults()`); v.Err() != nil {
		t.Fatal(v)
	}
	if v := ui.Eval(`oneNonNilResult()`); v.Int() != 1 {
		t.Fatal(v)
	}
	if v := ui.Eval(`oneNilResult()`); v.Err() != nil {
		t.Fatal(v)
	}
	if v := ui.Eval(`oneNonNilErrorResult()`); v.Err() == nil {
		t.Fatal(v)
	}
	if v := ui.Eval(`oneNilErrorResult()`); v.Err() != nil {
		t.Fatal(v)
	}
	if v := ui.Eval(`twoResultsNonNilError()`); v.Err() == nil {
		t.Fatal(v)
	}
	if v := ui.Eval(`twoResultsNilError()`); v.Err() != nil || v.Int() != 1 {
		t.Fatal(v)
	}
	if v := ui.Eval(`twoResultsBothNonNil()`); v.Err() == nil {
		t.Fatal(v)
	}
	if v := ui.Eval(`twoResultsBothNil()`); v.Err() != nil {
		t.Fatal(v)
	}
}


================================================
FILE: value.go
================================================
package lorca

import "encoding/json"

// Value is a generic type of a JSON value (primitive, object, array) and
// optionally an error value.
type Value interface {
	Err() error
	To(interface{}) error
	Float() float32
	Int() int
	String() string
	Bool() bool
	Object() map[string]Value
	Array() []Value
	Bytes() []byte
}

type value struct {
	err error
	raw json.RawMessage
}

func (v value) Err() error             { return v.err }
func (v value) Bytes() []byte          { return v.raw }
func (v value) To(x interface{}) error { return json.Unmarshal(v.raw, x) }
func (v value) Float() (f float32)     { v.To(&f); return f }
func (v value) Int() (i int)           { v.To(&i); return i }
func (v value) String() (s string)     { v.To(&s); return s }
func (v value) Bool() (b bool)         { v.To(&b); return b }
func (v value) Array() (values []Value) {
	array := []json.RawMessage{}
	v.To(&array)
	for _, el := range array {
		values = append(values, value{raw: el})
	}
	return values
}
func (v value) Object() (object map[string]Value) {
	object = map[string]Value{}
	kv := map[string]json.RawMessage{}
	v.To(&kv)
	for k, v := range kv {
		object[k] = value{raw: v}
	}
	return object
}


================================================
FILE: value_test.go
================================================
package lorca

import (
	"bytes"
	"encoding/json"
	"errors"
	"testing"
)

var errTest = errors.New("fail")

func TestValueError(t *testing.T) {
	v := value{err: errTest}
	if v.Err() != errTest {
		t.Fail()
	}

	v = value{raw: json.RawMessage(`"hello"`)}
	if v.Err() != nil {
		t.Fail()
	}
}

func TestValuePrimitive(t *testing.T) {
	v := value{raw: json.RawMessage(`42`)}
	if v.Int() != 42 {
		t.Fail()
	}
	v = value{raw: json.RawMessage(`"hello"`)}
	if v.Int() != 0 || v.String() != "hello" {
		t.Fail()
	}
	v = value{err: errTest}
	if v.Int() != 0 || v.String() != "" {
		t.Fail()
	}
}

func TestValueComplex(t *testing.T) {
	v := value{raw: json.RawMessage(`["foo", 42.3, {"x": 5}]`)}
	if len(v.Array()) != 3 {
		t.Fail()
	}
	if v.Array()[0].String() != "foo" {
		t.Fail()
	}
	if v.Array()[1].Float() != 42.3 {
		t.Fail()
	}
	if v.Array()[2].Object()["x"].Int() != 5 {
		t.Fail()
	}
}

func TestRawValue(t *testing.T) {
	var v Value

	v = value{raw: json.RawMessage(nil)}
	if v.Bytes() != nil {
		t.Fail()
	}

	v = value{raw: json.RawMessage(`"hello"`)}
	if !bytes.Equal(v.Bytes(), []byte(`"hello"`)) {
		t.Fail()
	}
}
Download .txt
gitextract_tqfsxivb/

├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── chrome.go
├── chrome_test.go
├── examples/
│   ├── counter/
│   │   ├── build-linux.sh
│   │   ├── build-macos.sh
│   │   ├── build-windows.bat
│   │   ├── icons/
│   │   │   └── icon.icns
│   │   ├── main.go
│   │   └── www/
│   │       └── index.html
│   ├── hello/
│   │   └── main.go
│   └── stopwatch/
│       └── main.go
├── export.go
├── go.mod
├── go.sum
├── locate.go
├── locate_test.go
├── messagebox.go
├── messagebox_windows.go
├── ui.go
├── ui_test.go
├── value.go
└── value_test.go
Download .txt
SYMBOL INDEX (77 symbols across 14 files)

FILE: chrome.go
  type result (line 22) | type result struct
  type bindingFunc (line 27) | type bindingFunc
  type msg (line 30) | type msg struct
  type chrome (line 38) | type chrome struct
    method findTarget (line 125) | func (c *chrome) findTarget() (string, error) {
    method startSession (line 152) | func (c *chrome) startSession(target string) (string, error) {
    method getWindowForTarget (line 207) | func (c *chrome) getWindowForTarget(target string) (windowTargetMessag...
    method readLoop (line 253) | func (c *chrome) readLoop() {
    method send (line 346) | func (c *chrome) send(method string, params h) (json.RawMessage, error) {
    method load (line 368) | func (c *chrome) load(url string) error {
    method eval (line 373) | func (c *chrome) eval(expr string) (json.RawMessage, error) {
    method bind (line 377) | func (c *chrome) bind(name string, f bindingFunc) error {
    method setBounds (line 427) | func (c *chrome) setBounds(b Bounds) error {
    method bounds (line 439) | func (c *chrome) bounds() (Bounds, error) {
    method pdf (line 451) | func (c *chrome) pdf(width, height int) ([]byte, error) {
    method png (line 466) | func (c *chrome) png(x, y, width, height int, bg uint32, scale float32...
    method kill (line 506) | func (c *chrome) kill() error {
  function newChromeWithArgs (line 50) | func newChromeWithArgs(chromeBinary string, args ...string) (*chrome, er...
  type WindowState (line 180) | type WindowState
  constant WindowStateNormal (line 184) | WindowStateNormal WindowState = "normal"
  constant WindowStateMaximized (line 186) | WindowStateMaximized WindowState = "maximized"
  constant WindowStateMinimized (line 188) | WindowStateMinimized WindowState = "minimized"
  constant WindowStateFullscreen (line 190) | WindowStateFullscreen WindowState = "fullscreen"
  type Bounds (line 194) | type Bounds struct
  type windowTargetMessage (line 202) | type windowTargetMessage struct
  type targetMessageTemplate (line 217) | type targetMessageTemplate struct
  type targetMessage (line 235) | type targetMessage struct
  function readUntilMatch (line 519) | func readUntilMatch(r io.ReadCloser, re *regexp.Regexp) ([]string, error) {
  function contains (line 532) | func contains(arr []string, x string) bool {

FILE: chrome_test.go
  function TestChromeEval (line 12) | func TestChromeEval(t *testing.T) {
  function TestChromeLoad (line 46) | func TestChromeLoad(t *testing.T) {
  function TestChromeBind (line 72) | func TestChromeBind(t *testing.T) {
  function TestChromeAsync (line 109) | func TestChromeAsync(t *testing.T) {

FILE: examples/counter/main.go
  type counter (line 23) | type counter struct
    method Add (line 28) | func (c *counter) Add(n int) {
    method Value (line 34) | func (c *counter) Value() int {
  function main (line 40) | func main() {

FILE: examples/hello/main.go
  function main (line 10) | func main() {

FILE: examples/stopwatch/main.go
  function main (line 13) | func main() {

FILE: export.go
  constant PageA4Width (line 11) | PageA4Width = 816
  constant PageA4Height (line 13) | PageA4Height = 1056
  function PDF (line 21) | func PDF(url, script string, width, height int) ([]byte, error) {
  function PNG (line 38) | func PNG(url, script string, x, y, width, height int, bg uint32, scale f...
  function doHeadless (line 47) | func doHeadless(url string, f func(c *chrome) ([]byte, error)) ([]byte, ...

FILE: locate.go
  function LocateChrome (line 16) | func LocateChrome() string {
  function PromptDownload (line 70) | func PromptDownload() {

FILE: locate_test.go
  function TestLocate (line 8) | func TestLocate(t *testing.T) {

FILE: messagebox.go
  function messageBox (line 13) | func messageBox(title, text string) bool {

FILE: messagebox_windows.go
  function messageBox (line 10) | func messageBox(title, text string) bool {

FILE: ui.go
  type UI (line 13) | type UI interface
  type ui (line 23) | type ui struct
    method Done (line 95) | func (u *ui) Done() <-chan struct{} {
    method Close (line 99) | func (u *ui) Close() error {
    method Load (line 111) | func (u *ui) Load(url string) error { return u.chrome.load(url) }
    method Bind (line 113) | func (u *ui) Bind(name string, f interface{}) error {
    method Eval (line 166) | func (u *ui) Eval(js string) Value {
    method SetBounds (line 171) | func (u *ui) SetBounds(b Bounds) error {
    method Bounds (line 175) | func (u *ui) Bounds() (Bounds, error) {
  function New (line 64) | func New(url, dir string, width, height int, customArgs ...string) (UI, ...

FILE: ui_test.go
  function TestEval (line 10) | func TestEval(t *testing.T) {
  function TestBind (line 35) | func TestBind(t *testing.T) {
  function TestFunctionReturnTypes (line 90) | func TestFunctionReturnTypes(t *testing.T) {

FILE: value.go
  type Value (line 7) | type Value interface
  type value (line 19) | type value struct
    method Err (line 24) | func (v value) Err() error             { return v.err }
    method Bytes (line 25) | func (v value) Bytes() []byte          { return v.raw }
    method To (line 26) | func (v value) To(x interface{}) error { return json.Unmarshal(v.raw, ...
    method Float (line 27) | func (v value) Float() (f float32)     { v.To(&f); return f }
    method Int (line 28) | func (v value) Int() (i int)           { v.To(&i); return i }
    method String (line 29) | func (v value) String() (s string)     { v.To(&s); return s }
    method Bool (line 30) | func (v value) Bool() (b bool)         { v.To(&b); return b }
    method Array (line 31) | func (v value) Array() (values []Value) {
    method Object (line 39) | func (v value) Object() (object map[string]Value) {

FILE: value_test.go
  function TestValueError (line 12) | func TestValueError(t *testing.T) {
  function TestValuePrimitive (line 24) | func TestValuePrimitive(t *testing.T) {
  function TestValueComplex (line 39) | func TestValueComplex(t *testing.T) {
  function TestRawValue (line 55) | func TestRawValue(t *testing.T) {
Condensed preview — 25 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (54K chars).
[
  {
    "path": ".github/workflows/ci.yml",
    "chars": 677,
    "preview": "name: CI Pipeline\non: [push, pull_request]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/c"
  },
  {
    "path": ".gitignore",
    "chars": 387,
    "preview": "# 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 t"
  },
  {
    "path": "LICENSE",
    "chars": 1070,
    "preview": "MIT License\n\nCopyright (c) 2018 Serge Zaitsev\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
  },
  {
    "path": "README.md",
    "chars": 4055,
    "preview": "# Lorca\n\n[![Build Status](https://img.shields.io/github/workflow/status/zserge/lorca/CI%20Pipeline)](https://github.com/"
  },
  {
    "path": "chrome.go",
    "chars": 13712,
    "preview": "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"
  },
  {
    "path": "chrome_test.go",
    "chars": 3440,
    "preview": "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"
  },
  {
    "path": "examples/counter/build-linux.sh",
    "chars": 855,
    "preview": "#!/bin/sh\n\nAPP=lorca-example\nAPPDIR=${APP}_1.0.0\n\nmkdir -p $APPDIR/usr/bin\nmkdir -p $APPDIR/usr/share/applications\nmkdir"
  },
  {
    "path": "examples/counter/build-macos.sh",
    "chars": 603,
    "preview": "#!/bin/sh\n\nAPP=\"Example.app\"\nmkdir -p $APP/Contents/{MacOS,Resources}\ngo build -o $APP/Contents/MacOS/lorca-example\ncat "
  },
  {
    "path": "examples/counter/build-windows.bat",
    "chars": 80,
    "preview": "@echo off\r\ngo generate\r\ngo build -ldflags \"-H windowsgui\" -o lorca-example.exe\r\n"
  },
  {
    "path": "examples/counter/main.go",
    "chars": 1892,
    "preview": "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/zse"
  },
  {
    "path": "examples/counter/www/index.html",
    "chars": 1862,
    "preview": "<!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* { "
  },
  {
    "path": "examples/hello/main.go",
    "chars": 408,
    "preview": "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 pass"
  },
  {
    "path": "examples/stopwatch/main.go",
    "chars": 1266,
    "preview": "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,"
  },
  {
    "path": "export.go",
    "chars": 2020,
    "preview": "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"
  },
  {
    "path": "go.mod",
    "chars": 101,
    "preview": "module github.com/zserge/lorca\n\ngo 1.16\n\nrequire golang.org/x/net v0.0.0-20200222125558-5a598a2470a0\n"
  },
  {
    "path": "go.sum",
    "chars": 504,
    "preview": "golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org"
  },
  {
    "path": "locate.go",
    "chars": 2585,
    "preview": "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 t"
  },
  {
    "path": "locate_test.go",
    "chars": 256,
    "preview": "package lorca\n\nimport (\n\t\"os/exec\"\n\t\"testing\"\n)\n\nfunc TestLocate(t *testing.T) {\n\tif exe := ChromeExecutable(); exe == \""
  },
  {
    "path": "messagebox.go",
    "chars": 870,
    "preview": "//+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, "
  },
  {
    "path": "messagebox_windows.go",
    "chars": 482,
    "preview": "//+build windows\n\npackage lorca\n\nimport (\n\t\"syscall\"\n\t\"unsafe\"\n)\n\nfunc messageBox(title, text string) bool {\n\tuser32 := "
  },
  {
    "path": "ui.go",
    "chars": 4658,
    "preview": "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 talkin"
  },
  {
    "path": "ui_test.go",
    "chars": 3711,
    "preview": "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(\"\""
  },
  {
    "path": "value.go",
    "chars": 1189,
    "preview": "package lorca\n\nimport \"encoding/json\"\n\n// Value is a generic type of a JSON value (primitive, object, array) and\n// opti"
  },
  {
    "path": "value_test.go",
    "chars": 1122,
    "preview": "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 TestVal"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the zserge/lorca GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 25 files (46.7 KB), approximately 14.6k tokens, and a symbol index with 77 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!