5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 07:21:32 +08:00

merge exp branch

This commit is contained in:
Lea Anthony 2023-01-18 21:42:49 +11:00
parent 18d0ed890a
commit 81645190f0
No known key found for this signature in database
GPG Key ID: 33DAF7BB90A58405
138 changed files with 17055 additions and 1 deletions

3
.gitignore vendored
View File

@ -31,3 +31,6 @@ v2/test/kitchensink/frontend/package.json.md5
v2/cmd/wails/internal/commands/initialise/templates/testtemplates/ v2/cmd/wails/internal/commands/initialise/templates/testtemplates/
.env .env
/website/static/img/.cache.json /website/static/img/.cache.json
/v3/.task
/v3/examples/build/bin/testapp

View File

@ -4,12 +4,13 @@ import (
"context" "context"
"embed" "embed"
"fmt" "fmt"
"time"
"github.com/wailsapp/wails/v2/pkg/application" "github.com/wailsapp/wails/v2/pkg/application"
"github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/windows" "github.com/wailsapp/wails/v2/pkg/options/windows"
"github.com/wailsapp/wails/v2/pkg/runtime" "github.com/wailsapp/wails/v2/pkg/runtime"
"time"
) )
//go:embed all:frontend/dist //go:embed all:frontend/dist

8
v3/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
examples/kitchensink/kitchensink
cmd/wails/wails
/examples/systray/systray
/examples/window/window
/examples/dialogs/dialogs
/examples/menu/menu
/examples/clipboard/clipboard
/examples/plain/plain

136
v3/Taskfile.yaml Normal file
View File

