Made go runtime package public.

Using loglevel store to keep loglevel in sync
This commit is contained in:
Lea Anthony
2020-10-10 13:57:32 +11:00
parent afea1cbb4c
commit ba6538da7c
23 changed files with 125 additions and 44 deletions

View File

@@ -1,36 +0,0 @@
package goruntime
import (
"fmt"
"os/exec"
"runtime"
)
// Browser defines all browser related operations
type Browser interface {
Open(url string) error
}
type browser struct{}
// Open a url / file using the system default application
// Credit: https://gist.github.com/hyg/9c4afcd91fe24316cbf0
func (b *browser) Open(url string) error {
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
return err
}
func newBrowser() *browser {
return &browser{}
}

View File

@@ -1,16 +0,0 @@
package goruntime
import (
"os"
"testing"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/servicebus"
)
func TestBrowserOpen(t *testing.T) {
mylogger := logger.New(os.Stdout)
myServiceBus := servicebus.New(mylogger)
myRuntime := New(myServiceBus)
myRuntime.Browser.Open("http://www.google.com")
}

View File

@@ -1,94 +0,0 @@
package goruntime
import (
"fmt"
"github.com/wailsapp/wails/v2/internal/crypto"
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/pkg/options"
)
// Dialog defines all Dialog related operations
type Dialog interface {
Open(dialogOptions *options.OpenDialog) []string
Save(dialogOptions *options.SaveDialog) string
}
// dialog exposes the Dialog interface
type dialog struct {
bus *servicebus.ServiceBus
}
// newDialogs creates a new Dialogs struct
func newDialog(bus *servicebus.ServiceBus) Dialog {
return &dialog{
bus: bus,
}
}
// processTitleAndFilter return the title and filter from the given params.
// title is the first string, filter is the second
func (r *dialog) processTitleAndFilter(params ...string) (string, string) {
var title, filter string
if len(params) > 0 {
title = params[0]
}
if len(params) > 1 {
filter = params[1]
}
return title, filter
}
// Open prompts the user to select a file
func (r *dialog) Open(dialogOptions *options.OpenDialog) []string {
// Create unique dialog callback
uniqueCallback := crypto.RandomID()
// Subscribe to the respose channel
responseTopic := "dialog:openselected:" + uniqueCallback
dialogResponseChannel, err := r.bus.Subscribe(responseTopic)
if err != nil {
fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error())
}
message := "dialog:select:open:" + uniqueCallback
r.bus.Publish(message, dialogOptions)
// Wait for result
var result *servicebus.Message = <-dialogResponseChannel
// Delete subscription to response topic
r.bus.UnSubscribe(responseTopic)
return result.Data().([]string)
}
// Save prompts the user to select a file
func (r *dialog) Save(dialogOptions *options.SaveDialog) string {
// Create unique dialog callback
uniqueCallback := crypto.RandomID()
// Subscribe to the respose channel
responseTopic := "dialog:saveselected:" + uniqueCallback
dialogResponseChannel, err := r.bus.Subscribe(responseTopic)
if err != nil {
fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error())
}
message := "dialog:select:save:" + uniqueCallback
r.bus.Publish(message, dialogOptions)
// Wait for result
var result *servicebus.Message = <-dialogResponseChannel
// Delete subscription to response topic
r.bus.UnSubscribe(responseTopic)
return result.Data().(string)
}

View File

@@ -1,68 +0,0 @@
package goruntime
import (
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/servicebus"
)
// Events defines all events related operations
type Events interface {
On(eventName string, callback func(optionalData ...interface{}))
Once(eventName string, callback func(optionalData ...interface{}))
OnMultiple(eventName string, callback func(optionalData ...interface{}), maxCallbacks int)
Emit(eventName string, optionalData ...interface{})
}
// event exposes the events interface
type event struct {
bus *servicebus.ServiceBus
}
// newEvents creates a new Events struct
func newEvents(bus *servicebus.ServiceBus) Events {
return &event{
bus: bus,
}
}
// On registers a listener for a particular event
func (r *event) On(eventName string, callback func(optionalData ...interface{})) {
eventMessage := &message.OnEventMessage{
Name: eventName,
Callback: callback,
Counter: -1,
}
r.bus.Publish("event:on", eventMessage)
}
// Once registers a listener for a particular event. After the first callback, the
// listener is deleted.
func (r *event) Once(eventName string, callback func(optionalData ...interface{})) {
eventMessage := &message.OnEventMessage{
Name: eventName,
Callback: callback,
Counter: 1,
}
r.bus.Publish("event:on", eventMessage)
}
// OnMultiple registers a listener for a particular event, for a given maximum amount of callbacks.
// Once the callback has been run `maxCallbacks` times, the listener is deleted.
func (r *event) OnMultiple(eventName string, callback func(optionalData ...interface{}), maxCallbacks int) {
eventMessage := &message.OnEventMessage{
Name: eventName,
Callback: callback,
Counter: maxCallbacks,
}
r.bus.Publish("event:on", eventMessage)
}
// Emit pass through
func (r *event) Emit(eventName string, optionalData ...interface{}) {
eventMessage := &message.EventMessage{
Name: eventName,
Data: optionalData,
}
r.bus.Publish("event:emit:from:g", eventMessage)
}

