Merge commit '25a157e6614739fbd4df1da3d11d46afe72ae9a8' as 'v2'

This commit is contained in:
Travis McLane
2020-09-01 19:34:51 -05:00
208 changed files with 23636 additions and 0 deletions

40
v2/NOTES.md Normal file
View File

@@ -0,0 +1,40 @@
# Packing linux
* create app, app.desktop, app.png (512x512)
* chmod +x app!
* ./linuxdeploy-x86_64.AppImage --appdir AppDir -i react.png -d react.desktop -e react --output appimage
# Wails Doctor
Tested on:
* Debian 8
* Ubuntu 20.04
* Ubuntu 19.10
* Solus 4.1
* Centos 8
* Gentoo
* OpenSUSE/leap
* Fedora 31
### Development
Add a new package manager processor here: `v2/internal/system/packagemanager/`. IsAvailable should work even if the package is installed.
Add your new package manager to the list of package managers in `v2/internal/system/packagemanager/packagemanager.go`:
```
var db = map[string]PackageManager{
"eopkg": NewEopkg(),
"apt": NewApt(),
"yum": NewYum(),
"pacman": NewPacman(),
"emerge": NewEmerge(),
"zypper": NewZypper(),
}
```
## Gentoo
* Setup docker image using: emerge-webrsync -x -v

View File

@@ -0,0 +1,117 @@
package build
import (
"fmt"
"os"
"runtime"
"strings"
"time"
"github.com/leaanthony/clir"
"github.com/leaanthony/slicer"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/pkg/commands/build"
)
// AddBuildSubcommand adds the `build` command for the Wails application
func AddBuildSubcommand(app *clir.Cli) {
outputType := "desktop"
validTargetTypes := slicer.String([]string{"desktop", "hybrid", "server"})
command := app.NewSubCommand("build", "Builds the application")
// Setup target type flag
description := "Type of application to build. Valid types: " + validTargetTypes.Join(",")
command.StringFlag("t", description, &outputType)
// Setup production flag
production := false
command.BoolFlag("production", "Build in production mode", &production)
// Setup pack flag
pack := false
command.BoolFlag("pack", "Create a platform specific package", &pack)
compilerCommand := "go"
command.StringFlag("compiler", "Use a different go compiler to build, eg go1.15beta1", &compilerCommand)
// Setup Platform flag
platform := runtime.GOOS
command.StringFlag("platform", "Platform to target", &platform)
// Quiet Build
quiet := false
command.BoolFlag("q", "Supress output to console", &quiet)
// ldflags to pass to `go`
ldflags := ""
command.StringFlag("ldflags", "optional ldflags", &ldflags)
// Log to file
logFile := ""
command.StringFlag("l", "Log to file", &logFile)
command.Action(func() error {
// Create logger
logger := logger.New()
if !quiet {
logger.AddOutput(os.Stdout)
}
// Validate output type
if !validTargetTypes.Contains(outputType) {
logger.Fatal(fmt.Sprintf("Output type '%s' is not valid.", outputType))
os.Exit(1)
}
if !quiet {
app.PrintBanner()
}
task := fmt.Sprintf("Building %s Application", strings.Title(outputType))
logger.Writeln(task)
logger.Writeln(strings.Repeat("-", len(task)))
// Setup mode
mode := build.Debug
if production {
mode = build.Production
}
// Create BuildOptions
buildOptions := &build.Options{
Logger: logger,
OutputType: outputType,
Mode: mode,
Pack: pack,
Platform: platform,
LDFlags: ldflags,
Compiler: compilerCommand,
}
return doBuild(buildOptions)
})
}
// doBuild is our main build command
func doBuild(buildOptions *build.Options) error {
// Start Time
start := time.Now()
outputFilename, err := build.Build(buildOptions)
if err != nil {
return err
}
// Output stats
elapsed := time.Since(start)
buildOptions.Logger.Writeln("")
buildOptions.Logger.Writeln(fmt.Sprintf("Built '%s' in %s.", outputFilename, elapsed.Round(time.Millisecond).String()))
buildOptions.Logger.Writeln("")
return nil
}

View File

@@ -0,0 +1,154 @@
package doctor
import (
"fmt"
"os"
"runtime"
"strings"
"text/tabwriter"
"github.com/leaanthony/clir"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/system"
"github.com/leaanthony/wailsv2/v2/internal/system/packagemanager"
)
// AddSubcommand adds the `doctor` command for the Wails application
func AddSubcommand(app *clir.Cli) error {
command := app.NewSubCommand("doctor", "Diagnose your environment")
command.Action(func() error {
// Create logger
logger := logger.New()
logger.AddOutput(os.Stdout)
app.PrintBanner()
print("Scanning system - please wait...")
// Get system info
info, err := system.GetInfo()
if err != nil {
return err
}
println("Done.")
// Start a new tabwriter
w := new(tabwriter.Writer)
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
// Write out the system information
fmt.Fprintf(w, "\n")
fmt.Fprintf(w, "System\n")
fmt.Fprintf(w, "------\n")
fmt.Fprintf(w, "%s\t%s\n", "OS:", info.OS.Name)
fmt.Fprintf(w, "%s\t%s\n", "Version: ", info.OS.Version)
fmt.Fprintf(w, "%s\t%s\n", "ID:", info.OS.ID)
// Exit early if PM not found
if info.PM == nil {
fmt.Fprintf(w, "\n%s\t%s", "Package Manager:", "Not Found")
w.Flush()
println()
return nil
}
fmt.Fprintf(w, "%s\t%s\n", "Package Manager: ", info.PM.Name())
// Output Go Information
fmt.Fprintf(w, "%s\t%s\n", "Go Version:", runtime.Version())
fmt.Fprintf(w, "%s\t%s\n", "Platform:", runtime.GOOS)
fmt.Fprintf(w, "%s\t%s\n", "Architecture:", runtime.GOARCH)
// Output Dependencies Status
var dependenciesMissing = []string{}
var externalPackages = []*packagemanager.Dependancy{}
var dependenciesAvailableRequired = 0
var dependenciesAvailableOptional = 0
fmt.Fprintf(w, "\n")
fmt.Fprintf(w, "Dependency\tPackage Name\tStatus\tVersion\n")
fmt.Fprintf(w, "----------\t------------\t------\t-------\n")
// Loop over dependencies
for _, dependency := range info.Dependencies {
name := dependency.Name
if dependency.Optional {
name += "*"
}
packageName := "Unknown"
status := "Not Found"
// If we found the package
if dependency.PackageName != "" {
packageName = dependency.PackageName
// If it's installed, update the status
if dependency.Installed {
status = "Installed"
} else {
// Generate meaningful status text
status = "Available"
if dependency.Optional {
dependenciesAvailableOptional++
} else {
dependenciesAvailableRequired++
}
}
} else {
if !dependency.Optional {
dependenciesMissing = append(dependenciesMissing, dependency.Name)
}
if dependency.External {
externalPackages = append(externalPackages, dependency)
}
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, packageName, status, dependency.Version)
}
fmt.Fprintf(w, "\n")
fmt.Fprintf(w, "* - Optional Dependency\n")
w.Flush()
println()
println("Diagnosis")
println("---------\n")
// Generate an appropriate diagnosis
if len(dependenciesMissing) == 0 && dependenciesAvailableRequired == 0 {
println("Your system is ready for Wails development!")
}
if dependenciesAvailableRequired != 0 {
println("Install required packages using: " + info.Dependencies.InstallAllRequiredCommand())
}
if dependenciesAvailableOptional != 0 {
println("Install optional packages using: " + info.Dependencies.InstallAllOptionalCommand())
}
if len(externalPackages) > 0 {
for _, p := range externalPackages {
if p.Optional {
print("[Optional] ")
}
println("Install " + p.Name + ": " + p.InstallCommand)
}
}
if len(dependenciesMissing) != 0 {
// TODO: Check if apps are available locally and if so, adjust the diagnosis
println("Fatal:")
println("Required dependencies missing: " + strings.Join(dependenciesMissing, " "))
println("Please read this article on how to resolve this: https://wails.app/guides/resolving-missing-packages")
}
println()
return nil
})
return nil
}

View File

@@ -0,0 +1,120 @@
package initialise
import (
"fmt"
"os"
"strings"
"time"
"github.com/leaanthony/clir"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/templates"
)
// AddSubcommand adds the `init` command for the Wails application
func AddSubcommand(app *clir.Cli) error {
// Load the template shortnames
validShortNames, err := templates.TemplateShortNames()
if err != nil {
return err
}
command := app.NewSubCommand("init", "Initialise a new Wails project")
// Setup template name flag
templateName := "vanilla"
description := "Name of template to use. Valid tempates: " + validShortNames.Join(" ")
command.StringFlag("t", description, &templateName)
// Setup project name
projectName := ""
command.StringFlag("n", "Name of project", &projectName)
// Setup project directory
projectDirectory := "."
command.StringFlag("d", "Project directory", &projectDirectory)
// Quiet Init
quiet := false
command.BoolFlag("q", "Supress output to console", &quiet)
// List templates
list := false
command.BoolFlag("l", "List templates", &list)
command.Action(func() error {
// Create logger
logger := logger.New()
if !quiet {
logger.AddOutput(os.Stdout)
}
// Are we listing templates?
if list {
app.PrintBanner()
err := templates.OutputList(logger)
logger.Writeln("")
return err
}
// Validate output type
if !validShortNames.Contains(templateName) {
logger.Write(fmt.Sprintf("ERROR: Template '%s' is not valid", templateName))
logger.Writeln("")
command.PrintHelp()
return nil
}
// Validate name
if len(projectName) == 0 {
logger.Writeln("ERROR: Project name required")
logger.Writeln("")
command.PrintHelp()
return nil
}
if !quiet {
app.PrintBanner()
}
task := fmt.Sprintf("Initialising Project %s", strings.Title(projectName))
logger.Writeln(task)
logger.Writeln(strings.Repeat("-", len(task)))
// Create Template Options
options := &templates.Options{
ProjectName: projectName,
TargetDir: projectDirectory,
TemplateName: templateName,
Logger: logger,
}
return initProject(options)
})
return nil
}
// initProject is our main init command
func initProject(options *templates.Options) error {
// Start Time
start := time.Now()
// Install the template
err := templates.Install(options)
if err != nil {
return err
}
// Output stats
elapsed := time.Since(start)
options.Logger.Writeln("")
options.Logger.Writeln(fmt.Sprintf("Initialised project '%s' in %s.", options.ProjectName, elapsed.Round(time.Millisecond).String()))
options.Logger.Writeln("")
return nil
}

38
v2/cmd/wails/main.go Normal file
View File

@@ -0,0 +1,38 @@
package main
import (
"os"
"github.com/leaanthony/clir"
"github.com/leaanthony/wailsv2/v2/cmd/wails/internal/commands/build"
"github.com/leaanthony/wailsv2/v2/cmd/wails/internal/commands/doctor"
"github.com/leaanthony/wailsv2/v2/cmd/wails/internal/commands/initialise"
)
func fatal(message string) {
println(message)
os.Exit(1)
}
func main() {
var err error
version := "v2.0.0-alpha"
app := clir.NewCli("Wails", "Go/HTML Application Framework", version)
build.AddBuildSubcommand(app)
err = initialise.AddSubcommand(app)
if err != nil {
fatal(err.Error())
}
err = doctor.AddSubcommand(app)
if err != nil {
fatal(err.Error())
}
err = app.Run()
if err != nil {
println("\n\nERROR: " + err.Error())
}
}

14
v2/go.mod Normal file
View File

@@ -0,0 +1,14 @@
module github.com/leaanthony/wailsv2/v2
go 1.13
require (
github.com/leaanthony/clir v1.0.2
github.com/leaanthony/gosod v0.0.3
github.com/leaanthony/slicer v1.4.1
github.com/matryer/is v1.3.0
github.com/olekukonko/tablewriter v0.0.4
github.com/xyproto/xpm v1.2.1
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
nhooyr.io/websocket v1.7.4
)

57
v2/go.sum Normal file
View File

@@ -0,0 +1,57 @@
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/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leaanthony/clir v1.0.2 h1:Ham9Ep7NH95il0x/ci5s2ExJa/K4B2dE8L2uaSj2cxo=
github.com/leaanthony/clir v1.0.2/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0=
github.com/leaanthony/gosod v0.0.3 h1:VlPilye0zoH4I0WihShyoiTHyiAN1v6dcBW4mMvGJao=
github.com/leaanthony/gosod v0.0.3/go.mod h1:nGMCb1PJfXwBDbOAike78jEYlpqge+xUKFf0iBKjKxU=
github.com/leaanthony/slicer v1.4.1 h1:X/SmRIDhkUAolP79mSTO0jTcVX1k504PJBqvV6TwP0w=
github.com/leaanthony/slicer v1.4.1/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
github.com/matryer/is v1.3.0 h1:9qiso3jaJrOe6qBRJRBt2Ldht05qDiFP9le0JOIhRSI=
github.com/matryer/is v1.3.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/xyproto/xpm v1.2.1 h1:trdvGjjWBsOOKzBBUPT6JvaIQM3acJEEYfbxN7M96wg=
github.com/xyproto/xpm v1.2.1/go.mod h1:cMnesLsD0PBXLgjDfTDEaKr8XyTFsnP1QycSqRw7BiY=
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
nhooyr.io/websocket v1.7.4 h1:w/LGB2sZT0RV8lZYR7nfyaYz4PUbYZ5oF7NBon2M0NY=
nhooyr.io/websocket v1.7.4/go.mod h1:PxYxCwFdFYQ0yRvtQz3s/dC+VEm7CSuC/4b9t8MQQxw=

View File

@@ -0,0 +1,41 @@
// +build !desktop,!hybrid,!server
package app
// This is the default application that will get run if the user compiles using `go build`.
// The reason we want to prevent that is that the `wails build` command does a lot of behind
// the scenes work such as asset compilation. If we allow `go build`, the state of these assets
// will be unknown and the application will not work as expected.
import (
"os"
"github.com/leaanthony/wailsv2/v2/internal/features"
)
// App defines a Wails application structure
type App struct {
Title string
Width int
Height int
Resizable bool
Features *features.Features
}
// CreateApp returns a null application
func CreateApp(options *Options) *App {
return &App{}
}
// Run the application
func (a *App) Run() error {
println(`FATAL: This application was built using "go build". This is unsupported. Please compile using "wails build".`)
os.Exit(1)
return nil
}
// Bind the dummy interface
func (a *App) Bind(dummy interface{}) error {
return nil
}

164
v2/internal/app/desktop.go Normal file
View File

@@ -0,0 +1,164 @@
// +build desktop,!server
package app
import (
"os"
"github.com/leaanthony/wailsv2/v2/internal/binding"
"github.com/leaanthony/wailsv2/v2/internal/features"
"github.com/leaanthony/wailsv2/v2/internal/ffenestri"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/messagedispatcher"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
"github.com/leaanthony/wailsv2/v2/internal/signal"
"github.com/leaanthony/wailsv2/v2/internal/subsystem"
)
// App defines a Wails application structure
type App struct {
window *ffenestri.Application
servicebus *servicebus.ServiceBus
logger *logger.Logger
signal *signal.Manager
// Subsystems
log *subsystem.Log
runtime *subsystem.Runtime
event *subsystem.Event
binding *subsystem.Binding
call *subsystem.Call
dispatcher *messagedispatcher.Dispatcher
// This is our binding DB
bindings *binding.Bindings
// Feature flags
Features *features.Features
}
// Create App
func CreateApp(options *Options) *App {
// Merge default options
options.mergeDefaults()
// Set up logger
myLogger := logger.New(os.Stdout)
myLogger.SetLogLevel(logger.TRACE)
window := ffenestri.NewApplicationWithConfig(&ffenestri.Config{
Title: options.Title,
Width: options.Width,
Height: options.Height,
MinWidth: options.MinWidth,
MinHeight: options.MinHeight,
MaxWidth: options.MaxWidth,
MaxHeight: options.MaxHeight,
Frameless: options.Frameless,
// This should be controlled by the compile time flags...
DevTools: true,
Resizable: !options.DisableResize,
Fullscreen: options.Fullscreen,
}, myLogger)
result := &App{
window: window,
servicebus: servicebus.New(myLogger),
logger: myLogger,
bindings: binding.NewBindings(myLogger),
Features: features.New(),
}
return result
}
// Run the application
func (a *App) Run() error {
// Setup signal handler
signal, err := signal.NewManager(a.servicebus, a.logger)
if err != nil {
return err
}
a.signal = signal
a.signal.Start()
// Start the service bus
a.servicebus.Debug()
a.servicebus.Start()
// Start the runtime
runtime, err := subsystem.NewRuntime(a.servicebus, a.logger)
if err != nil {
return err
}
a.runtime = runtime
a.runtime.Start()
// Start the binding subsystem
binding, err := subsystem.NewBinding(a.servicebus, a.logger, a.bindings, a.runtime.GoRuntime())
if err != nil {
return err
}
a.binding = binding
a.binding.Start()
// Start the logging subsystem
log, err := subsystem.NewLog(a.servicebus, a.logger)
if err != nil {
return err
}
a.log = log
a.log.Start()
// create the dispatcher
dispatcher, err := messagedispatcher.New(a.servicebus, a.logger)
if err != nil {
return err
}
a.dispatcher = dispatcher
dispatcher.Start()
// Start the eventing subsystem
event, err := subsystem.NewEvent(a.servicebus, a.logger)
if err != nil {
return err
}
a.event = event
a.event.Start()
// Start the call subsystem
call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB())
if err != nil {
return err
}
a.call = call
a.call.Start()
// Dump bindings as a debug
bindingDump, err := a.bindings.ToJSON()
if err != nil {
return err
}
result := a.window.Run(dispatcher, bindingDump, a.Features)
a.logger.Trace("Ffenestri.Run() exited")
a.servicebus.Stop()
return result
}
// Bind a struct to the application by passing in
// a pointer to it
func (a *App) Bind(structPtr interface{}) {
// Add the struct to the bindings
err := a.bindings.Add(structPtr)
if err != nil {
a.logger.Fatal("Error during binding: " + err.Error())
}
}

206
v2/internal/app/hybrid.go Normal file
View File

@@ -0,0 +1,206 @@
// +build !server,!desktop,hybrid
package app
import (
"os"
"path/filepath"
"github.com/leaanthony/clir"
"github.com/leaanthony/wailsv2/v2/internal/binding"
"github.com/leaanthony/wailsv2/v2/internal/features"
"github.com/leaanthony/wailsv2/v2/internal/ffenestri"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/messagedispatcher"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
"github.com/leaanthony/wailsv2/v2/internal/subsystem"
"github.com/leaanthony/wailsv2/v2/internal/webserver"
)
// Config defines the Application's configuration
type Config struct {
Title string // Title is the value to be displayed in the title bar
Width int // Width is the desired window width
Height int // Height is the desired window height
DevTools bool // DevTools enables or disables the browser development tools
Resizable bool // Resizable when False prevents window resizing
ServerEnabled bool // ServerEnabled when True allows remote connections
}
// App defines a Wails application structure
type App struct {
config Config
window *ffenestri.Application
webserver *webserver.WebServer
binding *subsystem.Binding
call *subsystem.Call
event *subsystem.Event
log *subsystem.Log
runtime *subsystem.Runtime
bindings *binding.Bindings
logger *logger.Logger
dispatcher *messagedispatcher.Dispatcher
servicebus *servicebus.ServiceBus
// Feature flags
Features *features.Features
}
// Create App
func CreateApp(options *Options) *App {
// Merge default options
options.mergeDefaults()
// Set up logger
myLogger := logger.New(os.Stdout)
myLogger.SetLogLevel(logger.INFO)
window := ffenestri.NewApplicationWithConfig(&ffenestri.Config{
Title: options.Title,
Width: options.Width,
Height: options.Height,
MinWidth: options.MinWidth,
MinHeight: options.MinHeight,
MaxWidth: options.MaxWidth,
MaxHeight: options.MaxHeight,
Frameless: options.Frameless,
// This should be controlled by the compile time flags...
DevTools: true,
Resizable: !options.DisableResize,
Fullscreen: options.Fullscreen,
}, myLogger)
app := &App{
window: window,
webserver: webserver.NewWebServer(myLogger),
servicebus: servicebus.New(myLogger),
logger: myLogger,
bindings: binding.NewBindings(myLogger),
}
return app
}
// Run the application
func (a *App) Run() error {
// Default app options
var port = 8080
var ip = "localhost"
var suppressLogging = false
// Create CLI
cli := clir.NewCli(filepath.Base(os.Args[0]), "Desktop/Server Build", "")
// Setup flags
cli.IntFlag("p", "Port to serve on", &port)
cli.StringFlag("i", "IP to serve on", &ip)
cli.BoolFlag("q", "Suppress logging", &suppressLogging)
// Setup main action
cli.Action(func() error {
// Set IP + Port
a.webserver.SetPort(port)
a.webserver.SetIP(ip)
a.webserver.SetBindings(a.bindings)
// Log information (if we aren't suppressing it)
if !suppressLogging {
cli.PrintBanner()
a.logger.Info("Running server at %s", a.webserver.URL())
}
a.servicebus.Start()
log, err := subsystem.NewLog(a.servicebus, a.logger)
if err != nil {
return err
}
a.log = log
a.log.Start()
dispatcher, err := messagedispatcher.New(a.servicebus, a.logger)
if err != nil {
return err
}
a.dispatcher = dispatcher
a.dispatcher.Start()
// Start the runtime
runtime, err := subsystem.NewRuntime(a.servicebus, a.logger)
if err != nil {
return err
}
a.runtime = runtime
a.runtime.Start()
// Start the binding subsystem
binding, err := subsystem.NewBinding(a.servicebus, a.logger, a.bindings, runtime.GoRuntime())
if err != nil {
return err
}
a.binding = binding
a.binding.Start()
// Start the eventing subsystem
event, err := subsystem.NewEvent(a.servicebus, a.logger)
if err != nil {
return err
}
a.event = event
a.event.Start()
// Start the call subsystem
call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB())
if err != nil {
return err
}
a.call = call
a.call.Start()
// Required so that the WailsInit functions are fired!
runtime.GoRuntime().Events.Emit("wails:loaded")
// Set IP + Port
a.webserver.SetPort(port)
a.webserver.SetIP(ip)
// Log information (if we aren't suppressing it)
if !suppressLogging {
cli.PrintBanner()
println("Running server at " + a.webserver.URL())
}
// Dump bindings as a debug
bindingDump, err := a.bindings.ToJSON()
if err != nil {
return err
}
go func() {
if err := a.webserver.Start(dispatcher, event); err != nil {
a.logger.Error("Webserver failed to start %s", err)
}
}()
result := a.window.Run(dispatcher, bindingDump, a.Features)
a.servicebus.Stop()
return result
})
return cli.Run()
}
// Bind a struct to the application by passing in
// a pointer to it
func (a *App) Bind(structPtr interface{}) {
// Add the struct to the bindings
err := a.bindings.Add(structPtr)
if err != nil {
a.logger.Fatal("Error during binding: " + err.Error())
}
}

View File

@@ -0,0 +1,34 @@
package app
// Options for creating the App
type Options struct {
Title string
Width int
Height int
DisableResize bool
Fullscreen bool
Frameless bool
MinWidth int
MinHeight int
MaxWidth int
MaxHeight int
}
// mergeDefaults will set the minimum default values for an application
func (o *Options) mergeDefaults() {
// Create a default title
if len(o.Title) == 0 {
o.Title = "My Wails App"
}
// Default width
if o.Width == 0 {
o.Width = 1024
}
// Default height
if o.Height == 0 {
o.Height = 768
}
}

155
v2/internal/app/server.go Normal file
View File

