Implement basic binding

This commit is contained in:
Lea Anthony 2023-02-26 20:49:29 +11:00
commit 00c458f948
17 changed files with 582 additions and 257 deletions

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
HELLO!
</body>
</html>

View file

@ -6,10 +6,16 @@ require github.com/wailsapp/wails/v3 v3.0.0-alpha.0
require (
github.com/imdario/mergo v0.3.12 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leaanthony/slicer v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/samber/lo v1.37.0 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
)
replace github.com/wailsapp/wails/v3 => ../../../
replace github.com/wailsapp/wails/v3 => ../..
replace github.com/wailsapp/wails/v2 => ../../../v2

View file

@ -1,14 +1,31 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY=
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw=
github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 h1:Wn+nhnS+VytzE0PegUzSh4T3hXJCtggKGD/4U5H9+wQ=
github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6/go.mod h1:zlNLI0E2c2qA6miiuAHtp0Bac8FaGH0tlhA19OssR/8=
github.com/wailsapp/wails/v3 v3.0.0-alpha.0 h1:T5gqG98Xr8LBf69oxlPkhpsFD59w2SnqUZk6XHj8Zoc=
github.com/wailsapp/wails/v3 v3.0.0-alpha.0/go.mod h1:OAfO5bP0TSUvCIHZYc6Dqfow/9RqxzHvYtmhWPpo1c0=
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 h1:RjggHMcaTVp0LOVZcW0bo8alwHrOaCrGUDgfWUHhnN4=
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@ -20,3 +37,4 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View file