@ -0,0 +1,136 @@
# https://taskfile.dev
version: '3'
tasks:
build-runtime-debug:
dir: internal/runtime
internal: true
cmds:
- npx esbuild desktop/main.js --bundle --sourcemap=inline --outfile=runtime_debug_desktop_{{.PLATFORM}}.js --define:DEBUG=true --define:WINDOWS={{.WINDOWS}} --define:DARWIN={{.DARWIN}} --define:LINUX={{.LINUX}} --define:PLATFORM={{.PLATFORM}}
build-runtime-debug-windows:
cmds:
- task: build-runtime-debug
vars:
WINDOWS: true
DARWIN: false
LINUX: false
PLATFORM: windows
build-runtime-debug-linux:
cmds:
- task: build-runtime-debug
vars:
WINDOWS: false
DARWIN: false
LINUX: true
PLATFORM: linux
build-runtime-debug-darwin:
cmds:
- task: build-runtime-debug
vars:
WINDOWS: false
DARWIN: true
LINUX: false
PLATFORM: darwin
build-runtime-production:
dir: internal/runtime
internal: true
cmds:
- npx esbuild desktop/main.js --bundle --minify --outfile=runtime_production_desktop_{{.PLATFORM}}.js --define:DEBUG=true --define:WINDOWS={{.WINDOWS}} --define:DARWIN={{.DARWIN}} --define:LINUX={{.LINUX}} --define:PLATFORM={{.PLATFORM}}
build-runtime-production-windows:
cmds:
- task: build-runtime-production
vars:
WINDOWS: true
DARWIN: false
LINUX: false
PLATFORM: windows
build-runtime-production-linux:
cmds:
- task: build-runtime-production
vars:
WINDOWS: false
DARWIN: false
LINUX: true
PLATFORM: linux
build-runtime-production-darwin:
cmds:
- task: build-runtime-production
vars:
WINDOWS: false
DARWIN: true
LINUX: false
PLATFORM: darwin
install-runtime-dev-deps:
dir: internal/runtime/dev
internal: true
sources:
- package.json
cmds:
- npm install
install-runtime-deps:
dir: internal/runtime
internal: true
sources:
- package.json
cmds:
- npm install
build-runtime-dev:
dir: internal/runtime/dev
deps:
- install-runtime-dev-deps
sources:
- ./*.js
generates:
- ../ipc_websocket.js
cmds:
- node build.js
build-runtime-ipc:
dir: internal/runtime
deps:
- install-runtime-dev-deps
sources:
- ./desktop/ipc.js
generates:
- ipc.js
cmds:
- npx esbuild desktop/ipc.js --bundle --minify --outfile=ipc.js
test-runtime:
dir: internal/runtime
cmds:
- npx vitest
build-runtime-all:
dir: internal/runtime
deps:
- build-runtime-production-darwin
- build-runtime-production-windows
- build-runtime-production-linux
- build-runtime-debug-darwin
- build-runtime-debug-windows
- build-runtime-debug-linux
- build-runtime-dev
- build-runtime-ipc
cmds:
- task: test-runtime
build-runtime:
dir: internal/runtime
deps:
- install-runtime-deps
cmds:
- task: build-runtime-all

79
v3/cmd/wails/README.md Normal file
View File

@ -0,0 +1,79 @@
# The Wails CLI
The Wails CLI is a command line tool that allows you to create, build and run Wails applications.
There are a number of commands related to tooling, such as icon generation and asset bundling.
## Commands
### run
The run command is for running tasks defined in `Taskfile.yml`.
| Flag | Type | Description | Default |
|--------------------|--------|------------------------------------------------------|-----------------------|
| `-t` | string | The name of the task to run | |
### generate
The `generate` command is used to generate resources and assets for your Wails project.
It can be used to generate many things including:
- application icons,
- resource files for Windows applications
- Info.plist files for macOS deployments
#### icon
The `icon` command generates icons for your project.
| Flag | Type | Description | Default |
|--------------------|--------|------------------------------------------------------|-----------------------|
| `-example` | bool | Generates example icon file (appicon.png) | |
| `-input` | string | The input image file | |
| `-sizes` | string | The sizes to generate in .ico file (comma separated) | "256,128,64,48,32,16" |
| `-windowsFilename` | string | The output filename for the Windows icon | icons.ico |
| `-macFilename` | string | The output filename for the Mac icon bundle | icons.icns |
```bash
wails generate icon -input myicon.png -sizes "32,64,128" -windowsFilename myicon.ico -macFilename myicon.icns
```
This will generate icons for mac and windows and save them in the current directory as `myicon.ico`
and `myicons.icns`.
#### syso
The `syso` command generates a Windows resource file (aka `.syso`).
```bash
wails generate syso <options>
```
| Flag | Type | Description | Default |
|-------------|--------|--------------------------------------------|------------------|
| `-example` | bool | Generates example manifest & info files | |
| `-manifest` | string | The manifest file | |
| `-info` | string | The info.json file | |
| `-icon` | string | The icon file | |
| `-out` | string | The output filename for the syso file | `wails.exe.syso` |
| `-arch` | string | The target architecture (amd64,arm64,386) | `runtime.GOOS` |
If `-example` is provided, the command will generate example manifest and info files
in the current directory and exit.
If `-manifest` is provided, the command will use the provided manifest file to generate
the syso file.
If `-info` is provided, the command will use the provided info.json file to set the version
information in the syso file.
NOTE: We use [winres](https://github.com/tc-hib/winres) to generate the syso file. Please
refer to the winres documentation for more information.
NOTE: Whilst the tool will work for 32-bit Windows, it is not supported. Please use 64-bit.
#### defaults
```bash
wails icon -input myicon.png -sizes "32,64,128" -windowsFilename myicon.ico -macFilename myicon.icns
```
This will generate all the default assets and resources in the current directory. I

28
v3/cmd/wails/main.go Normal file
View File

@ -0,0 +1,28 @@
package main
import (
"os"
"github.com/pterm/pterm"
"github.com/leaanthony/clir"
"github.com/wailsapp/wails/v3/internal/commands"
)
func main() {
app := clir.NewCli("wails", "The Wails CLI", "v3")
app.NewSubCommandFunction("init", "Initialise a new project", commands.Init)
task := app.NewSubCommand("task", "Run and list tasks")
task.NewSubCommandFunction("run", "Run a task", commands.RunTask)
task.NewSubCommandFunction("list", "List tasks", commands.ListTasks)
generate := app.NewSubCommand("generate", "Generation tools")
generate.NewSubCommandFunction("defaults", "Generate default build assets", commands.Defaults)
generate.NewSubCommandFunction("icons", "Generate icons", commands.GenerateIcons)
generate.NewSubCommandFunction("syso", "Generate Windows .syso file", commands.GenerateSyso)
generate.NewSubCommandFunction("bindings", "Generate bindings + models", commands.GenerateBindings)
err := app.Run()
if err != nil {
pterm.Error.Println(err)
os.Exit(1)
}
}

View File

@ -0,0 +1,31 @@
package main
import (
_ "embed"
"log"
"github.com/wailsapp/wails/v3/examples/binding/services"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/options"
)
type localStruct struct{}
func main() {
app := application.New(options.Application{
Bind: []interface{}{
&localStruct{},
&services.GreetService{},
},
})
app.NewWebviewWindow()
err := app.Run()
if err != nil {
log.Fatal(err)
}
}

View File

@ -0,0 +1,5 @@
package models
type Person struct {
Name string
}

View File

@ -0,0 +1,23 @@
package services
import (
"github.com/wailsapp/wails/v3/examples/binding/models"
)
type GreetService struct {
SomeVariable int
lowercase string
Parent *models.Person
}
func (*GreetService) Greet(name string) string {
return "Hello " + name
}
func (g *GreetService) GetPerson() *models.Person {
return g.Parent
}
func (g *GreetService) SetPerson(person *models.Person) {
g.Parent = person
}

View File

@ -0,0 +1,15 @@
# Build
Wails has adopted [Taskfile](https://taskfile.dev) as its build tool. This is optional
and any build tool can be used. However, Taskfile is a great tool, and we recommend it.
The Wails CLI has built-in integration with Taskfile so the standalone version is not a
requirement.
## Building
To build the example, run:
```bash
wails run -t build
```

View File

@ -0,0 +1,42 @@
version: '3'
tasks:
pre-build:
summary: Pre-build hooks
post-build:
summary: Post-build hooks
build:
summary: Builds the application
cmds:
- task: pre-build
- go build -gcflags=all="-N -l" -o bin/testapp main.go
- task: post-build
env:
CGO_CFLAGS: "-mmacosx-version-min=10.13"
CGO_LDFLAGS: "-mmacosx-version-min=10.13"
generate-icons:
summary: Generates Windows `.ico` and Mac `.icns` files from an image
dir: build
cmds:
# Generates both .ico and .icns files
- wails generate icon -input appicon.png
build-prod:
summary: Creates a production build of the application
cmds:
- go build -tags production -ldflags="-w -s" -o bin/testapp
package-darwin:
summary: Packages a production build of the application into a `.app` bundle
deps:
- build-prod
- generate-icons
cmds:
- mkdir -p buildtest.app/Contents/{MacOS,Resources}
- cp build/icons.icns buildtest.app/Contents/Resources
- cp bin/testapp buildtest.app/Contents/MacOS
- cp build/Info.plist buildtest.app/Contents

View File

@ -0,0 +1,35 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>My App</string>
<key>CFBundleExecutable</key>
<string>app</string>
<key>CFBundleIdentifier</key>
<string>com.wails.app</string>
<key>CFBundleVersion</key>
<string>v1.0.0</string>
<key>CFBundleGetInfoString</key>
<string>The ultimate thing</string>
<key>CFBundleShortVersionString</key>
<string>v1</string>
<key>CFBundleIconFile</key>
<string>icons</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>(c) Me</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>
</dict>
</plist>

View File

@ -0,0 +1,27 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>My App</string>
<key>CFBundleExecutable</key>
<string>app</string>
<key>CFBundleIdentifier</key>
<string>com.wails.app</string>
<key>CFBundleVersion</key>
<string>v1.0.0</string>
<key>CFBundleGetInfoString</key>
<string>The ultimate thing</string>
<key>CFBundleShortVersionString</key>
<string>v1</string>
<key>CFBundleIconFile</key>
<string>icons</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>(c) Me</string>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,15 @@
{
"fixed": {
"file_version": "v1.0.0"
},
"info": {
"0000": {
"ProductVersion": "v1.0.0",
"CompanyName": "My Company Name",
"FileDescription": "A thing that does a thing",
"LegalCopyright": "(c) 2023 My Company Name",
"ProductName": "My Product Name",
"Comments": "This is a comment"
}
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.wails.myproductname" version="v1.0.0.0" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

View File

@ -0,0 +1,27 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>My App</string>
<key>CFBundleExecutable</key>
<string>app</string>
<key>CFBundleIdentifier</key>
<string>com.wails.app</string>
<key>CFBundleVersion</key>
<string>v1.0.0</string>
<key>CFBundleGetInfoString</key>
<string>The ultimate thing</string>
<key>CFBundleShortVersionString</key>
<string>v1</string>
<key>CFBundleIconFile</key>
<string>icons</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>(c) Me</string>
</dict>
</plist>

275
v3/examples/build/main.go Executable file
View File

@ -0,0 +1,275 @@
package main
import (
_ "embed"
"fmt"
"log"
"math/rand"
"runtime"
"strconv"
"time"
"github.com/wailsapp/wails/v3/pkg/options"
"github.com/wailsapp/wails/v3/pkg/events"
"github.com/wailsapp/wails/v3/pkg/application"
)
func main() {
app := application.New(options.Application{
Name: "WebviewWindow Demo",
Description: "A demo of the WebviewWindow API",
Mac: options.Mac{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})
app.On(events.Mac.ApplicationDidFinishLaunching, func() {
log.Println("ApplicationDidFinishLaunching")
})
currentWindow := func(fn func(window *application.WebviewWindow)) {
if app.CurrentWindow() != nil {
fn(app.CurrentWindow())
} else {
println("Current WebviewWindow is nil")
}
}
// Create a custom menu
menu := app.NewMenu()
menu.AddRole(application.AppMenu)
windowCounter := 1
// Let's make a "Demo" menu
myMenu := menu.AddSubmenu("New")
myMenu.Add("New WebviewWindow").
SetAccelerator("CmdOrCtrl+N").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindow().
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
SetPosition(rand.Intn(1000), rand.Intn(800)).
SetURL("https://wails.io").
Show()
windowCounter++
})
myMenu.Add("New Frameless WebviewWindow").
SetAccelerator("CmdOrCtrl+F").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindow().
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
SetPosition(rand.Intn(1000), rand.Intn(800)).
SetURL("https://wails.io").
SetFrameless(true).
Show()
windowCounter++
})
if runtime.GOOS == "darwin" {
myMenu.Add("New WebviewWindow (TitleBarHiddenInset)").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindowWithOptions(&options.WebviewWindow{
Mac: options.MacWindow{
TitleBar: options.TitleBarHiddenInset,
InvisibleTitleBarHeight: 25,
},
}).
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
SetPosition(rand.Intn(1000), rand.Intn(800)).
SetHTML("<br/><br/><p>A TitleBarHiddenInset WebviewWindow example</p>").
Show()
windowCounter++
})
myMenu.Add("New WebviewWindow (TitleBarHiddenInsetUnified)").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindowWithOptions(&options.WebviewWindow{
Mac: options.MacWindow{
TitleBar: options.TitleBarHiddenInsetUnified,
InvisibleTitleBarHeight: 50,
},
}).
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
SetPosition(rand.Intn(1000), rand.Intn(800)).
SetHTML("<br/><br/><p>A TitleBarHiddenInsetUnified WebviewWindow example</p>").
Show()
windowCounter++
})
myMenu.Add("New WebviewWindow (TitleBarHidden)").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindowWithOptions(&options.WebviewWindow{
Mac: options.MacWindow{
TitleBar: options.TitleBarHidden,
InvisibleTitleBarHeight: 25,
},
}).
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
SetPosition(rand.Intn(1000), rand.Intn(800)).
SetHTML("<br/><br/><p>A TitleBarHidden WebviewWindow example</p>").
Show()
windowCounter++
})
}
sizeMenu := menu.AddSubmenu("Size")
sizeMenu.Add("Set Size (800,600)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetSize(800, 600)
})
})
sizeMenu.Add("Set Size (Random)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetSize(rand.Intn(800)+200, rand.Intn(600)+200)
})
})
sizeMenu.Add("Set Min Size (200,200)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetMinSize(200, 200)
})
})
sizeMenu.Add("Set Max Size (600,600)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetFullscreenButtonEnabled(false)
w.SetMaxSize(600, 600)
})
})
sizeMenu.Add("Get Current WebviewWindow Size").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
width, height := w.Size()
app.InfoDialog().SetTitle("Current WebviewWindow Size").SetMessage("Width: " + strconv.Itoa(width) + " Height: " + strconv.Itoa(height)).Show()
})
})
sizeMenu.Add("Reset Min Size").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetMinSize(0, 0)
})
})
sizeMenu.Add("Reset Max Size").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetMaxSize(0, 0)
w.SetFullscreenButtonEnabled(true)
})
})
positionMenu := menu.AddSubmenu("Position")
positionMenu.Add("Set Position (0,0)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetPosition(0, 0)
})
})
positionMenu.Add("Set Position (Random)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetPosition(rand.Intn(1000), rand.Intn(800))
})
})
positionMenu.Add("Get Position").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
x, y := w.Position()
app.InfoDialog().SetTitle("Current WebviewWindow Position").SetMessage("X: " + strconv.Itoa(x) + " Y: " + strconv.Itoa(y)).Show()
})
})
positionMenu.Add("Center").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.Center()
})
})
stateMenu := menu.AddSubmenu("State")
stateMenu.Add("Minimise (for 2 secs)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.Minimise()
time.Sleep(2 * time.Second)
w.Restore()
})
})
stateMenu.Add("Maximise").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.Maximise()
})
})
stateMenu.Add("Fullscreen").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.Fullscreen()
})
})
stateMenu.Add("UnFullscreen").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.UnFullscreen()
})
})
stateMenu.Add("Restore").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.Restore()
})
})
stateMenu.Add("Hide (for 2 seconds)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.Hide()
time.Sleep(2 * time.Second)
w.Show()
})
})
stateMenu.Add("Always on Top").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetAlwaysOnTop(true)
})
})
stateMenu.Add("Not always on Top").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetAlwaysOnTop(false)
})
})
stateMenu.Add("Google.com").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetURL("https://google.com")
})
})
stateMenu.Add("wails.io").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetURL("https://wails.io")
})
})
stateMenu.Add("Get Primary Screen").OnClick(func(ctx *application.Context) {
screen, err := app.GetPrimaryScreen()
if err != nil {
app.ErrorDialog().SetTitle("Error").SetMessage(err.Error()).Show()
return
}
msg := fmt.Sprintf("Screen: %+v", screen)
app.InfoDialog().SetTitle("Primary Screen").SetMessage(msg).Show()
})
stateMenu.Add("Get Screens").OnClick(func(ctx *application.Context) {
screens, err := app.GetScreens()
if err != nil {
app.ErrorDialog().SetTitle("Error").SetMessage(err.Error()).Show()
return
}
for _, screen := range screens {
msg := fmt.Sprintf("Screen: %+v", screen)
app.InfoDialog().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
}
})
stateMenu.Add("Get Screen for WebviewWindow").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
screen, err := w.GetScreen()
if err != nil {
app.ErrorDialog().SetTitle("Error").SetMessage(err.Error()).Show()
return
}
msg := fmt.Sprintf("Screen: %+v", screen)
app.InfoDialog().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
})
})
app.NewWebviewWindow()
app.SetMenu(menu)
err := app.Run()
if err != nil {
log.Fatal(err)
}
}

View File

@ -0,0 +1,71 @@
package main
import (
_ "embed"
"log"
"time"
"github.com/wailsapp/wails/v3/pkg/options"
"github.com/wailsapp/wails/v3/pkg/application"
)
func main() {
app := application.New(options.Application{
Name: "Clipboard Demo",
Description: "A demo of the clipboard API",
Mac: options.Mac{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})
// Create a custom menu
menu := app.NewMenu()
menu.AddRole(application.AppMenu)
setClipboardMenu := menu.AddSubmenu("Set Clipboard")
setClipboardMenu.Add("Set Text 'Hello'").OnClick(func(ctx *application.Context) {
success := app.Clipboard().SetText("Hello")
if !success {
app.InfoDialog().SetMessage("Failed to set clipboard text").Show()
}
})
setClipboardMenu.Add("Set Text 'World'").OnClick(func(ctx *application.Context) {
success := app.Clipboard().SetText("World")
if !success {
app.InfoDialog().SetMessage("Failed to set clipboard text").Show()
}
})
setClipboardMenu.Add("Set Text (current time)").OnClick(func(ctx *application.Context) {
success := app.Clipboard().SetText(time.Now().String())
if !success {
app.InfoDialog().SetMessage("Failed to set clipboard text").Show()
}
})
getClipboardMenu := menu.AddSubmenu("Get Clipboard")
getClipboardMenu.Add("Get Text").OnClick(func(ctx *application.Context) {
result := app.Clipboard().Text()
app.InfoDialog().SetMessage("Got:\n\n" + result).Show()
})
clearClipboardMenu := menu.AddSubmenu("Clear Clipboard")
clearClipboardMenu.Add("Clear Text").OnClick(func(ctx *application.Context) {
success := app.Clipboard().SetText("")
if success {
app.InfoDialog().SetMessage("Clipboard text cleared").Show()
} else {
app.InfoDialog().SetMessage("Clipboard text not cleared").Show()
}
})
app.SetMenu(menu)
app.NewWebviewWindow()
err := app.Run()
if err != nil {
log.Fatal(err.Error())
}
}

335
v3/examples/dialogs/main.go Normal file
View File

@ -0,0 +1,335 @@
package main
import (
_ "embed"
"log"
"os"
"runtime"
"strings"
"github.com/wailsapp/wails/v3/pkg/options"
"github.com/wailsapp/wails/v3/pkg/application"
)
func main() {
app := application.New(options.Application{
Name: "Dialogs Demo",
Description: "A demo of the dialogs API",
Mac: options.Mac{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})
// Create a custom menu
menu := app.NewMenu()
menu.AddRole(application.AppMenu)
// Let's make a "Demo" menu
infoMenu := menu.AddSubmenu("Info")
infoMenu.Add("Info").OnClick(func(ctx *application.Context) {
dialog := app.InfoDialog()
dialog.SetTitle("Custom Title")
dialog.SetMessage("This is a custom message")
dialog.Show()
})
infoMenu.Add("Info (Title only)").OnClick(func(ctx *application.Context) {
dialog := app.InfoDialog()
dialog.SetTitle("Custom Title")
dialog.Show()
})
infoMenu.Add("Info (Message only)").OnClick(func(ctx *application.Context) {
dialog := app.InfoDialog()
dialog.SetMessage("This is a custom message")
dialog.Show()
})
infoMenu.Add("Info (Custom Icon)").OnClick(func(ctx *application.Context) {
dialog := app.InfoDialog()
dialog.SetTitle("Custom Icon Example")
dialog.SetMessage("Using a custom icon")
dialog.SetIcon(application.DefaultApplicationIcon)
dialog.Show()
})
questionMenu := menu.AddSubmenu("Question")
questionMenu.Add("Question (No default)").OnClick(func(ctx *application.Context) {
dialog := app.QuestionDialog()
dialog.SetMessage("No default button")
dialog.AddButton("Yes")
dialog.AddButton("No")
dialog.Show()
})
questionMenu.Add("Question (With Default)").OnClick(func(ctx *application.Context) {
dialog := app.QuestionDialog()
dialog.SetTitle("Quit")
dialog.SetMessage("You have unsaved work. Are you sure you want to quit?")
dialog.AddButton("Yes").OnClick(func() {
app.Quit()
})
no := dialog.AddButton("No")
dialog.SetDefaultButton(no)
dialog.Show()
})
questionMenu.Add("Question (With Cancel)").OnClick(func(ctx *application.Context) {
dialog := app.QuestionDialog().
SetTitle("Update").
SetMessage("The cancel button is selected when pressing escape")
download := dialog.AddButton("📥 Download")
download.OnClick(func() {
app.InfoDialog().SetMessage("Downloading...").Show()
})
no := dialog.AddButton("Cancel")
dialog.SetDefaultButton(download)
dialog.SetCancelButton(no)
dialog.Show()
})
questionMenu.Add("Question (Custom Icon)").OnClick(func(ctx *application.Context) {
dialog := app.QuestionDialog()
dialog.SetTitle("Custom Icon Example")
dialog.SetMessage("Using a custom icon")
dialog.SetIcon(application.WailsLogoWhiteTransparent)
dialog.SetDefaultButton(dialog.AddButton("I like it!"))
dialog.AddButton("Not so keen...")
dialog.Show()
})
warningMenu := menu.AddSubmenu("Warning")
warningMenu.Add("Warning").OnClick(func(ctx *application.Context) {
dialog := app.WarningDialog()
dialog.SetTitle("Custom Title")
dialog.SetMessage("This is a custom message")
dialog.Show()
})
warningMenu.Add("Warning (Title only)").OnClick(func(ctx *application.Context) {
dialog := app.WarningDialog()
dialog.SetTitle("Custom Title")
dialog.Show()
})
warningMenu.Add("Warning (Message only)").OnClick(func(ctx *application.Context) {
dialog := app.WarningDialog()
dialog.SetMessage("This is a custom message")
dialog.Show()
})
warningMenu.Add("Warning (Custom Icon)").OnClick(func(ctx *application.Context) {
dialog := app.WarningDialog()
dialog.SetTitle("Custom Icon Example")
dialog.SetMessage("Using a custom icon")
dialog.SetIcon(application.DefaultApplicationIcon)
dialog.Show()
})
errorMenu := menu.AddSubmenu("Error")
errorMenu.Add("Error").OnClick(func(ctx *application.Context) {
dialog := app.ErrorDialog()
dialog.SetTitle("Ooops")
dialog.SetMessage("I accidentally the whole of Twitter")
dialog.Show()
})
errorMenu.Add("Error (Title Only)").OnClick(func(ctx *application.Context) {
dialog := app.ErrorDialog()
dialog.SetTitle("Custom Title")
dialog.Show()
})
errorMenu.Add("Error (Custom Message)").OnClick(func(ctx *application.Context) {
dialog := app.ErrorDialog()
dialog.SetMessage("This is a custom message")
dialog.Show()
})
errorMenu.Add("Error (Custom Icon)").OnClick(func(ctx *application.Context) {
dialog := app.ErrorDialog()
dialog.SetTitle("Custom Icon Example")
dialog.SetMessage("Using a custom icon")
dialog.SetIcon(application.WailsLogoWhite)
dialog.Show()
})
openMenu := menu.AddSubmenu("Open")
openMenu.Add("Open File").OnClick(func(ctx *application.Context) {
result, _ := app.OpenFileDialog().
CanChooseFiles(true).
PromptForSingleSelection()
if result != "" {
app.InfoDialog().SetMessage(result).Show()
} else {
app.InfoDialog().SetMessage("No file selected").Show()
}
})
openMenu.Add("Open File (Show Hidden Files)").OnClick(func(ctx *application.Context) {
result, _ := app.OpenFileDialog().
CanChooseFiles(true).
CanCreateDirectories(true).
ShowHiddenFiles(true).
PromptForSingleSelection()
if result != "" {
app.InfoDialog().SetMessage(result).Show()
} else {
app.InfoDialog().SetMessage("No file selected").Show()
}
})
openMenu.Add("Open File (Attach to window)").OnClick(func(ctx *application.Context) {
result, _ := app.OpenFileDialog().
CanChooseFiles(true).
CanCreateDirectories(true).
ShowHiddenFiles(true).
AttachToWindow(app.CurrentWindow()).
PromptForSingleSelection()
if result != "" {
app.InfoDialog().SetMessage(result).Show()
} else {
app.InfoDialog().SetMessage("No file selected").Show()
}
})
openMenu.Add("Open Multiple Files (Show Hidden Files)").OnClick(func(ctx *application.Context) {
result, _ := app.OpenFileDialog().
CanChooseFiles(true).
CanCreateDirectories(true).
ShowHiddenFiles(true).
PromptForMultipleSelection()
if len(result) > 0 {
app.InfoDialog().SetMessage(strings.Join(result, ",")).Show()
} else {
app.InfoDialog().SetMessage("No file selected").Show()
}
})
openMenu.Add("Open Directory").OnClick(func(ctx *application.Context) {
result, _ := app.OpenFileDialog().
CanChooseDirectories(true).
PromptForSingleSelection()
if result != "" {
app.InfoDialog().SetMessage(result).Show()
} else {
app.InfoDialog().SetMessage("No directory selected").Show()
}
})
openMenu.Add("Open Directory (Create Directories)").OnClick(func(ctx *application.Context) {
result, _ := app.OpenFileDialog().
CanChooseDirectories(true).
CanCreateDirectories(true).
PromptForSingleSelection()
if result != "" {
app.InfoDialog().SetMessage(result).Show()
} else {
app.InfoDialog().SetMessage("No directory selected").Show()
}
})
openMenu.Add("Open Directory (Resolves Aliases)").OnClick(func(ctx *application.Context) {
result, _ := app.OpenFileDialog().
CanChooseDirectories(true).
CanCreateDirectories(true).
ResolvesAliases(true).
PromptForSingleSelection()
if result != "" {
app.InfoDialog().SetMessage(result).Show()
} else {
app.InfoDialog().SetMessage("No directory selected").Show()
}
})
openMenu.Add("Open File/Directory (Set Title)").OnClick(func(ctx *application.Context) {
dialog := app.OpenFileDialog().
CanChooseDirectories(true).
CanCreateDirectories(true).
ResolvesAliases(true)
if runtime.GOOS == "darwin" {
dialog.SetMessage("Select a file/directory")
} else {
dialog.SetTitle("Select a file/directory")
}
result, _ := dialog.PromptForSingleSelection()
if result != "" {
app.InfoDialog().SetMessage(result).Show()
} else {
app.InfoDialog().SetMessage("No file/directory selected").Show()
}
})
openMenu.Add("Open (Full Example)").OnClick(func(ctx *application.Context) {
cwd, _ := os.Getwd()
dialog := app.OpenFileDialog().
SetTitle("Select a file").
SetMessage("Select a file to open").
SetButtonText("Let's do this!").
SetDirectory(cwd).
CanCreateDirectories(true).
ResolvesAliases(true).
AllowsOtherFileTypes(true).
TreatsFilePackagesAsDirectories(true).
ShowHiddenFiles(true).
CanSelectHiddenExtension(true).
AddFilter("Text Files", "*.txt; *.md").
AddFilter("Video Files", "*.mov; *.mp4; *.avi")
if runtime.GOOS == "darwin" {
dialog.SetMessage("Select a file")
} else {
dialog.SetTitle("Select a file")
}
result, _ := dialog.PromptForSingleSelection()
if result != "" {
app.InfoDialog().SetMessage(result).Show()
} else {
app.InfoDialog().SetMessage("No file selected").Show()
}
})
saveMenu := menu.AddSubmenu("Save")
saveMenu.Add("Select File (Defaults)").OnClick(func(ctx *application.Context) {
result, _ := app.SaveFileDialog().
PromptForSingleSelection()
if result != "" {
app.InfoDialog().SetMessage(result).Show()
}
})
saveMenu.Add("Select File (Attach To WebviewWindow)").OnClick(func(ctx *application.Context) {
result, _ := app.SaveFileDialog().
AttachToWindow(app.CurrentWindow()).
PromptForSingleSelection()
if result != "" {
app.InfoDialog().SetMessage(result).Show()
}
})
saveMenu.Add("Select File (Show Hidden Files)").OnClick(func(ctx *application.Context) {
result, _ := app.SaveFileDialog().
ShowHiddenFiles(true).
PromptForSingleSelection()
if result != "" {
app.InfoDialog().SetMessage(result).Show()
}
})
saveMenu.Add("Select File (Cannot Create Directories)").OnClick(func(ctx *application.Context) {
result, _ := app.SaveFileDialog().
CanCreateDirectories(false).
PromptForSingleSelection()
if result != "" {
app.InfoDialog().SetMessage(result).Show()
}
})
saveMenu.Add("Select File (Full Example)").OnClick(func(ctx *application.Context) {
result, _ := app.SaveFileDialog().
CanCreateDirectories(false).
ShowHiddenFiles(true).
SetMessage("Select a file").
SetDirectory("/Applications").
SetButtonText("Let's do this!").
SetFilename("README.md").
HideExtension(true).
AllowsOtherFileTypes(true).
TreatsFilePackagesAsDirectories(true).
ShowHiddenFiles(true).
PromptForSingleSelection()
if result != "" {
app.InfoDialog().SetMessage(result).Show()
}
})
app.SetMenu(menu)
app.NewWebviewWindow()
app.NewWebviewWindow()
err := app.Run()
if err != nil {
log.Fatal(err.Error())
}
}

View File

@ -0,0 +1,225 @@
package main
import (
_ "embed"
"log"
"runtime"
"sync"
"time"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/options"
)
func main() {
app := application.New(options.Application{
Name: "Menu Demo",
Description: "A demo of the menu system",
Mac: options.Mac{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})
/*
app.On(events.Mac.ApplicationDidFinishLaunching, func() {
println("ApplicationDidFinishLaunching")
})
app.On(events.Mac.ApplicationWillTerminate, func() {
println("ApplicationWillTerminate")
})
app.On(events.Mac.ApplicationDidBecomeActive, func() {
println("ApplicationDidBecomeActive")
})
app.On(events.Mac.ApplicationDidChangeBackingProperties, func() {
println("ApplicationDidChangeBackingProperties")
})
app.On(events.Mac.ApplicationDidChangeEffectiveAppearance, func() {
println("ApplicationDidChangeEffectiveAppearance")
})
app.On(events.Mac.ApplicationDidHide, func() {
println("ApplicationDidHide")
})
*/
menuCallback := func(ctx *application.Context) {
menuItem := ctx.ClickedMenuItem()
menuItem.SetLabel("Clicked!")
}
radioCallback := func(ctx *application.Context) {
menuItem := ctx.ClickedMenuItem()
menuItem.SetLabel(menuItem.Label() + "!")
}
myMenu := app.NewMenu()
file1 := myMenu.Add("File")
file1.SetTooltip("Create New Tray Menu")
file1.OnClick(menuCallback)
myMenu.Add("Create New Tray Menu").
SetAccelerator("CmdOrCtrl+N").
SetTooltip("ROFLCOPTER!!!!").
OnClick(func(ctx *application.Context) {
mySystray := app.NewSystemTray()
mySystray.SetLabel("Wails")
if runtime.GOOS == "darwin" {
mySystray.SetTemplateIcon(application.DefaultMacTemplateIcon)
} else {
mySystray.SetIcon(application.DefaultApplicationIcon)
}
myMenu := app.NewMenu()
myMenu.Add("Item 1")
myMenu.AddSeparator()
myMenu.Add("Kill this menu").OnClick(func(ctx *application.Context) {
mySystray.Destroy()
})
mySystray.SetMenu(myMenu)
})
myMenu.Add("Not Enabled").SetEnabled(false)
myMenu.AddSeparator()
myMenu.AddCheckbox("My checkbox", true).OnClick(menuCallback)
myMenu.AddSeparator()
myMenu.AddRadio("Radio 1", true).OnClick(radioCallback)
myMenu.AddRadio("Radio 2", false).OnClick(radioCallback)
myMenu.AddRadio("Radio 3", false).OnClick(radioCallback)
submenu := myMenu.AddSubmenu("Submenu")
submenu.Add("Submenu item 1").OnClick(menuCallback)
submenu.Add("Submenu item 2").OnClick(menuCallback)
submenu.Add("Submenu item 3").OnClick(menuCallback)
myMenu.AddSeparator()
file4 := myMenu.Add("File 4").OnClick(func(*application.Context) {
println("File 4 clicked")
})
myMenu.Add("Click to toggle").OnClick(func(*application.Context) {
enabled := file4.Enabled()
println("Enabled: ", enabled)
file4.SetEnabled(!enabled)
})
myMenu.Add("File 5").OnClick(menuCallback)
mySystray := app.NewSystemTray()
mySystray.SetLabel("Wails is awesome")
if runtime.GOOS == "darwin" {
mySystray.SetTemplateIcon(application.DefaultMacTemplateIcon)
} else {
mySystray.SetIcon(application.DefaultApplicationIcon)
}
mySystray.SetMenu(myMenu)
mySystray.SetIconPosition(application.NSImageLeading)
myWindow := app.NewWebviewWindowWithOptions(&options.WebviewWindow{
Title: "Kitchen Sink",
Width: 600,
Height: 400,
AlwaysOnTop: true,
DisableResize: false,
BackgroundColour: &options.RGBA{
Red: 255,
Green: 255,
Blue: 255,
Alpha: 30,
},
StartState: options.WindowStateMaximised,
Mac: options.MacWindow{
Backdrop: options.MacBackdropTranslucent,
Appearance: options.NSAppearanceNameDarkAqua,
},
})
/*
myWindow.On(events.Mac.WindowWillClose, func() {
println(myWindow.ID(), "WindowWillClose")
})
myWindow.On(events.Mac.WindowDidResize, func() {
//w, h := myWindow.Size()
//println(myWindow.ID(), "WindowDidResize", w, h)
})
myWindow.On(events.Mac.WindowDidMove, func() {
//x, y := myWindow.Position()
//println(myWindow.ID(), "WindowDidMove", x, y)
})
myWindow.On(events.Mac.WindowDidMiniaturize, func() {
println(myWindow.ID(), "WindowDidMiniaturize")
})
myWindow.On(events.Mac.WindowDidDeminiaturize, func() {
println(myWindow.ID(), "WindowDidDeminiaturize")
})
myWindow.On(events.Mac.WindowDidBecomeKey, func() {
println(myWindow.ID(), "WindowDidBecomeKey")
})
myWindow.On(events.Mac.WindowDidResignKey, func() {
println(myWindow.ID(), "WindowDidResignKey")
})
myWindow.On(events.Mac.WindowDidBecomeMain, func() {
println(myWindow.ID(), "WindowDidBecomeMain")
})
myWindow.On(events.Mac.WindowDidResignMain, func() {
println(myWindow.ID(), "WindowDidResignMain")
})
myWindow.On(events.Mac.WindowWillEnterFullScreen, func() {
println(myWindow.ID(), "WindowWillEnterFullScreen")
})
myWindow.On(events.Mac.WindowDidEnterFullScreen, func() {
println(myWindow.ID(), "WindowDidEnterFullScreen")
})
myWindow.On(events.Mac.WindowWillExitFullScreen, func() {
println(myWindow.ID(), "WindowWillExitFullScreen")
})
myWindow.On(events.Mac.WindowDidExitFullScreen, func() {
println(myWindow.ID(), "WindowDidExitFullScreen")
})
myWindow.On(events.Mac.WindowWillEnterVersionBrowser, func() {
println(myWindow.ID(), "WindowWillEnterVersionBrowser")
})
myWindow.On(events.Mac.WindowDidEnterVersionBrowser, func() {
println(myWindow.ID(), "WindowDidEnterVersionBrowser")
})
myWindow.On(events.Mac.WindowWillExitVersionBrowser, func() {
println(myWindow.ID(), "WindowWillExitVersionBrowser")
})
myWindow.On(events.Mac.WindowDidExitVersionBrowser, func() {
println(myWindow.ID(), "WindowDidExitVersionBrowser")
})
*/
var myWindow2 *application.WebviewWindow
var myWindow2Lock sync.RWMutex
myWindow2 = app.NewWebviewWindowWithOptions(&options.WebviewWindow{
Title: "#2",
Width: 1024,
Height: 768,
AlwaysOnTop: false,
URL: "https://google.com",
Mac: options.MacWindow{
Backdrop: options.MacBackdropTranslucent,
},
})
//myWindow2.On(events.Mac.WindowDidMove, func() {
// myWindow2Lock.RLock()
// x, y := myWindow2.Position()
// println(myWindow2.ID(), "WindowDidMove: ", x, y)
// myWindow2Lock.RUnlock()
//})
//
go func() {
time.Sleep(5 * time.Second)
myWindow2Lock.RLock()
myWindow.SetTitle("Wooooo")
myWindow.SetAlwaysOnTop(true)
myWindow2.SetTitle("OMG")
myWindow2.SetURL("https://wails.io")
myWindow.SetMinSize(600, 600)
myWindow.SetMaxSize(650, 650)
myWindow.Center()
myWindow2Lock.RUnlock()
}()
err := app.Run()
if err != nil {
log.Fatal(err)
}
}