@@ -0,0 +1,155 @@
// +build server,!desktop
package app
import (
"os"
"path/filepath"
"github.com/leaanthony/clir"
"github.com/leaanthony/wailsv2/v2/internal/binding"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/messagedispatcher"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
"github.com/leaanthony/wailsv2/v2/internal/subsystem"
"github.com/leaanthony/wailsv2/v2/internal/webserver"
)
// App defines a Wails application structure
type App struct {
binding *subsystem.Binding
call *subsystem.Call
event *subsystem.Event
log *subsystem.Log
runtime *subsystem.Runtime
bindings *binding.Bindings
logger *logger.Logger
dispatcher *messagedispatcher.Dispatcher
servicebus *servicebus.ServiceBus
webserver *webserver.WebServer
}
// Create App
func CreateApp(options *Options) *App {
options.mergeDefaults()
// We ignore the inputs (for now)
// TODO: Allow logger output override on CLI
myLogger := logger.New(os.Stdout)
myLogger.SetLogLevel(logger.TRACE)
result := &App{
bindings: binding.NewBindings(myLogger),
logger: myLogger,
servicebus: servicebus.New(myLogger),
webserver: webserver.NewWebServer(myLogger),
}
return result
}
// Run the application
func (a *App) Run() error {
// Default app options
var port = 8080
var ip = "localhost"
var supressLogging = false
var debugMode = false
// Create CLI
cli := clir.NewCli(filepath.Base(os.Args[0]), "Server Build", "")
// Setup flags
cli.IntFlag("p", "Port to serve on", &port)
cli.StringFlag("i", "IP to serve on", &ip)
cli.BoolFlag("d", "Debug mode", &debugMode)
cli.BoolFlag("q", "Supress logging", &supressLogging)
// Setup main action
cli.Action(func() error {
// Set IP + Port
a.webserver.SetPort(port)
a.webserver.SetIP(ip)
a.webserver.SetBindings(a.bindings)
// Log information (if we aren't supressing it)
if !supressLogging {
cli.PrintBanner()
a.logger.Info("Running server at %s", a.webserver.URL())
}
if debugMode {
a.servicebus.Debug()
}
a.servicebus.Start()
log, err := subsystem.NewLog(a.servicebus, a.logger)
if err != nil {
return err
}
a.log = log
a.log.Start()
dispatcher, err := messagedispatcher.New(a.servicebus, a.logger)
if err != nil {
return err
}
a.dispatcher = dispatcher
a.dispatcher.Start()
// Start the runtime
runtime, err := subsystem.NewRuntime(a.servicebus, a.logger)
if err != nil {
return err
}
a.runtime = runtime
a.runtime.Start()
// Start the binding subsystem
binding, err := subsystem.NewBinding(a.servicebus, a.logger, a.bindings, runtime.GoRuntime())
if err != nil {
return err
}
a.binding = binding
a.binding.Start()
// Start the eventing subsystem
event, err := subsystem.NewEvent(a.servicebus, a.logger)
if err != nil {
return err
}
a.event = event
a.event.Start()
// Start the call subsystem
call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB())
if err != nil {
return err
}
a.call = call
a.call.Start()
// Required so that the WailsInit functions are fired!
runtime.GoRuntime().Events.Emit("wails:loaded")
if err := a.webserver.Start(dispatcher, event); err != nil {
a.logger.Error("Webserver failed to start %s", err)
return err
}
return nil
})
return cli.Run()
}
// Bind a struct to the application by passing in
// a pointer to it
func (a *App) Bind(structPtr interface{}) {
// Add the struct to the bindings
err := a.bindings.Add(structPtr)
if err != nil {
a.logger.Fatal("Error during binding: " + err.Error())
}
}

View File

@@ -0,0 +1,112 @@
package assetdb
import (
"fmt"
"strings"
"unsafe"
)
// AssetDB is a database for assets encoded as byte slices
type AssetDB struct {
db map[string][]byte
}
// NewAssetDB creates a new AssetDB and initialises a blank db
func NewAssetDB() *AssetDB {
return &AssetDB{
db: make(map[string][]byte),
}
}
// AddAsset saves the given byte slice under the given name
func (a *AssetDB) AddAsset(name string, data []byte) {
a.db[name] = data
}
// Remove removes the named asset
func (a *AssetDB) Remove(name string) {
delete(a.db, name)
}
// Asset retrieves the byte slice for the given name
func (a *AssetDB) Read(name string) ([]byte, error) {
result := a.db[name]
if result == nil {
return nil, fmt.Errorf("asset '%s' not found", name)
}
return result, nil
}
// AssetAsString returns the asset as a string.
// It also returns a boolean indicating whether the asset existed or not.
func (a *AssetDB) String(name string) (string, error) {
asset, err := a.Read(name)
if err != nil {
return "", err
}
return *(*string)(unsafe.Pointer(&asset)), nil
}
func (a *AssetDB) Dump() {
fmt.Printf("Assets:\n")
for k, _ := range a.db {
fmt.Println(k)
}
}
// Serialize converts the entire database to a file that when compiled will
// reconstruct the AssetDB during init()
// name: name of the asset.AssetDB instance
// pkg: package name placed at the top of the file
func (a *AssetDB) Serialize(name, pkg string) string {
var cdata strings.Builder
// Set buffer size to 4k
cdata.Grow(4096)
// Write header
header := `// DO NOT EDIT - Generated automatically
package %s
import "github.com/leaanthony/wailsv2/v2/internal/assetdb"
var (
%s *assetdb.AssetDB = assetdb.NewAssetDB()
)
// AssetsDB is a clean interface to the assetdb.AssetDB struct
type AssetsDB interface {
Read(string) ([]byte, error)
String(string) (string, error)
}
// Assets returns the asset database
func Assets() AssetsDB {
return %s
}
func init() {
`
cdata.WriteString(fmt.Sprintf(header, pkg, name, name))
for aname, bytes := range a.db {
cdata.WriteString(fmt.Sprintf("\t%s.AddAsset(\"%s\", []byte{",
name,
aname))
l := len(bytes)
if l == 0 {
cdata.WriteString("0x00})\n")
continue
}
// Convert each byte to hex
for _, b := range bytes[:l-1] {
cdata.WriteString(fmt.Sprintf("0x%x, ", b))
}
cdata.WriteString(fmt.Sprintf("0x%x})\n", bytes[l-1]))
}
cdata.WriteString(`}`)
return cdata.String()
}

View File

@@ -0,0 +1,70 @@
package assetdb
import "testing"
import "github.com/matryer/is"
func TestExistsAsBytes(t *testing.T) {
is := is.New(t)
var helloworld = []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}
db := NewAssetDB()
db.AddAsset("hello", helloworld)
result, err := db.Read("hello")
is.True(err == nil)
is.Equal(result, helloworld)
}
func TestNotExistsAsBytes(t *testing.T) {
is := is.New(t)
var helloworld = []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}
db := NewAssetDB()
db.AddAsset("hello4", helloworld)
result, err := db.Read("hello")
is.True(err != nil)
is.True(result == nil)
}
func TestExistsAsString(t *testing.T) {
is := is.New(t)
var helloworld = []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}
db := NewAssetDB()
db.AddAsset("hello", helloworld)
result, err := db.String("hello")
// Ensure it exists
is.True(err == nil)
// Ensure the string is the same as the byte slice
is.Equal(result, "Hello, World!")
}
func TestNotExistsAsString(t *testing.T) {
is := is.New(t)
var helloworld = []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}
db := NewAssetDB()
db.AddAsset("hello", helloworld)
result, err := db.String("help")
// Ensure it doesn't exist
is.True(err != nil)
// Ensure the string is blank
is.Equal(result, "")
}

View File

@@ -0,0 +1,176 @@
// +build !desktop
package assetdb
import (
"errors"
"io"
"net/http"
"os"
"path"
"sort"
"strings"
"time"
)
var errWhence = errors.New("Seek: invalid whence")
var errOffset = errors.New("Seek: invalid offset")
// Open implements the http.FileSystem interface for the AssetDB
func (a *AssetDB) Open(name string) (http.File, error) {
if name == "/" || name == "" {
return &Entry{name: "/", dir: true, db: a}, nil
}
if data, ok := a.db[name]; ok {
return &Entry{name: name, data: data, size: len(data)}, nil
} else {
for n, _ := range a.db {
if strings.HasPrefix(n, name+"/") {
return &Entry{name: name, db: a, dir: true}, nil
}
}
}
return &Entry{}, os.ErrNotExist
}
// readdir returns the directory entries for the requested directory
func (a *AssetDB) readdir(name string) ([]os.FileInfo, error) {
dir := name
var ents []string
fim := make(map[string]os.FileInfo)
for fn, data := range a.db {
if strings.HasPrefix(fn, dir) {
pieces := strings.Split(fn[len(dir)+1:], "/")
if len(pieces) == 1 {
fim[pieces[0]] = FI{name: pieces[0], dir: false, size: len(data)}
ents = append(ents, pieces[0])
} else {
fim[pieces[0]] = FI{name: pieces[0], dir: true, size: -1}
ents = append(ents, pieces[0])
}
}
}
if len(ents) == 0 {
return nil, os.ErrNotExist
}
sort.Strings(ents)
var list []os.FileInfo
for _, dir := range ents {
list = append(list, fim[dir])
}
return list, nil
}
// Entry implements the http.File interface to allow for
// use in the http.FileSystem implementation of AssetDB
type Entry struct {
name string
data []byte
dir bool
size int
db *AssetDB
off int
}
// Close is a noop
func (e Entry) Close() error {
return nil
}
// Read fills the slice provided returning how many bytes were written
// and any errors encountered
func (e *Entry) Read(p []byte) (n int, err error) {
if e.off >= e.size {
return 0, io.EOF
}
numout := len(p)
if numout > e.size {
numout = e.size
}
for i := 0; i < numout; i++ {
p[i] = e.data[e.off+i]
}
e.off += numout
n = int(numout)
err = nil
return
}
// Seek seeks to the specified offset from the location specified by whence
func (e *Entry) Seek(offset int64, whence int) (int64, error) {
switch whence {
default:
return 0, errWhence
case io.SeekStart:
offset += 0
case io.SeekCurrent:
offset += int64(e.off)
case io.SeekEnd:
offset += int64(e.size)
}
if offset < 0 {
return 0, errOffset
}
e.off = int(offset)
return offset, nil
}
// Readdir returns the directory entries inside this entry if it is a directory
func (e Entry) Readdir(count int) ([]os.FileInfo, error) {
ents := []os.FileInfo{}
if !e.dir {
return ents, errors.New("Not a directory")
}
return e.db.readdir(e.name)
}
// Stat returns information about this directory entry
func (e Entry) Stat() (os.FileInfo, error) {
return FI{e.name, e.size, e.dir}, nil
}
// FI is the AssetDB implementation of os.FileInfo.
type FI struct {
name string
size int
dir bool
}
// IsDir returns true if this is a directory
func (fi FI) IsDir() bool {
return fi.dir
}
// ModTime always returns now
func (fi FI) ModTime() time.Time {
return time.Time{}
}
// Mode returns the file as readonly and directories
// as world writeable and executable
func (fi FI) Mode() os.FileMode {
if fi.IsDir() {
return 0755 | os.ModeDir
}
return 0444
}
// Name returns the name of this object without
// any leading slashes
func (fi FI) Name() string {
return path.Base(fi.name)
}
// Size returns the size of this item
func (fi FI) Size() int64 {
return int64(fi.size)
}
// Sys returns nil
func (fi FI) Sys() interface{} {
return nil
}

View File

@@ -0,0 +1,108 @@
package assetdb
import (
"fmt"
"os"
"testing"
"github.com/matryer/is"
)
func TestOpenLeadingSlash(t *testing.T) {
is := is.New(t)
var helloworld = []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}
db := NewAssetDB()
db.AddAsset("/hello", helloworld)
file, err := db.Open("/hello")
// Ensure it does exist
is.True(err == nil)
buff := make([]byte, len(helloworld))
n, err := file.Read(buff)
fmt.Printf("Error %v\n", err)
is.True(err == nil)
is.Equal(n, len(helloworld))
result := string(buff)
// Ensure the string is blank
is.Equal(result, string(helloworld))
}
func TestOpen(t *testing.T) {
is := is.New(t)
var helloworld = []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}
db := NewAssetDB()
db.AddAsset("/hello", helloworld)
file, err := db.Open("/hello")
// Ensure it does exist
is.True(err == nil)
buff := make([]byte, len(helloworld))
n, err := file.Read(buff)
is.True(err == nil)
is.Equal(n, len(helloworld))
result := string(buff)
// Ensure the string is blank
is.Equal(result, string(helloworld))
}
func TestReaddir(t *testing.T) {
is := is.New(t)
var helloworld = []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}
db := NewAssetDB()
db.AddAsset("/hello", helloworld)
db.AddAsset("/directory/hello", helloworld)
db.AddAsset("/directory/subdirectory/hello", helloworld)
dir, err := db.Open("/doesntexist")
is.True(err == os.ErrNotExist)
ents, err := dir.Readdir(-1)
is.Equal([]os.FileInfo{}, ents)
dir, err = db.Open("/")
is.True(dir != nil)
is.True(err == nil)
ents, err = dir.Readdir(-1)
is.True(err == nil)
is.Equal(3, len(ents))
}
func TestReaddirSubdirectory(t *testing.T) {
is := is.New(t)
var helloworld = []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}
db := NewAssetDB()
db.AddAsset("/hello", helloworld)
db.AddAsset("/directory/hello", helloworld)
db.AddAsset("/directory/subdirectory/hello", helloworld)
expected := []os.FileInfo{
FI{name: "hello", dir: false, size: len(helloworld)},
FI{name: "subdirectory", dir: true, size: -1},
}
dir, err := db.Open("/directory")
is.True(dir != nil)
is.True(err == nil)
ents, err := dir.Readdir(-1)
is.Equal(expected, ents)
// Check sub-subdirectory
dir, err = db.Open("/directory/subdirectory")
is.True(dir != nil)
is.True(err == nil)
ents, err = dir.Readdir(-1)
is.True(err == nil)
is.Equal([]os.FileInfo{FI{name: "hello", size: len(helloworld)}}, ents)
}

9
v2/internal/bind/bind.go Normal file
View File

@@ -0,0 +1,9 @@
package bind
func IsStructPointer(value interface{}) bool {
switch t := value.(type) {
default:
println(t)
}
return false
}

70
v2/internal/binding/binding.go Executable file
View File

@@ -0,0 +1,70 @@
package binding
import (
"fmt"
"strings"
"github.com/leaanthony/wailsv2/v2/internal/logger"
)
type Bindings struct {
db *DB
logger logger.CustomLogger
}
// NewBindings returns a new Bindings object
func NewBindings(logger *logger.Logger) *Bindings {
return &Bindings{
db: newDB(),
logger: logger.CustomLogger("Bindings"),
}
}
// Add the given struct methods to the Bindings
func (b *Bindings) Add(structPtr interface{}) error {
methods, err := getMethods(structPtr)
if err != nil {
return fmt.Errorf("unable to Add() - %s", err.Error())
}
for _, method := range methods {
splitName := strings.Split(method.Name, ".")
packageName := splitName[0]
structName := splitName[1]
methodName := splitName[2]
// Is this WailsInit?
if method.IsWailsInit() {
err := b.db.AddWailsInit(method)
if err != nil {
return err
}
b.logger.Trace("Registered WailsInit method: %s", method.Name)
continue
}
// Is this WailsShutdown?
if method.IsWailsShutdown() {
err := b.db.AddWailsShutdown(method)
if err != nil {
return err
}
b.logger.Trace("Registered WailsShutdown method: %s", method.Name)
continue
}
// Add it as a regular method
b.db.AddMethod(packageName, structName, methodName, method)
}
return nil
}
func (b *Bindings) DB() *DB {
return b.db
}
func (b *Bindings) ToJSON() (string, error) {
return b.db.ToJSON()
}

View File

@@ -0,0 +1,138 @@
package binding
import (
"fmt"
"reflect"
"strings"
)
// BoundMethod defines all the data related to a Go method that is
// bound to the Wails application
type BoundMethod struct {
Name string `json:"name"`
Inputs []*Parameter `json:"inputs,omitempty"`
Outputs []*Parameter `json:"outputs,omitempty"`
Comments string `json:"comments,omitempty"`
Method reflect.Value `json:"-"`
}
// IsWailsInit returns true if the method name is "WailsInit"
func (b *BoundMethod) IsWailsInit() bool {
return strings.HasSuffix(b.Name, "WailsInit")
}
// IsWailsShutdown returns true if the method name is "WailsShutdown"
func (b *BoundMethod) IsWailsShutdown() bool {
return strings.HasSuffix(b.Name, "WailsShutdown")
}
// VerifyWailsInit checks if the WailsInit signature is correct
func (b *BoundMethod) VerifyWailsInit() error {
// Must only have 1 input
if b.InputCount() != 1 {
return fmt.Errorf("invalid method signature for %s: expected `WailsInit(*wails.Runtime) error`", b.Name)
}
// Check input type
if !b.Inputs[0].IsType("*goruntime.Runtime") {
return fmt.Errorf("invalid method signature for %s: expected `WailsInit(*wails.Runtime) error`", b.Name)
}
// Must only have 1 output
if b.OutputCount() != 1 {
return fmt.Errorf("invalid method signature for %s: expected `WailsInit(*wails.Runtime) error`", b.Name)
}
// Check output type
if !b.Outputs[0].IsError() {
return fmt.Errorf("invalid method signature for %s: expected `WailsInit(*wails.Runtime) error`", b.Name)
}
// Input must be of type Runtime
return nil
}
// VerifyWailsShutdown checks if the WailsShutdown signature is correct
func (b *BoundMethod) VerifyWailsShutdown() error {
// Must have no inputs
if b.InputCount() != 0 {
return fmt.Errorf("invalid method signature for WailsShutdown: expected `WailsShutdown()`")
}
// Must have no outputs
if b.OutputCount() != 0 {
return fmt.Errorf("invalid method signature for WailsShutdown: expected `WailsShutdown()`")
}
// Input must be of type Runtime
return nil
}
// InputCount returns the number of inputs this bound method has
func (b *BoundMethod) InputCount() int {
return len(b.Inputs)
}
// OutputCount returns the number of outputs this bound method has
func (b *BoundMethod) OutputCount() int {
return len(b.Outputs)
}
// 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 {
// Attempt to convert the argument to the type expected by the method
value, err := convertArgToValue(arg, b.Inputs[index])
// If it fails, return a suitable error
if err != nil {
return nil, fmt.Errorf("%s (parameter %d): %s", b.Name, index+1, err.Error())
}
// Save the converted argument
callArgs[index] = value
}
// Do the call
callResults := b.Method.Call(callArgs)
//** Check results **//
var returnValue interface{}
var err error
switch b.OutputCount() {
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
}

150
v2/internal/binding/db.go Normal file
View File

@@ -0,0 +1,150 @@
package binding
import (
"encoding/json"
"sync"
"unsafe"
)
// DB is our database of method bindings
type DB struct {
// map[packagename] -> map[structname] -> map[methodname]*method
store map[string]map[string]map[string]*BoundMethod
// This uses fully qualified method names as a shortcut for store traversal.
// It used for performance gains at runtime
methodMap map[string]*BoundMethod
// These are slices of methods registered using WailsInit and WailsShutdown
wailsInitMethods []*BoundMethod
wailsShutdownMethods []*BoundMethod
// Lock to ensure sync access to the data
lock sync.RWMutex
}
func newDB() *DB {
return &DB{
store: make(map[string]map[string]map[string]*BoundMethod),
methodMap: make(map[string]*BoundMethod),
}
}
// GetMethodFromStore returns the method for the given package/struct/method names
// nil is returned if any one of those does not exist
func (d *DB) GetMethodFromStore(packageName string, structName string, methodName string) *BoundMethod {
// Lock the db whilst processing and unlock on return
d.lock.RLock()
defer d.lock.RUnlock()
structMap, exists := d.store[packageName]
if !exists {
return nil
}
methodMap, exists := structMap[structName]
if !exists {
return nil
}
return methodMap[methodName]
}
// GetMethod returns the method for the given qualified method name
// qualifiedMethodName is "packagename.structname.methodname"
func (d *DB) GetMethod(qualifiedMethodName string) *BoundMethod {
// Lock the db whilst processing and unlock on return
d.lock.RLock()
defer d.lock.RUnlock()
return d.methodMap[qualifiedMethodName]
}
// AddMethod adds the given method definition to the db using the given qualified path: packageName.structName.methodName
func (d *DB) AddMethod(packageName string, structName string, methodName string, methodDefinition *BoundMethod) {
// TODO: Validate inputs?
// Lock the db whilst processing and unlock on return
d.lock.Lock()
defer d.lock.Unlock()
// Get the map associated with the package name
structMap, exists := d.store[packageName]
if !exists {
// Create a new map for this packagename
d.store[packageName] = make(map[string]map[string]*BoundMethod)
structMap = d.store[packageName]
}
// Get the map associated with the struct name
methodMap, exists := structMap[structName]
if !exists {
// Create a new map for this packagename
structMap[structName] = make(map[string]*BoundMethod)
methodMap = structMap[structName]
}
// Store the method definition
methodMap[methodName] = methodDefinition
// Store in the methodMap
key := packageName + "." + structName + "." + methodName
d.methodMap[key] = methodDefinition
}
// AddWailsInit checks the given method is a WailsInit method and if it
// is, adds it to the list of WailsInit methods
func (d *DB) AddWailsInit(method *BoundMethod) error {
err := method.VerifyWailsInit()
if err != nil {
return err
}
// Lock the db whilst processing and unlock on return
d.lock.Lock()
defer d.lock.Unlock()
d.wailsInitMethods = append(d.wailsInitMethods, method)
return nil
}
// AddWailsShutdown checks the given method is a WailsInit method and if it
// is, adds it to the list of WailsShutdown methods
func (d *DB) AddWailsShutdown(method *BoundMethod) error {
err := method.VerifyWailsShutdown()
if err != nil {
return err
}
// Lock the db whilst processing and unlock on return
d.lock.Lock()
defer d.lock.Unlock()
d.wailsShutdownMethods = append(d.wailsShutdownMethods, method)
return nil
}
// ToJSON converts the method map to JSON
func (d *DB) ToJSON() (string, error) {
// Lock the db whilst processing and unlock on return
d.lock.RLock()
defer d.lock.RUnlock()
bytes, err := json.Marshal(&d.store)
// Return zero copy string as this string will be read only
return *(*string)(unsafe.Pointer(&bytes)), err
}
// WailsInitMethods returns the list of registered WailsInit methods
func (d *DB) WailsInitMethods() []*BoundMethod {
return d.wailsInitMethods
}
// WailsShutdownMethods returns the list of registered WailsInit methods
func (d *DB) WailsShutdownMethods() []*BoundMethod {
return d.wailsShutdownMethods
}

View File

@@ -0,0 +1,28 @@
package binding
import "reflect"
// Parameter defines a Go method parameter
type Parameter struct {
Name string `json:"name,omitempty"`
TypeName string `json:"type"`
reflectType reflect.Type
}
func newParameter(Name string, Type reflect.Type) *Parameter {
return &Parameter{
Name: Name,
TypeName: Type.String(),
reflectType: Type,
}
}
// IsType returns true if the given
func (p *Parameter) IsType(typename string) bool {
return p.TypeName == typename
}
// IsError returns true if the parameter type is an error
func (p *Parameter) IsError() bool {
return p.IsType("error")
}

109
v2/internal/binding/reflect.go Executable file
View File