View File

@@ -1,69 +0,0 @@
package goruntime
import (
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/pkg/logger"
)
// Log defines all Log related operations
type Log interface {
Print(message string)
Trace(message string)
Debug(message string)
Info(message string)
Warning(message string)
Error(message string)
Fatal(message string)
SetLogLevel(level logger.LogLevel)
}
type log struct {
bus *servicebus.ServiceBus
}
// newLog creates a new Log struct
func newLog(bus *servicebus.ServiceBus) Log {
return &log{
bus: bus,
}
}
// Print prints a Print level message
func (r *log) Print(message string) {
r.bus.Publish("log:print", message)
}
// Trace prints a Trace level message
func (r *log) Trace(message string) {
r.bus.Publish("log:trace", message)
}
// Debug prints a Debug level message
func (r *log) Debug(message string) {
r.bus.Publish("log:debug", message)
}
// Info prints a Info level message
func (r *log) Info(message string) {
r.bus.Publish("log:info", message)
}
// Warning prints a Warning level message
func (r *log) Warning(message string) {
r.bus.Publish("log:warning", message)
}
// Error prints a Error level message
func (r *log) Error(message string) {
r.bus.Publish("log:error", message)
}
// Fatal prints a Fatal level message
func (r *log) Fatal(message string) {
r.bus.Publish("log:fatal", message)
}
// Sets the log level
func (r *log) SetLogLevel(level logger.LogLevel) {
r.bus.Publish("log:setlevel", level)
}

View File

@@ -1,32 +0,0 @@
package goruntime
import "github.com/wailsapp/wails/v2/internal/servicebus"
// Runtime is a means for the user to interact with the application at runtime
type Runtime struct {
Browser Browser
Events Events
Window Window
Dialog Dialog
System System
Log Log
bus *servicebus.ServiceBus
}
// New creates a new runtime
func New(serviceBus *servicebus.ServiceBus) *Runtime {
return &Runtime{
Browser: newBrowser(),
Events: newEvents(serviceBus),
Window: newWindow(serviceBus),
Dialog: newDialog(serviceBus),
System: newSystem(serviceBus),
Log: newLog(serviceBus),
bus: serviceBus,
}
}
// Quit the application
func (r *Runtime) Quit() {
r.bus.Publish("quit", "runtime.Quit()")
}

View File