97
v3/examples/menu/main.go Normal file
View File

@ -0,0 +1,97 @@
package main
import (
_ "embed"
"log"
"github.com/wailsapp/wails/v3/pkg/options"
"github.com/wailsapp/wails/v3/pkg/application"
)
func main() {
app := application.New(options.Application{
Name: "Menu Demo",
Description: "A demo of the menu system",
Mac: options.Mac{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})
// Create a custom menu
menu := app.NewMenu()
menu.AddRole(application.AppMenu)
// Let's make a "Demo" menu
myMenu := menu.AddSubmenu("Demo")
// Disabled menu item
myMenu.Add("Not Enabled").SetEnabled(false)
// Click callbacks
myMenu.Add("Click Me!").OnClick(func(ctx *application.Context) {
ctx.ClickedMenuItem().SetLabel("Thanks mate!")
})
// You can control the current window from the menu
myMenu.Add("Lock WebviewWindow Resize").OnClick(func(ctx *application.Context) {
if app.CurrentWindow().Resizable() {
app.CurrentWindow().SetResizable(false)
ctx.ClickedMenuItem().SetLabel("Unlock WebviewWindow Resize")
} else {
app.CurrentWindow().SetResizable(true)
ctx.ClickedMenuItem().SetLabel("Lock WebviewWindow Resize")
}
})
myMenu.AddSeparator()
// Checkboxes will tell you their new state so you don't need to track it
myMenu.AddCheckbox("My checkbox", true).OnClick(func(context *application.Context) {
println("Clicked checkbox. Checked:", context.ClickedMenuItem().Checked())
})
myMenu.AddSeparator()
// Callbacks can be shared. This is useful for radio groups
radioCallback := func(ctx *application.Context) {
menuItem := ctx.ClickedMenuItem()
menuItem.SetLabel(menuItem.Label() + "!")
}
// Radio groups are created implicitly by placing radio items next to each other in a menu
myMenu.AddRadio("Radio 1", true).OnClick(radioCallback)
myMenu.AddRadio("Radio 2", false).OnClick(radioCallback)
myMenu.AddRadio("Radio 3", false).OnClick(radioCallback)
// Submenus are also supported
submenu := myMenu.AddSubmenu("Submenu")
submenu.Add("Submenu item 1")
submenu.Add("Submenu item 2")
submenu.Add("Submenu item 3")
myMenu.AddSeparator()
beatles := myMenu.Add("Hello").OnClick(func(*application.Context) {
println("The beatles would be proud")
})
myMenu.Add("Toggle the menuitem above").OnClick(func(*application.Context) {
if beatles.Enabled() {
beatles.SetEnabled(false)
beatles.SetLabel("Goodbye")
} else {
beatles.SetEnabled(true)
beatles.SetLabel("Hello")
}
})
app.SetMenu(menu)
app.NewWebviewWindow()
err := app.Run()
if err != nil {
log.Fatal(err.Error())
}
}

45
v3/examples/plain/main.go Normal file
View File

@ -0,0 +1,45 @@
package main
import (
_ "embed"
"log"
"net/http"
"github.com/wailsapp/wails/v3/pkg/options"
"github.com/wailsapp/wails/v3/pkg/application"
)
func main() {
app := application.New(options.Application{
Name: "Plain",
Description: "A demo of using raw HTML & CSS",
Mac: options.Mac{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})
// Create window
app.NewWebviewWindowWithOptions(&options.WebviewWindow{
Title: "Plain Bundle",
CSS: `body { background-color: rgba(255, 255, 255, 0); } .main { color: white; margin: 20%; }`,
Mac: options.MacWindow{
InvisibleTitleBarHeight: 50,
Backdrop: options.MacBackdropTranslucent,
TitleBar: options.TitleBarHiddenInset,
},
URL: "/",
Assets: options.Assets{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`<html><head><title>Plain Bundle</title></head><body><div class="main"><h1>Plain Bundle</h1><p>This is a plain bundle. It has no frontend code but this was Served by the AssetServer's Handler</p></div></body></html>`))
}),
},
})
err := app.Run()
if err != nil {
log.Fatal(err)
}
}

View File

@ -0,0 +1,47 @@
package main
import (
_ "embed"
"log"
"runtime"
"github.com/wailsapp/wails/v3/pkg/options"
"github.com/wailsapp/wails/v3/pkg/application"
)
func main() {
app := application.New(options.Application{
Name: "Systray Demo",
Description: "A demo of the Systray API",
Mac: options.Mac{
ActivationPolicy: options.ActivationPolicyAccessory,
},
})
systemTray := app.NewSystemTray()
if runtime.GOOS == "darwin" {
systemTray.SetIcon(application.DefaultMacTemplateIcon)
}
myMenu := app.NewMenu()
myMenu.Add("Hello World!").OnClick(func(ctx *application.Context) {
app.InfoDialog().SetTitle("Hello World!").SetMessage("Hello World!").Show()
})
subMenu := myMenu.AddSubmenu("Submenu")
subMenu.Add("Click me!").OnClick(func(ctx *application.Context) {
ctx.ClickedMenuItem().SetLabel("Clicked!")
})
myMenu.AddSeparator()
myMenu.Add("Quit").OnClick(func(ctx *application.Context) {
app.Quit()
})
systemTray.SetMenu(myMenu)
err := app.Run()
if err != nil {
log.Fatal(err)
}
}

277
v3/examples/window/main.go Normal file
View File

@ -0,0 +1,277 @@
package main
import (
_ "embed"
"fmt"
"log"
"math/rand"
"runtime"
"strconv"
"time"
"github.com/wailsapp/wails/v3/pkg/options"
"github.com/wailsapp/wails/v3/pkg/events"
"github.com/wailsapp/wails/v3/pkg/application"
)
func main() {
app := application.New(options.Application{
Name: "WebviewWindow Demo",
Description: "A demo of the WebviewWindow API",
Mac: options.Mac{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})
app.On(events.Mac.ApplicationDidFinishLaunching, func() {
log.Println("ApplicationDidFinishLaunching")
})
currentWindow := func(fn func(window *application.WebviewWindow)) {
if app.CurrentWindow() != nil {
fn(app.CurrentWindow())
} else {
println("Current WebviewWindow is nil")
}
}
// Create a custom menu
menu := app.NewMenu()
menu.AddRole(application.AppMenu)
windowCounter := 1
// Let's make a "Demo" menu
myMenu := menu.AddSubmenu("New")
myMenu.Add("New WebviewWindow").
SetAccelerator("CmdOrCtrl+N").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindow().
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
SetPosition(rand.Intn(1000), rand.Intn(800)).
SetURL("https://wails.io").
Show()
windowCounter++
})
myMenu.Add("New Frameless WebviewWindow").
SetAccelerator("CmdOrCtrl+F").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindowWithOptions(&options.WebviewWindow{
X: rand.Intn(1000),
Y: rand.Intn(800),
Frameless: true,
Mac: options.MacWindow{
InvisibleTitleBarHeight: 50,
},
}).Show()
windowCounter++
})
if runtime.GOOS == "darwin" {
myMenu.Add("New WebviewWindow (TitleBarHiddenInset)").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindowWithOptions(&options.WebviewWindow{
Mac: options.MacWindow{
TitleBar: options.TitleBarHiddenInset,
InvisibleTitleBarHeight: 25,
},
}).
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
SetPosition(rand.Intn(1000), rand.Intn(800)).
SetHTML("<br/><br/><p>A TitleBarHiddenInset WebviewWindow example</p>").
Show()
windowCounter++
})
myMenu.Add("New WebviewWindow (TitleBarHiddenInsetUnified)").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindowWithOptions(&options.WebviewWindow{
Mac: options.MacWindow{
TitleBar: options.TitleBarHiddenInsetUnified,
InvisibleTitleBarHeight: 50,
},
}).
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
SetPosition(rand.Intn(1000), rand.Intn(800)).
SetHTML("<br/><br/><p>A TitleBarHiddenInsetUnified WebviewWindow example</p>").
Show()
windowCounter++
})
myMenu.Add("New WebviewWindow (TitleBarHidden)").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindowWithOptions(&options.WebviewWindow{
Mac: options.MacWindow{
TitleBar: options.TitleBarHidden,
InvisibleTitleBarHeight: 25,
},
}).
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
SetPosition(rand.Intn(1000), rand.Intn(800)).
SetHTML("<br/><br/><p>A TitleBarHidden WebviewWindow example</p>").
Show()
windowCounter++
})
}
sizeMenu := menu.AddSubmenu("Size")
sizeMenu.Add("Set Size (800,600)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetSize(800, 600)
})
})
sizeMenu.Add("Set Size (Random)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetSize(rand.Intn(800)+200, rand.Intn(600)+200)
})
})
sizeMenu.Add("Set Min Size (200,200)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetMinSize(200, 200)
})
})
sizeMenu.Add("Set Max Size (600,600)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetFullscreenButtonEnabled(false)
w.SetMaxSize(600, 600)
})
})
sizeMenu.Add("Get Current WebviewWindow Size").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
width, height := w.Size()
app.InfoDialog().SetTitle("Current WebviewWindow Size").SetMessage("Width: " + strconv.Itoa(width) + " Height: " + strconv.Itoa(height)).Show()
})
})
sizeMenu.Add("Reset Min Size").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetMinSize(0, 0)
})
})
sizeMenu.Add("Reset Max Size").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetMaxSize(0, 0)
w.SetFullscreenButtonEnabled(true)
})
})
positionMenu := menu.AddSubmenu("Position")
positionMenu.Add("Set Position (0,0)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetPosition(0, 0)
})
})
positionMenu.Add("Set Position (Random)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetPosition(rand.Intn(1000), rand.Intn(800))
})
})
positionMenu.Add("Get Position").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
x, y := w.Position()
app.InfoDialog().SetTitle("Current WebviewWindow Position").SetMessage("X: " + strconv.Itoa(x) + " Y: " + strconv.Itoa(y)).Show()
})
})
positionMenu.Add("Center").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.Center()
})
})
stateMenu := menu.AddSubmenu("State")
stateMenu.Add("Minimise (for 2 secs)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.Minimise()
time.Sleep(2 * time.Second)
w.Restore()
})
})
stateMenu.Add("Maximise").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.Maximise()
})
})
stateMenu.Add("Fullscreen").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.Fullscreen()
})
})
stateMenu.Add("UnFullscreen").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.UnFullscreen()
})
})
stateMenu.Add("Restore").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.Restore()
})
})
stateMenu.Add("Hide (for 2 seconds)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.Hide()
time.Sleep(2 * time.Second)
w.Show()
})
})
stateMenu.Add("Always on Top").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetAlwaysOnTop(true)
})
})
stateMenu.Add("Not always on Top").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetAlwaysOnTop(false)
})
})
stateMenu.Add("Google.com").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetURL("https://google.com")
})
})
stateMenu.Add("wails.io").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetURL("https://wails.io")
})
})
stateMenu.Add("Get Primary Screen").OnClick(func(ctx *application.Context) {
screen, err := app.GetPrimaryScreen()
if err != nil {
app.ErrorDialog().SetTitle("Error").SetMessage(err.Error()).Show()
return
}
msg := fmt.Sprintf("Screen: %+v", screen)
app.InfoDialog().SetTitle("Primary Screen").SetMessage(msg).Show()
})
stateMenu.Add("Get Screens").OnClick(func(ctx *application.Context) {
screens, err := app.GetScreens()
if err != nil {
app.ErrorDialog().SetTitle("Error").SetMessage(err.Error()).Show()
return
}
for _, screen := range screens {
msg := fmt.Sprintf("Screen: %+v", screen)
app.InfoDialog().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
}
})
stateMenu.Add("Get Screen for WebviewWindow").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
screen, err := w.GetScreen()
if err != nil {
app.ErrorDialog().SetTitle("Error").SetMessage(err.Error()).Show()
return
}
msg := fmt.Sprintf("Screen: %+v", screen)
app.InfoDialog().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
})
})
app.NewWebviewWindow()
app.SetMenu(menu)
err := app.Run()
if err != nil {
log.Fatal(err)
}
}

56
v3/go.mod Normal file
View File

@ -0,0 +1,56 @@
module github.com/wailsapp/wails/v3
go 1.19
require (
github.com/go-task/task/v3 v3.19.1
github.com/jackmordaunt/icns/v2 v2.2.1
github.com/leaanthony/clir v1.5.0
github.com/leaanthony/winicon v1.0.0
github.com/pterm/pterm v0.12.51
github.com/samber/lo v1.37.0
github.com/stretchr/testify v1.8.1
github.com/tc-hib/winres v0.1.6
github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6
golang.org/x/tools v0.1.12
)
require (
atomicgo.dev/cursor v0.1.1 // indirect
atomicgo.dev/keyboard v0.2.8 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/gookit/color v1.5.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/joho/godotenv v1.4.0 // indirect
github.com/leaanthony/slicer v1.5.0 // indirect
github.com/lithammer/fuzzysearch v1.1.5 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-zglob v0.0.4 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/radovskyb/watcher v1.0.7 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/sajari/fuzzy v1.0.0 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/term v0.3.0 // indirect
golang.org/x/text v0.4.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
mvdan.cc/sh/v3 v3.6.0 // indirect
)
replace github.com/go-task/task/v3 v3.19.1 => github.com/wailsapp/task/v3 v3.19.1

179
v3/go.sum Normal file
View File

@ -0,0 +1,179 @@
atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg=
atomicgo.dev/cursor v0.1.1 h1:0t9sxQomCTRh5ug+hAMCs59x/UmC9QL6Ci5uosINKD4=
atomicgo.dev/cursor v0.1.1/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU=
atomicgo.dev/keyboard v0.2.8 h1:Di09BitwZgdTV1hPyX/b9Cqxi8HVuJQwWivnZUEqlj4=
atomicgo.dev/keyboard v0.2.8/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=
github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=
github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=
github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
github.com/MarvinJWendt/testza v0.5.1 h1:a9Fqx6vQrHQ4CyiaLhktfTTelwGotmFWy8MNhyaohw8=
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
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/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
github.com/gookit/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI=
github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/jackmordaunt/icns/v2 v2.2.1 h1:MGklwYP2yohKn2Bw7XxlF69LZe98S1vUfl5OvAulPwg=
github.com/jackmordaunt/icns/v2 v2.2.1/go.mod h1:6aYIB9eSzyfHHMKqDf17Xrs1zetQPReAkiUSHzdw4cI=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.2.0 h1:4ZexSFt8agMNzNisrsilL6RClWDC5YJnLHNIfTy4iuc=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0=
github.com/leaanthony/clir v1.5.0 h1:zaH7fgsZ5OLfr0YwJBwQ+EYxCjXQsHF+CRudIiZb0KQ=
github.com/leaanthony/clir v1.5.0/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0=
github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY=
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
github.com/leaanthony/winicon v1.0.0 h1:ZNt5U5dY71oEoKZ97UVwJRT4e+5xo5o/ieKuHuk8NqQ=
github.com/leaanthony/winicon v1.0.0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU=
github.com/lithammer/fuzzysearch v1.1.5 h1:Ag7aKU08wp0R9QCfF4GoGST9HbmAIeLP7xwMrOBEp1c=
github.com/lithammer/fuzzysearch v1.1.5/go.mod h1:1R1LRNk7yKid1BaQkmuLQaHruxcC4HmAH30Dh61Ih1Q=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-zglob v0.0.4 h1:LQi2iOm0/fGgu80AioIJ/1j9w9Oh+9DZ39J4VAGzHQM=
github.com/mattn/go-zglob v0.0.4/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
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/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
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/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=
github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
github.com/pterm/pterm v0.12.51 h1:iwhNG1FhQMgks+5kVyr/ClRk3WJCuL907nJN7RqmEpw=
github.com/pterm/pterm v0.12.51/go.mod h1:79BLm4vos2z+eOoHnDG7ZWuYtLaSStyaspKjGmSoxc4=
github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw=
github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tc-hib/winres v0.1.6 h1:qgsYHze+BxQPEYilxIz/KCQGaClvI2+yLBAZs+3+0B8=
github.com/tc-hib/winres v0.1.6/go.mod h1:pe6dOR40VOrGz8PkzreVKNvEKnlE8t4yR8A8naL+t7A=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/task/v3 v3.19.1 h1:syDKYaPBXgrXKKSJVEWcOEoSFtZzpvxqlHf90YRukRc=
github.com/wailsapp/task/v3 v3.19.1/go.mod h1:y7rWakbLR5gFElGgo6rA2dyr6vU/zNIDVfn3S4Of6OI=
github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 h1:Wn+nhnS+VytzE0PegUzSh4T3hXJCtggKGD/4U5H9+wQ=
github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6/go.mod h1:zlNLI0E2c2qA6miiuAHtp0Bac8FaGH0tlhA19OssR/8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 h1:RjggHMcaTVp0LOVZcW0bo8alwHrOaCrGUDgfWUHhnN4=
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/sh/v3 v3.6.0 h1:gtva4EXJ0dFNvl5bHjcUEvws+KRcDslT8VKheTYkbGU=
mvdan.cc/sh/v3 v3.6.0/go.mod h1:U4mhtBLZ32iWhif5/lD+ygy1zrgaQhUu+XFy7C8+TTA=

View File

@ -0,0 +1,42 @@
package commands
import (
"bytes"
"fmt"
"os"
"github.com/wailsapp/wails/v3/internal/parser"
)
type GenerateBindingsOptions struct {
ModelsFilename string `name:"m" description:"The filename for the models file" default:"models.ts"`
BindingsFilename string `name:"b" description:"The filename for the bindings file" default:"bindings.js"`
ProjectDirectory string `name:"d" description:"The project directory" default:"."`
}
func GenerateBindings(options *GenerateBindingsOptions) error {
parserContext, err := parser.ParseDirectory(options.ProjectDirectory)
if err != nil {
return fmt.Errorf("error parsing project: %v", err)
}
// Generate models
modelsData, err := parser.GenerateModels(parserContext)
if err != nil {
return fmt.Errorf("error generating models: %v", err)
}
var modelsFileData bytes.Buffer
modelsFileData.WriteString(`// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
`)
modelsFileData.Write(modelsData)
err = os.WriteFile(options.ModelsFilename, modelsFileData.Bytes(), 0755)
if err != nil {
return fmt.Errorf("error writing models file: %v", err)
}
return nil
}

