mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-02 18:10:48 +08:00
Initial commit of wails build
This commit is contained in:
parent
96996431b4
commit
4742fd7ed2
17
.1.gitignore
Normal file
17
.1.gitignore
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
lib/project/templates/vue
|
||||||
|
lib/project/templates/blank
|
||||||
|
tools
|
||||||
|
test
|
||||||
|
.vscode/
|
||||||
|
tmp
|
||||||
|
examples/**/example*
|
||||||
|
!examples/**/*.*
|
||||||
|
node_modules
|
||||||
|
cmd.old
|
||||||
|
lib.old
|
||||||
|
cmd/wails/wails
|
||||||
|
.DS_Store
|
||||||
|
rewrite
|
||||||
|
.rewrite
|
||||||
|
examples/WIP/*
|
||||||
|
docs
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -10,3 +10,8 @@
|
|||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
|
examples/**/example*
|
||||||
|
!examples/**/*.*
|
||||||
|
cmd/wails/wails
|
||||||
|
.DS_Store
|
20
a_wails-packr.go
Normal file
20
a_wails-packr.go
Normal file
File diff suppressed because one or more lines are too long
142
app.go
Normal file
142
app.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/wailsapp/wails/cmd"
|
||||||
|
"github.com/wailsapp/wails/cmd/frameworks"
|
||||||
|
)
|
||||||
|
|
||||||
|
// -------------------------------- Compile time Flags ------------------------------
|
||||||
|
|
||||||
|
// ReleaseMode indicates if we are in Release Mode
|
||||||
|
var ReleaseMode = false
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// App defines the main application struct
|
||||||
|
type App struct {
|
||||||
|
config *AppConfig // The Application configuration object
|
||||||
|
cli *cmd.Cli // In debug mode, we have a cli
|
||||||
|
renderer Renderer // The renderer is what we will render the app to
|
||||||
|
logLevel string // The log level of the app
|
||||||
|
headless bool // Indicates if the app should be started in headless mode
|
||||||
|
ipc *ipcManager // Handles the IPC calls
|
||||||
|
log *CustomLogger // Logger
|
||||||
|
bindingManager *bindingManager // Handles binding of Go code to renderer
|
||||||
|
eventManager *eventManager // Handles all the events
|
||||||
|
runtime *Runtime // The runtime object for registered structs
|
||||||
|
|
||||||
|
// This is a list of all the JS/CSS that needs injecting
|
||||||
|
// It will get injected in order
|
||||||
|
jsCache []string
|
||||||
|
cssCache []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateApp creates the application window with the given configuration
|
||||||
|
// If none given, the defaults are used
|
||||||
|
func CreateApp(optionalConfig ...*AppConfig) *App {
|
||||||
|
var userConfig *AppConfig
|
||||||
|
if len(optionalConfig) > 0 {
|
||||||
|
userConfig = optionalConfig[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &App{
|
||||||
|
logLevel: "debug",
|
||||||
|
renderer: &webViewRenderer{},
|
||||||
|
ipc: newIPCManager(),
|
||||||
|
bindingManager: newBindingManager(),
|
||||||
|
eventManager: newEventManager(),
|
||||||
|
log: newCustomLogger("App"),
|
||||||
|
}
|
||||||
|
|
||||||
|
appconfig, err := newAppConfig(userConfig)
|
||||||
|
if err != nil {
|
||||||
|
result.log.Fatalf("Cannot use custom HTML: %s", err.Error())
|
||||||
|
}
|
||||||
|
result.config = appconfig
|
||||||
|
|
||||||
|
// Set up the CLI if not in release mode
|
||||||
|
if !ReleaseMode {
|
||||||
|
result.cli = result.setupCli()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable Inspector in release mode
|
||||||
|
if ReleaseMode {
|
||||||
|
result.config.DisableInspector = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the app
|
||||||
|
func (a *App) Run() {
|
||||||
|
a.cli.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) start() error {
|
||||||
|
|
||||||
|
// Set the log level
|
||||||
|
setLogLevel(a.logLevel)
|
||||||
|
|
||||||
|
// Log starup
|
||||||
|
a.log.Info("Starting")
|
||||||
|
|
||||||
|
// Check if we are to run in headless mode
|
||||||
|
if a.headless {
|
||||||
|
a.renderer = &Headless{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialise the renderer
|
||||||
|
err := a.renderer.Initialise(a.config, a.ipc, a.eventManager)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start event manager and give it our renderer
|
||||||
|
a.eventManager.start(a.renderer)
|
||||||
|
|
||||||
|
// Start the IPC Manager and give it the event manager and binding manager
|
||||||
|
a.ipc.start(a.eventManager, a.bindingManager)
|
||||||
|
|
||||||
|
// Create the runtime
|
||||||
|
a.runtime = newRuntime(a.eventManager, a.renderer)
|
||||||
|
|
||||||
|
// Start binding manager and give it our renderer
|
||||||
|
err = a.bindingManager.start(a.renderer, a.runtime)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject framework, if specified
|
||||||
|
if frameworks.FrameworkToUse != nil {
|
||||||
|
a.renderer.InjectFramework(frameworks.FrameworkToUse.JS, frameworks.FrameworkToUse.CSS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject CSS
|
||||||
|
a.renderer.AddCSSList(a.cssCache)
|
||||||
|
|
||||||
|
// Inject JS
|
||||||
|
a.renderer.AddJSList(a.jsCache)
|
||||||
|
|
||||||
|
// Run the renderer
|
||||||
|
a.renderer.Run()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind allows the user to bind the given object
|
||||||
|
// with the application
|
||||||
|
func (a *App) Bind(object interface{}) {
|
||||||
|
a.bindingManager.bind(object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddJS adds a piece of Javascript to a cache that
|
||||||
|
// gets injected at runtime
|
||||||
|
func (a *App) AddJS(js string) {
|
||||||
|
a.jsCache = append(a.jsCache, js)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCSS adds a CSS string to a cache that
|
||||||
|
// gets injected at runtime
|
||||||
|
func (a *App) AddCSS(js string) {
|
||||||
|
a.cssCache = append(a.cssCache, js)
|
||||||
|
}
|
38
app_cli.go
Normal file
38
app_cli.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *App) setupCli() *cmd.Cli {
|
||||||
|
|
||||||
|
// var apiFilename string
|
||||||
|
|
||||||
|
result := cmd.NewCli(app.config.Title, "Debug build")
|
||||||
|
|
||||||
|
// Gen API
|
||||||
|
// result.Command("genapi", "Generate JS stubs for the registered Go plugins").
|
||||||
|
// StringFlag("o", "Output filename", &apiFilename).
|
||||||
|
// Action(func() error {
|
||||||
|
// app.renderer = N
|
||||||
|
// })
|
||||||
|
|
||||||
|
result.
|
||||||
|
StringFlag("loglevel", "Sets the log level [info|debug|error|panic|fatal]. Default debug", &app.logLevel).
|
||||||
|
BoolFlag("headless", "Runs the app in headless mode", &app.headless).
|
||||||
|
Action(app.start)
|
||||||
|
|
||||||
|
// Banner
|
||||||
|
result.PreRun(func(cli *cmd.Cli) error {
|
||||||
|
log := cmd.NewLogger()
|
||||||
|
log.PrintBanner()
|
||||||
|
fmt.Println()
|
||||||
|
result.PrintHelp()
|
||||||
|
log.YellowUnderline(app.config.Title + " - Debug Build")
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
99
app_config.go
Normal file
99
app_config.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/dchest/htmlmin"
|
||||||
|
"github.com/gobuffalo/packr"
|
||||||
|
)
|
||||||
|
|
||||||
|
var assets = packr.NewBox("./assets/default")
|
||||||
|
|
||||||
|
// AppConfig is the configuration structure used when creating a Wails App object
|
||||||
|
type AppConfig struct {
|
||||||
|
Width, Height int
|
||||||
|
Title string
|
||||||
|
defaultHTML string
|
||||||
|
HTML string
|
||||||
|
JS string
|
||||||
|
CSS string
|
||||||
|
Colour string
|
||||||
|
Resizable bool
|
||||||
|
DisableInspector bool
|
||||||
|
isHTMLFragment bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AppConfig) merge(in *AppConfig) error {
|
||||||
|
if in.CSS != "" {
|
||||||
|
a.CSS = in.CSS
|
||||||
|
}
|
||||||
|
if in.Title != "" {
|
||||||
|
a.Title = in.Title
|
||||||
|
}
|
||||||
|
if in.HTML != "" {
|
||||||
|
minified, err := htmlmin.Minify([]byte(in.HTML), &htmlmin.Options{
|
||||||
|
MinifyScripts: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
inlineHTML := string(minified)
|
||||||
|
inlineHTML = strings.Replace(inlineHTML, "'", "\\'", -1)
|
||||||
|
inlineHTML = strings.Replace(inlineHTML, "\n", " ", -1)
|
||||||
|
a.HTML = strings.TrimSpace(inlineHTML)
|
||||||
|
|
||||||
|
// Deduce whether this is a full html page or a fragment
|
||||||
|
// The document is determined to be a fragment if an HMTL
|
||||||
|
// tag exists and is located before the first div tag
|
||||||
|
HTMLTagIndex := strings.Index(a.HTML, "<html")
|
||||||
|
DivTagIndex := strings.Index(a.HTML, "<div")
|
||||||
|
|
||||||
|
if HTMLTagIndex == -1 {
|
||||||
|
a.isHTMLFragment = true
|
||||||
|
} else {
|
||||||
|
if DivTagIndex < HTMLTagIndex {
|
||||||
|
a.isHTMLFragment = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if in.Colour != "" {
|
||||||
|
a.Colour = in.Colour
|
||||||
|
}
|
||||||
|
|
||||||
|
if in.JS != "" {
|
||||||
|
a.JS = in.JS
|
||||||
|
}
|
||||||
|
|
||||||
|
if in.Width != 0 {
|
||||||
|
a.Width = in.Width
|
||||||
|
}
|
||||||
|
if in.Height != 0 {
|
||||||
|
a.Height = in.Height
|
||||||
|
}
|
||||||
|
a.Resizable = in.Resizable
|
||||||
|
a.DisableInspector = in.DisableInspector
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates the default configuration
|
||||||
|
func newAppConfig(userConfig *AppConfig) (*AppConfig, error) {
|
||||||
|
result := &AppConfig{
|
||||||
|
Width: 800,
|
||||||
|
Height: 600,
|
||||||
|
Resizable: true,
|
||||||
|
Title: "My Wails App",
|
||||||
|
Colour: "#FFF", // White by default
|
||||||
|
HTML: defaultAssets.String("default.html"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if userConfig != nil {
|
||||||
|
err := result.merge(userConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
160
binding_function.go
Normal file
160
binding_function.go
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type boundFunction struct {
|
||||||
|
fullName string
|
||||||
|
function reflect.Value
|
||||||
|
functionType reflect.Type
|
||||||
|
inputs []reflect.Type
|
||||||
|
returnTypes []reflect.Type
|
||||||
|
log *CustomLogger
|
||||||
|
hasErrorReturnType bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new bound function based on the given method + type
|
||||||
|
func newBoundFunction(object interface{}) (*boundFunction, error) {
|
||||||
|
|
||||||
|
objectValue := reflect.ValueOf(object)
|
||||||
|
objectType := reflect.TypeOf(object)
|
||||||
|
|
||||||
|
name := runtime.FuncForPC(objectValue.Pointer()).Name()
|
||||||
|
|
||||||
|
result := &boundFunction{
|
||||||
|
fullName: name,
|
||||||
|
function: objectValue,
|
||||||
|
functionType: objectType,
|
||||||
|
log: newCustomLogger(name),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := result.processParameters()
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *boundFunction) processParameters() error {
|
||||||
|
|
||||||
|
// Param processing
|
||||||
|
functionType := b.functionType
|
||||||
|
|
||||||
|
// Input parameters
|
||||||
|
inputParamCount := functionType.NumIn()
|
||||||
|
if inputParamCount > 0 {
|
||||||
|
b.inputs = make([]reflect.Type, inputParamCount)
|
||||||
|
// We start at 1 as the first param is the struct
|
||||||
|
for index := 0; index < inputParamCount; index++ {
|
||||||
|
param := functionType.In(index)
|
||||||
|
name := param.Name()
|
||||||
|
kind := param.Kind()
|
||||||
|
b.inputs[index] = param
|
||||||
|
typ := param
|
||||||
|
index := index
|
||||||
|
b.log.DebugFields("Input param", Fields{
|
||||||
|
"index": index,
|
||||||
|
"name": name,
|
||||||
|
"kind": kind,
|
||||||
|
"typ": typ,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process return/output declarations
|
||||||
|
returnParamsCount := functionType.NumOut()
|
||||||
|
// Guard against bad number of return types
|
||||||
|
switch returnParamsCount {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
// Check if it's an error type
|
||||||
|
param := functionType.Out(0)
|
||||||
|
paramName := param.Name()
|
||||||
|
if paramName == "error" {
|
||||||
|
b.hasErrorReturnType = true
|
||||||
|
}
|
||||||
|
// Save return type
|
||||||
|
b.returnTypes = append(b.returnTypes, param)
|
||||||
|
case 2:
|
||||||
|
// Check the second return type is an error
|
||||||
|
secondParam := functionType.Out(1)
|
||||||
|
secondParamName := secondParam.Name()
|
||||||
|
if secondParamName != "error" {
|
||||||
|
return fmt.Errorf("last return type of method '%s' must be an error (got %s)", b.fullName, secondParamName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the second return type is an error
|
||||||
|
firstParam := functionType.Out(0)
|
||||||
|
firstParamName := firstParam.Name()
|
||||||
|
if firstParamName == "error" {
|
||||||
|
return fmt.Errorf("first return type of method '%s' must not be an error", b.fullName)
|
||||||
|
}
|
||||||
|
b.hasErrorReturnType = true
|
||||||
|
|
||||||
|
// Save return types
|
||||||
|
b.returnTypes = append(b.returnTypes, firstParam)
|
||||||
|
b.returnTypes = append(b.returnTypes, secondParam)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("cannot register method '%s' with %d return parameters. Please use up to 2", b.fullName, returnParamsCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// call the method with the given data
|
||||||
|
func (b *boundFunction) call(data string) ([]reflect.Value, error) {
|
||||||
|
|
||||||
|
// The data will be an array of values so we will decode the
|
||||||
|
// input data into
|
||||||
|
var jsArgs []interface{}
|
||||||
|
d := json.NewDecoder(bytes.NewBufferString(data))
|
||||||
|
// d.UseNumber()
|
||||||
|
err := d.Decode(&jsArgs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Invalid data passed to method call: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check correct number of inputs
|
||||||
|
if len(jsArgs) != len(b.inputs) {
|
||||||
|
return nil, fmt.Errorf("Invalid number of parameters given to %s. Expected %d but got %d", b.fullName, len(b.inputs), len(jsArgs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up call
|
||||||
|
args := make([]reflect.Value, len(b.inputs))
|
||||||
|
for index := 0; index < len(b.inputs); index++ {
|
||||||
|
|
||||||
|
// Set the input values
|
||||||
|
value, err := b.setInputValue(index, b.inputs[index], jsArgs[index])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
args[index] = value
|
||||||
|
}
|
||||||
|
b.log.Infof("Unmarshalled Args: %+v\n", jsArgs)
|
||||||
|
b.log.Infof("Converted Args: %+v\n", args)
|
||||||
|
results := b.function.Call(args)
|
||||||
|
|
||||||
|
b.log.Debugf("results = %+v", results)
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempts to set the method input <typ> for parameter <index> with the given value <val>
|
||||||
|
func (b *boundFunction) setInputValue(index int, typ reflect.Type, val interface{}) (result reflect.Value, err error) {
|
||||||
|
|
||||||
|
// Catch type conversion panics thrown by convert
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
// Modify error
|
||||||
|
err = fmt.Errorf("%s for parameter %d of function %s", r.(string)[23:], index+1, b.fullName)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Do the conversion
|
||||||
|
result = reflect.ValueOf(val).Convert(typ)
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
267
binding_manager.go
Normal file
267
binding_manager.go
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
|
||||||
|
binding:
|
||||||
|
Name() // Full name (package+name)
|
||||||
|
Call(params)
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
type bindingManager struct {
|
||||||
|
methods map[string]*boundMethod
|
||||||
|
functions map[string]*boundFunction
|
||||||
|
initMethods []*boundMethod
|
||||||
|
log *CustomLogger
|
||||||
|
renderer Renderer
|
||||||
|
runtime *Runtime // The runtime object to pass to bound structs
|
||||||
|
objectsToBind []interface{}
|
||||||
|
bindPackageNames bool // Package name should be considered when binding
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBindingManager() *bindingManager {
|
||||||
|
result := &bindingManager{
|
||||||
|
methods: make(map[string]*boundMethod),
|
||||||
|
functions: make(map[string]*boundFunction),
|
||||||
|
log: newCustomLogger("Bind"),
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets flag to indicate package names should be considered when binding
|
||||||
|
func (b *bindingManager) BindPackageNames() {
|
||||||
|
b.bindPackageNames = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bindingManager) start(renderer Renderer, runtime *Runtime) error {
|
||||||
|
b.log.Info("Starting")
|
||||||
|
b.renderer = renderer
|
||||||
|
b.runtime = runtime
|
||||||
|
err := b.initialise()
|
||||||
|
if err != nil {
|
||||||
|
b.log.Errorf("Binding error: %s", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = b.callWailsInitMethods()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bindingManager) initialise() error {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
// var binding *boundMethod
|
||||||
|
|
||||||
|
b.log.Info("Binding Go Functions/Methods")
|
||||||
|
|
||||||
|
// Create bindings for objects
|
||||||
|
for _, object := range b.objectsToBind {
|
||||||
|
|
||||||
|
// Safeguard against nils
|
||||||
|
if object == nil {
|
||||||
|
return fmt.Errorf("attempted to bind nil object")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine kind of object
|
||||||
|
objectType := reflect.TypeOf(object)
|
||||||
|
objectKind := objectType.Kind()
|
||||||
|
|
||||||
|
switch objectKind {
|
||||||
|
case reflect.Ptr:
|
||||||
|
err = b.bindMethod(object)
|
||||||
|
case reflect.Func:
|
||||||
|
// spew.Dump(result.objectType.String())
|
||||||
|
err = b.bindFunction(object)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("cannot bind object of type '%s'", objectKind.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return error if set
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// bind the given struct method
|
||||||
|
func (b *bindingManager) bindMethod(object interface{}) error {
|
||||||
|
|
||||||
|
objectType := reflect.TypeOf(object)
|
||||||
|
baseName := objectType.String()
|
||||||
|
|
||||||
|
// Strip pointer if there
|
||||||
|
if baseName[0] == '*' {
|
||||||
|
baseName = baseName[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
b.log.Debugf("Processing struct: %s", baseName)
|
||||||
|
|
||||||
|
// Iterate over method definitions
|
||||||
|
for i := 0; i < objectType.NumMethod(); i++ {
|
||||||
|
|
||||||
|
// Get method definition
|
||||||
|
methodDef := objectType.Method(i)
|
||||||
|
methodName := methodDef.Name
|
||||||
|
fullMethodName := baseName + "." + methodName
|
||||||
|
method := reflect.ValueOf(object).MethodByName(methodName)
|
||||||
|
|
||||||
|
// Skip unexported methods
|
||||||
|
if !unicode.IsUpper([]rune(methodName)[0]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new boundMethod
|
||||||
|
newMethod, err := newBoundMethod(methodName, fullMethodName, method, objectType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a wails init function
|
||||||
|
if newMethod.isWailsInit {
|
||||||
|
b.log.Debugf("Detected WailsInit function: %s", fullMethodName)
|
||||||
|
b.initMethods = append(b.initMethods, newMethod)
|
||||||
|
} else {
|
||||||
|
// Save boundMethod
|
||||||
|
b.log.Infof("Bound Method: %s()", fullMethodName)
|
||||||
|
b.methods[fullMethodName] = newMethod
|
||||||
|
|
||||||
|
// Inform renderer of new binding
|
||||||
|
b.renderer.NewBinding(fullMethodName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// bind the given function object
|
||||||
|
func (b *bindingManager) bindFunction(object interface{}) error {
|
||||||
|
|
||||||
|
newFunction, err := newBoundFunction(object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save method
|
||||||
|
b.log.Infof("Bound Function: %s()", newFunction.fullName)
|
||||||
|
b.functions[newFunction.fullName] = newFunction
|
||||||
|
|
||||||
|
// Register with Renderer
|
||||||
|
b.renderer.NewBinding(newFunction.fullName)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the given object to be bound at start time
|
||||||
|
func (b *bindingManager) bind(object interface{}) {
|
||||||
|
// Store binding
|
||||||
|
b.objectsToBind = append(b.objectsToBind, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// process an incoming call request
|
||||||
|
func (b *bindingManager) processCall(callData *callData) (interface{}, error) {
|
||||||
|
b.log.Debugf("Wanting to call %s", callData.BindingName)
|
||||||
|
|
||||||
|
// Determine if this is function call or method call by the number of
|
||||||
|
// dots in the binding name
|
||||||
|
dotCount := 0
|
||||||
|
for _, character := range callData.BindingName {
|
||||||
|
if character == '.' {
|
||||||
|
dotCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return values
|
||||||
|
var result []reflect.Value
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// We need to catch reflect related panics and return
|
||||||
|
// a decent error message
|
||||||
|
// TODO: DEBUG THIS!
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = fmt.Errorf("%s", r.(string))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
switch dotCount {
|
||||||
|
case 1:
|
||||||
|
function := b.functions[callData.BindingName]
|
||||||
|
if function == nil {
|
||||||
|
return nil, fmt.Errorf("Invalid function name '%s'", callData.BindingName)
|
||||||
|
}
|
||||||
|
result, err = function.call(callData.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we have an error return type?
|
||||||
|
if function.hasErrorReturnType {
|
||||||
|
// We do - last result is an error type
|
||||||
|
// Check if the last result was nil
|
||||||
|
b.log.Debugf("# of return types: %d", len(function.returnTypes))
|
||||||
|
b.log.Debugf("# of results: %d", len(result))
|
||||||
|
errorResult := result[len(function.returnTypes)-1]
|
||||||
|
if !errorResult.IsNil() {
|
||||||
|
// It wasn't - we have an error
|
||||||
|
return nil, errorResult.Interface().(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result[0].Interface(), nil
|
||||||
|
case 2:
|
||||||
|
// do we have this method?
|
||||||
|
method := b.methods[callData.BindingName]
|
||||||
|
if method == nil {
|
||||||
|
return nil, fmt.Errorf("Invalid method name '%s'", callData.BindingName)
|
||||||
|
}
|
||||||
|
result, err = method.call(callData.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we have an error return type?
|
||||||
|
if method.hasErrorReturnType {
|
||||||
|
// We do - last result is an error type
|
||||||
|
// Check if the last result was nil
|
||||||
|
b.log.Debugf("# of return types: %d", len(method.returnTypes))
|
||||||
|
b.log.Debugf("# of results: %d", len(result))
|
||||||
|
errorResult := result[len(method.returnTypes)-1]
|
||||||
|
if !errorResult.IsNil() {
|
||||||
|
// It wasn't - we have an error
|
||||||
|
return nil, errorResult.Interface().(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result != nil {
|
||||||
|
return result[0].Interface(), nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("Invalid binding name '%s'", callData.BindingName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// callWailsInitMethods calls all of the WailsInit methods that were
|
||||||
|
// registered with the runtime object
|
||||||
|
func (b *bindingManager) callWailsInitMethods() error {
|
||||||
|
// Create reflect value for runtime object
|
||||||
|
runtimeValue := reflect.ValueOf(b.runtime)
|
||||||
|
params := []reflect.Value{runtimeValue}
|
||||||
|
|
||||||
|
// Iterate initMethods
|
||||||
|
for _, initMethod := range b.initMethods {
|
||||||
|
// Call
|
||||||
|
result := initMethod.method.Call(params)
|
||||||
|
// Check errors
|
||||||
|
err := result[0].Interface()
|
||||||
|
if err != nil {
|
||||||
|
return err.(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
211
binding_method.go
Normal file
211
binding_method.go
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type boundMethod struct {
|
||||||
|
Name string
|
||||||
|
fullName string
|
||||||
|
method reflect.Value
|
||||||
|
inputs []reflect.Type
|
||||||
|
returnTypes []reflect.Type
|
||||||
|
log *CustomLogger
|
||||||
|
hasErrorReturnType bool // Indicates if there is an error return type
|
||||||
|
isWailsInit bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new bound method based on the given method + type
|
||||||
|
func newBoundMethod(name string, fullName string, method reflect.Value, objectType reflect.Type) (*boundMethod, error) {
|
||||||
|
result := &boundMethod{
|
||||||
|
Name: name,
|
||||||
|
method: method,
|
||||||
|
fullName: fullName,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup logger
|
||||||
|
result.log = newCustomLogger(result.fullName)
|
||||||
|
|
||||||
|
// Check if Parameters are valid
|
||||||
|
err := result.processParameters()
|
||||||
|
|
||||||
|
// Are we a WailsInit method?
|
||||||
|
if result.Name == "WailsInit" {
|
||||||
|
err = result.processWailsInit()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *boundMethod) processParameters() error {
|
||||||
|
|
||||||
|
// Param processing
|
||||||
|
methodType := b.method.Type()
|
||||||
|
|
||||||
|
// Input parameters
|
||||||
|
inputParamCount := methodType.NumIn()
|
||||||
|
if inputParamCount > 0 {
|
||||||
|
b.inputs = make([]reflect.Type, inputParamCount)
|
||||||
|
// We start at 1 as the first param is the struct
|
||||||
|
for index := 0; index < inputParamCount; index++ {
|
||||||
|
param := methodType.In(index)
|
||||||
|
name := param.Name()
|
||||||
|
kind := param.Kind()
|
||||||
|
b.inputs[index] = param
|
||||||
|
typ := param
|
||||||
|
index := index
|
||||||
|
b.log.DebugFields("Input param", Fields{
|
||||||
|
"index": index,
|
||||||
|
"name": name,
|
||||||
|
"kind": kind,
|
||||||
|
"typ": typ,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process return/output declarations
|
||||||
|
returnParamsCount := methodType.NumOut()
|
||||||
|
// Guard against bad number of return types
|
||||||
|
switch returnParamsCount {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
// Check if it's an error type
|
||||||
|
param := methodType.Out(0)
|
||||||
|
paramName := param.Name()
|
||||||
|
if paramName == "error" {
|
||||||
|
b.hasErrorReturnType = true
|
||||||
|
}
|
||||||
|
// Save return type
|
||||||
|
b.returnTypes = append(b.returnTypes, param)
|
||||||
|
case 2:
|
||||||
|
// Check the second return type is an error
|
||||||
|
secondParam := methodType.Out(1)
|
||||||
|
secondParamName := secondParam.Name()
|
||||||
|
if secondParamName != "error" {
|
||||||
|
return fmt.Errorf("last return type of method '%s' must be an error (got %s)", b.Name, secondParamName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the second return type is an error
|
||||||
|
firstParam := methodType.Out(0)
|
||||||
|
firstParamName := firstParam.Name()
|
||||||
|
if firstParamName == "error" {
|
||||||
|
return fmt.Errorf("first return type of method '%s' must not be an error", b.Name)
|
||||||
|
}
|
||||||
|
b.hasErrorReturnType = true
|
||||||
|
|
||||||
|
// Save return types
|
||||||
|
b.returnTypes = append(b.returnTypes, firstParam)
|
||||||
|
b.returnTypes = append(b.returnTypes, secondParam)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("cannot register method '%s' with %d return parameters. Please use up to 2", b.Name, returnParamsCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// call the method with the given data
|
||||||
|
func (b *boundMethod) call(data string) ([]reflect.Value, error) {
|
||||||
|
|
||||||
|
// The data will be an array of values so we will decode the
|
||||||
|
// input data into
|
||||||
|
var jsArgs []interface{}
|
||||||
|
d := json.NewDecoder(bytes.NewBufferString(data))
|
||||||
|
// d.UseNumber()
|
||||||
|
err := d.Decode(&jsArgs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Invalid data passed to method call: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check correct number of inputs
|
||||||
|
if len(jsArgs) != len(b.inputs) {
|
||||||
|
return nil, fmt.Errorf("Invalid number of parameters given to %s. Expected %d but got %d", b.fullName, len(b.inputs), len(jsArgs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up call
|
||||||
|
args := make([]reflect.Value, len(b.inputs))
|
||||||
|
for index := 0; index < len(b.inputs); index++ {
|
||||||
|
|
||||||
|
// Set the input values
|
||||||
|
value, err := b.setInputValue(index, b.inputs[index], jsArgs[index])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
args[index] = value
|
||||||
|
}
|
||||||
|
b.log.Infof("Unmarshalled Args: %+v\n", jsArgs)
|
||||||
|
b.log.Infof("Converted Args: %+v\n", args)
|
||||||
|
results := b.method.Call(args)
|
||||||
|
|
||||||
|
b.log.Debugf("results = %+v", results)
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempts to set the method input <typ> for parameter <index> with the given value <val>
|
||||||
|
func (b *boundMethod) setInputValue(index int, typ reflect.Type, val interface{}) (result reflect.Value, err error) {
|
||||||
|
|
||||||
|
// Catch type conversion panics thrown by convert
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
// Modify error
|
||||||
|
fmt.Printf("Recovery message: %+v\n", r)
|
||||||
|
err = fmt.Errorf("%s for parameter %d of method %s", r.(string)[23:], index+1, b.fullName)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Do the conversion
|
||||||
|
// Handle nil values
|
||||||
|
if val == nil {
|
||||||
|
switch typ.Kind() {
|
||||||
|
case reflect.Chan,
|
||||||
|
reflect.Func,
|
||||||
|
reflect.Interface,
|
||||||
|
reflect.Map,
|
||||||
|
reflect.Ptr,
|
||||||
|
reflect.Slice:
|
||||||
|
logger.Debug("Converting nil to type")
|
||||||
|
result = reflect.ValueOf(val).Convert(typ)
|
||||||
|
default:
|
||||||
|
logger.Debug("Cannot convert nil to type, returning error")
|
||||||
|
return reflect.Zero(typ), fmt.Errorf("Unable to use null value for parameter %d of method %s", index+1, b.fullName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = reflect.ValueOf(val).Convert(typ)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *boundMethod) processWailsInit() error {
|
||||||
|
// We must have only 1 input, it must be *wails.Runtime
|
||||||
|
if len(b.inputs) != 1 {
|
||||||
|
return fmt.Errorf("Invalid WailsInit() definition. Expected 1 input, but got %d", len(b.inputs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// It must be *wails.Runtime
|
||||||
|
inputName := b.inputs[0].String()
|
||||||
|
b.log.Debugf("WailsInit input type: %s", inputName)
|
||||||
|
if inputName != "*wails.Runtime" {
|
||||||
|
return fmt.Errorf("Invalid WailsInit() definition. Expected input to be wails.Runtime, but got %s", inputName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We must have only 1 output, it must be error
|
||||||
|
if len(b.returnTypes) != 1 {
|
||||||
|
return fmt.Errorf("Invalid WailsInit() definition. Expected 1 return type, but got %d", len(b.returnTypes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// It must be *wails.Runtime
|
||||||
|
outputName := b.returnTypes[0].String()
|
||||||
|
b.log.Debugf("WailsInit output type: %s", outputName)
|
||||||
|
if outputName != "error" {
|
||||||
|
return fmt.Errorf("Invalid WailsInit() definition. Expected input to be error, but got %s", outputName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are indeed a wails Init method
|
||||||
|
b.isWailsInit = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
193
cmd/bundle.go
Normal file
193
cmd/bundle.go
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackmordaunt/icns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BundleHelper helps with the 'wails bundle' command
|
||||||
|
type BundleHelper struct {
|
||||||
|
fs *FSHelper
|
||||||
|
log *Logger
|
||||||
|
system *SystemHelper
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBundleHelper creates a new BundleHelper!
|
||||||
|
func NewBundleHelper() *BundleHelper {
|
||||||
|
return &BundleHelper{
|
||||||
|
fs: NewFSHelper(),
|
||||||
|
log: NewLogger(),
|
||||||
|
system: NewSystemHelper(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// var assetsBox packr.Box
|
||||||
|
|
||||||
|
type plistData struct {
|
||||||
|
Title string
|
||||||
|
Exe string
|
||||||
|
BundleID string
|
||||||
|
Version string
|
||||||
|
Author string
|
||||||
|
Date string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPlistData(title, exe, bundleID, version, author string) *plistData {
|
||||||
|
now := time.Now().Format(time.RFC822)
|
||||||
|
return &plistData{
|
||||||
|
Title: title,
|
||||||
|
Exe: exe,
|
||||||
|
Version: version,
|
||||||
|
BundleID: bundleID,
|
||||||
|
Author: author,
|
||||||
|
Date: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultString(val string, defaultVal string) string {
|
||||||
|
if val != "" {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BundleHelper) getBundleFileBaseDir() string {
|
||||||
|
return filepath.Join(b.system.homeDir, "go", "src", "github.com", "wailsapp", "wails", "cmd", "bundle", runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bundle the application into a platform specific package
|
||||||
|
func (b *BundleHelper) Bundle(po *ProjectOptions) error {
|
||||||
|
// Check we have the exe
|
||||||
|
if !b.fs.FileExists(po.BinaryName) {
|
||||||
|
return fmt.Errorf("cannot bundle non-existant binary file '%s'. Please build with 'wails build' first", po.BinaryName)
|
||||||
|
}
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "darwin":
|
||||||
|
return b.bundleOSX(po)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("platform '%s' not supported for bundling yet", runtime.GOOS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bundle the application
|
||||||
|
func (b *BundleHelper) bundleOSX(po *ProjectOptions) error {
|
||||||
|
|
||||||
|
system := NewSystemHelper()
|
||||||
|
config, err := system.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
name := defaultString(po.Name, "WailsTest")
|
||||||
|
exe := defaultString(po.BinaryName, name)
|
||||||
|
version := defaultString(po.Version, "0.1.0")
|
||||||
|
author := defaultString(config.Name, "Anonymous")
|
||||||
|
bundleID := strings.Join([]string{"wails", name, version}, ".")
|
||||||
|
plistData := newPlistData(name, exe, bundleID, version, author)
|
||||||
|
appname := po.Name + ".app"
|
||||||
|
|
||||||
|
// Check binary exists
|
||||||
|
source := path.Join(b.fs.Cwd(), exe)
|
||||||
|
if !b.fs.FileExists(source) {
|
||||||
|
// We need to build!
|
||||||
|
return fmt.Errorf("Target '%s' not available. Has it been compiled yet?", exe)
|
||||||
|
}
|
||||||
|
|
||||||
|
// REmove the existing bundle
|
||||||
|
os.RemoveAll(appname)
|
||||||
|
|
||||||
|
exeDir := path.Join(b.fs.Cwd(), appname, "/Contents/MacOS")
|
||||||
|
b.fs.MkDirs(exeDir, 0755)
|
||||||
|
resourceDir := path.Join(b.fs.Cwd(), appname, "/Contents/Resources")
|
||||||
|
b.fs.MkDirs(resourceDir, 0755)
|
||||||
|
tmpl := template.New("infoPlist")
|
||||||
|
plistFile := filepath.Join(b.getBundleFileBaseDir(), "info.plist")
|
||||||
|
infoPlist, err := ioutil.ReadFile(plistFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tmpl.Parse(string(infoPlist))
|
||||||
|
|
||||||
|
// Write the template to a buffer
|
||||||
|
var tpl bytes.Buffer
|
||||||
|
err = tmpl.Execute(&tpl, plistData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
filename := path.Join(b.fs.Cwd(), appname, "Contents", "Info.plist")
|
||||||
|
err = ioutil.WriteFile(filename, tpl.Bytes(), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy executable
|
||||||
|
target := path.Join(exeDir, exe)
|
||||||
|
err = b.fs.CopyFile(source, target)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Chmod(target, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = b.bundleIcon(resourceDir)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BundleHelper) bundleIcon(resourceDir string) error {
|
||||||
|
|
||||||
|
// TODO: Read this from project.json
|
||||||
|
const appIconFilename = "appicon.png"
|
||||||
|
|
||||||
|
srcIcon := path.Join(b.fs.Cwd(), appIconFilename)
|
||||||
|
|
||||||
|
// Check if appicon.png exists
|
||||||
|
if !b.fs.FileExists(srcIcon) {
|
||||||
|
|
||||||
|
// Install default icon
|
||||||
|
iconfile := filepath.Join(b.getBundleFileBaseDir(), "icon.png")
|
||||||
|
iconData, err := ioutil.ReadFile(iconfile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(srcIcon, iconData, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tgtBundle := path.Join(resourceDir, "iconfile.icns")
|
||||||
|
imageFile, err := os.Open(srcIcon)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer imageFile.Close()
|
||||||
|
srcImg, _, err := image.Decode(imageFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
|
||||||
|
}
|
||||||
|
dest, err := os.Create(tgtBundle)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
|
||||||
|
}
|
||||||
|
defer dest.Close()
|
||||||
|
if err := icns.Encode(dest, srcImg); err != nil {
|
||||||
|
return err
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -2,17 +2,23 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProgramHelper - Utility functions around installed applications
|
// ProgramHelper - Utility functions around installed applications
|
||||||
type ProgramHelper struct{}
|
type ProgramHelper struct {
|
||||||
|
shell *ShellHelper
|
||||||
|
}
|
||||||
|
|
||||||
// NewProgramHelper - Creates a new ProgramHelper
|
// NewProgramHelper - Creates a new ProgramHelper
|
||||||
func NewProgramHelper() *ProgramHelper {
|
func NewProgramHelper() *ProgramHelper {
|
||||||
return &ProgramHelper{}
|
return &ProgramHelper{
|
||||||
|
shell: NewShellHelper(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsInstalled tries to determine if the given binary name is installed
|
// IsInstalled tries to determine if the given binary name is installed
|
||||||
@ -83,3 +89,31 @@ func (p *Program) Run(vars ...string) (stdout, stderr string, exitCode int, err
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InstallGoPackage installs the given Go package
|
||||||
|
func (p *ProgramHelper) InstallGoPackage(packageName string) error {
|
||||||
|
args := strings.Split("get -u "+packageName, " ")
|
||||||
|
_, stderr, err := p.shell.Run("go", args...)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(stderr)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunCommand runs the given command
|
||||||
|
func (p *ProgramHelper) RunCommand(command string) error {
|
||||||
|
args := strings.Split(command, " ")
|
||||||
|
program := args[0]
|
||||||
|
// TODO: Run FindProgram here and get the full path to the exe
|
||||||
|
program, err := exec.LookPath(program)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("ERROR: Looks like '%s' isn't installed. Please install and try again.", program)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
args = args[1:]
|
||||||
|
_, stderr, err := p.shell.Run(program, args...)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(stderr)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
27
cmd/shell.go
Normal file
27
cmd/shell.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShellHelper helps with Shell commands
|
||||||
|
type ShellHelper struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewShellHelper creates a new ShellHelper!
|
||||||
|
func NewShellHelper() *ShellHelper {
|
||||||
|
return &ShellHelper{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the given command
|
||||||
|
func (sh *ShellHelper) Run(command string, vars ...string) (stdout, stderr string, err error) {
|
||||||
|
cmd := exec.Command(command, vars...)
|
||||||
|
var stdo, stde bytes.Buffer
|
||||||
|
cmd.Stdout = &stdo
|
||||||
|
cmd.Stderr = &stde
|
||||||
|
err = cmd.Run()
|
||||||
|
stdout = string(stdo.Bytes())
|
||||||
|
stderr = string(stde.Bytes())
|
||||||
|
return
|
||||||
|
}
|
@ -16,6 +16,7 @@ import (
|
|||||||
|
|
||||||
const templateSuffix = ".template"
|
const templateSuffix = ".template"
|
||||||
|
|
||||||
|
// TemplateHelper helps with creating projects
|
||||||
type TemplateHelper struct {
|
type TemplateHelper struct {
|
||||||
system *SystemHelper
|
system *SystemHelper
|
||||||
fs *FSHelper
|
fs *FSHelper
|
||||||
@ -25,12 +26,14 @@ type TemplateHelper struct {
|
|||||||
metadataFilename string
|
metadataFilename string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Template defines a single template
|
||||||
type Template struct {
|
type Template struct {
|
||||||
Name string
|
Name string
|
||||||
Dir string
|
Dir string
|
||||||
Metadata map[string]interface{}
|
Metadata map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewTemplateHelper creates a new template helper
|
||||||
func NewTemplateHelper() *TemplateHelper {
|
func NewTemplateHelper() *TemplateHelper {
|
||||||
result := TemplateHelper{
|
result := TemplateHelper{
|
||||||
system: NewSystemHelper(),
|
system: NewSystemHelper(),
|
||||||
@ -45,6 +48,7 @@ func NewTemplateHelper() *TemplateHelper {
|
|||||||
return &result
|
return &result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTemplateNames returns a map of all available templates
|
||||||
func (t *TemplateHelper) GetTemplateNames() (map[string]string, error) {
|
func (t *TemplateHelper) GetTemplateNames() (map[string]string, error) {
|
||||||
templateDirs, err := t.fs.GetSubdirs(t.templateDir)
|
templateDirs, err := t.fs.GetSubdirs(t.templateDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -53,6 +57,8 @@ func (t *TemplateHelper) GetTemplateNames() (map[string]string, error) {
|
|||||||
return templateDirs, nil
|
return templateDirs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTemplateDetails returns a map of Template structs containing details
|
||||||
|
// of the found templates
|
||||||
func (t *TemplateHelper) GetTemplateDetails() (map[string]*Template, error) {
|
func (t *TemplateHelper) GetTemplateDetails() (map[string]*Template, error) {
|
||||||
templateDirs, err := t.fs.GetSubdirs(t.templateDir)
|
templateDirs, err := t.fs.GetSubdirs(t.templateDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -81,6 +87,7 @@ func (t *TemplateHelper) GetTemplateDetails() (map[string]*Template, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadMetadata loads the template's 'metadata.json' file
|
||||||
func (t *TemplateHelper) LoadMetadata(dir string) (map[string]interface{}, error) {
|
func (t *TemplateHelper) LoadMetadata(dir string) (map[string]interface{}, error) {
|
||||||
templateFile := filepath.Join(dir, t.metadataFilename)
|
templateFile := filepath.Join(dir, t.metadataFilename)
|
||||||
result := make(map[string]interface{})
|
result := make(map[string]interface{})
|
||||||
@ -95,6 +102,7 @@ func (t *TemplateHelper) LoadMetadata(dir string) (map[string]interface{}, error
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TemplateExists returns true if the given template name exists
|
||||||
func (t *TemplateHelper) TemplateExists(templateName string) (bool, error) {
|
func (t *TemplateHelper) TemplateExists(templateName string) (bool, error) {
|
||||||
templates, err := t.GetTemplateNames()
|
templates, err := t.GetTemplateNames()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -104,6 +112,8 @@ func (t *TemplateHelper) TemplateExists(templateName string) (bool, error) {
|
|||||||
return exists, nil
|
return exists, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InstallTemplate installs the template given in the project options to the
|
||||||
|
// project path given
|
||||||
func (t *TemplateHelper) InstallTemplate(projectPath string, projectOptions *ProjectOptions) error {
|
func (t *TemplateHelper) InstallTemplate(projectPath string, projectOptions *ProjectOptions) error {
|
||||||
|
|
||||||
// Get template files
|
// Get template files
|
||||||
|
225
cmd/wails/3_build.go
Normal file
225
cmd/wails/3_build.go
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/leaanthony/spinner"
|
||||||
|
"github.com/wailsapp/wails/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
|
||||||
|
var bundle = false
|
||||||
|
var forceRebuild = false
|
||||||
|
buildSpinner := spinner.NewSpinner()
|
||||||
|
buildSpinner.SetSpinSpeed(50)
|
||||||
|
|
||||||
|
commandDescription := `This command will check to ensure all pre-requistes are installed prior to building. If not, it will attempt to install them. Building comprises of a number of steps: install frontend dependencies, build frontend, pack frontend, compile main application.`
|
||||||
|
initCmd := app.Command("build", "Builds your Wails project").
|
||||||
|
LongDescription(commandDescription).
|
||||||
|
BoolFlag("b", "Bundle application on successful build", &bundle).
|
||||||
|
BoolFlag("f", "Force rebuild of application components", &forceRebuild)
|
||||||
|
|
||||||
|
initCmd.Action(func() error {
|
||||||
|
log := cmd.NewLogger()
|
||||||
|
message := "Building Application"
|
||||||
|
if forceRebuild {
|
||||||
|
message += " (force rebuild)"
|
||||||
|
}
|
||||||
|
log.WhiteUnderline(message)
|
||||||
|
|
||||||
|
// Project options
|
||||||
|
projectOptions := &cmd.ProjectOptions{}
|
||||||
|
|
||||||
|
// Check we are in project directory
|
||||||
|
// Check project.json loads correctly
|
||||||
|
fs := cmd.NewFSHelper()
|
||||||
|
err := projectOptions.LoadConfig(fs.Cwd())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate config
|
||||||
|
// Check if we have a frontend
|
||||||
|
if projectOptions.FrontEnd != nil {
|
||||||
|
if projectOptions.FrontEnd.Dir == "" {
|
||||||
|
return fmt.Errorf("Frontend directory not set in project.json")
|
||||||
|
}
|
||||||
|
if projectOptions.FrontEnd.Build == "" {
|
||||||
|
return fmt.Errorf("Frontend build command not set in project.json")
|
||||||
|
}
|
||||||
|
if projectOptions.FrontEnd.Install == "" {
|
||||||
|
return fmt.Errorf("Frontend install command not set in project.json")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check pre-requisites are installed
|
||||||
|
|
||||||
|
// Program checker
|
||||||
|
program := cmd.NewProgramHelper()
|
||||||
|
|
||||||
|
if projectOptions.FrontEnd != nil {
|
||||||
|
// npm
|
||||||
|
if !program.IsInstalled("npm") {
|
||||||
|
return fmt.Errorf("it appears npm is not installed. Please install and run again")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// packr
|
||||||
|
if !program.IsInstalled("packr") {
|
||||||
|
buildSpinner.Start("Installing packr...")
|
||||||
|
err := program.InstallGoPackage("github.com/gobuffalo/packr/...")
|
||||||
|
if err != nil {
|
||||||
|
buildSpinner.Error()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buildSpinner.Success()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save project directory
|
||||||
|
projectDir := fs.Cwd()
|
||||||
|
|
||||||
|
// Install backend deps - needed?
|
||||||
|
if projectOptions.FrontEnd != nil {
|
||||||
|
// Install frontend deps
|
||||||
|
err = os.Chdir(projectOptions.FrontEnd.Dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if frontend deps have been updated
|
||||||
|
buildSpinner.Start("Installing frontend dependencies...")
|
||||||
|
|
||||||
|
requiresNPMInstall := true
|
||||||
|
|
||||||
|
// Read in package.json MD5
|
||||||
|
packageJSONMD5, err := fs.FileMD5("package.json")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const md5sumFile = "package.json.md5"
|
||||||
|
|
||||||
|
// If we aren't forcing the install and the md5sum file exists
|
||||||
|
if !forceRebuild && fs.FileExists(md5sumFile) {
|
||||||
|
// Yes - read contents
|
||||||
|
savedMD5sum, err := fs.LoadAsString(md5sumFile)
|
||||||
|
// File exists
|
||||||
|
if err == nil {
|
||||||
|
// Compare md5
|
||||||
|
if savedMD5sum == packageJSONMD5 {
|
||||||
|
// Same - no need for reinstall
|
||||||
|
requiresNPMInstall = false
|
||||||
|
buildSpinner.Success("Skipped frontend dependencies (-f to force rebuild)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Md5 sum package.json
|
||||||
|
// Different? Build
|
||||||
|
if requiresNPMInstall || forceRebuild {
|
||||||
|
// Install dependencies
|
||||||
|
err = program.RunCommand(projectOptions.FrontEnd.Install)
|
||||||
|
if err != nil {
|
||||||
|
buildSpinner.Error()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buildSpinner.Success()
|
||||||
|
|
||||||
|
// Update md5sum file
|
||||||
|
ioutil.WriteFile(md5sumFile, []byte(packageJSONMD5), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build frontend
|
||||||
|
buildSpinner.Start("Building frontend...")
|
||||||
|
err = program.RunCommand(projectOptions.FrontEnd.Build)
|
||||||
|
if err != nil {
|
||||||
|
buildSpinner.Error()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buildSpinner.Success()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run packr in project directory
|
||||||
|
err = os.Chdir(projectDir)
|
||||||
|
if err != nil {
|
||||||
|
buildSpinner.Error()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support build tags
|
||||||
|
buildTags := []string{}
|
||||||
|
|
||||||
|
// Do we have any frameworks specified?
|
||||||
|
if projectOptions.Framework != nil {
|
||||||
|
buildSpinner.Start()
|
||||||
|
buildSpinner.Success("Compiling support for " + projectOptions.Framework.Name)
|
||||||
|
buildTags = append(buildTags, projectOptions.Framework.BuildTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Initialise Go Module - if go.mod doesn't exist
|
||||||
|
// if !fs.FileExists("go.mod") {
|
||||||
|
// buildSpinner.Start("Initialising Go module...")
|
||||||
|
// err = program.RunCommand("go mod init " + projectOptions.BinaryName)
|
||||||
|
// if err != nil {
|
||||||
|
// buildSpinner.Error()
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// buildSpinner.Success()
|
||||||
|
// }
|
||||||
|
|
||||||
|
buildSpinner.Start("Installing Dependencies...")
|
||||||
|
installCommand := "go get"
|
||||||
|
err = program.RunCommand(installCommand)
|
||||||
|
if err != nil {
|
||||||
|
buildSpinner.Error()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buildSpinner.Success()
|
||||||
|
|
||||||
|
buildSpinner.Start("Packing + Compiling project...")
|
||||||
|
|
||||||
|
buildCommand := "packr build"
|
||||||
|
|
||||||
|
// Add build tags
|
||||||
|
if len(buildTags) > 0 {
|
||||||
|
buildCommand += fmt.Sprintf(" --tags '%s'", strings.Join(buildTags, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if projectOptions.BinaryName != "" {
|
||||||
|
buildCommand += " -o " + projectOptions.BinaryName
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are forcing a rebuild
|
||||||
|
if forceRebuild {
|
||||||
|
buildCommand += " -a"
|
||||||
|
}
|
||||||
|
|
||||||
|
err = program.RunCommand(buildCommand)
|
||||||
|
if err != nil {
|
||||||
|
buildSpinner.Error()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buildSpinner.Success()
|
||||||
|
|
||||||
|
if bundle == false {
|
||||||
|
logger.Yellow("Awesome! Project '%s' built!", projectOptions.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bundle app
|
||||||
|
buildSpinner.Start("Bundling Application")
|
||||||
|
bundler := cmd.NewBundleHelper()
|
||||||
|
err = bundler.Bundle(projectOptions)
|
||||||
|
if err != nil {
|
||||||
|
buildSpinner.Error()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buildSpinner.Success()
|
||||||
|
logger.Yellow("Awesome! Project '%s' built!", projectOptions.Name)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
148
event_manager.go
Normal file
148
event_manager.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// eventManager handles and processes events
|
||||||
|
type eventManager struct {
|
||||||
|
incomingEvents chan *eventData
|
||||||
|
listeners map[string][]*eventListener
|
||||||
|
exit bool
|
||||||
|
log *CustomLogger
|
||||||
|
renderer Renderer // Messages will be dispatched to the frontend
|
||||||
|
}
|
||||||
|
|
||||||
|
// newEventManager creates a new event manager with a 100 event buffer
|
||||||
|
func newEventManager() *eventManager {
|
||||||
|
return &eventManager{
|
||||||
|
incomingEvents: make(chan *eventData, 100),
|
||||||
|
listeners: make(map[string][]*eventListener),
|
||||||
|
exit: false,
|
||||||
|
log: newCustomLogger("Events"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushEvent places the given event on to the event queue
|
||||||
|
func (e *eventManager) PushEvent(eventData *eventData) {
|
||||||
|
e.incomingEvents <- eventData
|
||||||
|
}
|
||||||
|
|
||||||
|
// eventListener holds a callback function which is invoked when
|
||||||
|
// the event listened for is emitted. It has a counter which indicates
|
||||||
|
// how the total number of events it is interested in. A value of zero
|
||||||
|
// means it does not expire (default).
|
||||||
|
type eventListener struct {
|
||||||
|
callback func(...interface{}) // Function to call with emitted event data
|
||||||
|
counter int // Expire after counter callbacks. 0 = infinite
|
||||||
|
expired bool // Indicates if the listener has expired
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new event listener from the given callback function
|
||||||
|
func (e *eventManager) addEventListener(eventName string, callback func(...interface{}), counter int) error {
|
||||||
|
|
||||||
|
// Sanity check inputs
|
||||||
|
if callback == nil {
|
||||||
|
return fmt.Errorf("nil callback bassed to addEventListener")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check event has been registered before
|
||||||
|
if e.listeners[eventName] == nil {
|
||||||
|
e.listeners[eventName] = []*eventListener{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the callback
|
||||||
|
listener := &eventListener{
|
||||||
|
callback: callback,
|
||||||
|
counter: counter,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register listener
|
||||||
|
e.listeners[eventName] = append(e.listeners[eventName], listener)
|
||||||
|
|
||||||
|
// All good mate
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *eventManager) On(eventName string, callback func(...interface{})) {
|
||||||
|
// Add a persistent eventListener (counter = 0)
|
||||||
|
e.addEventListener(eventName, callback, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit broadcasts the given event to the subscribed listeners
|
||||||
|
func (e *eventManager) Emit(eventName string, optionalData ...interface{}) {
|
||||||
|
e.incomingEvents <- &eventData{Name: eventName, Data: optionalData}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts the event manager's queue processing
|
||||||
|
func (e *eventManager) start(renderer Renderer) {
|
||||||
|
|
||||||
|
e.log.Info("Starting")
|
||||||
|
|
||||||
|
// Store renderer
|
||||||
|
e.renderer = renderer
|
||||||
|
|
||||||
|
// Set up waitgroup so we can wait for goroutine to start
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
// Run main loop in seperate goroutine
|
||||||
|
go func() {
|
||||||
|
wg.Done()
|
||||||
|
e.log.Info("Listening")
|
||||||
|
for e.exit == false {
|
||||||
|
// TODO: Listen for application exit
|
||||||
|
select {
|
||||||
|
case event := <-e.incomingEvents:
|
||||||
|
e.log.DebugFields("Got Event", Fields{
|
||||||
|
"data": event.Data,
|
||||||
|
"name": event.Name,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Notify renderer
|
||||||
|
e.renderer.NotifyEvent(event)
|
||||||
|
|
||||||
|
// Notify Go listeners
|
||||||
|
var listenersToRemove []*eventListener
|
||||||
|
|
||||||
|
// Iterate listeners
|
||||||
|
for _, listener := range e.listeners[event.Name] {
|
||||||
|
|
||||||
|
// Call listener, perhaps with data
|
||||||
|
if event.Data == nil {
|
||||||
|
go listener.callback()
|
||||||
|
} else {
|
||||||
|
unpacked := event.Data.([]interface{})
|
||||||
|
go listener.callback(unpacked...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update listen counter
|
||||||
|
if listener.counter > 0 {
|
||||||
|
listener.counter = listener.counter - 1
|
||||||
|
if listener.counter == 0 {
|
||||||
|
listener.expired = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove expired listners in place
|
||||||
|
if len(listenersToRemove) > 0 {
|
||||||
|
listeners := e.listeners[event.Name][:0]
|
||||||
|
for _, listener := range listeners {
|
||||||
|
if !listener.expired {
|
||||||
|
listeners = append(listeners, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for goroutine to start
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *eventManager) stop() {
|
||||||
|
e.exit = true
|
||||||
|
}
|
9
go.mod
9
go.mod
@ -3,8 +3,14 @@ module github.com/wailsapp/wails
|
|||||||
require (
|
require (
|
||||||
github.com/AlecAivazis/survey v1.7.1
|
github.com/AlecAivazis/survey v1.7.1
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc
|
||||||
|
github.com/dchest/cssmin v0.0.0-20151210170030-fb8d9b44afdc // indirect
|
||||||
|
github.com/dchest/htmlmin v0.0.0-20150526090704-e254725e81ac
|
||||||
|
github.com/dchest/jsmin v0.0.0-20160823214000-faeced883947 // indirect
|
||||||
github.com/fatih/color v1.7.0
|
github.com/fatih/color v1.7.0
|
||||||
|
github.com/go-playground/colors v1.2.0
|
||||||
github.com/gobuffalo/packr v1.21.9
|
github.com/gobuffalo/packr v1.21.9
|
||||||
|
github.com/gorilla/websocket v1.4.0
|
||||||
|
github.com/jackmordaunt/icns v1.0.0
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/leaanthony/spinner v0.4.0
|
github.com/leaanthony/spinner v0.4.0
|
||||||
github.com/leaanthony/synx v0.0.0-20180923230033-60efbd9984b0 // indirect
|
github.com/leaanthony/synx v0.0.0-20180923230033-60efbd9984b0 // indirect
|
||||||
@ -13,5 +19,8 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.4 // indirect
|
github.com/mattn/go-isatty v0.0.4 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||||
github.com/mitchellh/go-homedir v1.0.0
|
github.com/mitchellh/go-homedir v1.0.0
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.2.0
|
||||||
|
golang.org/x/net v0.0.0-20190107155100-1a61f4433d85 // indirect
|
||||||
gopkg.in/AlecAivazis/survey.v1 v1.7.1 // indirect
|
gopkg.in/AlecAivazis/survey.v1 v1.7.1 // indirect
|
||||||
)
|
)
|
||||||
|
20
go.sum
20
go.sum
@ -14,6 +14,12 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz
|
|||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dchest/cssmin v0.0.0-20151210170030-fb8d9b44afdc h1:VBS1z48BFEe00G81z8MKOtwX7f/ISkuH38NscT8iVPw=
|
||||||
|
github.com/dchest/cssmin v0.0.0-20151210170030-fb8d9b44afdc/go.mod h1:ABJPuor7YlcsHmvJ1QxX38e2NcufLY3hm0yXv+cy9sI=
|
||||||
|
github.com/dchest/htmlmin v0.0.0-20150526090704-e254725e81ac h1:DpMwFluHWoZpV9ex5XjkWO4HyCz5HLVI8XbHw0FhHi4=
|
||||||
|
github.com/dchest/htmlmin v0.0.0-20150526090704-e254725e81ac/go.mod h1:XsAE+b4rOZc8gvgsgF+wU75mNBvBcyED1wdd9PBLlJ0=
|
||||||
|
github.com/dchest/jsmin v0.0.0-20160823214000-faeced883947 h1:Fm10/KNuoAyBm2P5P5H91Xy21hGcZnBdjR+cMdytv1M=
|
||||||
|
github.com/dchest/jsmin v0.0.0-20160823214000-faeced883947/go.mod h1:Dv9D0NUlAsaQcGQZa5kc5mqR9ua72SmA8VXi4cd+cBw=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
@ -22,6 +28,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
|
|||||||
github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/go-playground/colors v1.2.0 h1:0EdjTXKrr2g1L/LQTYtIqabeHpZuGZz1U4osS1T8+5M=
|
||||||
|
github.com/go-playground/colors v1.2.0/go.mod h1:miw1R2JIE19cclPxsXqNdzLZsk4DP4iF+m88bRc7kfM=
|
||||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
github.com/gobuffalo/buffalo v0.12.8-0.20181004233540-fac9bb505aa8/go.mod h1:sLyT7/dceRXJUxSsE813JTQtA3Eb1vjxWfo/N//vXIY=
|
github.com/gobuffalo/buffalo v0.12.8-0.20181004233540-fac9bb505aa8/go.mod h1:sLyT7/dceRXJUxSsE813JTQtA3Eb1vjxWfo/N//vXIY=
|
||||||
github.com/gobuffalo/buffalo v0.13.0/go.mod h1:Mjn1Ba9wpIbpbrD+lIDMy99pQ0H0LiddMIIDGse7qT4=
|
github.com/gobuffalo/buffalo v0.13.0/go.mod h1:Mjn1Ba9wpIbpbrD+lIDMy99pQ0H0LiddMIIDGse7qT4=
|
||||||
@ -185,11 +193,15 @@ github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5
|
|||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
github.com/gorilla/sessions v1.1.2/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
github.com/gorilla/sessions v1.1.2/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||||
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||||
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
|
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
|
||||||
github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
||||||
|
github.com/jackmordaunt/icns v1.0.0 h1:RYSxplerf/l/DUd09AHtITwckkv/mqjVv4DjYdPmAMQ=
|
||||||
|
github.com/jackmordaunt/icns v1.0.0/go.mod h1:7TTQVEuGzVVfOPPlLNHJIkzA6CoV7aH1Dv9dW351oOo=
|
||||||
github.com/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU=
|
github.com/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU=
|
||||||
github.com/joho/godotenv v1.2.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
github.com/joho/godotenv v1.2.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||||
@ -246,6 +258,8 @@ github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
|
|||||||
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/monoculum/formam v0.0.0-20180901015400-4e68be1d79ba/go.mod h1:RKgILGEJq24YyJ2ban8EO0RUVSJlF1pGsEvoLEACr/Q=
|
github.com/monoculum/formam v0.0.0-20180901015400-4e68be1d79ba/go.mod h1:RKgILGEJq24YyJ2ban8EO0RUVSJlF1pGsEvoLEACr/Q=
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
|
github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
@ -271,6 +285,7 @@ github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.
|
|||||||
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||||
github.com/sirupsen/logrus v1.1.0/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
|
github.com/sirupsen/logrus v1.1.0/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
|
||||||
github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
|
github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
|
||||||
|
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||||
@ -301,6 +316,7 @@ golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnf
|
|||||||
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180816102801-aaf60122140d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180816102801-aaf60122140d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@ -314,7 +330,10 @@ golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73r
|
|||||||
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181207154023-610586996380 h1:zPQexyRtNYBc7bcHmehl1dH6TB3qn8zytv8cBGLDNY0=
|
||||||
golang.org/x/net v0.0.0-20181207154023-610586996380/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181207154023-610586996380/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190107155100-1a61f4433d85 h1:3DfFuyqY+mca6oIDfim5rft3+Kl/CHLe7RdPrUMzwv0=
|
||||||
|
golang.org/x/net v0.0.0-20190107155100-1a61f4433d85/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@ -333,6 +352,7 @@ golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||||||
golang.org/x/sys v0.0.0-20181106135930-3a76605856fd/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181106135930-3a76605856fd/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e h1:njOxP/wVblhCLIUhjHXf6X+dzTt5OQ3vMQo9mkOIKIo=
|
||||||
golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
38
ipc_call.go
Normal file
38
ipc_call.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type callData struct {
|
||||||
|
BindingName string `json:"bindingName"`
|
||||||
|
Data string `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
messageProcessors["call"] = processCallData
|
||||||
|
}
|
||||||
|
|
||||||
|
func processCallData(message *ipcMessage) (*ipcMessage, error) {
|
||||||
|
|
||||||
|
var payload callData
|
||||||
|
|
||||||
|
// Decode binding call data
|
||||||
|
payloadMap := message.Payload.(map[string]interface{})
|
||||||
|
|
||||||
|
// Check for binding name
|
||||||
|
if payloadMap["bindingName"] == nil {
|
||||||
|
return nil, fmt.Errorf("bindingName not given in call")
|
||||||
|
}
|
||||||
|
payload.BindingName = payloadMap["bindingName"].(string)
|
||||||
|
|
||||||
|
// Check for data
|
||||||
|
if payloadMap["data"] != nil {
|
||||||
|
payload.Data = payloadMap["data"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reassign payload to decoded data
|
||||||
|
message.Payload = &payload
|
||||||
|
|
||||||
|
return message, nil
|
||||||
|
}
|
40
ipc_event.go
Normal file
40
ipc_event.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type eventData struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the message handler
|
||||||
|
func init() {
|
||||||
|
messageProcessors["event"] = processEventData
|
||||||
|
}
|
||||||
|
|
||||||
|
// This processes the given event message
|
||||||
|
func processEventData(message *ipcMessage) (*ipcMessage, error) {
|
||||||
|
|
||||||
|
// TODO: Is it worth double checking this is actually an event message,
|
||||||
|
// even though that's done by the caller?
|
||||||
|
var payload eventData
|
||||||
|
|
||||||
|
// Decode event data
|
||||||
|
payloadMap := message.Payload.(map[string]interface{})
|
||||||
|
payload.Name = payloadMap["name"].(string)
|
||||||
|
|
||||||
|
// decode the payload data
|
||||||
|
var data []interface{}
|
||||||
|
err := json.Unmarshal([]byte(payloadMap["data"].(string)), &data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
payload.Data = data
|
||||||
|
|
||||||
|
// Reassign payload to decoded data
|
||||||
|
message.Payload = &payload
|
||||||
|
|
||||||
|
return message, nil
|
||||||
|
}
|
27
ipc_log.go
Normal file
27
ipc_log.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
type logData struct {
|
||||||
|
Level string `json:"level"`
|
||||||
|
Message string `json:"string"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the message handler
|
||||||
|
func init() {
|
||||||
|
messageProcessors["log"] = processLogData
|
||||||
|
}
|
||||||
|
|
||||||
|
// This processes the given log message
|
||||||
|
func processLogData(message *ipcMessage) (*ipcMessage, error) {
|
||||||
|
|
||||||
|
var payload logData
|
||||||
|
|
||||||
|
// Decode event data
|
||||||
|
payloadMap := message.Payload.(map[string]interface{})
|
||||||
|
payload.Level = payloadMap["level"].(string)
|
||||||
|
payload.Message = payloadMap["message"].(string)
|
||||||
|
|
||||||
|
// Reassign payload to decoded data
|
||||||
|
message.Payload = &payload
|
||||||
|
|
||||||
|
return message, nil
|
||||||
|
}
|
162
ipc_manager.go
Normal file
162
ipc_manager.go
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ipcManager struct {
|
||||||
|
renderer Renderer // The renderer
|
||||||
|
messageQueue chan *ipcMessage
|
||||||
|
// quitChannel chan struct{}
|
||||||
|
// signals chan os.Signal
|
||||||
|
log *CustomLogger
|
||||||
|
eventManager *eventManager
|
||||||
|
bindingManager *bindingManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func newIPCManager() *ipcManager {
|
||||||
|
result := &ipcManager{
|
||||||
|
messageQueue: make(chan *ipcMessage, 100),
|
||||||
|
// quitChannel: make(chan struct{}),
|
||||||
|
// signals: make(chan os.Signal, 1),
|
||||||
|
log: newCustomLogger("IPC"),
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the renderer, returns the dispatch function
|
||||||
|
func (i *ipcManager) bindRenderer(renderer Renderer) {
|
||||||
|
i.renderer = renderer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ipcManager) start(eventManager *eventManager, bindingManager *bindingManager) {
|
||||||
|
|
||||||
|
// Store manager references
|
||||||
|
i.eventManager = eventManager
|
||||||
|
i.bindingManager = bindingManager
|
||||||
|
|
||||||
|
i.log.Info("Starting")
|
||||||
|
// signal.Notify(manager.signals, os.Interrupt)
|
||||||
|
go func() {
|
||||||
|
running := true
|
||||||
|
for running {
|
||||||
|
select {
|
||||||
|
case incomingMessage := <-i.messageQueue:
|
||||||
|
i.log.DebugFields("Processing message", Fields{
|
||||||
|
"1D": &incomingMessage,
|
||||||
|
})
|
||||||
|
switch incomingMessage.Type {
|
||||||
|
case "call":
|
||||||
|
callData := incomingMessage.Payload.(*callData)
|
||||||
|
i.log.DebugFields("Processing call", Fields{
|
||||||
|
"1D": &incomingMessage,
|
||||||
|
"bindingName": callData.BindingName,
|
||||||
|
"data": callData.Data,
|
||||||
|
})
|
||||||
|
go func() {
|
||||||
|
result, err := bindingManager.processCall(callData)
|
||||||
|
i.log.DebugFields("processed call", Fields{"result": result, "err": err})
|
||||||
|
if err != nil {
|
||||||
|
incomingMessage.ReturnError(err.Error())
|
||||||
|
} else {
|
||||||
|
incomingMessage.ReturnSuccess(result)
|
||||||
|
}
|
||||||
|
i.log.DebugFields("Finished processing call", Fields{
|
||||||
|
"1D": &incomingMessage,
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
case "event":
|
||||||
|
|
||||||
|
// Extract event data
|
||||||
|
eventData := incomingMessage.Payload.(*eventData)
|
||||||
|
|
||||||
|
// Log
|
||||||
|
i.log.DebugFields("Processing event", Fields{
|
||||||
|
"name": eventData.Name,
|
||||||
|
"data": eventData.Data,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Push the event to the event manager
|
||||||
|
i.eventManager.PushEvent(eventData)
|
||||||
|
|
||||||
|
// Log
|
||||||
|
i.log.DebugFields("Finished processing event", Fields{
|
||||||
|
"name": eventData.Name,
|
||||||
|
})
|
||||||
|
case "log":
|
||||||
|
logdata := incomingMessage.Payload.(*logData)
|
||||||
|
switch logdata.Level {
|
||||||
|
case "info":
|
||||||
|
logger.Info(logdata.Message)
|
||||||
|
case "debug":
|
||||||
|
logger.Debug(logdata.Message)
|
||||||
|
case "warning":
|
||||||
|
logger.Warning(logdata.Message)
|
||||||
|
case "error":
|
||||||
|
logger.Error(logdata.Message)
|
||||||
|
case "fatal":
|
||||||
|
logger.Fatal(logdata.Message)
|
||||||
|
default:
|
||||||
|
i.log.ErrorFields("Invalid log level sent", Fields{
|
||||||
|
"level": logdata.Level,
|
||||||
|
"message": logdata.Message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
i.log.Debugf("bad message sent to MessageQueue! Unknown type: %s", incomingMessage.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log
|
||||||
|
i.log.DebugFields("Finished processing message", Fields{
|
||||||
|
"1D": &incomingMessage,
|
||||||
|
})
|
||||||
|
// case <-manager.quitChannel:
|
||||||
|
// Debug("[MessageQueue] Quit caught")
|
||||||
|
// running = false
|
||||||
|
// case <-manager.signals:
|
||||||
|
// Debug("[MessageQueue] Signal caught")
|
||||||
|
// running = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i.log.Debug("Stopping")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch receives JSON encoded messages from the renderer.
|
||||||
|
// It processes the message to ensure that it is valid and places
|
||||||
|
// the processed message on the message queue
|
||||||
|
func (i *ipcManager) Dispatch(message string) {
|
||||||
|
|
||||||
|
// Create a new IPC Message
|
||||||
|
incomingMessage, err := newIPCMessage(message, i.SendResponse)
|
||||||
|
if err != nil {
|
||||||
|
i.log.ErrorFields("Could not understand incoming message! ", map[string]interface{}{
|
||||||
|
"message": message,
|
||||||
|
"error": err,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put message on queue
|
||||||
|
i.log.DebugFields("Message received", map[string]interface{}{
|
||||||
|
"type": incomingMessage.Type,
|
||||||
|
"payload": incomingMessage.Payload,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Put incoming message on the message queue
|
||||||
|
i.messageQueue <- incomingMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendResponse sends the given response back to the frontend
|
||||||
|
func (i *ipcManager) SendResponse(response *ipcResponse) error {
|
||||||
|
|
||||||
|
// Serialise the Message
|
||||||
|
data, err := response.Serialise()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call back to the front end
|
||||||
|
return i.renderer.Callback(data)
|
||||||
|
}
|
93
ipc_message.go
Normal file
93
ipc_message.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message handler
|
||||||
|
type messageProcessorFunc func(*ipcMessage) (*ipcMessage, error)
|
||||||
|
|
||||||
|
var messageProcessors = make(map[string]messageProcessorFunc)
|
||||||
|
|
||||||
|
// ipcMessage is the struct version of the Message sent from the frontend.
|
||||||
|
// The payload has the specialised message data
|
||||||
|
type ipcMessage struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Payload interface{} `json:"payload"`
|
||||||
|
CallbackID string `json:"callbackid,omitempty"`
|
||||||
|
sendResponse func(*ipcResponse) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMessage(incomingMessage string) (*ipcMessage, error) {
|
||||||
|
// Parse message
|
||||||
|
var message ipcMessage
|
||||||
|
err := json.Unmarshal([]byte(incomingMessage), &message)
|
||||||
|
return &message, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func newIPCMessage(incomingMessage string, responseFunction func(*ipcResponse) error) (*ipcMessage, error) {
|
||||||
|
|
||||||
|
// Parse the Message
|
||||||
|
message, err := parseMessage(incomingMessage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check message type is valid
|
||||||
|
messageProcessor := messageProcessors[message.Type]
|
||||||
|
if messageProcessor == nil {
|
||||||
|
return nil, fmt.Errorf("unknown message type: %s", message.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process message payload
|
||||||
|
message, err = messageProcessor(message)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the response function
|
||||||
|
message.sendResponse = responseFunction
|
||||||
|
|
||||||
|
return message, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasCallbackID checks if the message can send an error back to the frontend
|
||||||
|
func (m *ipcMessage) hasCallbackID() error {
|
||||||
|
if m.CallbackID == "" {
|
||||||
|
return fmt.Errorf("attempted to return error to message with no Callback ID")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReturnError returns an error back to the frontend
|
||||||
|
func (m *ipcMessage) ReturnError(format string, args ...interface{}) error {
|
||||||
|
|
||||||
|
// Ignore ReturnError if no callback ID given
|
||||||
|
err := m.hasCallbackID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create response
|
||||||
|
response := newErrorResponse(m.CallbackID, fmt.Sprintf(format, args...))
|
||||||
|
|
||||||
|
// Send response
|
||||||
|
return m.sendResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReturnSuccess returns a success message back with the given data
|
||||||
|
func (m *ipcMessage) ReturnSuccess(data interface{}) error {
|
||||||
|
|
||||||
|
// Ignore ReturnSuccess if no callback ID given
|
||||||
|
err := m.hasCallbackID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the response
|
||||||
|
response := newSuccessResponse(m.CallbackID, data)
|
||||||
|
|
||||||
|
// Send response
|
||||||
|
return m.sendResponse(response)
|
||||||
|
}
|
43
ipc_response.go
Normal file
43
ipc_response.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ipcResponse contains the response data from an RPC call
|
||||||
|
type ipcResponse struct {
|
||||||
|
CallbackID string `json:"callbackid"`
|
||||||
|
ErrorMessage string `json:"error,omitempty"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// newErrorResponse returns the given error message to the frontend with the callbackid
|
||||||
|
func newErrorResponse(callbackID string, errorMessage string) *ipcResponse {
|
||||||
|
// Create response object
|
||||||
|
result := &ipcResponse{
|
||||||
|
CallbackID: callbackID,
|
||||||
|
ErrorMessage: errorMessage,
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSuccessResponse returns the given data to the frontend with the callbackid
|
||||||
|
func newSuccessResponse(callbackID string, data interface{}) *ipcResponse {
|
||||||
|
|
||||||
|
// Create response object
|
||||||
|
result := &ipcResponse{
|
||||||
|
CallbackID: callbackID,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialise formats the response to a string
|
||||||
|
func (i *ipcResponse) Serialise() (string, error) {
|
||||||
|
b, err := json.Marshal(i)
|
||||||
|
result := strings.Replace(string(b), "\\", "\\\\", -1)
|
||||||
|
result = strings.Replace(result, "'", "\\'", -1)
|
||||||
|
return result, err
|
||||||
|
}
|
40
log.go
Normal file
40
log.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Global logger reference
|
||||||
|
var logger = log.New()
|
||||||
|
|
||||||
|
// Fields is used by the customLogger object to output
|
||||||
|
// fields along with a message
|
||||||
|
type Fields map[string]interface{}
|
||||||
|
|
||||||
|
// Default options for the global logger
|
||||||
|
func init() {
|
||||||
|
logger.SetOutput(os.Stdout)
|
||||||
|
logger.SetLevel(log.DebugLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the log level to the given level
|
||||||
|
func setLogLevel(level string) {
|
||||||
|
switch strings.ToLower(level) {
|
||||||
|
case "info":
|
||||||
|
logger.SetLevel(log.InfoLevel)
|
||||||
|
case "debug":
|
||||||
|
logger.SetLevel(log.DebugLevel)
|
||||||
|
case "warn":
|
||||||
|
logger.SetLevel(log.WarnLevel)
|
||||||
|
case "fatal":
|
||||||
|
logger.SetLevel(log.FatalLevel)
|
||||||
|
case "panic":
|
||||||
|
logger.SetLevel(log.PanicLevel)
|
||||||
|
default:
|
||||||
|
logger.SetLevel(log.DebugLevel)
|
||||||
|
logger.Warnf("Log level '%s' not recognised. Setting to Debug.", level)
|
||||||
|
}
|
||||||
|
}
|
82
log_custom.go
Normal file
82
log_custom.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
type CustomLogger struct {
|
||||||
|
prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCustomLogger(prefix string) *CustomLogger {
|
||||||
|
return &CustomLogger{
|
||||||
|
prefix: "[" + prefix + "] ",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomLogger) Info(message string) {
|
||||||
|
logger.Info(c.prefix + message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomLogger) Infof(message string, args ...interface{}) {
|
||||||
|
logger.Infof(c.prefix+message, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomLogger) InfoFields(message string, fields Fields) {
|
||||||
|
logger.WithFields(map[string]interface{}(fields)).Info(c.prefix + message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomLogger) Debug(message string) {
|
||||||
|
logger.Debug(c.prefix + message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomLogger) Debugf(message string, args ...interface{}) {
|
||||||
|
logger.Debugf(c.prefix+message, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomLogger) DebugFields(message string, fields Fields) {
|
||||||
|
logger.WithFields(map[string]interface{}(fields)).Debug(c.prefix + message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomLogger) Warn(message string) {
|
||||||
|
logger.Warn(c.prefix + message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomLogger) Warnf(message string, args ...interface{}) {
|
||||||
|
logger.Warnf(c.prefix+message, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomLogger) WarnFields(message string, fields Fields) {
|
||||||
|
logger.WithFields(map[string]interface{}(fields)).Warn(c.prefix + message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomLogger) Error(message string) {
|
||||||
|
logger.Error(c.prefix + message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomLogger) Errorf(message string, args ...interface{}) {
|
||||||
|
logger.Errorf(c.prefix+message, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomLogger) ErrorFields(message string, fields Fields) {
|
||||||
|
logger.WithFields(map[string]interface{}(fields)).Error(c.prefix + message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomLogger) Fatal(message string) {
|
||||||
|
logger.Fatal(c.prefix + message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomLogger) Fatalf(message string, args ...interface{}) {
|
||||||
|
logger.Fatalf(c.prefix+message, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomLogger) FatalFields(message string, fields Fields) {
|
||||||
|
logger.WithFields(map[string]interface{}(fields)).Fatal(c.prefix + message)
|
||||||
|
}
|
||||||
|
func (c *CustomLogger) Panic(message string) {
|
||||||
|
logger.Panic(c.prefix + message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomLogger) Panicf(message string, args ...interface{}) {
|
||||||
|
logger.Panicf(c.prefix+message, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomLogger) PanicFields(message string, fields Fields) {
|
||||||
|
logger.WithFields(map[string]interface{}(fields)).Panic(c.prefix + message)
|
||||||
|
}
|
31
renderer.go
Normal file
31
renderer.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
// Renderer is an interface describing a Wails target to render the app to
|
||||||
|
type Renderer interface {
|
||||||
|
Initialise(*AppConfig, *ipcManager, *eventManager) error
|
||||||
|
Run() error
|
||||||
|
|
||||||
|
// Binding
|
||||||
|
NewBinding(bindingName string) error
|
||||||
|
Callback(data string) error
|
||||||
|
|
||||||
|
// Events
|
||||||
|
NotifyEvent(eventData *eventData) error
|
||||||
|
|
||||||
|
// Injection
|
||||||
|
InjectFramework(js string, css string)
|
||||||
|
AddJSList(js []string)
|
||||||
|
AddCSSList(css []string)
|
||||||
|
|
||||||
|
// Dialog Runtime
|
||||||
|
SelectFile() string
|
||||||
|
SelectDirectory() string
|
||||||
|
SelectSaveFile() string
|
||||||
|
|
||||||
|
// Window Runtime
|
||||||
|
SetColour(string) error
|
||||||
|
Fullscreen()
|
||||||
|
UnFullscreen()
|
||||||
|
SetTitle(title string)
|
||||||
|
Close()
|
||||||
|
}
|
279
renderer_headless.go
Normal file
279
renderer_headless.go
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/dchest/htmlmin"
|
||||||
|
"github.com/gobuffalo/packr"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
var headlessAssets = packr.NewBox("./assets/headless")
|
||||||
|
var defaultAssets = packr.NewBox("./assets/default")
|
||||||
|
|
||||||
|
// Window defines the main application window
|
||||||
|
// Default values in []
|
||||||
|
type Headless struct {
|
||||||
|
// Common
|
||||||
|
log *CustomLogger
|
||||||
|
ipcManager *ipcManager
|
||||||
|
appConfig *AppConfig
|
||||||
|
eventManager *eventManager
|
||||||
|
bindingCache []string
|
||||||
|
frameworkJS string
|
||||||
|
frameworkCSS string
|
||||||
|
jsCache []string
|
||||||
|
cssCache []string
|
||||||
|
|
||||||
|
// Headless specific
|
||||||
|
initialisationJS []string
|
||||||
|
server *http.Server
|
||||||
|
theConnection *websocket.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headless) Initialise(appConfig *AppConfig, ipcManager *ipcManager, eventManager *eventManager) error {
|
||||||
|
h.ipcManager = ipcManager
|
||||||
|
h.appConfig = appConfig
|
||||||
|
h.eventManager = eventManager
|
||||||
|
h.log = newCustomLogger("Headless")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headless) evalJS(js string) error {
|
||||||
|
if h.theConnection == nil {
|
||||||
|
h.initialisationJS = append(h.initialisationJS, js)
|
||||||
|
} else {
|
||||||
|
h.sendMessage(h.theConnection, js)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headless) injectCSS(css string) {
|
||||||
|
// Minify css to overcome issues in the browser with carriage returns
|
||||||
|
minified, err := htmlmin.Minify([]byte(css), &htmlmin.Options{
|
||||||
|
MinifyStyles: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
h.log.Fatal("Unable to minify CSS: " + css)
|
||||||
|
}
|
||||||
|
minifiedCSS := string(minified)
|
||||||
|
minifiedCSS = strings.Replace(minifiedCSS, "'", "\\'", -1)
|
||||||
|
minifiedCSS = strings.Replace(minifiedCSS, "\n", " ", -1)
|
||||||
|
inject := fmt.Sprintf("wails._.injectCSS('%s')", minifiedCSS)
|
||||||
|
h.evalJS(inject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headless) rootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
indexHTML := headlessAssets.String("index.html")
|
||||||
|
fmt.Fprintf(w, "%s", indexHTML)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headless) wsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := websocket.Upgrade(w, r, w.Header(), 1024, 1024)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Could not open websocket connection", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
h.theConnection = conn
|
||||||
|
h.log.Infof("Connection %p accepted.", h.theConnection)
|
||||||
|
conn.SetCloseHandler(func(int, string) error {
|
||||||
|
h.log.Infof("Connection %p dropped.", h.theConnection)
|
||||||
|
h.theConnection = nil
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
go h.start(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headless) sendMessage(conn *websocket.Conn, msg string) {
|
||||||
|
if err := conn.WriteMessage(websocket.TextMessage, []byte(msg)); err != nil {
|
||||||
|
h.log.Error(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headless) start(conn *websocket.Conn) {
|
||||||
|
|
||||||
|
// set external.invoke
|
||||||
|
h.log.Infof("Connected to frontend.")
|
||||||
|
|
||||||
|
// If we are given an HTML fragment, load jquery
|
||||||
|
// for the html() function
|
||||||
|
if h.appConfig.isHTMLFragment {
|
||||||
|
// Inject jquery
|
||||||
|
jquery := defaultAssets.String("jquery.3.3.1.min.js")
|
||||||
|
h.evalJS(jquery)
|
||||||
|
}
|
||||||
|
|
||||||
|
wailsRuntime := defaultAssets.String("wails.js")
|
||||||
|
h.evalJS(wailsRuntime)
|
||||||
|
|
||||||
|
// Inject the initial JS
|
||||||
|
for _, js := range h.initialisationJS {
|
||||||
|
h.sendMessage(conn, js)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject bindings
|
||||||
|
for _, binding := range h.bindingCache {
|
||||||
|
h.evalJS(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject Framework
|
||||||
|
if h.frameworkJS != "" {
|
||||||
|
h.evalJS(h.frameworkJS)
|
||||||
|
}
|
||||||
|
if h.frameworkCSS != "" {
|
||||||
|
h.injectCSS(h.frameworkCSS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If given an HMTL fragment, mount it on #app
|
||||||
|
// Otherwise, replace the html tag
|
||||||
|
var injectHTML string
|
||||||
|
if h.appConfig.isHTMLFragment {
|
||||||
|
injectHTML = fmt.Sprintf("$('#app').html('%s')", h.appConfig.HTML)
|
||||||
|
} else {
|
||||||
|
injectHTML = fmt.Sprintf("$('html').html('%s')", h.appConfig.HTML)
|
||||||
|
}
|
||||||
|
h.evalJS(injectHTML)
|
||||||
|
|
||||||
|
// Inject user CSS
|
||||||
|
if h.appConfig.CSS != "" {
|
||||||
|
outputCSS := fmt.Sprintf("%.45s", h.appConfig.CSS)
|
||||||
|
if len(outputCSS) > 45 {
|
||||||
|
outputCSS += "..."
|
||||||
|
}
|
||||||
|
h.log.DebugFields("Inject User CSS", Fields{"css": outputCSS})
|
||||||
|
h.injectCSS(h.appConfig.CSS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject all the CSS files that have been added
|
||||||
|
for _, css := range h.cssCache {
|
||||||
|
h.injectCSS(css)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject all the JS files that have been added
|
||||||
|
for _, js := range h.jsCache {
|
||||||
|
h.evalJS(js)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject user JS
|
||||||
|
if h.appConfig.JS != "" {
|
||||||
|
outputJS := fmt.Sprintf("%.45s", h.appConfig.JS)
|
||||||
|
if len(outputJS) > 45 {
|
||||||
|
outputJS += "..."
|
||||||
|
}
|
||||||
|
h.log.DebugFields("Inject User JS", Fields{"js": outputJS})
|
||||||
|
h.evalJS(h.appConfig.JS)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
messageType, buffer, err := conn.ReadMessage()
|
||||||
|
if messageType == -1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
h.log.Errorf("Error reading message: ", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
h.log.Infof("Got message: %#v\n", string(buffer))
|
||||||
|
|
||||||
|
h.ipcManager.Dispatch(string(buffer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headless) Run() error {
|
||||||
|
h.server = &http.Server{Addr: ":34115"}
|
||||||
|
http.HandleFunc("/ws", h.wsHandler)
|
||||||
|
http.HandleFunc("/", h.rootHandler)
|
||||||
|
|
||||||
|
h.log.Info("Started on port 34115")
|
||||||
|
h.log.Info("Application running at http://localhost:34115")
|
||||||
|
|
||||||
|
err := h.server.ListenAndServe()
|
||||||
|
if err != nil {
|
||||||
|
h.log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headless) NewBinding(methodName string) error {
|
||||||
|
objectCode := fmt.Sprintf("window.wails._.newBinding(`%s`);", methodName)
|
||||||
|
h.bindingCache = append(h.bindingCache, objectCode)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headless) InjectFramework(js, css string) {
|
||||||
|
h.frameworkJS = js
|
||||||
|
h.frameworkCSS = css
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headless) SelectFile() string {
|
||||||
|
h.log.Error("SelectFile() unsupported in headless mode")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
func (h *Headless) SelectDirectory() string {
|
||||||
|
h.log.Error("SelectDirectory() unsupported in headless mode")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
func (h *Headless) SelectSaveFile() string {
|
||||||
|
h.log.Error("SelectSaveFile() unsupported in headless mode")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
func (h *Headless) AddJSList(jsCache []string) {
|
||||||
|
h.jsCache = jsCache
|
||||||
|
}
|
||||||
|
func (h *Headless) AddCSSList(cssCache []string) {
|
||||||
|
h.cssCache = cssCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback sends a callback to the frontend
|
||||||
|
func (h *Headless) Callback(data string) error {
|
||||||
|
callbackCMD := fmt.Sprintf("window.wails._.callback('%s');", data)
|
||||||
|
return h.evalJS(callbackCMD)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headless) NotifyEvent(event *eventData) error {
|
||||||
|
|
||||||
|
// Look out! Nils about!
|
||||||
|
var err error
|
||||||
|
if event == nil {
|
||||||
|
err = fmt.Errorf("Sent nil event to renderer.webViewRenderer")
|
||||||
|
logger.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default data is a blank array
|
||||||
|
data := []byte("[]")
|
||||||
|
|
||||||
|
// Process event data
|
||||||
|
if event.Data != nil {
|
||||||
|
// Marshall the data
|
||||||
|
data, err = json.Marshal(event.Data)
|
||||||
|
if err != nil {
|
||||||
|
h.log.Errorf("Cannot unmarshall JSON data in event: %s ", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message := fmt.Sprintf("wails._.notify('%s','%s')", event.Name, data)
|
||||||
|
return h.evalJS(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headless) SetColour(colour string) error {
|
||||||
|
h.log.WarnFields("SetColour ignored for headless more", Fields{"col": colour})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (h *Headless) Fullscreen() {
|
||||||
|
h.log.Warn("Fullscreen() unsupported in headless mode")
|
||||||
|
}
|
||||||
|
func (h *Headless) UnFullscreen() {
|
||||||
|
h.log.Warn("UnFullscreen() unsupported in headless mode")
|
||||||
|
}
|
||||||
|
func (h *Headless) SetTitle(title string) {
|
||||||
|
h.log.WarnFields("SetTitle() unsupported in headless mode", Fields{"title": title})
|
||||||
|
}
|
||||||
|
func (h *Headless) Close() {
|
||||||
|
h.log.Warn("Close() unsupported in headless mode")
|
||||||
|
}
|
383
renderer_webview.go
Normal file
383
renderer_webview.go
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-playground/colors"
|
||||||
|
"github.com/gobuffalo/packr"
|
||||||
|
"github.com/wailsapp/wails/webview"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Window defines the main application window
|
||||||
|
// Default values in []
|
||||||
|
type webViewRenderer struct {
|
||||||
|
window webview.WebView // The webview object
|
||||||
|
ipc *ipcManager
|
||||||
|
log *CustomLogger
|
||||||
|
config *AppConfig
|
||||||
|
eventManager *eventManager
|
||||||
|
bindingCache []string
|
||||||
|
frameworkJS string
|
||||||
|
frameworkCSS string
|
||||||
|
|
||||||
|
// This is a list of all the JS/CSS that needs injecting
|
||||||
|
// It will get injected in order
|
||||||
|
jsCache []string
|
||||||
|
cssCache []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialise sets up the WebView
|
||||||
|
func (w *webViewRenderer) Initialise(config *AppConfig, ipc *ipcManager, eventManager *eventManager) error {
|
||||||
|
|
||||||
|
// Store reference to eventManager
|
||||||
|
w.eventManager = eventManager
|
||||||
|
|
||||||
|
// Set up logger
|
||||||
|
w.log = newCustomLogger("WebView")
|
||||||
|
|
||||||
|
// Set up the dispatcher function
|
||||||
|
w.ipc = ipc
|
||||||
|
ipc.bindRenderer(w)
|
||||||
|
|
||||||
|
// Save the config
|
||||||
|
w.config = config
|
||||||
|
|
||||||
|
// Create the WebView instance
|
||||||
|
w.window = webview.NewWebview(webview.Settings{
|
||||||
|
Width: config.Width,
|
||||||
|
Height: config.Height,
|
||||||
|
Title: config.Title,
|
||||||
|
Resizable: config.Resizable,
|
||||||
|
URL: config.defaultHTML,
|
||||||
|
Debug: !config.DisableInspector,
|
||||||
|
ExternalInvokeCallback: func(_ webview.WebView, message string) {
|
||||||
|
w.ipc.Dispatch(message)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// SignalManager.OnExit(w.Exit)
|
||||||
|
|
||||||
|
// Set colour
|
||||||
|
err := w.SetColour(config.Colour)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.log.Info("Initialised")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webViewRenderer) SetColour(colour string) error {
|
||||||
|
color, err := colors.Parse(colour)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rgba := color.ToRGBA()
|
||||||
|
alpha := uint8(255 * rgba.A)
|
||||||
|
w.window.Dispatch(func() {
|
||||||
|
w.window.SetColor(rgba.R, rgba.G, rgba.B, alpha)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// evalJS evaluates the given js in the WebView
|
||||||
|
// I should rename this to evilJS lol
|
||||||
|
func (w *webViewRenderer) evalJS(js string) error {
|
||||||
|
outputJS := fmt.Sprintf("%.45s", js)
|
||||||
|
if len(js) > 45 {
|
||||||
|
outputJS += "..."
|
||||||
|
}
|
||||||
|
w.log.DebugFields("Eval", Fields{"js": outputJS})
|
||||||
|
//
|
||||||
|
w.window.Dispatch(func() {
|
||||||
|
w.window.Eval(js)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// evalJSSync evaluates the given js in the WebView synchronously
|
||||||
|
// Do not call this from the main thread or you'll nuke your app because
|
||||||
|
// you won't get the callback.
|
||||||
|
func (w *webViewRenderer) evalJSSync(js string) error {
|
||||||
|
|
||||||
|
minified, err := escapeJS(js)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
outputJS := fmt.Sprintf("%.45s", js)
|
||||||
|
if len(js) > 45 {
|
||||||
|
outputJS += "..."
|
||||||
|
}
|
||||||
|
w.log.DebugFields("EvalSync", Fields{"js": outputJS})
|
||||||
|
|
||||||
|
ID := fmt.Sprintf("syncjs:%d:%d", time.Now().Unix(), rand.Intn(9999))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
exit := false
|
||||||
|
// We are done when we recieve the Callback ID
|
||||||
|
w.log.Debug("SyncJS: sending with ID = " + ID)
|
||||||
|
w.eventManager.On(ID, func(...interface{}) {
|
||||||
|
w.log.Debug("SyncJS: Got callback ID = " + ID)
|
||||||
|
wg.Done()
|
||||||
|
exit = true
|
||||||
|
})
|
||||||
|
command := fmt.Sprintf("wails._.addScript('%s', '%s')", minified, ID)
|
||||||
|
w.window.Dispatch(func() {
|
||||||
|
w.window.Eval(command)
|
||||||
|
})
|
||||||
|
for exit == false {
|
||||||
|
time.Sleep(time.Millisecond * 1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// injectCSS adds the given CSS to the WebView
|
||||||
|
func (w *webViewRenderer) injectCSS(css string) {
|
||||||
|
w.window.Dispatch(func() {
|
||||||
|
w.window.InjectCSS(css)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit the window
|
||||||
|
func (w *webViewRenderer) Exit() {
|
||||||
|
w.window.Exit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the window main loop
|
||||||
|
func (w *webViewRenderer) Run() error {
|
||||||
|
|
||||||
|
w.log.Info("Run()")
|
||||||
|
|
||||||
|
// Runtime assets
|
||||||
|
assets := packr.NewBox("./assets/default")
|
||||||
|
|
||||||
|
wailsRuntime := assets.String("wails.js")
|
||||||
|
w.evalJS(wailsRuntime)
|
||||||
|
|
||||||
|
// Ping the wait channel when the wails runtime is loaded
|
||||||
|
w.eventManager.On("wails:loaded", func(...interface{}) {
|
||||||
|
|
||||||
|
// Run this in a different go routine to free up the main process
|
||||||
|
go func() {
|
||||||
|
// Will we mount a custom component
|
||||||
|
// Inject jquery
|
||||||
|
jquery := assets.String("jquery.3.3.1.min.js")
|
||||||
|
w.evalJSSync(jquery)
|
||||||
|
|
||||||
|
// Inject Bindings
|
||||||
|
for _, binding := range w.bindingCache {
|
||||||
|
w.evalJSSync(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject Framework
|
||||||
|
if w.frameworkJS != "" {
|
||||||
|
w.evalJSSync(w.frameworkJS)
|
||||||
|
}
|
||||||
|
if w.frameworkCSS != "" {
|
||||||
|
w.injectCSS(w.frameworkCSS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we have custom html?
|
||||||
|
// If given an HMTL fragment, mount it on #app
|
||||||
|
// Otherwise, replace the html tag
|
||||||
|
var injectHTML string
|
||||||
|
if w.config.isHTMLFragment {
|
||||||
|
injectHTML = fmt.Sprintf("$('#app').html('%s')", w.config.HTML)
|
||||||
|
} else {
|
||||||
|
injectHTML = fmt.Sprintf("$('html').html('%s')", w.config.HTML)
|
||||||
|
}
|
||||||
|
w.evalJSSync(injectHTML)
|
||||||
|
|
||||||
|
// Inject user CSS
|
||||||
|
if w.config.CSS != "" {
|
||||||
|
outputCSS := fmt.Sprintf("%.45s", w.config.CSS)
|
||||||
|
if len(outputCSS) > 45 {
|
||||||
|
outputCSS += "..."
|
||||||
|
}
|
||||||
|
w.log.DebugFields("Inject User CSS", Fields{"css": outputCSS})
|
||||||
|
w.injectCSS(w.config.CSS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject all the CSS files that have been added
|
||||||
|
for _, css := range w.cssCache {
|
||||||
|
w.injectCSS(css)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject all the JS files that have been added
|
||||||
|
for _, js := range w.jsCache {
|
||||||
|
w.evalJSSync(js)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject user JS
|
||||||
|
if w.config.JS != "" {
|
||||||
|
outputJS := fmt.Sprintf("%.45s", w.config.JS)
|
||||||
|
if len(outputJS) > 45 {
|
||||||
|
outputJS += "..."
|
||||||
|
}
|
||||||
|
w.log.DebugFields("Inject User JS", Fields{"js": outputJS})
|
||||||
|
w.evalJSSync(w.config.JS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit that everything is loaded and ready
|
||||||
|
w.eventManager.Emit("wails:ready")
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Kick off main window loop
|
||||||
|
w.window.Run()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binds the given method name with the front end
|
||||||
|
func (w *webViewRenderer) NewBinding(methodName string) error {
|
||||||
|
objectCode := fmt.Sprintf("window.wails._.newBinding('%s');", methodName)
|
||||||
|
w.bindingCache = append(w.bindingCache, objectCode)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webViewRenderer) InjectFramework(js, css string) {
|
||||||
|
w.frameworkJS = js
|
||||||
|
w.frameworkCSS = css
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webViewRenderer) SelectFile() string {
|
||||||
|
var result string
|
||||||
|
|
||||||
|
// We need to run this on the main thread, however Dispatch is
|
||||||
|
// non-blocking so we launch this in a goroutine and wait for
|
||||||
|
// dispatch to finish before returning the result
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
w.window.Dispatch(func() {
|
||||||
|
result = w.window.Dialog(webview.DialogTypeOpen, 0, "Select File", "")
|
||||||
|
wg.Done()
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webViewRenderer) SelectDirectory() string {
|
||||||
|
var result string
|
||||||
|
// We need to run this on the main thread, however Dispatch is
|
||||||
|
// non-blocking so we launch this in a goroutine and wait for
|
||||||
|
// dispatch to finish before returning the result
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
w.window.Dispatch(func() {
|
||||||
|
result = w.window.Dialog(webview.DialogTypeOpen, webview.DialogFlagDirectory, "Select Directory", "")
|
||||||
|
wg.Done()
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webViewRenderer) SelectSaveFile() string {
|
||||||
|
var result string
|
||||||
|
// We need to run this on the main thread, however Dispatch is
|
||||||
|
// non-blocking so we launch this in a goroutine and wait for
|
||||||
|
// dispatch to finish before returning the result
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
w.window.Dispatch(func() {
|
||||||
|
result = w.window.Dialog(webview.DialogTypeSave, 0, "Save file", "")
|
||||||
|
wg.Done()
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddJS adds a piece of Javascript to a cache that
|
||||||
|
// gets injected at runtime
|
||||||
|
func (w *webViewRenderer) AddJSList(jsCache []string) {
|
||||||
|
w.jsCache = jsCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCSSList sets the cssCache to the given list of strings
|
||||||
|
func (w *webViewRenderer) AddCSSList(cssCache []string) {
|
||||||
|
w.cssCache = cssCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback sends a callback to the frontend
|
||||||
|
func (w *webViewRenderer) Callback(data string) error {
|
||||||
|
callbackCMD := fmt.Sprintf("window.wails._.callback('%s');", data)
|
||||||
|
return w.evalJS(callbackCMD)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webViewRenderer) NotifyEvent(event *eventData) error {
|
||||||
|
|
||||||
|
// Look out! Nils about!
|
||||||
|
var err error
|
||||||
|
if event == nil {
|
||||||
|
err = fmt.Errorf("Sent nil event to renderer.webViewRenderer")
|
||||||
|
logger.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default data is a blank array
|
||||||
|
data := []byte("[]")
|
||||||
|
|
||||||
|
// Process event data
|
||||||
|
if event.Data != nil {
|
||||||
|
// Marshall the data
|
||||||
|
data, err = json.Marshal(event.Data)
|
||||||
|
if err != nil {
|
||||||
|
w.log.Errorf("Cannot unmarshall JSON data in event: %s ", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message := fmt.Sprintf("wails._.notify('%s','%s')", event.Name, data)
|
||||||
|
return w.evalJS(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window
|
||||||
|
func (w *webViewRenderer) Fullscreen() {
|
||||||
|
if w.config.Resizable == false {
|
||||||
|
w.log.Warn("Cannot call Fullscreen() - App.Resizable = false")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.window.Dispatch(func() {
|
||||||
|
w.window.SetFullscreen(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webViewRenderer) UnFullscreen() {
|
||||||
|
if w.config.Resizable == false {
|
||||||
|
w.log.Warn("Cannot call UnFullscreen() - App.Resizable = false")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.window.Dispatch(func() {
|
||||||
|
w.window.SetFullscreen(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webViewRenderer) SetTitle(title string) {
|
||||||
|
w.window.Dispatch(func() {
|
||||||
|
w.window.SetTitle(title)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webViewRenderer) Close() {
|
||||||
|
w.window.Dispatch(func() {
|
||||||
|
w.window.Terminate()
|
||||||
|
})
|
||||||
|
}
|
17
runtime.go
Normal file
17
runtime.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
type Runtime struct {
|
||||||
|
Events *RuntimeEvents
|
||||||
|
Log *RuntimeLog
|
||||||
|
Dialog *RuntimeDialog
|
||||||
|
Window *RuntimeWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRuntime(eventManager *eventManager, renderer Renderer) *Runtime {
|
||||||
|
return &Runtime{
|
||||||
|
Events: newRuntimeEvents(eventManager),
|
||||||
|
Log: newRuntimeLog(),
|
||||||
|
Dialog: newRuntimeDialog(renderer),
|
||||||
|
Window: newRuntimeWindow(renderer),
|
||||||
|
}
|
||||||
|
}
|
23
runtime_dialog.go
Normal file
23
runtime_dialog.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
type RuntimeDialog struct {
|
||||||
|
renderer Renderer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRuntimeDialog(renderer Renderer) *RuntimeDialog {
|
||||||
|
return &RuntimeDialog{
|
||||||
|
renderer: renderer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuntimeDialog) SelectFile() string {
|
||||||
|
return r.renderer.SelectFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuntimeDialog) SelectDirectory() string {
|
||||||
|
return r.renderer.SelectDirectory()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuntimeDialog) SelectSaveFile() string {
|
||||||
|
return r.renderer.SelectSaveFile()
|
||||||
|
}
|
21
runtime_events.go
Normal file
21
runtime_events.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
type RuntimeEvents struct {
|
||||||
|
eventManager *eventManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRuntimeEvents(eventManager *eventManager) *RuntimeEvents {
|
||||||
|
return &RuntimeEvents{
|
||||||
|
eventManager: eventManager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// On pass through
|
||||||
|
func (r *RuntimeEvents) On(eventName string, callback func(optionalData ...interface{})) {
|
||||||
|
r.eventManager.On(eventName, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit pass through
|
||||||
|
func (r *RuntimeEvents) Emit(eventName string, optionalData ...interface{}) {
|
||||||
|
r.eventManager.Emit(eventName, optionalData)
|
||||||
|
}
|
12
runtime_log.go
Normal file
12
runtime_log.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
type RuntimeLog struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRuntimeLog() *RuntimeLog {
|
||||||
|
return &RuntimeLog{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuntimeLog) New(prefix string) *CustomLogger {
|
||||||
|
return newCustomLogger(prefix)
|
||||||
|
}
|
32
runtime_window.go
Normal file
32
runtime_window.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
type RuntimeWindow struct {
|
||||||
|
renderer Renderer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRuntimeWindow(renderer Renderer) *RuntimeWindow {
|
||||||
|
return &RuntimeWindow{
|
||||||
|
renderer: renderer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuntimeWindow) SetColour(colour string) error {
|
||||||
|
return r.renderer.SetColour(colour)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuntimeWindow) Fullscreen() {
|
||||||
|
r.renderer.Fullscreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuntimeWindow) UnFullscreen() {
|
||||||
|
r.renderer.UnFullscreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuntimeWindow) SetTitle(title string) {
|
||||||
|
r.renderer.SetTitle(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuntimeWindow) Close() {
|
||||||
|
// TODO: Add shutdown mechanism
|
||||||
|
r.renderer.Close()
|
||||||
|
}
|
12
utils.go
Normal file
12
utils.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package wails
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func escapeJS(js string) (string, error) {
|
||||||
|
result := strings.Replace(js, "\\", "\\\\", -1)
|
||||||
|
result = strings.Replace(result, "'", "\\'", -1)
|
||||||
|
result = strings.Replace(result, "\n", "\\n", -1)
|
||||||
|
return result, nil
|
||||||
|
}
|
372
webview/webview.go
Normal file
372
webview/webview.go
Normal file
@ -0,0 +1,372 @@
|
|||||||
|
// Package wails implements Go bindings to https://github.com/zserge/webview C library.
|
||||||
|
// It is a modified version of webview.go from that repository
|
||||||
|
|
||||||
|
// Bindings closely repeat the C APIs and include both, a simplified
|
||||||
|
// single-function API to just open a full-screen webview window, and a more
|
||||||
|
// advanced and featureful set of APIs, including Go-to-JavaScript bindings.
|
||||||
|
//
|
||||||
|
// The library uses gtk-webkit, Cocoa/Webkit and MSHTML (IE8..11) as a browser
|
||||||
|
// engine and supports Linux, MacOS and Windows 7..10 respectively.
|
||||||
|
//
|
||||||
|
package webview
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo linux openbsd freebsd CFLAGS: -DWEBVIEW_GTK=1
|
||||||
|
#cgo linux openbsd freebsd pkg-config: gtk+-3.0 webkit2gtk-4.0
|
||||||
|
|
||||||
|
#cgo windows CFLAGS: -DWEBVIEW_WINAPI=1
|
||||||
|
#cgo windows LDFLAGS: -lole32 -lcomctl32 -loleaut32 -luuid -lgdi32
|
||||||
|
|
||||||
|
#cgo darwin CFLAGS: -DWEBVIEW_COCOA=1 -x objective-c
|
||||||
|
#cgo darwin LDFLAGS: -framework Cocoa -framework WebKit
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#define WEBVIEW_STATIC
|
||||||
|
#define WEBVIEW_IMPLEMENTATION
|
||||||
|
#include "webview.h"
|
||||||
|
|
||||||
|
extern void _webviewExternalInvokeCallback(void *, void *);
|
||||||
|
|
||||||
|
static inline void CgoWebViewFree(void *w) {
|
||||||
|
free((void *)((struct webview *)w)->title);
|
||||||
|
free((void *)((struct webview *)w)->url);
|
||||||
|
free(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void *CgoWebViewCreate(int width, int height, char *title, char *url, int resizable, int debug) {
|
||||||
|
struct webview *w = (struct webview *) calloc(1, sizeof(*w));
|
||||||
|
w->width = width;
|
||||||
|
w->height = height;
|
||||||
|
w->title = title;
|
||||||
|
w->url = url;
|
||||||
|
w->resizable = resizable;
|
||||||
|
w->debug = debug;
|
||||||
|
w->external_invoke_cb = (webview_external_invoke_cb_t) _webviewExternalInvokeCallback;
|
||||||
|
if (webview_init(w) != 0) {
|
||||||
|
CgoWebViewFree(w);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return (void *)w;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int CgoWebViewLoop(void *w, int blocking) {
|
||||||
|
return webview_loop((struct webview *)w, blocking);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void CgoWebViewTerminate(void *w) {
|
||||||
|
webview_terminate((struct webview *)w);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void CgoWebViewExit(void *w) {
|
||||||
|
webview_exit((struct webview *)w);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void CgoWebViewSetTitle(void *w, char *title) {
|
||||||
|
webview_set_title((struct webview *)w, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void CgoWebViewSetFullscreen(void *w, int fullscreen) {
|
||||||
|
webview_set_fullscreen((struct webview *)w, fullscreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void CgoWebViewSetColor(void *w, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
|
||||||
|
webview_set_color((struct webview *)w, r, g, b, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void CgoDialog(void *w, int dlgtype, int flags,
|
||||||
|
char *title, char *arg, char *res, size_t ressz) {
|
||||||
|
webview_dialog(w, dlgtype, flags,
|
||||||
|
(const char*)title, (const char*) arg, res, ressz);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int CgoWebViewEval(void *w, char *js) {
|
||||||
|
return webview_eval((struct webview *)w, js);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void CgoWebViewInjectCSS(void *w, char *css) {
|
||||||
|
webview_inject_css((struct webview *)w, css);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern void _webviewDispatchGoCallback(void *);
|
||||||
|
static inline void _webview_dispatch_cb(struct webview *w, void *arg) {
|
||||||
|
_webviewDispatchGoCallback(arg);
|
||||||
|
}
|
||||||
|
static inline void CgoWebViewDispatch(void *w, uintptr_t arg) {
|
||||||
|
webview_dispatch((struct webview *)w, _webview_dispatch_cb, (void *)arg);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Ensure that main.main is called from the main thread
|
||||||
|
runtime.LockOSThread()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open is a simplified API to open a single native window with a full-size webview in
|
||||||
|
// it. It can be helpful if you want to communicate with the core app using XHR
|
||||||
|
// or WebSockets (as opposed to using JavaScript bindings).
|
||||||
|
//
|
||||||
|
// Window appearance can be customized using title, width, height and resizable parameters.
|
||||||
|
// URL must be provided and can user either a http or https protocol, or be a
|
||||||
|
// local file:// URL. On some platforms "data:" URLs are also supported
|
||||||
|
// (Linux/MacOS).
|
||||||
|
func Open(title, url string, w, h int, resizable bool) error {
|
||||||
|
titleStr := C.CString(title)
|
||||||
|
defer C.free(unsafe.Pointer(titleStr))
|
||||||
|
urlStr := C.CString(url)
|
||||||
|
defer C.free(unsafe.Pointer(urlStr))
|
||||||
|
resize := C.int(0)
|
||||||
|
if resizable {
|
||||||
|
resize = C.int(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := C.webview(titleStr, urlStr, C.int(w), C.int(h), resize)
|
||||||
|
if r != 0 {
|
||||||
|
return errors.New("failed to create webview")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExternalInvokeCallbackFunc is a function type that is called every time
|
||||||
|
// "window.external.invoke()" is called from JavaScript. Data is the only
|
||||||
|
// obligatory string parameter passed into the "invoke(data)" function from
|
||||||
|
// JavaScript. To pass more complex data serialized JSON or base64 encoded
|
||||||
|
// string can be used.
|
||||||
|
type ExternalInvokeCallbackFunc func(w WebView, data string)
|
||||||
|
|
||||||
|
// Settings is a set of parameters to customize the initial WebView appearance
|
||||||
|
// and behavior. It is passed into the webview.New() constructor.
|
||||||
|
type Settings struct {
|
||||||
|
// WebView main window title
|
||||||
|
Title string
|
||||||
|
// URL to open in a webview
|
||||||
|
URL string
|
||||||
|
// Window width in pixels
|
||||||
|
Width int
|
||||||
|
// Window height in pixels
|
||||||
|
Height int
|
||||||
|
// Allows/disallows window resizing
|
||||||
|
Resizable bool
|
||||||
|
// Enable debugging tools (Linux/BSD/MacOS, on Windows use Firebug)
|
||||||
|
Debug bool
|
||||||
|
// A callback that is executed when JavaScript calls "window.external.invoke()"
|
||||||
|
ExternalInvokeCallback ExternalInvokeCallbackFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebView is an interface that wraps the basic methods for controlling the UI
|
||||||
|
// loop, handling multithreading and providing JavaScript bindings.
|
||||||
|
type WebView interface {
|
||||||
|
// Run() starts the main UI loop until the user closes the webview window or
|
||||||
|
// Terminate() is called.
|
||||||
|
Run()
|
||||||
|
// Loop() runs a single iteration of the main UI.
|
||||||
|
Loop(blocking bool) bool
|
||||||
|
// SetTitle() changes window title. This method must be called from the main
|
||||||
|
// thread only. See Dispatch() for more details.
|
||||||
|
SetTitle(title string)
|
||||||
|
// SetFullscreen() controls window full-screen mode. This method must be
|
||||||
|
// called from the main thread only. See Dispatch() for more details.
|
||||||
|
SetFullscreen(fullscreen bool)
|
||||||
|
// SetColor() changes window background color. This method must be called from
|
||||||
|
// the main thread only. See Dispatch() for more details.
|
||||||
|
SetColor(r, g, b, a uint8)
|
||||||
|
// Eval() evaluates an arbitrary JS code inside the webview. This method must
|
||||||
|
// be called from the main thread only. See Dispatch() for more details.
|
||||||
|
Eval(js string) error
|
||||||
|
// InjectJS() injects an arbitrary block of CSS code using the JS API. This
|
||||||
|
// method must be called from the main thread only. See Dispatch() for more
|
||||||
|
// details.
|
||||||
|
InjectCSS(css string)
|
||||||
|
// Dialog() opens a system dialog of the given type and title. String
|
||||||
|
// argument can be provided for certain dialogs, such as alert boxes. For
|
||||||
|
// alert boxes argument is a message inside the dialog box.
|
||||||
|
Dialog(dlgType DialogType, flags int, title string, arg string) string
|
||||||
|
// Terminate() breaks the main UI loop. This method must be called from the main thread
|
||||||
|
// only. See Dispatch() for more details.
|
||||||
|
Terminate()
|
||||||
|
// Dispatch() schedules some arbitrary function to be executed on the main UI
|
||||||
|
// thread. This may be helpful if you want to run some JavaScript from
|
||||||
|
// background threads/goroutines, or to terminate the app.
|
||||||
|
Dispatch(func())
|
||||||
|
// Exit() closes the window and cleans up the resources. Use Terminate() to
|
||||||
|
// forcefully break out of the main UI loop.
|
||||||
|
Exit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialogType is an enumeration of all supported system dialog types
|
||||||
|
type DialogType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DialogTypeOpen is a system file open dialog
|
||||||
|
DialogTypeOpen DialogType = iota
|
||||||
|
// DialogTypeSave is a system file save dialog
|
||||||
|
DialogTypeSave
|
||||||
|
// DialogTypeAlert is a system alert dialog (message box)
|
||||||
|
DialogTypeAlert
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DialogFlagFile is a normal file picker dialog
|
||||||
|
DialogFlagFile = C.WEBVIEW_DIALOG_FLAG_FILE
|
||||||
|
// DialogFlagDirectory is an open directory dialog
|
||||||
|
DialogFlagDirectory = C.WEBVIEW_DIALOG_FLAG_DIRECTORY
|
||||||
|
// DialogFlagInfo is an info alert dialog
|
||||||
|
DialogFlagInfo = C.WEBVIEW_DIALOG_FLAG_INFO
|
||||||
|
// DialogFlagWarning is a warning alert dialog
|
||||||
|
DialogFlagWarning = C.WEBVIEW_DIALOG_FLAG_WARNING
|
||||||
|
// DialogFlagError is an error dialog
|
||||||
|
DialogFlagError = C.WEBVIEW_DIALOG_FLAG_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
m sync.Mutex
|
||||||
|
index uintptr
|
||||||
|
fns = map[uintptr]func(){}
|
||||||
|
cbs = map[WebView]ExternalInvokeCallbackFunc{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type webview struct {
|
||||||
|
w unsafe.Pointer
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ WebView = &webview{}
|
||||||
|
|
||||||
|
func boolToInt(b bool) int {
|
||||||
|
if b {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWebview creates and opens a new webview window using the given settings. The
|
||||||
|
// returned object implements the WebView interface. This function returns nil
|
||||||
|
// if a window can not be created.
|
||||||
|
func NewWebview(settings Settings) WebView {
|
||||||
|
if settings.Width == 0 {
|
||||||
|
settings.Width = 640
|
||||||
|
}
|
||||||
|
if settings.Height == 0 {
|
||||||
|
settings.Height = 480
|
||||||
|
}
|
||||||
|
if settings.Title == "" {
|
||||||
|
settings.Title = "WebView"
|
||||||
|
}
|
||||||
|
w := &webview{}
|
||||||
|
w.w = C.CgoWebViewCreate(C.int(settings.Width), C.int(settings.Height),
|
||||||
|
C.CString(settings.Title), C.CString(settings.URL),
|
||||||
|
C.int(boolToInt(settings.Resizable)), C.int(boolToInt(settings.Debug)))
|
||||||
|
m.Lock()
|
||||||
|
if settings.ExternalInvokeCallback != nil {
|
||||||
|
cbs[w] = settings.ExternalInvokeCallback
|
||||||
|
} else {
|
||||||
|
cbs[w] = func(w WebView, data string) {}
|
||||||
|
}
|
||||||
|
m.Unlock()
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) Loop(blocking bool) bool {
|
||||||
|
block := C.int(0)
|
||||||
|
if blocking {
|
||||||
|
block = 1
|
||||||
|
}
|
||||||
|
return C.CgoWebViewLoop(w.w, block) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) Run() {
|
||||||
|
for w.Loop(true) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) Exit() {
|
||||||
|
C.CgoWebViewExit(w.w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) Dispatch(f func()) {
|
||||||
|
m.Lock()
|
||||||
|
for ; fns[index] != nil; index++ {
|
||||||
|
}
|
||||||
|
fns[index] = f
|
||||||
|
m.Unlock()
|
||||||
|
C.CgoWebViewDispatch(w.w, C.uintptr_t(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) SetTitle(title string) {
|
||||||
|
p := C.CString(title)
|
||||||
|
defer C.free(unsafe.Pointer(p))
|
||||||
|
C.CgoWebViewSetTitle(w.w, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) SetColor(r, g, b, a uint8) {
|
||||||
|
C.CgoWebViewSetColor(w.w, C.uint8_t(r), C.uint8_t(g), C.uint8_t(b), C.uint8_t(a))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) SetFullscreen(fullscreen bool) {
|
||||||
|
C.CgoWebViewSetFullscreen(w.w, C.int(boolToInt(fullscreen)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) Dialog(dlgType DialogType, flags int, title string, arg string) string {
|
||||||
|
const maxPath = 4096
|
||||||
|
titlePtr := C.CString(title)
|
||||||
|
defer C.free(unsafe.Pointer(titlePtr))
|
||||||
|
argPtr := C.CString(arg)
|
||||||
|
defer C.free(unsafe.Pointer(argPtr))
|
||||||
|
resultPtr := (*C.char)(C.calloc((C.size_t)(unsafe.Sizeof((*C.char)(nil))), (C.size_t)(maxPath)))
|
||||||
|
defer C.free(unsafe.Pointer(resultPtr))
|
||||||
|
C.CgoDialog(w.w, C.int(dlgType), C.int(flags), titlePtr,
|
||||||
|
argPtr, resultPtr, C.size_t(maxPath))
|
||||||
|
return C.GoString(resultPtr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) Eval(js string) error {
|
||||||
|
p := C.CString(js)
|
||||||
|
defer C.free(unsafe.Pointer(p))
|
||||||
|
switch C.CgoWebViewEval(w.w, p) {
|
||||||
|
case -1:
|
||||||
|
return errors.New("evaluation failed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) InjectCSS(css string) {
|
||||||
|
p := C.CString(css)
|
||||||
|
defer C.free(unsafe.Pointer(p))
|
||||||
|
C.CgoWebViewInjectCSS(w.w, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) Terminate() {
|
||||||
|
C.CgoWebViewTerminate(w.w)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export _webviewDispatchGoCallback
|
||||||
|
func _webviewDispatchGoCallback(index unsafe.Pointer) {
|
||||||
|
var f func()
|
||||||
|
m.Lock()
|
||||||
|
f = fns[uintptr(index)]
|
||||||
|
delete(fns, uintptr(index))
|
||||||
|
m.Unlock()
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
|
||||||
|
//export _webviewExternalInvokeCallback
|
||||||
|
func _webviewExternalInvokeCallback(w unsafe.Pointer, data unsafe.Pointer) {
|
||||||
|
m.Lock()
|
||||||
|
var (
|
||||||
|
cb ExternalInvokeCallbackFunc
|
||||||
|
wv WebView
|
||||||
|
)
|
||||||
|
for wv, cb = range cbs {
|
||||||
|
if wv.(*webview).w == w {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.Unlock()
|
||||||
|
cb(wv, C.GoString((*C.char)(data)))
|
||||||
|
}
|
1927
webview/webview.h
Normal file
1927
webview/webview.h
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user