@@ -0,0 +1,109 @@
package binding
import (
"fmt"
"reflect"
)
// isStructPtr returns true if the value given is a
// pointer to a struct
func isStructPtr(value interface{}) bool {
return reflect.ValueOf(value).Kind() == reflect.Ptr &&
reflect.ValueOf(value).Elem().Kind() == reflect.Struct
}
func getMethods(value interface{}) ([]*BoundMethod, error) {
// Create result placeholder
var result []*BoundMethod
// Check type
if !isStructPtr(value) {
return nil, fmt.Errorf("not a pointer to an interface")
}
// Process Struct
structType := reflect.TypeOf(value)
structValue := reflect.ValueOf(value)
baseName := structType.String()[1:]
// Process Methods
for i := 0; i < structType.NumMethod(); i++ {
methodDef := structType.Method(i)
methodName := methodDef.Name
fullMethodName := baseName + "." + methodName
method := structValue.MethodByName(methodName)
// Create new method
boundMethod := &BoundMethod{
Name: fullMethodName,
Inputs: nil,
Outputs: nil,
Comments: "",
Method: method,
}
// Iterate inputs
methodType := method.Type()
inputParamCount := methodType.NumIn()
var inputs []*Parameter
for inputIndex := 0; inputIndex < inputParamCount; inputIndex++ {
input := methodType.In(inputIndex)
thisParam := newParameter("", input)
inputs = append(inputs, thisParam)
}
boundMethod.Inputs = inputs
// Iterate outputs
outputParamCount := methodType.NumOut()
var outputs []*Parameter
for outputIndex := 0; outputIndex < outputParamCount; outputIndex++ {
output := methodType.Out(outputIndex)
thisParam := newParameter("", output)
outputs = append(outputs, thisParam)
}
boundMethod.Outputs = outputs
// Save method in result
result = append(result, boundMethod)
}
return result, nil
}
// convertArgToValue
func convertArgToValue(input interface{}, target *Parameter) (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", r.(string)[23:])
}
}()
// Do the conversion
// Handle nil values
if input == nil {
switch target.reflectType.Kind() {
case reflect.Chan,
reflect.Func,
reflect.Interface,
reflect.Map,
reflect.Ptr,
reflect.Slice:
result = reflect.ValueOf(input).Convert(target.reflectType)
default:
return reflect.Zero(target.reflectType), fmt.Errorf("Unable to use null value")
}
} else {
result = reflect.ValueOf(input).Convert(target.reflectType)
}
// We don't like doing this but it's the only way to
// handle recover() correctly
return
}

View File

@@ -0,0 +1,17 @@
package crypto
import (
"crypto/rand"
"fmt"
"log"
)
// RandomID returns a random ID as a string
func RandomID() string {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
log.Fatal(err)
}
return fmt.Sprintf("%x", b)
}

View File

@@ -0,0 +1,13 @@
package features
// Features holds generic and platform specific feature flags
type Features struct {
Linux *Linux
}
// New creates a new Features object
func New() *Features {
return &Features{
Linux: &Linux{},
}
}

View File

@@ -0,0 +1,5 @@
package features
// Linux holds linux specific feature flags
type Linux struct {
}

View File

@@ -0,0 +1,23 @@
// +build linux
package ffenestri
/*
#cgo linux CFLAGS: -DFFENESTRI_LINUX=1
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0
#include <stdlib.h>
#include "ffenestri.h"
*/
import "C"
import "github.com/leaanthony/wailsv2/v2/internal/features"
func (a *Application) processOSFeatureFlags(features *features.Features) {
// Process Linux features
// linux := features.Linux
}

View File

@@ -0,0 +1,196 @@
package ffenestri
import (
"runtime"
"strings"
"unsafe"
"github.com/leaanthony/wailsv2/v2/internal/features"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/messagedispatcher"
)
/*
#cgo linux CFLAGS: -DFFENESTRI_LINUX=1
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0
#include <stdlib.h>
#include "ffenestri.h"
*/
import "C"
// DEBUG is the global Ffenestri debug flag.
// TODO: move to compile time.
var DEBUG bool = true
// Config defines how our application should be configured
type Config struct {
Title string
Width int
Height int
MinWidth int
MinHeight int
MaxWidth int
MaxHeight int
DevTools bool
Resizable bool
Fullscreen bool
Frameless bool
}
var defaultConfig = &Config{
Title: "My Wails App",
Width: 800,
Height: 600,
DevTools: true,
Resizable: true,
Fullscreen: false,
Frameless: false,
}
// Application is our main application object
type Application struct {
config *Config
memory []unsafe.Pointer
// This is the main app pointer
app unsafe.Pointer
// Logger
logger logger.CustomLogger
}
func (a *Application) saveMemoryReference(mem unsafe.Pointer) {
a.memory = append(a.memory, mem)
}
func (a *Application) string2CString(str string) *C.char {
result := C.CString(str)
a.saveMemoryReference(unsafe.Pointer(result))
return result
}
func init() {
runtime.LockOSThread()
}
// NewApplicationWithConfig creates a new application based on the given config
func NewApplicationWithConfig(config *Config, logger *logger.Logger) *Application {
return &Application{
config: config,
logger: logger.CustomLogger("Ffenestri"),
}
}
// NewApplication creates a new Application with the default config
func NewApplication(logger *logger.Logger) *Application {
return &Application{
config: defaultConfig,
logger: logger.CustomLogger("Ffenestri"),
}
}
func (a *Application) freeMemory() {
for _, mem := range a.memory {
// fmt.Printf("Freeing memory: %+v\n", mem)
C.free(mem)
}
}
// bool2Cint converts a Go boolean to a C integer
func (a *Application) bool2Cint(value bool) C.int {
if value {
return C.int(1)
}
return C.int(0)
}
// dispatcher is the interface to send messages to
var dispatcher *messagedispatcher.DispatchClient
// Dispatcher is what we register out client with
type Dispatcher interface {
RegisterClient(client messagedispatcher.Client) *messagedispatcher.DispatchClient
}
// DispatchClient is the means for passing messages to the backend
type DispatchClient interface {
SendMessage(string)
}
// Run the application
func (a *Application) Run(incomingDispatcher Dispatcher, bindings string, features *features.Features) error {
title := a.string2CString(a.config.Title)
width := C.int(a.config.Width)
height := C.int(a.config.Height)
resizable := a.bool2Cint(a.config.Resizable)
devtools := a.bool2Cint(a.config.DevTools)
fullscreen := a.bool2Cint(a.config.Fullscreen)
app := C.NewApplication(title, width, height, resizable, devtools, fullscreen)
// Save app reference
a.app = unsafe.Pointer(app)
// Set Min Window Size
minWidth := C.int(a.config.MinWidth)
minHeight := C.int(a.config.MinHeight)
C.SetMinWindowSize(a.app, minWidth, minHeight)
// Set Max Window Size
maxWidth := C.int(a.config.MaxWidth)
maxHeight := C.int(a.config.MaxHeight)
C.SetMaxWindowSize(a.app, maxWidth, maxHeight)
// Set debug if needed
C.SetDebug(app, a.bool2Cint(DEBUG))
// Set Frameless
if a.config.Frameless {
C.DisableFrame(a.app)
}
// Escape bindings so C doesn't freak out
bindings = strings.ReplaceAll(bindings, `"`, `\"`)
// Set bindings
C.SetBindings(app, a.string2CString(bindings))
// Process feature flags
a.processFeatureFlags(features)
// save the dispatcher in a package variable so that the C callbacks
// can access it
dispatcher = incomingDispatcher.RegisterClient(newClient(a))
// Check we could initialise the application
if app != nil {
// Yes - Save memory reference and run app, cleaning up afterwards
a.saveMemoryReference(unsafe.Pointer(app))
C.Run(app, 0, nil)
} else {
// Oh no! We couldn't initialise the application
a.logger.Fatal("Cannot initialise Application.")
}
a.freeMemory()
return nil
}
// messageFromWindowCallback is called by any messages sent in
// webkit to window.external.invoke. It relays the message on to
// the dispatcher.
//export messageFromWindowCallback
func messageFromWindowCallback(data *C.char) {
dispatcher.DispatchMessage(C.GoString(data))
}
func (a *Application) processFeatureFlags(features *features.Features) {
// Process generic features
// Process OS Specific flags
a.processOSFeatureFlags(features)
}

View File

@@ -0,0 +1,24 @@
#ifndef __FFENESTRI_H__
#define __FFENESTRI_H__
#include <stdio.h>
extern void *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen);
extern void SetMinWindowSize(void *app, int minWidth, int minHeight);
extern void SetMaxWindowSize(void *app, int maxWidth, int maxHeight);
extern void Run(void *app, int argc, char **argv);
extern void DestroyApplication(void *app);
extern void SetDebug(void *app, int flag);
extern void SetBindings(void *app, const char *bindings);
extern void ExecJS(void *app, const char *script);
extern void Quit(void *app);
extern void SetTitle(void *app, const char *title);
extern void Fullscreen(void *app);
extern void UnFullscreen(void *app);
extern int SetColour(void *app, const char *colourString);
extern void DisableFrame(void *app);
extern char *SaveFileDialogOnMainThread(void *appPointer, char *title);
extern char *OpenFileDialogOnMainThread(void *appPointer, char *title);
extern char *OpenDirectoryDialogOnMainThread(void *appPointer, char *title);
#endif

View File

@@ -0,0 +1,109 @@
package ffenestri
/*
#cgo linux CFLAGS: -DFFENESTRI_LINUX=1
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0
#include <stdlib.h>
#include "ffenestri.h"
*/
import "C"
import (
"strconv"
"unsafe"
"github.com/leaanthony/wailsv2/v2/internal/logger"
)
// Client is our implentation of messageDispatcher.Client
type Client struct {
app *Application
logger logger.CustomLogger
}
func newClient(app *Application) *Client {
return &Client{
app: app,
logger: app.logger,
}
}
// Quit the application
func (c *Client) Quit() {
c.app.logger.Trace("Got shutdown message")
C.Quit(c.app.app)
}
// NotifyEvent will pass on the event message to the frontend
func (c *Client) NotifyEvent(message string) {
eventMessage := `window.wails._.Notify(` + strconv.Quote(message) + `);`
c.app.logger.Trace("eventMessage = %+v", eventMessage)
C.ExecJS(c.app.app, c.app.string2CString(eventMessage))
}
// CallResult contains the result of the call from JS
func (c *Client) CallResult(message string) {
callbackMessage := `window.wails._.Callback(` + strconv.Quote(message) + `);`
c.app.logger.Trace("callbackMessage = %+v", callbackMessage)
C.ExecJS(c.app.app, c.app.string2CString(callbackMessage))
}
// WindowSetTitle sets the window title to the given string
func (c *Client) WindowSetTitle(title string) {
C.SetTitle(c.app.app, c.app.string2CString(title))
}
// WindowFullscreen will set the window to be fullscreen
func (c *Client) WindowFullscreen() {
C.Fullscreen(c.app.app)
}
// WindowUnFullscreen will unfullscreen the window
func (c *Client) WindowUnFullscreen() {
C.UnFullscreen(c.app.app)
}
// WindowSetColour sets the window colour
func (c *Client) WindowSetColour(colour string) bool {
result := C.SetColour(c.app.app, c.app.string2CString(colour))
return result == 1
}
// OpenFileDialog will open a file dialog with the given title
func (c *Client) OpenFileDialog(title string) string {
cstring := C.OpenFileDialogOnMainThread(c.app.app, c.app.string2CString(title))
var result string
if cstring != nil {
result = C.GoString(cstring)
// Free the C string that was allocated by the dialog
C.free(unsafe.Pointer(cstring))
}
return result
}
// SaveFileDialog will open a save file dialog with the given title
func (c *Client) SaveFileDialog(title string) string {
cstring := C.SaveFileDialogOnMainThread(c.app.app, c.app.string2CString(title))
var result string
if cstring != nil {
result = C.GoString(cstring)
// Free the C string that was allocated by the dialog
C.free(unsafe.Pointer(cstring))
}
return result
}
// OpenDirectoryDialog will open a directory dialog with the given title
func (c *Client) OpenDirectoryDialog(title string) string {
cstring := C.OpenDirectoryDialogOnMainThread(c.app.app, c.app.string2CString(title))
var result string
if cstring != nil {
result = C.GoString(cstring)
// Free the C string that was allocated by the dialog
C.free(unsafe.Pointer(cstring))
}
return result
}

View File

@@ -0,0 +1,856 @@
#ifndef __FFENESTRI_LINUX_H__
#define __FFENESTRI_LINUX_H__
#include "gtk/gtk.h"
#include "webkit2/webkit2.h"
#include <time.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
// References to assets
extern const unsigned char *assets[];
extern const unsigned char runtime;
extern const char *icon[];
// Constants
#define PRIMARY_MOUSE_BUTTON 1
#define MIDDLE_MOUSE_BUTTON 2
#define SECONDARY_MOUSE_BUTTON 3
// MAIN DEBUG FLAG
int debug;
// Credit: https://stackoverflow.com/a/8465083
char *concat(const char *s1, const char *s2)
{
const size_t len1 = strlen(s1);
const size_t len2 = strlen(s2);
char *result = malloc(len1 + len2 + 1);
memcpy(result, s1, len1);
memcpy(result + len1, s2, len2 + 1);
return result;
}
// Debug works like sprintf but mutes if the global debug flag is true
// Credit: https://stackoverflow.com/a/20639708
void Debug(char *message, ...)
{
if (debug)
{
char *temp = concat("TRACE | Ffenestri (C) | ", message);
message = concat(temp, "\n");
free(temp);
va_list args;
va_start(args, message);
vprintf(message, args);
free(message);
va_end(args);
}
}
extern void messageFromWindowCallback(const char *);
typedef void (*ffenestriCallback)(const char *);
struct Application
{
// Gtk Data
GtkApplication *application;
GtkWindow *mainWindow;
GtkWidget *webView;
int signalInvoke;
int signalWindowDrag;
int signalButtonPressed;
int signalButtonReleased;
int signalLoadChanged;
// Saves the events for the drag mouse button
GdkEventButton *dragButtonEvent;
// The number of the default drag button
int dragButton;
// Window Data
const char *title;
char *id;
int width;
int height;
int resizable;
int devtools;
int fullscreen;
int minWidth;
int minHeight;
int maxWidth;
int maxHeight;
int frame;
// User Data
char *HTML;
// Callback
ffenestriCallback sendMessageToBackend;
// Bindings
const char *bindings;
// Lock - used for sync operations (Should we be using g_mutex?)
int lock;
};
void *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen)
{
// Setup main application struct
struct Application *result = malloc(sizeof(struct Application));
result->title = title;
result->width = width;
result->height = height;
result->resizable = resizable;
result->devtools = devtools;
result->fullscreen = fullscreen;
result->minWidth = 0;
result->minHeight = 0;
result->maxWidth = 0;
result->maxHeight = 0;
result->frame = 1;
// Default drag button is PRIMARY
result->dragButton = PRIMARY_MOUSE_BUTTON;
// printf("\n\nWidth: %d\n", result->width);
// printf("Height: %d\n\n", result->height);
// Features
result->sendMessageToBackend = (ffenestriCallback)messageFromWindowCallback;
// Create a unique ID based on the current unix timestamp
char temp[11];
sprintf(&temp[0], "%d", (int)time(NULL));
result->id = concat("wails.app", &temp[0]);
// Create the main GTK application
GApplicationFlags flags = G_APPLICATION_FLAGS_NONE;
result->application = gtk_application_new(result->id, flags);
// Return the application struct
return (void *)result;
}
void DestroyApplication(void *appPointer)
{
Debug("Destroying Application");
struct Application *app = (struct Application *)appPointer;
g_application_quit(G_APPLICATION(app->application));
// Release the GTK ID string
if (app->id != NULL)
{
free(app->id);
app->id = NULL;
}
else
{
Debug("Almost a double free for app->id");
}
// Free the bindings
if (app->bindings != NULL)
{
free((void *)app->bindings);
app->bindings = NULL;
}
else
{
Debug("Almost a double free for app->bindings");
}
// Disconnect signal handlers
WebKitUserContentManager *manager = webkit_web_view_get_user_content_manager((WebKitWebView *)app->webView);
g_signal_handler_disconnect(manager, app->signalInvoke);
g_signal_handler_disconnect(manager, app->signalWindowDrag);
g_signal_handler_disconnect(app->webView, app->signalButtonPressed);
g_signal_handler_disconnect(app->webView, app->signalButtonReleased);
g_signal_handler_disconnect(app->webView, app->signalLoadChanged);
// Release the main GTK Application
if (app->application != NULL)
{
g_object_unref(app->application);
app->application = NULL;
}
else
{
Debug("Almost a double free for app->application");
}
Debug("Finished Destroying Application");
}
// Quit will stop the gtk application and free up all the memory
// used by the application
void Quit(void *appPointer)
{
Debug("Quit Called");
struct Application *app = (struct Application *)appPointer;
gtk_window_close((GtkWindow *)app->mainWindow);
g_application_quit((GApplication *)app->application);
DestroyApplication(appPointer);
}
// SetTitle sets the main window title to the given string
void SetTitle(void *appPointer, const char *title)
{
struct Application *app = (struct Application *)appPointer;
gtk_window_set_title(app->mainWindow, title);
}
// Fullscreen sets the main window to be fullscreen
void Fullscreen(void *appPointer)
{
struct Application *app = (struct Application *)appPointer;
gtk_window_fullscreen(app->mainWindow);
}
// UnFullscreen resets the main window after a fullscreen
void UnFullscreen(void *appPointer)
{
struct Application *app = (struct Application *)appPointer;
gtk_window_unfullscreen(app->mainWindow);
}
void Center(void *appPointer)
{
struct Application *app = (struct Application *)appPointer;
gtk_window_set_position(app->mainWindow, GTK_WIN_POS_CENTER);
}
void setMinMaxSize(void *appPointer)
{
struct Application *app = (struct Application *)appPointer;
GdkGeometry size;
size.min_width = size.min_height = size.max_width = size.max_height = 0;
int flags = 0;
if (app->maxHeight > 0 && app->maxWidth > 0)
{
size.max_height = app->maxHeight;
size.max_width = app->maxWidth;
flags |= GDK_HINT_MAX_SIZE;
}
if (app->minHeight > 0 && app->minWidth > 0)
{
size.min_height = app->minHeight;
size.min_width = app->minWidth;
flags |= GDK_HINT_MIN_SIZE;
}
Debug("size: %dx%d", app->width, app->height);
Debug("min: %dx%d", size.min_width, size.min_height);
Debug("max: %dx%d", size.max_width, size.max_height);
gtk_window_set_geometry_hints(app->mainWindow, NULL, &size, flags);
}
// OpenFileDialog opens a dialog to select a file
// NOTE: The result is a string that will need to be freed!
char *OpenFileDialog(void *appPointer, char *title)
{
struct Application *app = (struct Application *)appPointer;
GtkFileChooserNative *native;
GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN;
gint res;
char *filename;
native = gtk_file_chooser_native_new(title,
app->mainWindow,
action,
"_Open",
"_Cancel");
res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));
if (res == GTK_RESPONSE_ACCEPT)
{
GtkFileChooser *chooser = GTK_FILE_CHOOSER(native);
filename = gtk_file_chooser_get_filename(chooser);
}
g_object_unref(native);
return filename;
}
// SaveFileDialog opens a dialog to select a file
// NOTE: The result is a string that will need to be freed!
char *SaveFileDialog(void *appPointer, char *title)
{
struct Application *app = (struct Application *)appPointer;
GtkFileChooserNative *native;
GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_SAVE;
gint res;
char *filename;
native = gtk_file_chooser_native_new(title,
app->mainWindow,
action,
"_Save",
"_Cancel");
res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));
if (res == GTK_RESPONSE_ACCEPT)
{
GtkFileChooser *chooser = GTK_FILE_CHOOSER(native);
filename = gtk_file_chooser_get_filename(chooser);
}
g_object_unref(native);
return filename;
}
// OpenDirectoryDialog opens a dialog to select a directory
// NOTE: The result is a string that will need to be freed!
char *OpenDirectoryDialog(void *appPointer, char *title)
{
struct Application *app = (struct Application *)appPointer;
GtkFileChooserNative *native;
GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
gint res;
char *foldername;
native = gtk_file_chooser_native_new(title,
app->mainWindow,
action,
"_Open",
"_Cancel");
res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));
if (res == GTK_RESPONSE_ACCEPT)
{
GtkFileChooser *chooser = GTK_FILE_CHOOSER(native);
foldername = gtk_file_chooser_get_filename(chooser);
}
g_object_unref(native);
return foldername;
}
void SetMinWindowSize(void *appPointer, int minWidth, int minHeight)
{
struct Application *app = (struct Application *)appPointer;
app->minWidth = minWidth;
app->minHeight = minHeight;
}
void SetMaxWindowSize(void *appPointer, int maxWidth, int maxHeight)
{
struct Application *app = (struct Application *)appPointer;
app->maxWidth = maxWidth;
app->maxHeight = maxHeight;
}
// SetColour sets the colour of the webview to the given colour string
int SetColour(void *appPointer, const char *colourString)
{
struct Application *app = (struct Application *)appPointer;
GdkRGBA rgba;
gboolean result = gdk_rgba_parse(&rgba, colourString);
if (result == FALSE)
{
return 0;
}
Debug("Setting webview colour to: %s", colourString);
webkit_web_view_get_background_color((WebKitWebView *)(app->webView), &rgba);
return 1;
}
// DisableFrame disables the window frame
void DisableFrame(void *appPointer)
{
struct Application *app = (struct Application *)appPointer;
app->frame = 0;
}
void syncCallback(GObject *source_object,
GAsyncResult *res,
gpointer user_data)
{
struct Application *app = (struct Application *)user_data;
app->lock = 0;
}
void syncEval(struct Application *app, const gchar *script)
{
WebKitWebView *webView = (WebKitWebView *)(app->webView);
// Debug("[%p] webview\n", webView);
// Debug("[%p] Running sync\n", script);
// wait for lock to free
while (app->lock == 1)
{
g_main_context_iteration(0, true);
}
// Set lock
app->lock = 1;
//
webkit_web_view_run_javascript(
webView,
script,
NULL, syncCallback, app);
while (app->lock == 1)
{
// Debug("[%p] Waiting for callback\n", script);
g_main_context_iteration(0, true);
}
// Debug("[%p] Finished\n", script);
}
void asyncEval(WebKitWebView *webView, const gchar *script)
{
webkit_web_view_run_javascript(
webView,
script,
NULL, NULL, NULL);
}
typedef void (*dispatchMethod)(void *app, void *);
struct dispatchData
{
struct Application *app;
dispatchMethod method;
void *args;
};
gboolean executeMethod(gpointer data)
{
struct dispatchData *d = (struct dispatchData *)data;
struct Application *app = (struct Application *)(d->app);
// Debug("Webview %p\n", app->webView);
// Debug("Args %s\n", d->args);
// Debug("Method %p\n", (d->method));
(d->method)(app, d->args);
// Debug("Method Execute Complete. Freeing memory");
g_free(d);
return FALSE;
}
void ExecJS(void *app, char *js)
{
struct dispatchData *data =
(struct dispatchData *)g_new(struct dispatchData, 1);
data->method = (dispatchMethod)syncEval;
data->args = js;
data->app = app;
gdk_threads_add_idle(executeMethod, data);
}
typedef char *(*dialogMethod)(void *app, void *);
struct dialogCall
{
struct Application *app;
dialogMethod method;
void *args;
char *result;
int done;
};
gboolean executeMethodWithReturn(gpointer data)
{
struct dialogCall *d = (struct dialogCall *)data;
struct Application *app = (struct Application *)(d->app);
Debug("Webview %p\n", app->webView);
Debug("Args %s\n", d->args);
Debug("Method %p\n", (d->method));
d->result = (d->method)(app, d->args);
d->done = 1;
// Debug("Method Execute Complete. Freeing memory");
return FALSE;
}
char *OpenFileDialogOnMainThread(void *app, char *title)
{
struct dialogCall *data =
(struct dialogCall *)g_new(struct dialogCall, 1);
data->result = NULL;
data->done = 0;
data->method = (dialogMethod)OpenFileDialog;
data->args = title;
data->app = app;
gdk_threads_add_idle(executeMethodWithReturn, data);
while (data->done == 0)
{
// Debug("Waiting for dialog");
usleep(100000);
}
Debug("Dialog done");
Debug("Result = %s\n", data->result);
g_free(data);
// Fingers crossed this wasn't freed by g_free above
return data->result;
}
char *SaveFileDialogOnMainThread(void *app, char *title)
{
struct dialogCall *data =
(struct dialogCall *)g_new(struct dialogCall, 1);
data->result = NULL;
data->done = 0;
data->method = (dialogMethod)SaveFileDialog;
data->args = title;
data->app = app;
gdk_threads_add_idle(executeMethodWithReturn, data);
while (data->done == 0)
{
// Debug("Waiting for dialog");
usleep(100000);
}
Debug("Dialog done");
Debug("Result = %s\n", data->result);
g_free(data);
// Fingers crossed this wasn't freed by g_free above
return data->result;
}
char *OpenDirectoryDialogOnMainThread(void *app, char *title)
{
struct dialogCall *data =
(struct dialogCall *)g_new(struct dialogCall, 1);
data->result = NULL;
data->done = 0;
data->method = (dialogMethod)OpenDirectoryDialog;
data->args = title;
data->app = app;
gdk_threads_add_idle(executeMethodWithReturn, data);
while (data->done == 0)
{
// Debug("Waiting for dialog");
usleep(100000);
}
Debug("Directory Dialog done");
Debug("Result = %s\n", data->result);
g_free(data);
// Fingers crossed this wasn't freed by g_free above
return data->result;
}
// Sets the icon to the XPM stored in icon
void setIcon(struct Application *app)
{
GdkPixbuf *appIcon = gdk_pixbuf_new_from_xpm_data((const char **)icon);
gtk_window_set_icon(app->mainWindow, appIcon);
}
static void load_finished_cb(WebKitWebView *webView,
WebKitLoadEvent load_event,
gpointer userData)
{
struct Application *app;
switch (load_event)
{
// case WEBKIT_LOAD_STARTED:
// /* New load, we have now a provisional URI */
// // printf("Start downloading %s\n", webkit_web_view_get_uri(web_view));
// /* Here we could start a spinner or update the
// * location bar with the provisional URI */
// break;
// case WEBKIT_LOAD_REDIRECTED:
// // printf("Redirected to: %s\n", webkit_web_view_get_uri(web_view));
// break;
// case WEBKIT_LOAD_COMMITTED:
// /* The load is being performed. Current URI is
// * the final one and it won't change unless a new
// * load is requested or a navigation within the
// * same page is performed */
// // printf("Loading: %s\n", webkit_web_view_get_uri(web_view));
// break;
case WEBKIT_LOAD_FINISHED:
/* Load finished, we can now stop the spinner */
// printf("Finished loading: %s\n", webkit_web_view_get_uri(web_view));
app = (struct Application *)userData;
// Bindings
Debug("Binding Methods");
syncEval(app, app->bindings);
// Runtime
Debug("Setting up Wails runtime");
syncEval(app, &runtime);
// Loop over assets
int index = 1;
while (1)
{
// Get next asset pointer
const char *asset = assets[index];
// If we have no more assets, break
if (asset == 0x00)
{
break;
}
// sync eval the asset
syncEval(app, asset);
index++;
};
// Set the icon
setIcon(app);
// Setup fullscreen
if (app->fullscreen)
{
Debug("Going fullscreen");
Fullscreen(app);
}
// Setup resize
gtk_window_resize(GTK_WINDOW(app->mainWindow), app->width, app->height);
if (app->resizable)
{
gtk_window_set_default_size(GTK_WINDOW(app->mainWindow), app->width, app->height);
}
else
{
gtk_widget_set_size_request(GTK_WIDGET(app->mainWindow), app->width, app->height);
gtk_window_resize(GTK_WINDOW(app->mainWindow), app->width, app->height);
// Fix the min/max to the window size for good measure
app->minHeight = app->maxHeight = app->height;
app->minWidth = app->maxWidth = app->width;
}
gtk_window_set_resizable(GTK_WINDOW(app->mainWindow), app->resizable ? TRUE : FALSE);
setMinMaxSize(app);
// Centre by default
Center(app);
// Show window and focus
gtk_widget_show_all(GTK_WIDGET(app->mainWindow));
gtk_widget_grab_focus(app->webView);
break;
}
}
static gboolean
disable_context_menu_cb(WebKitWebView *web_view,
WebKitContextMenu *context_menu,
GdkEvent *event,
WebKitHitTestResult *hit_test_result,
gpointer user_data)
{
return TRUE;
}
static void printEvent(const char *message, GdkEventButton *event)
{
Debug("%s: [button:%d] [x:%f] [y:%f] [time:%d]",
message,
event->button,
event->x_root,
event->y_root,
event->time);
}
static void dragWindow(WebKitUserContentManager *contentManager,
WebKitJavascriptResult *result,
gpointer arg)
{
struct Application *app = (struct Application *)arg;
// If we get this message erroneously, ignore
if (app->dragButtonEvent == NULL)
{
return;
}
// Ignore non-toplevel widgets
GtkWidget *window = gtk_widget_get_toplevel(GTK_WIDGET(app->webView));
if (!GTK_IS_WINDOW(window))
{
return;
}
// Initiate the drag
printEvent("Starting drag with event", app->dragButtonEvent);
gtk_window_begin_move_drag(app->mainWindow,
app->dragButton,
app->dragButtonEvent->x_root,
app->dragButtonEvent->y_root,
app->dragButtonEvent->time);
// Clear the event
app->dragButtonEvent = NULL;
return FALSE;
}
gboolean buttonPress(GtkWidget *widget, GdkEventButton *event, gpointer arg)
{
struct Application *app = (struct Application *)arg;
Debug("I am pressing a button");
if (event->type == GDK_BUTTON_PRESS && event->button == app->dragButton)
{
printEvent("Drag button event was saved", event);
app->dragButtonEvent = event;
}
return FALSE;
}
gboolean buttonRelease(GtkWidget *widget, GdkEventButton *event, gpointer arg)
{
struct Application *app = (struct Application *)arg;
Debug("I am releasing a button");
if (event->type == GDK_BUTTON_RELEASE && event->button == app->dragButton)
{
printEvent("Drag button event was reset", event);
app->dragButtonEvent = NULL;
}
return FALSE;
}
static void sendMessageToBackend(WebKitUserContentManager *contentManager,
WebKitJavascriptResult *result,
gpointer arg)
{
struct Application *app = (struct Application *)arg;
#if WEBKIT_MAJOR_VERSION >= 2 && WEBKIT_MINOR_VERSION >= 22
JSCValue *value = webkit_javascript_result_get_js_value(result);
char *message = jsc_value_to_string(value);
#else
JSGlobalContextRef context = webkit_javascript_result_get_global_context(result);
JSValueRef value = webkit_javascript_result_get_value(result);
JSStringRef js = JSValueToStringCopy(context, value, NULL);
size_t messageSize = JSStringGetMaximumUTF8CStringSize(js);
char *message = g_new(char, messageSize);
JSStringGetUTF8CString(js, message, messageSize);
JSStringRelease(js);
#endif
app->sendMessageToBackend(message);
g_free(message);
}
void SetDebug(void *applicationPointer, int flag)
{
struct Application *app = (struct Application *)applicationPointer;
debug = flag;
}
void SetBindings(void *applicationPointer, const char *bindings)
{
struct Application *app = (struct Application *)applicationPointer;
const char *temp = concat("window.wailsbindings = \"", bindings);
const char *jscall = concat(temp, "\";");
free((void *)temp);
app->bindings = jscall;
}
// This is called when the close button on the window is pressed
gboolean close_button_pressed(GtkWidget *widget,
GdkEvent *event,
gpointer user_data)
{
struct Application *app = (struct Application *)user_data;
app->sendMessageToBackend("WC"); // Window Close
return TRUE;
}
static void setupWindow(void *applicationPointer)
{
struct Application *app = (struct Application *)applicationPointer;
// Create the window
GtkWidget *mainWindow = gtk_application_window_new(app->application);
// Save reference
app->mainWindow = GTK_WINDOW(mainWindow);
// Setup frame
gtk_window_set_decorated((GtkWindow *)mainWindow, app->frame);
// Setup title
gtk_window_set_title(GTK_WINDOW(mainWindow), app->title);
// Setup script handler
WebKitUserContentManager *contentManager = webkit_user_content_manager_new();
// Setup the invoke handler
webkit_user_content_manager_register_script_message_handler(contentManager, "external");
app->signalInvoke = g_signal_connect(contentManager, "script-message-received::external", G_CALLBACK(sendMessageToBackend), app);
// Setup the window drag handler
webkit_user_content_manager_register_script_message_handler(contentManager, "windowDrag");
app->signalWindowDrag = g_signal_connect(contentManager, "script-message-received::windowDrag", G_CALLBACK(dragWindow), app);
GtkWidget *webView = webkit_web_view_new_with_user_content_manager(contentManager);
// Save reference
app->webView = webView;
// Add the webview to the window
gtk_container_add(GTK_CONTAINER(mainWindow), webView);
// Setup the mouse handlers
app->signalButtonPressed = g_signal_connect(app->webView, "button-press-event", G_CALLBACK(buttonPress), app);
app->signalButtonReleased = g_signal_connect(app->webView, "button-release-event", G_CALLBACK(buttonRelease), app);
// Load default HTML
app->signalLoadChanged = g_signal_connect(G_OBJECT(webView), "load-changed", G_CALLBACK(load_finished_cb), app);
// Load the user's HTML
// assets[0] is the HTML because the asset array is bundled like that by convention
webkit_web_view_load_uri(WEBKIT_WEB_VIEW(webView), assets[0]);
// Check if we want to enable the dev tools
if (app->devtools)
{
WebKitSettings *settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(webView));
// webkit_settings_set_enable_write_console_messages_to_stdout(settings, true);
webkit_settings_set_enable_developer_extras(settings, true);
}
else
{
g_signal_connect(G_OBJECT(webView), "context-menu", G_CALLBACK(disable_context_menu_cb), app);
}
// Listen for close button signal
g_signal_connect(GTK_WIDGET(mainWindow), "delete-event", G_CALLBACK(close_button_pressed), app);
}
static void
activate(GtkApplication *app,
gpointer user_data)
{
struct Application *mainApp = (struct Application *)user_data;
// Main Window
setupWindow(mainApp);
}
void Run(void *applicationPointer, int argc, char **argv)
{
struct Application *app = (struct Application *)applicationPointer;
g_signal_connect(app->application, "activate", G_CALLBACK(activate), app);
g_application_run(G_APPLICATION(app->application), argc, argv);
}
#endif