View File

@ -0,0 +1,61 @@
package commands
import (
_ "embed"
"os"
)
//go:embed defaults/info.json
var Info []byte
//go:embed defaults/wails.exe.manifest
var Manifest []byte
//go:embed defaults/appicon.png
var AppIcon []byte
//go:embed defaults/icons.ico
var IconsIco []byte
//go:embed defaults/Info.plist
var InfoPlist []byte
//go:embed defaults/Info.dev.plist
var InfoDevPlist []byte
//go:embed defaults/icons.icns
var IconsIcns []byte
var AllAssets = map[string][]byte{
"info.json": Info,
"wails.exe.manifest": Manifest,
"appicon.png": AppIcon,
"icons.ico": IconsIco,
"Info.plist": InfoPlist,
"Info.dev.plist": InfoDevPlist,
"icons.icns": IconsIcns,
}
type DefaultsOptions struct {
Dir string `description:"The directory to generate the files into"`
}
func Defaults(options *DefaultsOptions) error {
dir := options.Dir
if dir == "" {
dir = "."
}
for filename, data := range AllAssets {
// If file exists, skip it
if _, err := os.Stat(dir + "/" + filename); err == nil {
println("Skipping " + filename)
continue
}
err := os.WriteFile(dir+"/"+filename, data, 0644)
if err != nil {
return err
}
println("Generated " + filename)
}
return nil
}

View File

@ -0,0 +1,32 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key>
<string>{{.Name}}</string>
<key>CFBundleIdentifier</key>
<string>com.wails.{{.Name}}</string>
<key>CFBundleVersion</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key>
<string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>{{.Info.Copyright}}</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,27 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key>
<string>{{.Name}}</string>
<key>CFBundleIdentifier</key>
<string>com.wails.{{.Name}}</string>
<key>CFBundleVersion</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key>
<string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>{{.Info.Copyright}}</string>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,15 @@
{
"fixed": {
"file_version": "v1.0.0"
},
"info": {
"0000": {
"ProductVersion": "v1.0.0",
"CompanyName": "My Company Name",
"FileDescription": "A thing that does a thing",
"LegalCopyright": "(c) 2023 My Company Name",
"ProductName": "My Product Name",
"Comments": "This is a comment"
}
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.wails.myproductname" version="v1.0.0.0" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

View File

@ -0,0 +1,135 @@
package commands
import (
"bytes"
"fmt"
"image"
"os"
"strconv"
"strings"
"github.com/jackmordaunt/icns/v2"
"github.com/leaanthony/winicon"
)
type IconsOptions struct {
Example bool `description:"Generate example icon file (appicon.png) in the current directory"`
Input string `description:"The input image file"`
Sizes string `description:"The sizes to generate in .ico file (comma separated)"`
WindowsFilename string `description:"The output filename for the Windows icon"`
MacFilename string `description:"The output filename for the Mac icon bundle"`
}
func (i *IconsOptions) Default() *IconsOptions {
return &IconsOptions{
Sizes: "256,128,64,48,32,16",
MacFilename: "icons.icns",
WindowsFilename: "icons.ico",
}
}
func GenerateIcons(options *IconsOptions) error {
if options.Example {
return generateExampleIcon()
}
if options.Input == "" {
return fmt.Errorf("input is required")
}
if options.WindowsFilename == "" && options.MacFilename == "" {
return fmt.Errorf("at least one output filename is required")
}
// Parse sizes
var sizes = []int{256, 128, 64, 48, 32, 16}
var err error
if options.Sizes != "" {
sizes, err = parseSizes(options.Sizes)
if err != nil {
return err
}
}
iconData, err := os.ReadFile(options.Input)
if err != nil {
return err
}
if options.WindowsFilename != "" {
err := generateWindowsIcon(iconData, sizes, options)
if err != nil {
return err
}
}
if options.MacFilename != "" {
err := generateMacIcon(iconData, options)
if err != nil {
return err
}
}
return nil
}
func generateExampleIcon() error {
return os.WriteFile("appicon.png", []byte(AppIcon), 0644)
}
func parseSizes(sizes string) ([]int, error) {
// split the input string by comma and confirm that each one is an integer
parsedSizes := strings.Split(sizes, ",")
var result []int
for _, size := range parsedSizes {
s, err := strconv.Atoi(size)
if err != nil {
return nil, err
}
if s == 0 {
continue
}
result = append(result, s)
}
// put all integers in a slice and return
return result, nil
}
func generateMacIcon(iconData []byte, options *IconsOptions) error {
srcImg, _, err := image.Decode(bytes.NewBuffer(iconData))
if err != nil {
return err
}
dest, err := os.Create(options.MacFilename)
if err != nil {
return err
}
defer func() {
err = dest.Close()
if err == nil {
return
}
}()
return icns.Encode(dest, srcImg)
}
func generateWindowsIcon(iconData []byte, sizes []int, options *IconsOptions) error {
var output bytes.Buffer
err := winicon.GenerateIcon(bytes.NewBuffer(iconData), &output, sizes)
if err != nil {
return err
}
err = os.WriteFile(options.WindowsFilename, output.Bytes(), 0644)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,285 @@
package commands
import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"
)
func TestGenerateIcon(t *testing.T) {
tests := []struct {
name string
setup func() *IconsOptions
wantErr bool
test func() error
}{
{
name: "should generate an icon when using the `example` flag",
setup: func() *IconsOptions {
return &IconsOptions{
Example: true,
}
},
wantErr: false,
test: func() error {
// the file `appicon.png` should be created in the current directory
// check for the existence of the file
f, err := os.Stat("appicon.png")
if err != nil {
return err
}
defer func() {
err := os.Remove("appicon.png")
if err != nil {
panic(err)
}
}()
if f.IsDir() {
return fmt.Errorf("appicon.png is a directory")
}
if f.Size() == 0 {
return fmt.Errorf("appicon.png is empty")
}
return nil
},
},
{
name: "should generate a .ico file when using the `input` flag and `windowsfilena me` flag",
setup: func() *IconsOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "appicon.png")
return &IconsOptions{
Input: exampleIcon,
WindowsFilename: "appicon.ico",
}
},
wantErr: false,
test: func() error {
// the file `appicon.ico` should be created in the current directory
// check for the existence of the file
f, err := os.Stat("appicon.ico")
if err != nil {
return err
}
defer func() {
// Remove the file
err = os.Remove("appicon.ico")
if err != nil {
return
}
}()
if f.IsDir() {
return fmt.Errorf("appicon.ico is a directory")
}
if f.Size() == 0 {
return fmt.Errorf("appicon.ico is empty")
}
// Remove the file
err = os.Remove("appicon.ico")
if err != nil {
return err
}
return nil
},
},
{
name: "should generate a .icns file when using the `input` flag and `macfilename` flag",
setup: func() *IconsOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "appicon.png")
return &IconsOptions{
Input: exampleIcon,
MacFilename: "appicon.icns",
}
},
wantErr: false,
test: func() error {
// the file `appicon.icns` should be created in the current directory
// check for the existence of the file
f, err := os.Stat("appicon.icns")
if err != nil {
return err
}
defer func() {
// Remove the file
err = os.Remove("appicon.icns")
if err != nil {
panic(err)
}
}()
if f.IsDir() {
return fmt.Errorf("appicon.icns is a directory")
}
if f.Size() == 0 {
return fmt.Errorf("appicon.icns is empty")
}
// Remove the file
return nil
},
},
{
name: "should generate a small .ico file when using the `input` flag and `sizes` flag",
setup: func() *IconsOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "appicon.png")
return &IconsOptions{
Input: exampleIcon,
Sizes: "16",
WindowsFilename: "appicon.ico",
}
},
wantErr: false,
test: func() error {
// the file `appicon.ico` should be created in the current directory
// check for the existence of the file
f, err := os.Stat("appicon.ico")
if err != nil {
return err
}
defer func() {
err := os.Remove("appicon.ico")
if err != nil {
panic(err)
}
}()
// The size of the file should be 571 bytes
if f.Size() != 571 {
return fmt.Errorf("appicon.ico is not the correct size. Got %d", f.Size())
}
if f.IsDir() {
return fmt.Errorf("appicon.ico is a directory")
}
if f.Size() == 0 {
return fmt.Errorf("appicon.ico is empty")
}
return nil
},
},
{
name: "should error if no input file is provided",
setup: func() *IconsOptions {
return &IconsOptions{}
},
wantErr: true,
},
{
name: "should error if neither mac or windows filename is provided",
setup: func() *IconsOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "appicon.png")
return &IconsOptions{
Input: exampleIcon,
}
},
wantErr: true,
},
{
name: "should error if bad sizes provided",
setup: func() *IconsOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "appicon.png")
return &IconsOptions{
Input: exampleIcon,
WindowsFilename: "appicon.ico",
Sizes: "bad",
}
},
wantErr: true,
},
{
name: "should ignore 0 size",
setup: func() *IconsOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "appicon.png")
return &IconsOptions{
Input: exampleIcon,
WindowsFilename: "appicon.ico",
Sizes: "0,16",
}
},
wantErr: false,
test: func() error {
// Test the file exists and has 571 bytes
f, err := os.Stat("appicon.ico")
if err != nil {
return err
}
defer func() {
err := os.Remove("appicon.ico")
if err != nil {
panic(err)
}
}()
if f.Size() != 571 {
return fmt.Errorf("appicon.ico is not the correct size. Got %d", f.Size())
}
if f.IsDir() {
return fmt.Errorf("appicon.ico is a directory")
}
if f.Size() == 0 {
return fmt.Errorf("appicon.ico is empty")
}
return nil
},
},
{
name: "should error if the input file does not exist",
setup: func() *IconsOptions {
return &IconsOptions{
Input: "doesnotexist.png",
WindowsFilename: "appicon.ico",
}
},
wantErr: true,
},
{
name: "should error if the input file is not a png",
setup: func() *IconsOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
return &IconsOptions{
Input: thisFile,
WindowsFilename: "appicon.ico",
}
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
options := tt.setup()
err := GenerateIcons(options)
if (err != nil) != tt.wantErr {
t.Errorf("GenerateIcon() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.test != nil {
if err := tt.test(); err != nil {
t.Errorf("GenerateIcon() test error = %v", err)
}
}
})
}
}

View File

@ -0,0 +1,8 @@
package commands
type InitOptions struct {
}
func Init(options *InitOptions) error {
return nil
}

View File

@ -0,0 +1,123 @@
package commands
import (
"fmt"
"os"
"runtime"
"github.com/tc-hib/winres"
"github.com/tc-hib/winres/version"
)
type SysoOptions struct {
Example bool `description:"Generate example manifest & info files"`
Manifest string `description:"The manifest file"`
Info string `description:"The info.json file"`
Icon string `description:"The icon file"`
Out string `description:"The output filename for the syso file"`
Arch string `description:"The target architecture"`
}
func (i *SysoOptions) Default() *SysoOptions {
return &SysoOptions{
Arch: runtime.GOARCH,
}
}
func GenerateSyso(options *SysoOptions) error {
// Generate example files?
if options.Example {
return generateExampleSyso()
}
if options.Manifest == "" {
return fmt.Errorf("manifest is required")
}
if options.Icon == "" {
return fmt.Errorf("icon is required")
}
rs := winres.ResourceSet{}
// Process Icon
iconFile, err := os.Open(options.Icon)
if err != nil {
return err
}
defer iconFile.Close()
ico, err := winres.LoadICO(iconFile)
if err != nil {
return fmt.Errorf("couldn't load icon '%s': %v", options.Icon, err)
}
err = rs.SetIcon(winres.RT_ICON, ico)
if err != nil {
return err
}
// Process Manifest
manifestData, err := os.ReadFile(options.Manifest)
if err != nil {
return err
}
xmlData, err := winres.AppManifestFromXML(manifestData)
if err != nil {
return err
}
rs.SetManifest(xmlData)
if options.Info != "" {
infoData, err := os.ReadFile(options.Info)
if err != nil {
return err
}
if len(infoData) != 0 {
var v version.Info
if err := v.UnmarshalJSON(infoData); err != nil {
return err
}
rs.SetVersionInfo(v)
}
}
targetFile := options.Out
if targetFile == "" {
targetFile = "rsrc_windows_" + options.Arch + ".syso"
}
fout, err := os.Create(targetFile)
if err != nil {
return err
}
defer fout.Close()
archs := map[string]winres.Arch{
"amd64": winres.ArchAMD64,
"arm64": winres.ArchARM64,
"386": winres.ArchI386,
}
targetArch, supported := archs[options.Arch]
if !supported {
return fmt.Errorf("arch '%s' not supported", options.Arch)
}
err = rs.WriteObject(fout, targetArch)
if err != nil {
return err
}
return nil
}
func generateExampleSyso() error {
// Generate example info.json
err := os.WriteFile("info.json", Info, 0644)
if err != nil {
return err
}
// Generate example manifest
err = os.WriteFile("wails.exe.manifest", Manifest, 0644)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,189 @@
package commands
import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"
)
func TestGenerateSyso(t *testing.T) {
tests := []struct {
name string
setup func() *SysoOptions
wantErr bool
test func() error
}{
{
name: "should generate example info and manifest files when using the `example` flag",
setup: func() *SysoOptions {
return &SysoOptions{
Example: true,
}
},
wantErr: false,
test: func() error {
// the file `info.json` should be created in the current directory
// check for the existence of the file
f, err := os.Stat("info.json")
if err != nil {
return err
}
m, err := os.Stat("wails.exe.manifest")
if err != nil {
return err
}
defer func() {
err := os.Remove("info.json")
err2 := os.Remove("wails.exe.manifest")
if err != nil {
panic(err)
}
if err2 != nil {
panic(err2)
}
}()
if f.IsDir() {
return fmt.Errorf("info.json is a directory")
}
if f.Size() == 0 {
return fmt.Errorf("info.json is empty")
}
if m.IsDir() {
return fmt.Errorf("wails.exe.manifest is a directory")
}
if m.Size() == 0 {
return fmt.Errorf("wails.exe.manifest is empty")
}
return nil
},
},
{
name: "should error if manifest filename is not provided",
setup: func() *SysoOptions {
return &SysoOptions{
Manifest: "",
}
},
wantErr: true,
},
{
name: "should error if icon filename is not provided",
setup: func() *SysoOptions {
return &SysoOptions{
Manifest: "test.manifest",
Icon: "",
}
},
wantErr: true,
},
{
name: "should error if icon filename does not exist",
setup: func() *SysoOptions {
return &SysoOptions{
Manifest: "test.manifest",
Icon: "icon.ico",
}
},
wantErr: true,
},
{
name: "should error if icon is wrong format",
setup: func() *SysoOptions {
_, thisFile, _, _ := runtime.Caller(1)
return &SysoOptions{
Manifest: "test.manifest",
Icon: thisFile,
}
},
wantErr: true,
},
{
name: "should error if manifest filename does not exist",
setup: func() *SysoOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "icon.ico")
return &SysoOptions{
Manifest: "test.manifest",
Icon: exampleIcon,
}
},
wantErr: true,
},
{
name: "should error if manifest is wrong format",
setup: func() *SysoOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "icon.ico")
return &SysoOptions{
Manifest: exampleIcon,
Icon: exampleIcon,
}
},
wantErr: true,
},
{
name: "should error if info file does not exist",
setup: func() *SysoOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "icon.ico")
// Get the path to the example manifest
exampleManifest := filepath.Join(localDir, "examples", "wails.exe.manifest")
return &SysoOptions{
Manifest: exampleManifest,
Icon: exampleIcon,
Info: "doesnotexist.json",
}
},
wantErr: true,
},
{
name: "should error if info file is wrong format",
setup: func() *SysoOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "icon.ico")
// Get the path to the example manifest
exampleManifest := filepath.Join(localDir, "examples", "wails.exe.manifest")
return &SysoOptions{
Manifest: exampleManifest,
Icon: exampleIcon,
Info: thisFile,
}
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
options := tt.setup()
err := GenerateSyso(options)
if (err != nil) != tt.wantErr {
t.Errorf("GenerateSyso() error = %v, wantErr %v", err, tt.wantErr)
return
}
if (err != nil) && tt.wantErr {
println(err.Error())
return
}
if tt.test != nil {
if err := tt.test(); err != nil {
t.Errorf("GenerateSyso() test error = %v", err)
}
}
})
}
}

View File

@ -0,0 +1,66 @@
package commands
import (
"context"
"fmt"
"github.com/pterm/pterm"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/taskfile"
)
type RunTaskOptions struct {
Name string `name:"n" description:"The name of the task to run"`
}
func RunTask(options *RunTaskOptions) error {
if options.Name == "" {
return fmt.Errorf("name of task required")
}
e := task.Executor{}
err := e.Setup()
if err != nil {
return err
}
build := taskfile.Call{
Task: options.Name,
Vars: nil,
}
return e.Run(context.Background(), build)
}
type ListTaskOptions struct {
}
func ListTasks(options *ListTaskOptions) error {
e := task.Executor{}
if err := e.Setup(); err != nil {
return err
}
tasks := e.GetTaskList()
if len(tasks) == 0 {
return fmt.Errorf("no tasks found. Ensure there is a `Taskfile.yml` in your project. You can generate a default takfile by running `wails generate defaults`")
}
tableData := [][]string{
{"Task", "Summary"},
}
println()
for _, thisTask := range tasks {
if thisTask.Internal {
continue
}
var thisRow = make([]string, 2)
thisRow[0] = thisTask.Task
thisRow[1] = thisTask.Summary
tableData = append(tableData, thisRow)
}
err := pterm.DefaultTable.WithHasHeader(true).WithHeaderRowSeparator("-").WithData(tableData).Render()
if err != nil {
return err
}
println()
return nil
}

View File