@@ -1,295 +0,0 @@
// Package goruntime contains all the methods and data structures related to the
// runtime library of Wails. This includes both Go and JS runtimes.
package goruntime
import (
"bytes"
"encoding/json"
"fmt"
"os"
"reflect"
"sync"
)
// Options defines the optional data that may be used
// when creating a Store
type Options struct {
// The name of the store
Name string
// The runtime to attach the store to
Runtime *Runtime
// Indicates if notifying Go listeners should be notified of updates
// synchronously (on the current thread) or asynchronously using
// goroutines
NotifySynchronously bool
}
// StoreProvider is a struct that creates Stores
type StoreProvider struct {
runtime *Runtime
}
// NewStoreProvider creates new stores using the provided Runtime reference.
func NewStoreProvider(runtime *Runtime) *StoreProvider {
return &StoreProvider{
runtime: runtime,
}
}
// Store is where we keep named data
type Store struct {
name string
data reflect.Value
dataType reflect.Type
eventPrefix string
callbacks []reflect.Value
runtime *Runtime
notifySynchronously bool
// Lock
mux sync.Mutex
// Error handler
errorHandler func(error)
}
func fatal(err error) {
println(err.Error())
os.Exit(1)
}
// New creates a new store
func (p *StoreProvider) New(name string, defaultValue interface{}) *Store {
dataType := reflect.TypeOf(defaultValue)
result := Store{
name: name,
runtime: p.runtime,
data: reflect.ValueOf(defaultValue),
dataType: dataType,
}
// Setup the sync listener
result.setupListener()
return &result
}
// OnError takes a function that will be called
// whenever an error occurs
func (s *Store) OnError(callback func(error)) {
s.errorHandler = callback
}
// Processes the updates sent by the front end
func (s *Store) processUpdatedData(data string) error {
// Decode incoming data
var rawdata json.RawMessage
d := json.NewDecoder(bytes.NewBufferString(data))
err := d.Decode(&rawdata)
if err != nil {
return err
}
// Create a new instance of our data and unmarshal
// the received value into it
newData := reflect.New(s.dataType).Interface()
err = json.Unmarshal(rawdata, &newData)
if err != nil {
return err
}
// Lock mutex for writing
s.mux.Lock()
// Handle nulls
if newData == nil {
s.data = reflect.Zero(s.dataType)
} else {
// Store the resultant value in the data store
s.data = reflect.ValueOf(newData).Elem()
}
// Unlock mutex
s.mux.Unlock()
return nil
}
// Setup listener for front end changes
func (s *Store) setupListener() {
// Listen for updates from the front end
s.runtime.Events.On("wails:sync:store:updatedbyfrontend:"+s.name, func(data ...interface{}) {
// Process the incoming data
err := s.processUpdatedData(data[0].(string))
if err != nil {
if s.errorHandler != nil {
s.errorHandler(err)
return
}
}
// Notify listeners
s.notify()
})
}
// notify the listeners of the current data state
func (s *Store) notify() {
// Execute callbacks
for _, callback := range s.callbacks {
// Build args
args := []reflect.Value{s.data}
if s.notifySynchronously {
callback.Call(args)
} else {
go callback.Call(args)
}
}
}
// Set will update the data held by the store
// and notify listeners of the change
func (s *Store) Set(data interface{}) error {
inType := reflect.TypeOf(data)
if inType != s.dataType {
return fmt.Errorf("invalid data given in Store.Set(). Expected %s, got %s", s.dataType.String(), inType.String())
}
// Save data
s.mux.Lock()
s.data = reflect.ValueOf(data)
s.mux.Unlock()
// Stringify data
newdata, err := json.Marshal(data)
if err != nil {
if s.errorHandler != nil {
return err
}
}
// Emit event to front end
s.runtime.Events.Emit("wails:sync:store:updatedbybackend:"+s.name, string(newdata))
// Notify subscribers
s.notify()
return nil
}
// callbackCheck ensures the given function to Subscribe() is
// of the correct signature. Absolutely cannot wait for
// generics to land rather than writing this nonsense.
func (s *Store) callbackCheck(callback interface{}) error {
// Get type
callbackType := reflect.TypeOf(callback)
// Check callback is a function
if callbackType.Kind() != reflect.Func {
return fmt.Errorf("invalid value given to store.Subscribe(). Expected 'func(%s)'", s.dataType.String())
}
// Check input param
if callbackType.NumIn() != 1 {
return fmt.Errorf("invalid number of parameters given in callback function. Expected 1")
}
// Check input data type
if callbackType.In(0) != s.dataType {
return fmt.Errorf("invalid type for input parameter given in callback function. Expected %s, got %s", s.dataType.String(), callbackType.In(0))
}
// Check output param
if callbackType.NumOut() != 0 {
return fmt.Errorf("invalid number of return parameters given in callback function. Expected 0")
}
return nil
}
// Subscribe will subscribe to updates to the store by
// providing a callback. Any updates to the store are sent
// to the callback
func (s *Store) Subscribe(callback interface{}) {
err := s.callbackCheck(callback)
if err != nil {
fatal(err)
}
callbackFunc := reflect.ValueOf(callback)
s.callbacks = append(s.callbacks, callbackFunc)
}
// updaterCheck ensures the given function to Update() is
// of the correct signature. Absolutely cannot wait for
// generics to land rather than writing this nonsense.
func (s *Store) updaterCheck(updater interface{}) error {
// Get type
updaterType := reflect.TypeOf(updater)
// Check updater is a function
if updaterType.Kind() != reflect.Func {
return fmt.Errorf("invalid value given to store.Update(). Expected 'func(%s) %s'", s.dataType.String(), s.dataType.String())
}
// Check input param
if updaterType.NumIn() != 1 {
return fmt.Errorf("invalid number of parameters given in updater function. Expected 1")
}
// Check input data type
if updaterType.In(0) != s.dataType {
return fmt.Errorf("invalid type for input parameter given in updater function. Expected %s, got %s", s.dataType.String(), updaterType.In(0))
}
// Check output param
if updaterType.NumOut() != 1 {
return fmt.Errorf("invalid number of return parameters given in updater function. Expected 1")
}
// Check output data type
if updaterType.Out(0) != s.dataType {
return fmt.Errorf("invalid type for return parameter given in updater function. Expected %s, got %s", s.dataType.String(), updaterType.Out(0))
}
return nil
}
// Update takes a function that is passed the current state.
// The result of that function is then set as the new state
// of the store. This will notify listeners of the change
func (s *Store) Update(updater interface{}) {
err := s.updaterCheck(updater)
if err != nil {
fatal(err)
}
// Build args
args := []reflect.Value{s.data}
// Make call
results := reflect.ValueOf(updater).Call(args)
// We will only have 1 result. Set the store to it
s.Set(results[0].Interface())
}