146
v2/internal/fs/fs.go Normal file
View File

@@ -0,0 +1,146 @@
package fs
import (
"crypto/md5"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"unsafe"
)
// LocalDirectory gets the caller's file directory
// Equivalent to node's __DIRNAME
func LocalDirectory() string {
_, thisFile, _, _ := runtime.Caller(1)
return filepath.Dir(thisFile)
}
// Mkdir will create the given directory
func Mkdir(dirname string) error {
return os.Mkdir(dirname, 0755)
}
// CopyFile from source to target
func CopyFile(source string, target string) error {
s, err := os.Open(source)
if err != nil {
return err
}
defer s.Close()
d, err := os.Create(target)
if err != nil {
return err
}
if _, err := io.Copy(d, s); err != nil {
d.Close()
return err
}
return d.Close()
}
// DirExists - Returns true if the given path resolves to a directory on the filesystem
func DirExists(path string) bool {
fi, err := os.Lstat(path)
if err != nil {
return false
}
return fi.Mode().IsDir()
}
// FileExists returns a boolean value indicating whether
// the given file exists
func FileExists(path string) bool {
fi, err := os.Lstat(path)
if err != nil {
return false
}
return fi.Mode().IsRegular()
}
// RelativePath returns a qualified path created by joining the
// directory of the calling file and the given relative path.
//
// Example: RelativePath("..") in *this* file would give you '/path/to/wails2/v2/internal`
func RelativePath(relativepath string, optionalpaths ...string) string {
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// If we have optional paths, join them to the relativepath
if len(optionalpaths) > 0 {
paths := []string{relativepath}
paths = append(paths, optionalpaths...)
relativepath = filepath.Join(paths...)
}
result, err := filepath.Abs(filepath.Join(localDir, relativepath))
if err != nil {
// I'm allowing this for 1 reason only: It's fatal if the path
// supplied is wrong as it's only used internally in Wails. If we get
// that path wrong, we should know about it immediately. The other reason is
// that it cuts down a ton of unnecassary error handling.
panic(err)
}
return result
}
// MustLoadString attempts to load a string and will abort with a fatal message if
// something goes wrong
func MustLoadString(filename string) string {
data, err := ioutil.ReadFile(filename)
if err != nil {
fmt.Printf("FATAL: Unable to load file '%s': %s\n", filename, err.Error())
os.Exit(1)
}
return *(*string)(unsafe.Pointer(&data))
}
// MD5File returns the md5sum of the given file
func MD5File(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close()
h := md5.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
// MustMD5File will call MD5File and abort the program on error
func MustMD5File(filename string) string {
result, err := MD5File(filename)
if err != nil {
println("FATAL: Unable to MD5Sum file:", err.Error())
os.Exit(1)
}
return result
}
// MustWriteString will attempt to write the given data to the given filename
// It will abort the program in the event of a failure
func MustWriteString(filename string, data string) {
err := ioutil.WriteFile(filename, []byte(data), 0755)
if err != nil {
fatal("Unable to write file", filename, ":", err.Error())
os.Exit(1)
}
}
// fatal will print the optional messages and die
func fatal(message ...string) {
if len(message) > 0 {
print("FATAL:")
for text := range message {
print(text)
}
}
os.Exit(1)
}

31
v2/internal/fs/fs_test.go Normal file
View File

@@ -0,0 +1,31 @@
package fs
import (
"os"
"path/filepath"
"testing"
"github.com/matryer/is"
)
func TestRelativePath(t *testing.T) {
is := is.New(t)
cwd, err := os.Getwd()
is.Equal(err, nil)
// Check current directory
actual := RelativePath(".")
is.Equal(actual, cwd)
// Check 2 parameters
actual = RelativePath("..", "fs")
is.Equal(actual, cwd)
// Check 3 parameters including filename
actual = RelativePath("..", "fs", "fs.go")
expected := filepath.Join(cwd, "fs.go")
is.Equal(actual, expected)
}

108
v2/internal/html/asset.go Normal file
View File

@@ -0,0 +1,108 @@
package html
import (
"fmt"
"io/ioutil"
"net/url"
"path/filepath"
"regexp"
"strings"
"unsafe"
)
type assetTypes struct {
JS string
CSS string
FAVICON string
HTML string
}
// AssetTypes is an enum for the asset type keys
var AssetTypes *assetTypes = &assetTypes{
JS: "javascript",
CSS: "css",
FAVICON: "favicon",
HTML: "html",
}
// Asset describes an asset type and its path
type Asset struct {
Type string
Path string
Data string
}
// Load the asset from disk
func (a *Asset) Load(basedirectory string) error {
assetpath := filepath.Join(basedirectory, a.Path)
data, err := ioutil.ReadFile(assetpath)
if err != nil {
return err
}
a.Data = string(data)
return nil
}
// AsString returns the data as a READ ONLY string
func (a *Asset) AsString() string {
return a.Data
}
// AsCHexData processes the asset data so it may be used by C
func (a *Asset) AsCHexData() string {
// This will be our final string to hexify
dataString := a.Data
switch a.Type {
case AssetTypes.HTML:
// Escape HTML
var re = regexp.MustCompile(`\s{2,}`)
result := re.ReplaceAllString(a.Data, ``)
result = strings.ReplaceAll(result, "\n", "")
result = strings.ReplaceAll(result, "\r\n", "")
result = strings.ReplaceAll(result, "\n", "")
url := url.URL{Path: result}
urlString := strings.ReplaceAll(url.String(), "/", "%2f")
// Save Data uRI string
dataString = "data:text/html;charset=utf-8," + urlString
case AssetTypes.CSS:
// Escape CSS data
var re = regexp.MustCompile(`\s{2,}`)
result := re.ReplaceAllString(a.Data, ``)
result = strings.ReplaceAll(result, "\n", "")
result = strings.ReplaceAll(result, "\r\n", "")
result = strings.ReplaceAll(result, "\n", "")
result = strings.ReplaceAll(result, "\t", "")
result = strings.ReplaceAll(result, `\`, `\\`)
result = strings.ReplaceAll(result, `"`, `\"`)
result = strings.ReplaceAll(result, `'`, `\'`)
result = strings.ReplaceAll(result, ` {`, `{`)
result = strings.ReplaceAll(result, `: `, `:`)
dataString = fmt.Sprintf("window.wails._.InjectCSS(\"%s\");", result)
}
// Get byte data of the string
bytes := *(*[]byte)(unsafe.Pointer(&dataString))
// Create a strings builder
var cdata strings.Builder
// Set buffer size to 4k
cdata.Grow(4096)
// Convert each byte to hex
for _, b := range bytes {
cdata.WriteString(fmt.Sprintf("0x%x, ", b))
}
return cdata.String()
}
func (a *Asset) Dump() {
fmt.Printf("{ Type: %s, Path: %s, Data: %+v }\n", a.Type, a.Path, a.Data[:10])
}

View File

@@ -0,0 +1,195 @@
package html
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"path/filepath"
"strings"
"github.com/leaanthony/slicer"
"github.com/leaanthony/wailsv2/v2/internal/assetdb"
"golang.org/x/net/html"
)
// AssetBundle is a collection of Assets
type AssetBundle struct {
assets []*Asset
basedirectory string
}
// NewAssetBundle creates a new AssetBundle struct containing
// the given html and all the assets referenced by it
func NewAssetBundle(pathToHTML string) (*AssetBundle, error) {
// Create result
result := &AssetBundle{
basedirectory: filepath.Dir(pathToHTML),
}
err := result.loadAssets(pathToHTML)
if err != nil {
return nil, err
}
return result, nil
}
// loadAssets processes the given html file and loads in
// all referenced assets
func (a *AssetBundle) loadAssets(pathToHTML string) error {
// Save HTML
htmlAsset := &Asset{
Type: AssetTypes.HTML,
Path: filepath.Base(pathToHTML),
}
err := htmlAsset.Load(a.basedirectory)
if err != nil {
return err
}
a.assets = append(a.assets, htmlAsset)
return a.processHTML(htmlAsset.AsString())
}
// Credit to: https://drstearns.github.io/tutorials/tokenizing/
func (a *AssetBundle) processHTML(htmldata string) error {
// Tokenize the html
buf := bytes.NewBufferString(htmldata)
tokenizer := html.NewTokenizer(buf)
for {
//get the next token type
tokenType := tokenizer.Next()
//if it's an error token, we either reached
//the end of the file, or the HTML was malformed
if tokenType == html.ErrorToken {
err := tokenizer.Err()
if err == io.EOF {
//end of the file, break out of the loop
break
}
//otherwise, there was an error tokenizing,
//which likely means the HTML was malformed.
//since this is a simple command-line utility,
//we can just use log.Fatalf() to report the error
//and exit the process with a non-zero status code
return tokenizer.Err()
}
//process the token according to the token type...
if tokenType == html.StartTagToken {
//get the token
token := tokenizer.Token()
//if the name of the element is "title"
if "link" == token.Data {
//the next token should be the page title
tokenType = tokenizer.Next()
//just make sure it's actually a text token
asset := &Asset{}
for _, attr := range token.Attr {
// Favicon
if attr.Key == "rel" && attr.Val == "icon" {
asset.Type = AssetTypes.FAVICON
}
if attr.Key == "href" {
asset.Path = attr.Val
}
// stylesheet
if attr.Key == "rel" && attr.Val == "stylesheet" {
asset.Type = AssetTypes.CSS
}
}
err := asset.Load(a.basedirectory)
if err != nil {
return err
}
a.assets = append(a.assets, asset)
}
if "script" == token.Data {
tokenType = tokenizer.Next()
//just make sure it's actually a text token
asset := &Asset{Type: AssetTypes.JS}
for _, attr := range token.Attr {
if attr.Key == "src" {
asset.Path = attr.Val
break
}
}
err := asset.Load(a.basedirectory)
if err != nil {
return err
}
a.assets = append(a.assets, asset)
}
}
}
return nil
}
// WriteToCFile dumps all the assets to C files in the given directory
func (a *AssetBundle) WriteToCFile(targetDir string) (string, error) {
// Write out the assets.c file
var cdata strings.Builder
// Write header
header := `// assets.c
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL.
// This file was auto-generated. DO NOT MODIFY.
`
cdata.WriteString(header)
// Loop over the Assets
var err error
assetVariables := slicer.String()
var variableName string
for index, asset := range a.assets {
variableName = fmt.Sprintf("%s%d", asset.Type, index)
assetCdata := fmt.Sprintf("const unsigned char %s[]={ %s0x00 };\n", variableName, asset.AsCHexData())
cdata.WriteString(assetCdata)
assetVariables.Add(variableName)
}
if assetVariables.Length() > 0 {
cdata.WriteString(fmt.Sprintf("\nconst char *assets[] = { %s, 0x00 };", assetVariables.Join(", ")))
} else {
cdata.WriteString("\nconst char *assets[] = { 0x00 };")
}
// Save file
assetsFile := filepath.Join(targetDir, "assets.c")
err = ioutil.WriteFile(assetsFile, []byte(cdata.String()), 0600)
if err != nil {
return "", err
}
return assetsFile, nil
}
// ConvertToAssetDB returns an assetdb.AssetDB initialized with
// the items in the AssetBundle
func (a *AssetBundle) ConvertToAssetDB() (*assetdb.AssetDB, error) {
assetdb := assetdb.NewAssetDB()
// Loop over the Assets
for _, asset := range a.assets {
assetdb.AddAsset(asset.Path, []byte(asset.Data))
}
return assetdb, nil
}
func (a *AssetBundle) Dump() {
println("Assets:")
for _, asset := range a.assets {
asset.Dump()
}
}

View File

@@ -0,0 +1,98 @@
package logger
import (
"fmt"
"os"
)
// CustomLogger defines what a user can do with a logger
type CustomLogger interface {
// Writeln writes directly to the output with no log level plus line ending
Writeln(message string) error
// Write writes directly to the output with no log level
Write(message string) error
// Trace level logging. Works like Sprintf.
Trace(format string, args ...interface{}) error
// Debug level logging. Works like Sprintf.
Debug(format string, args ...interface{}) error
// Info level logging. Works like Sprintf.
Info(format string, args ...interface{}) error
// Warning level logging. Works like Sprintf.
Warning(format string, args ...interface{}) error
// Error level logging. Works like Sprintf.
Error(format string, args ...interface{}) error
// Fatal level logging. Works like Sprintf.
Fatal(format string, args ...interface{})
}
// customLogger is a utlility to log messages to a number of destinations
type customLogger struct {
logger *Logger
name string
}
// New creates a new customLogger. You may pass in a number of `io.Writer`s that
// are the targets for the logs
func newcustomLogger(logger *Logger, name string) *customLogger {
result := &customLogger{
name: name,
logger: logger,
}
return result
}
// Writeln writes directly to the output with no log level
// Appends a carriage return to the message
func (l *customLogger) Writeln(message string) error {
return l.logger.Writeln(message)
}
// Write writes directly to the output with no log level
func (l *customLogger) Write(message string) error {
return l.logger.Write(message)
}
// Trace level logging. Works like Sprintf.
func (l *customLogger) Trace(format string, args ...interface{}) error {
format = fmt.Sprintf("%s | %s", l.name, format)
return l.logger.processLogMessage(TRACE, format, args...)
}
// Debug level logging. Works like Sprintf.
func (l *customLogger) Debug(format string, args ...interface{}) error {
format = fmt.Sprintf("%s | %s", l.name, format)
return l.logger.processLogMessage(DEBUG, format, args...)
}
// Info level logging. Works like Sprintf.
func (l *customLogger) Info(format string, args ...interface{}) error {
format = fmt.Sprintf("%s | %s", l.name, format)
return l.logger.processLogMessage(INFO, format, args...)
}
// Warning level logging. Works like Sprintf.
func (l *customLogger) Warning(format string, args ...interface{}) error {
format = fmt.Sprintf("%s | %s", l.name, format)
return l.logger.processLogMessage(WARNING, format, args...)
}
// Error level logging. Works like Sprintf.
func (l *customLogger) Error(format string, args ...interface{}) error {
format = fmt.Sprintf("%s | %s", l.name, format)
return l.logger.processLogMessage(ERROR, format, args...)
}
// Fatal level logging. Works like Sprintf.
func (l *customLogger) Fatal(format string, args ...interface{}) {
format = fmt.Sprintf("%s | %s", l.name, format)
l.logger.processLogMessage(FATAL, format, args...)
os.Exit(1)
}

View File

@@ -0,0 +1,143 @@
package logger
import (
"fmt"
"io"
"os"
"sync"
)
// Logger is a utlility to log messages to a number of destinations
type Logger struct {
writers []io.Writer
logLevel uint8
showLevelInLog bool
lock sync.RWMutex
}
// New creates a new Logger. You may pass in a number of `io.Writer`s that
// are the targets for the logs
func New(writers ...io.Writer) *Logger {
result := &Logger{
logLevel: INFO,
showLevelInLog: true,
}
for _, writer := range writers {
result.AddOutput(writer)
}
return result
}
// Writers gets the log writers
func (l *Logger) Writers() []io.Writer {
return l.writers
}
// CustomLogger creates a new custom logger that prints out a name/id
// before the messages
func (l *Logger) CustomLogger(name string) CustomLogger {
return newcustomLogger(l, name)
}
// HideLogLevel removes the loglevel text from the start of each logged line
func (l *Logger) HideLogLevel() {
l.showLevelInLog = true
}
// SetLogLevel sets the minimum level of logs that will be output
func (l *Logger) SetLogLevel(level uint8) {
l.logLevel = level
}
// AddOutput adds the given `io.Writer` to the list of destinations
// that get logged to
func (l *Logger) AddOutput(writer io.Writer) {
l.writers = append(l.writers, writer)
}
func (l *Logger) write(loglevel uint8, message string) error {
// Don't print logs lower than the current log level
if loglevel < l.logLevel {
return nil
}
// Show log level text if enabled
if l.showLevelInLog {
message = mapLogLevel[loglevel] + message
}
// write out the logs
l.lock.Lock()
for _, writer := range l.writers {
_, err := writer.Write([]byte(message))
if err != nil {
l.lock.Unlock() // Because defer is slow
return err
}
}
l.lock.Unlock()
return nil
}
// writeln appends a newline character to the message before writing
func (l *Logger) writeln(loglevel uint8, message string) error {
return l.write(loglevel, message+"\n")
}
// Writeln writes directly to the output with no log level
// Appends a carriage return to the message
func (l *Logger) Writeln(message string) error {
return l.write(BYPASS, message+"\n")
}
// Write writes directly to the output with no log level
func (l *Logger) Write(message string) error {
return l.write(BYPASS, message)
}
// processLogMessage formats the given message before writing it out
func (l *Logger) processLogMessage(loglevel uint8, format string, args ...interface{}) error {
message := fmt.Sprintf(format, args...)
return l.writeln(loglevel, message)
}
// Trace level logging. Works like Sprintf.
func (l *Logger) Trace(format string, args ...interface{}) error {
return l.processLogMessage(TRACE, format, args...)
}
// CustomTrace returns a custom Logging function that will insert the given name before the message
func (l *Logger) CustomTrace(name string) func(format string, args ...interface{}) {
return func(format string, args ...interface{}) {
format = name + " | " + format
l.processLogMessage(TRACE, format, args...)
}
}
// Debug level logging. Works like Sprintf.
func (l *Logger) Debug(format string, args ...interface{}) error {
return l.processLogMessage(DEBUG, format, args...)
}
// Info level logging. Works like Sprintf.
func (l *Logger) Info(format string, args ...interface{}) error {
return l.processLogMessage(INFO, format, args...)
}
// Warning level logging. Works like Sprintf.
func (l *Logger) Warning(format string, args ...interface{}) error {
return l.processLogMessage(WARNING, format, args...)
}
// Error level logging. Works like Sprintf.
func (l *Logger) Error(format string, args ...interface{}) error {
return l.processLogMessage(ERROR, format, args...)
}
// Fatal level logging. Works like Sprintf.
func (l *Logger) Fatal(format string, args ...interface{}) {
l.processLogMessage(FATAL, format, args...)
os.Exit(1)
}

View File

@@ -0,0 +1,34 @@
package logger
const (
// TRACE level
TRACE uint8 = 0
// DEBUG level logging
DEBUG uint8 = 1
// INFO level logging
INFO uint8 = 2
// WARNING level logging
WARNING uint8 = 4
// ERROR level logging
ERROR uint8 = 8
// FATAL level logging
FATAL uint8 = 16
// BYPASS level logging - does not use a log level
BYPASS uint8 = 255
)
var mapLogLevel = map[uint8]string{
TRACE: "TRACE | ",
DEBUG: "DEBUG | ",
INFO: "INFO | ",
WARNING: "WARN | ",
ERROR: "ERROR | ",
FATAL: "FATAL | ",
BYPASS: "",
}

View File

@@ -0,0 +1,202 @@
package logger
import (
"bytes"
"io/ioutil"
"log"
"os"
"testing"
"github.com/matryer/is"
)
func TestByteBufferLogger(t *testing.T) {
is := is.New(t)
// Create new byte buffer logger
var buf bytes.Buffer
myLogger := New(&buf)
myLogger.SetLogLevel(TRACE)
tests := map[uint8]string{
TRACE: "TRACE | I am a message!\n",
DEBUG: "DEBUG | I am a message!\n",
WARNING: "WARN | I am a message!\n",
INFO: "INFO | I am a message!\n",
ERROR: "ERROR | I am a message!\n",
}
methods := map[uint8]func(string, ...interface{}) error{
TRACE: myLogger.Trace,
DEBUG: myLogger.Debug,
WARNING: myLogger.Warning,
INFO: myLogger.Info,
ERROR: myLogger.Error,
}
for level, expected := range tests {
buf.Reset()
method := methods[level]
// Write message
err := method("I am a message!")
if err != nil {
panic(err)
}
actual := buf.String()
is.Equal(actual, expected)
}
}
func TestCustomLogger(t *testing.T) {
is := is.New(t)
// Create new byte buffer logger
var buf bytes.Buffer
myLogger := New(&buf)
myLogger.SetLogLevel(TRACE)
customLogger := myLogger.CustomLogger("Test")
tests := map[uint8]string{
TRACE: "TRACE | Test | I am a message!\n",
DEBUG: "DEBUG | Test | I am a message!\n",
WARNING: "WARN | Test | I am a message!\n",
INFO: "INFO | Test | I am a message!\n",
ERROR: "ERROR | Test | I am a message!\n",
}
methods := map[uint8]func(string, ...interface{}) error{
TRACE: customLogger.Trace,
DEBUG: customLogger.Debug,
WARNING: customLogger.Warning,
INFO: customLogger.Info,
ERROR: customLogger.Error,
}
for level, expected := range tests {
buf.Reset()
method := methods[level]
// Write message
err := method("I am a message!")
if err != nil {
panic(err)
}
actual := buf.String()
is.Equal(actual, expected)
}
}
func TestWriteln(t *testing.T) {
is := is.New(t)
// Create new byte buffer logger
var buf bytes.Buffer
myLogger := New(&buf)
myLogger.SetLogLevel(DEBUG)
buf.Reset()
// Write message
err := myLogger.Writeln("I am a message!")
if err != nil {
panic(err)
}
actual := buf.String()
is.Equal(actual, "I am a message!\n")
buf.Reset()
// Write message
err = myLogger.Write("I am a message!")
if err != nil {
panic(err)
}
actual = buf.String()
is.Equal(actual, "I am a message!")
}
func TestLogLevel(t *testing.T) {
is := is.New(t)
// Create new byte buffer logger
var buf bytes.Buffer
myLogger := New(&buf)
myLogger.SetLogLevel(ERROR)
tests := map[uint8]string{
TRACE: "",
DEBUG: "",
WARNING: "",
INFO: "",
ERROR: "ERROR | I am a message!\n",
}
methods := map[uint8]func(string, ...interface{}) error{
TRACE: myLogger.Trace,
DEBUG: myLogger.Debug,
WARNING: myLogger.Warning,
INFO: myLogger.Info,
ERROR: myLogger.Error,
}
for level := range tests {
method := methods[level]
// Write message
err := method("I am a message!")
if err != nil {
panic(err)
}
}
actual := buf.String()
is.Equal(actual, "ERROR | I am a message!\n")
}
func TestFileLogger(t *testing.T) {
is := is.New(t)
// Create new byte buffer logger
file, err := ioutil.TempFile(".", "wailsv2test")
if err != nil {
log.Fatal(err)
}
defer os.Remove(file.Name())
myLogger := New(file)
myLogger.SetLogLevel(DEBUG)
// Write message
err = myLogger.Info("I am a message!")
if err != nil {
panic(err)
}
actual, err := ioutil.ReadFile(file.Name())
if err != nil {
panic(err)
}
is.Equal(string(actual), "INFO | I am a message!\n")
}

View File

@@ -0,0 +1,76 @@
package messagedispatcher
import (
"fmt"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/messagedispatcher/message"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
)
type Client interface {
Quit()
NotifyEvent(message string)
CallResult(message string)
SaveFileDialog(title string) string
OpenFileDialog(title string) string
OpenDirectoryDialog(title string) string
WindowSetTitle(title string)
WindowFullscreen()
WindowUnFullscreen()
WindowSetColour(colour string) bool
}
// DispatchClient is what the frontends use to interface with the
// dispatcher
type DispatchClient struct {
id string
logger logger.CustomLogger
bus *servicebus.ServiceBus
// Client
frontend Client
}
func newDispatchClient(id string, frontend Client, logger logger.CustomLogger, bus *servicebus.ServiceBus) *DispatchClient {
return &DispatchClient{
id: id,
frontend: frontend,
logger: logger,
bus: bus,
}
}
// DispatchMessage is called by the front ends. It is passed
// an IPC message, translates it to a more concrete message
// type then publishes it on the service bus.
func (d *DispatchClient) DispatchMessage(incomingMessage string) {
// Parse the message
d.logger.Trace(fmt.Sprintf("Received message: %+v", incomingMessage))
parsedMessage, err := message.Parse(incomingMessage)
if err != nil {
d.logger.Trace("Error: " + err.Error())
return
}
// Save this client id
parsedMessage.ClientID = d.id
d.logger.Trace("I got a parsedMessage: %+v", parsedMessage)
// Check error
if err != nil {
d.logger.Trace("Error: " + err.Error())
// Hrm... what do we do with this?
d.bus.PublishForTarget("generic:message", incomingMessage, d.id)
return
}
// Publish the parsed message
d.bus.PublishForTarget(parsedMessage.Topic, parsedMessage.Data, d.id)
}

View File

@@ -0,0 +1,37 @@
package message
import (
"encoding/json"
"fmt"
)
type CallMessage struct {
Name string `json:"name"`
Args []interface{} `json:"args"`
CallbackID string `json:"callbackID,omitempty"`
}
// callMessageParser does what it says on the tin!
func callMessageParser(message string) (*parsedMessage, error) {
// Sanity check: Call messages must be at least 3 bytes `C{}``
if len(message) < 3 {
return nil, fmt.Errorf("call message was an invalid length")
}
callMessage := new(CallMessage)
m := message[1:]
err := json.Unmarshal([]byte(m), callMessage)
if err != nil {
println(err.Error())
return nil, err
}
topic := "call:invoke"
// Create a new parsed message struct
parsedMessage := &parsedMessage{Topic: topic, Data: callMessage}
return parsedMessage, nil
}

View File

@@ -0,0 +1,47 @@
package message
import "fmt"
import "encoding/json"
type EventMessage struct {
Name string `json:"name"`
Data []interface{} `json:"data"`
}
type OnEventMessage struct {
Name string
Callback func(optionalData ...interface{})
}
// eventMessageParser does what it says on the tin!
func eventMessageParser(message string) (*parsedMessage, error) {
// Sanity check: Event messages must be at least 2 bytes
if len(message) < 3 {
return nil, fmt.Errorf("event message was an invalid length")
}
eventMessage := new(EventMessage)
direction := message[1]
// Switch the event type (with or without data)
switch message[0] {
case 'e':
eventMessage.Name = message[2:]
case 'E':
m := message[2:]
err := json.Unmarshal([]byte(m), eventMessage)
if err != nil {
println(err.Error())
return nil, err
}
}
topic := "event:emit:from:" + string(direction)
// Create a new parsed message struct
parsedMessage := &parsedMessage{Topic: topic, Data: eventMessage}
return parsedMessage, nil
}

View File

@@ -0,0 +1,33 @@
package message
import "fmt"
var logMessageMap = map[byte]string{
'D': "log:debug",
'I': "log:info",
'W': "log:warning",
'E': "log:error",
'F': "log:fatal",
}
// logMessageParser does what it says on the tin!
func logMessageParser(message string) (*parsedMessage, error) {
// Sanity check: Log messages must be at least 2 bytes
if len(message) < 2 {
return nil, fmt.Errorf("log message was an invalid length")
}
// Switch on the log type
messageTopic := logMessageMap[message[1]]
// If the type is invalid, raise error
if messageTopic == "" {
return nil, fmt.Errorf("log message type '%b' invalid", message[1])
}
// Create a new parsed message struct
parsedMessage := &parsedMessage{Topic: messageTopic, Data: message[2:]}
return parsedMessage, nil
}

View File

@@ -0,0 +1,31 @@
package message
import "fmt"
// Parse
type parsedMessage struct {
Topic string
ClientID string
Data interface{}
}
// Map of different message parsers based on the header byte of the message
var messageParsers = map[byte]func(string) (*parsedMessage, error){
'L': logMessageParser,
'R': runtimeMessageParser,
'E': eventMessageParser,
'e': eventMessageParser,
'C': callMessageParser,
'W': windowMessageParser,
}
// Parse will attempt to parse the given message
func Parse(message string) (*parsedMessage, error) {
parseMethod := messageParsers[message[0]]
if parseMethod == nil {
return nil, fmt.Errorf("message type '%b' invalid", message[0])
}
return parseMethod(message)
}

View File

@@ -0,0 +1,36 @@
package message
import "fmt"
// runtimeMessageParser does what it says on the tin!
func runtimeMessageParser(message string) (*parsedMessage, error) {
// Sanity check: Log messages must be at least 2 bytes
if len(message) < 3 {
return nil, fmt.Errorf("runtime message was an invalid length")
}
// Switch on the runtime module type
module := message[1]
switch module {
case 'B':
return processBrowserMessage(message)
}
return nil, fmt.Errorf("unknown message: %s", message)
}
// processBrowserMessage expects messages of the following format:
// RB<METHOD><DATA>
func processBrowserMessage(message string) (*parsedMessage, error) {
method := message[2]
switch method {
case 'U':
// Open URL
url := message[3:]
return &parsedMessage{Topic: "runtime:browser:openurl", Data: url}, nil
}
return nil, fmt.Errorf("unknown browser message: %s", message)
}

View File

@@ -0,0 +1,31 @@
package message
import "fmt"
// windowMessageParser does what it says on the tin!
func windowMessageParser(message string) (*parsedMessage, error) {
// Sanity check: Window messages must be at least 2 bytes
if len(message) < 2 {
return nil, fmt.Errorf("window message was an invalid length")
}
// Extract event type
windowEvent := message[1]
parsedMessage := &parsedMessage{}
// Switch the windowEvent type
switch windowEvent {
// Closed window
case 'C':
parsedMessage.Topic = "quit"
parsedMessage.Data = "Window Closed"
// Unknown event type
default:
return nil, fmt.Errorf("unknown message: %s", message)
}
return parsedMessage, nil
}

View File

@@ -0,0 +1,319 @@
package messagedispatcher
import (
"encoding/json"
"strings"
"sync"
"github.com/leaanthony/wailsv2/v2/internal/crypto"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/messagedispatcher/message"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
)
// Dispatcher translates messages received from the frontend
// and publishes them onto the service bus
type Dispatcher struct {
quitChannel <-chan *servicebus.Message
resultChannel <-chan *servicebus.Message
eventChannel <-chan *servicebus.Message
windowChannel <-chan *servicebus.Message
dialogChannel <-chan *servicebus.Message
running bool
servicebus *servicebus.ServiceBus
logger logger.CustomLogger
// Clients
clients map[string]*DispatchClient
lock sync.RWMutex
}
// New dispatcher. Needs a service bus to send to.
func New(servicebus *servicebus.ServiceBus, logger *logger.Logger) (*Dispatcher, error) {
// Subscribe to call result messages
resultChannel, err := servicebus.Subscribe("call:result")
if err != nil {
return nil, err
}
// Subscribe to event messages
eventChannel, err := servicebus.Subscribe("event:emit")
if err != nil {
return nil, err
}
// Subscribe to quit messages
quitChannel, err := servicebus.Subscribe("quit")
if err != nil {
return nil, err
}
// Subscribe to window messages
windowChannel, err := servicebus.Subscribe("window")
if err != nil {
return nil, err
}
// Subscribe to dialog events
dialogChannel, err := servicebus.Subscribe("dialog:select")
if err != nil {
return nil, err
}
result := &Dispatcher{
servicebus: servicebus,
eventChannel: eventChannel,
logger: logger.CustomLogger("Message Dispatcher"),
clients: make(map[string]*DispatchClient),
resultChannel: resultChannel,
quitChannel: quitChannel,
windowChannel: windowChannel,
dialogChannel: dialogChannel,
}
return result, nil
}
// Start the subsystem
func (d *Dispatcher) Start() error {
d.logger.Trace("Starting")
d.running = true
// Spin off a go routine
go func() {
for d.running {
select {
case <-d.quitChannel:
d.processQuit()
d.running = false
case resultMessage := <-d.resultChannel:
d.processCallResult(resultMessage)
case eventMessage := <-d.eventChannel:
d.processEvent(eventMessage)
case windowMessage := <-d.windowChannel:
d.processWindowMessage(windowMessage)
case dialogMessage := <-d.dialogChannel:
d.processDialogMessage(dialogMessage)
}
}
// Call shutdown
d.shutdown()
}()
return nil
}
func (d *Dispatcher) processQuit() {
d.lock.RLock()
defer d.lock.RUnlock()
for _, client := range d.clients {
client.frontend.Quit()
}
}
func (d *Dispatcher) shutdown() {
d.logger.Trace("Shutdown")
}
// RegisterClient will register the given callback with the dispatcher
// and return a DispatchClient that the caller can use to send messages
func (d *Dispatcher) RegisterClient(client Client) *DispatchClient {
d.lock.Lock()
defer d.lock.Unlock()
// Create ID
id := d.getUniqueID()
d.clients[id] = newDispatchClient(id, client, d.logger, d.servicebus)
return d.clients[id]
}
// RemoveClient will remove the registered client
func (d *Dispatcher) RemoveClient(dc *DispatchClient) {
d.lock.Lock()
defer d.lock.Unlock()
delete(d.clients, dc.id)
}
func (d *Dispatcher) getUniqueID() string {
var uid string
for {
uid = crypto.RandomID()
if d.clients[uid] == nil {
break
}
}
return uid
}
func (d *Dispatcher) processCallResult(result *servicebus.Message) {
target := result.Target()
if target == "" {
// This is an error. Calls are 1:1!
d.logger.Fatal("No target for call result: %+v", result)
}
d.lock.RLock()
client := d.clients[target]
d.lock.RUnlock()
if client == nil {
// This is fatal - unknown target!
d.logger.Fatal("Unknown target for call result: %+v", result)
}
d.logger.Trace("Sending message to client %s: R%s", target, result.Data().(string))
client.frontend.CallResult(result.Data().(string))
}
// processEvent will
func (d *Dispatcher) processEvent(result *servicebus.Message) {
d.logger.Trace("Got event in message dispatcher: %+v", result)
splitTopic := strings.Split(result.Topic(), ":")
eventType := splitTopic[1]
switch eventType {
case "emit":
eventFrom := splitTopic[3]
if eventFrom == "g" {
// This was sent from Go - notify frontend
eventData := result.Data().(*message.EventMessage)
// Unpack event
payload, err := json.Marshal(eventData)
if err != nil {
d.logger.Error("Unable to marshal eventData: %s", err.Error())
return
}
d.lock.RLock()
for _, client := range d.clients {
client.frontend.NotifyEvent(string(payload))
}
d.lock.RUnlock()
}
default:
d.logger.Error("Unknown event type: %s", eventType)
}
}
// processWindowMessage processes messages intended for the window
func (d *Dispatcher) processWindowMessage(result *servicebus.Message) {
d.lock.RLock()
defer d.lock.RUnlock()
splitTopic := strings.Split(result.Topic(), ":")
command := splitTopic[1]
switch command {
case "settitle":
title, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid title for 'window:settitle' : %#v", result.Data())
return
}
// Notify clients
for _, client := range d.clients {
client.frontend.WindowSetTitle(title)
}
case "fullscreen":
// Notify clients
for _, client := range d.clients {
client.frontend.WindowFullscreen()
}
case "unfullscreen":
// Notify clients
for _, client := range d.clients {
client.frontend.WindowUnFullscreen()
}
case "setcolour":
colour, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid colour for 'window:setcolour' : %#v", result.Data())
return
}
// Notify clients
for _, client := range d.clients {
client.frontend.WindowSetColour(colour)
}
default:
d.logger.Error("Unknown window command: %s", command)
}
d.logger.Trace("Got window in message dispatcher: %+v", result)
}
// processDialogMessage processes dialog messages
func (d *Dispatcher) processDialogMessage(result *servicebus.Message) {
splitTopic := strings.Split(result.Topic(), ":")
if len(splitTopic) < 4 {
d.logger.Error("Invalid dialog message : %#v", result.Data())
return
}
command := splitTopic[1]
switch command {
case "select":
dialogType := splitTopic[2]
title := splitTopic[3]
switch dialogType {
case "file":
responseTopic, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid responseTopic for 'dialog:select:file' : %#v", result.Data())
return
}
d.logger.Info("Opening File dialog! responseTopic = %s", responseTopic)
// TODO: Work out what we mean in a multi window environment...
// For now we will just pick the first one
var result string
for _, client := range d.clients {
result = client.frontend.OpenFileDialog(title)
}
// Send dummy response
d.servicebus.Publish(responseTopic, result)
case "filesave":
responseTopic, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid responseTopic for 'dialog:select:filesave' : %#v", result.Data())
return
}
d.logger.Info("Opening Save File dialog! responseTopic = %s", responseTopic)
// TODO: Work out what we mean in a multi window environment...
// For now we will just pick the first one
var result string
for _, client := range d.clients {
result = client.frontend.SaveFileDialog(title)
}
// Send dummy response
d.servicebus.Publish(responseTopic, result)
case "directory":
responseTopic, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid responseTopic for 'dialog:select:directory' : %#v", result.Data())
return
}
d.logger.Info("Opening Directory dialog! responseTopic = %s", responseTopic)
// TODO: Work out what we mean in a multi window environment...
// For now we will just pick the first one
var result string
for _, client := range d.clients {
result = client.frontend.OpenDirectoryDialog(title)
}
// Send dummy response
d.servicebus.Publish(responseTopic, result)
default:
d.logger.Error("Unknown dialog command: %s", command)
}
}
}