@ -0,0 +1,38 @@
package commands
import "testing"
func TestBuild(t *testing.T) {
type args struct {
options *RunTaskOptions
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "should error if task name not provided",
args: args{
options: &RunTaskOptions{},
},
wantErr: true,
},
{
name: "should work if task name provided",
args: args{
options: &RunTaskOptions{
Name: "build",
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := RunTask(tt.args.options); (err != nil) != tt.wantErr {
t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@ -0,0 +1,24 @@
# Parser
This package contains the static analyser used for parsing Wails projects so that we may:
- Generate the bindings for the frontend
- Generate Typescript definitions for the structs used by the bindings
## Implemented
- [x] Parsing of structs
- [x] Generation of models
- [x] Scalars
- [x] Arrays
- [ ] Maps
- [x] Structs
- [ ] Generation of bindings
## Limitations
There are many ways to write a Go program so there are many different program structures that we would need to support. This is a work in progress and will be improved over time. The current limitations are:
- The call to `application.New()` must be in the `main` package
- Bound structs must be declared as struct literals

View File

@ -0,0 +1,95 @@
package parser
import (
"bytes"
"fmt"
"go/ast"
"go/types"
"sort"
"strings"
"unicode"
)
func GenerateModels(context *Context) ([]byte, error) {
var buf bytes.Buffer
var pkgs []Package
specs := context.GetBoundStructs()
for pkg, pkgSpecs := range specs {
pkgs = append(pkgs, Package{Name: pkg, Specs: pkgSpecs})
}
knownStructs := newAllModels(specs)
sort.Slice(pkgs, func(i, j int) bool { return pkgs[i].Name < pkgs[j].Name })
for _, pkg := range pkgs {
if _, err := fmt.Fprintf(&buf, "namespace %s {\n", pkg.Name); err != nil {
return nil, err
}
sort.Slice(pkg.Specs, func(i, j int) bool { return pkg.Specs[i].Name.Name < pkg.Specs[j].Name.Name })
for _, spec := range pkg.Specs {
if structType, ok := spec.Type.(*ast.StructType); ok {
if _, err := fmt.Fprintf(&buf, " class %s {\n", spec.Name.Name); err != nil {
return nil, err
}
for _, field := range structType.Fields.List {
// Ignore field names that have a lower case first letter
if !unicode.IsUpper(rune(field.Names[0].Name[0])) {
continue
}
// Get the Go type of the field
goType := types.ExprString(field.Type)
// Check if the type is an array
if arrayType, ok := field.Type.(*ast.ArrayType); ok {
// Get the element type of the array
elementType := types.ExprString(arrayType.Elt)
// Look up the corresponding TypeScript type
tsType, ok := goToTS[elementType]
if !ok {
// strip off the * prefix if it is there
if strings.HasPrefix(elementType, "*") {
elementType = elementType[1:]
}
if knownStructs.exists(elementType) {
tsType = elementType
} else {
tsType = "any"
}
}
// Output the field as an array of the corresponding TypeScript type
if _, err := fmt.Fprintf(&buf, " %s: %s[];\n", field.Names[0].Name, tsType); err != nil {
return nil, err
}
} else {
// strip off the * prefix if it is there
if strings.HasPrefix(goType, "*") {
goType = goType[1:]
}
// Look up the corresponding TypeScript type
tsType, ok := goToTS[goType]
if !ok {
if knownStructs.exists(goType) {
tsType = goType
} else {
tsType = "any"
}
}
// Output the field as the corresponding TypeScript type
if _, err := fmt.Fprintf(&buf, " %s: %s;\n", field.Names[0].Name, tsType); err != nil {
return nil, err
}
}
}
if _, err := fmt.Fprintf(&buf, " }\n"); err != nil {
return nil, err
}
}
}
if _, err := fmt.Fprintf(&buf, "}\n\n"); err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}

View File

@ -0,0 +1,531 @@
package parser
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"strings"
"github.com/samber/lo"
"golang.org/x/tools/go/packages"
)
var Debug = false
func debug(msg string, args ...interface{}) {
if Debug {
println(fmt.Sprintf(msg, args...))
}
}
type parsedPackage struct {
name string
pkg *ast.Package
boundStructs map[string]*ast.TypeSpec
}
type Context struct {
packages map[string]*parsedPackage
}
func (c *Context) GetBoundStructs() map[string][]*ast.TypeSpec {
structs := make(map[string][]*ast.TypeSpec)
for _, pkg := range c.packages {
for _, structType := range pkg.boundStructs {
structs[pkg.name] = append(structs[pkg.name], structType)
}
}
return structs
}
func ParseDirectory(dir string) (*Context, error) {
// Parse the directory
fset := token.NewFileSet()
if dir == "." || dir == "" {
cwd, err := os.Getwd()
if err != nil {
return nil, err
}
dir = cwd
}
println("Parsing directory " + dir)
pkgs, err := parser.ParseDir(fset, dir, nil, parser.AllErrors)
if err != nil {
return nil, err
}
context := &Context{
packages: make(map[string]*parsedPackage),
}
// Iterate through the packages
for _, pkg := range pkgs {
context.packages[pkg.Name] = &parsedPackage{
name: pkg.Name,
pkg: pkg,
boundStructs: make(map[string]*ast.TypeSpec),
}
}
findApplicationNewCalls(context)
return context, nil
}
func findApplicationNewCalls(context *Context) {
// Iterate through the packages
currentPackages := lo.Keys(context.packages)
for _, packageName := range currentPackages {
thisPackage := context.packages[packageName]
debug("Parsing package: %s", packageName)
// Iterate through the package's files
for _, file := range thisPackage.pkg.Files {
// Use an ast.Inspector to find the calls to application.New
ast.Inspect(file, func(n ast.Node) bool {
// Check if the node is a call expression
callExpr, ok := n.(*ast.CallExpr)
if !ok {
return true
}
// Check if the function being called is "application.New"
selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
if selExpr.Sel.Name != "New" {
return true
}
if id, ok := selExpr.X.(*ast.Ident); !ok || id.Name != "application" {
return true
}
// Check there is only 1 argument
if len(callExpr.Args) != 1 {
return true
}
// Check argument 1 is a struct literal
structLit, ok := callExpr.Args[0].(*ast.CompositeLit)
if !ok {
return true
}
// Check struct literal is of type "options.Application"
selectorExpr, ok := structLit.Type.(*ast.SelectorExpr)
if !ok {
return true
}
if selectorExpr.Sel.Name != "Application" {
return true
}
if id, ok := selectorExpr.X.(*ast.Ident); !ok || id.Name != "options" {
return true
}
for _, elt := range structLit.Elts {
// Find the "Bind" field
kvExpr, ok := elt.(*ast.KeyValueExpr)
if !ok {
continue
}
if id, ok := kvExpr.Key.(*ast.Ident); !ok || id.Name != "Bind" {
continue
}
// Check the value is a slice of interfaces
sliceExpr, ok := kvExpr.Value.(*ast.CompositeLit)
if !ok {
continue
}
var arrayType *ast.ArrayType
if arrayType, ok = sliceExpr.Type.(*ast.ArrayType); !ok {
continue
}
// Check array type is of type "interface{}"
if _, ok := arrayType.Elt.(*ast.InterfaceType); !ok {
continue
}
// Iterate through the slice elements
for _, elt := range sliceExpr.Elts {
// Check the element is a unary expression
unaryExpr, ok := elt.(*ast.UnaryExpr)
if ok {
// Check the unary expression is a composite lit
boundStructLit, ok := unaryExpr.X.(*ast.CompositeLit)
if !ok {
continue
}
// Check if the composite lit is a struct
if _, ok := boundStructLit.Type.(*ast.StructType); ok {
// Parse struct
continue
}
// Check if the lit is an ident
ident, ok := boundStructLit.Type.(*ast.Ident)
if ok {
if ident.Obj == nil {
structTypeSpec := findStructInPackage(thisPackage.pkg, ident.Name)
thisPackage.boundStructs[ident.Name] = structTypeSpec
findNestedStructs(structTypeSpec, file, packageName, context)
continue
}
// Check if the ident is a struct type
if t, ok := ident.Obj.Decl.(*ast.TypeSpec); ok {
thisPackage.boundStructs[ident.Name] = t
findNestedStructs(t, file, packageName, context)
continue
}
// Check the typespec decl is a struct
if _, ok := ident.Obj.Decl.(*ast.StructType); ok {
continue
}
}
// Check if the lit is a selector
selector, ok := boundStructLit.Type.(*ast.SelectorExpr)
if ok {
getStructsFromSelector(selector, file, context)
continue
}
}
}
}
return true
})
}
}
}
func getStructsFromSelector(selector *ast.SelectorExpr, file *ast.File, context *Context) {
debug("getStructsFromSelector called with selector '%s' on file '%s.go'", selector.Sel.Name, file.Name.Name)
// extract package name from selector
packageName := selector.X.(*ast.Ident).Name
if context.packages[packageName] == nil {
context.packages[packageName] = &parsedPackage{
name: packageName,
boundStructs: make(map[string]*ast.TypeSpec),
}
}
// extract struct name from selector
structName := selector.Sel.Name
// Find the package name from the imports
for _, imp := range file.Imports {
var match bool
if imp.Name == nil || imp.Name.Name == packageName {
match = true
}
if match == false {
pathSplit := strings.Split(imp.Path.Value, "/")
endPath := strings.Trim(pathSplit[len(pathSplit)-1], `"`)
match = endPath == packageName
}
if match {
// We have the import
cfg := &packages.Config{
Mode: packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps | packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedModule,
}
pkgs, err := packages.Load(cfg, strings.Trim(imp.Path.Value, `"`))
if err != nil {
panic(err)
}
foundPackage := pkgs[0]
// Iterate the files in the package and find struct types
for _, parsedFile := range foundPackage.Syntax {
ast.Inspect(parsedFile, func(n ast.Node) bool {
if n == nil {
return false
}
switch n.(type) {
case *ast.TypeSpec:
typeSpec := n.(*ast.TypeSpec)
if typeSpec.Name.Name == structName {
if _, ok := context.packages[packageName].boundStructs[structName]; !ok {
debug("Adding struct '%s' in package '%s'", structName, packageName)
context.packages[packageName].boundStructs[typeSpec.Name.Name] = typeSpec
findNestedStructs(typeSpec, parsedFile, packageName, context)
}
return false
}
}
return true
})
}
continue
}
}
}
func findNestedStructs(t *ast.TypeSpec, parsedFile *ast.File, pkgName string, context *Context) {
debug("findNestedStructs called with type '%s' on file '%s.go'", t.Name.Name, parsedFile.Name.Name)
structType, ok := t.Type.(*ast.StructType)
if !ok {
return
}
for _, field := range structType.Fields.List {
for _, ident := range field.Names {
switch t := ident.Obj.Decl.(*ast.Field).Type.(type) {
case *ast.Ident:
if t.Obj == nil {
continue
}
if t.Obj.Kind == ast.Typ {
if _, ok := t.Obj.Decl.(*ast.TypeSpec); ok {
if _, ok := context.packages[pkgName].boundStructs[t.Name]; !ok {
debug("Adding nested struct '%s' to package '%s'", t.Name, pkgName)
context.packages[pkgName].boundStructs[t.Name] = t.Obj.Decl.(*ast.TypeSpec)
findNestedStructs(t.Obj.Decl.(*ast.TypeSpec), parsedFile, pkgName, context)
}
}
}
case *ast.SelectorExpr:
if ident, ok := t.X.(*ast.Ident); ok {
if ident.IsExported() {
getStructsFromSelector(t, parsedFile, context)
}
}
case *ast.StarExpr:
if sel, ok := t.X.(*ast.SelectorExpr); ok {
if _, ok := sel.X.(*ast.Ident); ok {
if ident.IsExported() {
getStructsFromSelector(sel, parsedFile, context)
}
}
}
}
}
}
findStructsInMethods(t.Name.Name, parsedFile, pkgName, context)
}
func findStructsInMethods(name string, parsedFile *ast.File, pkgName string, context *Context) {
debug("findStructsInMethods called with type '%s' on file '%s.go'", name, parsedFile.Name.Name)
// Find the struct declaration for the given name
var structDecl *ast.TypeSpec
for _, decl := range parsedFile.Decls {
if fn, ok := decl.(*ast.FuncDecl); ok {
// check the receiver name is the same as the name given
if fn.Recv == nil {
continue
}
// Check if the receiver is a pointer
if starExpr, ok := fn.Recv.List[0].Type.(*ast.StarExpr); ok {
if ident, ok := starExpr.X.(*ast.Ident); ok {
if ident.Name != name {
continue
}
}
} else {
if ident, ok := fn.Recv.List[0].Type.(*ast.Ident); ok {
if ident.Name != name {
continue
}
}
}
findStructsInMethodParams(fn, parsedFile, pkgName, context)
}
}
if structDecl == nil {
return
}
// Iterate the methods in the struct
}
func findStructsInMethodParams(f *ast.FuncDecl, parsedFile *ast.File, pkgName string, context *Context) {
debug("findStructsInMethodParams called with type '%s' on file '%s.go'", f.Name.Name, parsedFile.Name.Name)
if f.Type.Params == nil {
for _, field := range f.Type.Params.List {
parseField(field, parsedFile, pkgName, context)
}
}
if f.Type.Results != nil {
for _, field := range f.Type.Results.List {
parseField(field, parsedFile, pkgName, context)
}
}
}
func parseField(field *ast.Field, parsedFile *ast.File, pkgName string, context *Context) {
if se, ok := field.Type.(*ast.StarExpr); ok {
// Check if the star expr is a struct
if selExp, ok := se.X.(*ast.SelectorExpr); ok {
getStructsFromSelector(selExp, parsedFile, context)
return
}
if ident, ok := se.X.(*ast.Ident); ok {
if ident.Obj == nil {
return
}
if ident.Obj.Kind == ast.Typ {
if _, ok := ident.Obj.Decl.(*ast.TypeSpec); ok {
if _, ok := context.packages[pkgName].boundStructs[ident.Name]; !ok {
debug("Adding field struct '%s' to package '%s'", ident.Name, pkgName)
context.packages[pkgName].boundStructs[ident.Name] = ident.Obj.Decl.(*ast.TypeSpec)
findNestedStructs(ident.Obj.Decl.(*ast.TypeSpec), parsedFile, pkgName, context)
} else {
debug("Struct %s already bound", ident.Name)
}
}
}
}
}
if selExp, ok := field.Type.(*ast.SelectorExpr); ok {
getStructsFromSelector(selExp, parsedFile, context)
return
}
if ident, ok := field.Type.(*ast.Ident); ok {
if ident.Obj == nil {
return
}
if ident.Obj.Kind == ast.Typ {
if _, ok := ident.Obj.Decl.(*ast.TypeSpec); ok {
if _, ok := context.packages[pkgName].boundStructs[ident.Name]; !ok {
debug("Adding field struct '%s' to package '%s'", ident.Name, pkgName)
context.packages[pkgName].boundStructs[ident.Name] = ident.Obj.Decl.(*ast.TypeSpec)
findNestedStructs(ident.Obj.Decl.(*ast.TypeSpec), parsedFile, pkgName, context)
} else {
debug("Struct %s already bound", ident.Name)
}
}
}
}
}
func findStructInPackage(pkg *ast.Package, name string) *ast.TypeSpec {
for _, file := range pkg.Files {
for _, decl := range file.Decls {
if gen, ok := decl.(*ast.GenDecl); ok && gen.Tok == token.TYPE {
for _, spec := range gen.Specs {
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
if typeSpec.Name.Name == name {
if _, ok := typeSpec.Type.(*ast.StructType); ok {
return typeSpec
}
}
}
}
}
}
}
return nil
}
type Package struct {
Name string
Specs []*ast.TypeSpec
}
var goToTS = map[string]string{
"int": "number",
"int8": "number",
"int16": "number",
"int32": "number",
"int64": "number",
"uint": "number",
"uint8": "number",
"uint16": "number",
"uint32": "number",
"uint64": "number",
"float32": "number",
"float64": "number",
"string": "string",
"bool": "boolean",
}
//func GenerateModels(specs map[string][]*ast.TypeSpec) ([]byte, error) {
// var buf bytes.Buffer
// var packages []Package
// for pkg, pkgSpecs := range specs {
// packages = append(packages, Package{Name: pkg, Specs: pkgSpecs})
// }
// sort.Slice(packages, func(i, j int) bool { return packages[i].Name < packages[j].Name })
// for _, pkg := range packages {
// if _, err := fmt.Fprintf(&buf, "namespace %s {\n", pkg.Name); err != nil {
// return nil, err
// }
// sort.Slice(pkg.Specs, func(i, j int) bool { return pkg.Specs[i].Name.Name < pkg.Specs[j].Name.Name })
// for _, spec := range pkg.Specs {
// if structType, ok := spec.Type.(*ast.StructType); ok {
// if _, err := fmt.Fprintf(&buf, " class %s {\n", spec.Name.Name); err != nil {
// return nil, err
// }
//
// for _, field := range structType.Fields.List {
//
// // Get the Go type of the field
// goType := types.ExprString(field.Type)
// // Look up the corresponding TypeScript type
// tsType, ok := goToTS[goType]
// if !ok {
// tsType = goType
// }
//
// if _, err := fmt.Fprintf(&buf, " %s: %s;\n", field.Names[0].Name, tsType); err != nil {
// return nil, err
// }
// }
//
// if _, err := fmt.Fprintf(&buf, " }\n"); err != nil {
// return nil, err
// }
// if _, err := fmt.Fprintf(&buf, " }\n"); err != nil {
// return nil, err
// }
// }
// }
//
// if _, err := fmt.Fprintf(&buf, "}\n"); err != nil {
// return nil, err
// }
// }
// return buf.Bytes(), nil
//}
type allModels struct {
known map[string]map[string]struct{}
}
func newAllModels(models map[string][]*ast.TypeSpec) *allModels {
result := &allModels{known: make(map[string]map[string]struct{})}
// iterate over all models
for pkg, pkgSpecs := range models {
for _, spec := range pkgSpecs {
result.known[pkg] = make(map[string]struct{})
result.known[pkg][spec.Name.Name] = struct{}{}
}
}
return result
}
func (k *allModels) exists(name string) bool {
// Split the name into package and type
parts := strings.Split(name, ".")
typ := parts[0]
pkg := "main"
if len(parts) == 2 {
pkg = parts[0]
typ = parts[1]
}
knownPkg, ok := k.known[pkg]
if !ok {
return false
}
_, ok = knownPkg[typ]
return ok
}

View File

@ -0,0 +1,144 @@
package parser
import (
"testing"
"github.com/samber/lo"
"github.com/stretchr/testify/require"
)
func TestParseDirectory(t *testing.T) {
tests := []struct {
name string
dir string
want []string
wantErr bool
}{
{
name: "should find single bound service",
dir: "testdata/struct_literal_single",
want: []string{"main.GreetService"},
wantErr: false,
},
{
name: "should find multiple bound services",
dir: "testdata/struct_literal_multiple",
want: []string{"main.GreetService", "main.OtherService"},
wantErr: false,
},
{
name: "should find multiple bound services over multiple files",
dir: "testdata/struct_literal_multiple_files",
want: []string{"main.GreetService", "main.OtherService"},
wantErr: false,
},
{
name: "should find bound services from other packages",
dir: "../../examples/binding",
want: []string{"main.localStruct", "services.GreetService", "models.Person"},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Debug = true
got, err := ParseDirectory(tt.dir)
if (err != nil) != tt.wantErr {
t.Errorf("ParseDirectory() error = %v, wantErr %v", err, tt.wantErr)
return
}
for name, pkg := range got.packages {
for structName := range pkg.boundStructs {
require.True(t, lo.Contains(tt.want, name+"."+structName))
tt.want = lo.Without(tt.want, name+"."+structName)
}
}
require.Empty(t, tt.want)
})
}
}
func TestGenerateTypeScript(t *testing.T) {
tests := []struct {
name string
dir string
want string
wantErr bool
}{
{
name: "should find single bound service",
dir: "testdata/struct_literal_single",
want: `namespace main {
class GreetService {
SomeVariable: number;
}
}
`,
wantErr: false,
},
{
name: "should find multiple bound services",
dir: "testdata/struct_literal_multiple",
want: `namespace main {
class GreetService {
SomeVariable: number;
}
class OtherService {
}
}
`,
wantErr: false,
},
{
name: "should find multiple bound services over multiple files",
dir: "testdata/struct_literal_multiple_files",
want: `namespace main {
class GreetService {
SomeVariable: number;
}
class OtherService {
}
}
`,
wantErr: false,
},
{
name: "should find bound services from other packages",
dir: "../../examples/binding",
want: `namespace main {
class localStruct {
}
}
namespace models {
class Person {
Name: string;
}
}
namespace services {
class GreetService {
SomeVariable: number;
Parent: models.Person;
}
}
`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Debug = true
context, err := ParseDirectory(tt.dir)
if (err != nil) != tt.wantErr {
t.Errorf("ParseDirectory() error = %v, wantErr %v", err, tt.wantErr)
return
}
ts, err := GenerateModels(context)
require.NoError(t, err)
require.Equal(t, tt.want, string(ts))
})
}
}

View File

@ -0,0 +1,40 @@
package main
import (
_ "embed"
"log"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/options"
)
type GreetService struct {
SomeVariable int
lowerCase string
}
func (*GreetService) Greet(name string) string {
return "Hello " + name
}
type OtherService struct {
t int
}
func main() {
app := application.New(options.Application{
Bind: []interface{}{
&GreetService{},
&OtherService{},
},
})
app.NewWebviewWindow()
err := app.Run()
if err != nil {
log.Fatal(err)
}
}

View File

@ -0,0 +1,14 @@
package main
import (
_ "embed"
)
type GreetService struct {
SomeVariable int
lowerCase string
}
func (*GreetService) Greet(name string) string {
return "Hello " + name
}

View File

@ -0,0 +1,27 @@
package main
import (
_ "embed"
"log"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/options"
)
func main() {
app := application.New(options.Application{
Bind: []interface{}{
&GreetService{},
&OtherService{},
},
})
app.NewWebviewWindow()
err := app.Run()
if err != nil {
log.Fatal(err)
}
}

View File

@ -0,0 +1,5 @@
package main
type OtherService struct {
t int
}

View File

@ -0,0 +1,35 @@
package main
import (
_ "embed"
"log"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/options"
)
type GreetService struct {
SomeVariable int
lowerCase string
}
func (*GreetService) Greet(name string) string {
return "Hello " + name
}
func main() {
app := application.New(options.Application{
Bind: []interface{}{
&GreetService{},
},
})
app.NewWebviewWindow()
err := app.Run()
if err != nil {
log.Fatal(err)
}
}

View File

@ -0,0 +1,26 @@
//go:build production
package runtime
var RuntimeAssetsBundle = &RuntimeAssets{
desktopIPC: DesktopIPC,
runtimeDesktopJS: DesktopRuntime,
}
type RuntimeAssets struct {
desktopIPC []byte
websocketIPC []byte
runtimeDesktopJS []byte
}
func (r *RuntimeAssets) DesktopIPC() []byte {
return r.desktopIPC
}
func (r *RuntimeAssets) WebsocketIPC() []byte {
return r.websocketIPC
}
func (r *RuntimeAssets) RuntimeDesktopJS() []byte {
return r.runtimeDesktopJS
}

View File

@ -0,0 +1,27 @@
//go:build !production
package runtime
var RuntimeAssetsBundle = &RuntimeAssets{
desktopIPC: DesktopIPC,
websocketIPC: WebsocketIPC,
runtimeDesktopJS: DesktopRuntime,
}
type RuntimeAssets struct {
desktopIPC []byte
websocketIPC []byte
runtimeDesktopJS []byte
}
func (r *RuntimeAssets) DesktopIPC() []byte {
return r.desktopIPC
}
func (r *RuntimeAssets) WebsocketIPC() []byte {
return r.websocketIPC
}
func (r *RuntimeAssets) RuntimeDesktopJS() []byte {
return r.runtimeDesktopJS
}

View File

@ -0,0 +1,67 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
import {Call} from './calls';
// This is where we bind go method wrappers
window.go = {};
export function SetBindings(bindingsMap) {
try {
bindingsMap = JSON.parse(bindingsMap);
} catch (e) {
console.error(e);
}
// Initialise the bindings map
window.go = window.go || {};
// Iterate package names
Object.keys(bindingsMap).forEach((packageName) => {
// Create inner map if it doesn't exist
window.go[packageName] = window.go[packageName] || {};
// Iterate struct names
Object.keys(bindingsMap[packageName]).forEach((structName) => {
// Create inner map if it doesn't exist
window.go[packageName][structName] = window.go[packageName][structName] || {};
Object.keys(bindingsMap[packageName][structName]).forEach((methodName) => {
window.go[packageName][structName][methodName] = function () {
// No timeout by default
let timeout = 0;
// Actual function
function dynamic() {
const 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;
}();
});
});
});
}

View File

@ -0,0 +1,8 @@
/**
* @description: Use the system default browser to open the url
* @param {string} url
* @return {void}
*/
export function BrowserOpenURL(url) {
window.WailsInvoke('BO:' + url);
}

View File

@ -0,0 +1,189 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
export const 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 {any=} args
* @param {number=} timeout
* @returns
*/
export function Call(name, args, timeout) {
// Timeout infinite by default
if (timeout == null) {
timeout = 0;
}
let windowID = window.wails.window.ID();
// Create a promise
return new Promise(function (resolve, reject) {
// Create a unique callbackID
var callbackID;
do {
callbackID = name + '-' + randomFunc();
} while (callbacks[callbackID]);
var timeoutHandle;
// Set timeout
if (timeout > 0) {
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,
windowID,
};
// Make the call
window.WailsInvoke('C' + JSON.stringify(payload));
} catch (e) {
// eslint-disable-next-line
console.error(e);
}
});
}
window.ObfuscatedCall = (id, args, timeout) => {
// Timeout infinite by default
if (timeout == null) {
timeout = 0;
}
// Create a promise
return new Promise(function (resolve, reject) {
// Create a unique callbackID
var callbackID;
do {
callbackID = id + '-' + randomFunc();
} while (callbacks[callbackID]);
var timeoutHandle;
// Set timeout
if (timeout > 0) {
timeoutHandle = setTimeout(function () {
reject(Error('Call to method ' + id + ' timed out. Request ID: ' + callbackID));
}, timeout);
}
// Store callback
callbacks[callbackID] = {
timeoutHandle: timeoutHandle,
reject: reject,
resolve: resolve
};
try {
const payload = {
id,
args,
callbackID,
windowID: window.wails.window.ID(),
};
// Make the call
window.WailsInvoke('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) {
// Parse the message
let message;
try {
message = JSON.parse(incomingMessage);
} catch (e) {
const error = `Invalid JSON passed to callback: ${e.message}. Message: ${incomingMessage}`;
runtime.LogDebug(error);
throw new Error(error);
}
let callbackID = message.callbackid;
let 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);
}
}

View File

@ -0,0 +1,212 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
// 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 {string} eventName
* @param {function} callback
* @param {number} maxCallbacks
* @memberof Listener
*/
constructor(eventName, callback, maxCallbacks) {
this.eventName = eventName;
// Default of -1 means infinite
this.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 (this.maxCallbacks === -1) {
return false;
}
// Decrement maxCallbacks. Return true if now 0, otherwise false
this.maxCallbacks -= 1;
return this.maxCallbacks === 0;
};
}
}
export const eventListeners = {};
/**
* Registers an event listener that will be invoked `maxCallbacks` times before being destroyed
*
* @export
* @param {string} eventName
* @param {function} callback
* @param {number} maxCallbacks
* @returns {function} A function to cancel the listener
*/
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
eventListeners[eventName] = eventListeners[eventName] || [];
const thisListener = new Listener(eventName, callback, maxCallbacks);
eventListeners[eventName].push(thisListener);
return () => listenerOff(thisListener);
}
/**
* Registers an event listener that will be invoked every time the event is emitted
*
* @export
* @param {string} eventName
* @param {function} callback
* @returns {function} A function to cancel the listener
*/
export function EventsOn(eventName, callback) {
return EventsOnMultiple(eventName, callback, -1);
}
/**
* Registers an event listener that will be invoked once then destroyed
*
* @export
* @param {string} eventName
* @param {function} callback
* @returns {function} A function to cancel the listener
*/
export function EventsOnce(eventName, callback) {
return EventsOnMultiple(eventName, callback, 1);
}
function notifyListeners(eventData) {
// Get the event name
let eventName = eventData.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];
let data = eventData.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 listeners
if (newEventListenerList.length === 0) {
removeListener(eventName);
} else {
eventListeners[eventName] = newEventListenerList;
}
}
}
/**
* Notify informs frontend listeners that an event was emitted with the given data
*
* @export
* @param {string} notifyMessage - encoded notification message
*/
export function EventsNotify(notifyMessage) {
// Parse the message
let message;
try {
message = JSON.parse(notifyMessage);
} catch (e) {
const error = 'Invalid JSON passed to Notify: ' + notifyMessage;
throw new Error(error);
}
notifyListeners(message);
}
/**
* Emit an event with the given name and data
*
* @export
* @param {string} eventName
*/
export function EventsEmit(eventName) {
const payload = {
name: eventName,
data: [].slice.apply(arguments).slice(1),
};
// Notify JS listeners
notifyListeners(payload);
// Notify Go listeners
window.WailsInvoke('EE' + JSON.stringify(payload));
}
function removeListener(eventName) {
// Remove local listeners
delete eventListeners[eventName];
// Notify Go listeners
window.WailsInvoke('EX' + eventName);
}
/**
* Off unregisters a listener previously registered with On,
* optionally multiple listeneres can be unregistered via `additionalEventNames`
*
* @param {string} eventName
* @param {...string} additionalEventNames
*/
export function EventsOff(eventName, ...additionalEventNames) {
removeListener(eventName)
if (additionalEventNames.length > 0) {
additionalEventNames.forEach(eventName => {
removeListener(eventName)
})
}
}
/**
* Off unregisters all event listeners previously registered with On
*/
export function EventsOffAll() {
const eventNames = Object.keys(eventListeners);
for (let i = 0; i !== eventNames.length; i++) {
removeListener(eventNames[i]);
}
}
/**
* listenerOff unregisters a listener previously registered with EventsOn
*
* @param {Listener} listener
*/
function listenerOff(listener) {
const eventName = listener.eventName;
// Remove local listener
eventListeners[eventName] = eventListeners[eventName].filter(l => l !== listener);
// Clean up if there are no event listeners left
if (eventListeners[eventName].length === 0) {
removeListener(eventName);
}
}

