mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-02 07:21:32 +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
|
||||
*.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 (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// ProgramHelper - Utility functions around installed applications
|
||||
type ProgramHelper struct{}
|
||||
type ProgramHelper struct {
|
||||
shell *ShellHelper
|
||||
}
|
||||
|
||||
// NewProgramHelper - Creates a new ProgramHelper
|
||||
func NewProgramHelper() *ProgramHelper {
|
||||
return &ProgramHelper{}
|
||||
return &ProgramHelper{
|
||||
shell: NewShellHelper(),
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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"
|
||||
|
||||
// TemplateHelper helps with creating projects
|
||||
type TemplateHelper struct {
|
||||
system *SystemHelper
|
||||
fs *FSHelper
|
||||
@ -25,12 +26,14 @@ type TemplateHelper struct {
|
||||
metadataFilename string
|
||||
}
|
||||
|
||||
// Template defines a single template
|
||||
type Template struct {
|
||||
Name string
|
||||
Dir string
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
|
||||
// NewTemplateHelper creates a new template helper
|
||||
func NewTemplateHelper() *TemplateHelper {
|
||||
result := TemplateHelper{
|
||||
system: NewSystemHelper(),
|
||||
@ -45,6 +48,7 @@ func NewTemplateHelper() *TemplateHelper {
|
||||
return &result
|
||||
}
|
||||
|
||||
// GetTemplateNames returns a map of all available templates
|
||||
func (t *TemplateHelper) GetTemplateNames() (map[string]string, error) {
|
||||
templateDirs, err := t.fs.GetSubdirs(t.templateDir)
|
||||
if err != nil {
|
||||
@ -53,6 +57,8 @@ func (t *TemplateHelper) GetTemplateNames() (map[string]string, error) {
|
||||
return templateDirs, nil
|
||||
}
|
||||
|
||||
// GetTemplateDetails returns a map of Template structs containing details
|
||||
// of the found templates
|
||||
func (t *TemplateHelper) GetTemplateDetails() (map[string]*Template, error) {
|
||||
templateDirs, err := t.fs.GetSubdirs(t.templateDir)
|
||||
if err != nil {
|
||||
@ -81,6 +87,7 @@ func (t *TemplateHelper) GetTemplateDetails() (map[string]*Template, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// LoadMetadata loads the template's 'metadata.json' file
|
||||
func (t *TemplateHelper) LoadMetadata(dir string) (map[string]interface{}, error) {
|
||||
templateFile := filepath.Join(dir, t.metadataFilename)
|
||||
result := make(map[string]interface{})
|
||||
@ -95,6 +102,7 @@ func (t *TemplateHelper) LoadMetadata(dir string) (map[string]interface{}, error
|
||||
return result, err
|
||||
}
|
||||
|
||||
// TemplateExists returns true if the given template name exists
|
||||
func (t *TemplateHelper) TemplateExists(templateName string) (bool, error) {
|
||||
templates, err := t.GetTemplateNames()
|
||||
if err != nil {
|
||||
@ -104,6 +112,8 @@ func (t *TemplateHelper) TemplateExists(templateName string) (bool, error) {
|
||||
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 {
|
||||
|
||||
// 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 (
|
||||
github.com/AlecAivazis/survey v1.7.1
|
||||
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/go-playground/colors v1.2.0
|
||||
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/leaanthony/spinner v0.4.0
|
||||
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/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
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
|
||||
)
|
||||
|
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/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/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/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d/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.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
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/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=
|
||||
@ -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/sessions v1.1.2/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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
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/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/joho/godotenv v1.2.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
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.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
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/onsi/ginkgo v1.6.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.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.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
||||
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/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-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-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
||||
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-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-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-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-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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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-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-20181206074257-70b957f3b65e h1:njOxP/wVblhCLIUhjHXf6X+dzTt5OQ3vMQo9mkOIKIo=
|
||||
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/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