View File

@@ -0,0 +1,80 @@
package project
import (
"encoding/json"
"io/ioutil"
"path/filepath"
"runtime"
"strings"
)
// Project holds the data related to a Wails project
type Project struct {
/*** Application Data ***/
Name string `json:"name"`
// Application HTML, JS and CSS filenames
HTML string `json:"html"`
JS string `json:"js"`
CSS string `json:"css"`
BuildCommand string `json:"frontend:build"`
InstallCommand string `json:"frontend:install"`
/*** Internal Data ***/
// The path to the project directory
Path string
// The output filename
OutputFilename string `json:"outputfilename"`
// The type of application. EG: Desktop, Server, etc
OutputType string
// The platform to target
Platform string
}
// Load the project from the current working directory
func Load(projectPath string) (*Project, error) {
// Attempt to load project.json
projectFile := filepath.Join(projectPath, "wails.json")
rawBytes, err := ioutil.ReadFile(projectFile)
if err != nil {
return nil, err
}
// Unmarshal JSON
var result Project
err = json.Unmarshal(rawBytes, &result)
if err != nil {
return nil, err
}
// Fix up our project paths
result.Path = filepath.ToSlash(projectPath) + "/"
result.HTML = filepath.Join(projectPath, result.HTML)
result.JS = filepath.Join(projectPath, result.JS)
result.CSS = filepath.Join(projectPath, result.CSS)
// Create default name if not given
if result.Name == "" {
result.Name = "wailsapp"
}
// Fix up OutputFilename
switch runtime.GOOS {
case "windows":
if !strings.HasSuffix(result.OutputFilename, ".exe") {
result.OutputFilename += ".exe"
}
case "darwin", "linux":
if strings.HasSuffix(result.OutputFilename, ".exe") {
result.OutputFilename = strings.TrimSuffix(result.OutputFilename, ".exe")
}
}
// Return our project data
return &result, nil
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,36 @@
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

@@ -0,0 +1,16 @@
package goruntime
import (
"os"
"testing"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/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

@@ -0,0 +1,105 @@
package goruntime
import (
"fmt"
"github.com/leaanthony/wailsv2/v2/internal/crypto"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
)
// Dialog defines all Dialog related operations
type Dialog interface {
SaveFile(title string) string
SelectFile(title string) string
SelectDirectory(title string) 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,
}
}
// SelectFile prompts the user to select a file
func (r *dialog) SelectFile(title string) string {
// Create unique dialog callback
uniqueCallback := crypto.RandomID()
// Subscribe to the respose channel
responseTopic := "dialog:fileselected:" + uniqueCallback
dialogResponseChannel, err := r.bus.Subscribe(responseTopic)
if err != nil {
fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error())
}
// Publish dialog request
// TODO: Add title to request
r.bus.Publish("dialog:select:file:"+title, responseTopic)
// Wait for result
result := <-dialogResponseChannel
// Delete subscription to response topic
r.bus.UnSubscribe(responseTopic)
return result.Data().(string)
}
// SaveFile prompts the user to select a file to save to
func (r *dialog) SaveFile(title string) string {
// Create unique dialog callback
uniqueCallback := crypto.RandomID()
// Subscribe to the respose channel
responseTopic := "dialog:filesaveselected:" + uniqueCallback
dialogResponseChannel, err := r.bus.Subscribe(responseTopic)
if err != nil {
fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error())
}
// Publish dialog request
// TODO: Add title to request
r.bus.Publish("dialog:select:filesave:"+title, responseTopic)
// Wait for result
result := <-dialogResponseChannel
// Delete subscription to response topic
r.bus.UnSubscribe(responseTopic)
return result.Data().(string)
}
// SelectDirectory prompts the user to select a file
func (r *dialog) SelectDirectory(title string) string {
// Create unique dialog callback
uniqueCallback := crypto.RandomID()
// Subscribe to the respose channel
responseTopic := "dialog:directoryselected:" + uniqueCallback
dialogResponseChannel, err := r.bus.Subscribe(responseTopic)
if err != nil {
fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error())
}
// Publish dialog request
// TODO: Add title to request
r.bus.Publish("dialog:select:directory:"+title, responseTopic)
// Wait for result
var result *servicebus.Message = <-dialogResponseChannel
// Delete subscription to response topic
r.bus.UnSubscribe(responseTopic)
return result.Data().(string)
}