View File

@ -0,0 +1,132 @@
import { EventsOnMultiple, EventsNotify, eventListeners, EventsOn, EventsEmit, EventsOffAll, EventsOnce, EventsOff } from './events'
import { expect, describe, it, beforeAll, vi, afterEach, beforeEach } from 'vitest'
// Edit an assertion and save to see HMR in action
beforeAll(() => {
window.WailsInvoke = vi.fn(() => {})
})
afterEach(() => {
EventsOffAll();
vi.resetAllMocks()
})
describe('EventsOnMultiple', () => {
it('should stop after a specified number of times', () => {
const cb = vi.fn()
EventsOnMultiple('a', cb, 5)
EventsNotify(JSON.stringify({name: 'a', data: {}}))
EventsNotify(JSON.stringify({name: 'a', data: {}}))
EventsNotify(JSON.stringify({name: 'a', data: {}}))
EventsNotify(JSON.stringify({name: 'a', data: {}}))
EventsNotify(JSON.stringify({name: 'a', data: {}}))
EventsNotify(JSON.stringify({name: 'a', data: {}}))
expect(cb).toBeCalledTimes(5);
expect(window.WailsInvoke).toBeCalledTimes(1);
expect(window.WailsInvoke).toHaveBeenLastCalledWith('EXa');
})
it('should return a cancel fn', () => {
const cb = vi.fn()
const cancel = EventsOnMultiple('a', cb, 5)
EventsNotify(JSON.stringify({name: 'a', data: {}}))
EventsNotify(JSON.stringify({name: 'a', data: {}}))
cancel()
EventsNotify(JSON.stringify({name: 'a', data: {}}))
EventsNotify(JSON.stringify({name: 'a', data: {}}))
expect(cb).toBeCalledTimes(2)
expect(window.WailsInvoke).toBeCalledTimes(1);
expect(window.WailsInvoke).toHaveBeenLastCalledWith('EXa');
})
})
describe('EventsOn', () => {
it('should create a listener with a count of -1', () => {
EventsOn('a', () => {})
expect(eventListeners['a'][0].maxCallbacks).toBe(-1)
})
it('should return a cancel fn', () => {
const cancel = EventsOn('a', () => {})
cancel();
expect(window.WailsInvoke).toBeCalledTimes(1);
expect(window.WailsInvoke).toHaveBeenLastCalledWith('EXa');
})
})
describe('EventsOnce', () => {
it('should create a listener with a count of 1', () => {
EventsOnce('a', () => {})
expect(eventListeners['a'][0].maxCallbacks).toBe(1)
})
it('should return a cancel fn', () => {
const cancel = EventsOn('a', () => {})
cancel();
expect(window.WailsInvoke).toBeCalledTimes(1);
expect(window.WailsInvoke).toHaveBeenLastCalledWith('EXa');
})
})
describe('EventsNotify', () => {
it('should inform a listener', () => {
const cb = vi.fn()
EventsOn('a', cb)
EventsNotify(JSON.stringify({name: 'a', data: ["one", "two", "three"]}))
expect(cb).toBeCalledTimes(1);
expect(cb).toHaveBeenLastCalledWith("one", "two", "three");
expect(window.WailsInvoke).toBeCalledTimes(0);
})
})
describe('EventsEmit', () => {
it('should emit an event', () => {
EventsEmit('a', 'one', 'two', 'three')
expect(window.WailsInvoke).toBeCalledTimes(1);
const calledWith = window.WailsInvoke.calls[0][0];
expect(calledWith.slice(0, 2)).toBe('EE')
expect(JSON.parse(calledWith.slice(2))).toStrictEqual({data: ["one", "two", "three"], name: "a"})
})
})
describe('EventsOff', () => {
beforeEach(() => {
EventsOn('a', () => {})
EventsOn('a', () => {})
EventsOn('a', () => {})
EventsOn('b', () => {})
EventsOn('c', () => {})
})
it('should cancel all event listeners for a single type', () => {
EventsOff('a')
expect(eventListeners['a']).toBeUndefined()
expect(eventListeners['b']).not.toBeUndefined()
expect(eventListeners['c']).not.toBeUndefined()
expect(window.WailsInvoke).toBeCalledTimes(1);
expect(window.WailsInvoke).toHaveBeenLastCalledWith('EXa');
})
it('should cancel all event listeners for multiple types', () => {
EventsOff('a', 'b')
expect(eventListeners['a']).toBeUndefined()
expect(eventListeners['b']).toBeUndefined()
expect(eventListeners['c']).not.toBeUndefined()
expect(window.WailsInvoke).toBeCalledTimes(2);
expect(window.WailsInvoke.calls).toStrictEqual([['EXa'], ['EXb']]);
})
})
describe('EventsOffAll', () => {
it('should cancel all event listeners', () => {
EventsOn('a', () => {})
EventsOn('a', () => {})
EventsOn('a', () => {})
EventsOn('b', () => {})
EventsOn('c', () => {})
EventsOffAll()
expect(eventListeners).toStrictEqual({})
expect(window.WailsInvoke).toBeCalledTimes(3);
expect(window.WailsInvoke.calls).toStrictEqual([['EXa'], ['EXb'], ['EXc']]);
})
})

View File

@ -0,0 +1,23 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
/**
* WailsInvoke sends the given message to the backend
*
* @param {string} message
*/
(function () {
window.WailsInvoke = (message) => {
WINDOWS && window.chrome.webview.postMessage(message);
(DARWIN || LINUX) && window.webkit.messageHandlers.wails.postMessage(message);
}
})();

View File

@ -0,0 +1,113 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
/**
* 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]
window.WailsInvoke('L' + level + message);
}
/**
* Log the given trace message with the backend
*
* @export
* @param {string} message
*/
export function LogTrace(message) {
sendLogMessage('T', message);
}
/**
* Log the given message with the backend
*
* @export
* @param {string} message
*/
export function LogPrint(message) {
sendLogMessage('P', message);
}
/**
* Log the given debug message with the backend
*
* @export
* @param {string} message
*/
export function LogDebug(message) {
sendLogMessage('D', message);
}
/**
* Log the given info message with the backend
*
* @export
* @param {string} message
*/
export function LogInfo(message) {
sendLogMessage('I', message);
}
/**
* Log the given warning message with the backend
*
* @export
* @param {string} message
*/
export function LogWarning(message) {
sendLogMessage('W', message);
}
/**
* Log the given error message with the backend
*
* @export
* @param {string} message
*/
export function LogError(message) {
sendLogMessage('E', message);
}
/**
* Log the given fatal message with the backend
*
* @export
* @param {string} message
*/
export function LogFatal(message) {
sendLogMessage('F', message);
}
/**
* Sets the Log level to the given log level
*
* @export
* @param {number} loglevel
*/
export function SetLogLevel(loglevel) {
sendLogMessage('S', loglevel);
}
// Log levels
export const LogLevel = {
TRACE: 1,
DEBUG: 2,
INFO: 3,
WARNING: 4,
ERROR: 5,
};

View File

@ -0,0 +1,80 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 9 */
import "./ipc.js";
import {Callback, callbacks} from './calls';
import {EventsNotify, eventListeners} from "./events";
import {SetBindings} from "./bindings";
import * as Window from "./window";
import * as Screen from "./screen";
import * as Browser from "./browser";
import * as Log from './log';
let windowID = -1;
export function Quit() {
window.WailsInvoke('Q');
}
export function Show() {
window.WailsInvoke('S');
}
export function Hide() {
window.WailsInvoke('H');
}
// export function Environment() {
// return Call(":wails:Environment");
// }
// Internal wails endpoints
window.wails = {
Callback,
callbacks,
EventsNotify,
eventListeners,
SetBindings,
window: {
ID: () => {
return windowID
},
}
};
window.runtime = {
...Log,
...Window,
...Browser,
...Screen,
EventsOn,
EventsOnce,
EventsOnMultiple,
EventsEmit,
EventsOff,
// Environment,
Show,
Hide,
Quit,
}
// Process the expected runtime config from the backend
if( window.wails_config ) {
windowID = window.wails_config.windowID;
window.wails_config = null;
}
if (DEBUG) {
console.log("Wails v3.0.0 Debug Mode Enabled");
}

View File

@ -0,0 +1,25 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 9 */
import {Call} from "./calls";
/**
* Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
* @export
* @typedef {import('../wrapper/runtime').Screen} Screen
* @return {Promise<{Screen[]}>} The screens
*/
export function ScreenGetAll() {
return Call(":wails:ScreenGetAll");
}

View File