@ -1,20 +1,31 @@
package main
import (
"embed"
_ "embed"
"log"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed assets
var assets embed.FS
func main() {
app := application.New(application.Options{
Bind: []interface{}{
&GreetService{},
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})
app.NewWebviewWindow()
app.NewWebviewWindowWithOptions(&application.WebviewWindowOptions{
Assets: application.AssetOptions{
FS: assets,
},
})
err := app.Run()

View file

@ -1,13 +1,5 @@
package commands
import (
"bytes"
"fmt"
"os"
"github.com/wailsapp/wails/v3/internal/parser"
)
type GenerateBindingsOptions struct {
ModelsFilename string `name:"m" description:"The filename for the models file" default:"models.ts"`
BindingsFilename string `name:"b" description:"The filename for the bindings file" default:"bindings.js"`
@ -16,27 +8,27 @@ type GenerateBindingsOptions struct {
func GenerateBindings(options *GenerateBindingsOptions) error {
parserContext, err := parser.ParseDirectory(options.ProjectDirectory)
if err != nil {
return fmt.Errorf("error parsing project: %v", err)
}
// Generate models
modelsData, err := parser.GenerateModels(parserContext)
if err != nil {
return fmt.Errorf("error generating models: %v", err)
}
var modelsFileData bytes.Buffer
modelsFileData.WriteString(`// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
`)
modelsFileData.Write(modelsData)
err = os.WriteFile(options.ModelsFilename, modelsFileData.Bytes(), 0755)
if err != nil {
return fmt.Errorf("error writing models file: %v", err)
}
println("Generated models file '" + options.ModelsFilename + "'")
// parserContext, err := parser.ParseDirectory(options.ProjectDirectory)
// if err != nil {
// return fmt.Errorf("error parsing project: %v", err)
// }
//
// // Generate models
// modelsData, err := parser.GenerateModels(parserContext)
// if err != nil {
// return fmt.Errorf("error generating models: %v", err)
// }
//
// var modelsFileData bytes.Buffer
// modelsFileData.WriteString(`// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
//// This file is automatically generated. DO NOT EDIT
//
//`)
// modelsFileData.Write(modelsData)
// err = os.WriteFile(options.ModelsFilename, modelsFileData.Bytes(), 0755)
// if err != nil {
// return fmt.Errorf("error writing models file: %v", err)
// }
// println("Generated models file '" + options.ModelsFilename + "'")
return nil
}

View file

@ -0,0 +1,65 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 9 */
import {newRuntimeCaller} from "./runtime";
import { nanoid } from 'nanoid/non-secure';
let call = newRuntimeCaller("call");
let callResponses = new Map();
function generateID() {
let result;
do {
result = nanoid();
} while (callResponses.has(result));
return result;
}
export function callCallback(id, data, isJSON) {
let p = callResponses.get(id);
if (p) {
if (isJSON) {
p.resolve(JSON.parse(data));
} else {
p.resolve(data);
}
callResponses.delete(id);
}
}
export function callErrorCallback(id, message) {
let p = callResponses.get(id);
if (p) {
p.reject(message);
callResponses.delete(id);
}
}
function dialog(type, options) {
return new Promise((resolve, reject) => {
let id = generateID();
options = options || {};
options["call-id"] = id;
callResponses.set(id, {resolve, reject});
call(type, options).catch((error) => {
reject(error);
callResponses.delete(id);
});
});
}
export function Call(options) {
return dialog("Call", options);
}

View file

@ -12,7 +12,7 @@ The electron alternative for Go
import {newRuntimeCaller} from "./runtime";
import { nanoid } from 'nanoid/non-secure'
import { nanoid } from 'nanoid/non-secure';
let call = newRuntimeCaller("dialog");
@ -54,7 +54,7 @@ function dialog(type, options) {
call(type, options).catch((error) => {
reject(error);
dialogResponses.delete(id);
})
});
});
}

View file

@ -14,7 +14,7 @@ import * as Clipboard from './clipboard';
import * as Application from './application';
import * as Log from './log';
import * as Screens from './screens';
import {Call, callErrorCallback, callCallback} from "./calls";
import {newWindow} from "./window";
import {dispatchCustomEvent, Emit, Off, OffAll, On, Once, OnMultiple} from "./events";
import {dialogCallback, dialogErrorCallback, Error, Info, OpenFile, Question, SaveFile, Warning,} from "./dialogs";
@ -30,8 +30,9 @@ window._wails = {
dialogCallback,
dialogErrorCallback,
dispatchCustomEvent,
}
callCallback,
callErrorCallback,
};
export function newRuntime(id) {
return {
@ -43,6 +44,7 @@ export function newRuntime(id) {
},
Log,
Screens,
Call,
WML: {
Reload: reloadWML,
},

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -2,11 +2,19 @@ package application
import (
"fmt"
"github.com/samber/lo"
"reflect"
"runtime"
"strings"
)
type CallOptions struct {
PackageName string `json:"packageName"`
StructName string `json:"structName"`
MethodName string `json:"methodName"`
Args []any `json:"args"`
}
// Parameter defines a Go method parameter
type Parameter struct {
Name string `json:"name,omitempty"`
@ -88,6 +96,22 @@ func (b *Bindings) Add(structPtr interface{}) error {
return nil
}
func (b *Bindings) Get(options *CallOptions) *BoundMethod {
_, ok := b.boundMethods[options.PackageName]
if !ok {
return nil
}
_, ok = b.boundMethods[options.PackageName][options.StructName]
if !ok {
return nil
}
method, ok := b.boundMethods[options.PackageName][options.StructName][options.MethodName]
if !ok {
return nil
}
return method
}
func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) {
// Create result placeholder
@ -106,7 +130,7 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) {
return nil, fmt.Errorf("%s is a function, not a pointer to a struct. Wails v2 has deprecated the binding of functions. Please wrap your functions up in a struct and bind a pointer to that struct.", name)
}
return nil, fmt.Errorf("not a pointer to a struct.")
return nil, fmt.Errorf("not a pointer to a struct")
}
// Process Struct
@ -120,13 +144,14 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) {
methodDef := structType.Method(i)
methodName := methodDef.Name
packageName, structName, _ := strings.Cut(baseName, ".")
fullMethodName := baseName + "." + methodName
method := structValue.MethodByName(methodName)
packagePath, _ := lo.Coalesce(structType.PkgPath(), "main")
// Create new method
boundMethod := &BoundMethod{
Name: fullMethodName,
Name: methodName,
PackageName: packageName,
PackagePath: packagePath,
StructName: structName,
Inputs: nil,
Outputs: nil,
@ -141,34 +166,6 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) {
for inputIndex := 0; inputIndex < inputParamCount; inputIndex++ {
input := methodType.In(inputIndex)
thisParam := newParameter("", input)
//
//thisInput := input
//
//if thisInput.Kind() == reflect.Slice {
// thisInput = thisInput.Elem()
//}
//
//// Process struct pointer params
//if thisInput.Kind() == reflect.Ptr {
// if thisInput.Elem().Kind() == reflect.Struct {
// typ := thisInput.Elem()
// a := reflect.New(typ)
// s := reflect.Indirect(a).Interface()
// name := typ.Name()
// packageName := getPackageName(thisInput.String())
// b.AddStructToGenerateTS(packageName, name, s)
// }
//}
//
//// Process struct params
//if thisInput.Kind() == reflect.Struct {
// a := reflect.New(thisInput)
// s := reflect.Indirect(a).Interface()
// name := thisInput.Name()
// packageName := getPackageName(thisInput.String())
// b.AddStructToGenerateTS(packageName, name, s)
//}
inputs = append(inputs, thisParam)
}
@ -179,33 +176,6 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) {
for outputIndex := 0; outputIndex < outputParamCount; outputIndex++ {
output := methodType.Out(outputIndex)
thisParam := newParameter("", output)
//
//thisOutput := output
//
//if thisOutput.Kind() == reflect.Slice {
// thisOutput = thisOutput.Elem()
//}
//
//// Process struct pointer params
//if thisOutput.Kind() == reflect.Ptr {
// if thisOutput.Elem().Kind() == reflect.Struct {
// typ := thisOutput.Elem()
// a := reflect.New(typ)
// s := reflect.Indirect(a).Interface()
// name := typ.Name()
// packageName := getPackageName(thisOutput.String())
// }
//}
//
//// Process struct params
//if thisOutput.Kind() == reflect.Struct {
// a := reflect.New(thisOutput)
// s := reflect.Indirect(a).Interface()
// name := thisOutput.Name()
// packageName := getPackageName(thisOutput.String())
// b.AddStructToGenerateTS(packageName, name, s)
//}
outputs = append(outputs, thisParam)
}
boundMethod.Outputs = outputs
@ -217,6 +187,56 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) {
return result, nil
}
// Call will attempt to call this bound method with the given args
func (b *BoundMethod) Call(args []interface{}) (interface{}, error) {
// Check inputs
expectedInputLength := len(b.Inputs)
actualInputLength := len(args)
if expectedInputLength != actualInputLength {
return nil, fmt.Errorf("%s takes %d inputs. Received %d", b.Name, expectedInputLength, actualInputLength)
}
/** Convert inputs to reflect values **/
// Create slice for the input arguments to the method call
callArgs := make([]reflect.Value, expectedInputLength)
// Iterate over given arguments
for index, arg := range args {
// Save the converted argument
callArgs[index] = reflect.ValueOf(arg)
}
// Do the call
callResults := b.Method.Call(callArgs)
//** Check results **//
var returnValue interface{}
var err error
switch len(b.Outputs) {
case 1:
// Loop over results and determine if the result
// is an error or not
for _, result := range callResults {
interfac := result.Interface()
temp, ok := interfac.(error)
if ok {
err = temp
} else {
returnValue = interfac
}
}
case 2:
returnValue = callResults[0].Interface()
if temp, ok := callResults[1].Interface().(error); ok {
err = temp
}
}
return returnValue, err
}
// isStructPtr returns true if the value given is a
// pointer to a struct
func isStructPtr(value interface{}) bool {
@ -233,10 +253,3 @@ func isFunction(value interface{}) bool {
func isStruct(value interface{}) bool {
return reflect.ValueOf(value).Kind() == reflect.Struct
}
func getPackageName(in string) string {
result := strings.Split(in, ".")[0]
result = strings.ReplaceAll(result, "[]", "")
result = strings.ReplaceAll(result, "*", "")
return result
}

View file

@ -71,6 +71,8 @@ func (m *MessageProcessor) HandleRuntimeCall(rw http.ResponseWriter, r *http.Req
m.processContextMenuMethod(method, rw, r, targetWindow, params)
case "screens":
m.processScreensMethod(method, rw, r, targetWindow, params)
case "call":
m.processCallMethod(method, rw, r, targetWindow, params)
default:
m.httpError(rw, "Unknown runtime call: %s", object)
}

View file

@ -0,0 +1,65 @@
package application
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
)
func (m *MessageProcessor) callErrorCallback(message string, callID *string, err error) {
errorMsg := fmt.Sprintf(message, err)
m.Error(errorMsg)
msg := "_wails.callErrorCallback('" + *callID + "', " + strconv.Quote(errorMsg) + ");"
m.window.ExecJS(msg)
}
func (m *MessageProcessor) callCallback(callID *string, result string, isJSON bool) {
msg := fmt.Sprintf("_wails.callCallback('%s', %s, %v);", *callID, strconv.Quote(result), isJSON)
m.window.ExecJS(msg)
}
func (m *MessageProcessor) processCallMethod(method string, rw http.ResponseWriter, _ *http.Request, _ *WebviewWindow, params QueryParams) {
args, err := params.Args()
if err != nil {
m.httpError(rw, "Unable to parse arguments: %s", err)
return
}
callID := args.String("call-id")
if callID == nil {
m.Error("call-id is required")
return
}
switch method {
case "Call":
var options CallOptions
err := params.ToStruct(&options)
if err != nil {
m.callErrorCallback("Error parsing call options: %s", callID, err)
return
}
bindings := globalApplication.bindings.Get(&options)
if bindings == nil {
m.callErrorCallback("Error getting binding for method: %s", callID, fmt.Errorf("'%s' not found", options.MethodName))
return
}
go func() {
result, err := bindings.Call(options.Args)
if err != nil {
m.callErrorCallback("Error calling method: %s", callID, err)
return
}
// convert result to json
jsonResult, err := json.Marshal(result)
if err != nil {
m.callErrorCallback("Error converting result to json: %s", callID, err)
return
}
m.callCallback(callID, string(jsonResult), true)
}()
m.ok(rw)
default:
m.httpError(rw, "Unknown dialog method: %s", method)
}
}