View File

@@ -0,0 +1,43 @@
package goruntime
import (
"github.com/leaanthony/wailsv2/v2/internal/messagedispatcher/message"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
)
// Events defines all events related operations
type Events interface {
On(eventName string, callback func(optionalData ...interface{}))
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 pass through
func (r *event) On(eventName string, callback func(optionalData ...interface{})) {
eventMessage := &message.OnEventMessage{
Name: eventName,
Callback: callback,
}
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

@@ -0,0 +1,28 @@
package goruntime
import "github.com/leaanthony/wailsv2/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
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),
bus: serviceBus,
}
}
// Quit the application
func (r *Runtime) Quit() {
r.bus.Publish("quit", "runtime.Quit()")
}

View File

@@ -0,0 +1,52 @@
package goruntime
import "github.com/leaanthony/wailsv2/v2/internal/servicebus"
// Window defines all Window related operations
type Window interface {
Close()
SetTitle(title string)
Fullscreen()
UnFullscreen()
SetColour(colour string)
}
// 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 string
func (w *window) SetColour(colour string) {
w.bus.Publish("window:setcolour", colour)
}

View File

@@ -0,0 +1,24 @@
{
"env": {
"browser": true,
"es6": true,
"amd": true,
"node": true,
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2016,
"sourceType": "module",
},
"rules": {
"linebreak-style": 0,
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
}

View File

@@ -0,0 +1,22 @@
/* eslint-disable */
module.exports = function (api) {
api.cache(true);
const presets = [
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": {
"version": 3,
"proposals": true
}
}
]
];
return {
presets,
};
}

View File

@@ -0,0 +1,107 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
import { Call } from './calls';
window.backend = {};
/**
Map of this format:
{
packageName: {
structName: {
methodName: {
name: "",
inputs: [
{
type: <type>
}
],
outputs: [
{
type: <type>
}
]
}
}
}
}
*/
export function SetBindings(bindingsMap) {
try {
bindingsMap = JSON.parse(bindingsMap);
} catch (e) {
console.error(e);
}
// Initialise the backend map
window.backend = window.backend || {};
// Iterate package names
Object.keys(bindingsMap).forEach((packageName) => {
// Create inner map if it doesn't exist
window.backend[packageName] = window.backend[packageName] || {};
// Iterate struct names
Object.keys(bindingsMap[packageName]).forEach((structName) => {
// Create inner map if it doesn't exist
window.backend[packageName][structName] = window.backend[packageName][structName] || {};
Object.keys(bindingsMap[packageName][structName]).forEach((methodName) => {
window.backend[packageName][structName][methodName] = function () {
// No timeout by default
var timeout = 0;
// Actual function
function dynamic() {
var args = [].slice.call(arguments);
return Call([packageName, structName, methodName].join('.'), args, timeout);
}
// Allow setting timeout to function
dynamic.setTimeout = function (newTimeout) {
timeout = newTimeout;
};
// Allow getting timeout to function
dynamic.getTimeout = function () {
return timeout;
};
return dynamic;
}();
});
});
});
}
// /**
// * Determines if the given identifier is valid Javascript
// *
// * @param {boolean} name
// * @returns
// */
// function isValidIdentifier(name) {
// // Don't xss yourself :-)
// try {
// new Function('var ' + name);
// return true;
// } catch (e) {
// return false;
// }
// }

View File

@@ -0,0 +1,34 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
import { SendMessage } from 'ipc';
/**
* Opens the given URL in the system browser
*
* @export
* @param {string} url
* @returns
*/
export function OpenURL(url) {
return SendMessage('RBU' + url);
}
/**
* Opens the given filename using the system's default file handler
*
* @export
* @param {sting} filename
* @returns
*/
export function OpenFile(filename) {
return SendMessage('runtime:browser:openfile', filename);
}

View File

@@ -0,0 +1,156 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
import { Debug } from './log';
import { SendMessage } from 'ipc';
var callbacks = {};
/**
* Returns a number from the native browser random function
*
* @returns number
*/
function cryptoRandom() {
var array = new Uint32Array(1);
return window.crypto.getRandomValues(array)[0];
}
/**
* Returns a number using da old-skool Math.Random
* I likes to call it LOLRandom
*
* @returns number
*/
function basicRandom() {
return Math.random() * 9007199254740991;
}
// Pick a random number function based on browser capability
var randomFunc;
if (window.crypto) {
randomFunc = cryptoRandom;
} else {
randomFunc = basicRandom;
}
/**
* Call sends a message to the backend to call the binding with the
* given data. A promise is returned and will be completed when the
* backend responds. This will be resolved when the call was successful
* or rejected if an error is passed back.
* There is a timeout mechanism. If the call doesn't respond in the given
* time (in milliseconds) then the promise is rejected.
*
* @export
* @param {string} name
* @param {string} args
* @param {number=} timeout
* @returns
*/
export function Call(name, args, timeout) {
// Timeout infinite by default
if (timeout == null || timeout == undefined) {
timeout = 0;
}
// Create a promise
return new Promise(function (resolve, reject) {
// Create a unique callbackID
var callbackID;
do {
callbackID = name + '-' + randomFunc();
} while (callbacks[callbackID]);
// Set timeout
if (timeout > 0) {
var timeoutHandle = setTimeout(function () {
reject(Error('Call to ' + name + ' timed out. Request ID: ' + callbackID));
}, timeout);
}
// Store callback
callbacks[callbackID] = {
timeoutHandle: timeoutHandle,
reject: reject,
resolve: resolve
};
try {
const payload = {
name,
args,
callbackID,
};
// Make the call
SendMessage('C' + JSON.stringify(payload));
} catch (e) {
// eslint-disable-next-line
console.error(e);
}
});
}
/**
* Called by the backend to return data to a previously called
* binding invocation
*
* @export
* @param {string} incomingMessage
*/
export function Callback(incomingMessage) {
// Decode the message - Credit: https://stackoverflow.com/a/13865680
//incomingMessage = decodeURIComponent(incomingMessage.replace(/\s+/g, '').replace(/[0-9a-f]{2}/g, '%$&'));
// Parse the message
var message;
try {
message = JSON.parse(incomingMessage);
} catch (e) {
const error = `Invalid JSON passed to callback: ${e.message}. Message: ${incomingMessage}`;
Debug(error);
throw new Error(error);
}
var callbackID = message.callbackid;
var callbackData = callbacks[callbackID];
if (!callbackData) {
const error = `Callback '${callbackID}' not registered!!!`;
console.error(error); // eslint-disable-line
throw new Error(error);
}
clearTimeout(callbackData.timeoutHandle);
delete callbacks[callbackID];
if (message.error) {
callbackData.reject(message.error);
} else {
callbackData.resolve(message.result);
}
}
/**
* SystemCall is used to call wails methods from the frontend
*
* @export
* @param {string} method
* @param {any[]=} data
* @returns
*/
export function SystemCall(method, data) {
return Call('.wails.' + method, data);
}

View File

@@ -0,0 +1,35 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
import { SetBindings } from './bindings';
import { Init } from './main';
// Setup global error handler
window.onerror = function (/*msg, url, lineNo, columnNo, error*/) {
// window.wails.Log.Error('**** Caught Unhandled Error ****');
// window.wails.Log.Error('Message: ' + msg);
// window.wails.Log.Error('URL: ' + url);
// window.wails.Log.Error('Line No: ' + lineNo);
// window.wails.Log.Error('Column No: ' + columnNo);
// window.wails.Log.Error('error: ' + error);
(function () { window.wails.Log.Error(new Error().stack); })();
};
// Initialise the Runtime
Init();
// Load Bindings if they exist
if (window.wailsbindings) {
SetBindings(window.wailsbindings);
}
// Emit loaded event. Leaving this for now. It will show any errors if runtime fails to load.
window.wails.Events.Emit('wails:loaded');

View File

@@ -0,0 +1,202 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
import { Error } from './log';
import { SendMessage } from 'ipc';
// Defines a single listener with a maximum number of times to callback
/**
* The Listener class defines a listener! :-)
*
* @class Listener
*/
class Listener {
/**
* Creates an instance of Listener.
* @param {function} callback
* @param {number} maxCallbacks
* @memberof Listener
*/
constructor(callback, maxCallbacks) {
// Default of -1 means infinite
maxCallbacks = maxCallbacks || -1;
// Callback invokes the callback with the given data
// Returns true if this listener should be destroyed
this.Callback = (data) => {
callback.apply(null, data);
// If maxCallbacks is infinite, return false (do not destroy)
if (maxCallbacks === -1) {
return false;
}
// Decrement maxCallbacks. Return true if now 0, otherwise false
maxCallbacks -= 1;
return maxCallbacks === 0;
};
}
}
var eventListeners = {};
/**
* Registers an event listener that will be invoked `maxCallbacks` times before being destroyed
*
* @export
* @param {string} eventName
* @param {function} callback
* @param {number} maxCallbacks
*/
export function OnMultiple(eventName, callback, maxCallbacks) {
eventListeners[eventName] = eventListeners[eventName] || [];
const thisListener = new Listener(callback, maxCallbacks);
console.log('Pushing event listener: ' + eventName);
eventListeners[eventName].push(thisListener);
}
/**
* Registers an event listener that will be invoked every time the event is emitted
*
* @export
* @param {string} eventName
* @param {function} callback
*/
export function On(eventName, callback) {
OnMultiple(eventName, callback);
}
/**
* Registers an event listener that will be invoked once then destroyed
*
* @export
* @param {string} eventName
* @param {function} callback
*/
export function Once(eventName, callback) {
OnMultiple(eventName, callback, 1);
}
/**
* Notify informs frontend listeners that an event was emitted with the given data
*
* @export
* @param {string} encoded notification message
*/
export function Notify(notifyMessage) {
// Parse the message
var message;
try {
message = JSON.parse(notifyMessage);
} catch (e) {
const error = 'Invalid JSON passed to Notify: ' + notifyMessage;
throw new Error(error);
}
var eventName = message.name;
// Check if we have any listeners for this event
if (eventListeners[eventName]) {
// Keep a list of listener indexes to destroy
const newEventListenerList = eventListeners[eventName].slice();
// Iterate listeners
for (let count = 0; count < eventListeners[eventName].length; count += 1) {
// Get next listener
const listener = eventListeners[eventName][count];
var data = message.data;
// Do the callback
const destroy = listener.Callback(data);
if (destroy) {
// if the listener indicated to destroy itself, add it to the destroy list
newEventListenerList.splice(count, 1);
}
}
// Update callbacks with new list of listners
eventListeners[eventName] = newEventListenerList;
}
}
/**
* Emit an event with the given name and data
*
* @export
* @param {string} eventName
*/
export function Emit(eventName) {
// Calculate the data
if (arguments.length > 1) {
// Notify backend
const payload = {
name: eventName,
data: [].slice.apply(arguments).slice(1),
};
SendMessage('Ej' + JSON.stringify(payload));
} else {
SendMessage('ej' + eventName);
}
}
// Callbacks for the heartbeat calls
const heartbeatCallbacks = {};
/**
* Heartbeat emits the event `eventName`, every `timeInMilliseconds` milliseconds until
* the event is acknowledged via `Event.Acknowledge`. Once this happens, `callback` is invoked ONCE
*
* @export
* @param {string} eventName
* @param {number} timeInMilliseconds
* @param {function} callback
*/
export function Heartbeat(eventName, timeInMilliseconds, callback) {
// Declare interval variable
let interval = null;
// Setup callback
function dynamicCallback() {
// Kill interval
clearInterval(interval);
// Callback
callback();
}
// Register callback
heartbeatCallbacks[eventName] = dynamicCallback;
// Start emitting the event
interval = setInterval(function () {
Emit(eventName);
}, timeInMilliseconds);
}
/**
* Acknowledges a heartbeat event by name
*
* @export
* @param {string} eventName
*/
export function Acknowledge(eventName) {
// If we are waiting for acknowledgement for this event type
if (heartbeatCallbacks[eventName]) {
// Acknowledge!
heartbeatCallbacks[eventName]();
} else {
throw new Error(`Cannot acknowledge unknown heartbeat '${eventName}'`);
}
}

View File

@@ -0,0 +1,76 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
import { SendMessage } from 'ipc';
/**
* Sends a log message to the backend with the given level + message
*
* @param {string} level
* @param {string} message
*/
function sendLogMessage(level, message) {
// Log Message format:
// l[type][message]
SendMessage('L' + level + message);
}
/**
* Log the given debug message with the backend
*
* @export
* @param {string} message
*/
export function Debug(message) {
sendLogMessage('D', message);
}
/**
* Log the given info message with the backend
*
* @export
* @param {string} message
*/
export function Info(message) {
sendLogMessage('I', message);
}
/**
* Log the given warning message with the backend
*
* @export
* @param {string} message
*/
export function Warning(message) {
sendLogMessage('W', message);
}
/**
* Log the given error message with the backend
*
* @export
* @param {string} message
*/
export function Error(message) {
sendLogMessage('E', message);
}
/**
* Log the given fatal message with the backend
*
* @export
* @param {string} message
*/
export function Fatal(message) {
sendLogMessage('F', message);
}

View File

@@ -0,0 +1,47 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
import * as Log from './log';
import * as Browser from './browser';
import { On, OnMultiple, Emit, Notify, Heartbeat, Acknowledge } from './events';
import { Callback } from './calls';
import { AddScript, InjectCSS } from './utils';
import { AddIPCListener } from 'ipc';
import * as Platform from 'platform';
export function Init() {
// Backend is where the Go struct wrappers get bound to
window.backend = {};
// Initialise global if not already
window.wails = {
System: Platform.System,
Log,
Browser,
Events: {
On,
OnMultiple,
Emit,
Heartbeat,
Acknowledge,
},
_: {
Callback,
Notify,
AddScript,
InjectCSS,
Init,
AddIPCListener
}
};
// Do platform specific Init
Platform.Init();
}

View File

@@ -0,0 +1,182 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
import { Init } from './main';
import { SetBindings } from './bindings';
function init() {
// Bridge object
window.wailsbridge = {
reconnectOverlay: null,
reconnectTimer: 300,
wsURL: 'ws://' + window.location.hostname + ':8080/ws',
connectionState: null,
config: {},
websocket: null,
callback: null,
overlayHTML:
'<div class="wails-reconnect-overlay"><div class="wails-reconnect-overlay-content"><div class="wails-reconnect-overlay-title">Wails Bridge</div><br><div class="wails-reconnect-overlay-loadingspinner"></div><br><div id="wails-reconnect-overlay-message">Waiting for backend</div></div></div>',
overlayCSS:
'.wails-reconnect-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.6);font-family:sans-serif;display:none;z-index:999999}.wails-reconnect-overlay-content{padding:20px 30px;text-align:center;width:20em;position:relative;height:14em;border-radius:1em;margin:5% auto 0;background-color:#fff;box-shadow:1px 1px 20px 3px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC8AAAAuCAMAAACPpbA7AAAAqFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQAAAAAAAAEBAQAAAAAAAAAAAAEBAQEBAQDAwMBAQEAAAABAQEAAAAAAAAAAAABAQEAAAAAAAACAgICAgIBAQEAAAAAAAAAAAAAAAAAAAAAAAABAQEAAAACAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBQWKCj6oAAAAN3RSTlMALiIqDhkGBAswJjP0GxP6NR4W9/ztjRDMhWU50G9g5eHXvbZ9XEI9xZTcqZl2aldKo55QwoCvZUgzhAAAAs9JREFUSMeNleeWqjAUhU0BCaH3Itiw9zKT93+zG02QK1hm/5HF+jzZJ6fQe6cyXE+jg9X7o9wxuylIIf4Tv2V3+bOrEXnf8dwQ/KQIGDN2/S+4OmVCVXL/ScBnfibxURqIByP/hONE8r8T+bDMlQ98KSl7Y8hzjpS8v1qtDh8u5f8KQpGpfnPPhqG8JeogN37Hq9eaN2xRhIwAaGnvws8F1ShxqK5ob2twYi1FAMD4rXsYtnC/JEiRbl4cUrCWhnMCLRFemXezXbb59QK4WASOsm6n2W1+4CBT2JmtzQ6fsrbGubR/NFbd2g5Y179+5w/GEHaKsHjYCet7CgrXU3txarNC7YxOVJtIj4/ERzMdZfzc31hp+8cD6eGILgarZY9uZ12hAs03vfBD9C171gS5Omz7OcvxALQIn4u8RRBBBcsi9WW2woO9ipLgfzpYlggg3ZRdROUC8KT7QLqq3W9KB5BbdFVg4929kdwp6+qaZnMCCNBdj+NyN1W885Ry/AL3D4AQbsVV4noCiM/C83kyYq80XlDAYQtralOiDzoRAHlotWl8q2tjvYlOgcg1A8jEApZa+C06TBdAz2Qv0wu11I/zZOyJQ6EwGez2P2b8PIQr1hwwnAZsAxwA4UAYOyXUxM/xp6tHAn4GUmPGM9R28oVxgC0e/zQJJI6DyhyZ1r7uzRQhpcW7x7vTaWSzKSG6aep77kroTEl3U81uSVaUTtgEINfC8epx+Q4F9SpplHG84Ek6m4RAq9/TLkOBrxyeuddZhHvGIp1XXfFy3Z3vtwNblKGiDn+J+92vwwABHghj7HnzlS1H5kB49AZvdGCFgiBPq69qfXPr3y++yilF0ON4R8eR7spAsLpZ95NqAW5tab1c4vkZm6aleajchMwYTdILQQTwE2OV411ZM9WztDjPql12caBi6gDpUKmDd4U1XNdQxZ4LIXQ5/Tr4P7I9tYcFrDK3AAAAAElFTkSuQmCC);background-repeat:no-repeat;background-position:center}.wails-reconnect-overlay-title{font-size:2em}.wails-reconnect-overlay-message{font-size:1.3em}.wails-reconnect-overlay-loadingspinner{pointer-events:none;width:2.5em;height:2.5em;border:.4em solid transparent;border-color:#3E67EC #eee #eee;border-radius:50%;animation:loadingspin 1s linear infinite;margin:auto;padding:2.5em}@keyframes loadingspin{100%{transform:rotate(360deg)}}',
log: function (message) {
// eslint-disable-next-line
console.log(
'%c wails bridge %c ' + message + ' ',
'background: #aa0000; color: #fff; border-radius: 3px 0px 0px 3px; padding: 1px; font-size: 0.7rem',
'background: #009900; color: #fff; border-radius: 0px 3px 3px 0px; padding: 1px; font-size: 0.7rem'
);
}
};
}
// Adapted from webview - thanks zserge!
function injectCSS(css) {
var elem = document.createElement('style');
elem.setAttribute('type', 'text/css');
if (elem.styleSheet) {
elem.styleSheet.cssText = css;
} else {
elem.appendChild(document.createTextNode(css));
}
var head = document.head || document.getElementsByTagName('head')[0];
head.appendChild(elem);
}
// Creates a node in the Dom
function createNode(parent, elementType, id, className, content) {
var d = document.createElement(elementType);
if (id) {
d.id = id;
}
if (className) {
d.className = className;
}
if (content) {
d.innerHTML = content;
}
parent.appendChild(d);
return d;
}
// Sets up the overlay
function setupOverlay() {
var body = document.body;
var wailsBridgeNode = createNode(body, 'div', 'wails-bridge');
wailsBridgeNode.innerHTML = window.wailsbridge.overlayHTML;
// Inject the overlay CSS
injectCSS(window.wailsbridge.overlayCSS);
}
// Start the Wails Bridge
function startBridge() {
// Setup the overlay
setupOverlay();
window.wailsbridge.websocket = null;
window.wailsbridge.connectTimer = null;
window.wailsbridge.reconnectOverlay = document.querySelector(
'.wails-reconnect-overlay'
);
window.wailsbridge.connectionState = 'disconnected';
// Shows the overlay
function showReconnectOverlay() {
window.wailsbridge.reconnectOverlay.style.display = 'block';
}
// Hides the overlay
function hideReconnectOverlay() {
window.wailsbridge.reconnectOverlay.style.display = 'none';
}
// Handles incoming websocket connections
function handleConnect() {
window.wailsbridge.log('Connected to backend');
hideReconnectOverlay();
clearInterval(window.wailsbridge.connectTimer);
window.wailsbridge.websocket.onclose = handleDisconnect;
window.wailsbridge.websocket.onmessage = handleMessage;
window.wailsbridge.connectionState = 'connected';
}
// Handles websocket disconnects
function handleDisconnect() {
window.wailsbridge.log('Disconnected from backend');
window.wailsbridge.websocket = null;
window.wailsbridge.connectionState = 'disconnected';
showReconnectOverlay();
connect();
}
// Try to connect to the backend every 300ms (default value).
// Change this value in the main wailsbridge object.
function connect() {
window.wailsbridge.connectTimer = setInterval(function () {
if (window.wailsbridge.websocket == null) {
window.wailsbridge.websocket = new WebSocket(window.wailsbridge.wsURL);
window.wailsbridge.websocket.onopen = handleConnect;
window.wailsbridge.websocket.onerror = function (e) {
e.stopImmediatePropagation();
e.stopPropagation();
e.preventDefault();
window.wailsbridge.websocket = null;
return false;
};
}
}, window.wailsbridge.reconnectTimer);
}
function handleMessage(message) {
switch (message.data[0]) {
case 'e':
case 'E':
window.wails._.Notify(message.data.slice(1));
break;
case 'R':
window.wails._.Callback(message.data.slice(1));
break;
default:
window.wails.Log.Error('Unknown message type received: ' + message.data[0]);
}
}
// Start by showing the overlay...
showReconnectOverlay();
// ...and attempt to connect
connect();
}
function start() {
// Set up the bridge
init();
// Start Bridge
startBridge();
// Load bindings
window.wailspreinit = function () {
if (window.wailsbindings) {
SetBindings(window.wailsbindings);
}
};
Init();
// Save the binding script
window.SetBindings = SetBindings;
}
// Start your engines!
start();