@ -0,0 +1,269 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 9 */
import {Call} from "./calls";
export function WindowReload() {
window.location.reload();
}
export function WindowReloadApp() {
window.WailsInvoke('WR');
}
export function WindowSetSystemDefaultTheme() {
window.WailsInvoke('WASDT');
}
export function WindowSetLightTheme() {
window.WailsInvoke('WALT');
}
export function WindowSetDarkTheme() {
window.WailsInvoke('WADT');
}
/**
* Place the window in the center of the screen
*
* @export
*/
export function WindowCenter() {
window.WailsInvoke('Wc');
}
/**
* Sets the window title
*
* @param {string} title
* @export
*/
export function WindowSetTitle(title) {
window.WailsInvoke('WT' + title);
}
/**
* Makes the window go fullscreen
*
* @export
*/
export function WindowFullscreen() {
window.WailsInvoke('WF');
}
/**
* Reverts the window from fullscreen
*
* @export
*/
export function WindowUnfullscreen() {
window.WailsInvoke('Wf');
}
/**
* Returns the state of the window, i.e. whether the window is in full screen mode or not.
*
* @export
* @return {Promise<boolean>} The state of the window
*/
export function WindowIsFullscreen() {
return Call(":wails:WindowIsFullscreen");
}
/**
* Set the Size of the window
*
* @export
* @param {number} width
* @param {number} height
*/
export function WindowSetSize(width, height) {
window.WailsInvoke('Ws:' + width + ':' + height);
}
/**
* Get the Size of the window
*
* @export
* @return {Promise<{w: number, h: number}>} The size of the window
*/
export function WindowGetSize() {
return Call(":wails:WindowGetSize");
}
/**
* Set the maximum size of the window
*
* @export
* @param {number} width
* @param {number} height
*/
export function WindowSetMaxSize(width, height) {
window.WailsInvoke('WZ:' + width + ':' + height);
}
/**
* Set the minimum size of the window
*
* @export
* @param {number} width
* @param {number} height
*/
export function WindowSetMinSize(width, height) {
window.WailsInvoke('Wz:' + width + ':' + height);
}
/**
* Set the window AlwaysOnTop or not on top
*
* @export
*/
export function WindowSetAlwaysOnTop(b) {
window.WailsInvoke('WATP:' + (b ? '1' : '0'));
}
/**
* Set the Position of the window
*
* @export
* @param {number} x
* @param {number} y
*/
export function WindowSetPosition(x, y) {
window.WailsInvoke('Wp:' + x + ':' + y);
}
/**
* Get the Position of the window
*
* @export
* @return {Promise<{x: number, y: number}>} The position of the window
*/
export function WindowGetPosition() {
return Call(":wails:WindowGetPos");
}
/**
* Hide the Window
*
* @export
*/
export function WindowHide() {
window.WailsInvoke('WH');
}
/**
* Show the Window
*
* @export
*/
export function WindowShow() {
window.WailsInvoke('WS');
}
/**
* Maximise the Window
*
* @export
*/
export function WindowMaximise() {
window.WailsInvoke('WM');
}
/**
* Toggle the Maximise of the Window
*
* @export
*/
export function WindowToggleMaximise() {
window.WailsInvoke('Wt');
}
/**
* Unmaximise the Window
*
* @export
*/
export function WindowUnmaximise() {
window.WailsInvoke('WU');
}
/**
* Returns the state of the window, i.e. whether the window is maximised or not.
*
* @export
* @return {Promise<boolean>} The state of the window
*/
export function WindowIsMaximised() {
return Call(":wails:WindowIsMaximised");
}
/**
* Minimise the Window
*
* @export
*/
export function WindowMinimise() {
window.WailsInvoke('Wm');
}
/**
* Unminimise the Window
*
* @export
*/
export function WindowUnminimise() {
window.WailsInvoke('Wu');
}
/**
* Returns the state of the window, i.e. whether the window is minimised or not.
*
* @export
* @return {Promise<boolean>} The state of the window
*/
export function WindowIsMinimised() {
return Call(":wails:WindowIsMinimised");
}
/**
* Returns the state of the window, i.e. whether the window is normal or not.
*
* @export
* @return {Promise<boolean>} The state of the window
*/
export function WindowIsNormal() {
return Call(":wails:WindowIsNormal");
}
/**
* Sets the background colour of the window
*
* @export
* @param {number} R Red
* @param {number} G Green
* @param {number} B Blue
* @param {number} A Alpha
*/
export function WindowSetBackgroundColour(R, G, B, A) {
let rgba = JSON.stringify({r: R || 0, g: G || 0, b: B || 0, a: A || 255});
window.WailsInvoke('Wr:' + rgba);
}

View File

@ -0,0 +1,54 @@
<script>
import {overlayVisible} from './store'
import {fade,} from 'svelte/transition';
</script>
{#if $overlayVisible }
<div class="wails-reconnect-overlay" transition:fade="{{ duration: 300 }}">
<div class="wails-reconnect-overlay-content">
<div class="wails-reconnect-overlay-loadingspinner"></div>
</div>
</div>
{/if}
<style>
.wails-reconnect-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
backdrop-filter: blur(2px) saturate(0%) contrast(50%) brightness(25%);
z-index: 999999
}
.wails-reconnect-overlay-content {
position: relative;
top: 50%;
transform: translateY(-50%);
margin: 0;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAA7CAMAAAAEsocZAAAC91BMVEUAAACzQ0PjMjLkMjLZLS7XLS+vJCjkMjKlEx6uGyHjMDGiFx7GJyrAISjUKy3mMzPlMjLjMzOsGyDKJirkMjK6HyXmMjLgMDC6IiLcMjLULC3MJyrRKSy+IibmMzPmMjK7ISXlMjLIJimzHSLkMjKtGiHZLC7BIifgMDCpGSDFIivcLy+yHSKoGR+eFBzNKCvlMjKxHSPkMTKxHSLmMjLKJyq5ICXDJCe6ISXdLzDkMjLmMzPFJSm2HyTlMTLhMDGyHSKUEBmhFx24HyTCJCjHJijjMzOiFh7mMjJ6BhDaLDCuGyOKABjnMzPGJinJJiquHCGEChSmGB/pMzOiFh7VKy3OKCu1HiSvHCLjMTLMKCrBIyeICxWxHCLDIyjSKizBIyh+CBO9ISa6ISWDChS9Iie1HyXVLC7FJSrLKCrlMjLiMTGPDhicFRywGyKXFBuhFx1/BxO7IiXkMTGeFBx8BxLkMTGnGR/GJCi4ICWsGyGJDxXSLS2yGiHSKi3CJCfnMzPQKiyECRTKJiq6ISWUERq/Iye0HiPDJCjGJSm6ICaPDxiTEBrdLy+3HyXSKiy0HyOQEBi4ICWhFh1+CBO9IieODhfSKyzWLC2LDhh8BxHKKCq7ISWaFBzkMzPqNDTTLC3EJSiHDBacExyvGyO1HyTPKCy+IieoGSC7ISaVEhrMKCvQKyusGyG0HiKACBPIJSq/JCaABxR5BRLEJCnkMzPJJinEJimPDRZ2BRKqHx/jMjLnMzPgMDHULC3NKSvQKSzsNDTWLS7SKyy3HyTKJyrDJSjbLzDYLC6mGB/GJSnVLC61HiPLKCrHJSm/Iye8Iia6ICWzHSKxHCLaLi/PKSupGR+7ICXpMzPbLi/IJinJJSmsGyGrGiCkFx6PDheJCxaFChXBIyfAIieSDxmBCBPlMjLeLzDdLzC5HySMDRe+ISWvGyGcFBzSKSzPJyvMJyrEJCjDIyefFRyWERriMDHUKiy/ISaZExv0NjbwNTXuNDTrMzMI0c+yAAAAu3RSTlMAA8HR/gwGgAj+MEpGCsC+hGpjQjYnIxgWBfzx7urizMrFqqB1bF83KhsR/fz8+/r5+fXv7unZ1tC+t6mmopqKdW1nYVpVRjUeHhIQBPr59/b28/Hx8ODg3NvUw8O/vKeim5aNioiDgn1vZWNjX1xUU1JPTUVFPT08Mi4qJyIh/Pv7+/n4+Pf39fT08/Du7efn5uXj4uHa19XNwsG/vrq2tbSuramlnpyYkpGNiIZ+enRraGVjVVBKOzghdjzRsAAABJVJREFUWMPtllVQG1EYhTc0ASpoobS0FCulUHd3oUjd3d3d3d3d3d2b7CYhnkBCCHGDEIK7Vh56d0NpOgwkYfLQzvA9ZrLfnPvfc+8uVEst/yheBJup3Nya2MjU6pa/jWLZtxjXpZFtVB4uVNI6m5gIruNkVFebqIb5Ug2ym4TIEM/gtUOGbg613oBzjAzZFrZ+lXu/3TIiMXXS5M6HTvrNHeLpZLEh6suGNW9fzZ9zd/qVi2eOHygqi5cDE5GUrJocONgzyqo0UXNSUlKSEhMztFqtXq9vNxImAmS3g7Y6QlbjdBWVGW36jt4wDGTUXjUsafh5zJWRkdFuZGtWGnCRmg+HasiGMUClTTzW0ZuVgLlGDIPM4Lhi0IrVq+tv2hS21fNrSONQgpM9DsJ4t3fM9PkvJuKj2ZjrZwvILKvaSTgciUSirjt6dOfOpyd169bDb9rMOwF9Hj4OD100gY0YXYb299bjzMrqj9doNByJWlVXFB9DT5dmJuvy+cq83JyuS6ayEYSHulKL8dmFnBkrCeZlHKMrC5XRhXGCZB2Ty1fkleRQaMCFT2DBsEafzRFJu7/2MicbKynPhQUDLiZwMWLJZKNLzoLbJBYVcurSmbmn+rcyJ8vCMgmlmaW6gnwun/+3C96VpAUuET1ZgRR36r2xWlnYSnf3oKABA14uXDDvydxHs6cpTV1p3hlJ2rJCiUjIZCByItXg8sHJijuvT64CuMTABUYvb6NN1Jdp1PH7D7f3bo2eS5KvW4RJr7atWT5w4MBBg9zdBw9+37BS7QIoFS5WnIaj12dr1DEXFgdvr4fh4eFl+u/wz8uf3jjHic8s4DL2Dal0IANyUBeCRCcwOBJV26JsjSpGwHVuSai69jvqD+jr56OgtKy0zAAK5mLTVBKVKL5tNthGAR9JneJQ/bFsHNzy+U7IlCYROxtMpIjR0ceoQVnowracLLpAQWETqV361bPoFo3cEbz2zYLZM7t3HWXcxmiBOgttS1ycWkTXMWh4mGigdug9DFdttqCFgTN6nD0q1XEVSoCxEjyFCi2eNC6Z69MRVIImJ6JQSf5gcFVCuF+aDhCa1F6MJFDaiNBQAh2TMfWBjhmLsAxUjG/fmjs0qjJck8D0GPBcuUuZW1LS/tIsPzqmQt17PvZQknlwnf4tHDBc+7t5VV3QQCkdc+Ur8/hdrz0but0RCumWiYbiKmLJ7EVbRomj4Q7+y5wsaXvfTGFpQcHB7n2WbG4MGdniw2Tm8xl5Yhr7MrSYHQ3uampz10aWyHyuzxvqaW/6W4MjXAUD3QV2aw97ZxhGjxCohYf5TpTHMXU1BbsAuoFnkRygVieIGAbqiF7rrH4rfWpKJouBCtyHJF8ctEyGubBa+C6NsMYEUonJFITHZqWBxXUA12Dv76Tf/PgOBmeNiiLG1pcKo1HAq8jLpY4JU1yWEixVNaOgoRJAKBSZHTZTU+wJOMtUDZvlVITC6FTlksyrEBoPHXpxxbzdaqzigUtVDkJVIOtVQ9UEOR4VGUh/kHWq0edJ6CxnZ+eePXva2bnY/cF/I1RLLf8vvwDANdMSMegxcAAAAABJRU5ErkJggg==);
background-repeat: no-repeat;
background-position: center
}
.wails-reconnect-overlay-loadingspinner {
pointer-events: none;
width: 2.5em;
height: 2.5em;
border: .4em solid transparent;
border-color: #f00 #eee0 #f00 #eee0;
border-radius: 50%;
animation: loadingspin 1s linear infinite;
margin: auto;
padding: 2.5em
}
@keyframes loadingspin {
100% {
transform: rotate(360deg);
}
}
</style>

View File

@ -0,0 +1,15 @@
/* jshint esversion: 8 */
const esbuild = require("esbuild");
const sveltePlugin = require("esbuild-svelte");
esbuild
.build({
entryPoints: ["main.js"],
bundle: true,
minify: true,
outfile: "../ipc_websocket.js",
plugins: [sveltePlugin({compileOptions: {css: true}})],
logLevel: "info",
sourcemap: "inline",
})
.catch(() => process.exit(1));

View File

@ -0,0 +1,8 @@
export function log(message) {
// eslint-disable-next-line
console.log(
'%c wails dev %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'
);
}

View File

@ -0,0 +1,125 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
import {log} from "./log";
import Overlay from "./Overlay.svelte";
import {hideOverlay, showOverlay} from "./store";
let components = {};
let wailsInvokeInternal = null;
let messageQueue = [];
window.WailsInvoke = (message) => {
if (!wailsInvokeInternal) {
console.log("Queueing: " + message);
messageQueue.push(message);
return;
}
wailsInvokeInternal(message);
};
window.addEventListener('DOMContentLoaded', () => {
components.overlay = new Overlay({
target: document.body,
anchor: document.querySelector('#wails-spinner'),
});
});
let websocket = null;
let connectTimer;
window.onbeforeunload = function () {
if (websocket) {
websocket.onclose = function () {
};
websocket.close();
websocket = null;
}
};
// ...and attempt to connect
connect();
function setupIPCBridge() {
wailsInvokeInternal = (message) => {
websocket.send(message);
};
for (let i = 0; i < messageQueue.length; i++) {
console.log("sending queued message: " + messageQueue[i]);
window.WailsInvoke(messageQueue[i]);
}
messageQueue = [];
}
// Handles incoming websocket connections
function handleConnect() {
log('Connected to backend');
hideOverlay();
setupIPCBridge();
clearInterval(connectTimer);
websocket.onclose = handleDisconnect;
websocket.onmessage = handleMessage;
}
// Handles websocket disconnects
function handleDisconnect() {
log('Disconnected from backend');
websocket = null;
showOverlay();
connect();
}
function _connect() {
if (websocket == null) {
websocket = new WebSocket('ws://' + window.location.host + '/wails/ipc');
websocket.onopen = handleConnect;
websocket.onerror = function (e) {
e.stopImmediatePropagation();
e.stopPropagation();
e.preventDefault();
websocket = null;
return false;
};
}
}
// Try to connect to the backend every .5s
function connect() {
_connect();
connectTimer = setInterval(_connect, 500);
}
function handleMessage(message) {
if (message.data === "reload") {
window.runtime.WindowReload();
return;
}
if (message.data === "reloadapp") {
window.runtime.WindowReloadApp()
return;
}
// As a bridge we ignore js and css injections
switch (message.data[0]) {
// Notifications
case 'n':
window.wails.EventsNotify(message.data.slice(1));
break;
case 'c':
const callbackData = message.data.slice(1);
window.wails.Callback(callbackData);
break;
default:
log('Unknown message: ' + message.data);
}
}

1536
v3/internal/runtime/dev/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
{
"name": "dev",
"version": "3.0.0",
"description": "Wails JS Dev",
"main": "main.js",
"scripts": {
"build": "run-p build:*",
"build:dev": "node build.js"
},
"author": "Lea Anthony <lea.anthony@gmail.com>",
"license": "ISC",
"devDependencies": {
"esbuild": "^0.12.17",
"esbuild-svelte": "^0.5.6",
"npm-run-all": "^4.1.5",
"svelte": "^3.49.0"
}
}

View File

@ -0,0 +1,12 @@
import {writable} from 'svelte/store';
/** Overlay */
export const overlayVisible = writable(false);
export function showOverlay() {
overlayVisible.set(true);
}
export function hideOverlay() {
overlayVisible.set(false);
}

View File

@ -0,0 +1,9 @@
package runtime
import _ "embed"
//go:embed ipc_websocket.js
var WebsocketIPC []byte
//go:embed ipc.js
var DesktopIPC []byte

View File

@ -0,0 +1 @@
(()=>{(function(){window.WailsInvoke=e=>{WINDOWS&&window.chrome.webview.postMessage(e),(DARWIN||LINUX)&&window.webkit.messageHandlers.wails.postMessage(e)}})();})();

File diff suppressed because one or more lines are too long

1503
v3/internal/runtime/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
{
"name": "runtime",
"version": "3.0.0",
"description": "Wails JS Runtime",
"main": "index.js",
"scripts": {},
"author": "Lea Anthony <lea.anthony@gmail.com>",
"license": "ISC",
"devDependencies": {
"esbuild": "^0.15.6",
"happy-dom": "^8.1.3",
"vitest": "^0.24.3"
}
}

View File

@ -0,0 +1,8 @@
//go:build darwin && !production
package runtime
import _ "embed"
//go:embed runtime_debug_desktop_darwin.js
var DesktopRuntime []byte

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
//go:build linux && !production
package runtime
import _ "embed"
//go:embed runtime_debug_desktop_linux.js
var DesktopRuntime []byte

View File

@ -0,0 +1,8 @@
//go:build windows && !production
package runtime
import _ "embed"
//go:embed runtime_debug_desktop_windows.js
var DesktopRuntime []byte

View File

@ -0,0 +1,8 @@
//go:build darwin && production
package runtime
import _ "embed"
//go:embed runtime_production_desktop_darwin.js
var DesktopRuntime []byte

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
//go:build linux && production
package runtime
import _ "embed"
//go:embed runtime_production_desktop_linux.js
var DesktopRuntime []byte

View File

@ -0,0 +1,8 @@
//go:build windows && production
package runtime
import _ "embed"
//go:embed runtime_production_desktop_windows.js
var DesktopRuntime []byte

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'happy-dom',
},
})

View File

@ -0,0 +1,10 @@
Features
- [ ] AssetServer
- [ ] Offline page if navigating to external URL
- [x] Application menu
Bugs
- [ ] Resize Window
- [ ] Fullscreen/Maximise/Minimise/Restore

View File

@ -0,0 +1,12 @@
//go:build darwin
#ifndef appdelegate_h
#define appdelegate_h
#import <Cocoa/Cocoa.h>
@interface AppDelegate : NSObject <NSApplicationDelegate>
@property bool shouldTerminateWhenLastWindowClosed;
@end
#endif

View File

