Compare commits

...

43 Commits

Author SHA1 Message Date
Lea Anthony
5cd08e45f0 v2.0.0-alpha.20 2021-01-31 22:50:01 +11:00
Lea Anthony
c2d03f0e6e Update NSFontWeightRegular to float const 2021-01-31 22:49:29 +11:00
Lea Anthony
d3501f4cb7 v2.0.0-alpha.19 2021-01-31 21:59:16 +11:00
Lea Anthony
ee82cd25b7 Menu Items default to 12 pt on Mac 2021-01-31 21:58:41 +11:00
Lea Anthony
bbb07e17d9 v2.0.0-alpha.18 2021-01-31 21:20:40 +11:00
Lea Anthony
e6b40b55c4 Add nil guard for binding 2021-01-31 21:10:30 +11:00
Lea Anthony
7573f68df3 Add argument guard for methods 2021-01-31 15:35:33 +11:00
Lea Anthony
ceaacc7ba9 Re-added Store.Set() using deep copy 2021-01-30 21:36:59 +11:00
Lea Anthony
0e24f75753 Deep copy initial value in store 2021-01-30 21:29:36 +11:00
Lea Anthony
82b9deeee4 Unexport set 2021-01-30 20:47:10 +11:00
Lea Anthony
cfa40b797f More race condition fixes 2021-01-29 13:03:05 +11:00
Lea Anthony
5aeb68acb7 Fix for calculating new window size 2021-01-28 19:26:26 +11:00
Lea Anthony
b81101414f Support Min/Max Size in Window runtime 2021-01-28 18:48:07 +11:00
Lea Anthony
7ae89d04bb Fixed data race in store. Handle nils better 2021-01-28 07:15:29 +11:00
Lea Anthony
1c566f3802 Fixed data race in servicebus 2021-01-28 06:23:49 +11:00
Lea Anthony
c9c3c9ab90 Don't bind startup/shutdown methods 2021-01-27 21:12:17 +11:00
Lea Anthony
56394ac50e Use mutex when doing a Store resync 2021-01-27 06:03:17 +11:00
Lea Anthony
f7c2f12ab2 Added error handler for dealing with loading user js code 2021-01-26 16:01:06 +11:00
Lea Anthony
a6bb6e0c93 v2.0.0-alpha.17 2021-01-26 12:30:45 +11:00
Lea Anthony
4a5863e6ac Ported update command 2021-01-26 12:30:14 +11:00
Lea Anthony
913fe8d184 v2.0.0-alpha.16 2021-01-26 11:11:23 +11:00
Lea Anthony
4ce8130cdf Replace CreateApp with single Run() method 2021-01-26 11:09:17 +11:00
Lea Anthony
fe87463b78 Move Bind() into app config 2021-01-26 07:04:12 +11:00
Lea Anthony
fe0f0e29e8 Update CreateApp API 2021-01-26 06:45:23 +11:00
Lea Anthony
83d6dac7cf Add appType to builds. Update server code to compile 2021-01-26 06:40:41 +11:00
Lea Anthony
02500e0930 Update vanilla template with new API 2021-01-26 06:40:18 +11:00
Lea Anthony
5e1187f437 Startup/Shutdown hook warnings 2021-01-26 06:39:54 +11:00
Lea Anthony
064ff3b65e Change build wording 2021-01-26 06:38:54 +11:00
Lea Anthony
b5c7019bf0 Fix compile warnings 2021-01-26 06:37:33 +11:00
Lea Anthony
e9d16e77a3 Add support for loglevel flag in debug builds 2021-01-25 21:42:31 +11:00
Lea Anthony
2415d4c531 v2.0.0-alpha.15 2021-01-25 21:21:44 +11:00
Lea Anthony
3f75213ce3 Small linting fixes 2021-01-25 21:05:20 +11:00
Lea Anthony
6120ceabf1 Support Fonts & Colours for menuitems 2021-01-25 21:04:57 +11:00
Lea Anthony
95a95d1750 Ensure store does initial resync 2021-01-25 21:02:36 +11:00
Lea Anthony
d923e84456 v2.0.0-alpha.14 2021-01-23 21:00:04 +11:00
Lea Anthony
343f573e78 Fix menu tooltips 2021-01-23 20:59:02 +11:00
Lea Anthony
c6d87da4f0 v2.0.0-alpha.13 2021-01-23 20:39:43 +11:00
Lea Anthony
a9faebe51a MenuItem image support 2021-01-23 20:32:42 +11:00
Lea Anthony
d436f5d1be v2.0.0-alpha.12 2021-01-23 16:22:00 +11:00
Lea Anthony
f40899821f Support ToolTips 2021-01-23 16:14:48 +11:00
Lea Anthony
2a64ed19a3 v2.0.0-alpha.11 2021-01-22 14:57:50 +11:00
Lea Anthony
47bca0be88 Support updating tray labels in an efficient manner 2021-01-16 11:35:49 +11:00
Lea Anthony
7ac8cc6b8b Add Menu.Merge() to combine 2 menus 2021-01-15 11:53:55 +11:00
61 changed files with 2444 additions and 188 deletions

View File