View File

@@ -0,0 +1,38 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
import { Emit } from './events';
export function AddScript(js, callbackID) {
var script = document.createElement('script');
script.text = js;
document.body.appendChild(script);
if (callbackID) {
Emit(callbackID);
}
}
// Adapted from webview - thanks zserge!
export function InjectCSS(css) {
try {
var elem = document.createElement('style');
elem.setAttribute('type', 'text/css');
if (elem.styleSheet) {
elem.styleSheet.cssText = css;
} else {
elem.appendChild(document.createTextNode(css));
}
var head = document.head || document.getElementsByTagName('head')[0];
head.appendChild(elem);
} catch (e) {
console.log(e);
}
}

View File

@@ -0,0 +1,41 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
import * as Platform from 'platform';
// IPC Listeners
var listeners = [];
/**
* Adds a listener to IPC messages
* @param {function} callback
*/
export function AddIPCListener(callback) {
listeners.push(callback);
}
/**
* SendMessage sends the given message to the backend
*
* @param {string} message
*/
export function SendMessage(message) {
// Call Platform specific invoke method
Platform.SendMessage(message);
// Also send to listeners
if (listeners.length > 0) {
for (var i = 0; i < listeners.length; i++) {
listeners[i](message);
}
}
}

View File

@@ -0,0 +1,41 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
/**
* Initialises platform specific code
*/
export const System = {
Platform: "linux",
AppType: "desktop"
}
export function SendMessage(message) {
window.webkit.messageHandlers.external.postMessage(message);
}
export function Init() {
// Setup drag handler
// Based on code from: https://github.com/patr0nus/DeskGap
window.addEventListener('mousedown', function (e) {
var currentElement = e.target;
while (currentElement != null) {
if (currentElement.hasAttribute('data-wails-no-drag')) {
break;
} else if (currentElement.hasAttribute('data-wails-drag')) {
window.webkit.messageHandlers.windowDrag.postMessage(null);
break;
}
currentElement = currentElement.parentElement;
}
});
}

5988
v2/internal/runtime/js/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
{
"name": "wails-runtime",
"version": "1.0.0",
"description": "The Javascript Wails Runtime",
"main": "index.js",
"scripts": {
"build:desktop": "./node_modules/.bin/eslint core/ && ./node_modules/.bin/webpack --env desktop --colors",
"build:server": "./node_modules/.bin/eslint core/ && ./node_modules/.bin/webpack --env server --colors",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/wailsapp/runtime.git"
},
"keywords": [
"Wails",
"Go",
"Javascript",
"Runtime"
],
"browserslist": [
"> 5%",
"IE 9"
],
"author": "Lea Anthony <lea.anthony@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/wailsapp/runtime/issues"
},
"homepage": "https://github.com/wailsapp/runtime#readme",
"devDependencies": {
"@babel/cli": "^7.7.7",
"@babel/core": "^7.7.7",
"@babel/plugin-transform-object-assign": "^7.7.4",
"@babel/preset-env": "^7.7.7",
"babel-loader": "^8.0.6",
"babel-preset-minify": "^0.5.1",
"core-js": "^3.6.1",
"eslint": "^6.8.0",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10"
}
}

View File

@@ -0,0 +1 @@
77a32d4461a2cad598edfd551fe64dcd

View File

@@ -0,0 +1 @@
bridge.js

View File

@@ -0,0 +1,3 @@
# Wails Runtime
This module is the Javascript runtime library for the [Wails](https://wails.app) framework. It is intended to be installed as part of a [Wails](https://wails.app) project, not a standalone module.

View File

@@ -0,0 +1,37 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
/**
* Opens the given URL in the system browser
*
* @export
* @param {string} url
* @returns
*/
function OpenURL(url) {
return window.wails.Browser.OpenURL(url);
}
/**
* Opens the given filename using the system's default file handler
*
* @export
* @param {sting} filename
* @returns
*/
function OpenFile(filename) {
return window.wails.Browser.OpenFile(filename);
}
module.exports = {
OpenURL: OpenURL,
OpenFile: OpenFile
};

View File

@@ -0,0 +1,90 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
/**
* Registers an event listener that will be invoked `maxCallbacks` times before being destroyed
*
* @export
* @param {string} eventName
* @param {function} callback
* @param {number} maxCallbacks
*/
function OnMultiple(eventName, callback, maxCallbacks) {
window.wails.Events.OnMultiple(eventName, callback, maxCallbacks);
}
/**
* Registers an event listener that will be invoked every time the event is emitted
*
* @export
* @param {string} eventName
* @param {function} callback
*/
function On(eventName, callback) {
OnMultiple(eventName, callback);
}
/**
* Registers an event listener that will be invoked once then destroyed
*
* @export
* @param {string} eventName
* @param {function} callback
*/
function Once(eventName, callback) {
OnMultiple(eventName, callback, 1);
}
/**
* Emit an event with the given name and data
*
* @export
* @param {string} eventName
*/
function Emit(eventName) {
var args = [eventName].slice.call(arguments);
return window.wails.Events.Emit.apply(null, args);
}
/**
* Heartbeat emits the event `eventName`, every `timeInMilliseconds` milliseconds until
* the event is acknowledged via `Event.Acknowledge`. Once this happens, `callback` is invoked ONCE
*
* @export
* @param {string} eventName
* @param {number} timeInMilliseconds
* @param {function} callback
*/
function Heartbeat(eventName, timeInMilliseconds, callback) {
window.wails.Events.Heartbeat(eventName, timeInMilliseconds, callback);
}
/**
* Acknowledges a heartbeat event by name
*
* @export
* @param {string} eventName
*/
function Acknowledge(eventName) {
return window.wails.Events.Acknowledge(eventName);
}
module.exports = {
OnMultiple: OnMultiple,
On: On,
Once: Once,
Emit: Emit,
Heartbeat: Heartbeat,
Acknowledge: Acknowledge
};

View File

@@ -0,0 +1,21 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
/**
* Initialises the Wails runtime
*
* @param {function} callback
*/
function Init(callback) {
window.wails._.Init(callback);
}
module.exports = Init;

View File

@@ -0,0 +1,70 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
/**
* Log the given debug message with the backend
*
* @export
* @param {string} message
*/
function Debug(message) {
window.wails.Log.Debug(message);
}
/**
* Log the given info message with the backend
*
* @export
* @param {string} message
*/
function Info(message) {
window.wails.Log.Info(message);
}
/**
* Log the given warning message with the backend
*
* @export
* @param {string} message
*/
function Warning(message) {
window.wails.Log.Warning(message);
}
/**
* Log the given error message with the backend
*
* @export
* @param {string} message
*/
function Error(message) {
window.wails.Log.Error(message);
}
/**
* Log the given fatal message with the backend
*
* @export
* @param {string} message
*/
function Fatal(message) {
window.wails.Log.Fatal(message);
}
module.exports = {
Debug: Debug,
Info: Info,
Warning: Warning,
Error: Error,
Fatal: Fatal
};

View File

@@ -0,0 +1,22 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
const Log = require('./log');
const Browser = require('./browser');
const Events = require('./events');
const Init = require('./init');
module.exports = {
Log: Log,
Browser: Browser,
Events: Events,
Init: Init
};

View File

@@ -0,0 +1,28 @@
{
"name": "@wailsapp/runtime",
"version": "1.0.10",
"description": "Wails Javascript runtime library",
"main": "main.js",
"types": "runtime.d.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/wailsapp/wails.git"
},
"keywords": [
"Wails",
"Javascript",
"Go"
],
"author": "Lea Anthony <lea.anthony@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/wailsapp/wails/issues"
},
"homepage": "https://github.com/wailsapp/wails#readme",
"devDependencies": {
"dts-gen": "^0.5.8"
}
}

View File

@@ -0,0 +1,26 @@
export = wailsapp__runtime;
declare const wailsapp__runtime: {
Browser: {
OpenFile(filename: string): Promise<any>;
OpenURL(url: string): Promise<any>;
};
Events: {
Acknowledge(eventName: string): void;
Emit(eventName: string): void;
Heartbeat(eventName: string, timeInMilliseconds: number, callback: () => void): void;
On(eventName: string, callback: () => void): void;
OnMultiple(eventName: string, callback: () => void, maxCallbacks: number): void;
Once(eventName: string, callback: () => void): void;
};
Init(callback: () => void): void;
Log: {
Debug(message: string): void;
Error(message: string): void;
Fatal(message: string): void;
Info(message: string): void;
Warning(message: string): void;
};
};

View File

@@ -0,0 +1,52 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
// IPC Listeners
var listeners = [];
/**
* Adds a listener to IPC messages
* @param {function} callback
*/
export function AddIPCListener(callback) {
listeners.push(callback);
}
/**
* Invoke sends the given message to the backend
*
* @param {string} message
*/
function Invoke(message) {
if (window.wailsbridge && window.wailsbridge.websocket) {
window.wailsbridge.websocket.send(JSON.stringify(message));
} else {
console.log('Invoke called with: ' + message + ' but no runtime is available');
}
// Also send to listeners
if (listeners.length > 0) {
for (var i = 0; i < listeners.length; i++) {
listeners[i](message);
}
}
}
/**
* Sends a message to the backend based on the given type, payload and callbackID
*
* @export
* @param {string} message
*/
export function SendMessage(message) {
Invoke(message);
}

View File

@@ -0,0 +1,21 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
/**
* Initialises platform specific code
*/
export const System = {
Platform: "linux",
AppType: "server"
}
export function Init() { }

View File

@@ -0,0 +1,4 @@
/* eslint-disable */
module.exports = (env) => {
return require(`./webpack.${env}.js`);
};

View File

@@ -0,0 +1,51 @@
/* eslint-disable */
const path = require('path');
const platform = process.env.WAILSPLATFORM;
if (!platform) {
console.error("FATAL: Environment variable WAILSPLATFORM not set!");
process.exit(1);
}
module.exports = {
entry: './core/desktop',
mode: 'production',
output: {
path: path.resolve(__dirname, '..', 'assets'),
filename: 'desktop.js',
library: 'Wails'
},
resolve: {
alias: {
ipc$: path.resolve(__dirname, 'desktop/ipc.js'),
platform$: path.resolve(__dirname, `desktop/${platform}.js`)
}
},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
plugins: ['@babel/plugin-transform-object-assign'],
presets: [
[
'@babel/preset-env',
{
'useBuiltIns': 'entry',
'corejs': {
'version': 3,
'proposals': true
}
}
]
]
}
}
}
]
}
};

View File

@@ -0,0 +1,54 @@
/* eslint-disable */
const path = require('path');
const platform = process.env.WAILSPLATFORM;
if (!platform) {
console.error("FATAL: Environment variable WAILSPLATFORM not set!");
process.exit(1);
}
module.exports = {
entry: './core/server',
mode: 'production',
output: {
path: path.resolve(__dirname, '..', 'assets'),
filename: 'server.js',
library: 'Wails'
},
resolve: {
alias: {
ipc$: path.resolve(__dirname, 'server/ipc.js'),
platform$: path.resolve(__dirname, `server/${platform}.js`)
}
},
module: {
rules: [
{
test: /\.m?js$/,
include: [
path.resolve(__dirname, "server"),
],
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
plugins: ['@babel/plugin-transform-object-assign'],
presets: [
[
'@babel/preset-env',
{
'useBuiltIns': 'entry',
'corejs': {
'version': 3,
'proposals': true
}
}
]
]
}
}
}
]
}
};

View File

@@ -0,0 +1,43 @@
package servicebus
// Message is a service bus message that contains a
// topic and data
type Message struct {
topic string
data interface{}
target string
}
// NewMessage creates a new message with the given
// topic and data
func NewMessage(topic string, data interface{}) *Message {
return &Message{
topic: topic,
data: data,
}
}
// NewMessageForTarget creates a new message with the given
// topic and data
func NewMessageForTarget(topic string, data interface{}, target string) *Message {
return &Message{
topic: topic,
data: data,
target: target,
}
}
// Topic returns the message topic
func (m *Message) Topic() string {
return m.topic
}
// Data returns the message data
func (m *Message) Data() interface{} {
return m.data
}
// Target returns the message Target
func (m *Message) Target() string {
return m.target
}

View File

@@ -0,0 +1,192 @@
package servicebus
import (
"fmt"
"strings"
"sync"
"github.com/leaanthony/wailsv2/v2/internal/logger"
)
// ServiceBus is a messaging bus for Wails applications
type ServiceBus struct {
listeners map[string][]chan *Message
messageQueue chan *Message
quitChannel chan struct{}
wg sync.WaitGroup
lock sync.RWMutex
closed bool
debug bool
logger logger.CustomLogger
}
// New creates a new ServiceBus
// The internal message queue is set to 100 messages
// Listener queues are set to 10
func New(logger *logger.Logger) *ServiceBus {
return &ServiceBus{
listeners: make(map[string][]chan *Message),
messageQueue: make(chan *Message, 100),
quitChannel: make(chan struct{}, 1),
logger: logger.CustomLogger("Service Bus"),
}
}
// dispatch the given message to the listeners
func (s *ServiceBus) dispatchMessage(message *Message) {
// Lock to prevent additions to the listeners
s.lock.RLock()
defer s.lock.RUnlock()
// Iterate over listener's topics
for topic := range s.listeners {
// If the topic matches
if strings.HasPrefix(message.Topic(), topic) {
// Iterate over the listeners
for _, callback := range s.listeners[topic] {
// Process the message
callback <- message
}
}
}
}
// Debug puts the service bus into debug mode.
func (s *ServiceBus) Debug() {
s.debug = true
}
// Start the service bus
func (s *ServiceBus) Start() error {
s.logger.Trace("Starting")
// Prevent starting when closed
if s.closed {
return fmt.Errorf("cannot call start on closed servicebus")
}
// We run in a different thread
go func() {
quit := false
s.wg.Add(1)
// Loop until we get a quit message
for !quit {
select {
// Listen for messages
case message := <-s.messageQueue:
// Log message if in debug mode
if s.debug {
s.logger.Trace("Got message: { Topic: %s, Interface: %#v }", message.Topic(), message.Data())
}
// Dispatch message
s.dispatchMessage(message)
// Listen for quit messages
case <-s.quitChannel:
quit = true
}
}
// Indicate we have shut down
s.wg.Done()
}()
return nil
}
// Stop the service bus
func (s *ServiceBus) Stop() error {
// Prevent subscribing when closed
if s.closed {
return fmt.Errorf("cannot call stop on closed servicebus")
}
s.closed = true
// Send quit message
s.quitChannel <- struct{}{}
// Wait for dispatcher to stop
s.wg.Wait()
// Close down subscriber channels
s.lock.Lock()
defer s.lock.Unlock()
for _, subscribers := range s.listeners {
for _, channel := range subscribers {
close(channel)
}
}
// Close message queue
close(s.messageQueue)
s.logger.Trace("Stopped")
return nil
}
// UnSubscribe removes the listeners for the given topic (Use with caution!)
func (s *ServiceBus) UnSubscribe(topic string) {
// Prevent any reads or writes to the listeners whilst
// we create a new one
s.lock.Lock()
defer s.lock.Unlock()
s.listeners[topic] = nil
}
// Subscribe is used to register a listener's interest in a topic
func (s *ServiceBus) Subscribe(topic string) (<-chan *Message, error) {
// Prevent subscribing when closed
if s.closed {
return nil, fmt.Errorf("cannot call subscribe on closed servicebus")
}
// Prevent any reads or writes to the listeners whilst
// we create a new one
s.lock.Lock()
defer s.lock.Unlock()
// Append the new listener
listener := make(chan *Message, 10)
s.listeners[topic] = append(s.listeners[topic], listener)
return (<-chan *Message)(listener), nil
}
// Publish sends the given message on the service bus
func (s *ServiceBus) Publish(topic string, data interface{}) error {
// Prevent publish when closed
if s.closed {
return fmt.Errorf("cannot call publish on closed servicebus")
}
message := NewMessage(topic, data)
s.messageQueue <- message
return nil
}
// PublishForTarget sends the given message on the service bus for the given target
func (s *ServiceBus) PublishForTarget(topic string, data interface{}, target string) error {
// Prevent publish when closed
if s.closed {
return fmt.Errorf("cannot call publish on closed servicebus")
}
message := NewMessageForTarget(topic, data, target)
s.messageQueue <- message
return nil
}

View File

@@ -0,0 +1,230 @@
package servicebus
import (
"sync"
"testing"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/matryer/is"
)
type Person interface {
FullName() string
}
type person struct {
Firstname string
Lastname string
}
func newPerson(firstname string, lastname string) *person {
result := &person{}
result.Firstname = firstname
result.Lastname = lastname
return result
}
func (p *person) FullName() string {
return p.Firstname + " " + p.Lastname
}
func TestSingleTopic(t *testing.T) {
is := is.New(t)
var expected string = "I am a message!"
var actual string
var wg sync.WaitGroup
// Create new bus
bus := New(logger.New())
messageChannel, _ := bus.Subscribe("hello")
wg.Add(1)
go func() {
message := <-messageChannel
actual = message.Data().(string)
wg.Done()
}()
bus.Start()
bus.Publish("hello", "I am a message!")
wg.Wait()
bus.Stop()
is.Equal(actual, expected)
}
func TestMultipleTopics(t *testing.T) {
is := is.New(t)
var hello string
var world string
var expected string = "Hello World!"
var wg sync.WaitGroup
// Create new bus
bus := New(logger.New())
// Create subscriptions
helloChannel, _ := bus.Subscribe("hello")
worldChannel, _ := bus.Subscribe("world")
wg.Add(1)
go func() {
counter := 2
for counter > 0 {
select {
case helloMessage := <-helloChannel:
hello = helloMessage.Data().(string)
counter--
case worldMessage := <-worldChannel:
world = worldMessage.Data().(string)
counter--
}
}
wg.Done()
}()
bus.Start()
bus.Publish("hello", "Hello ")
bus.Publish("world", "World!")
wg.Wait()
bus.Stop()
is.Equal(hello+world, expected)
}
func TestSingleTopicWildcard(t *testing.T) {
is := is.New(t)
var expected string = "I am a message!"
var actual string
var wg sync.WaitGroup
// Create new bus
bus := New(logger.New())
messageChannel, _ := bus.Subscribe("hello")
wg.Add(1)
go func() {
message := <-messageChannel
actual = message.Data().(string)
wg.Done()
}()
bus.Start()
bus.Publish("hello:wildcard:test", "I am a message!")
wg.Wait()
bus.Stop()
is.Equal(actual, expected)
}
func TestMultipleTopicsWildcard(t *testing.T) {
is := is.New(t)
var hello string
var world string
var expected string = "Hello World!"
var wg sync.WaitGroup
// Create new bus
bus := New(logger.New())
helloChannel, _ := bus.Subscribe("hello")
worldChannel, _ := bus.Subscribe("world")
wg.Add(1)
go func() {
counter := 2
for counter > 0 {
select {
case helloMessage := <-helloChannel:
hello = helloMessage.Data().(string)
counter--
case worldMessage := <-worldChannel:
world = worldMessage.Data().(string)
counter--
}
}
wg.Done()
}()
bus.Start()
bus.Publish("hello:wildcard:test", "Hello ")
bus.Publish("world:wildcard:test", "World!")
wg.Wait()
bus.Stop()
is.Equal(hello+world, expected)
}
func TestStructData(t *testing.T) {
is := is.New(t)
var expected string = "Tom Jones"
var actual string
var wg sync.WaitGroup
// Create new bus
bus := New(logger.New())
messageChannel, _ := bus.Subscribe("person")
wg.Add(1)
go func() {
message := <-messageChannel
p := message.Data().(*person)
actual = p.FullName()
wg.Done()
}()
bus.Start()
bus.Publish("person", newPerson("Tom", "Jones"))
wg.Wait()
bus.Stop()
is.Equal(actual, expected)
}
func TestErrors(t *testing.T) {
is := is.New(t)
// Create new bus
bus := New(logger.New())
_, err := bus.Subscribe("person")
is.NoErr(err)
err = bus.Start()
is.NoErr(err)
err = bus.Publish("person", newPerson("Tom", "Jones"))
is.NoErr(err)
err = bus.Stop()
is.NoErr(err)
err = bus.Stop()
is.True(err != nil)
err = bus.Start()
is.True(err != nil)
_, err = bus.Subscribe("person")
is.True(err != nil)
err = bus.Publish("person", newPerson("Tom", "Jones"))
is.True(err != nil)
}

View File