@ -0,0 +1,142 @@
//go:build darwin
#import "app_delegate.h"
#import "../events/events.h"
extern bool hasListeners(unsigned int);
@implementation AppDelegate
- (void)dealloc
{
[super dealloc];
}
// Create the applicationShouldTerminateAfterLastWindowClosed: method
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication
{
return self.shouldTerminateWhenLastWindowClosed;
}
// GENERATED EVENTS START
- (void)applicationDidBecomeActive:(NSNotification *)notification {
if( hasListeners(EventApplicationDidBecomeActive) ) {
processApplicationEvent(EventApplicationDidBecomeActive);
}
}
- (void)applicationDidChangeBackingProperties:(NSNotification *)notification {
if( hasListeners(EventApplicationDidChangeBackingProperties) ) {
processApplicationEvent(EventApplicationDidChangeBackingProperties);
}
}
- (void)applicationDidChangeEffectiveAppearance:(NSNotification *)notification {
if( hasListeners(EventApplicationDidChangeEffectiveAppearance) ) {
processApplicationEvent(EventApplicationDidChangeEffectiveAppearance);
}
}
- (void)applicationDidChangeIcon:(NSNotification *)notification {
if( hasListeners(EventApplicationDidChangeIcon) ) {
processApplicationEvent(EventApplicationDidChangeIcon);
}
}
- (void)applicationDidChangeOcclusionState:(NSNotification *)notification {
if( hasListeners(EventApplicationDidChangeOcclusionState) ) {
processApplicationEvent(EventApplicationDidChangeOcclusionState);
}
}
- (void)applicationDidChangeScreenParameters:(NSNotification *)notification {
if( hasListeners(EventApplicationDidChangeScreenParameters) ) {
processApplicationEvent(EventApplicationDidChangeScreenParameters);
}
}
- (void)applicationDidChangeStatusBarFrame:(NSNotification *)notification {
if( hasListeners(EventApplicationDidChangeStatusBarFrame) ) {
processApplicationEvent(EventApplicationDidChangeStatusBarFrame);
}
}
- (void)applicationDidChangeStatusBarOrientation:(NSNotification *)notification {
if( hasListeners(EventApplicationDidChangeStatusBarOrientation) ) {
processApplicationEvent(EventApplicationDidChangeStatusBarOrientation);
}
}
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
if( hasListeners(EventApplicationDidFinishLaunching) ) {
processApplicationEvent(EventApplicationDidFinishLaunching);
}
}
- (void)applicationDidHide:(NSNotification *)notification {
if( hasListeners(EventApplicationDidHide) ) {
processApplicationEvent(EventApplicationDidHide);
}
}
- (void)applicationDidResignActive:(NSNotification *)notification {
if( hasListeners(EventApplicationDidResignActive) ) {
processApplicationEvent(EventApplicationDidResignActive);
}
}
- (void)applicationDidUnhide:(NSNotification *)notification {
if( hasListeners(EventApplicationDidUnhide) ) {
processApplicationEvent(EventApplicationDidUnhide);
}
}
- (void)applicationDidUpdate:(NSNotification *)notification {
if( hasListeners(EventApplicationDidUpdate) ) {
processApplicationEvent(EventApplicationDidUpdate);
}
}
- (void)applicationWillBecomeActive:(NSNotification *)notification {
if( hasListeners(EventApplicationWillBecomeActive) ) {
processApplicationEvent(EventApplicationWillBecomeActive);
}
}
- (void)applicationWillFinishLaunching:(NSNotification *)notification {
if( hasListeners(EventApplicationWillFinishLaunching) ) {
processApplicationEvent(EventApplicationWillFinishLaunching);
}
}
- (void)applicationWillHide:(NSNotification *)notification {
if( hasListeners(EventApplicationWillHide) ) {
processApplicationEvent(EventApplicationWillHide);
}
}
- (void)applicationWillResignActive:(NSNotification *)notification {
if( hasListeners(EventApplicationWillResignActive) ) {
processApplicationEvent(EventApplicationWillResignActive);
}
}
- (void)applicationWillTerminate:(NSNotification *)notification {
if( hasListeners(EventApplicationWillTerminate) ) {
processApplicationEvent(EventApplicationWillTerminate);
}
}
- (void)applicationWillUnhide:(NSNotification *)notification {
if( hasListeners(EventApplicationWillUnhide) ) {
processApplicationEvent(EventApplicationWillUnhide);
}
}
- (void)applicationWillUpdate:(NSNotification *)notification {
if( hasListeners(EventApplicationWillUpdate) ) {
processApplicationEvent(EventApplicationWillUpdate);
}
}
// GENERATED EVENTS END
@end

View File

@ -0,0 +1,384 @@
package application
import "C"
import (
"log"
"runtime"
"sync"
"github.com/wailsapp/wails/v3/pkg/events"
"github.com/wailsapp/wails/v3/pkg/options"
"github.com/wailsapp/wails/v2/pkg/assetserver/webview"
)
var globalApplication *App
func init() {
runtime.LockOSThread()
}
func New(appOptions options.Application) *App {
if globalApplication != nil {
return globalApplication
}
mergeApplicationDefaults(&appOptions)
result := &App{
options: appOptions,
applicationEventListeners: make(map[uint][]func()),
systemTrays: make(map[uint]*SystemTray),
}
globalApplication = result
return result
}
func mergeApplicationDefaults(o *options.Application) {
if o.Name == "" {
o.Name = "My Wails Application"
}
if o.Description == "" {
o.Description = "An application written using Wails"
}
if o.Icon == nil {
o.Icon = DefaultApplicationIcon
}
}
type platformApp interface {
run() error
destroy()
setApplicationMenu(menu *Menu)
name() string
getCurrentWindowID() uint
showAboutDialog(name string, description string, icon []byte)
setIcon(icon []byte)
on(id uint)
dispatchOnMainThread(id uint)
}
// Messages sent from javascript get routed here
type windowMessage struct {
windowId uint
message string
}
var windowMessageBuffer = make(chan *windowMessage)
type webViewAssetRequest struct {
windowId uint
request webview.Request
}
var webviewRequests = make(chan *webViewAssetRequest)
type App struct {
options options.Application
applicationEventListeners map[uint][]func()
applicationEventListenersLock sync.RWMutex
// Windows
windows map[uint]*WebviewWindow
windowsLock sync.Mutex
windowAliases map[string]uint
windowAliasesLock sync.Mutex
// System Trays
systemTrays map[uint]*SystemTray
systemTraysLock sync.Mutex
systemTrayID uint
systemTrayIDLock sync.RWMutex
// MenuItems
menuItems map[uint]*MenuItem
menuItemsLock sync.Mutex
// Running
running bool
// platform app
impl platformApp
// The main application menu
ApplicationMenu *Menu
// About MessageDialog
clipboard *Clipboard
}
func (a *App) getSystemTrayID() uint {
a.systemTrayIDLock.Lock()
defer a.systemTrayIDLock.Unlock()
a.systemTrayID++
return a.systemTrayID
}
func (a *App) On(eventType events.ApplicationEventType, callback func()) {
eventID := uint(eventType)
a.applicationEventListenersLock.Lock()
defer a.applicationEventListenersLock.Unlock()
a.applicationEventListeners[eventID] = append(a.applicationEventListeners[eventID], callback)
if a.impl != nil {
a.impl.on(eventID)
}
}
func (a *App) NewWebviewWindow() *WebviewWindow {
return a.NewWebviewWindowWithOptions(nil)
}
func (a *App) NewWebviewWindowWithOptions(windowOptions *options.WebviewWindow) *WebviewWindow {
// Ensure we have sane defaults
if windowOptions == nil {
windowOptions = options.WindowDefaults
}
newWindow := NewWindow(windowOptions)
id := newWindow.id
if a.windows == nil {
a.windows = make(map[uint]*WebviewWindow)
}
a.windowsLock.Lock()
a.windows[id] = newWindow
a.windowsLock.Unlock()
if windowOptions.Alias != "" {
if a.windowAliases == nil {
a.windowAliases = make(map[string]uint)
}
a.windowAliasesLock.Lock()
a.windowAliases[windowOptions.Alias] = id
a.windowAliasesLock.Unlock()
}
if a.running {
newWindow.run()
}
return newWindow
}
func (a *App) NewSystemTray() *SystemTray {
id := a.getSystemTrayID()
newSystemTray := NewSystemTray(id)
a.systemTraysLock.Lock()
a.systemTrays[id] = newSystemTray
a.systemTraysLock.Unlock()
if a.running {
newSystemTray.Run()
}
return newSystemTray
}
func (a *App) Run() error {
a.impl = newPlatformApp(a)
a.running = true
go func() {
for {
event := <-applicationEvents
a.handleApplicationEvent(event)
}
}()
go func() {
for {
event := <-windowEvents
a.handleWindowEvent(event)
}
}()
go func() {
for {
event := <-webviewRequests
a.handleWebViewRequest(event)
event.request.Release()
}
}()
go func() {
for {
event := <-windowMessageBuffer
a.handleWindowMessage(event)
}
}()
go func() {
for {
menuItemID := <-menuItemClicked
a.handleMenuItemClicked(menuItemID)
}
}()
// run windows
for _, window := range a.windows {
go window.run()
}
// run system trays
for _, systray := range a.systemTrays {
go systray.Run()
}
// set the application menu
a.impl.setApplicationMenu(a.ApplicationMenu)
// set the application icon
a.impl.setIcon(a.options.Icon)
return a.impl.run()
}
func (a *App) handleApplicationEvent(event uint) {
a.applicationEventListenersLock.RLock()
listeners, ok := a.applicationEventListeners[event]
a.applicationEventListenersLock.RUnlock()
if !ok {
return
}
for _, listener := range listeners {
go listener()
}
}
func (a *App) handleWindowMessage(event *windowMessage) {
// Get window from window map
a.windowsLock.Lock()
window, ok := a.windows[event.windowId]
a.windowsLock.Unlock()
if !ok {
log.Printf("WebviewWindow #%d not found", event.windowId)
return
}
// Get callback from window
window.handleMessage(event.message)
}
func (a *App) handleWebViewRequest(event *webViewAssetRequest) {
// Get window from window map
a.windowsLock.Lock()
window, ok := a.windows[event.windowId]
a.windowsLock.Unlock()
if !ok {
log.Printf("WebviewWindow #%d not found", event.windowId)
return
}
// Get callback from window
window.handleWebViewRequest(event.request)
}
func (a *App) handleWindowEvent(event *WindowEvent) {
// Get window from window map
a.windowsLock.Lock()
window, ok := a.windows[event.WindowID]
a.windowsLock.Unlock()
if !ok {
log.Printf("WebviewWindow #%d not found", event.WindowID)
return
}
window.handleWindowEvent(event.EventID)
}
func (a *App) handleMenuItemClicked(menuItemID uint) {
menuItem := getMenuItemByID(menuItemID)
if menuItem == nil {
log.Printf("MenuItem #%d not found", menuItemID)
return
}
menuItem.handleClick()
}
func (a *App) CurrentWindow() *WebviewWindow {
if a.impl == nil {
return nil
}
id := a.impl.getCurrentWindowID()
a.windowsLock.Lock()
defer a.windowsLock.Unlock()
return a.windows[id]
}
func (a *App) Quit() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
a.windowsLock.Lock()
for _, window := range a.windows {
window.Destroy()
}
a.windowsLock.Unlock()
wg.Done()
}()
go func() {
a.systemTraysLock.Lock()
for _, systray := range a.systemTrays {
systray.Destroy()
}
a.systemTraysLock.Unlock()
wg.Done()
}()
wg.Wait()
a.impl.destroy()
}
func (a *App) SetMenu(menu *Menu) {
a.ApplicationMenu = menu
if a.impl != nil {
a.impl.setApplicationMenu(menu)
}
}
func (a *App) ShowAboutDialog() {
if a.impl != nil {
a.impl.showAboutDialog(a.options.Name, a.options.Description, a.options.Icon)
}
}
func (a *App) InfoDialog() *MessageDialog {
return newMessageDialog(InfoDialog)
}
func (a *App) QuestionDialog() *MessageDialog {
return newMessageDialog(QuestionDialog)
}
func (a *App) WarningDialog() *MessageDialog {
return newMessageDialog(WarningDialog)
}
func (a *App) ErrorDialog() *MessageDialog {
return newMessageDialog(ErrorDialog)
}
func (a *App) OpenDirectoryDialog() *MessageDialog {
return newMessageDialog(OpenDirectoryDialog)
}
func (a *App) OpenFileDialog() *OpenFileDialog {
return newOpenFileDialog()
}
func (a *App) SaveFileDialog() *SaveFileDialog {
return newSaveFileDialog()
}
func (a *App) GetPrimaryScreen() (*Screen, error) {
return getPrimaryScreen()
}
func (a *App) GetScreens() ([]*Screen, error) {
return getScreens()
}
func (a *App) Clipboard() *Clipboard {
if a.clipboard == nil {
a.clipboard = newClipboard()
}
return a.clipboard
}
func (a *App) dispatchOnMainThread(fn func()) {
mainThreadFunctionStoreLock.Lock()
id := generateFunctionStoreID()
mainThreadFunctionStore[id] = fn
mainThreadFunctionStoreLock.Unlock()
// Call platform specific dispatch function
a.impl.dispatchOnMainThread(id)
}

View File

@ -0,0 +1,11 @@
//go:build darwin
#ifndef application_h
#define application_h
static void init(void);
static void run(void);
static void setActivationPolicy(int policy);
static char *getAppName(void);
#endif

View File

@ -0,0 +1,225 @@
//go:build darwin
package application
/*
#cgo CFLAGS: -mmacosx-version-min=10.13 -x objective-c
#cgo LDFLAGS: -framework Cocoa -mmacosx-version-min=10.13
#include "application.h"
#include "app_delegate.h"
#include "webview_window.h"
#include <stdlib.h>
extern void registerListener(unsigned int event);
#import <Cocoa/Cocoa.h>
static AppDelegate *appDelegate = nil;
static void init(void) {
[NSApplication sharedApplication];
appDelegate = [[AppDelegate alloc] init];
[NSApp setDelegate:appDelegate];
[NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskLeftMouseDown handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) {
NSWindow* eventWindow = [event window];
if (eventWindow == nil ) {
return event;
}
WebviewWindowDelegate* windowDelegate = (WebviewWindowDelegate*)[eventWindow delegate];
if (windowDelegate == nil) {
return event;
}
if ([windowDelegate respondsToSelector:@selector(handleLeftMouseDown:)]) {
[windowDelegate handleLeftMouseDown:event];
}
return event;
}];
[NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskLeftMouseUp handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) {
NSWindow* eventWindow = [event window];
if (eventWindow == nil ) {
return event;
}
WebviewWindowDelegate* windowDelegate = (WebviewWindowDelegate*)[eventWindow delegate];
if (windowDelegate == nil) {
return event;
}
if ([windowDelegate respondsToSelector:@selector(handleLeftMouseUp:)]) {
[windowDelegate handleLeftMouseUp:eventWindow];
}
return event;
}];
}
static void setApplicationShouldTerminateAfterLastWindowClosed(bool shouldTerminate) {
// Get the NSApp delegate
AppDelegate *appDelegate = (AppDelegate*)[NSApp delegate];
// Set the applicationShouldTerminateAfterLastWindowClosed boolean
appDelegate.shouldTerminateWhenLastWindowClosed = shouldTerminate;
}
static void setActivationPolicy(int policy) {
[NSApp setActivationPolicy:policy];
}
static void activateIgnoringOtherApps() {
[NSApp activateIgnoringOtherApps:YES];
}
static void run(void) {
@autoreleasepool {
[NSApp run];
[appDelegate release];
}
}
// Destroy application
static void destroyApp(void) {
[NSApp terminate:nil];
}
// Set the application menu
static void setApplicationMenu(void *menu) {
NSMenu *nsMenu = (__bridge NSMenu *)menu;
[NSApp setMainMenu:menu];
}
// Get the application name
static char* getAppName(void) {
NSString *appName = [NSRunningApplication currentApplication].localizedName;
if( appName == nil ) {
appName = [[NSProcessInfo processInfo] processName];
}
return strdup([appName UTF8String]);
}
// get the current window ID
static unsigned int getCurrentWindowID(void) {
NSWindow *window = [NSApp keyWindow];
// Get the window delegate
WebviewWindowDelegate *delegate = (WebviewWindowDelegate*)[window delegate];
return delegate.windowId;
}
// Set the application icon
static void setApplicationIcon(void *icon, int length) {
// On main thread
dispatch_async(dispatch_get_main_queue(), ^{
NSImage *image = [[NSImage alloc] initWithData:[NSData dataWithBytes:icon length:length]];
[NSApp setApplicationIconImage:image];
});
}
*/
import "C"
import (
"unsafe"
"github.com/wailsapp/wails/v2/pkg/assetserver/webview"
"github.com/wailsapp/wails/v3/pkg/events"
)
type macosApp struct {
applicationMenu unsafe.Pointer
parent *App
}
func (m *macosApp) on(eventID uint) {
C.registerListener(C.uint(eventID))
}
func (m *macosApp) setIcon(icon []byte) {
C.setApplicationIcon(unsafe.Pointer(&icon[0]), C.int(len(icon)))
}
func (m *macosApp) name() string {
appName := C.getAppName()
defer C.free(unsafe.Pointer(appName))
return C.GoString(appName)
}
func (m *macosApp) getCurrentWindowID() uint {
return uint(C.getCurrentWindowID())
}
func (m *macosApp) setApplicationMenu(menu *Menu) {
if menu == nil {
// Create a default menu for mac
menu = defaultApplicationMenu()
}
menu.Update()
// Convert impl to macosMenu object
m.applicationMenu = (menu.impl).(*macosMenu).nsMenu
C.setApplicationMenu(m.applicationMenu)
}
func (m *macosApp) run() error {
// Add a hook to the ApplicationDidFinishLaunching event
m.parent.On(events.Mac.ApplicationDidFinishLaunching, func() {
C.setApplicationShouldTerminateAfterLastWindowClosed(C.bool(m.parent.options.Mac.ApplicationShouldTerminateAfterLastWindowClosed))
C.setActivationPolicy(C.int(m.parent.options.Mac.ActivationPolicy))
C.activateIgnoringOtherApps()
})
// setup event listeners
for eventID := range m.parent.applicationEventListeners {
m.on(eventID)
}
C.run()
return nil
}
func (m *macosApp) destroy() {
C.destroyApp()
}
func newPlatformApp(app *App) *macosApp {
C.init()
return &macosApp{
parent: app,
}
}
//export processApplicationEvent
func processApplicationEvent(eventID C.uint) {
applicationEvents <- uint(eventID)
}
//export processWindowEvent
func processWindowEvent(windowID C.uint, eventID C.uint) {
windowEvents <- &WindowEvent{
WindowID: uint(windowID),
EventID: uint(eventID),
}
}
//export processMessage
func processMessage(windowID C.uint, message *C.char) {
windowMessageBuffer <- &windowMessage{
windowId: uint(windowID),
message: C.GoString(message),
}
}
//export processURLRequest
func processURLRequest(windowID C.uint, wkUrlSchemeTask unsafe.Pointer) {
webviewRequests <- &webViewAssetRequest{
windowId: uint(windowID),
request: webview.NewRequest(wkUrlSchemeTask),
}
}
//export processMenuItemClick
func processMenuItemClick(menuID C.uint) {
menuItemClicked <- uint(menuID)
}
func setIcon(icon []byte) {
if icon == nil {
return
}
C.setApplicationIcon(unsafe.Pointer(&icon[0]), C.int(len(icon)))
}

View File

@ -0,0 +1,26 @@
package application
import "C"
type clipboardImpl interface {
setText(text string) bool
text() string
}
type Clipboard struct {
impl clipboardImpl
}
func newClipboard() *Clipboard {
return &Clipboard{
impl: newClipboardImpl(),
}
}
func (c *Clipboard) SetText(text string) bool {
return c.impl.setText(text)
}
func (c *Clipboard) Text() string {
return c.impl.text()
}

View File

@ -0,0 +1,56 @@
//go:build darwin
package application
/*
#cgo CFLAGS: -mmacosx-version-min=10.13 -x objective-c
#cgo LDFLAGS: -framework Cocoa -mmacosx-version-min=10.13
#import <Cocoa/Cocoa.h>
#import <stdlib.h>
bool setClipboardText(const char* text) {
NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
NSError *error = nil;
NSString *string = [NSString stringWithUTF8String:text];
[pasteBoard clearContents];
return [pasteBoard setString:string forType:NSPasteboardTypeString];
}
const char* getClipboardText() {
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
NSString *text = [pasteboard stringForType:NSPasteboardTypeString];
return [text UTF8String];
}
*/
import "C"
import (
"sync"
"unsafe"
)
var clipboardLock sync.RWMutex
type macosClipboard struct{}
func (m macosClipboard) setText(text string) bool {
clipboardLock.Lock()
defer clipboardLock.Unlock()
cText := C.CString(text)
success := C.setClipboardText(cText)
C.free(unsafe.Pointer(cText))
return bool(success)
}
func (m macosClipboard) text() string {
clipboardLock.RLock()
defer clipboardLock.RUnlock()
clipboardText := C.getClipboardText()
result := C.GoString(clipboardText)
return result
}
func newClipboardImpl() *macosClipboard {
return &macosClipboard{}
}

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