@@ -26,7 +26,7 @@ export function OpenURL(url) {
* Opens the given filename using the system's default file handler * Opens the given filename using the system's default file handler
* *
* @export * @export
* @param {sting} filename * @param {string} filename
* @returns * @returns
*/ */
export function OpenFile(filename) { export function OpenFile(filename) {

View File

@@ -62,7 +62,7 @@ if (window.crypto) {
export function Call(bindingName, data, timeout) { export function Call(bindingName, data, timeout) {
// Timeout infinite by default // Timeout infinite by default
if (timeout == null || timeout == undefined) { if (timeout == null) {
timeout = 0; timeout = 0;
} }

View File

@@ -45,7 +45,7 @@ function Invoke(message) {
* *
* @export * @export
* @param {string} type * @param {string} type
* @param {string} payload * @param {Object} payload
* @param {string=} callbackID * @param {string=} callbackID
*/ */
export function SendMessage(type, payload, callbackID) { export function SendMessage(type, payload, callbackID) {

View File

@@ -0,0 +1,164 @@
package update
import (
"fmt"
"io"
"log"
"os"
"github.com/wailsapp/wails/v2/internal/shell"
"github.com/wailsapp/wails/v2/internal/github"
"github.com/leaanthony/clir"
"github.com/wailsapp/wails/v2/pkg/clilogger"
)
// AddSubcommand adds the `init` command for the Wails application
func AddSubcommand(app *clir.Cli, w io.Writer, currentVersion string) error {
command := app.NewSubCommand("update", "Update the Wails CLI")
command.LongDescription(`This command allows you to update your version of Wails.`)
// Setup flags
var prereleaseRequired bool
command.BoolFlag("pre", "Update to latest Prerelease", &prereleaseRequired)
var specificVersion string
command.StringFlag("version", "Install a specific version (Overrides other flags)", &specificVersion)
command.Action(func() error {
// Create logger
logger := clilogger.New(w)
// Print banner
app.PrintBanner()
logger.Println("Checking for updates...")
var desiredVersion *github.SemanticVersion
var err error
var valid bool
if len(specificVersion) > 0 {
// Check if this is a valid version
valid, err = github.IsValidTag(specificVersion)
if err == nil {
if !valid {
err = fmt.Errorf("version '%s' is invalid", specificVersion)
} else {
desiredVersion, err = github.NewSemanticVersion(specificVersion)
}
}
} else {
if prereleaseRequired {
desiredVersion, err = github.GetLatestPreRelease()
} else {
desiredVersion, err = github.GetLatestStableRelease()
}
}
if err != nil {
return err
}
fmt.Println()
fmt.Println(" Current Version : " + currentVersion)
if len(specificVersion) > 0 {
fmt.Printf(" Desired Version : v%s\n", desiredVersion)
} else {
if prereleaseRequired {
fmt.Printf(" Latest Prerelease : v%s\n", desiredVersion)
} else {
fmt.Printf(" Latest Release : v%s\n", desiredVersion)
}
}
return updateToVersion(logger, desiredVersion, len(specificVersion) > 0, currentVersion)
})
return nil
}
func updateToVersion(logger *clilogger.CLILogger, targetVersion *github.SemanticVersion, force bool, currentVersion string) error {
var targetVersionString = "v" + targetVersion.String()
// Early exit
if targetVersionString == currentVersion {
logger.Println("Looks like you're up to date!")
return nil
}
var desiredVersion string
if !force {
compareVersion := currentVersion
currentVersion, err := github.NewSemanticVersion(compareVersion)
if err != nil {
return err
}
var success bool
// Release -> Pre-Release = Massage current version to prerelease format
if targetVersion.IsPreRelease() && currentVersion.IsRelease() {
testVersion, err := github.NewSemanticVersion(compareVersion + "-0")
if err != nil {
return err
}
success, _ = targetVersion.IsGreaterThan(testVersion)
}
// Pre-Release -> Release = Massage target version to prerelease format
if targetVersion.IsRelease() && currentVersion.IsPreRelease() {
// We are ok with greater than or equal
mainversion := currentVersion.MainVersion()
targetVersion, err = github.NewSemanticVersion(targetVersion.String())
if err != nil {
return err
}
success, _ = targetVersion.IsGreaterThanOrEqual(mainversion)
}
// Release -> Release = Standard check
if (targetVersion.IsRelease() && currentVersion.IsRelease()) ||
(targetVersion.IsPreRelease() && currentVersion.IsPreRelease()) {
success, _ = targetVersion.IsGreaterThan(currentVersion)
}
// Compare
if !success {
logger.Println("Error: The requested version is lower than the current version.")
logger.Println("If this is what you really want to do, use `wails update -version %s`", targetVersionString)
return nil
}
desiredVersion = "v" + targetVersion.String()
} else {
desiredVersion = "v" + targetVersion.String()
}
fmt.Println()
logger.Print("Installing Wails " + desiredVersion + "...")
// Run command in non module directory
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatal("Cannot find home directory! Please file a bug report!")
}
sout, serr, err := shell.RunCommand(homeDir, "go", "get", "github.com/wailsapp/wails/v2/cmd/wails@"+desiredVersion)
if err != nil {
logger.Println("Failed.")
logger.Println(sout + `\n` + serr)
return err
}
fmt.Println()
logger.Println("Wails updated to " + desiredVersion)
return nil
}

View File

@@ -3,6 +3,8 @@ package main
import ( import (
"os" "os"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/update"
"github.com/leaanthony/clir" "github.com/leaanthony/clir"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/build" "github.com/wailsapp/wails/v2/cmd/wails/internal/commands/build"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/debug" "github.com/wailsapp/wails/v2/cmd/wails/internal/commands/debug"
@@ -48,6 +50,11 @@ func main() {
fatal(err.Error()) fatal(err.Error())
} }
err = update.AddSubcommand(app, os.Stdout, version)
if err != nil {
fatal(err.Error())
}
err = app.Run() err = app.Run()
if err != nil { if err != nil {
println("\n\nERROR: " + err.Error()) println("\n\nERROR: " + err.Error())

View File

@@ -1,3 +1,3 @@
package main package main
var version = "v2.0.0-alpha.10" var version = "v2.0.0-alpha.20"

View File

@@ -3,6 +3,7 @@ module github.com/wailsapp/wails/v2
go 1.15 go 1.15
require ( require (
github.com/Masterminds/semver v1.5.0
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/fatih/structtag v1.2.0 github.com/fatih/structtag v1.2.0
github.com/fsnotify/fsnotify v1.4.9 github.com/fsnotify/fsnotify v1.4.9

View File

@@ -1,3 +1,5 @@
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View File

@@ -2,11 +2,39 @@
package app package app
import (
"flag"
"github.com/wailsapp/wails/v2/pkg/logger"
"strings"
)
// Init initialises the application for a debug environment // Init initialises the application for a debug environment
func (a *App) Init() error { func (a *App) Init() error {
// Indicate debug mode // Indicate debug mode
a.debug = true a.debug = true
// Enable dev tools
a.options.DevTools = true if a.appType == "desktop" {
// Enable dev tools
a.options.DevTools = true
}
// Set log levels
greeting := flag.String("loglevel", "debug", "Loglevel to use - Trace, Debug, Info, Warning, Error")
flag.Parse()
if len(*greeting) > 0 {
switch strings.ToLower(*greeting) {
case "trace":
a.logger.SetLogLevel(logger.TRACE)
case "info":
a.logger.SetLogLevel(logger.INFO)
case "warning":
a.logger.SetLogLevel(logger.WARNING)
case "error":
a.logger.SetLogLevel(logger.ERROR)
default:
a.logger.SetLogLevel(logger.DEBUG)
}
}
return nil return nil
} }

View File

@@ -8,9 +8,10 @@ package app
// will be unknown and the application will not work as expected. // will be unknown and the application will not work as expected.
import ( import (
"github.com/wailsapp/wails/v2/internal/logger"
"os" "os"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options"
) )
@@ -38,7 +39,3 @@ func (a *App) Run() error {
os.Exit(1) os.Exit(1)
return nil return nil
} }
// Bind the dummy interface
func (a *App) Bind(_ interface{}) {
}

View File

@@ -17,6 +17,8 @@ import (
// App defines a Wails application structure // App defines a Wails application structure
type App struct { type App struct {
appType string
window *ffenestri.Application window *ffenestri.Application
servicebus *servicebus.ServiceBus servicebus *servicebus.ServiceBus
logger *logger.Logger logger *logger.Logger
@@ -79,11 +81,15 @@ func CreateApp(appoptions *options.App) (*App, error) {
window := ffenestri.NewApplicationWithConfig(appoptions, myLogger, menuManager) window := ffenestri.NewApplicationWithConfig(appoptions, myLogger, menuManager)
// Create binding exemptions - Ugly hack. There must be a better way
bindingExemptions := []interface{}{appoptions.Startup, appoptions.Shutdown}
result := &App{ result := &App{
appType: "desktop",
window: window, window: window,
servicebus: servicebus.New(myLogger), servicebus: servicebus.New(myLogger),
logger: myLogger, logger: myLogger,
bindings: binding.NewBindings(myLogger), bindings: binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions),
menuManager: menuManager, menuManager: menuManager,
startupCallback: appoptions.Startup, startupCallback: appoptions.Startup,
shutdownCallback: appoptions.Shutdown, shutdownCallback: appoptions.Shutdown,
@@ -213,14 +219,3 @@ func (a *App) Run() error {
return result 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())
}
}

View File

@@ -75,7 +75,7 @@ func CreateApp(options *Options) *App {
webserver: webserver.NewWebServer(myLogger), webserver: webserver.NewWebServer(myLogger),
servicebus: servicebus.New(myLogger), servicebus: servicebus.New(myLogger),
logger: myLogger, logger: myLogger,
bindings: binding.NewBindings(myLogger), bindings: binding.NewBindings(myLogger, options.Bind),
} }
// Initialise the app // Initialise the app
@@ -192,14 +192,3 @@ func (a *App) Run() error {
return cli.Run() return cli.Run()
} }
// Bind a struct to the application by passing in
// a pointer to it
func (a *App) Bind(structPtr interface{}) {
// Add the struct to the bindings
err := a.bindings.Add(structPtr)
if err != nil {
a.logger.Fatal("Error during binding: " + err.Error())
}
}

View File

@@ -6,10 +6,13 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/leaanthony/clir" "github.com/leaanthony/clir"
"github.com/wailsapp/wails/v2/internal/binding" "github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/logger" "github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher" "github.com/wailsapp/wails/v2/internal/messagedispatcher"
"github.com/wailsapp/wails/v2/internal/runtime"
"github.com/wailsapp/wails/v2/internal/servicebus" "github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/internal/subsystem" "github.com/wailsapp/wails/v2/internal/subsystem"
"github.com/wailsapp/wails/v2/internal/webserver" "github.com/wailsapp/wails/v2/internal/webserver"
@@ -17,12 +20,16 @@ import (
// App defines a Wails application structure // App defines a Wails application structure
type App struct { type App struct {
appType string
binding *subsystem.Binding binding *subsystem.Binding
call *subsystem.Call call *subsystem.Call
event *subsystem.Event event *subsystem.Event
log *subsystem.Log log *subsystem.Log
runtime *subsystem.Runtime runtime *subsystem.Runtime
options *options.App
bindings *binding.Bindings bindings *binding.Bindings
logger *logger.Logger logger *logger.Logger
dispatcher *messagedispatcher.Dispatcher dispatcher *messagedispatcher.Dispatcher
@@ -30,28 +37,40 @@ type App struct {
webserver *webserver.WebServer webserver *webserver.WebServer
debug bool debug bool
// Application Stores
loglevelStore *runtime.Store
appconfigStore *runtime.Store
// Startup/Shutdown
startupCallback func(*runtime.Runtime)
shutdownCallback func()
} }
// Create App // Create App
func CreateApp(options *Options) *App { func CreateApp(appoptions *options.App) (*App, error) {
options.mergeDefaults()
// We ignore the inputs (for now)
// TODO: Allow logger output override on CLI // Merge default options
myLogger := logger.New(os.Stdout) options.MergeDefaults(appoptions)
myLogger.SetLogLevel(logger.TRACE)
// Set up logger
myLogger := logger.New(appoptions.Logger)
myLogger.SetLogLevel(appoptions.LogLevel)
result := &App{ result := &App{
bindings: binding.NewBindings(myLogger), appType: "server",
logger: myLogger, bindings: binding.NewBindings(myLogger, options.Bind),
servicebus: servicebus.New(myLogger), logger: myLogger,
webserver: webserver.NewWebServer(myLogger), servicebus: servicebus.New(myLogger),
webserver: webserver.NewWebServer(myLogger),
startupCallback: appoptions.Startup,
shutdownCallback: appoptions.Shutdown,
} }
// Initialise app // Initialise app
result.Init() result.Init()
return result return result, nil
} }
// Run the application // Run the application
@@ -88,8 +107,21 @@ func (a *App) Run() error {
if debugMode { if debugMode {
a.servicebus.Debug() a.servicebus.Debug()
} }
// Start the runtime
runtime, err := subsystem.NewRuntime(a.servicebus, a.logger, a.startupCallback, a.shutdownCallback)
if err != nil {
return err
}
a.runtime = runtime
a.runtime.Start()
// Application Stores
a.loglevelStore = a.runtime.GoRuntime().Store.New("wails:loglevel", a.options.LogLevel)
a.appconfigStore = a.runtime.GoRuntime().Store.New("wails:appconfig", a.options)
a.servicebus.Start() a.servicebus.Start()
log, err := subsystem.NewLog(a.servicebus, a.logger) log, err := subsystem.NewLog(a.servicebus, a.logger, a.loglevelStore)
if err != nil { if err != nil {
return err return err
} }
@@ -102,14 +134,6 @@ func (a *App) Run() error {
a.dispatcher = dispatcher a.dispatcher = dispatcher
a.dispatcher.Start() 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 // Start the binding subsystem
binding, err := subsystem.NewBinding(a.servicebus, a.logger, a.bindings, runtime.GoRuntime()) binding, err := subsystem.NewBinding(a.servicebus, a.logger, a.bindings, runtime.GoRuntime())
if err != nil { if err != nil {
@@ -127,7 +151,7 @@ func (a *App) Run() error {
a.event.Start() a.event.Start()
// Start the call subsystem // Start the call subsystem
call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB()) call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB(), a.runtime.GoRuntime())
if err != nil { if err != nil {
return err return err
} }
@@ -147,14 +171,3 @@ func (a *App) Run() error {
return cli.Run() return cli.Run()
} }
// Bind a struct to the application by passing in
// a pointer to it
func (a *App) Bind(structPtr interface{}) {
// Add the struct to the bindings
err := a.bindings.Add(structPtr)
if err != nil {
a.logger.Fatal("Error during binding: " + err.Error())
}
}

View File

@@ -2,28 +2,53 @@ package binding
import ( import (
"fmt" "fmt"
"reflect"
"runtime"
"strings" "strings"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/internal/logger" "github.com/wailsapp/wails/v2/internal/logger"
) )
type Bindings struct { type Bindings struct {
db *DB db *DB
logger logger.CustomLogger logger logger.CustomLogger
exemptions slicer.StringSlicer
} }
// NewBindings returns a new Bindings object // NewBindings returns a new Bindings object
func NewBindings(logger *logger.Logger) *Bindings { func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exemptions []interface{}) *Bindings {
return &Bindings{ result := &Bindings{
db: newDB(), db: newDB(),
logger: logger.CustomLogger("Bindings"), logger: logger.CustomLogger("Bindings"),
} }
for _, exemption := range exemptions {
if exemptions == nil {
continue
}
name := runtime.FuncForPC(reflect.ValueOf(exemption).Pointer()).Name()
// Yuk yuk yuk! Is there a better way?
name = strings.TrimSuffix(name, "-fm")
result.exemptions.Add(name)
}
// Add the structs to bind
for _, ptr := range structPointersToBind {
err := result.Add(ptr)
if err != nil {
logger.Fatal("Error during binding: " + err.Error())
}
}
return result
} }
// Add the given struct methods to the Bindings // Add the given struct methods to the Bindings
func (b *Bindings) Add(structPtr interface{}) error { func (b *Bindings) Add(structPtr interface{}) error {
methods, err := getMethods(structPtr) methods, err := b.getMethods(structPtr)
if err != nil { if err != nil {
return fmt.Errorf("cannot bind value to app: %s", err.Error()) return fmt.Errorf("cannot bind value to app: %s", err.Error())
} }
@@ -36,7 +61,6 @@ func (b *Bindings) Add(structPtr interface{}) error {
// Add it as a regular method // Add it as a regular method
b.db.AddMethod(packageName, structName, methodName, method) b.db.AddMethod(packageName, structName, methodName, method)
} }
return nil return nil
} }

View File

@@ -30,6 +30,9 @@ func (b *BoundMethod) OutputCount() int {
func (b *BoundMethod) ParseArgs(args []json.RawMessage) ([]interface{}, error) { func (b *BoundMethod) ParseArgs(args []json.RawMessage) ([]interface{}, error) {
result := make([]interface{}, b.InputCount()) result := make([]interface{}, b.InputCount())
if len(args) != b.InputCount() {
return nil, fmt.Errorf("received %d arguments to method '%s', expected %d", len(args), b.Name, b.InputCount())
}
for index, arg := range args { for index, arg := range args {
typ := b.Inputs[index].reflectType typ := b.Inputs[index].reflectType
inputValue := reflect.New(typ).Interface() inputValue := reflect.New(typ).Interface()

View File

@@ -23,7 +23,7 @@ func isStruct(value interface{}) bool {
return reflect.ValueOf(value).Kind() == reflect.Struct return reflect.ValueOf(value).Kind() == reflect.Struct
} }
func getMethods(value interface{}) ([]*BoundMethod, error) { func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) {
// Create result placeholder // Create result placeholder
var result []*BoundMethod var result []*BoundMethod
@@ -56,6 +56,11 @@ func getMethods(value interface{}) ([]*BoundMethod, error) {
fullMethodName := baseName + "." + methodName fullMethodName := baseName + "." + methodName
method := structValue.MethodByName(methodName) method := structValue.MethodByName(methodName)
methodReflectName := runtime.FuncForPC(methodDef.Func.Pointer()).Name()
if b.exemptions.Contains(methodReflectName) {
continue
}
// Create new method // Create new method
boundMethod := &BoundMethod{ boundMethod := &BoundMethod{
Name: fullMethodName, Name: fullMethodName,

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Joel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,125 @@
// deepcopy makes deep copies of things. A standard copy will copy the
// pointers: deep copy copies the values pointed to. Unexported field
// values are not copied.
//
// Copyright (c)2014-2016, Joel Scoble (github.com/mohae), all rights reserved.
// License: MIT, for more details check the included LICENSE file.
package deepcopy
import (
"reflect"
"time"
)
// Interface for delegating copy process to type
type Interface interface {
DeepCopy() interface{}
}
// Iface is an alias to Copy; this exists for backwards compatibility reasons.
func Iface(iface interface{}) interface{} {
return Copy(iface)
}
// Copy creates a deep copy of whatever is passed to it and returns the copy
// in an interface{}. The returned value will need to be asserted to the
// correct type.
func Copy(src interface{}) interface{} {
if src == nil {
return nil
}
// Make the interface a reflect.Value
original := reflect.ValueOf(src)
// Make a copy of the same type as the original.
cpy := reflect.New(original.Type()).Elem()
// Recursively copy the original.
copyRecursive(original, cpy)
// Return the copy as an interface.
return cpy.Interface()
}
// copyRecursive does the actual copying of the interface. It currently has
// limited support for what it can handle. Add as needed.
func copyRecursive(original, cpy reflect.Value) {
// check for implement deepcopy.Interface
if original.CanInterface() {
if copier, ok := original.Interface().(Interface); ok {
cpy.Set(reflect.ValueOf(copier.DeepCopy()))
return
}
}
// handle according to original's Kind
switch original.Kind() {
case reflect.Ptr:
// Get the actual value being pointed to.
originalValue := original.Elem()
// if it isn't valid, return.
if !originalValue.IsValid() {
return
}
cpy.Set(reflect.New(originalValue.Type()))
copyRecursive(originalValue, cpy.Elem())
case reflect.Interface:
// If this is a nil, don't do anything
if original.IsNil() {
return
}
// Get the value for the interface, not the pointer.
originalValue := original.Elem()
// Get the value by calling Elem().
copyValue := reflect.New(originalValue.Type()).Elem()
copyRecursive(originalValue, copyValue)
cpy.Set(copyValue)
case reflect.Struct:
t, ok := original.Interface().(time.Time)
if ok {
cpy.Set(reflect.ValueOf(t))
return
}
// Go through each field of the struct and copy it.
for i := 0; i < original.NumField(); i++ {
// The Type's StructField for a given field is checked to see if StructField.PkgPath
// is set to determine if the field is exported or not because CanSet() returns false
// for settable fields. I'm not sure why. -mohae
if original.Type().Field(i).PkgPath != "" {
continue
}
copyRecursive(original.Field(i), cpy.Field(i))
}
case reflect.Slice:
if original.IsNil() {
return
}
// Make a new slice and copy each element.
cpy.Set(reflect.MakeSlice(original.Type(), original.Len(), original.Cap()))
for i := 0; i < original.Len(); i++ {
copyRecursive(original.Index(i), cpy.Index(i))
}
case reflect.Map:
if original.IsNil() {
return
}
cpy.Set(reflect.MakeMap(original.Type()))
for _, key := range original.MapKeys() {
originalValue := original.MapIndex(key)
copyValue := reflect.New(originalValue.Type()).Elem()
copyRecursive(originalValue, copyValue)
copyKey := Copy(key.Interface())
cpy.SetMapIndex(reflect.ValueOf(copyKey), copyValue)
}
default:
cpy.Set(original)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -86,3 +86,10 @@ bool getJSONInt(JsonNode *item, const char* key, int *result) {
return false; return false;
} }
JsonNode* mustParseJSON(const char* JSON) {
JsonNode* parsedUpdate = json_decode(JSON);
if ( parsedUpdate == NULL ) {
ABORT("Unable to decode JSON: %s\n", JSON);
}
return parsedUpdate;
}

View File

@@ -35,4 +35,6 @@ JsonNode* mustJSONObject(JsonNode *node, const char* key);
bool getJSONBool(JsonNode *item, const char* key, bool *result); bool getJSONBool(JsonNode *item, const char* key, bool *result);
bool getJSONInt(JsonNode *item, const char* key, int *result); bool getJSONInt(JsonNode *item, const char* key, int *result);
JsonNode* mustParseJSON(const char* JSON);
#endif //ASSETS_C_COMMON_H #endif //ASSETS_C_COMMON_H

View File

@@ -37,6 +37,7 @@ extern void DarkModeEnabled(struct Application*, char *callbackID);
extern void SetApplicationMenu(struct Application*, const char *); extern void SetApplicationMenu(struct Application*, const char *);
extern void AddTrayMenu(struct Application*, const char *menuTrayJSON); extern void AddTrayMenu(struct Application*, const char *menuTrayJSON);
extern void SetTrayMenu(struct Application*, const char *menuTrayJSON); extern void SetTrayMenu(struct Application*, const char *menuTrayJSON);
extern void UpdateTrayMenuLabel(struct Application*, const char* JSON);
extern void AddContextMenu(struct Application*, char *contextMenuJSON); extern void AddContextMenu(struct Application*, char *contextMenuJSON);
extern void UpdateContextMenu(struct Application*, char *contextMenuJSON); extern void UpdateContextMenu(struct Application*, char *contextMenuJSON);

View File

@@ -12,9 +12,10 @@ package ffenestri
import "C" import "C"
import ( import (
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"strconv" "strconv"
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"github.com/wailsapp/wails/v2/internal/logger" "github.com/wailsapp/wails/v2/internal/logger"
) )
@@ -113,6 +114,14 @@ func (c *Client) WindowSize(width int, height int) {
C.SetSize(c.app.app, C.int(width), C.int(height)) C.SetSize(c.app.app, C.int(width), C.int(height))
} }
func (c *Client) WindowSetMinSize(width int, height int) {
C.SetMinWindowSize(c.app.app, C.int(width), C.int(height))
}
func (c *Client) WindowSetMaxSize(width int, height int) {
C.SetMaxWindowSize(c.app.app, C.int(width), C.int(height))
}
// WindowSetColour sets the window colour // WindowSetColour sets the window colour
func (c *Client) WindowSetColour(colour int) { func (c *Client) WindowSetColour(colour int) {
r, g, b, a := intToColour(colour) r, g, b, a := intToColour(colour)
@@ -192,6 +201,10 @@ func (c *Client) SetTrayMenu(trayMenuJSON string) {
C.SetTrayMenu(c.app.app, c.app.string2CString(trayMenuJSON)) C.SetTrayMenu(c.app.app, c.app.string2CString(trayMenuJSON))
} }
func (c *Client) UpdateTrayMenuLabel(JSON string) {
C.UpdateTrayMenuLabel(c.app.app, c.app.string2CString(JSON))
}
func (c *Client) UpdateContextMenu(contextMenuJSON string) { func (c *Client) UpdateContextMenu(contextMenuJSON string) {
C.UpdateContextMenu(c.app.app, c.app.string2CString(contextMenuJSON)) C.UpdateContextMenu(c.app.app, c.app.string2CString(contextMenuJSON))
} }

View File

@@ -140,6 +140,16 @@ void Debug(struct Application *app, const char *message, ... ) {
} }
} }
void Error(struct Application *app, const char *message, ... ) {
const char *temp = concat("LEFfenestri (C) | ", message);
va_list args;
va_start(args, message);
vsnprintf(logbuffer, MAXMESSAGE, temp, args);
app->sendMessageToBackend(&logbuffer[0]);
MEMFREE(temp);
va_end(args);
}
void Fatal(struct Application *app, const char *message, ... ) { void Fatal(struct Application *app, const char *message, ... ) {
const char *temp = concat("LFFfenestri (C) | ", message); const char *temp = concat("LFFfenestri (C) | ", message);
va_list args; va_list args;
@@ -150,6 +160,12 @@ void Fatal(struct Application *app, const char *message, ... ) {
va_end(args); va_end(args);
} }
// Requires NSString input EG lookupStringConstant(str("NSFontAttributeName"))
void* lookupStringConstant(id constantName) {
void ** dataPtr = CFBundleGetDataPointerForName(CFBundleGetBundleWithIdentifier((CFStringRef)str("com.apple.AppKit")), (CFStringRef) constantName);
return (dataPtr ? *dataPtr : nil);
}
bool isRetina(struct Application *app) { bool isRetina(struct Application *app) {
CGFloat scale = GET_BACKINGSCALEFACTOR(app->mainWindow); CGFloat scale = GET_BACKINGSCALEFACTOR(app->mainWindow);
if( (int)scale == 1 ) { if( (int)scale == 1 ) {
@@ -240,7 +256,10 @@ void messageHandler(id self, SEL cmd, id contentController, id message) {
struct Application *app = (struct Application *)objc_getAssociatedObject( struct Application *app = (struct Application *)objc_getAssociatedObject(
self, "application"); self, "application");
const char *name = (const char *)msg(msg(message, s("name")), s("UTF8String")); const char *name = (const char *)msg(msg(message, s("name")), s("UTF8String"));
if( strcmp(name, "completed") == 0) { if( strcmp(name, "error") == 0 ) {
printf("There was a Javascript error. Please open the devtools for more information.\n");
Show(app);
} else if( strcmp(name, "completed") == 0) {
// Delete handler // Delete handler
msg(app->manager, s("removeScriptMessageHandlerForName:"), str("completed")); msg(app->manager, s("removeScriptMessageHandlerForName:"), str("completed"));
@@ -444,6 +463,7 @@ void DestroyApplication(struct Application *app) {
msg(app->manager, s("removeScriptMessageHandlerForName:"), str("contextMenu")); msg(app->manager, s("removeScriptMessageHandlerForName:"), str("contextMenu"));
msg(app->manager, s("removeScriptMessageHandlerForName:"), str("windowDrag")); msg(app->manager, s("removeScriptMessageHandlerForName:"), str("windowDrag"));
msg(app->manager, s("removeScriptMessageHandlerForName:"), str("external")); msg(app->manager, s("removeScriptMessageHandlerForName:"), str("external"));
msg(app->manager, s("removeScriptMessageHandlerForName:"), str("error"));
// Close main window // Close main window
msg(app->mainWindow, s("close")); msg(app->mainWindow, s("close"));
@@ -879,6 +899,20 @@ void setMinMaxSize(struct Application *app)
{ {
msg(app->mainWindow, s("setMinSize:"), CGSizeMake(app->minWidth, app->minHeight)); msg(app->mainWindow, s("setMinSize:"), CGSizeMake(app->minWidth, app->minHeight));
} }
// Calculate if window needs resizing
int newWidth = app->width;
int newHeight = app->height;
if (app->maxWidth > 0 && app->width > app->maxWidth) newWidth = app->maxWidth;
if (app->minWidth > 0 && app->width < app->minWidth) newWidth = app->minWidth;
if (app->maxHeight > 0 && app->height > app->maxHeight ) newHeight = app->maxHeight;
if (app->minHeight > 0 && app->height < app->minHeight ) newHeight = app->minHeight;
// If we have any change, resize window
if ( newWidth != app->width || newHeight != app->height ) {
SetSize(app, newWidth, newHeight);
}
} }
void SetMinWindowSize(struct Application *app, int minWidth, int minHeight) void SetMinWindowSize(struct Application *app, int minWidth, int minHeight)
@@ -913,7 +947,8 @@ void SetDebug(void *applicationPointer, int flag) {
} }
// SetContextMenus sets the context menu map for this application
// AddContextMenu sets the context menu map for this application
void AddContextMenu(struct Application *app, const char *contextMenuJSON) { void AddContextMenu(struct Application *app, const char *contextMenuJSON) {
AddContextMenuToStore(app->contextMenuStore, contextMenuJSON); AddContextMenuToStore(app->contextMenuStore, contextMenuJSON);
} }
@@ -932,6 +967,13 @@ void SetTrayMenu(struct Application *app, const char* trayMenuJSON) {
); );
} }
void UpdateTrayMenuLabel(struct Application* app, const char* JSON) {
ON_MAIN_THREAD(
UpdateTrayMenuLabelInStore(app->trayMenuStore, JSON);
);
}
void SetBindings(struct Application *app, const char *bindings) { void SetBindings(struct Application *app, const char *bindings) {
const char* temp = concat("window.wailsbindings = \"", bindings); const char* temp = concat("window.wailsbindings = \"", bindings);
const char* jscall = concat(temp, "\";"); const char* jscall = concat(temp, "\";");
@@ -1520,6 +1562,7 @@ void Run(struct Application *app, int argc, char **argv) {
id manager = msg(config, s("userContentController")); id manager = msg(config, s("userContentController"));
msg(manager, s("addScriptMessageHandler:name:"), app->delegate, str("external")); msg(manager, s("addScriptMessageHandler:name:"), app->delegate, str("external"));
msg(manager, s("addScriptMessageHandler:name:"), app->delegate, str("completed")); msg(manager, s("addScriptMessageHandler:name:"), app->delegate, str("completed"));
msg(manager, s("addScriptMessageHandler:name:"), app->delegate, str("error"));
app->manager = manager; app->manager = manager;
id wkwebview = msg(c("WKWebView"), s("alloc")); id wkwebview = msg(c("WKWebView"), s("alloc"));

View File

@@ -2,7 +2,7 @@ package ffenestri
/* /*
#cgo darwin CFLAGS: -DFFENESTRI_DARWIN=1 #cgo darwin CFLAGS: -DFFENESTRI_DARWIN=1
#cgo darwin LDFLAGS: -framework WebKit -lobjc #cgo darwin LDFLAGS: -framework WebKit -framework CoreFoundation -lobjc
#include "ffenestri.h" #include "ffenestri.h"
#include "ffenestri_darwin.h" #include "ffenestri_darwin.h"

View File

@@ -6,6 +6,7 @@
#define OBJC_OLD_DISPATCH_PROTOTYPES 1 #define OBJC_OLD_DISPATCH_PROTOTYPES 1
#include <objc/objc-runtime.h> #include <objc/objc-runtime.h>
#include <CoreGraphics/CoreGraphics.h> #include <CoreGraphics/CoreGraphics.h>
#include <CoreFoundation/CoreFoundation.h>
#include "json.h" #include "json.h"
#include "hashmap.h" #include "hashmap.h"
#include "stdlib.h" #include "stdlib.h"
@@ -20,7 +21,6 @@
#define strunicode(input) msg(c("NSString"), s("stringWithFormat:"), str("%C"), (unsigned short)input) #define strunicode(input) msg(c("NSString"), s("stringWithFormat:"), str("%C"), (unsigned short)input)
#define cstr(input) (const char *)msg(input, s("UTF8String")) #define cstr(input) (const char *)msg(input, s("UTF8String"))
#define url(input) msg(c("NSURL"), s("fileURLWithPath:"), str(input)) #define url(input) msg(c("NSURL"), s("fileURLWithPath:"), str(input))
#define ALLOC(classname) msg(c(classname), s("alloc")) #define ALLOC(classname) msg(c(classname), s("alloc"))
#define ALLOC_INIT(classname) msg(msg(c(classname), s("alloc")), s("init")) #define ALLOC_INIT(classname) msg(msg(c(classname), s("alloc")), s("init"))
#define GET_FRAME(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("frame")) #define GET_FRAME(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("frame"))
@@ -107,7 +107,9 @@ void SetAppearance(struct Application* app, const char *);
void WebviewIsTransparent(struct Application* app); void WebviewIsTransparent(struct Application* app);
void WindowBackgroundIsTranslucent(struct Application* app); void WindowBackgroundIsTranslucent(struct Application* app);
void SetTray(struct Application* app, const char *, const char *, const char *); void SetTray(struct Application* app, const char *, const char *, const char *);
void SetContextMenus(struct Application* app, const char *); //void SetContextMenus(struct Application* app, const char *);
void AddTrayMenu(struct Application* app, const char *); void AddTrayMenu(struct Application* app, const char *);
void* lookupStringConstant(id constantName);
#endif #endif

View File

@@ -5,6 +5,7 @@
#include "ffenestri_darwin.h" #include "ffenestri_darwin.h"
#include "menu_darwin.h" #include "menu_darwin.h"
#include "contextmenus_darwin.h" #include "contextmenus_darwin.h"
#include "common.h"
// NewMenu creates a new Menu struct, saving the given menu structure as JSON // NewMenu creates a new Menu struct, saving the given menu structure as JSON
Menu* NewMenu(JsonNode *menuData) { Menu* NewMenu(JsonNode *menuData) {
@@ -63,8 +64,6 @@ MenuItemCallbackData* CreateMenuItemCallbackData(Menu *menu, id menuItem, const
return result; return result;
} }
void DeleteMenu(Menu *menu) { void DeleteMenu(Menu *menu) {
// Free menu item hashmap // Free menu item hashmap
@@ -99,7 +98,11 @@ void DeleteMenu(Menu *menu) {
// Creates a JSON message for the given menuItemID and data // Creates a JSON message for the given menuItemID and data
const char* createMenuClickedMessage(const char *menuItemID, const char *data, enum MenuType menuType, const char *parentID) { const char* createMenuClickedMessage(const char *menuItemID, const char *data, enum MenuType menuType, const char *parentID) {
JsonNode *jsonObject = json_mkobject(); JsonNode *jsonObject = json_mkobject();
if (menuItemID == NULL ) {
ABORT("Item ID NULL for menu!!\n");
}
json_append_member(jsonObject, "menuItemID", json_mkstring(menuItemID)); json_append_member(jsonObject, "menuItemID", json_mkstring(menuItemID));
json_append_member(jsonObject, "menuType", json_mkstring(MenuTypeAsString[(int)menuType])); json_append_member(jsonObject, "menuType", json_mkstring(MenuTypeAsString[(int)menuType]));
if (data != NULL) { if (data != NULL) {
@@ -572,7 +575,7 @@ id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const c
return item; return item;
} }
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers) { id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA) {
id item = ALLOC("NSMenuItem"); id item = ALLOC("NSMenuItem");
// Create a MenuItemCallbackData // Create a MenuItemCallbackData
@@ -585,6 +588,73 @@ id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), msg(item, s("initWithTitle:action:keyEquivalent:"), str(title),
s("menuItemCallback:"), key); s("menuItemCallback:"), key);
if( tooltip != NULL ) {
msg(item, s("setToolTip:"), str(tooltip));
}
// Process image
if( image != NULL && strlen(image) > 0) {
id data = ALLOC("NSData");
id imageData = msg(data, s("initWithBase64EncodedString:options:"), str(image), 0);
id nsimage = ALLOC("NSImage");
msg(nsimage, s("initWithData:"), imageData);
msg(item, s("setImage:"), nsimage);
}
// Process Menu Item attributes
id dictionary = ALLOC_INIT("NSMutableDictionary");
// Process font
id font;
CGFloat fontSizeFloat = (CGFloat)fontSize;
// Check if valid
id fontNameAsNSString = str(fontName);
id fontsOnSystem = msg(msg(c("NSFontManager"), s("sharedFontManager")), s("availableFonts"));
bool valid = msg(fontsOnSystem, s("containsObject:"), fontNameAsNSString);
if( valid ) {
font = msg(c("NSFont"), s("fontWithName:size:"), fontNameAsNSString, fontSizeFloat);
} else {
bool supportsMonospacedDigitSystemFont = (bool) msg(c("NSFont"), s("respondsToSelector:"), s("monospacedDigitSystemFontOfSize:weight:"));
if( supportsMonospacedDigitSystemFont ) {
font = msg(c("NSFont"), s("monospacedDigitSystemFontOfSize:weight:"), fontSizeFloat, NSFontWeightRegular);
} else {
font = msg(c("NSFont"), s("menuFontOfSize:"), fontSizeFloat);
}
}
// Add font to dictionary
msg(dictionary, s("setObject:forKey:"), font, lookupStringConstant(str("NSFontAttributeName")));
// Add offset to dictionary
id offset = msg(c("NSNumber"), s("numberWithFloat:"), 0.0);
msg(dictionary, s("setObject:forKey:"), offset, lookupStringConstant(str("NSBaselineOffsetAttributeName")));
// RGBA
if( RGBA != NULL && strlen(RGBA) > 0) {
unsigned short r, g, b, a;
// white by default
r = g = b = a = 255;
int count = sscanf(RGBA, "#%02hx%02hx%02hx%02hx", &r, &g, &b, &a);
if (count > 0) {
id colour = msg(c("NSColor"), s("colorWithCalibratedRed:green:blue:alpha:"),
(float)r / 255.0,
(float)g / 255.0,
(float)b / 255.0,
(float)a / 255.0);
msg(dictionary, s("setObject:forKey:"), colour, lookupStringConstant(str("NSForegroundColorAttributeName")));
msg(colour, s("release"));
}
}
id attributedString = ALLOC("NSMutableAttributedString");
msg(attributedString, s("initWithString:attributes:"), str(title), dictionary);
msg(dictionary, s("release"));
msg(item, s("setAttributedTitle:"), attributedString);
msg(attributedString, s("autorelease"));
msg(item, s("setEnabled:"), !disabled); msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease")); msg(item, s("autorelease"));
@@ -666,6 +736,13 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
const char *acceleratorkey = NULL; const char *acceleratorkey = NULL;
const char **modifiers = NULL; const char **modifiers = NULL;
const char *tooltip = getJSONString(item, "Tooltip");
const char *image = getJSONString(item, "Image");
const char *fontName = getJSONString(item, "FontName");
const char *RGBA = getJSONString(item, "RGBA");
int fontSize = 12;
getJSONInt(item, "FontSize", &fontSize);
// If we have an accelerator // If we have an accelerator
if( accelerator != NULL ) { if( accelerator != NULL ) {
// Get the key // Get the key
@@ -698,7 +775,7 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
if( type != NULL ) { if( type != NULL ) {
if( STREQ(type->string_, "Text")) { if( STREQ(type->string_, "Text")) {
processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers); processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA);
} }
else if ( STREQ(type->string_, "Separator")) { else if ( STREQ(type->string_, "Separator")) {
addSeparator(parentMenu); addSeparator(parentMenu);

View File

@@ -14,6 +14,21 @@ static const char *MenuTypeAsString[] = {
"ApplicationMenu", "ContextMenu", "TrayMenu", "ApplicationMenu", "ContextMenu", "TrayMenu",
}; };
typedef struct _NSRange {
unsigned long location;
unsigned long length;
} NSRange;
#define NSFontWeightUltraLight -0.8
#define NSFontWeightThin -0.6
#define NSFontWeightLight -0.4
#define NSFontWeightRegular 0.0
#define NSFontWeightMedium 0.23
#define NSFontWeightSemibold 0.3
#define NSFontWeightBold 0.4
#define NSFontWeightHeavy 0.56
#define NSFontWeightBlack 0.62
extern void messageFromWindowCallback(const char *); extern void messageFromWindowCallback(const char *);
typedef struct { typedef struct {
@@ -90,8 +105,7 @@ id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char
id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *key); id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *key);
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers); id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA);
void processMenuItem(Menu *menu, id parentMenu, JsonNode *item); void processMenuItem(Menu *menu, id parentMenu, JsonNode *item);
void processMenuData(Menu *menu, JsonNode *menuData); void processMenuData(Menu *menu, JsonNode *menuData);

File diff suppressed because one or more lines are too long

View File

@@ -49,42 +49,19 @@ void DumpTrayMenu(TrayMenu* trayMenu) {
printf(" ['%s':%p] = { label: '%s', icon: '%s', menu: %p, statusbar: %p }\n", trayMenu->ID, trayMenu, trayMenu->label, trayMenu->icon, trayMenu->menu, trayMenu->statusbaritem ); printf(" ['%s':%p] = { label: '%s', icon: '%s', menu: %p, statusbar: %p }\n", trayMenu->ID, trayMenu, trayMenu->label, trayMenu->icon, trayMenu->menu, trayMenu->statusbaritem );
} }
void ShowTrayMenu(TrayMenu* trayMenu) {
// Create a status bar item if we don't have one void UpdateTrayLabel(TrayMenu *trayMenu, const char *label) {
if( trayMenu->statusbaritem == NULL ) {
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") );
trayMenu->statusbaritem = msg(statusBar, s("statusItemWithLength:"), NSVariableStatusItemLength);
msg(trayMenu->statusbaritem, s("retain"));
}
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
msg(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
// Update the icon if needed
UpdateTrayMenuIcon(trayMenu);
// Update the label if needed
UpdateTrayMenuLabel(trayMenu);
// Update the menu
id menu = GetMenu(trayMenu->menu);
msg(trayMenu->statusbaritem, s("setMenu:"), menu);
}
void UpdateTrayMenuLabel(TrayMenu *trayMenu) {
// Exit early if NULL // Exit early if NULL
if( trayMenu->label == NULL ) { if( trayMenu->label == NULL ) {
return; return;
} }
// We don't check for a // Update button label
id statusBarButton = msg(trayMenu->statusbaritem, s("button")); id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
msg(statusBarButton, s("setTitle:"), str(trayMenu->label)); msg(statusBarButton, s("setTitle:"), str(label));
} }
void UpdateTrayMenuIcon(TrayMenu *trayMenu) { void UpdateTrayIcon(TrayMenu *trayMenu) {
// Exit early if NULL // Exit early if NULL
if( trayMenu->icon == NULL ) { if( trayMenu->icon == NULL ) {
@@ -105,6 +82,32 @@ void UpdateTrayMenuIcon(TrayMenu *trayMenu) {
msg(statusBarButton, s("setImage:"), trayImage); msg(statusBarButton, s("setImage:"), trayImage);
} }
void ShowTrayMenu(TrayMenu* trayMenu) {
// Create a status bar item if we don't have one
if( trayMenu->statusbaritem == NULL ) {
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") );
trayMenu->statusbaritem = msg(statusBar, s("statusItemWithLength:"), NSVariableStatusItemLength);
msg(trayMenu->statusbaritem, s("retain"));
}
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
msg(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
// Update the icon if needed
UpdateTrayIcon(trayMenu);
// Update the label if needed
UpdateTrayLabel(trayMenu, trayMenu->label);
// Update the menu
id menu = GetMenu(trayMenu->menu);
msg(trayMenu->statusbaritem, s("setMenu:"), menu);
}
// UpdateTrayMenuInPlace receives 2 menus. The current menu gets // UpdateTrayMenuInPlace receives 2 menus. The current menu gets
// updated with the data from the new menu. // updated with the data from the new menu.
void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu) { void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu) {

View File

@@ -27,8 +27,8 @@ TrayMenu* NewTrayMenu(const char *trayJSON);
void DumpTrayMenu(TrayMenu* trayMenu); void DumpTrayMenu(TrayMenu* trayMenu);
void ShowTrayMenu(TrayMenu* trayMenu); void ShowTrayMenu(TrayMenu* trayMenu);
void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu); void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu);
void UpdateTrayMenuIcon(TrayMenu *trayMenu); void UpdateTrayIcon(TrayMenu *trayMenu);
void UpdateTrayMenuLabel(TrayMenu *trayMenu); void UpdateTrayLabel(TrayMenu *trayMenu, const char*);
void LoadTrayIcons(); void LoadTrayIcons();
void UnloadTrayIcons(); void UnloadTrayIcons();

View File

@@ -72,15 +72,38 @@ TrayMenu* GetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
return hashmap_get(&store->trayMenuMap, menuID, strlen(menuID)); return hashmap_get(&store->trayMenuMap, menuID, strlen(menuID));
} }
TrayMenu* MustGetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
// Get the current menu
TrayMenu* result = hashmap_get(&store->trayMenuMap, menuID, strlen(menuID));
if (result == NULL ) {
ABORT("Unable to find TrayMenu with ID '%s' in the TrayMenuStore!", menuID);
}
return result;
}
void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON) {
// Parse the JSON
JsonNode *parsedUpdate = mustParseJSON(JSON);
// Get the data out
const char* ID = mustJSONString(parsedUpdate, "ID");
const char* Label = mustJSONString(parsedUpdate, "Label");
// Check we have this menu
TrayMenu *menu = MustGetTrayMenuFromStore(store, ID);
UpdateTrayLabel(menu, Label);
}
void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) { void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
TrayMenu* newMenu = NewTrayMenu(menuJSON); TrayMenu* newMenu = NewTrayMenu(menuJSON);
// DumpTrayMenu(newMenu);
// Get the current menu // Get the current menu
TrayMenu *currentMenu = GetTrayMenuFromStore(store, newMenu->ID); TrayMenu *currentMenu = GetTrayMenuFromStore(store, newMenu->ID);
// If we don't have a menu, we create one // If we don't have a menu, we create one
if ( currentMenu == NULL ) { if ( currentMenu == NULL ) {
// Store the new menu // Store the new menu
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu); hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
@@ -88,6 +111,7 @@ void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
ShowTrayMenu(newMenu); ShowTrayMenu(newMenu);
return; return;
} }
// DumpTrayMenu(currentMenu);
// Save the status bar reference // Save the status bar reference
newMenu->statusbaritem = currentMenu->statusbaritem; newMenu->statusbaritem = currentMenu->statusbaritem;
@@ -98,12 +122,6 @@ void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
DeleteMenu(currentMenu->menu); DeleteMenu(currentMenu->menu);
currentMenu->menu = NULL; currentMenu->menu = NULL;
// Free JSON
if (currentMenu->processedJSON != NULL ) {
json_delete(currentMenu->processedJSON);
currentMenu->processedJSON = NULL;
}
// Free the tray menu memory // Free the tray menu memory
MEMFREE(currentMenu); MEMFREE(currentMenu);

View File

@@ -22,4 +22,6 @@ void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON);
void ShowTrayMenusInStore(TrayMenuStore* store); void ShowTrayMenusInStore(TrayMenuStore* store);
void DeleteTrayMenuStore(TrayMenuStore* store); void DeleteTrayMenuStore(TrayMenuStore* store);
void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON);
#endif //TRAYMENUSTORE_DARWIN_H #endif //TRAYMENUSTORE_DARWIN_H

View File

@@ -0,0 +1,103 @@
package github
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"sort"
"strings"
)
// GetVersionTags gets the list of tags on the Wails repo
// It returns a list of sorted tags in descending order
func GetVersionTags() ([]*SemanticVersion, error) {
result := []*SemanticVersion{}
var err error
resp, err := http.Get("https://api.github.com/repos/wailsapp/wails/tags")
if err != nil {
return result, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return result, err
}
data := []map[string]interface{}{}
err = json.Unmarshal(body, &data)
if err != nil {
return result, err
}
// Convert tag data to Version structs
for _, tag := range data {
version := tag["name"].(string)
if !strings.HasPrefix(version, "v2") {
continue
}
semver, err := NewSemanticVersion(version)
if err != nil {
return result, err
}
result = append(result, semver)
}
// Reverse Sort
sort.Sort(sort.Reverse(SemverCollection(result)))
return result, err
}
// GetLatestStableRelease gets the latest stable release on GitHub
func GetLatestStableRelease() (result *SemanticVersion, err error) {
tags, err := GetVersionTags()
if err != nil {
return nil, err
}
for _, tag := range tags {
if tag.IsRelease() {
return tag, nil
}
}
return nil, fmt.Errorf("no release tag found")
}
// GetLatestPreRelease gets the latest prerelease on GitHub
func GetLatestPreRelease() (result *SemanticVersion, err error) {
tags, err := GetVersionTags()
if err != nil {
return nil, err
}
for _, tag := range tags {
if tag.IsPreRelease() {
return tag, nil
}
}
return nil, fmt.Errorf("no prerelease tag found")
}
// IsValidTag returns true if the given string is a valid tag
func IsValidTag(tagVersion string) (bool, error) {
if tagVersion[0] == 'v' {
tagVersion = tagVersion[1:]
}
tags, err := GetVersionTags()
if err != nil {
return false, err
}
for _, tag := range tags {
if tag.String() == tagVersion {
return true, nil
}
}
return false, nil
}

View File

@@ -0,0 +1,106 @@
package github
import (
"fmt"
"github.com/Masterminds/semver"
)
// SemanticVersion is a struct containing a semantic version
type SemanticVersion struct {
Version *semver.Version
}
// NewSemanticVersion creates a new SemanticVersion object with the given version string
func NewSemanticVersion(version string) (*SemanticVersion, error) {
semverVersion, err := semver.NewVersion(version)
if err != nil {
return nil, err
}
return &SemanticVersion{
Version: semverVersion,
}, nil
}
// IsRelease returns true if it's a release version
func (s *SemanticVersion) IsRelease() bool {
// Limit to v2
if s.Version.Major() != 2 {
return false
}
return len(s.Version.Prerelease()) == 0 && len(s.Version.Metadata()) == 0
}
// IsPreRelease returns true if it's a prerelease version
func (s *SemanticVersion) IsPreRelease() bool {
// Limit to v1
if s.Version.Major() != 2 {
return false
}
return len(s.Version.Prerelease()) > 0
}
func (s *SemanticVersion) String() string {
return s.Version.String()
}
// IsGreaterThan returns true if this version is greater than the given version
func (s *SemanticVersion) IsGreaterThan(version *SemanticVersion) (bool, error) {
// Set up new constraint
constraint, err := semver.NewConstraint("> " + version.Version.String())
if err != nil {
return false, err
}
// Check if the desired one is greater than the requested on
success, msgs := constraint.Validate(s.Version)
if !success {
return false, msgs[0]
}
return true, nil
}
// IsGreaterThanOrEqual returns true if this version is greater than or equal the given version
func (s *SemanticVersion) IsGreaterThanOrEqual(version *SemanticVersion) (bool, error) {
// Set up new constraint
constraint, err := semver.NewConstraint(">= " + version.Version.String())
if err != nil {
return false, err
}
// Check if the desired one is greater than the requested on
success, msgs := constraint.Validate(s.Version)
if !success {
return false, msgs[0]
}
return true, nil
}
// MainVersion returns the main version of any version+prerelease+metadata
// EG: MainVersion("1.2.3-pre") => "1.2.3"
func (s *SemanticVersion) MainVersion() *SemanticVersion {
mainVersion := fmt.Sprintf("%d.%d.%d", s.Version.Major(), s.Version.Minor(), s.Version.Patch())
result, _ := NewSemanticVersion(mainVersion)
return result
}
// SemverCollection is a collection of SemanticVersion objects
type SemverCollection []*SemanticVersion
// Len returns the length of a collection. The number of Version instances
// on the slice.
func (c SemverCollection) Len() int {
return len(c)
}
// Less is needed for the sort interface to compare two Version objects on the
// slice. If checks if one is less than the other.
func (c SemverCollection) Less(i, j int) bool {
return c[i].Version.LessThan(c[j].Version)
}
// Swap is needed for the sort interface to replace the Version objects
// at two different positions in the slice.
func (c SemverCollection) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}

View File

@@ -9,28 +9,35 @@ import (
type ProcessedMenuItem struct { type ProcessedMenuItem struct {
ID string ID string
// Label is what appears as the menu text // Label is what appears as the menu text
Label string Label string `json:",omitempty"`
// Role is a predefined menu type // Role is a predefined menu type
Role menu.Role `json:"Role,omitempty"` Role menu.Role `json:",omitempty"`
// Accelerator holds a representation of a key binding // Accelerator holds a representation of a key binding
Accelerator *keys.Accelerator `json:"Accelerator,omitempty"` Accelerator *keys.Accelerator `json:",omitempty"`
// Type of MenuItem, EG: Checkbox, Text, Separator, Radio, Submenu // Type of MenuItem, EG: Checkbox, Text, Separator, Radio, Submenu
Type menu.Type Type menu.Type
// Disabled makes the item unselectable // Disabled makes the item unselectable
Disabled bool Disabled bool `json:",omitempty"`
// Hidden ensures that the item is not shown in the menu // Hidden ensures that the item is not shown in the menu
Hidden bool Hidden bool `json:",omitempty"`
// Checked indicates if the item is selected (used by Checkbox and Radio types only) // Checked indicates if the item is selected (used by Checkbox and Radio types only)
Checked bool Checked bool `json:",omitempty"`
// Submenu contains a list of menu items that will be shown as a submenu // Submenu contains a list of menu items that will be shown as a submenu
//SubMenu []*MenuItem `json:"SubMenu,omitempty"` //SubMenu []*MenuItem `json:"SubMenu,omitempty"`
SubMenu *ProcessedMenu `json:"SubMenu,omitempty"` SubMenu *ProcessedMenu `json:",omitempty"`
// Foreground colour in hex RGBA format EG: 0xFF0000FF = #FF0000FF = red // Colour
Foreground int RGBA string `json:",omitempty"`
// Background colour // Font
Background int FontSize int `json:",omitempty"`
FontName string `json:",omitempty"`
// Image - base64 image data
Image string `json:",omitempty"`
// Tooltip
Tooltip string `json:",omitempty"`
} }
func NewProcessedMenuItem(menuItemMap *MenuItemMap, menuItem *menu.MenuItem) *ProcessedMenuItem { func NewProcessedMenuItem(menuItemMap *MenuItemMap, menuItem *menu.MenuItem) *ProcessedMenuItem {
@@ -45,8 +52,12 @@ func NewProcessedMenuItem(menuItemMap *MenuItemMap, menuItem *menu.MenuItem) *Pr
Disabled: menuItem.Disabled, Disabled: menuItem.Disabled,
Hidden: menuItem.Hidden, Hidden: menuItem.Hidden,
Checked: menuItem.Checked, Checked: menuItem.Checked,
Foreground: menuItem.Foreground, SubMenu: nil,
Background: menuItem.Background, RGBA: menuItem.RGBA,
FontSize: menuItem.FontSize,
FontName: menuItem.FontName,
Image: menuItem.Image,
Tooltip: menuItem.Tooltip,
} }
if menuItem.SubMenu != nil { if menuItem.SubMenu != nil {

View File

@@ -3,6 +3,7 @@ package menumanager
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/pkg/errors"
"github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/menu"
"sync" "sync"
) )
@@ -94,6 +95,31 @@ func (m *Manager) GetTrayMenus() ([]string, error) {
return result, nil return result, nil
} }
func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) {
trayID, trayMenuKnown := m.trayMenuPointers[trayMenu]
if !trayMenuKnown {
return "", fmt.Errorf("[UpdateTrayMenuLabel] unknown tray id for tray %s", trayMenu.Label)
}
type LabelUpdate struct {
ID string
Label string
}
update := &LabelUpdate{
ID: trayID,
Label: trayMenu.Label,
}
data, err := json.Marshal(update)
if err != nil {
return "", errors.Wrap(err, "[UpdateTrayMenuLabel] ")
}
return string(data), nil
}
func (m *Manager) GetContextMenus() ([]string, error) { func (m *Manager) GetContextMenus() ([]string, error) {
result := []string{} result := []string{}
for _, contextMenu := range m.contextMenus { for _, contextMenu := range m.contextMenus {

View File

@@ -2,6 +2,7 @@ package messagedispatcher
import ( import (
"fmt" "fmt"
"github.com/wailsapp/wails/v2/internal/logger" "github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message" "github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/servicebus" "github.com/wailsapp/wails/v2/internal/servicebus"
@@ -26,12 +27,15 @@ type Client interface {
WindowUnminimise() WindowUnminimise()
WindowPosition(x int, y int) WindowPosition(x int, y int)
WindowSize(width int, height int) WindowSize(width int, height int)
WindowSetMinSize(width int, height int)
WindowSetMaxSize(width int, height int)
WindowFullscreen() WindowFullscreen()
WindowUnFullscreen() WindowUnFullscreen()
WindowSetColour(colour int) WindowSetColour(colour int)
DarkModeEnabled(callbackID string) DarkModeEnabled(callbackID string)
SetApplicationMenu(menuJSON string) SetApplicationMenu(menuJSON string)
SetTrayMenu(trayMenuJSON string) SetTrayMenu(trayMenuJSON string)
UpdateTrayMenuLabel(JSON string)
UpdateContextMenu(contextMenuJSON string) UpdateContextMenu(contextMenuJSON string)
} }

View File

@@ -2,11 +2,12 @@ package messagedispatcher
import ( import (
"encoding/json" "encoding/json"
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"github.com/wailsapp/wails/v2/internal/crypto" "github.com/wailsapp/wails/v2/internal/crypto"
"github.com/wailsapp/wails/v2/internal/logger" "github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message" "github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
@@ -349,6 +350,38 @@ func (d *Dispatcher) processWindowMessage(result *servicebus.Message) {
for _, client := range d.clients { for _, client := range d.clients {
client.frontend.WindowSize(w, h) client.frontend.WindowSize(w, h)
} }
case "minsize":
// We need 2 arguments
if len(splitTopic) != 4 {
d.logger.Error("Invalid number of parameters for 'window:minsize' : %#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:minsize' : %#v", result.Data())
return
}
// Notifh clients
for _, client := range d.clients {
client.frontend.WindowSetMinSize(w, h)
}
case "maxsize":
// We need 2 arguments
if len(splitTopic) != 4 {
d.logger.Error("Invalid number of parameters for 'window:maxsize' : %#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:maxsize' : %#v", result.Data())
return
}
// Notifh clients
for _, client := range d.clients {
client.frontend.WindowSetMaxSize(w, h)
}
default: default:
d.logger.Error("Unknown window command: %s", command) d.logger.Error("Unknown window command: %s", command)
} }
@@ -473,6 +506,20 @@ func (d *Dispatcher) processMenuMessage(result *servicebus.Message) {
client.frontend.UpdateContextMenu(updatedContextMenu) client.frontend.UpdateContextMenu(updatedContextMenu)
} }
case "updatetraymenulabel":
updatedTrayMenuLabel, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid data for 'menufrontend:updatetraymenulabel' : %#v",
result.Data())
return
}
// TODO: Work out what we mean in a multi window environment...
// For now we will just pick the first one
for _, client := range d.clients {
client.frontend.UpdateTrayMenuLabel(updatedTrayMenuLabel)
}
default: default:
d.logger.Error("Unknown menufrontend command: %s", command) d.logger.Error("Unknown menufrontend command: %s", command)
} }

File diff suppressed because one or more lines are too long

View File

@@ -10,6 +10,7 @@ The lightweight framework for web-like apps
/* jshint esversion: 6 */ /* jshint esversion: 6 */
import { SetBindings } from './bindings'; import { SetBindings } from './bindings';
import { Init } from './main'; import { Init } from './main';
import {RaiseError} from '../desktop/darwin';
// Setup global error handler // Setup global error handler
window.onerror = function (msg, url, lineNo, columnNo, error) { window.onerror = function (msg, url, lineNo, columnNo, error) {
@@ -21,7 +22,7 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
error: JSON.stringify(error), error: JSON.stringify(error),
stack: function() { return JSON.stringify(new Error().stack); }(), stack: function() { return JSON.stringify(new Error().stack); }(),
}; };
window.wails.Log.Error(JSON.stringify(errorMessage)); RaiseError(errorMessage);
}; };
// Initialise the Runtime // Initialise the Runtime

View File

@@ -25,6 +25,10 @@ export function SendMessage(message) {
window.webkit.messageHandlers.external.postMessage(message); window.webkit.messageHandlers.external.postMessage(message);
} }
export function RaiseError(message) {
window.webkit.messageHandlers.error.postMessage(message);
}
export function Init() { export function Init() {
// Setup drag handler // Setup drag handler

View File

@@ -13,7 +13,7 @@ module.exports = {
mode: 'production', mode: 'production',
output: { output: {
path: path.resolve(__dirname, '..', 'assets'), path: path.resolve(__dirname, '..', 'assets'),
filename: 'desktop.js', filename: 'desktop_'+platform+'.js',
library: 'Wails' library: 'Wails'
}, },
resolve: { resolve: {

View File

@@ -10,6 +10,7 @@ type Menu interface {
UpdateApplicationMenu() UpdateApplicationMenu()
UpdateContextMenu(contextMenu *menu.ContextMenu) UpdateContextMenu(contextMenu *menu.ContextMenu)
SetTrayMenu(trayMenu *menu.TrayMenu) SetTrayMenu(trayMenu *menu.TrayMenu)
UpdateTrayMenuLabel(trayMenu *menu.TrayMenu)
} }
type menuRuntime struct { type menuRuntime struct {
@@ -34,3 +35,7 @@ func (m *menuRuntime) UpdateContextMenu(contextMenu *menu.ContextMenu) {
func (m *menuRuntime) SetTrayMenu(trayMenu *menu.TrayMenu) { func (m *menuRuntime) SetTrayMenu(trayMenu *menu.TrayMenu) {
m.bus.Publish("menu:settraymenu", trayMenu) m.bus.Publish("menu:settraymenu", trayMenu)
} }
func (m *menuRuntime) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) {
m.bus.Publish("menu:updatetraymenulabel", trayMenu)
}

View File

@@ -3,12 +3,13 @@ package main
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/wailsapp/wails/v2/internal/shell"
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
"strings" "strings"
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/wailsapp/wails/v2/internal/shell"
) )
func main() { func main() {
@@ -49,7 +50,7 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
wailsJS := fs.RelativePath("../../../internal/runtime/assets/desktop.js") wailsJS := fs.RelativePath("../../../internal/runtime/assets/desktop_" + platform + ".js")
runtimeData, err := ioutil.ReadFile(wailsJS) runtimeData, err := ioutil.ReadFile(wailsJS)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@@ -6,9 +6,12 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
golog "log"
"os" "os"
"reflect" "reflect"
"sync" "sync"
"github.com/wailsapp/wails/v2/internal/deepcopy"
) )
// Options defines the optional data that may be used // Options defines the optional data that may be used
@@ -64,21 +67,31 @@ func fatal(err error) {
// New creates a new store // New creates a new store
func (p *StoreProvider) New(name string, defaultValue interface{}) *Store { func (p *StoreProvider) New(name string, defaultValue interface{}) *Store {
dataType := reflect.TypeOf(defaultValue) if defaultValue == nil {
golog.Fatal("Cannot initialise a store with nil")
}
result := Store{ result := Store{
name: name, name: name,
runtime: p.runtime, runtime: p.runtime,
data: reflect.ValueOf(defaultValue),
dataType: dataType,
} }
// Setup the sync listener // Setup the sync listener
result.setupListener() result.setupListener()
result.Set(defaultValue)
return &result return &result
} }
func (s *Store) lock() {
s.mux.Lock()
}
func (s *Store) unlock() {
s.mux.Unlock()
}
// OnError takes a function that will be called // OnError takes a function that will be called
// whenever an error occurs // whenever an error occurs
func (s *Store) OnError(callback func(error)) { func (s *Store) OnError(callback func(error)) {
@@ -105,7 +118,7 @@ func (s *Store) processUpdatedData(data string) error {
} }
// Lock mutex for writing // Lock mutex for writing
s.mux.Lock() s.lock()
// Handle nulls // Handle nulls
if newData == nil { if newData == nil {
@@ -116,7 +129,7 @@ func (s *Store) processUpdatedData(data string) error {
} }
// Unlock mutex // Unlock mutex
s.mux.Unlock() s.unlock()
return nil return nil
} }
@@ -146,20 +159,34 @@ func (s *Store) setupListener() {
// Resetting the curent data will resync // Resetting the curent data will resync
s.resync() s.resync()
}) })
// Do initial resync
s.resync()
} }
func (s *Store) resync() { func (s *Store) resync() {
// Stringify data
newdata, err := json.Marshal(s.data.Interface()) // Lock
if err != nil { s.lock()
if s.errorHandler != nil { defer s.unlock()
s.errorHandler(err)
return var result string
if s.data.IsValid() {
rawdata, err := json.Marshal(s.data.Interface())
if err != nil {
if s.errorHandler != nil {
s.errorHandler(err)
return
}
} }
result = string(rawdata)
} else {
result = "{}"
} }
// Emit event to front end // Emit event to front end
s.runtime.Events.Emit("wails:sync:store:updatedbybackend:"+s.name, string(newdata)) s.runtime.Events.Emit("wails:sync:store:updatedbybackend:"+s.name, result)
// Notify subscribers // Notify subscribers
s.notify() s.notify()
@@ -172,7 +199,9 @@ func (s *Store) notify() {
for _, callback := range s.callbacks { for _, callback := range s.callbacks {
// Build args // Build args
s.lock()
args := []reflect.Value{s.data} args := []reflect.Value{s.data}
s.unlock()
if s.notifySynchronously { if s.notifySynchronously {
callback.Call(args) callback.Call(args)
@@ -187,16 +216,31 @@ func (s *Store) notify() {
// and notify listeners of the change // and notify listeners of the change
func (s *Store) Set(data interface{}) error { func (s *Store) Set(data interface{}) error {
inType := reflect.TypeOf(data) if data == nil {
return fmt.Errorf("cannot set store to nil")
}
if inType != s.dataType { s.lock()
return fmt.Errorf("invalid data given in Store.Set(). Expected %s, got %s", s.dataType.String(), inType.String())
dataCopy := deepcopy.Copy(data)
if dataCopy != nil {
inType := reflect.TypeOf(dataCopy)
if inType != s.dataType && s.data.IsValid() {
s.unlock()
return fmt.Errorf("invalid data given in Store.Set(). Expected %s, got %s", s.dataType.String(), inType.String())
}
}
if s.dataType == nil {
s.dataType = reflect.TypeOf(dataCopy)
} }
// Save data // Save data
s.mux.Lock() s.data = reflect.ValueOf(dataCopy)
s.data = reflect.ValueOf(data)
s.mux.Unlock() s.unlock()
// Resync with subscribers // Resync with subscribers
s.resync() s.resync()
@@ -247,7 +291,9 @@ func (s *Store) Subscribe(callback interface{}) {
callbackFunc := reflect.ValueOf(callback) callbackFunc := reflect.ValueOf(callback)
s.lock()
s.callbacks = append(s.callbacks, callbackFunc) s.callbacks = append(s.callbacks, callbackFunc)
s.unlock()
} }
// updaterCheck ensures the given function to Update() is // updaterCheck ensures the given function to Update() is
@@ -297,7 +343,9 @@ func (s *Store) Update(updater interface{}) {
} }
// Build args // Build args
s.lock()
args := []reflect.Value{s.data} args := []reflect.Value{s.data}
s.unlock()
// Make call // Make call
results := reflect.ValueOf(updater).Call(args) results := reflect.ValueOf(updater).Call(args)
@@ -308,5 +356,12 @@ func (s *Store) Update(updater interface{}) {
// Get returns the value of the data that's kept in the current state / Store // Get returns the value of the data that's kept in the current state / Store
func (s *Store) Get() interface{} { func (s *Store) Get() interface{} {
s.lock()
defer s.unlock()
if !s.data.IsValid() {
return nil
}
return s.data.Interface() return s.data.Interface()
} }

View File

@@ -0,0 +1,165 @@
package runtime
import (
"context"
"math/rand"
"sync"
"testing"
"time"
internallogger "github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/pkg/logger"
is2 "github.com/matryer/is"
)
func TestStoreProvider_NewWithNilDefault(t *testing.T) {
is := is2.New(t)
defaultLogger := logger.NewDefaultLogger()
testLogger := internallogger.New(defaultLogger)
//testLogger.SetLogLevel(logger.TRACE)
serviceBus := servicebus.New(testLogger)
err := serviceBus.Start()
is.NoErr(err)
defer serviceBus.Stop()
testRuntime := New(serviceBus)
storeProvider := newStore(testRuntime)
testStore := storeProvider.New("test", 0)
// You should be able to write a new value into a
// store initialised with nil
err = testStore.Set(100)
is.NoErr(err)
// You shouldn't be able to write different types to the
// store
err = testStore.Set(false)
is.True(err != nil)
}
func TestStoreProvider_NewWithScalarDefault(t *testing.T) {
is := is2.New(t)
defaultLogger := logger.NewDefaultLogger()
testLogger := internallogger.New(defaultLogger)
//testLogger.SetLogLevel(logger.TRACE)
serviceBus := servicebus.New(testLogger)
err := serviceBus.Start()
is.NoErr(err)
defer serviceBus.Stop()
testRuntime := New(serviceBus)
storeProvider := newStore(testRuntime)
testStore := storeProvider.New("test", 100)
value := testStore.Get()
is.Equal(value, 100)
testStore.resync()
value = testStore.Get()
is.Equal(value, 100)
}
func TestStoreProvider_NewWithStructDefault(t *testing.T) {
is := is2.New(t)
defaultLogger := logger.NewDefaultLogger()
testLogger := internallogger.New(defaultLogger)
//testLogger.SetLogLevel(logger.TRACE)
serviceBus := servicebus.New(testLogger)
err := serviceBus.Start()
is.NoErr(err)
defer serviceBus.Stop()
testRuntime := New(serviceBus)
storeProvider := newStore(testRuntime)
type TestValue struct {
Name string
}
testValue := &TestValue{
Name: "hi",
}
testStore := storeProvider.New("test", testValue)
err = testStore.Set(testValue)
is.NoErr(err)
testStore.resync()
value := testStore.Get()
is.Equal(value, testValue)
is.Equal(value.(*TestValue).Name, "hi")
testValue = &TestValue{
Name: "there",
}
err = testStore.Set(testValue)
is.NoErr(err)
testStore.resync()
value = testStore.Get()
is.Equal(value, testValue)
is.Equal(value.(*TestValue).Name, "there")
}
func TestStoreProvider_RapidReadWrite(t *testing.T) {
is := is2.New(t)
defaultLogger := logger.NewDefaultLogger()
testLogger := internallogger.New(defaultLogger)
//testLogger.SetLogLevel(logger.TRACE)
serviceBus := servicebus.New(testLogger)
err := serviceBus.Start()
is.NoErr(err)
defer serviceBus.Stop()
testRuntime := New(serviceBus)
storeProvider := newStore(testRuntime)
testStore := storeProvider.New("test", 1)
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second)
var wg sync.WaitGroup
readers := 100
writers := 100
wg.Add(readers + writers)
// Setup readers
go func(testStore *Store, ctx context.Context) {
for readerCount := 0; readerCount < readers; readerCount++ {
go func(store *Store, ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
wg.Done()
return
default:
store.Get()
}
}
}(testStore, ctx, readerCount)
}
}(testStore, ctx)
// Setup writers
go func(testStore *Store, ctx context.Context) {
for writerCount := 0; writerCount < writers; writerCount++ {
go func(store *Store, ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
wg.Done()
return
default:
err := store.Set(rand.Int())
is.NoErr(err)
}
}
}(testStore, ctx, writerCount)
}
}(testStore, ctx)
wg.Wait()
}

View File

@@ -18,6 +18,8 @@ type Window interface {
Unminimise() Unminimise()
SetTitle(title string) SetTitle(title string)
SetSize(width int, height int) SetSize(width int, height int)
SetMinSize(width int, height int)
SetMaxSize(width int, height int)
SetPosition(x int, y int) SetPosition(x int, y int)
Fullscreen() Fullscreen()
UnFullscreen() UnFullscreen()
@@ -85,6 +87,18 @@ func (w *window) SetSize(width int, height int) {
w.bus.Publish(message, "") w.bus.Publish(message, "")
} }
// SetSize sets the size of the window
func (w *window) SetMinSize(width int, height int) {
message := fmt.Sprintf("window:minsize:%d:%d", width, height)
w.bus.Publish(message, "")
}
// SetSize sets the size of the window
func (w *window) SetMaxSize(width int, height int) {
message := fmt.Sprintf("window:maxsize:%d:%d", width, height)
w.bus.Publish(message, "")
}
// SetPosition sets the position of the window // SetPosition sets the position of the window
func (w *window) SetPosition(x int, y int) { func (w *window) SetPosition(x int, y int) {
message := fmt.Sprintf("window:position:%d:%d", x, y) message := fmt.Sprintf("window:position:%d:%d", x, y)

View File

@@ -71,10 +71,11 @@ func (s *ServiceBus) Start() error {
} }
// We run in a different thread // We run in a different thread
s.wg.Add(1)
go func() { go func() {
quit := false quit := false
s.wg.Add(1)
// Loop until we get a quit message // Loop until we get a quit message
for !quit { for !quit {

View File

@@ -131,6 +131,17 @@ func (m *Menu) Start() error {
// Notify frontend of menu change // Notify frontend of menu change
m.bus.Publish("menufrontend:settraymenu", updatedMenu) m.bus.Publish("menufrontend:settraymenu", updatedMenu)
case "updatetraymenulabel":
trayMenu := menuMessage.Data().(*menu.TrayMenu)
updatedLabel, err := m.menuManager.UpdateTrayMenuLabel(trayMenu)
if err != nil {
m.logger.Trace("%s", err.Error())
return
}
// Notify frontend of menu change
m.bus.Publish("menufrontend:updatetraymenulabel", updatedLabel)
default: default:
m.logger.Error("unknown menu message: %+v", menuMessage) m.logger.Error("unknown menu message: %+v", menuMessage)
} }

View File

@@ -83,7 +83,7 @@ func (r *Runtime) Start() error {
if r.startupCallback != nil { if r.startupCallback != nil {
go r.startupCallback(r.runtime) go r.startupCallback(r.runtime)
} else { } else {
r.logger.Error("no startup callback registered!") r.logger.Warning("no startup callback registered!")
} }
default: default:
r.logger.Error("unknown hook message: %+v", hooksMessage) r.logger.Error("unknown hook message: %+v", hooksMessage)
@@ -131,6 +131,8 @@ func (r *Runtime) GoRuntime() *runtime.Runtime {
func (r *Runtime) shutdown() { func (r *Runtime) shutdown() {
if r.shutdownCallback != nil { if r.shutdownCallback != nil {
go r.shutdownCallback() go r.shutdownCallback()
} else {
r.logger.Warning("no shutdown callback registered!")
} }
r.logger.Trace("Shutdown") r.logger.Trace("Shutdown")
} }

View File

@@ -2,14 +2,21 @@ package main
import ( import (
"github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2"
"log"
) )
func main() { func main() {
// Create application with options // Create application with options
app := wails.CreateApp("{{.ProjectName}}", 1024, 768) app, err := wails.CreateApp("{{.ProjectName}}", 1024, 768)
if err != nil {
log.Fatal(err)
}
app.Bind(newBasic()) app.Bind(newBasic())
app.Run() err = app.Run()
if err != nil {
log.Fatal(err)
}
} }

View File

@@ -20,6 +20,14 @@ type WebClient struct {
running bool running bool
} }
func (wc *WebClient) SetTrayMenu(trayMenuJSON string) {
wc.logger.Info("Not implemented in server build")
}
func (wc *WebClient) UpdateTrayMenuLabel(trayMenuJSON string) {
wc.logger.Info("Not implemented in server build")
}
func (wc *WebClient) MessageDialog(dialogOptions *dialog.MessageDialog, callbackID string) { func (wc *WebClient) MessageDialog(dialogOptions *dialog.MessageDialog, callbackID string) {
wc.logger.Info("Not implemented in server build") wc.logger.Info("Not implemented in server build")
} }

View File

@@ -211,7 +211,6 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
options.CompiledBinary = compiledBinary options.CompiledBinary = compiledBinary
// Create the command // Create the command
fmt.Printf("Compile command: %+v", commands.AsSlice())
cmd := exec.Command(options.Compiler, commands.AsSlice()...) cmd := exec.Command(options.Compiler, commands.AsSlice()...)
// Set the directory // Set the directory

View File

@@ -104,7 +104,7 @@ func Build(options *Options) (string, error) {
// return "", err // return "", err
// } // }
if !options.IgnoreFrontend { if !options.IgnoreFrontend {
outputLogger.Println(" - Building Wails Frontend") outputLogger.Println(" - Building Project Frontend")
err = builder.BuildFrontend(outputLogger) err = builder.BuildFrontend(outputLogger)
if err != nil { if err != nil {
return "", err return "", err

View File

@@ -12,6 +12,14 @@ func (m *Menu) Append(item *MenuItem) {
m.Items = append(m.Items, item) m.Items = append(m.Items, item)
} }
// Merge will append the items in the given menu
// into this menu
func (m *Menu) Merge(menu *Menu) {
for _, item := range menu.Items {
m.Items = append(m.Items, item)
}
}
func (m *Menu) Prepend(item *MenuItem) { func (m *Menu) Prepend(item *MenuItem) {
m.Items = append([]*MenuItem{item}, m.Items...) m.Items = append([]*MenuItem{item}, m.Items...)
} }

View File

@@ -28,11 +28,18 @@ type MenuItem struct {
// Callback function when menu clicked // Callback function when menu clicked
Click Callback `json:"-"` Click Callback `json:"-"`
// Foreground colour in hex RGBA format EG: 0xFF0000FF = #FF0000FF = red // Colour
Foreground int RGBA string
// Background colour // Font
Background int FontSize int
FontName string
// Image - base64 image data
Image string
// Tooltip
Tooltip string
// This holds the menu item's parent. // This holds the menu item's parent.
parent *MenuItem parent *MenuItem

View File

@@ -1,11 +1,12 @@
package options package options
import ( import (
wailsruntime "github.com/wailsapp/wails/v2/internal/runtime"
"github.com/wailsapp/wails/v2/pkg/menu"
"log" "log"
"runtime" "runtime"
wailsruntime "github.com/wailsapp/wails/v2/internal/runtime"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/imdario/mergo" "github.com/imdario/mergo"
"github.com/wailsapp/wails/v2/pkg/logger" "github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/options/mac" "github.com/wailsapp/wails/v2/pkg/options/mac"
@@ -33,6 +34,7 @@ type App struct {
LogLevel logger.LogLevel LogLevel logger.LogLevel
Startup func(*wailsruntime.Runtime) `json:"-"` Startup func(*wailsruntime.Runtime) `json:"-"`
Shutdown func() `json:"-"` Shutdown func() `json:"-"`
Bind []interface{}
} }
// MergeDefaults will set the minimum default values for an application // MergeDefaults will set the minimum default values for an application

10
v2/pkg/str/str.go Normal file
View File

@@ -0,0 +1,10 @@
package str
import (
"fmt"
"time"
)
func UnixNow() string {
return fmt.Sprintf("%+v", time.Now().Unix())
}

View File

@@ -58,6 +58,7 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

View File

@@ -14,19 +14,12 @@ type Runtime = runtime.Runtime
// Store is an alias for the Store object // Store is an alias for the Store object
type Store = runtime.Store type Store = runtime.Store
// CreateAppWithOptions creates an application based on the given config // Run creates an application based on the given config and executes it
func CreateAppWithOptions(options *options.App) (*app.App, error) { func Run(options *options.App) error {
return app.CreateApp(options) app, err := app.CreateApp(options)
} if err != nil {
return err
// CreateApp creates an application based on the given title, width and height
func CreateApp(title string, width int, height int) (*app.App, error) {
options := &options.App{
Title: title,
Width: width,
Height: height,
} }
return app.CreateApp(options) return app.Run()
} }