@@ -0,0 +1,90 @@
package shell
import (
"bytes"
"os"
"os/exec"
)
type Command struct {
command string
args []string
env []string
dir string
stdo, stde bytes.Buffer
}
func NewCommand(command string) *Command {
return &Command{
command: command,
env: os.Environ(),
}
}
func (c *Command) Dir(dir string) {
c.dir = dir
}
func (c *Command) Env(name string, value string) {
c.env = append(c.env, name+"="+value)
}
func (c *Command) Run() error {
cmd := exec.Command(c.command, c.args...)
if c.dir != "" {
cmd.Dir = c.dir
}
cmd.Stdout = &c.stdo
cmd.Stderr = &c.stde
return cmd.Run()
}
func (c *Command) Stdout() string {
return c.stdo.String()
}
func (c *Command) Stderr() string {
return c.stde.String()
}
func (c *Command) AddArgs(args []string) {
for _, arg := range args {
c.args = append(c.args, arg)
}
}
// CreateCommand returns a *Cmd struct that when run, will run the given command + args in the given directory
func CreateCommand(directory string, command string, args ...string) *exec.Cmd {
cmd := exec.Command(command, args...)
cmd.Dir = directory
return cmd
}
// RunCommand will run the given command + args in the given directory
// Will return stdout, stderr and error
func RunCommand(directory string, command string, args ...string) (string, string, error) {
cmd := CreateCommand(directory, command, args...)
var stdo, stde bytes.Buffer
cmd.Stdout = &stdo
cmd.Stderr = &stde
err := cmd.Run()
return stdo.String(), stde.String(), err
}
// RunCommandVerbose will run the given command + args in the given directory
// Will return an error if one occurs
func RunCommandVerbose(directory string, command string, args ...string) error {
cmd := CreateCommand(directory, command, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
return err
}
// CommandExists returns true if the given command can be found on the shell
func CommandExists(name string) bool {
_, err := exec.LookPath(name)
if err != nil {
return false
}
return true
}

View File

@@ -0,0 +1,68 @@
package signal
import (
"os"
gosignal "os/signal"
"syscall"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
)
// Manager manages signals such as CTRL-C
type Manager struct {
// Service Bus
bus *servicebus.ServiceBus
// logger
logger logger.CustomLogger
// signalChannel
signalchannel chan os.Signal
// Quit channel
quitChannel <-chan *servicebus.Message
}
// NewManager creates a new signal manager
func NewManager(bus *servicebus.ServiceBus, logger *logger.Logger) (*Manager, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
if err != nil {
return nil, err
}
result := &Manager{
bus: bus,
logger: logger.CustomLogger("Event Manager"),
signalchannel: make(chan os.Signal, 2),
quitChannel: quitChannel,
}
return result, nil
}
// Start the Signal Manager
func (m *Manager) Start() {
// Hook into interrupts
gosignal.Notify(m.signalchannel, os.Interrupt, syscall.SIGTERM)
// Spin off signal listener
go func() {
running := true
for running {
select {
case <-m.signalchannel:
println()
m.logger.Trace("Ctrl+C detected. Shutting down...")
m.bus.Publish("quit", "ctrl-c pressed")
case <-m.quitChannel:
running = false
break
}
}
m.logger.Trace("Shutdown")
}()
}

View File

@@ -0,0 +1,113 @@
package subsystem
import (
"github.com/leaanthony/wailsv2/v2/internal/binding"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/runtime/goruntime"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
)
// Binding is the Binding subsystem. It manages all service bus messages
// starting with "binding".
type Binding struct {
quitChannel <-chan *servicebus.Message
bindingChannel <-chan *servicebus.Message
running bool
// Binding db
bindings *binding.Bindings
// logger
logger logger.CustomLogger
// runtime
runtime *goruntime.Runtime
}
// NewBinding creates a new binding subsystem. Uses the given bindings db for reference.
func NewBinding(bus *servicebus.ServiceBus, logger *logger.Logger, bindings *binding.Bindings, runtime *goruntime.Runtime) (*Binding, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
if err != nil {
return nil, err
}
// Subscribe to event messages
bindingChannel, err := bus.Subscribe("binding")
if err != nil {
return nil, err
}
result := &Binding{
quitChannel: quitChannel,
bindingChannel: bindingChannel,
logger: logger.CustomLogger("Binding Subsystem"),
bindings: bindings,
runtime: runtime,
}
// Call WailsInit methods once the frontend is loaded
// TODO: Double check that this is actually being emitted
// when we want it to be
runtime.Events.On("wails:loaded", func(...interface{}) {
result.logger.Trace("Calling WailsInit() methods")
result.CallWailsInit()
})
return result, nil
}
// Start the subsystem
func (b *Binding) Start() error {
b.running = true
b.logger.Trace("Starting")
// Spin off a go routine
go func() {
for b.running {
select {
case <-b.quitChannel:
b.running = false
case bindingMessage := <-b.bindingChannel:
b.logger.Trace("Got binding message: %+v", bindingMessage)
}
}
// Call shutdown
b.shutdown()
}()
return nil
}
// CallWailsInit will callback to the registered WailsInit
// methods with the runtime object
func (b *Binding) CallWailsInit() error {
for _, wailsinit := range b.bindings.DB().WailsInitMethods() {
_, err := wailsinit.Call([]interface{}{b.runtime})
if err != nil {
return err
}
}
return nil
}
// CallWailsShutdown will callback to the registered WailsShutdown
// methods with the runtime object
func (b *Binding) CallWailsShutdown() error {
for _, wailsshutdown := range b.bindings.DB().WailsShutdownMethods() {
_, err := wailsshutdown.Call([]interface{}{})
if err != nil {
return err
}
}
return nil
}
func (b *Binding) shutdown() {
b.CallWailsShutdown()
b.logger.Trace("Shutdown")
}

View File

@@ -0,0 +1,148 @@
package subsystem
import (
"encoding/json"
"fmt"
"github.com/leaanthony/wailsv2/v2/internal/binding"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/messagedispatcher/message"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
)
// Call is the Call subsystem. It manages all service bus messages
// starting with "call".
type Call struct {
quitChannel <-chan *servicebus.Message
callChannel <-chan *servicebus.Message
running bool
// bindings DB
DB *binding.DB
// ServiceBus
bus *servicebus.ServiceBus
// logger
logger logger.CustomLogger
}
// NewCall creates a new log subsystem
func NewCall(bus *servicebus.ServiceBus, logger *logger.Logger, DB *binding.DB) (*Call, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
if err != nil {
return nil, err
}
// Subscribe to event messages
callChannel, err := bus.Subscribe("call:invoke")
if err != nil {
return nil, err
}
result := &Call{
quitChannel: quitChannel,
callChannel: callChannel,
logger: logger.CustomLogger("Call Subsystem"),
DB: DB,
bus: bus,
}
return result, nil
}
// Start the subsystem
func (c *Call) Start() error {
c.running = true
// Spin off a go routine
go func() {
for c.running {
select {
case <-c.quitChannel:
c.running = false
case callMessage := <-c.callChannel:
c.processCall(callMessage)
}
}
// Call shutdown
c.shutdown()
}()
return nil
}
func (c *Call) processCall(callMessage *servicebus.Message) {
c.logger.Trace("Got message: %+v", callMessage)
// Extract payload
payload := callMessage.Data().(*message.CallMessage)
// Lookup method
registeredMethod := c.DB.GetMethod(payload.Name)
// Check we have it
if registeredMethod == nil {
c.sendError(fmt.Errorf("Method not registered"), payload, callMessage.Target())
return
}
c.logger.Trace("Got registered method: %+v", registeredMethod)
result, err := registeredMethod.Call(payload.Args)
if err != nil {
c.sendError(err, payload, callMessage.Target())
return
}
c.logger.Trace("registeredMethod.Call: %+v, %+v", result, err)
// process result
c.sendResult(result, payload, callMessage.Target())
}
func (c *Call) sendResult(result interface{}, payload *message.CallMessage, clientID string) {
c.logger.Trace("Sending success result with CallbackID '%s' : %+v\n", payload.CallbackID, result)
message := &CallbackMessage{
Result: result,
CallbackID: payload.CallbackID,
}
messageData, err := json.Marshal(message)
c.logger.Trace("json message data: %+v\n", string(messageData))
if err != nil {
// what now?
c.logger.Fatal(err.Error())
}
c.bus.PublishForTarget("call:result", string(messageData), clientID)
}
func (c *Call) sendError(err error, payload *message.CallMessage, clientID string) {
c.logger.Trace("Sending error result with CallbackID '%s' : %+v\n", payload.CallbackID, err.Error())
message := &CallbackMessage{
Err: err.Error(),
CallbackID: payload.CallbackID,
}
messageData, err := json.Marshal(message)
c.logger.Trace("json message data: %+v\n", string(messageData))
if err != nil {
// what now?
c.logger.Fatal(err.Error())
}
c.bus.PublishForTarget("call:result", string(messageData), clientID)
}
func (c *Call) shutdown() {
c.logger.Trace("Shutdown")
}
// CallbackMessage defines a message that contains the result of a call
type CallbackMessage struct {
Result interface{} `json:"result"`
Err string `json:"error"`
CallbackID string `json:"callbackid"`
}

View File

@@ -0,0 +1,193 @@
package subsystem
import (
"strings"
"sync"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/messagedispatcher/message"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
)
// 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 int64 // The number of times this callback may be called. -1 = infinite
delete bool // Flag to indicate that this listener should be deleted
}
// Event is the Eventing subsystem. It manages all service bus messages
// starting with "event".
type Event struct {
quitChannel <-chan *servicebus.Message
eventChannel <-chan *servicebus.Message
running bool
// Event listeners
listeners map[string][]*eventListener
notifyLock sync.RWMutex
// logger
logger logger.CustomLogger
}
// NewEvent creates a new log subsystem
func NewEvent(bus *servicebus.ServiceBus, logger *logger.Logger) (*Event, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
if err != nil {
return nil, err
}
// Subscribe to event messages
eventChannel, err := bus.Subscribe("event")
if err != nil {
return nil, err
}
result := &Event{
quitChannel: quitChannel,
eventChannel: eventChannel,
logger: logger.CustomLogger("Event Subsystem"),
listeners: make(map[string][]*eventListener),
}
return result, nil
}
// RegisterListener provides a means of subscribing to events of type "eventName"
func (e *Event) RegisterListener(eventName string, callback func(...interface{})) {
// Create new eventListener
thisListener := &eventListener{
callback: callback,
counter: 0,
delete: false,
}
e.notifyLock.Lock()
// Append the new listener to the listeners slice
e.listeners[eventName] = append(e.listeners[eventName], thisListener)
e.notifyLock.Unlock()
}
// Start the subsystem
func (e *Event) Start() error {
e.logger.Trace("Starting")
e.running = true
// Spin off a go routine
go func() {
for e.running {
select {
case <-e.quitChannel:
e.running = false
break
case eventMessage := <-e.eventChannel:
splitTopic := strings.Split(eventMessage.Topic(), ":")
eventType := splitTopic[1]
switch eventType {
case "emit":
if len(splitTopic) != 4 {
e.logger.Error("Received emit message with invalid topic format. Expected 4 sections in topic, got %s", splitTopic)
continue
}
eventSource := splitTopic[3]
e.logger.Trace("Got Event Message: %s %+v", eventMessage.Topic(), eventMessage.Data())
event := eventMessage.Data().(*message.EventMessage)
eventName := event.Name
switch eventSource {
case "j":
// Notify Go Subscribers
e.logger.Trace("Notify Go subscribers to event '%s'", eventName)
go e.notifyListeners(eventName, event)
case "g":
// Notify Go listeners
e.logger.Trace("Got Go Event: %s", eventName)
go e.notifyListeners(eventName, event)
default:
e.logger.Error("unknown emit event message: %+v", eventMessage)
}
case "on":
// We wish to subscribe to an event channel
var message *message.OnEventMessage = eventMessage.Data().(*message.OnEventMessage)
eventName := message.Name
callback := message.Callback
e.RegisterListener(eventName, callback)
e.logger.Trace("Registered listener for event '%s' with callback %p", eventName, callback)
default:
e.logger.Error("unknown event message: %+v", eventMessage)
}
}
}
// Call shutdown
e.shutdown()
}()
return nil
}
// Notifies listeners for the given event name
func (e *Event) notifyListeners(eventName string, message *message.EventMessage) {
// Get list of event listeners
listeners := e.listeners[eventName]
if listeners == nil {
println("no listeners for", eventName)
return
}
// Lock the listeners
e.notifyLock.Lock()
// We have a dirty flag to indicate that there are items to delete
itemsToDelete := false
// Callback in goroutine
for _, listener := range listeners {
if listener.counter > 0 {
listener.counter--
}
go listener.callback(message.Data...)
if listener.counter == 0 {
listener.delete = true
itemsToDelete = true
}
}
// Do we have items to delete?
if itemsToDelete == true {
// Create a new Listeners slice
var newListeners = []*eventListener{}
// Iterate over current listeners
for _, listener := range listeners {
// If we aren't deleting the listener, add it to the new list
if !listener.delete {
newListeners = append(newListeners, listener)
}
}
// Save new listeners
e.listeners[eventName] = newListeners
}
// Unlock
e.notifyLock.Unlock()
}
func (e *Event) shutdown() {
e.logger.Trace("Shutdown")
}

View File

@@ -0,0 +1,50 @@
package subsystem
import (
"os"
"sync"
"testing"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/messagedispatcher/message"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
"github.com/matryer/is"
)
func TestSingleTopic(t *testing.T) {
is := is.New(t)
var expected string = "I am a message!"
var actual string
var wg sync.WaitGroup
// Create new bus
myLogger := logger.New(os.Stdout)
myLogger.SetLogLevel(logger.TRACE)
bus := servicebus.New(myLogger)
eventSubsystem, _ := NewEvent(bus, myLogger)
eventSubsystem.Start()
eventSubsystem.RegisterListener("test", func(data ...interface{}) {
is.Equal(len(data), 1)
actual = data[0].(string)
wg.Done()
})
wg.Add(1)
eventMessage := &message.EventMessage{
Name: "test",
Data: []interface{}{"I am a message!"},
}
bus.Start()
bus.Publish("event:test:from:j", eventMessage)
wg.Wait()
bus.Stop()
is.Equal(actual, expected)
}

View File

@@ -0,0 +1,79 @@
package subsystem
import (
"strings"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
)
// Log is the Logging subsystem. It handles messages with topics starting
// with "log:"
type Log struct {
logChannel <-chan *servicebus.Message
quitChannel <-chan *servicebus.Message
running bool
// Logger!
logger *logger.Logger
}
// NewLog creates a new log subsystem
func NewLog(bus *servicebus.ServiceBus, logger *logger.Logger) (*Log, error) {
// Subscribe to log messages
logChannel, err := bus.Subscribe("log")
if err != nil {
return nil, err
}
// Subscribe to quit messages
quitChannel, err := bus.Subscribe("quit")
if err != nil {
return nil, err
}
result := &Log{
logChannel: logChannel,
quitChannel: quitChannel,
logger: logger,
}
return result, nil
}
// Start the subsystem
func (l *Log) Start() error {
l.running = true
// Spin off a go routine
go func() {
for l.running {
select {
case <-l.quitChannel:
l.running = false
break
case logMessage := <-l.logChannel:
logType := strings.TrimPrefix(logMessage.Topic(), "log:")
switch logType {
case "debug":
l.logger.Debug(logMessage.Data().(string))
case "info":
l.logger.Info(logMessage.Data().(string))
case "warning":
l.logger.Warning(logMessage.Data().(string))
case "error":
l.logger.Error(logMessage.Data().(string))
case "fatal":
l.logger.Fatal(logMessage.Data().(string))
default:
l.logger.Error("unknown log message: %+v", logMessage)
}
}
}
l.logger.Trace("Logger Shutdown")
}()
return nil
}

View File

@@ -0,0 +1,116 @@
package subsystem
import (
"fmt"
"strings"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/runtime/goruntime"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
)
// Runtime is the Runtime subsystem. It handles messages with topics starting
// with "runtime:"
type Runtime struct {
quitChannel <-chan *servicebus.Message
runtimeChannel <-chan *servicebus.Message
running bool
logger logger.CustomLogger
// Runtime library
runtime *goruntime.Runtime
}
// NewRuntime creates a new runtime subsystem
func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger) (*Runtime, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
if err != nil {
return nil, err
}
// Subscribe to log messages
runtimeChannel, err := bus.Subscribe("runtime")
if err != nil {
return nil, err
}
result := &Runtime{
quitChannel: quitChannel,
runtimeChannel: runtimeChannel,
logger: logger.CustomLogger("Runtime Subsystem"),
runtime: goruntime.New(bus),
}
return result, nil
}
// Start the subsystem
func (r *Runtime) Start() error {
r.running = true
// Spin off a go routine
go func() {
for r.running {
select {
case <-r.quitChannel:
r.running = false
break
case runtimeMessage := <-r.runtimeChannel:
r.logger.Trace(fmt.Sprintf("Received message: %+v", runtimeMessage))
// Topics have the format: "runtime:category:call"
messageSlice := strings.Split(runtimeMessage.Topic(), ":")
if len(messageSlice) != 3 {
r.logger.Error("Invalid runtime message: %#v\n", runtimeMessage)
continue
}
category := messageSlice[1]
method := messageSlice[2]
var err error
switch category {
case "browser":
err = r.processBrowserMessage(method, runtimeMessage.Data())
default:
fmt.Errorf("unknown log message: %+v", runtimeMessage)
}
// If we had an error, log it
if err != nil {
r.logger.Error(err.Error())
}
}
}
// Call shutdown
r.shutdown()
}()
return nil
}
// GoRuntime returns the Go Runtime object
func (r *Runtime) GoRuntime() *goruntime.Runtime {
return r.runtime
}
func (r *Runtime) shutdown() {
r.logger.Trace("Shutdown")
}
func (r *Runtime) processBrowserMessage(method string, data interface{}) error {
switch method {
case "openurl":
url, ok := data.(string)
if !ok {
return fmt.Errorf("expected 1 string parameter for runtime:browser:openurl")
}
go r.runtime.Browser.Open(url)
default:
return fmt.Errorf("unknown method runtime:browser:%s", method)
}
return nil
}

View File

@@ -0,0 +1,5 @@
package apps
func Find() []Apps {
}

View File

@@ -0,0 +1,13 @@
package operatingsystem
// OS contains information about the operating system
type OS struct {
ID string
Name string
Version string
}
// Info retrieves information about the current platform
func Info() (*OS, error) {
return platformInfo()
}

View File

@@ -0,0 +1,51 @@
// +build linux
package operatingsystem
import (
"fmt"
"io/ioutil"
"os"
"strings"
)
// platformInfo is the platform specific method to get system information
func platformInfo() (*OS, error) {
_, err := os.Stat("/etc/os-release")
if os.IsNotExist(err) {
return nil, fmt.Errorf("unable to read system information")
}
osRelease, _ := ioutil.ReadFile("/etc/os-release")
return parseOsRelease(string(osRelease)), nil
}
func parseOsRelease(osRelease string) *OS {
// Default value
var result OS
result.ID = "Unknown"
result.Name = "Unknown"
result.Version = "Unknown"
// Split into lines
lines := strings.Split(osRelease, "\n")
// Iterate lines
for _, line := range lines {
// Split each line by the equals char
splitLine := strings.SplitN(line, "=", 2)
// Check we have
if len(splitLine) != 2 {
continue
}
switch splitLine[0] {
case "ID":
result.ID = strings.ToLower(strings.Trim(splitLine[1], "\""))
case "NAME":
result.Name = strings.Trim(splitLine[1], "\"")
case "VERSION_ID":
result.Version = strings.Trim(splitLine[1], "\"")
}
}
return &result
}

View File

@@ -0,0 +1,97 @@
// +build linux
package packagemanager
import (
"regexp"
"strings"
"github.com/leaanthony/wailsv2/v2/internal/shell"
)
// Apt represents the Apt manager
type Apt struct {
name string
osid string
}
// NewApt creates a new Apt instance
func NewApt(osid string) *Apt {
return &Apt{
name: "apt",
osid: osid,
}
}
// Packages returns the libraries that we need for Wails to compile
// They will potentially differ on different distributions or versions
func (a *Apt) Packages() packagemap {
return packagemap{
"libgtk-3": []*Package{
{Name: "libgtk-3-dev", SystemPackage: true, Library: true},
},
"libwebkit": []*Package{
{Name: "libwebkit2gtk-4.0-dev", SystemPackage: true, Library: true},
},
"gcc": []*Package{
{Name: "build-essential", SystemPackage: true},
},
"pkg-config": []*Package{
{Name: "pkg-config", SystemPackage: true},
},
"npm": []*Package{
{Name: "npm", SystemPackage: true},
},
"docker": []*Package{
{Name: "docker.io", SystemPackage: true, Optional: true},
},
}
}
// Name returns the name of the package manager
func (a *Apt) Name() string {
return a.name
}
// PackageInstalled tests if the given package name is installed
func (a *Apt) PackageInstalled(pkg *Package) (bool, error) {
if pkg.SystemPackage == false {
return false, nil
}
stdout, _, err := shell.RunCommand(".", "apt", "-qq", "list", pkg.Name)
return strings.Contains(stdout, "[installed]"), err
}
// PackageAvailable tests if the given package is available for installation
func (a *Apt) PackageAvailable(pkg *Package) (bool, error) {
if pkg.SystemPackage == false {
return false, nil
}
stdout, _, err := shell.RunCommand(".", "apt", "-qq", "list", pkg.Name)
// We add a space to ensure we get a full match, not partial match
output := a.removeEscapeSequences(stdout)
installed := strings.HasPrefix(output, pkg.Name)
a.getPackageVersion(pkg, output)
return installed, err
}
// InstallCommand returns the package manager specific command to install a package
func (a *Apt) InstallCommand(pkg *Package) string {
if pkg.SystemPackage == false {
return pkg.InstallCommand[a.osid]
}
return "sudo apt install " + pkg.Name
}
func (a *Apt) removeEscapeSequences(in string) string {
escapechars, _ := regexp.Compile(`\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])`)
return escapechars.ReplaceAllString(in, "")
}
func (a *Apt) getPackageVersion(pkg *Package, output string) {
splitOutput := strings.Split(output, " ")
if len(splitOutput) > 1 {
pkg.Version = splitOutput[1]
}
}

View File

@@ -0,0 +1,117 @@
// +build linux
package packagemanager
import (
"os/exec"
"regexp"
"strings"
"github.com/leaanthony/wailsv2/v2/internal/shell"
)
// Emerge represents the Emerge package manager
type Emerge struct {
name string
osid string
}
// NewEmerge creates a new Emerge instance
func NewEmerge(osid string) *Emerge {
return &Emerge{
name: "emerge",
osid: osid,
}
}
// Packages returns the libraries that we need for Wails to compile
// They will potentially differ on different distributions or versions
func (e *Emerge) Packages() packagemap {
return packagemap{
"libgtk-3": []*Package{
{Name: "x11-libs/gtk+", SystemPackage: true, Library: true},
},
"libwebkit": []*Package{
{Name: "net-libs/webkit-gtk", SystemPackage: true, Library: true},
},
"gcc": []*Package{
{Name: "sys-devel/gcc", SystemPackage: true},
},
"pkg-config": []*Package{
{Name: "dev-util/pkgconf", SystemPackage: true},
},
"npm": []*Package{
{Name: "net-libs/nodejs", SystemPackage: true},
},
"docker": []*Package{
{Name: "app-emulation/docker", SystemPackage: true, Optional: true},
},
}
}
// Name returns the name of the package manager
func (e *Emerge) Name() string {
return e.name
}
// PackageInstalled tests if the given package name is installed
func (e *Emerge) PackageInstalled(pkg *Package) (bool, error) {
if pkg.SystemPackage == false {
return false, nil
}
stdout, _, err := shell.RunCommand(".", "emerge", "-s", pkg.Name+"$")
if err != nil {
_, ok := err.(*exec.ExitError)
if ok {
return false, nil
}
return false, err
}
regex := `.*\*\s+` + regexp.QuoteMeta(pkg.Name) + `\n(?:\S|\s)+?Latest version installed: (.*)`
installedRegex := regexp.MustCompile(regex)
matches := installedRegex.FindStringSubmatch(stdout)
pkg.Version = ""
noOfMatches := len(matches)
installed := false
if noOfMatches > 1 && matches[1] != "[ Not Installed ]" {
installed = true
pkg.Version = strings.TrimSpace(matches[1])
}
return installed, err
}
// PackageAvailable tests if the given package is available for installation
func (e *Emerge) PackageAvailable(pkg *Package) (bool, error) {
if pkg.SystemPackage == false {
return false, nil
}
stdout, _, err := shell.RunCommand(".", "emerge", "-s", pkg.Name+"$")
// We add a space to ensure we get a full match, not partial match
if err != nil {
_, ok := err.(*exec.ExitError)
if ok {
return false, nil
}
return false, err
}
installedRegex := regexp.MustCompile(`.*\*\s+` + regexp.QuoteMeta(pkg.Name) + `\n(?:\S|\s)+?Latest version available: (.*)`)
matches := installedRegex.FindStringSubmatch(stdout)
pkg.Version = ""
noOfMatches := len(matches)
available := false
if noOfMatches > 1 {
available = true
pkg.Version = strings.TrimSpace(matches[1])
}
return available, nil
}
// InstallCommand returns the package manager specific command to install a package
func (e *Emerge) InstallCommand(pkg *Package) string {
if pkg.SystemPackage == false {
return pkg.InstallCommand[e.osid]
}
return "sudo emerge " + pkg.Name
}

Some files were not shown because too many files have changed in this diff Show More