View File

@@ -1,50 +0,0 @@
package goruntime
import (
"fmt"
"github.com/wailsapp/wails/v2/internal/crypto"
"github.com/wailsapp/wails/v2/internal/servicebus"
)
// System defines all System related operations
type System interface {
IsDarkMode() bool
}
// system exposes the System interface
type system struct {
bus *servicebus.ServiceBus
}
// newSystem creates a new System struct
func newSystem(bus *servicebus.ServiceBus) System {
return &system{
bus: bus,
}
}
// On pass through
func (r *system) IsDarkMode() bool {
// Create unique system callback
uniqueCallback := crypto.RandomID()
// Subscribe to the respose channel
responseTopic := "systemresponse:" + uniqueCallback
systemResponseChannel, err := r.bus.Subscribe(responseTopic)
if err != nil {
fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error())
}
message := "system:isdarkmode:" + uniqueCallback
r.bus.Publish(message, nil)
// Wait for result
var result *servicebus.Message = <-systemResponseChannel
// Delete subscription to response topic
r.bus.UnSubscribe(responseTopic)
return result.Data().(bool)
}

View File

@@ -1,102 +0,0 @@
package goruntime
import (
"github.com/wailsapp/wails/v2/internal/servicebus"
)
// Window defines all Window related operations
type Window interface {
Close()
Show()
Hide()
Maximise()
Unmaximise()
Minimise()
Unminimise()
SetTitle(title string)
Fullscreen()
UnFullscreen()
SetColour(colour int)
}
// Window exposes the Windows interface
type window struct {
bus *servicebus.ServiceBus
}
// newWindow creates a new window struct
func newWindow(bus *servicebus.ServiceBus) Window {
return &window{
bus: bus,
}
}
// Close the Window
// DISCUSSION:
// Should we even be doing this now we have a server build?
// Runtime.Quit() makes more sense than closing a window...
func (w *window) Close() {
w.bus.Publish("quit", "runtime.Close()")
}
// SetTitle sets the title of the window
func (w *window) SetTitle(title string) {
w.bus.Publish("window:settitle", title)
}
// Fullscreen makes the window fullscreen
func (w *window) Fullscreen() {
w.bus.Publish("window:fullscreen", "")
}
// UnFullscreen makes the window UnFullscreen
func (w *window) UnFullscreen() {
w.bus.Publish("window:unfullscreen", "")
}
// SetColour sets the window colour to the given int
func (w *window) SetColour(colour int) {
w.bus.Publish("window:setcolour", colour)
}
// Show shows the window if hidden
func (w *window) Show() {
w.bus.Publish("window:show", "")
}
// Hide the window
func (w *window) Hide() {
w.bus.Publish("window:hide", "")
}
// SetSize sets the size of the window
func (w *window) SetSize(width int, height int) {
size := []int{width, height}
w.bus.Publish("window:setsize", size)
}
// SetPosition sets the position of the window
func (w *window) SetPosition(x int, y int) {
position := []int{x, y}
w.bus.Publish("window:position", position)
}
// Maximise the window
func (w *window) Maximise() {
w.bus.Publish("window:maximise", "")
}
// Unmaximise the window
func (w *window) Unmaximise() {
w.bus.Publish("window:unmaximise", "")
}
// Minimise the window
func (w *window) Minimise() {
w.bus.Publish("window:minimise", "")
}
// Unminimise the window
func (w *window) Unminimise() {
w.bus.Publish("window:unminimise", "")
}

View File

@@ -49,6 +49,7 @@ export function Init() {
// Setup system
window.wails.System.IsDarkMode = Store.New('isdarkmode');
window.wails.System.LogLevel = Store.New('loglevel');
// Do platform specific Init
Platform.Init();