mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-02 06:32:10 +08:00
Merge commit 'a213e8bcd1d8e4e5c764978879d875d2d55dd400' as 'v2'
This commit is contained in:
commit
c158fd369a
40
v2/NOTES.md
Normal file
40
v2/NOTES.md
Normal 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
|
5
v2/README.md
Normal file
5
v2/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Wails v2 ALPHA
|
||||
|
||||
This branch contains WORK IN PROGRESS! There are no guarantees. Use at your peril!
|
||||
|
||||
This document will be updated as progress is made.
|
116
v2/cmd/wails/internal/commands/build/build.go
Normal file
116
v2/cmd/wails/internal/commands/build/build.go
Normal file
@ -0,0 +1,116 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/leaanthony/clir"
|
||||
"github.com/leaanthony/slicer"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/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) {
|
||||
return fmt.Errorf("output type '%s' is not valid", outputType)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
261
v2/cmd/wails/internal/commands/dev/dev.go
Normal file
261
v2/cmd/wails/internal/commands/dev/dev.go
Normal file
@ -0,0 +1,261 @@
|
||||
package dev
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/leaanthony/clir"
|
||||
"github.com/leaanthony/slicer"
|
||||
"github.com/wailsapp/wails/v2/internal/fs"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/process"
|
||||
"github.com/wailsapp/wails/v2/pkg/commands/build"
|
||||
)
|
||||
|
||||
// AddSubcommand adds the `dev` command for the Wails application
|
||||
func AddSubcommand(app *clir.Cli) error {
|
||||
|
||||
command := app.NewSubCommand("dev", "Development mode")
|
||||
|
||||
outputType := "desktop"
|
||||
|
||||
validTargetTypes := slicer.String([]string{"desktop", "hybrid", "server"})
|
||||
|
||||
// Setup target type flag
|
||||
description := "Type of application to develop. Valid types: " + validTargetTypes.Join(",")
|
||||
command.StringFlag("t", description, &outputType)
|
||||
|
||||
// Passthrough ldflags
|
||||
ldflags := ""
|
||||
command.StringFlag("ldflags", "optional ldflags", &ldflags)
|
||||
|
||||
// compiler command
|
||||
compilerCommand := "go"
|
||||
command.StringFlag("compiler", "Use a different go compiler to build, eg go1.15beta1", &compilerCommand)
|
||||
|
||||
// extensions to trigger rebuilds
|
||||
extensions := "go"
|
||||
command.StringFlag("m", "Extensions to trigger rebuilds (comma separated) eg go,js,css,html", &extensions)
|
||||
|
||||
command.Action(func() error {
|
||||
|
||||
// Validate inputs
|
||||
if !validTargetTypes.Contains(outputType) {
|
||||
return fmt.Errorf("output type '%s' is not valid", outputType)
|
||||
}
|
||||
|
||||
// Create logger
|
||||
logger := logger.New()
|
||||
logger.AddOutput(os.Stdout)
|
||||
app.PrintBanner()
|
||||
|
||||
// TODO: Check you are in a project directory
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
var debugBinaryProcess *process.Process = nil
|
||||
var buildFrontend bool = true
|
||||
var extensionsThatTriggerARebuild = strings.Split(extensions, ",")
|
||||
|
||||
// Setup signal handler
|
||||
quitChannel := make(chan os.Signal, 1)
|
||||
signal.Notify(quitChannel, os.Interrupt, os.Kill, syscall.SIGTERM)
|
||||
|
||||
debounceQuit := make(chan bool, 1)
|
||||
|
||||
// Do initial build
|
||||
logger.Info("Building application for development...")
|
||||
debugBinaryProcess = restartApp(logger, outputType, ldflags, compilerCommand, buildFrontend, debugBinaryProcess)
|
||||
|
||||
go debounce(100*time.Millisecond, watcher.Events, debounceQuit, func(event fsnotify.Event) {
|
||||
// logger.Info("event: %+v", event)
|
||||
|
||||
// Check for new directories
|
||||
if event.Op&fsnotify.Create == fsnotify.Create {
|
||||
// If this is a folder, add it to our watch list
|
||||
if fs.DirExists(event.Name) {
|
||||
if !strings.Contains(event.Name, "node_modules") {
|
||||
watcher.Add(event.Name)
|
||||
logger.Info("Watching directory: %s", event.Name)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check for file writes
|
||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||
|
||||
// logger.Info("modified file: %s", event.Name)
|
||||
var rebuild bool = false
|
||||
|
||||
// Iterate all file patterns
|
||||
for _, pattern := range extensionsThatTriggerARebuild {
|
||||
rebuild = strings.HasSuffix(event.Name, pattern)
|
||||
if err != nil {
|
||||
logger.Fatal(err.Error())
|
||||
}
|
||||
if rebuild {
|
||||
// Only build frontend when the file isn't a Go file
|
||||
buildFrontend = !strings.HasSuffix(event.Name, "go")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !rebuild {
|
||||
logger.Info("Filename change: %s did not match extension list %s", event.Name, extensions)
|
||||
return
|
||||
}
|
||||
|
||||
if buildFrontend {
|
||||
logger.Info("Full rebuild triggered: %s updated", event.Name)
|
||||
} else {
|
||||
logger.Info("Partial build triggered: %s updated", event.Name)
|
||||
}
|
||||
|
||||
// Do a rebuild
|
||||
|
||||
// Try and build the app
|
||||
newBinaryProcess := restartApp(logger, outputType, ldflags, compilerCommand, buildFrontend, debugBinaryProcess)
|
||||
|
||||
// If we have a new process, save it
|
||||
if newBinaryProcess != nil {
|
||||
debugBinaryProcess = newBinaryProcess
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
// Get project dir
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get all subdirectories
|
||||
dirs, err := fs.GetSubdirectories(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Setup a watcher for non-node_modules directories
|
||||
dirs.Each(func(dir string) {
|
||||
if strings.Contains(dir, "node_modules") {
|
||||
return
|
||||
}
|
||||
logger.Info("Watching directory: %s", dir)
|
||||
err = watcher.Add(dir)
|
||||
if err != nil {
|
||||
logger.Fatal(err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
// Wait until we get a quit signal
|
||||
quit := false
|
||||
for quit == false {
|
||||
select {
|
||||
case <-quitChannel:
|
||||
println()
|
||||
// Notify debouncer to quit
|
||||
debounceQuit <- true
|
||||
quit = true
|
||||
}
|
||||
}
|
||||
|
||||
// Kill the current program if running
|
||||
if debugBinaryProcess != nil {
|
||||
debugBinaryProcess.Kill()
|
||||
}
|
||||
|
||||
logger.Info("Development mode exited")
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Credit: https://drailing.net/2018/01/debounce-function-for-golang/
|
||||
func debounce(interval time.Duration, input chan fsnotify.Event, quitChannel chan bool, cb func(arg fsnotify.Event)) {
|
||||
var item fsnotify.Event
|
||||
timer := time.NewTimer(interval)
|
||||
exit:
|
||||
for {
|
||||
select {
|
||||
case item = <-input:
|
||||
timer.Reset(interval)
|
||||
case <-timer.C:
|
||||
if item.Name != "" {
|
||||
cb(item)
|
||||
}
|
||||
case <-quitChannel:
|
||||
break exit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func restartApp(logger *logger.Logger, outputType string, ldflags string, compilerCommand string, buildFrontend bool, debugBinaryProcess *process.Process) *process.Process {
|
||||
|
||||
appBinary, err := buildApp(logger, outputType, ldflags, compilerCommand, buildFrontend)
|
||||
println()
|
||||
if err != nil {
|
||||
logger.Error("Build Failed: %s", err.Error())
|
||||
return nil
|
||||
}
|
||||
logger.Info("Build new binary: %s", appBinary)
|
||||
|
||||
// Kill existing binary if need be
|
||||
if debugBinaryProcess != nil {
|
||||
killError := debugBinaryProcess.Kill()
|
||||
|
||||
if killError != nil {
|
||||
logger.Fatal("Unable to kill debug binary (PID: %d)!", debugBinaryProcess.PID())
|
||||
}
|
||||
|
||||
debugBinaryProcess = nil
|
||||
}
|
||||
|
||||
// TODO: Generate `backend.js`
|
||||
|
||||
// Start up new binary
|
||||
newProcess := process.NewProcess(logger, appBinary)
|
||||
err = newProcess.Start()
|
||||
if err != nil {
|
||||
// Remove binary
|
||||
fs.DeleteFile(appBinary)
|
||||
logger.Fatal("Unable to start application: %s", err.Error())
|
||||
}
|
||||
|
||||
return newProcess
|
||||
}
|
||||
|
||||
func buildApp(logger *logger.Logger, outputType string, ldflags string, compilerCommand string, buildFrontend bool) (string, error) {
|
||||
|
||||
// Create random output file
|
||||
outputFile := fmt.Sprintf("debug-%d", time.Now().Unix())
|
||||
|
||||
// Create BuildOptions
|
||||
buildOptions := &build.Options{
|
||||
Logger: logger,
|
||||
OutputType: outputType,
|
||||
Mode: build.Debug,
|
||||
Pack: false,
|
||||
Platform: runtime.GOOS,
|
||||
LDFlags: ldflags,
|
||||
Compiler: compilerCommand,
|
||||
OutputFile: outputFile,
|
||||
IgnoreFrontend: !buildFrontend,
|
||||
}
|
||||
|
||||
return build.Build(buildOptions)
|
||||
|
||||
}
|
154
v2/cmd/wails/internal/commands/doctor/doctor.go
Normal file
154
v2/cmd/wails/internal/commands/doctor/doctor.go
Normal file
@ -0,0 +1,154 @@
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/leaanthony/clir"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/system"
|
||||
"github.com/wailsapp/wails/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
|
||||
}
|
120
v2/cmd/wails/internal/commands/initialise/initialise.go
Normal file
120
v2/cmd/wails/internal/commands/initialise/initialise.go
Normal file
@ -0,0 +1,120 @@
|
||||
package initialise
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/leaanthony/clir"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/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
|
||||
}
|
44
v2/cmd/wails/main.go
Normal file
44
v2/cmd/wails/main.go
Normal file
@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/leaanthony/clir"
|
||||
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/build"
|
||||
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/dev"
|
||||
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/doctor"
|
||||
"github.com/wailsapp/wails/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 = dev.AddSubcommand(app)
|
||||
if err != nil {
|
||||
fatal(err.Error())
|
||||
}
|
||||
|
||||
err = app.Run()
|
||||
if err != nil {
|
||||
println("\n\nERROR: " + err.Error())
|
||||
}
|
||||
}
|
18
v2/go.mod
Normal file
18
v2/go.mod
Normal file
@ -0,0 +1,18 @@
|
||||
module github.com/wailsapp/wails/v2
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/leaanthony/clir v1.0.4
|
||||
github.com/leaanthony/gosod v0.0.4
|
||||
github.com/leaanthony/slicer v1.4.1
|
||||
github.com/matryer/is v1.4.0
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.4
|
||||
github.com/xyproto/xpm v1.2.1
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202
|
||||
golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
|
||||
nhooyr.io/websocket v1.8.6
|
||||
)
|
114
v2/go.sum
Normal file
114
v2/go.sum
Normal file
@ -0,0 +1,114 @@
|
||||
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/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
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.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
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/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
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.4 h1:Dov2y9zWJmZr7CjaCe86lKa4b5CSxskGAt2yBkoDyiU=
|
||||
github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0=
|
||||
github.com/leaanthony/gosod v0.0.4 h1:v4hepo4IyL8E8c9qzDsvYcA0KGh7Npf8As74K5ibQpI=
|
||||
github.com/leaanthony/gosod v0.0.4/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/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/xyproto/xpm v1.2.1 h1:trdvGjjWBsOOKzBBUPT6JvaIQM3acJEEYfbxN7M96wg=
|
||||
github.com/xyproto/xpm v1.2.1/go.mod h1:cMnesLsD0PBXLgjDfTDEaKr8XyTFsnP1QycSqRw7BiY=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82 h1:shxDsb9Dz27xzk3A0DxP0JuJnZMpKrdg8+E14eiUAX4=
|
||||
golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/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=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k=
|
||||
nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
10
v2/internal/app/debug.go
Normal file
10
v2/internal/app/debug.go
Normal file
@ -0,0 +1,10 @@
|
||||
// +build debug
|
||||
|
||||
package app
|
||||
|
||||
// Init initialises the application for a debug environment
|
||||
func (a *App) Init() error {
|
||||
a.debug = true
|
||||
println("Initialising debug options")
|
||||
return nil
|
||||
}
|
44
v2/internal/app/default.go
Normal file
44
v2/internal/app/default.go
Normal file
@ -0,0 +1,44 @@
|
||||
// +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/wailsapp/wails/v2/internal/features"
|
||||
)
|
||||
|
||||
// App defines a Wails application structure
|
||||
type App struct {
|
||||
Title string
|
||||
Width int
|
||||
Height int
|
||||
Resizable bool
|
||||
|
||||
// Indicates if the app is running in debug mode
|
||||
debug 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
|
||||
}
|
171
v2/internal/app/desktop.go
Normal file
171
v2/internal/app/desktop.go
Normal file
@ -0,0 +1,171 @@
|
||||
// +build desktop,!server
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/binding"
|
||||
"github.com/wailsapp/wails/v2/internal/features"
|
||||
"github.com/wailsapp/wails/v2/internal/ffenestri"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
"github.com/wailsapp/wails/v2/internal/signal"
|
||||
"github.com/wailsapp/wails/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
|
||||
|
||||
// Indicates if the app is in debug mode
|
||||
debug bool
|
||||
|
||||
// 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,
|
||||
StartHidden: options.StartHidden,
|
||||
|
||||
// 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(),
|
||||
}
|
||||
|
||||
// Initialise the app
|
||||
result.Init()
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
212
v2/internal/app/hybrid.go
Normal file
212
v2/internal/app/hybrid.go
Normal file
@ -0,0 +1,212 @@
|
||||
// +build !server,!desktop,hybrid
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/leaanthony/clir"
|
||||
"github.com/wailsapp/wails/v2/internal/binding"
|
||||
"github.com/wailsapp/wails/v2/internal/features"
|
||||
"github.com/wailsapp/wails/v2/internal/ffenestri"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
"github.com/wailsapp/wails/v2/internal/subsystem"
|
||||
"github.com/wailsapp/wails/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
|
||||
|
||||
debug bool
|
||||
|
||||
// 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,
|
||||
StartHidden: options.StartHidden,
|
||||
|
||||
// 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),
|
||||
}
|
||||
|
||||
// Initialise the app
|
||||
app.Init()
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
35
v2/internal/app/options.go
Normal file
35
v2/internal/app/options.go
Normal file
@ -0,0 +1,35 @@
|
||||
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
|
||||
StartHidden bool
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
9
v2/internal/app/production.go
Normal file
9
v2/internal/app/production.go
Normal file
@ -0,0 +1,9 @@
|
||||
// +build !debug
|
||||
|
||||
package app
|
||||
|
||||
// Init initialises the application for a production environment
|
||||
func (a *App) Init() error {
|
||||
println("Processing production cli options")
|
||||
return nil
|
||||
}
|
160
v2/internal/app/server.go
Normal file
160
v2/internal/app/server.go
Normal file
@ -0,0 +1,160 @@
|
||||
// +build server,!desktop
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/leaanthony/clir"
|
||||
"github.com/wailsapp/wails/v2/internal/binding"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
"github.com/wailsapp/wails/v2/internal/subsystem"
|
||||
"github.com/wailsapp/wails/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
|
||||
|
||||
debug bool
|
||||
}
|
||||
|
||||
// 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),
|
||||
}
|
||||
|
||||
// Initialise app
|
||||
result.Init()
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
112
v2/internal/assetdb/assetdb.go
Normal file
112
v2/internal/assetdb/assetdb.go
Normal 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/wailsapp/wails/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()
|
||||
}
|
70
v2/internal/assetdb/assetdb_test.go
Normal file
70
v2/internal/assetdb/assetdb_test.go
Normal 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, "")
|
||||
}
|
176
v2/internal/assetdb/filesystem.go
Normal file
176
v2/internal/assetdb/filesystem.go
Normal 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
|
||||
}
|
108
v2/internal/assetdb/filesystem_test.go
Normal file
108
v2/internal/assetdb/filesystem_test.go
Normal 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
9
v2/internal/bind/bind.go
Normal 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
70
v2/internal/binding/binding.go
Executable file
@ -0,0 +1,70 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/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("cannout bind value to app: %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()
|
||||
}
|
138
v2/internal/binding/boundMethod.go
Normal file
138
v2/internal/binding/boundMethod.go
Normal 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
150
v2/internal/binding/db.go
Normal 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
|
||||
}
|
28
v2/internal/binding/parameter.go
Normal file
28
v2/internal/binding/parameter.go
Normal 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")
|
||||
}
|
120
v2/internal/binding/reflect.go
Executable file
120
v2/internal/binding/reflect.go
Executable file
@ -0,0 +1,120 @@
|
||||
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
|
||||
}
|
||||
|
||||
// isStructPtr returns true if the value given is a struct
|
||||
func isStruct(value interface{}) bool {
|
||||
return reflect.ValueOf(value).Kind() == reflect.Struct
|
||||
}
|
||||
|
||||
func getMethods(value interface{}) ([]*BoundMethod, error) {
|
||||
|
||||
// Create result placeholder
|
||||
var result []*BoundMethod
|
||||
|
||||
// Check type
|
||||
if !isStructPtr(value) {
|
||||
|
||||
if isStruct(value) {
|
||||
name := reflect.ValueOf(value).Type().Name()
|
||||
return nil, fmt.Errorf("%s is a struct, not a pointer to a struct", name)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("not a pointer to a struct")
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
}
|
17
v2/internal/crypto/crypto.go
Normal file
17
v2/internal/crypto/crypto.go
Normal 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)
|
||||
}
|
13
v2/internal/features/features.go
Normal file
13
v2/internal/features/features.go
Normal 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{},
|
||||
}
|
||||
}
|
5
v2/internal/features/features_linux.go
Normal file
5
v2/internal/features/features_linux.go
Normal file
@ -0,0 +1,5 @@
|
||||
package features
|
||||
|
||||
// Linux holds linux specific feature flags
|
||||
type Linux struct {
|
||||
}
|
23
v2/internal/ffenestri/features_linux.go
Normal file
23
v2/internal/ffenestri/features_linux.go
Normal 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/wailsapp/wails/v2/internal/features"
|
||||
|
||||
func (a *Application) processOSFeatureFlags(features *features.Features) {
|
||||
|
||||
// Process Linux features
|
||||
// linux := features.Linux
|
||||
|
||||
}
|
199
v2/internal/ffenestri/ffenestri.go
Normal file
199
v2/internal/ffenestri/ffenestri.go
Normal file
@ -0,0 +1,199 @@
|
||||
package ffenestri
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/features"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/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
|
||||
StartHidden bool
|
||||
}
|
||||
|
||||
var defaultConfig = &Config{
|
||||
Title: "My Wails App",
|
||||
Width: 800,
|
||||
Height: 600,
|
||||
DevTools: true,
|
||||
Resizable: true,
|
||||
Fullscreen: false,
|
||||
Frameless: false,
|
||||
StartHidden: 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)
|
||||
startHidden := a.bool2Cint(a.config.StartHidden)
|
||||
app := C.NewApplication(title, width, height, resizable, devtools, fullscreen, startHidden)
|
||||
|
||||
// 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)
|
||||
}
|
33
v2/internal/ffenestri/ffenestri.h
Normal file
33
v2/internal/ffenestri/ffenestri.h
Normal file
@ -0,0 +1,33 @@
|
||||
#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, int startHidden);
|
||||
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 Hide(void *app);
|
||||
extern void Show(void *app);
|
||||
extern void Center(void *app);
|
||||
extern void Maximise(void *app);
|
||||
extern void Unmaximise(void *app);
|
||||
extern void Minimise(void *app);
|
||||
extern void Unminimise(void *app);
|
||||
extern void SetSize(void *app, int width, int height);
|
||||
extern void SetPosition(void *app, int x, int y);
|
||||
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 *SaveFileDialog(void *appPointer, char *title, char *filter);
|
||||
extern char *OpenFileDialog(void *appPointer, char *title, char *filter);
|
||||
extern char *OpenDirectoryDialog(void *appPointer, char *title, char *filter);
|
||||
|
||||
#endif
|
159
v2/internal/ffenestri/ffenestri_client.go
Normal file
159
v2/internal/ffenestri/ffenestri_client.go
Normal file
@ -0,0 +1,159 @@
|
||||
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/wailsapp/wails/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)
|
||||
}
|
||||
|
||||
// WindowShow will show the window
|
||||
func (c *Client) WindowShow() {
|
||||
C.Show(c.app.app)
|
||||
}
|
||||
|
||||
// WindowHide will hide the window
|
||||
func (c *Client) WindowHide() {
|
||||
C.Hide(c.app.app)
|
||||
}
|
||||
|
||||
// WindowCenter will hide the window
|
||||
func (c *Client) WindowCenter() {
|
||||
C.Center(c.app.app)
|
||||
}
|
||||
|
||||
// WindowMaximise will maximise the window
|
||||
func (c *Client) WindowMaximise() {
|
||||
C.Maximise(c.app.app)
|
||||
}
|
||||
|
||||
// WindowMinimise will minimise the window
|
||||
func (c *Client) WindowMinimise() {
|
||||
C.Minimise(c.app.app)
|
||||
}
|
||||
|
||||
// WindowUnmaximise will unmaximise the window
|
||||
func (c *Client) WindowUnmaximise() {
|
||||
C.Unmaximise(c.app.app)
|
||||
}
|
||||
|
||||
// WindowUnminimise will unminimise the window
|
||||
func (c *Client) WindowUnminimise() {
|
||||
C.Unminimise(c.app.app)
|
||||
}
|
||||
|
||||
// WindowPosition will position the window to x,y on the
|
||||
// monitor that the window is mostly on
|
||||
func (c *Client) WindowPosition(x int, y int) {
|
||||
C.SetPosition(c.app.app, C.int(x), C.int(y))
|
||||
}
|
||||
|
||||
// WindowSize will resize the window to the given
|
||||
// width and height
|
||||
func (c *Client) WindowSize(width int, height int) {
|
||||
C.SetSize(c.app.app, C.int(width), C.int(height))
|
||||
}
|
||||
|
||||
// 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, filter string) string {
|
||||
|
||||
cstring := C.OpenFileDialog(c.app.app, c.app.string2CString(title), c.app.string2CString(filter))
|
||||
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, filter string) string {
|
||||
|
||||
cstring := C.SaveFileDialog(c.app.app, c.app.string2CString(title), c.app.string2CString(filter))
|
||||
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, filter string) string {
|
||||
|
||||
cstring := C.OpenDirectoryDialog(c.app.app, c.app.string2CString(title), c.app.string2CString(filter))
|
||||
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
|
||||
}
|
984
v2/internal/ffenestri/ffenestri_linux.c
Normal file
984
v2/internal/ffenestri/ffenestri_linux.c
Normal file
@ -0,0 +1,984 @@
|
||||
|
||||
#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 startHidden;
|
||||
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, int startHidden)
|
||||
{
|
||||
// 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;
|
||||
result->startHidden = startHidden;
|
||||
|
||||
// Default drag button is PRIMARY
|
||||
result->dragButton = PRIMARY_MOUSE_BUTTON;
|
||||
|
||||
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(struct Application *app)
|
||||
{
|
||||
Debug("Destroying Application");
|
||||
|
||||
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);
|
||||
if( app->frame == 0) {
|
||||
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(struct Application *app)
|
||||
{
|
||||
Debug("Quit Called");
|
||||
gtk_window_close((GtkWindow *)app->mainWindow);
|
||||
g_application_quit((GApplication *)app->application);
|
||||
DestroyApplication(app);
|
||||
}
|
||||
|
||||
// SetTitle sets the main window title to the given string
|
||||
void SetTitle(struct Application *app, const char *title)
|
||||
{
|
||||
gtk_window_set_title(app->mainWindow, title);
|
||||
}
|
||||
|
||||
// Fullscreen sets the main window to be fullscreen
|
||||
void Fullscreen(struct Application *app)
|
||||
{
|
||||
gtk_window_fullscreen(app->mainWindow);
|
||||
}
|
||||
|
||||
// UnFullscreen resets the main window after a fullscreen
|
||||
void UnFullscreen(struct Application *app)
|
||||
{
|
||||
gtk_window_unfullscreen(app->mainWindow);
|
||||
}
|
||||
|
||||
void setMinMaxSize(struct Application *app)
|
||||
{
|
||||
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;
|
||||
}
|
||||
gtk_window_set_geometry_hints(app->mainWindow, NULL, &size, flags);
|
||||
}
|
||||
|
||||
char *fileDialogInternal(struct Application *app, GtkFileChooserAction chooserAction, char **args) {
|
||||
GtkFileChooserNative *native;
|
||||
GtkFileChooserAction action = chooserAction;
|
||||
gint res;
|
||||
char *filename;
|
||||
|
||||
char *title = args[0];
|
||||
char *filter = args[1];
|
||||
|
||||
native = gtk_file_chooser_native_new(title,
|
||||
app->mainWindow,
|
||||
action,
|
||||
"_Open",
|
||||
"_Cancel");
|
||||
|
||||
GtkFileChooser *chooser = GTK_FILE_CHOOSER(native);
|
||||
|
||||
// If we have filters, process them
|
||||
if (filter[0] != '\0') {
|
||||
GtkFileFilter *file_filter = gtk_file_filter_new();
|
||||
gchar **filters = g_strsplit(filter, ",", -1);
|
||||
gint i;
|
||||
for(i = 0; filters && filters[i]; i++) {
|
||||
gtk_file_filter_add_pattern(file_filter, filters[i]);
|
||||
// Debug("Adding filter pattern: %s\n", filters[i]);
|
||||
}
|
||||
gtk_file_filter_set_name(file_filter, filter);
|
||||
gtk_file_chooser_add_filter(chooser, file_filter);
|
||||
g_strfreev(filters);
|
||||
}
|
||||
|
||||
res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));
|
||||
if (res == GTK_RESPONSE_ACCEPT)
|
||||
{
|
||||
filename = gtk_file_chooser_get_filename(chooser);
|
||||
}
|
||||
|
||||
g_object_unref(native);
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
// openFileDialogInternal opens a dialog to select a file
|
||||
// NOTE: The result is a string that will need to be freed!
|
||||
char *openFileDialogInternal(struct Application *app, char **args)
|
||||
{
|
||||
return fileDialogInternal(app, GTK_FILE_CHOOSER_ACTION_OPEN, args);
|
||||
}
|
||||
|
||||
// saveFileDialogInternal opens a dialog to select a file
|
||||
// NOTE: The result is a string that will need to be freed!
|
||||
char *saveFileDialogInternal(struct Application *app, char **args)
|
||||
{
|
||||
return fileDialogInternal(app, GTK_FILE_CHOOSER_ACTION_SAVE, args);
|
||||
}
|
||||
|
||||
|
||||
// openDirectoryDialogInternal opens a dialog to select a directory
|
||||
// NOTE: The result is a string that will need to be freed!
|
||||
char *openDirectoryDialogInternal(struct Application *app, char **args)
|
||||
{
|
||||
return fileDialogInternal(app, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, args);
|
||||
}
|
||||
|
||||
void SetMinWindowSize(struct Application *app, int minWidth, int minHeight)
|
||||
{
|
||||
app->minWidth = minWidth;
|
||||
app->minHeight = minHeight;
|
||||
}
|
||||
|
||||
void SetMaxWindowSize(struct Application *app, int maxWidth, int maxHeight)
|
||||
{
|
||||
app->maxWidth = maxWidth;
|
||||
app->maxHeight = maxHeight;
|
||||
}
|
||||
|
||||
// SetColour sets the colour of the webview to the given colour string
|
||||
int SetColour(struct Application *app, const char *colourString)
|
||||
{
|
||||
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(struct Application *app)
|
||||
{
|
||||
app->frame = 0;
|
||||
}
|
||||
|
||||
void syncCallback(GObject *source_object,
|
||||
GAsyncResult *res,
|
||||
void *data)
|
||||
{
|
||||
struct Application *app = (struct Application *)data;
|
||||
app->lock = 0;
|
||||
}
|
||||
|
||||
void syncEval(struct Application *app, const gchar *script)
|
||||
{
|
||||
|
||||
WebKitWebView *webView = (WebKitWebView *)(app->webView);
|
||||
|
||||
// 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, (void*)app);
|
||||
|
||||
while (app->lock == 1)
|
||||
{
|
||||
g_main_context_iteration(0, true);
|
||||
}
|
||||
}
|
||||
|
||||
void asyncEval(WebKitWebView *webView, const gchar *script)
|
||||
{
|
||||
webkit_web_view_run_javascript(
|
||||
webView,
|
||||
script,
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
typedef void (*dispatchMethod)(struct Application *app, void *);
|
||||
|
||||
struct dispatchData
|
||||
{
|
||||
struct Application *app;
|
||||
dispatchMethod method;
|
||||
void *args;
|
||||
};
|
||||
|
||||
gboolean executeMethod(gpointer data)
|
||||
{
|
||||
struct dispatchData *d = (struct dispatchData *)data;
|
||||
(d->method)(d->app, d->args);
|
||||
g_free(d);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void ExecJS(struct Application *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)(struct Application *app, void *);
|
||||
|
||||
struct dialogCall
|
||||
{
|
||||
struct Application *app;
|
||||
dialogMethod method;
|
||||
void *args;
|
||||
void *filter;
|
||||
char *result;
|
||||
int done;
|
||||
};
|
||||
|
||||
gboolean executeMethodWithReturn(gpointer data)
|
||||
{
|
||||
struct dialogCall *d = (struct dialogCall *)data;
|
||||
|
||||
d->result = (d->method)(d->app, d->args);
|
||||
d->done = 1;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
char *OpenFileDialog(struct Application *app, char *title, char *filter)
|
||||
{
|
||||
struct dialogCall *data = (struct dialogCall *)g_new(struct dialogCall, 1);
|
||||
data->result = NULL;
|
||||
data->done = 0;
|
||||
data->method = (dialogMethod)openFileDialogInternal;
|
||||
const char* dialogArgs[]={ title, filter };
|
||||
data->args = dialogArgs;
|
||||
data->app = app;
|
||||
|
||||
gdk_threads_add_idle(executeMethodWithReturn, data);
|
||||
|
||||
while (data->done == 0)
|
||||
{
|
||||
usleep(100000);
|
||||
}
|
||||
g_free(data);
|
||||
return data->result;
|
||||
}
|
||||
|
||||
char *SaveFileDialog(struct Application *app, char *title, char *filter)
|
||||
{
|
||||
struct dialogCall *data = (struct dialogCall *)g_new(struct dialogCall, 1);
|
||||
data->result = NULL;
|
||||
data->done = 0;
|
||||
data->method = (dialogMethod)saveFileDialogInternal;
|
||||
const char* dialogArgs[]={ title, filter };
|
||||
data->args = dialogArgs;
|
||||
data->app = app;
|
||||
|
||||
gdk_threads_add_idle(executeMethodWithReturn, data);
|
||||
|
||||
while (data->done == 0)
|
||||
{
|
||||
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 *OpenDirectoryDialog(struct Application *app, char *title, char *filter)
|
||||
{
|
||||
struct dialogCall *data = (struct dialogCall *)g_new(struct dialogCall, 1);
|
||||
data->result = NULL;
|
||||
data->done = 0;
|
||||
data->method = (dialogMethod)openDirectoryDialogInternal;
|
||||
const char* dialogArgs[]={ title, filter };
|
||||
data->args = dialogArgs;
|
||||
data->app = app;
|
||||
|
||||
gdk_threads_add_idle(executeMethodWithReturn, data);
|
||||
|
||||
while (data->done == 0)
|
||||
{
|
||||
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,
|
||||
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));
|
||||
|
||||
// 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
|
||||
gtk_window_set_position(app->mainWindow, GTK_WIN_POS_CENTER);
|
||||
|
||||
// Show window and focus
|
||||
if( app->startHidden == 0) {
|
||||
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,
|
||||
struct Application *app)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
gboolean buttonPress(GtkWidget *widget, GdkEventButton *event, struct Application *app)
|
||||
{
|
||||
if (event->type == GDK_BUTTON_PRESS && event->button == app->dragButton)
|
||||
{
|
||||
app->dragButtonEvent = event;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gboolean buttonRelease(GtkWidget *widget, GdkEventButton *event, struct Application *app)
|
||||
{
|
||||
if (event->type == GDK_BUTTON_RELEASE && event->button == app->dragButton)
|
||||
{
|
||||
app->dragButtonEvent = NULL;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void sendMessageToBackend(WebKitUserContentManager *contentManager,
|
||||
WebKitJavascriptResult *result,
|
||||
struct Application *app)
|
||||
{
|
||||
#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(struct Application *app, int flag)
|
||||
{
|
||||
debug = flag;
|
||||
}
|
||||
|
||||
// getCurrentMonitorGeometry gets the geometry of the monitor
|
||||
// that the window is mostly on.
|
||||
GdkRectangle getCurrentMonitorGeometry(GtkWindow *window) {
|
||||
// Get the monitor that the window is currently on
|
||||
GdkDisplay *display = gtk_widget_get_display(GTK_WIDGET(window));
|
||||
GdkWindow *gdk_window = gtk_widget_get_window(GTK_WIDGET(window));
|
||||
GdkMonitor *monitor = gdk_display_get_monitor_at_window (display, gdk_window);
|
||||
|
||||
// Get the geometry of the monitor
|
||||
GdkRectangle result;
|
||||
gdk_monitor_get_geometry (monitor,&result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*******************
|
||||
* Window Position *
|
||||
*******************/
|
||||
|
||||
// Position holds an x/y corrdinate
|
||||
struct Position {
|
||||
int x;
|
||||
int y;
|
||||
};
|
||||
|
||||
// Internal call for setting the position of the window.
|
||||
void setPositionInternal(struct Application *app, struct Position *pos) {
|
||||
|
||||
// Get the monitor geometry
|
||||
GdkRectangle m = getCurrentMonitorGeometry(app->mainWindow);
|
||||
|
||||
// Move the window relative to the monitor
|
||||
gtk_window_move(app->mainWindow, m.x + pos->x, m.y + pos->y);
|
||||
|
||||
// Free memory
|
||||
free(pos);
|
||||
}
|
||||
|
||||
// SetPosition sets the position of the window to the given x/y
|
||||
// coordinates. The x/y values are relative to the monitor
|
||||
// the window is mostly on.
|
||||
void SetPosition(struct Application *app, int x, int y) {
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)setPositionInternal;
|
||||
struct Position *pos = malloc(sizeof(struct Position));
|
||||
pos->x = x;
|
||||
pos->y = y;
|
||||
data->args = pos;
|
||||
data->app = app;
|
||||
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
/***************
|
||||
* Window Size *
|
||||
***************/
|
||||
|
||||
// Size holds a width/height
|
||||
struct Size {
|
||||
int width;
|
||||
int height;
|
||||
};
|
||||
|
||||
// Internal call for setting the size of the window.
|
||||
void setSizeInternal(struct Application *app, struct Size *size) {
|
||||
gtk_window_resize(app->mainWindow, size->width, size->height);
|
||||
free(size);
|
||||
}
|
||||
|
||||
// SetSize sets the size of the window to the given width/height
|
||||
void SetSize(struct Application *app, int width, int height) {
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)setSizeInternal;
|
||||
struct Size *size = malloc(sizeof(struct Size));
|
||||
size->width = width;
|
||||
size->height = height;
|
||||
data->args = size;
|
||||
data->app = app;
|
||||
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
|
||||
// centerInternal will center the main window on the monitor it is mostly in
|
||||
void centerInternal(struct Application *app)
|
||||
{
|
||||
// Get the geometry of the monitor
|
||||
GdkRectangle m = getCurrentMonitorGeometry(app->mainWindow);
|
||||
|
||||
// Get the window width/height
|
||||
int windowWidth, windowHeight;
|
||||
gtk_window_get_size(app->mainWindow, &windowWidth, &windowHeight);
|
||||
|
||||
// Place the window at the center of the monitor
|
||||
gtk_window_move(app->mainWindow, ((m.width - windowWidth) / 2) + m.x, ((m.height - windowHeight) / 2) + m.y);
|
||||
}
|
||||
|
||||
// Center the window
|
||||
void Center(struct Application *app) {
|
||||
|
||||
// Setup a call to centerInternal on the main thread
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)centerInternal;
|
||||
data->app = app;
|
||||
|
||||
// Add call to main thread
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
// hideInternal hides the main window
|
||||
void hideInternal(struct Application *app) {
|
||||
gtk_widget_hide (GTK_WIDGET(app->mainWindow));
|
||||
}
|
||||
|
||||
// Hide places the hideInternal method onto the main thread for execution
|
||||
void Hide(struct Application *app) {
|
||||
|
||||
// Setup a call to hideInternal on the main thread
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)hideInternal;
|
||||
data->app = app;
|
||||
|
||||
// Add call to main thread
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
// showInternal shows the main window
|
||||
void showInternal(struct Application *app) {
|
||||
gtk_widget_show_all(GTK_WIDGET(app->mainWindow));
|
||||
gtk_widget_grab_focus(app->webView);
|
||||
}
|
||||
|
||||
// Show places the showInternal method onto the main thread for execution
|
||||
void Show(struct Application *app) {
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)showInternal;
|
||||
data->app = app;
|
||||
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
|
||||
// maximiseInternal maximises the main window
|
||||
void maximiseInternal(struct Application *app) {
|
||||
gtk_window_maximize(GTK_WIDGET(app->mainWindow));
|
||||
}
|
||||
|
||||
// Maximise places the maximiseInternal method onto the main thread for execution
|
||||
void Maximise(struct Application *app) {
|
||||
|
||||
// Setup a call to maximiseInternal on the main thread
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)maximiseInternal;
|
||||
data->app = app;
|
||||
|
||||
// Add call to main thread
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
// unmaximiseInternal unmaximises the main window
|
||||
void unmaximiseInternal(struct Application *app) {
|
||||
gtk_window_unmaximize(GTK_WIDGET(app->mainWindow));
|
||||
}
|
||||
|
||||
// Unmaximise places the unmaximiseInternal method onto the main thread for execution
|
||||
void Unmaximise(struct Application *app) {
|
||||
|
||||
// Setup a call to unmaximiseInternal on the main thread
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)unmaximiseInternal;
|
||||
data->app = app;
|
||||
|
||||
// Add call to main thread
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
|
||||
// minimiseInternal minimises the main window
|
||||
void minimiseInternal(struct Application *app) {
|
||||
gtk_window_iconify(app->mainWindow);
|
||||
}
|
||||
|
||||
// Minimise places the minimiseInternal method onto the main thread for execution
|
||||
void Minimise(struct Application *app) {
|
||||
|
||||
// Setup a call to minimiseInternal on the main thread
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)minimiseInternal;
|
||||
data->app = app;
|
||||
|
||||
// Add call to main thread
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
// unminimiseInternal unminimises the main window
|
||||
void unminimiseInternal(struct Application *app) {
|
||||
gtk_window_present(app->mainWindow);
|
||||
}
|
||||
|
||||
// Unminimise places the unminimiseInternal method onto the main thread for execution
|
||||
void Unminimise(struct Application *app) {
|
||||
|
||||
// Setup a call to unminimiseInternal on the main thread
|
||||
struct dispatchData *data = (struct dispatchData *)g_new(struct dispatchData, 1);
|
||||
data->method = (dispatchMethod)unminimiseInternal;
|
||||
data->app = app;
|
||||
|
||||
// Add call to main thread
|
||||
gdk_threads_add_idle(executeMethod, data);
|
||||
}
|
||||
|
||||
|
||||
void SetBindings(struct Application *app, const char *bindings)
|
||||
{
|
||||
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,
|
||||
struct Application *app)
|
||||
{
|
||||
app->sendMessageToBackend("WC"); // Window Close
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void setupWindow(struct Application *app)
|
||||
{
|
||||
|
||||
// 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 if this is a frameless app
|
||||
if ( app->frame == 0 ) {
|
||||
webkit_user_content_manager_register_script_message_handler(contentManager, "windowDrag");
|
||||
app->signalWindowDrag = g_signal_connect(contentManager, "script-message-received::windowDrag", G_CALLBACK(dragWindow), app);
|
||||
// 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);
|
||||
}
|
||||
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);
|
||||
|
||||
|
||||
// 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* _, struct Application *app)
|
||||
{
|
||||
setupWindow(app);
|
||||
}
|
||||
|
||||
void Run(struct Application *app, int argc, char **argv)
|
||||
{
|
||||
g_signal_connect(app->application, "activate", G_CALLBACK(activate), app);
|
||||
g_application_run(G_APPLICATION(app->application), argc, argv);
|
||||
}
|
||||
|
||||
#endif
|
171
v2/internal/fs/fs.go
Normal file
171
v2/internal/fs/fs.go
Normal file
@ -0,0 +1,171 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
||||
"github.com/leaanthony/slicer"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// DeleteFile will delete the given file
|
||||
func DeleteFile(filename string) error {
|
||||
return os.Remove(filename)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// GetSubdirectories returns a list of subdirectories for the given root directory
|
||||
func GetSubdirectories(rootDir string) (*slicer.StringSlicer, error) {
|
||||
var result slicer.StringSlicer
|
||||
|
||||
// Iterate root dir
|
||||
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If we have a directory, save it
|
||||
if info.IsDir() {
|
||||
result.Add(path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return &result, err
|
||||
}
|
31
v2/internal/fs/fs_test.go
Normal file
31
v2/internal/fs/fs_test.go
Normal 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
108
v2/internal/html/asset.go
Normal 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])
|
||||
}
|
195
v2/internal/html/assetbundle.go
Normal file
195
v2/internal/html/assetbundle.go
Normal file
@ -0,0 +1,195 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/leaanthony/slicer"
|
||||
"github.com/wailsapp/wails/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()
|
||||
}
|
||||
}
|
98
v2/internal/logger/custom_logger.go
Normal file
98
v2/internal/logger/custom_logger.go
Normal 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)
|
||||
}
|
143
v2/internal/logger/default_logger.go
Normal file
143
v2/internal/logger/default_logger.go
Normal 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)
|
||||
}
|
34
v2/internal/logger/logger.go
Normal file
34
v2/internal/logger/logger.go
Normal 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: "",
|
||||
}
|
202
v2/internal/logger/logger_test.go
Normal file
202
v2/internal/logger/logger_test.go
Normal 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")
|
||||
|
||||
}
|
86
v2/internal/messagedispatcher/dispatchclient.go
Normal file
86
v2/internal/messagedispatcher/dispatchclient.go
Normal file
@ -0,0 +1,86 @@
|
||||
package messagedispatcher
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
)
|
||||
|
||||
// Client defines what a frontend client can do
|
||||
type Client interface {
|
||||
Quit()
|
||||
NotifyEvent(message string)
|
||||
CallResult(message string)
|
||||
SaveFileDialog(title string, filter string) string
|
||||
OpenFileDialog(title string, filter string) string
|
||||
OpenDirectoryDialog(title string, filter string) string
|
||||
WindowSetTitle(title string)
|
||||
WindowShow()
|
||||
WindowHide()
|
||||
WindowCenter()
|
||||
WindowMaximise()
|
||||
WindowUnmaximise()
|
||||
WindowMinimise()
|
||||
WindowUnminimise()
|
||||
WindowPosition(x int, y int)
|
||||
WindowSize(width int, height int)
|
||||
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)
|
||||
|
||||
}
|
37
v2/internal/messagedispatcher/message/call.go
Normal file
37
v2/internal/messagedispatcher/message/call.go
Normal 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
|
||||
}
|
47
v2/internal/messagedispatcher/message/event.go
Normal file
47
v2/internal/messagedispatcher/message/event.go
Normal 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
|
||||
}
|
33
v2/internal/messagedispatcher/message/log.go
Normal file
33
v2/internal/messagedispatcher/message/log.go
Normal 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
|
||||
}
|
31
v2/internal/messagedispatcher/message/messageparser.go
Normal file
31
v2/internal/messagedispatcher/message/messageparser.go
Normal 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)
|
||||
}
|
36
v2/internal/messagedispatcher/message/runtime.go
Normal file
36
v2/internal/messagedispatcher/message/runtime.go
Normal 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)
|
||||
|
||||
}
|
76
v2/internal/messagedispatcher/message/window.go
Normal file
76
v2/internal/messagedispatcher/message/window.go
Normal file
@ -0,0 +1,76 @@
|
||||
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"
|
||||
|
||||
// Center window
|
||||
case 'c':
|
||||
parsedMessage.Topic = "window:center"
|
||||
parsedMessage.Data = ""
|
||||
|
||||
// Hide window
|
||||
case 'H':
|
||||
parsedMessage.Topic = "window:hide"
|
||||
parsedMessage.Data = ""
|
||||
|
||||
// Show window
|
||||
case 'S':
|
||||
parsedMessage.Topic = "window:show"
|
||||
parsedMessage.Data = ""
|
||||
|
||||
// Position window
|
||||
case 'p':
|
||||
parsedMessage.Topic = "window:position:" + message[3:]
|
||||
parsedMessage.Data = ""
|
||||
|
||||
// Set window size
|
||||
case 's':
|
||||
parsedMessage.Topic = "window:size:" + message[3:]
|
||||
parsedMessage.Data = ""
|
||||
|
||||
// Maximise window
|
||||
case 'M':
|
||||
parsedMessage.Topic = "window:maximise"
|
||||
parsedMessage.Data = ""
|
||||
|
||||
// Unmaximise window
|
||||
case 'U':
|
||||
parsedMessage.Topic = "window:unmaximise"
|
||||
parsedMessage.Data = ""
|
||||
|
||||
// Minimise window
|
||||
case 'm':
|
||||
parsedMessage.Topic = "window:minimise"
|
||||
parsedMessage.Data = ""
|
||||
|
||||
// Unminimise window
|
||||
case 'u':
|
||||
parsedMessage.Topic = "window:unminimise"
|
||||
parsedMessage.Data = ""
|
||||
|
||||
// Unknown event type
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown message: %s", message)
|
||||
}
|
||||
|
||||
return parsedMessage, nil
|
||||
}
|
391
v2/internal/messagedispatcher/messagedispatcher.go
Normal file
391
v2/internal/messagedispatcher/messagedispatcher.go
Normal file
@ -0,0 +1,391 @@
|
||||
package messagedispatcher
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/crypto"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
||||
"github.com/wailsapp/wails/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)
|
||||
}
|
||||
case "show":
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowShow()
|
||||
}
|
||||
case "hide":
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowHide()
|
||||
}
|
||||
case "center":
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowCenter()
|
||||
}
|
||||
case "maximise":
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowMaximise()
|
||||
}
|
||||
case "unmaximise":
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowUnmaximise()
|
||||
}
|
||||
case "minimise":
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowMinimise()
|
||||
}
|
||||
case "unminimise":
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowUnminimise()
|
||||
}
|
||||
case "position":
|
||||
// We need 2 arguments
|
||||
if len(splitTopic) != 4 {
|
||||
d.logger.Error("Invalid number of parameters for 'window:position' : %#v", result.Data())
|
||||
return
|
||||
}
|
||||
x, err1 := strconv.Atoi(splitTopic[2])
|
||||
y, err2 := strconv.Atoi(splitTopic[3])
|
||||
if err1 != nil || err2 != nil {
|
||||
d.logger.Error("Invalid integer parameters for 'window:position' : %#v", result.Data())
|
||||
return
|
||||
}
|
||||
// Notify clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowPosition(x, y)
|
||||
}
|
||||
case "size":
|
||||
// We need 2 arguments
|
||||
if len(splitTopic) != 4 {
|
||||
d.logger.Error("Invalid number of parameters for 'window:size' : %#v", result.Data())
|
||||
return
|
||||
}
|
||||
w, err1 := strconv.Atoi(splitTopic[2])
|
||||
h, err2 := strconv.Atoi(splitTopic[3])
|
||||
if err1 != nil || err2 != nil {
|
||||
d.logger.Error("Invalid integer parameters for 'window:size' : %#v", result.Data())
|
||||
return
|
||||
}
|
||||
// Notifh clients
|
||||
for _, client := range d.clients {
|
||||
client.frontend.WindowSize(w, h)
|
||||
}
|
||||
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]
|
||||
filter := ""
|
||||
if len(splitTopic) > 4 {
|
||||
filter = splitTopic[4]
|
||||
}
|
||||
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, filter)
|
||||
}
|
||||
|
||||
// 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, filter)
|
||||
}
|
||||
|
||||
// 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, filter)
|
||||
}
|
||||
// Send dummy response
|
||||
d.servicebus.Publish(responseTopic, result)
|
||||
|
||||
default:
|
||||
d.logger.Error("Unknown dialog command: %s", command)
|
||||
}
|
||||
}
|
||||
}
|
10
v2/internal/parse/README.md
Normal file
10
v2/internal/parse/README.md
Normal file
@ -0,0 +1,10 @@
|
||||
# Parse
|
||||
|
||||
Parse will attempt to parse your Wails project to perform a number of tasks:
|
||||
* Verify that you have bound struct pointers
|
||||
* Generate JS helper files/docs
|
||||
|
||||
It currently checks bindings correctly if your code binds using one of the following methods:
|
||||
* Literal Binding: `app.Bind(&MyStruct{})`
|
||||
* Variable Binding: `app.Bind(m)` - m can be `m := &MyStruct{}` or `m := newMyStruct()`
|
||||
* Function Binding: `app.Bind(newMyStruct())`
|
441
v2/internal/parse/parse.go
Normal file
441
v2/internal/parse/parse.go
Normal file
@ -0,0 +1,441 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/leaanthony/slicer"
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
var internalMethods = slicer.String([]string{"WailsInit", "Wails Shutdown"})
|
||||
|
||||
var structCache = make(map[string]*ParsedStruct)
|
||||
var boundStructs = make(map[string]*ParsedStruct)
|
||||
var boundMethods = []string{}
|
||||
var boundStructPointerLiterals = []string{}
|
||||
var boundStructLiterals = slicer.StringSlicer{}
|
||||
var boundVariables = slicer.StringSlicer{}
|
||||
var app = ""
|
||||
var structPointerFunctionDecls = make(map[string]string)
|
||||
var structFunctionDecls = make(map[string]string)
|
||||
var variableStructDecls = make(map[string]string)
|
||||
var variableFunctionDecls = make(map[string]string)
|
||||
|
||||
type Parameter struct {
|
||||
Name string
|
||||
Type string
|
||||
}
|
||||
|
||||
type ParsedMethod struct {
|
||||
Struct string
|
||||
Name string
|
||||
Comments []string
|
||||
Inputs []*Parameter
|
||||
Returns []*Parameter
|
||||
}
|
||||
|
||||
type ParsedStruct struct {
|
||||
Name string
|
||||
Methods []*ParsedMethod
|
||||
}
|
||||
|
||||
type BoundStructs []*ParsedStruct
|
||||
|
||||
func ParseProject(projectPath string) (BoundStructs, error) {
|
||||
|
||||
cfg := &packages.Config{Mode: packages.NeedFiles | packages.NeedSyntax | packages.NeedTypesInfo}
|
||||
pkgs, err := packages.Load(cfg, projectPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "load: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if packages.PrintErrors(pkgs) > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Iterate the packages
|
||||
for _, pkg := range pkgs {
|
||||
|
||||
// Iterate the files
|
||||
for _, file := range pkg.Syntax {
|
||||
|
||||
var wailsPkgVar = ""
|
||||
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
var s string
|
||||
switch x := n.(type) {
|
||||
// Parse import declarations
|
||||
case *ast.ImportSpec:
|
||||
// Determine what wails has been imported as
|
||||
if x.Path.Value == `"github.com/wailsapp/wails/v2"` {
|
||||
wailsPkgVar = x.Name.Name
|
||||
}
|
||||
// Parse calls. We are looking for app.Bind() calls
|
||||
case *ast.CallExpr:
|
||||
f, ok := x.Fun.(*ast.SelectorExpr)
|
||||
if ok {
|
||||
n, ok := f.X.(*ast.Ident)
|
||||
if ok {
|
||||
//Check this is the Bind() call associated with the app variable
|
||||
if n.Name == app && f.Sel.Name == "Bind" {
|
||||
if len(x.Args) == 1 {
|
||||
ce, ok := x.Args[0].(*ast.CallExpr)
|
||||
if ok {
|
||||
n, ok := ce.Fun.(*ast.Ident)
|
||||
if ok {
|
||||
// We found a bind method using a function call
|
||||
// EG: app.Bind( newMyStruct() )
|
||||
boundMethods = append(boundMethods, n.Name)
|
||||
}
|
||||
} else {
|
||||
// We also want to check for Bind( &MyStruct{} )
|
||||
ue, ok := x.Args[0].(*ast.UnaryExpr)
|
||||
if ok {
|
||||
if ue.Op.String() == "&" {
|
||||
cl, ok := ue.X.(*ast.CompositeLit)
|
||||
if ok {
|
||||
t, ok := cl.Type.(*ast.Ident)
|
||||
if ok {
|
||||
// We have found Bind( &MyStruct{} )
|
||||
boundStructPointerLiterals = append(boundStructPointerLiterals, t.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Let's check when the user binds a struct,
|
||||
// rather than a struct pointer: Bind( MyStruct{} )
|
||||
// We do this to provide better hints to the user
|
||||
cl, ok := x.Args[0].(*ast.CompositeLit)
|
||||
if ok {
|
||||
t, ok := cl.Type.(*ast.Ident)
|
||||
if ok {
|
||||
boundStructLiterals.Add(t.Name)
|
||||
}
|
||||
} else {
|
||||
// Also check for when we bind a variable
|
||||
// myVariable := &MyStruct{}
|
||||
// app.Bind( myVariable )
|
||||
i, ok := x.Args[0].(*ast.Ident)
|
||||
if ok {
|
||||
boundVariables.Add(i.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We scan assignments for a number of reasons:
|
||||
// * Determine the variable containing the main application
|
||||
// * Determine the type of variables that get used in Bind()
|
||||
// * Determine the type of variables that get created with var := &MyStruct{}
|
||||
case *ast.AssignStmt:
|
||||
for _, rhs := range x.Rhs {
|
||||
ce, ok := rhs.(*ast.CallExpr)
|
||||
if ok {
|
||||
se, ok := ce.Fun.(*ast.SelectorExpr)
|
||||
if ok {
|
||||
i, ok := se.X.(*ast.Ident)
|
||||
if ok {
|
||||
// Have we found the wails package name?
|
||||
if i.Name == wailsPkgVar {
|
||||
// Check we are calling a function to create the app
|
||||
if se.Sel.Name == "CreateApp" || se.Sel.Name == "CreateAppWithOptions" {
|
||||
if len(x.Lhs) == 1 {
|
||||
i, ok := x.Lhs[0].(*ast.Ident)
|
||||
if ok {
|
||||
// Found the app variable name
|
||||
app = i.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check for function assignment
|
||||
// a := newMyStruct()
|
||||
fe, ok := ce.Fun.(*ast.Ident)
|
||||
if ok {
|
||||
if len(x.Lhs) == 1 {
|
||||
i, ok := x.Lhs[0].(*ast.Ident)
|
||||
if ok {
|
||||
// Store the variable -> Function mapping
|
||||
// so we can later resolve the type
|
||||
variableFunctionDecls[i.Name] = fe.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check for literal assignment of struct
|
||||
// EG: myvar := MyStruct{}
|
||||
ue, ok := rhs.(*ast.UnaryExpr)
|
||||
if ok {
|
||||
cl, ok := ue.X.(*ast.CompositeLit)
|
||||
if ok {
|
||||
t, ok := cl.Type.(*ast.Ident)
|
||||
if ok {
|
||||
if len(x.Lhs) == 1 {
|
||||
i, ok := x.Lhs[0].(*ast.Ident)
|
||||
if ok {
|
||||
variableStructDecls[i.Name] = t.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// We scan for functions to build up a list of function names
|
||||
// for a number of reasons:
|
||||
// * Determine which functions are struct methods that are bound
|
||||
// * Determine
|
||||
case *ast.FuncDecl:
|
||||
if x.Recv != nil {
|
||||
// This is a struct method
|
||||
for _, field := range x.Recv.List {
|
||||
se, ok := field.Type.(*ast.StarExpr)
|
||||
if ok {
|
||||
// This is a struct pointer method
|
||||
i, ok := se.X.(*ast.Ident)
|
||||
if ok {
|
||||
// We want to ignore Internal functions
|
||||
if internalMethods.Contains(x.Name.Name) {
|
||||
continue
|
||||
}
|
||||
// If we haven't already found this struct,
|
||||
// Create a placeholder in the cache
|
||||
parsedStruct := structCache[i.Name]
|
||||
if parsedStruct == nil {
|
||||
structCache[i.Name] = &ParsedStruct{
|
||||
Name: i.Name,
|
||||
}
|
||||
parsedStruct = structCache[i.Name]
|
||||
}
|
||||
|
||||
// If this method is Public
|
||||
if string(x.Name.Name[0]) == strings.ToUpper((string(x.Name.Name[0]))) {
|
||||
structMethod := &ParsedMethod{
|
||||
Struct: i.Name,
|
||||
Name: x.Name.Name,
|
||||
}
|
||||
// Check if the method has comments.
|
||||
// If so, save it with the parsed method
|
||||
if x.Doc != nil {
|
||||
for _, comment := range x.Doc.List {
|
||||
stringComment := comment.Text
|
||||
if strings.HasPrefix(stringComment, "//") {
|
||||
stringComment = stringComment[2:]
|
||||
}
|
||||
structMethod.Comments = append(structMethod.Comments, strings.TrimSpace(stringComment))
|
||||
}
|
||||
}
|
||||
|
||||
// Save the input parameters
|
||||
for _, inputField := range x.Type.Params.List {
|
||||
t, ok := inputField.Type.(*ast.Ident)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, name := range inputField.Names {
|
||||
structMethod.Inputs = append(structMethod.Inputs, &Parameter{Name: name.Name, Type: t.Name})
|
||||
}
|
||||
}
|
||||
|
||||
// Save the output parameters
|
||||
for _, outputField := range x.Type.Results.List {
|
||||
t, ok := outputField.Type.(*ast.Ident)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if len(outputField.Names) == 0 {
|
||||
structMethod.Returns = append(structMethod.Returns, &Parameter{Type: t.Name})
|
||||
} else {
|
||||
for _, name := range outputField.Names {
|
||||
structMethod.Returns = append(structMethod.Returns, &Parameter{Name: name.Name, Type: t.Name})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append this method to the parsed struct
|
||||
parsedStruct.Methods = append(parsedStruct.Methods, structMethod)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is a function declaration
|
||||
// We care about its name and return type
|
||||
// This will allow us to resolve types later
|
||||
functionName := x.Name.Name
|
||||
|
||||
// Look for one that returns a single value
|
||||
if x.Type != nil && x.Type.Results != nil && x.Type.Results.List != nil {
|
||||
if len(x.Type.Results.List) == 1 {
|
||||
// Check for *struct
|
||||
t, ok := x.Type.Results.List[0].Type.(*ast.StarExpr)
|
||||
if ok {
|
||||
s, ok := t.X.(*ast.Ident)
|
||||
if ok {
|
||||
// println("*** Function", functionName, "found which returns: *"+s.Name)
|
||||
structPointerFunctionDecls[functionName] = s.Name
|
||||
}
|
||||
} else {
|
||||
// Check for functions that return a struct
|
||||
// This is to help us provide hints if the user binds a struct
|
||||
t, ok := x.Type.Results.List[0].Type.(*ast.Ident)
|
||||
if ok {
|
||||
// println("*** Function", functionName, "found which returns: "+t.Name)
|
||||
structFunctionDecls[functionName] = t.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
// spew.Dump(file)
|
||||
}
|
||||
}
|
||||
|
||||
/***** Update bound structs ******/
|
||||
|
||||
// Resolve bound Methods
|
||||
for _, method := range boundMethods {
|
||||
s, ok := structPointerFunctionDecls[method]
|
||||
if !ok {
|
||||
s, ok = structFunctionDecls[method]
|
||||
if !ok {
|
||||
println("Fatal: Bind statement using", method, "but cannot find", method, "declaration")
|
||||
} else {
|
||||
println("Fatal: Cannot bind struct using method `" + method + "` because it returns a struct (" + s + "). Return a pointer to " + s + " instead.")
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
structDefinition := structCache[s]
|
||||
if structDefinition == nil {
|
||||
println("Fatal: Bind statement using `"+method+"` but cannot find struct", s, "definition")
|
||||
os.Exit(1)
|
||||
}
|
||||
boundStructs[s] = structDefinition
|
||||
}
|
||||
|
||||
// Resolve bound vars
|
||||
for _, structLiteral := range boundStructPointerLiterals {
|
||||
s, ok := structCache[structLiteral]
|
||||
if !ok {
|
||||
println("Fatal: Bind statement using", structLiteral, "but cannot find", structLiteral, "declaration")
|
||||
os.Exit(1)
|
||||
}
|
||||
boundStructs[structLiteral] = s
|
||||
}
|
||||
|
||||
// Resolve bound variables
|
||||
boundVariables.Each(func(variable string) {
|
||||
v, ok := variableStructDecls[variable]
|
||||
if !ok {
|
||||
method, ok := variableFunctionDecls[variable]
|
||||
if !ok {
|
||||
println("Fatal: Bind statement using variable `" + variable + "` which does not resolve to a struct pointer")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Resolve function name
|
||||
v, ok = structPointerFunctionDecls[method]
|
||||
if !ok {
|
||||
v, ok = structFunctionDecls[method]
|
||||
if !ok {
|
||||
println("Fatal: Bind statement using", method, "but cannot find", method, "declaration")
|
||||
} else {
|
||||
println("Fatal: Cannot bind variable `" + variable + "` because it resolves to a struct (" + v + "). Return a pointer to " + v + " instead.")
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
s, ok := structCache[v]
|
||||
if !ok {
|
||||
println("Fatal: Bind statement using variable `" + variable + "` which resolves to a `" + v + "` but cannot find its declaration")
|
||||
os.Exit(1)
|
||||
}
|
||||
boundStructs[v] = s
|
||||
|
||||
})
|
||||
|
||||
// Check for struct literals
|
||||
boundStructLiterals.Each(func(structName string) {
|
||||
println("Fatal: Cannot bind struct using struct literal `" + structName + "{}`. Create a pointer to " + structName + " instead.")
|
||||
os.Exit(1)
|
||||
})
|
||||
|
||||
// Check for bound variables
|
||||
// boundVariables.Each(func(varName string) {
|
||||
// println("Fatal: Cannot bind struct using struct literal `" + structName + "{}`. Create a pointer to " + structName + " instead.")
|
||||
// })
|
||||
|
||||
// spew.Dump(boundStructs)
|
||||
// os.Exit(0)
|
||||
|
||||
// }
|
||||
// Inspect the AST and print all identifiers and literals.
|
||||
|
||||
println("export {")
|
||||
|
||||
noOfStructs := len(boundStructs)
|
||||
structCount := 0
|
||||
for _, s := range boundStructs {
|
||||
structCount++
|
||||
println()
|
||||
println(" " + s.Name + ": {")
|
||||
println()
|
||||
noOfMethods := len(s.Methods)
|
||||
for methodCount, m := range s.Methods {
|
||||
println(" /****************")
|
||||
for _, comment := range m.Comments {
|
||||
println(" *", comment)
|
||||
}
|
||||
if len(m.Comments) > 0 {
|
||||
println(" *")
|
||||
}
|
||||
inputNames := ""
|
||||
for _, input := range m.Inputs {
|
||||
println(" * @param {"+input.Type+"}", input.Name)
|
||||
inputNames += input.Name + ", "
|
||||
}
|
||||
print(" * @return Promise<")
|
||||
for _, output := range m.Returns {
|
||||
print(output.Type + "|")
|
||||
}
|
||||
println("Error>")
|
||||
println(" *")
|
||||
println(" ***/")
|
||||
if len(inputNames) > 2 {
|
||||
inputNames = inputNames[:len(inputNames)-2]
|
||||
}
|
||||
println(" ", m.Name+": function("+inputNames+") {")
|
||||
println(" return window.backend." + s.Name + "." + m.Name + "(" + inputNames + ");")
|
||||
print(" }")
|
||||
if methodCount < noOfMethods-1 {
|
||||
print(",")
|
||||
}
|
||||
println()
|
||||
println()
|
||||
}
|
||||
print(" }")
|
||||
if structCount < noOfStructs-1 {
|
||||
print(",")
|
||||
}
|
||||
println()
|
||||
}
|
||||
println()
|
||||
println("}")
|
||||
println()
|
||||
}
|
63
v2/internal/process/process.go
Normal file
63
v2/internal/process/process.go
Normal file
@ -0,0 +1,63 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
)
|
||||
|
||||
// Process defines a process that can be executed
|
||||
type Process struct {
|
||||
logger *logger.Logger
|
||||
cmd *exec.Cmd
|
||||
exitChannel chan bool
|
||||
Running bool
|
||||
}
|
||||
|
||||
// NewProcess creates a new process struct
|
||||
func NewProcess(logger *logger.Logger, cmd string, args ...string) *Process {
|
||||
return &Process{
|
||||
logger: logger,
|
||||
cmd: exec.Command(cmd, args...),
|
||||
exitChannel: make(chan bool, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// Start the process
|
||||
func (p *Process) Start() error {
|
||||
|
||||
err := p.cmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Running = true
|
||||
|
||||
go func(cmd *exec.Cmd, running *bool, logger *logger.Logger, exitChannel chan bool) {
|
||||
logger.Info("Starting process (PID: %d)", cmd.Process.Pid)
|
||||
cmd.Wait()
|
||||
logger.Info("Exiting process (PID: %d)", cmd.Process.Pid)
|
||||
*running = false
|
||||
exitChannel <- true
|
||||
}(p.cmd, &p.Running, p.logger, p.exitChannel)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Kill the process
|
||||
func (p *Process) Kill() error {
|
||||
if !p.Running {
|
||||
return nil
|
||||
}
|
||||
err := p.cmd.Process.Kill()
|
||||
|
||||
// Wait for command to exit properly
|
||||
<-p.exitChannel
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// PID returns the process PID
|
||||
func (p *Process) PID() int {
|
||||
return p.cmd.Process.Pid
|
||||
}
|
80
v2/internal/project/project.go
Normal file
80
v2/internal/project/project.go
Normal 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
|
||||
}
|
1
v2/internal/runtime/assets/wails.js
Normal file
1
v2/internal/runtime/assets/wails.js
Normal file
File diff suppressed because one or more lines are too long
36
v2/internal/runtime/goruntime/browser.go
Normal file
36
v2/internal/runtime/goruntime/browser.go
Normal 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{}
|
||||
}
|
16
v2/internal/runtime/goruntime/browser_test.go
Normal file
16
v2/internal/runtime/goruntime/browser_test.go
Normal file
@ -0,0 +1,16 @@
|
||||
package goruntime
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
)
|
||||
|
||||
func TestBrowserOpen(t *testing.T) {
|
||||
mylogger := logger.New(os.Stdout)
|
||||
myServiceBus := servicebus.New(mylogger)
|
||||
myRuntime := New(myServiceBus)
|
||||
myRuntime.Browser.Open("http://www.google.com")
|
||||
}
|
140
v2/internal/runtime/goruntime/dialog.go
Normal file
140
v2/internal/runtime/goruntime/dialog.go
Normal file
@ -0,0 +1,140 @@
|
||||
package goruntime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/crypto"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
)
|
||||
|
||||
// Dialog defines all Dialog related operations
|
||||
type Dialog interface {
|
||||
SaveFile(params ...string) string
|
||||
SelectFile(params ...string) string
|
||||
SelectDirectory(params ...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,
|
||||
}
|
||||
}
|
||||
|
||||
// processTitleAndFilter return the title and filter from the given params.
|
||||
// title is the first string, filter is the second
|
||||
func (r *dialog) processTitleAndFilter(params ...string) (string, string) {
|
||||
|
||||
var title, filter string
|
||||
|
||||
if len(params) > 0 {
|
||||
title = params[0]
|
||||
}
|
||||
|
||||
if len(params) > 1 {
|
||||
filter = params[1]
|
||||
}
|
||||
|
||||
return title, filter
|
||||
}
|
||||
|
||||
// SelectFile prompts the user to select a file
|
||||
func (r *dialog) SelectFile(params ...string) string {
|
||||
|
||||
// Extract title + filter
|
||||
title, filter := r.processTitleAndFilter(params...)
|
||||
|
||||
// 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
|
||||
message := "dialog:select:file:" + title
|
||||
if filter != "" {
|
||||
message += ":" + filter
|
||||
}
|
||||
r.bus.Publish(message, 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(params ...string) string {
|
||||
|
||||
// Extract title + filter
|
||||
title, filter := r.processTitleAndFilter(params...)
|
||||
|
||||
// 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
|
||||
message := "dialog:select:filesave:" + title
|
||||
if filter != "" {
|
||||
message += ":" + filter
|
||||
}
|
||||
r.bus.Publish(message, 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(params ...string) string {
|
||||
|
||||
// Extract title + filter
|
||||
title, filter := r.processTitleAndFilter(params...)
|
||||
|
||||
// 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
|
||||
message := "dialog:select:directory:" + title
|
||||
if filter != "" {
|
||||
message += ":" + filter
|
||||
}
|
||||
r.bus.Publish(message, responseTopic)
|
||||
|
||||
// Wait for result
|
||||
var result *servicebus.Message = <-dialogResponseChannel
|
||||
|
||||
// Delete subscription to response topic
|
||||
r.bus.UnSubscribe(responseTopic)
|
||||
|
||||
return result.Data().(string)
|
||||
}
|
43
v2/internal/runtime/goruntime/events.go
Normal file
43
v2/internal/runtime/goruntime/events.go
Normal file
@ -0,0 +1,43 @@
|
||||
package goruntime
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
)
|
||||
|
||||
// Events defines all events related operations
|
||||
type Events interface {
|
||||
On(eventName string, callback func(optionalData ...interface{}))
|
||||
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)
|
||||
}
|
28
v2/internal/runtime/goruntime/runtime.go
Normal file
28
v2/internal/runtime/goruntime/runtime.go
Normal file
@ -0,0 +1,28 @@
|
||||
package goruntime
|
||||
|
||||
import "github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
|
||||
// Runtime is a means for the user to interact with the application at runtime
|
||||
type Runtime struct {
|
||||
Browser Browser
|
||||
Events Events
|
||||
Window Window
|
||||
Dialog Dialog
|
||||
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()")
|
||||
}
|
102
v2/internal/runtime/goruntime/window.go
Normal file
102
v2/internal/runtime/goruntime/window.go
Normal file
@ -0,0 +1,102 @@
|
||||
package goruntime
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
)
|
||||
|
||||
// Window defines all Window related operations
|
||||
type Window interface {
|
||||
Close()
|
||||
Show()
|
||||
Hide()
|
||||
Maximise()
|
||||
Unmaximise()
|
||||
Minimise()
|
||||
Unminimise()
|
||||
SetTitle(title string)
|
||||
Fullscreen()
|
||||
UnFullscreen()
|
||||
SetColour(colour 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)
|
||||
}
|
||||
|
||||
// Show shows the window if hidden
|
||||
func (w *window) Show() {
|
||||
w.bus.Publish("window:show", "")
|
||||
}
|
||||
|
||||
// Hide the window
|
||||
func (w *window) Hide() {
|
||||
w.bus.Publish("window:hide", "")
|
||||
}
|
||||
|
||||
// SetSize sets the size of the window
|
||||
func (w *window) SetSize(width int, height int) {
|
||||
size := []int{width, height}
|
||||
w.bus.Publish("window:setsize", size)
|
||||
}
|
||||
|
||||
// SetPosition sets the position of the window
|
||||
func (w *window) SetPosition(x int, y int) {
|
||||
position := []int{x, y}
|
||||
w.bus.Publish("window:position", position)
|
||||
}
|
||||
|
||||
// Maximise the window
|
||||
func (w *window) Maximise() {
|
||||
w.bus.Publish("window:maximise", "")
|
||||
}
|
||||
|
||||
// Unmaximise the window
|
||||
func (w *window) Unmaximise() {
|
||||
w.bus.Publish("window:unmaximise", "")
|
||||
}
|
||||
|
||||
// Minimise the window
|
||||
func (w *window) Minimise() {
|
||||
w.bus.Publish("window:minimise", "")
|
||||
}
|
||||
|
||||
// Unminimise the window
|
||||
func (w *window) Unminimise() {
|
||||
w.bus.Publish("window:unminimise", "")
|
||||
}
|
24
v2/internal/runtime/js/.eslintrc
Normal file
24
v2/internal/runtime/js/.eslintrc
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
22
v2/internal/runtime/js/babel.config.js
Normal file
22
v2/internal/runtime/js/babel.config.js
Normal 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,
|
||||
};
|
||||
}
|
107
v2/internal/runtime/js/core/bindings.js
Normal file
107
v2/internal/runtime/js/core/bindings.js
Normal 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;
|
||||
// }
|
||||
// }
|
||||
|
34
v2/internal/runtime/js/core/browser.js
Normal file
34
v2/internal/runtime/js/core/browser.js
Normal 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);
|
||||
}
|
156
v2/internal/runtime/js/core/calls.js
Normal file
156
v2/internal/runtime/js/core/calls.js
Normal 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);
|
||||
}
|
35
v2/internal/runtime/js/core/desktop.js
Normal file
35
v2/internal/runtime/js/core/desktop.js
Normal 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');
|
||||
|
202
v2/internal/runtime/js/core/events.js
Normal file
202
v2/internal/runtime/js/core/events.js
Normal 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}'`);
|
||||
}
|
||||
}
|
76
v2/internal/runtime/js/core/log.js
Normal file
76
v2/internal/runtime/js/core/log.js
Normal 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);
|
||||
}
|
49
v2/internal/runtime/js/core/main.js
Normal file
49
v2/internal/runtime/js/core/main.js
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
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 * as Window from './window';
|
||||
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,
|
||||
Window,
|
||||
Events: {
|
||||
On,
|
||||
OnMultiple,
|
||||
Emit,
|
||||
Heartbeat,
|
||||
Acknowledge,
|
||||
},
|
||||
_: {
|
||||
Callback,
|
||||
Notify,
|
||||
AddScript,
|
||||
InjectCSS,
|
||||
Init,
|
||||
AddIPCListener
|
||||
}
|
||||
};
|
||||
|
||||
// Do platform specific Init
|
||||
Platform.Init();
|
||||
}
|
182
v2/internal/runtime/js/core/server.js
Normal file
182
v2/internal/runtime/js/core/server.js
Normal 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();
|
38
v2/internal/runtime/js/core/utils.js
Normal file
38
v2/internal/runtime/js/core/utils.js
Normal 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);
|
||||
}
|
||||
}
|
107
v2/internal/runtime/js/core/window.js
Normal file
107
v2/internal/runtime/js/core/window.js
Normal file
@ -0,0 +1,107 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The lightweight framework for web-like apps
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
/* jshint esversion: 6 */
|
||||
|
||||
import { SendMessage } from 'ipc';
|
||||
|
||||
/**
|
||||
* Place the window in the center of the screen
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function Center() {
|
||||
SendMessage('Wc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Size of the window
|
||||
*
|
||||
* @export
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
*/
|
||||
export function SetSize(width, height) {
|
||||
SendMessage('Ws:' + width + ':' + height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Position of the window
|
||||
*
|
||||
* @export
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
*/
|
||||
export function SetPosition(x, y) {
|
||||
SendMessage('Wp:' + x + ':' + y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the Window
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function Hide() {
|
||||
SendMessage('WH');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Window
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function Show() {
|
||||
SendMessage('WS');
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximise the Window
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function Maximise() {
|
||||
SendMessage('WM');
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmaximise the Window
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function Unmaximise() {
|
||||
SendMessage('WU');
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimise the Window
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function Minimise() {
|
||||
SendMessage('Wm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Unminimise the Window
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function Unminimise() {
|
||||
SendMessage('Wu');
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the Window
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export function Close() {
|
||||
SendMessage('WC');
|
||||
}
|
41
v2/internal/runtime/js/desktop/ipc.js
Normal file
41
v2/internal/runtime/js/desktop/ipc.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
41
v2/internal/runtime/js/desktop/linux.js
Normal file
41
v2/internal/runtime/js/desktop/linux.js
Normal 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
5988
v2/internal/runtime/js/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
v2/internal/runtime/js/package.json
Normal file
43
v2/internal/runtime/js/package.json
Normal 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"
|
||||
}
|
||||
}
|
1
v2/internal/runtime/js/package.json.md5
Normal file
1
v2/internal/runtime/js/package.json.md5
Normal file
@ -0,0 +1 @@
|
||||
77a32d4461a2cad598edfd551fe64dcd
|
1
v2/internal/runtime/js/runtime/.npmignore
Normal file
1
v2/internal/runtime/js/runtime/.npmignore
Normal file
@ -0,0 +1 @@
|
||||
bridge.js
|
3
v2/internal/runtime/js/runtime/README.md
Normal file
3
v2/internal/runtime/js/runtime/README.md
Normal 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.
|
37
v2/internal/runtime/js/runtime/browser.js
Normal file
37
v2/internal/runtime/js/runtime/browser.js
Normal 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
|
||||
};
|
90
v2/internal/runtime/js/runtime/events.js
Normal file
90
v2/internal/runtime/js/runtime/events.js
Normal 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
|
||||
};
|
21
v2/internal/runtime/js/runtime/init.js
Normal file
21
v2/internal/runtime/js/runtime/init.js
Normal 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;
|
70
v2/internal/runtime/js/runtime/log.js
Normal file
70
v2/internal/runtime/js/runtime/log.js
Normal 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
|
||||
};
|
22
v2/internal/runtime/js/runtime/main.js
Normal file
22
v2/internal/runtime/js/runtime/main.js
Normal 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
|
||||
};
|
28
v2/internal/runtime/js/runtime/package.json
Normal file
28
v2/internal/runtime/js/runtime/package.json
Normal 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"
|
||||
}
|
||||
}
|
26
v2/internal/runtime/js/runtime/runtime.d.ts
vendored
Normal file
26
v2/internal/runtime/js/runtime/runtime.d.ts
vendored
Normal 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;
|
||||
};
|
||||
};
|
||||
|
||||
|
52
v2/internal/runtime/js/server/ipc.js
Normal file
52
v2/internal/runtime/js/server/ipc.js
Normal 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);
|
||||
}
|
21
v2/internal/runtime/js/server/linux.js
Normal file
21
v2/internal/runtime/js/server/linux.js
Normal 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() { }
|
4
v2/internal/runtime/js/webpack.config.js
Normal file
4
v2/internal/runtime/js/webpack.config.js
Normal file
@ -0,0 +1,4 @@
|
||||
/* eslint-disable */
|
||||
module.exports = (env) => {
|
||||
return require(`./webpack.${env}.js`);
|
||||
};
|
51
v2/internal/runtime/js/webpack.desktop.js
Normal file
51
v2/internal/runtime/js/webpack.desktop.js
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
54
v2/internal/runtime/js/webpack.server.js
Normal file
54
v2/internal/runtime/js/webpack.server.js
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
43
v2/internal/servicebus/message.go
Normal file
43
v2/internal/servicebus/message.go
Normal 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
|
||||
}
|
192
v2/internal/servicebus/servicebus.go
Normal file
192
v2/internal/servicebus/servicebus.go
Normal file
@ -0,0 +1,192 @@
|
||||
package servicebus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/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
|
||||
}
|
230
v2/internal/servicebus/servicebus_test.go
Normal file
230
v2/internal/servicebus/servicebus_test.go
Normal file
@ -0,0 +1,230 @@
|
||||
package servicebus
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
}
|
90
v2/internal/shell/shell.go
Normal file
90
v2/internal/shell/shell.go
Normal 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
|
||||
}
|
68
v2/internal/signal/signal.go
Normal file
68
v2/internal/signal/signal.go
Normal file
@ -0,0 +1,68 @@
|
||||
package signal
|
||||
|
||||
import (
|
||||
"os"
|
||||
gosignal "os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/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")
|
||||
}()
|
||||
}
|
113
v2/internal/subsystem/binding.go
Normal file
113
v2/internal/subsystem/binding.go
Normal file
@ -0,0 +1,113 @@
|
||||
package subsystem
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v2/internal/binding"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/runtime/goruntime"
|
||||
"github.com/wailsapp/wails/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")
|
||||
}
|
148
v2/internal/subsystem/call.go
Normal file
148
v2/internal/subsystem/call.go
Normal file
@ -0,0 +1,148 @@
|
||||
package subsystem
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/binding"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
||||
"github.com/wailsapp/wails/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"`
|
||||
}
|
193
v2/internal/subsystem/event.go
Normal file
193
v2/internal/subsystem/event.go
Normal file
@ -0,0 +1,193 @@
|
||||
package subsystem
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
||||
"github.com/wailsapp/wails/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")
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user