Generate bindings in package. Support dialogs in dev mode.

This commit is contained in:
Lea Anthony
2021-02-09 21:10:06 +11:00
parent e860f3a00e
commit 21a0245985
14 changed files with 256 additions and 17 deletions

View File

@@ -105,7 +105,7 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
}
if !rebuild {
logger.Println("Filename change: %s did not match extension list %s", event.Name, extensions)
logger.Println("Filename change: %s did not match extension list (%s)", event.Name, extensions)
return
}

View File

@@ -22,6 +22,7 @@ require (
github.com/tdewolff/test v1.0.6 // indirect
github.com/xyproto/xpm v1.2.1
golang.org/x/net v0.0.0-20200822124328-c89045814202
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c
golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82
nhooyr.io/websocket v1.8.6

View File

@@ -92,6 +92,8 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@@ -97,6 +97,7 @@ func CreateApp(appoptions *options.App) (*App, error) {
startupCallback: appoptions.Startup,
shutdownCallback: appoptions.Shutdown,
bridge: bridge.NewBridge(myLogger),
menuManager: menuManager,
}
result.options = appoptions
@@ -208,6 +209,9 @@ func (a *App) Run() error {
return err
}
// Generate backend.js
a.bindings.GenerateBackendJS()
err = a.bridge.Run(dispatcher, bindingDump, a.debug)
a.logger.Trace("Bridge.Run() exited")
if err != nil {

View File

@@ -0,0 +1,11 @@
{
"name": "backend",
"version": "1.0.0",
"description": "Package to wrap backend method calls",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

View File

@@ -0,0 +1,150 @@
package binding
import (
"bytes"
_ "embed"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/leaanthony/slicer"
)
const _comment = `
const backend = {
main: {
"xbarApp": {
"GetCategories": () => {
window.backend.main.xbarApp.GetCategories.call(arguments);
},
/**
* @param {string} arg1
*/
"InstallPlugin": (arg1) => {
window.backend.main.xbarApp.InstallPlugin.call(arguments);
},
"GetPlugins": () => {
window.backend.main.xbarApp.GetPlugins.call(arguments);
}
}
}
}
export default backend;`
//go:embed assets/package.json
var packageJSON []byte
func (b *Bindings) GenerateBackendJS() {
store := b.db.store
var output bytes.Buffer
output.WriteString(`// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
const backend = {`)
output.WriteString("\n")
for packageName, packages := range store {
output.WriteString(fmt.Sprintf(" \"%s\": {", packageName))
output.WriteString("\n")
for structName, structs := range packages {
output.WriteString(fmt.Sprintf(" \"%s\": {", structName))
output.WriteString("\n")
for methodName, methodDetails := range structs {
output.WriteString(" /**\n")
output.WriteString(" * " + methodName + "\n")
var args slicer.StringSlicer
for count, input := range methodDetails.Inputs {
arg := fmt.Sprintf("arg%d", count+1)
args.Add(arg)
output.WriteString(fmt.Sprintf(" * @param {%s} %s - Go Type: %s\n", goTypeToJSDocType(input.TypeName), arg, input.TypeName))
}
returnType := "Promise"
returnTypeDetails := ""
if methodDetails.OutputCount() > 0 {
firstType := goTypeToJSDocType(methodDetails.Outputs[0].TypeName)
returnType += "<" + firstType
if methodDetails.OutputCount() == 2 {
secondType := goTypeToJSDocType(methodDetails.Outputs[1].TypeName)
returnType += "|" + secondType
}
returnType += ">"
returnTypeDetails = " - Go Type: " + methodDetails.Outputs[0].TypeName
}
output.WriteString(" * @returns {" + returnType + "} " + returnTypeDetails + "\n")
output.WriteString(" */\n")
argsString := args.Join(", ")
output.WriteString(fmt.Sprintf(" \"%s\": (%s) => {", methodName, argsString))
output.WriteString("\n")
output.WriteString(fmt.Sprintf(" return window.backend.%s.%s.%s(%s);", packageName, structName, methodName, argsString))
output.WriteString("\n")
output.WriteString(fmt.Sprintf(" },"))
output.WriteString("\n")
}
output.WriteString(fmt.Sprintf(" }"))
output.WriteString("\n")
}
output.WriteString(fmt.Sprintf(" }\n"))
output.WriteString("\n")
}
output.WriteString(`};
export default backend;`)
output.WriteString("\n")
dirname, err := fs.RelativeToCwd("frontend/src/backend")
if err != nil {
log.Fatal(err)
}
if !fs.DirExists(dirname) {
err := fs.Mkdir(dirname)
if err != nil {
log.Fatal(err)
}
}
packageJsonFile := filepath.Join(dirname, "package.json")
if !fs.FileExists(packageJsonFile) {
err := os.WriteFile(packageJsonFile, packageJSON, 0755)
if err != nil {
log.Fatal(err)
}
}
filename := filepath.Join(dirname, "index.js")
err = os.WriteFile(filename, output.Bytes(), 0755)
if err != nil {
log.Fatal(err)
}
}
func goTypeToJSDocType(input string) string {
switch true {
case input == "string":
return "string"
case input == "error":
return "Error"
case
strings.HasPrefix(input, "int"),
strings.HasPrefix(input, "uint"),
strings.HasPrefix(input, "float"):
return "number"
case input == "bool":
return "boolean"
case strings.HasPrefix(input, "[]"):
arrayType := goTypeToJSDocType(input[2:])
return "Array.<" + arrayType + ">"
default:
return "any"
}
}

View File

@@ -6,6 +6,8 @@ import (
"net/http"
"sync"
"golang.org/x/sync/semaphore"
"github.com/wailsapp/wails/v2/internal/messagedispatcher"
"github.com/gorilla/websocket"
@@ -25,13 +27,16 @@ type Bridge struct {
ctx context.Context
cancel context.CancelFunc
dialogSemaphore *semaphore.Weighted
}
func NewBridge(myLogger *logger.Logger) *Bridge {
result := &Bridge{
myLogger: myLogger,
upgrader: websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }},
sessions: make(map[string]*session),
myLogger: myLogger,
upgrader: websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }},
sessions: make(map[string]*session),
dialogSemaphore: semaphore.NewWeighted(1),
}
myLogger.SetLogLevel(1)
@@ -80,12 +85,12 @@ func (b *Bridge) wsBridgeHandler(w http.ResponseWriter, r *http.Request) {
func (b *Bridge) startSession(conn *websocket.Conn) {
// Create a new session for this connection
s := newSession(conn, b.bindings, b.dispatcher, b.myLogger, b.ctx)
s := newSession(conn, b.bindings, b.dispatcher, b.myLogger, b.ctx, b.dialogSemaphore)
// Setup the close handler
conn.SetCloseHandler(func(int, string) error {
b.myLogger.Info("Connection dropped [%s].", s.Identifier())
b.dispatcher.RemoveClient(s.client)
b.mu.Lock()
delete(b.sessions, s.Identifier())
b.mu.Unlock()

View File

@@ -1,11 +1,19 @@
package bridge
import (
"fmt"
"os/exec"
"strconv"
"strings"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"golang.org/x/sync/semaphore"
)
type BridgeClient struct {
session *session
session *session
dialogSemaphore *semaphore.Weighted
}
func (b BridgeClient) Quit() {
@@ -31,7 +39,42 @@ func (b BridgeClient) SaveDialog(dialogOptions *dialog.SaveDialog, callbackID st
}
func (b BridgeClient) MessageDialog(dialogOptions *dialog.MessageDialog, callbackID string) {
b.session.log.Info("MessageDialog unsupported in Bridge mode")
// Check there aren't other dialogs going on
if !b.dialogSemaphore.TryAcquire(1) {
return
}
defer b.dialogSemaphore.Release(1)
osa, err := exec.LookPath("osascript")
if err != nil {
b.session.log.Info("MessageDialog unavailable (osascript not found)")
return
}
var btns slicer.StringSlicer
defaultButton := ""
for index, btn := range dialogOptions.Buttons {
btns.Add(strconv.Quote(btn))
if btn == dialogOptions.DefaultButton {
defaultButton = fmt.Sprintf("default button %d", index+1)
}
}
buttons := "{" + btns.Join(",") + "}"
script := fmt.Sprintf("display dialog \"%s\" buttons %s %s with title \"%s\"", dialogOptions.Message, buttons, defaultButton, dialogOptions.Title)
b.session.log.Info("OSASCRIPT: %s", script)
go func() {
out, err := exec.Command(osa, "-e", script).Output()
if err != nil {
b.session.log.Error(err.Error())
return
}
b.session.log.Info(string(out))
buttonPressed := strings.TrimSpace(strings.TrimPrefix(string(out), "button returned:"))
b.session.client.DispatchMessage("DM" + callbackID + "|" + buttonPressed)
}()
}
func (b BridgeClient) WindowSetTitle(title string) {
@@ -114,8 +157,9 @@ func (b BridgeClient) UpdateContextMenu(contextMenuJSON string) {
b.session.log.Info("UpdateContextMenu unsupported in Bridge mode")
}
func newBridgeClient(session *session) *BridgeClient {
func newBridgeClient(session *session, dialogSemaphore *semaphore.Weighted) *BridgeClient {
return &BridgeClient{
session: session,
session: session,
dialogSemaphore: dialogSemaphore,
}
}

View File

@@ -8,6 +8,7 @@ import (
"time"
"github.com/wailsapp/wails/v2/internal/messagedispatcher"
"golang.org/x/sync/semaphore"
"github.com/gorilla/websocket"
"github.com/wailsapp/wails/v2/internal/logger"
@@ -37,7 +38,7 @@ type session struct {
client *messagedispatcher.DispatchClient
}
func newSession(conn *websocket.Conn, bindings string, dispatcher *messagedispatcher.Dispatcher, logger *logger.Logger, ctx context.Context) *session {
func newSession(conn *websocket.Conn, bindings string, dispatcher *messagedispatcher.Dispatcher, logger *logger.Logger, ctx context.Context, dialogSemaphore *semaphore.Weighted) *session {
result := &session{
conn: conn,
bindings: bindings,
@@ -47,7 +48,7 @@ func newSession(conn *websocket.Conn, bindings string, dispatcher *messagedispat
ctx: ctx,
}
result.client = dispatcher.RegisterClient(newBridgeClient(result))
result.client = dispatcher.RegisterClient(newBridgeClient(result, dialogSemaphore))
return result
@@ -95,7 +96,8 @@ func (s *session) start(firstSession bool) {
}
if err != nil {
s.log.Error("Error reading message: %v", err)
continue
err = s.conn.Close()
return
}
message := string(buffer)

View File

@@ -785,7 +785,7 @@ extern void MessageDialog(struct Application *app, char *callbackID, char *type,
buttonPressed = button4;
}
// Construct callback message. Format "DS<callbackID>|<selected button index>"
// Construct callback message. Format "DM<callbackID>|<selected button index>"
const char *callback = concat("DM", callbackID);
const char *header = concat(callback, "|");
const char *responseMessage = concat(header, buttonPressed);

View File

@@ -27,7 +27,7 @@ func dialogMessageParser(message string) (*parsedMessage, error) {
if idx < 0 {
return nil, fmt.Errorf("Invalid dialog response message format: %+v", message)
}
callbackID := message[:idx+1]
callbackID := message[:idx]
payloadData := message[idx+1:]
switch dialogType {

View File

@@ -10,6 +10,7 @@ The lightweight framework for web-like apps
/* jshint esversion: 6 */
function init() {
// Bridge object
window.wailsbridge = {
reconnectOverlay: null,
@@ -32,6 +33,15 @@ function init() {
);
}
};
window.onbeforeunload = function() {
if( window.wails.websocket ) {
window.wails.websocket.onclose = function () { };
window.wails.websocket.close();
window.wails.websocket = null;
}
}
}
function setupIPCBridge() {
@@ -173,6 +183,10 @@ function startBridge() {
addScript(message);
window.wailsbridge.log('Loaded Wails Runtime');
// We need to now send a message to the backend telling it
// we have loaded (System Start)
window.webkit.messageHandlers.external.postMessage("SS");
// Now wails runtime is loaded, wails for the ready event
// and callback to the main app
// window.wails.Events.On('wails:loaded', function () {

View File

@@ -1,6 +1,6 @@
{
"name": "@wails/runtime",
"version": "1.3.7",
"version": "1.3.8",
"description": "Wails V2 Javascript runtime library",
"main": "main.js",
"types": "runtime.d.ts",

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strings"
"sync"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/runtime"
@@ -30,6 +31,9 @@ type Runtime struct {
//ctx
ctx context.Context
// Startup Hook
startupOnce sync.Once
}
// NewRuntime creates a new runtime subsystem
@@ -75,7 +79,9 @@ func (r *Runtime) Start() error {
switch hook {
case "startup":
if r.startupCallback != nil {
go r.startupCallback(r.runtime)
r.startupOnce.Do(func() {
go r.startupCallback(r.runtime)
})
} else {
r.logger.Warning("no startup callback registered!")
}