Switch remote deploy to vendored source builds

Move remote deployment to a vendored source bundle built on the target host via Docker so redeploys no longer require local cross-compilation or host Go installation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
GitHub Actions
2026-05-08 12:19:18 +08:00
parent bb27566e38
commit c1a0fe2949
1320 changed files with 497125 additions and 11 deletions

View File

@@ -0,0 +1,102 @@
package application
import (
"context"
"sync"
"github.com/wailsapp/wails/v2/internal/app"
"github.com/wailsapp/wails/v2/internal/signal"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
)
// Application is the main Wails application
type Application struct {
application *app.App
options *options.App
// running flag
running bool
shutdown sync.Once
}
// NewWithOptions creates a new Application with the given options
func NewWithOptions(options *options.App) *Application {
if options == nil {
return New()
}
return &Application{
options: options,
}
}
// New creates a new Application with the default options
func New() *Application {
return &Application{
options: &options.App{},
}
}
// SetApplicationMenu sets the application menu
func (a *Application) SetApplicationMenu(appMenu *menu.Menu) {
if a.running {
a.application.SetApplicationMenu(appMenu)
return
}
a.options.Menu = appMenu
}
// Run starts the application
func (a *Application) Run() error {
err := applicationInit()
if err != nil {
return err
}
application, err := app.CreateApp(a.options)
if err != nil {
return err
}
a.application = application
// Control-C handlers
signal.OnShutdown(func() {
a.application.Shutdown()
})
signal.Start()
a.running = true
err = a.application.Run()
return err
}
// Quit will shut down the application
func (a *Application) Quit() {
a.shutdown.Do(func() {
a.application.Shutdown()
})
}
// Bind the given struct to the application
func (a *Application) Bind(boundStruct any) {
a.options.Bind = append(a.options.Bind, boundStruct)
}
func (a *Application) On(eventType EventType, callback func()) {
c := func(ctx context.Context) {
callback()
}
switch eventType {
case StartUp:
a.options.OnStartup = c
case ShutDown:
a.options.OnShutdown = c
case DomReady:
a.options.OnDomReady = c
}
}

View File

@@ -0,0 +1,9 @@
package application
type EventType int
const (
StartUp EventType = iota
ShutDown
DomReady
)

View File

@@ -0,0 +1,8 @@
//go:build !windows
// +build !windows
package application
func applicationInit() error {
return nil
}

View File

@@ -0,0 +1,16 @@
//go:build windows
package application
import (
"fmt"
"syscall"
)
func applicationInit() error {
status, r, err := syscall.NewLazyDLL("user32.dll").NewProc("SetProcessDPIAware").Call()
if status == 0 {
return fmt.Errorf("exit status %d: %v %v", status, r, err)
}
return nil
}

View File

@@ -0,0 +1,205 @@
package assetserver
import (
"bytes"
"embed"
"errors"
"fmt"
"io"
iofs "io/fs"
"net/http"
"os"
"path"
"strconv"
"strings"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
type Logger interface {
Debug(message string, args ...interface{})
Error(message string, args ...interface{})
}
//go:embed defaultindex.html
var defaultHTML []byte
const (
indexHTML = "index.html"
)
type assetHandler struct {
fs iofs.FS
handler http.Handler
logger Logger
retryMissingFiles bool
}
func NewAssetHandler(options assetserver.Options, log Logger) (http.Handler, error) {
vfs := options.Assets
if vfs != nil {
if _, err := vfs.Open("."); err != nil {
return nil, err
}
subDir, err := FindPathToFile(vfs, indexHTML)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
msg := "no `index.html` could be found in your Assets fs.FS"
if embedFs, isEmbedFs := vfs.(embed.FS); isEmbedFs {
rootFolder, _ := FindEmbedRootPath(embedFs)
msg += fmt.Sprintf(", please make sure the embedded directory '%s' is correct and contains your assets", rootFolder)
}
return nil, fmt.Errorf(msg)
}
return nil, err
}
vfs, err = iofs.Sub(vfs, path.Clean(subDir))
if err != nil {
return nil, err
}
}
var result http.Handler = &assetHandler{
fs: vfs,
handler: options.Handler,
logger: log,
}
if middleware := options.Middleware; middleware != nil {
result = middleware(result)
}
return result, nil
}
func (d *assetHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
url := req.URL.Path
handler := d.handler
if strings.EqualFold(req.Method, http.MethodGet) {
filename := path.Clean(strings.TrimPrefix(url, "/"))
d.logDebug("Handling request '%s' (file='%s')", url, filename)
if err := d.serveFSFile(rw, req, filename); err != nil {
if os.IsNotExist(err) {
if handler != nil {
d.logDebug("File '%s' not found, serving '%s' by AssetHandler", filename, url)
handler.ServeHTTP(rw, req)
err = nil
} else {
rw.WriteHeader(http.StatusNotFound)
err = nil
}
}
if err != nil {
d.logError("Unable to handle request '%s': %s", url, err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
}
} else if handler != nil {
d.logDebug("No GET request, serving '%s' by AssetHandler", url)
handler.ServeHTTP(rw, req)
} else {
rw.WriteHeader(http.StatusMethodNotAllowed)
}
}
// serveFSFile will try to load the file from the fs.FS and write it to the response
func (d *assetHandler) serveFSFile(rw http.ResponseWriter, req *http.Request, filename string) error {
if d.fs == nil {
return os.ErrNotExist
}
file, err := d.fs.Open(filename)
if err != nil {
return err
}
defer file.Close()
statInfo, err := file.Stat()
if err != nil {
return err
}
url := req.URL.Path
isDirectoryPath := url == "" || url[len(url)-1] == '/'
if statInfo.IsDir() {
if !isDirectoryPath {
// If the URL doesn't end in a slash normally a http.redirect should be done, but that currently doesn't work on
// WebKit WebViews (macOS/Linux).
// So we handle this as a specific error
return fmt.Errorf("a directory has been requested without a trailing slash, please add a trailing slash to your request")
}
filename = path.Join(filename, indexHTML)
file, err = d.fs.Open(filename)
if err != nil {
return err
}
defer file.Close()
statInfo, err = file.Stat()
if err != nil {
return err
}
} else if isDirectoryPath {
return fmt.Errorf("a file has been requested with a trailing slash, please remove the trailing slash from your request")
}
var buf [512]byte
var n int
if _, haveType := rw.Header()[HeaderContentType]; !haveType {
// Detect MimeType by sniffing the first 512 bytes
n, err = file.Read(buf[:])
if err != nil && err != io.EOF {
return err
}
// Do the custom MimeType sniffing even though http.ServeContent would do it in case
// of an io.ReadSeeker. We would like to have a consistent behaviour in both cases.
if contentType := GetMimetype(filename, buf[:n]); contentType != "" {
rw.Header().Set(HeaderContentType, contentType)
}
}
if fileSeeker, _ := file.(io.ReadSeeker); fileSeeker != nil {
if _, err := fileSeeker.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("seeker can't seek")
}
http.ServeContent(rw, req, statInfo.Name(), statInfo.ModTime(), fileSeeker)
return nil
}
size := strconv.FormatInt(statInfo.Size(), 10)
rw.Header().Set(HeaderContentLength, size)
// Write the first 512 bytes used for MimeType sniffing
_, err = io.Copy(rw, bytes.NewReader(buf[:n]))
if err != nil {
return err
}
// Copy the remaining content of the file
_, err = io.Copy(rw, file)
return err
}
func (d *assetHandler) logDebug(message string, args ...interface{}) {
if d.logger != nil {
d.logger.Debug("[AssetHandler] "+message, args...)
}
}
func (d *assetHandler) logError(message string, args ...interface{}) {
if d.logger != nil {
d.logger.Error("[AssetHandler] "+message, args...)
}
}

View File

@@ -0,0 +1,84 @@
package assetserver
import (
"errors"
"fmt"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"net/http"
"net/http/httputil"
"net/url"
)
func NewProxyServer(proxyURL string) http.Handler {
parsedURL, err := url.Parse(proxyURL)
if err != nil {
panic(err)
}
return httputil.NewSingleHostReverseProxy(parsedURL)
}
func NewExternalAssetsHandler(logger Logger, options assetserver.Options, url *url.URL) http.Handler {
baseHandler := options.Handler
errSkipProxy := fmt.Errorf("skip proxying")
proxy := httputil.NewSingleHostReverseProxy(url)
baseDirector := proxy.Director
proxy.Director = func(r *http.Request) {
baseDirector(r)
if logger != nil {
logger.Debug("[ExternalAssetHandler] Loading '%s'", r.URL)
}
}
proxy.ModifyResponse = func(res *http.Response) error {
if baseHandler == nil {
return nil
}
if res.StatusCode == http.StatusSwitchingProtocols {
return nil
}
if res.StatusCode == http.StatusNotFound || res.StatusCode == http.StatusMethodNotAllowed {
return errSkipProxy
}
return nil
}
proxy.ErrorHandler = func(rw http.ResponseWriter, r *http.Request, err error) {
if baseHandler != nil && errors.Is(err, errSkipProxy) {
if logger != nil {
logger.Debug("[ExternalAssetHandler] '%s' returned not found, using AssetHandler", r.URL)
}
baseHandler.ServeHTTP(rw, r)
} else {
if logger != nil {
logger.Error("[ExternalAssetHandler] Proxy error: %v", err)
}
rw.WriteHeader(http.StatusBadGateway)
}
}
var result http.Handler = http.HandlerFunc(
func(rw http.ResponseWriter, req *http.Request) {
if req.Method == http.MethodGet {
proxy.ServeHTTP(rw, req)
return
}
if baseHandler != nil {
baseHandler.ServeHTTP(rw, req)
return
}
rw.WriteHeader(http.StatusMethodNotAllowed)
})
if middleware := options.Middleware; middleware != nil {
result = middleware(result)
}
return result
}

View File

@@ -0,0 +1,255 @@
package assetserver
import (
"bytes"
"fmt"
"math/rand"
"net/http"
"strings"
"golang.org/x/net/html"
"html/template"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
const (
runtimeJSPath = "/wails/runtime.js"
ipcJSPath = "/wails/ipc.js"
runtimePath = "/wails/runtime"
)
type RuntimeAssets interface {
DesktopIPC() []byte
WebsocketIPC() []byte
RuntimeDesktopJS() []byte
}
type RuntimeHandler interface {
HandleRuntimeCall(w http.ResponseWriter, r *http.Request)
}
type AssetServer struct {
handler http.Handler
runtimeJS []byte
ipcJS func(*http.Request) []byte
logger Logger
runtime RuntimeAssets
servingFromDisk bool
appendSpinnerToBody bool
// Use http based runtime
runtimeHandler RuntimeHandler
// plugin scripts
pluginScripts map[string]string
assetServerWebView
}
func NewAssetServerMainPage(bindingsJSON string, options *options.App, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) {
assetOptions, err := BuildAssetServerConfig(options)
if err != nil {
return nil, err
}
return NewAssetServer(bindingsJSON, assetOptions, servingFromDisk, logger, runtime)
}
func NewAssetServer(bindingsJSON string, options assetserver.Options, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) {
handler, err := NewAssetHandler(options, logger)
if err != nil {
return nil, err
}
return NewAssetServerWithHandler(handler, bindingsJSON, servingFromDisk, logger, runtime)
}
func NewAssetServerWithHandler(handler http.Handler, bindingsJSON string, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) {
var buffer bytes.Buffer
if bindingsJSON != "" {
escapedBindingsJSON := template.JSEscapeString(bindingsJSON)
buffer.WriteString(`window.wailsbindings='` + escapedBindingsJSON + `';` + "\n")
}
buffer.Write(runtime.RuntimeDesktopJS())
result := &AssetServer{
handler: handler,
runtimeJS: buffer.Bytes(),
// Check if we have been given a directory to serve assets from.
// If so, this means we are in dev mode and are serving assets off disk.
// We indicate this through the `servingFromDisk` flag to ensure requests
// aren't cached in dev mode.
servingFromDisk: servingFromDisk,
logger: logger,
runtime: runtime,
}
return result, nil
}
func (d *AssetServer) UseRuntimeHandler(handler RuntimeHandler) {
d.runtimeHandler = handler
}
func (d *AssetServer) AddPluginScript(pluginName string, script string) {
if d.pluginScripts == nil {
d.pluginScripts = make(map[string]string)
}
pluginName = strings.ReplaceAll(pluginName, "/", "_")
pluginName = html.EscapeString(pluginName)
pluginScriptName := fmt.Sprintf("/plugin_%s_%d.js", pluginName, rand.Intn(100000))
d.pluginScripts[pluginScriptName] = script
}
func (d *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if isWebSocket(req) {
// WebSockets are not supported by the AssetServer
rw.WriteHeader(http.StatusNotImplemented)
return
}
if d.servingFromDisk {
rw.Header().Add(HeaderCacheControl, "no-cache")
}
handler := d.handler
if req.Method != http.MethodGet {
handler.ServeHTTP(rw, req)
return
}
path := req.URL.Path
if path == runtimeJSPath {
d.writeBlob(rw, path, d.runtimeJS)
} else if path == runtimePath && d.runtimeHandler != nil {
d.runtimeHandler.HandleRuntimeCall(rw, req)
} else if path == ipcJSPath {
content := d.runtime.DesktopIPC()
if d.ipcJS != nil {
content = d.ipcJS(req)
}
d.writeBlob(rw, path, content)
} else if script, ok := d.pluginScripts[path]; ok {
d.writeBlob(rw, path, []byte(script))
} else if d.isRuntimeInjectionMatch(path) {
recorder := &bodyRecorder{
ResponseWriter: rw,
doRecord: func(code int, h http.Header) bool {
if code == http.StatusNotFound {
return true
}
if code != http.StatusOK {
return false
}
return strings.Contains(h.Get(HeaderContentType), "text/html")
},
}
handler.ServeHTTP(recorder, req)
body := recorder.Body()
if body == nil {
// The body has been streamed and not recorded, we are finished
return
}
code := recorder.Code()
switch code {
case http.StatusOK:
content, err := d.processIndexHTML(body.Bytes())
if err != nil {
d.serveError(rw, err, "Unable to processIndexHTML")
return
}
d.writeBlob(rw, indexHTML, content)
case http.StatusNotFound:
d.writeBlob(rw, indexHTML, defaultHTML)
default:
rw.WriteHeader(code)
}
} else {
handler.ServeHTTP(rw, req)
}
}
func (d *AssetServer) processIndexHTML(indexHTML []byte) ([]byte, error) {
htmlNode, err := getHTMLNode(indexHTML)
if err != nil {
return nil, err
}
if d.appendSpinnerToBody {
err = appendSpinnerToBody(htmlNode)
if err != nil {
return nil, err
}
}
if err := insertScriptInHead(htmlNode, runtimeJSPath); err != nil {
return nil, err
}
if err := insertScriptInHead(htmlNode, ipcJSPath); err != nil {
return nil, err
}
// Inject plugins
for scriptName := range d.pluginScripts {
if err := insertScriptInHead(htmlNode, scriptName); err != nil {
return nil, err
}
}
var buffer bytes.Buffer
err = html.Render(&buffer, htmlNode)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func (d *AssetServer) writeBlob(rw http.ResponseWriter, filename string, blob []byte) {
err := serveFile(rw, filename, blob)
if err != nil {
d.serveError(rw, err, "Unable to write content %s", filename)
}
}
func (d *AssetServer) serveError(rw http.ResponseWriter, err error, msg string, args ...interface{}) {
args = append(args, err)
d.logError(msg+": %s", args...)
rw.WriteHeader(http.StatusInternalServerError)
}
func (d *AssetServer) logDebug(message string, args ...interface{}) {
if d.logger != nil {
d.logger.Debug("[AssetServer] "+message, args...)
}
}
func (d *AssetServer) logError(message string, args ...interface{}) {
if d.logger != nil {
d.logger.Error("[AssetServer] "+message, args...)
}
}
func (AssetServer) isRuntimeInjectionMatch(path string) bool {
if path == "" {
path = "/"
}
return strings.HasSuffix(path, "/") ||
strings.HasSuffix(path, "/"+indexHTML)
}

View File

@@ -0,0 +1,31 @@
//go:build dev
// +build dev
package assetserver
import (
"net/http"
"strings"
)
/*
The assetserver for the dev mode.
Depending on the UserAgent it injects a websocket based IPC script into `index.html` or the default desktop IPC. The
default desktop IPC is injected when the webview accesses the devserver.
*/
func NewDevAssetServer(handler http.Handler, bindingsJSON string, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) {
result, err := NewAssetServerWithHandler(handler, bindingsJSON, servingFromDisk, logger, runtime)
if err != nil {
return nil, err
}
result.appendSpinnerToBody = true
result.ipcJS = func(req *http.Request) []byte {
if strings.Contains(req.UserAgent(), WailsUserAgentValue) {
return runtime.DesktopIPC()
}
return runtime.WebsocketIPC()
}
return result, nil
}

View File

@@ -0,0 +1,185 @@
package assetserver
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"github.com/wailsapp/wails/v2/pkg/assetserver/webview"
)
type assetServerWebView struct {
// ExpectedWebViewHost is checked against the Request Host of every WebViewRequest, other hosts won't be processed.
ExpectedWebViewHost string
dispatchInit sync.Once
dispatchReqC chan<- webview.Request
dispatchWorkers int
}
// ServeWebViewRequest processes the HTTP Request asynchronously by faking a golang HTTP Server.
// The request will be finished with a StatusNotImplemented code if no handler has written to the response.
// The AssetServer takes ownership of the request and the caller mustn't close it or access it in any other way.
func (d *AssetServer) ServeWebViewRequest(req webview.Request) {
d.dispatchInit.Do(func() {
workers := d.dispatchWorkers
if workers <= 0 {
return
}
workerC := make(chan webview.Request, workers*2)
for i := 0; i < workers; i++ {
go func() {
for req := range workerC {
d.processWebViewRequest(req)
}
}()
}
dispatchC := make(chan webview.Request)
go queueingDispatcher(50, dispatchC, workerC)
d.dispatchReqC = dispatchC
})
if d.dispatchReqC == nil {
go d.processWebViewRequest(req)
} else {
d.dispatchReqC <- req
}
}
func (d *AssetServer) processWebViewRequest(r webview.Request) {
uri, _ := r.URL()
d.processWebViewRequestInternal(r)
if err := r.Close(); err != nil {
d.logError("Unable to call close for request for uri '%s'", uri)
}
}
// processWebViewRequestInternal processes the HTTP Request by faking a golang HTTP Server.
// The request will be finished with a StatusNotImplemented code if no handler has written to the response.
func (d *AssetServer) processWebViewRequestInternal(r webview.Request) {
uri := "unknown"
var err error
wrw := r.Response()
defer func() {
if err := wrw.Finish(); err != nil {
d.logError("Error finishing request '%s': %s", uri, err)
}
}()
var rw http.ResponseWriter = &contentTypeSniffer{rw: wrw} // Make sure we have a Content-Type sniffer
defer rw.WriteHeader(http.StatusNotImplemented) // This is a NOP when a handler has already written and set the status
uri, err = r.URL()
if err != nil {
d.logError("Error processing request, unable to get URL: %s (HttpResponse=500)", err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
method, err := r.Method()
if err != nil {
d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Method: %w", err))
return
}
header, err := r.Header()
if err != nil {
d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Header: %w", err))
return
}
body, err := r.Body()
if err != nil {
d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Body: %w", err))
return
}
if body == nil {
body = http.NoBody
}
defer body.Close()
req, err := http.NewRequest(method, uri, body)
if err != nil {
d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Request: %w", err))
return
}
// For server requests, the URL is parsed from the URI supplied on the Request-Line as stored in RequestURI. For
// most requests, fields other than Path and RawQuery will be empty. (See RFC 7230, Section 5.3)
req.URL.Scheme = ""
req.URL.Host = ""
req.URL.Fragment = ""
req.URL.RawFragment = ""
if url := req.URL; req.RequestURI == "" && url != nil {
req.RequestURI = url.String()
}
req.Header = header
if req.RemoteAddr == "" {
// 192.0.2.0/24 is "TEST-NET" in RFC 5737
req.RemoteAddr = "192.0.2.1:1234"
}
if req.ContentLength == 0 {
req.ContentLength = -1
} else {
size := strconv.FormatInt(req.ContentLength, 10)
req.Header.Set(HeaderContentLength, size)
}
if host := req.Header.Get(HeaderHost); host != "" {
req.Host = host
}
if expectedHost := d.ExpectedWebViewHost; expectedHost != "" && expectedHost != req.Host {
d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("expected host '%s' in request, but was '%s'", expectedHost, req.Host))
return
}
d.ServeHTTP(rw, req)
}
func (d *AssetServer) webviewRequestErrorHandler(uri string, rw http.ResponseWriter, err error) {
logInfo := uri
if uri, err := url.ParseRequestURI(uri); err == nil {
logInfo = strings.Replace(logInfo, fmt.Sprintf("%s://%s", uri.Scheme, uri.Host), "", 1)
}
d.logError("Error processing request '%s': %s (HttpResponse=500)", logInfo, err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
func queueingDispatcher[T any](minQueueSize uint, inC <-chan T, outC chan<- T) {
q := newRingqueue[T](minQueueSize)
for {
in, ok := <-inC
if !ok {
return
}
q.Add(in)
for q.Len() != 0 {
out, _ := q.Peek()
select {
case outC <- out:
q.Remove()
case in, ok := <-inC:
if !ok {
return
}
q.Add(in)
}
}
}
}

View File

@@ -0,0 +1,61 @@
package assetserver
import (
"bytes"
"net/http"
)
type bodyRecorder struct {
http.ResponseWriter
doRecord func(code int, header http.Header) bool
body *bytes.Buffer
code int
wroteHeader bool
}
func (rw *bodyRecorder) Write(buf []byte) (int, error) {
rw.writeHeader(buf, http.StatusOK)
if rw.body != nil {
return rw.body.Write(buf)
}
return rw.ResponseWriter.Write(buf)
}
func (rw *bodyRecorder) WriteHeader(code int) {
rw.writeHeader(nil, code)
}
func (rw *bodyRecorder) Code() int {
return rw.code
}
func (rw *bodyRecorder) Body() *bytes.Buffer {
return rw.body
}
func (rw *bodyRecorder) writeHeader(buf []byte, code int) {
if rw.wroteHeader {
return
}
if rw.doRecord != nil {
header := rw.Header()
if len(buf) != 0 {
if _, hasType := header[HeaderContentType]; !hasType {
header.Set(HeaderContentType, http.DetectContentType(buf))
}
}
if rw.doRecord(code, header) {
rw.body = bytes.NewBuffer(nil)
}
}
if rw.body == nil {
rw.ResponseWriter.WriteHeader(code)
}
rw.code = code
rw.wroteHeader = true
}

View File

@@ -0,0 +1,135 @@
package assetserver
import (
"bytes"
"errors"
"io"
"net/http"
"strconv"
"strings"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"golang.org/x/net/html"
)
func BuildAssetServerConfig(appOptions *options.App) (assetserver.Options, error) {
var options assetserver.Options
if opt := appOptions.AssetServer; opt != nil {
if appOptions.Assets != nil || appOptions.AssetsHandler != nil {
panic("It's not possible to use the deprecated Assets and AssetsHandler options and the new AssetServer option at the same time. Please migrate all your Assets options to the AssetServer option.")
}
options = *opt
} else {
options = assetserver.Options{
Assets: appOptions.Assets,
Handler: appOptions.AssetsHandler,
}
}
return options, options.Validate()
}
const (
HeaderHost = "Host"
HeaderContentType = "Content-Type"
HeaderContentLength = "Content-Length"
HeaderUserAgent = "User-Agent"
HeaderCacheControl = "Cache-Control"
HeaderUpgrade = "Upgrade"
WailsUserAgentValue = "wails.io"
)
func serveFile(rw http.ResponseWriter, filename string, blob []byte) error {
header := rw.Header()
header.Set(HeaderContentLength, strconv.Itoa(len(blob)))
if mimeType := header.Get(HeaderContentType); mimeType == "" {
mimeType = GetMimetype(filename, blob)
header.Set(HeaderContentType, mimeType)
}
rw.WriteHeader(http.StatusOK)
_, err := io.Copy(rw, bytes.NewReader(blob))
return err
}
func createScriptNode(scriptName string) *html.Node {
return &html.Node{
Type: html.ElementNode,
Data: "script",
Attr: []html.Attribute{
{
Key: "src",
Val: scriptName,
},
},
}
}
func createDivNode(id string) *html.Node {
return &html.Node{
Type: html.ElementNode,
Data: "div",
Attr: []html.Attribute{
{
Namespace: "",
Key: "id",
Val: id,
},
},
}
}
func insertScriptInHead(htmlNode *html.Node, scriptName string) error {
headNode := findFirstTag(htmlNode, "head")
if headNode == nil {
return errors.New("cannot find head in HTML")
}
scriptNode := createScriptNode(scriptName)
if headNode.FirstChild != nil {
headNode.InsertBefore(scriptNode, headNode.FirstChild)
} else {
headNode.AppendChild(scriptNode)
}
return nil
}
func appendSpinnerToBody(htmlNode *html.Node) error {
bodyNode := findFirstTag(htmlNode, "body")
if bodyNode == nil {
return errors.New("cannot find body in HTML")
}
scriptNode := createDivNode("wails-spinner")
bodyNode.AppendChild(scriptNode)
return nil
}
func getHTMLNode(htmldata []byte) (*html.Node, error) {
return html.Parse(bytes.NewReader(htmldata))
}
func findFirstTag(htmlnode *html.Node, tagName string) *html.Node {
var extractor func(*html.Node) *html.Node
var result *html.Node
extractor = func(node *html.Node) *html.Node {
if node.Type == html.ElementNode && node.Data == tagName {
return node
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
result := extractor(child)
if result != nil {
return result
}
}
return nil
}
result = extractor(htmlnode)
return result
}
func isWebSocket(req *http.Request) bool {
upgrade := req.Header.Get(HeaderUpgrade)
return strings.EqualFold(upgrade, "websocket")
}

View File

@@ -0,0 +1,42 @@
package assetserver
import (
"net/http"
)
type contentTypeSniffer struct {
rw http.ResponseWriter
wroteHeader bool
}
func (rw *contentTypeSniffer) Header() http.Header {
return rw.rw.Header()
}
func (rw *contentTypeSniffer) Write(buf []byte) (int, error) {
rw.writeHeader(buf)
return rw.rw.Write(buf)
}
func (rw *contentTypeSniffer) WriteHeader(code int) {
if rw.wroteHeader {
return
}
rw.rw.WriteHeader(code)
rw.wroteHeader = true
}
func (rw *contentTypeSniffer) writeHeader(b []byte) {
if rw.wroteHeader {
return
}
m := rw.rw.Header()
if _, hasType := m[HeaderContentType]; !hasType {
m.Set(HeaderContentType, http.DetectContentType(b))
}
rw.WriteHeader(http.StatusOK)
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,75 @@
package assetserver
import (
"embed"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
)
// FindEmbedRootPath finds the root path in the embed FS. It's the directory which contains all the files.
func FindEmbedRootPath(fsys embed.FS) (string, error) {
stopErr := fmt.Errorf("files or multiple dirs found")
fPath := ""
err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
fPath = path
if entries, dErr := fs.ReadDir(fsys, path); dErr != nil {
return dErr
} else if len(entries) <= 1 {
return nil
}
}
return stopErr
})
if err != nil && err != stopErr {
return "", err
}
return fPath, nil
}
func FindPathToFile(fsys fs.FS, file string) (string, error) {
stat, _ := fs.Stat(fsys, file)
if stat != nil {
return ".", nil
}
var indexFiles []string
err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(path, file) {
indexFiles = append(indexFiles, path)
}
return nil
})
if err != nil {
return "", err
}
if len(indexFiles) > 1 {
selected := indexFiles[0]
for _, f := range indexFiles {
if len(f) < len(selected) {
selected = f
}
}
path, _ := filepath.Split(selected)
return path, nil
}
if len(indexFiles) > 0 {
path, _ := filepath.Split(indexFiles[0])
return path, nil
}
return "", fmt.Errorf("%s: %w", file, os.ErrNotExist)
}

View File

@@ -0,0 +1,67 @@
package assetserver
import (
"net/http"
"path/filepath"
"sync"
"github.com/wailsapp/mimetype"
)
var (
mimeCache = map[string]string{}
mimeMutex sync.Mutex
// The list of builtin mime-types by extension as defined by
// the golang standard lib package "mime"
// The standard lib also takes into account mime type definitions from
// etc files like '/etc/apache2/mime.types' but we want to have the
// same behavivour on all platforms and not depend on some external file.
mimeTypesByExt = map[string]string{
".avif": "image/avif",
".css": "text/css; charset=utf-8",
".gif": "image/gif",
".htm": "text/html; charset=utf-8",
".html": "text/html; charset=utf-8",
".jpeg": "image/jpeg",
".jpg": "image/jpeg",
".js": "text/javascript; charset=utf-8",
".json": "application/json",
".mjs": "text/javascript; charset=utf-8",
".pdf": "application/pdf",
".png": "image/png",
".svg": "image/svg+xml",
".wasm": "application/wasm",
".webp": "image/webp",
".xml": "text/xml; charset=utf-8",
}
)
func GetMimetype(filename string, data []byte) string {
mimeMutex.Lock()
defer mimeMutex.Unlock()
result := mimeTypesByExt[filepath.Ext(filename)]
if result != "" {
return result
}
result = mimeCache[filename]
if result != "" {
return result
}
detect := mimetype.Detect(data)
if detect == nil {
result = http.DetectContentType(data)
} else {
result = detect.String()
}
if result == "" {
result = "application/octet-stream"
}
mimeCache[filename] = result
return result
}

View File

@@ -0,0 +1,101 @@
// Code from https://github.com/erikdubbelboer/ringqueue
/*
The MIT License (MIT)
Copyright (c) 2015 Erik Dubbelboer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package assetserver
type ringqueue[T any] struct {
nodes []T
head int
tail int
cnt int
minSize int
}
func newRingqueue[T any](minSize uint) *ringqueue[T] {
if minSize < 2 {
minSize = 2
}
return &ringqueue[T]{
nodes: make([]T, minSize),
minSize: int(minSize),
}
}
func (q *ringqueue[T]) resize(n int) {
nodes := make([]T, n)
if q.head < q.tail {
copy(nodes, q.nodes[q.head:q.tail])
} else {
copy(nodes, q.nodes[q.head:])
copy(nodes[len(q.nodes)-q.head:], q.nodes[:q.tail])
}
q.tail = q.cnt % n
q.head = 0
q.nodes = nodes
}
func (q *ringqueue[T]) Add(i T) {
if q.cnt == len(q.nodes) {
// Also tested a grow rate of 1.5, see: http://stackoverflow.com/questions/2269063/buffer-growth-strategy
// In Go this resulted in a higher memory usage.
q.resize(q.cnt * 2)
}
q.nodes[q.tail] = i
q.tail = (q.tail + 1) % len(q.nodes)
q.cnt++
}
func (q *ringqueue[T]) Peek() (T, bool) {
if q.cnt == 0 {
var none T
return none, false
}
return q.nodes[q.head], true
}
func (q *ringqueue[T]) Remove() (T, bool) {
if q.cnt == 0 {
var none T
return none, false
}
i := q.nodes[q.head]
q.head = (q.head + 1) % len(q.nodes)
q.cnt--
if n := len(q.nodes) / 2; n > q.minSize && q.cnt <= n {
q.resize(n)
}
return i, true
}
func (q *ringqueue[T]) Cap() int {
return cap(q.nodes)
}
func (q *ringqueue[T]) Len() int {
return q.cnt
}

View File

@@ -0,0 +1,17 @@
package webview
import (
"io"
"net/http"
)
type Request interface {
URL() (string, error)
Method() (string, error)
Header() (http.Header, error)
Body() (io.ReadCloser, error)
Response() ResponseWriter
Close() error
}

View File

@@ -0,0 +1,251 @@
//go:build darwin
package webview
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework WebKit
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
#include <string.h>
static void URLSchemeTaskRetain(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
[urlSchemeTask retain];
}
static void URLSchemeTaskRelease(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
[urlSchemeTask release];
}
static const char * URLSchemeTaskRequestURL(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
return [urlSchemeTask.request.URL.absoluteString UTF8String];
}
}
static const char * URLSchemeTaskRequestMethod(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
return [urlSchemeTask.request.HTTPMethod UTF8String];
}
}
static const char * URLSchemeTaskRequestHeadersJSON(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
NSData *headerData = [NSJSONSerialization dataWithJSONObject: urlSchemeTask.request.allHTTPHeaderFields options:0 error: nil];
if (!headerData) {
return nil;
}
NSString* headerString = [[[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding] autorelease];
const char * headerJSON = [headerString UTF8String];
return strdup(headerJSON);
}
}
static bool URLSchemeTaskRequestBodyBytes(void *wkUrlSchemeTask, const void **body, int *bodyLen) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
if (!urlSchemeTask.request.HTTPBody) {
return false;
}
*body = urlSchemeTask.request.HTTPBody.bytes;
*bodyLen = urlSchemeTask.request.HTTPBody.length;
return true;
}
}
static bool URLSchemeTaskRequestBodyStreamOpen(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
if (!urlSchemeTask.request.HTTPBodyStream) {
return false;
}
[urlSchemeTask.request.HTTPBodyStream open];
return true;
}
}
static void URLSchemeTaskRequestBodyStreamClose(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
if (!urlSchemeTask.request.HTTPBodyStream) {
return;
}
[urlSchemeTask.request.HTTPBodyStream close];
}
}
static int URLSchemeTaskRequestBodyStreamRead(void *wkUrlSchemeTask, void *buf, int bufLen) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
NSInputStream *stream = urlSchemeTask.request.HTTPBodyStream;
if (!stream) {
return -2;
}
NSStreamStatus status = stream.streamStatus;
if (status == NSStreamStatusAtEnd || !stream.hasBytesAvailable) {
return 0;
} else if (status != NSStreamStatusOpen) {
return -3;
}
return [stream read:buf maxLength:bufLen];
}
}
*/
import "C"
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"unsafe"
)
// NewRequest creates as new WebViewRequest based on a pointer to an `id<WKURLSchemeTask>`
func NewRequest(wkURLSchemeTask unsafe.Pointer) Request {
C.URLSchemeTaskRetain(wkURLSchemeTask)
return newRequestFinalizer(&request{task: wkURLSchemeTask})
}
var _ Request = &request{}
type request struct {
task unsafe.Pointer
header http.Header
body io.ReadCloser
rw *responseWriter
}
func (r *request) URL() (string, error) {
return C.GoString(C.URLSchemeTaskRequestURL(r.task)), nil
}
func (r *request) Method() (string, error) {
return C.GoString(C.URLSchemeTaskRequestMethod(r.task)), nil
}
func (r *request) Header() (http.Header, error) {
if r.header != nil {
return r.header, nil
}
header := http.Header{}
if cHeaders := C.URLSchemeTaskRequestHeadersJSON(r.task); cHeaders != nil {
if headers := C.GoString(cHeaders); headers != "" {
var h map[string]string
if err := json.Unmarshal([]byte(headers), &h); err != nil {
return nil, fmt.Errorf("unable to unmarshal request headers: %s", err)
}
for k, v := range h {
header.Add(k, v)
}
}
C.free(unsafe.Pointer(cHeaders))
}
r.header = header
return header, nil
}
func (r *request) Body() (io.ReadCloser, error) {
if r.body != nil {
return r.body, nil
}
var body unsafe.Pointer
var bodyLen C.int
if C.URLSchemeTaskRequestBodyBytes(r.task, &body, &bodyLen) {
if body != nil && bodyLen > 0 {
r.body = io.NopCloser(bytes.NewReader(C.GoBytes(body, bodyLen)))
} else {
r.body = http.NoBody
}
} else if C.URLSchemeTaskRequestBodyStreamOpen(r.task) {
r.body = &requestBodyStreamReader{task: r.task}
}
return r.body, nil
}
func (r *request) Response() ResponseWriter {
if r.rw != nil {
return r.rw
}
r.rw = &responseWriter{r: r}
return r.rw
}
func (r *request) Close() error {
var err error
if r.body != nil {
err = r.body.Close()
}
err = r.Response().Finish()
if err != nil {
return err
}
C.URLSchemeTaskRelease(r.task)
return err
}
var _ io.ReadCloser = &requestBodyStreamReader{}
type requestBodyStreamReader struct {
task unsafe.Pointer
closed bool
}
// Read implements io.Reader
func (r *requestBodyStreamReader) Read(p []byte) (n int, err error) {
var content unsafe.Pointer
var contentLen int
if p != nil {
content = unsafe.Pointer(&p[0])
contentLen = len(p)
}
res := C.URLSchemeTaskRequestBodyStreamRead(r.task, content, C.int(contentLen))
if res > 0 {
return int(res), nil
}
switch res {
case 0:
return 0, io.EOF
case -1:
return 0, fmt.Errorf("body: stream error")
case -2:
return 0, fmt.Errorf("body: no stream defined")
case -3:
return 0, io.ErrClosedPipe
default:
return 0, fmt.Errorf("body: unknown error %d", res)
}
}
func (r *requestBodyStreamReader) Close() error {
if r.closed {
return nil
}
r.closed = true
C.URLSchemeTaskRequestBodyStreamClose(r.task)
return nil
}

View File

@@ -0,0 +1,40 @@
package webview
import (
"runtime"
"sync/atomic"
)
var _ Request = &requestFinalizer{}
type requestFinalizer struct {
Request
closed int32
}
// newRequestFinalizer returns a request with a runtime finalizer to make sure it will be closed from the finalizer
// if it has not been already closed.
// It also makes sure Close() of the wrapping request is only called once.
func newRequestFinalizer(r Request) Request {
rf := &requestFinalizer{Request: r}
// Make sure to async release since it might block the finalizer goroutine for a longer period
runtime.SetFinalizer(rf, func(obj *requestFinalizer) { rf.close(true) })
return rf
}
func (r *requestFinalizer) Close() error {
return r.close(false)
}
func (r *requestFinalizer) close(asyncRelease bool) error {
if atomic.CompareAndSwapInt32(&r.closed, 0, 1) {
runtime.SetFinalizer(r, nil)
if asyncRelease {
go r.Request.Close()
return nil
} else {
return r.Request.Close()
}
}
return nil
}

View File

@@ -0,0 +1,85 @@
//go:build linux
// +build linux
package webview
/*
#cgo linux pkg-config: gtk+-3.0 gio-unix-2.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
#cgo webkit2_41 pkg-config: webkit2gtk-4.1
#include "gtk/gtk.h"
#include "webkit2/webkit2.h"
*/
import "C"
import (
"io"
"net/http"
"unsafe"
)
// NewRequest creates as new WebViewRequest based on a pointer to an `WebKitURISchemeRequest`
func NewRequest(webKitURISchemeRequest unsafe.Pointer) Request {
webkitReq := (*C.WebKitURISchemeRequest)(webKitURISchemeRequest)
C.g_object_ref(C.gpointer(webkitReq))
req := &request{req: webkitReq}
return newRequestFinalizer(req)
}
var _ Request = &request{}
type request struct {
req *C.WebKitURISchemeRequest
header http.Header
body io.ReadCloser
rw *responseWriter
}
func (r *request) URL() (string, error) {
return C.GoString(C.webkit_uri_scheme_request_get_uri(r.req)), nil
}
func (r *request) Method() (string, error) {
return webkit_uri_scheme_request_get_http_method(r.req), nil
}
func (r *request) Header() (http.Header, error) {
if r.header != nil {
return r.header, nil
}
r.header = webkit_uri_scheme_request_get_http_headers(r.req)
return r.header, nil
}
func (r *request) Body() (io.ReadCloser, error) {
if r.body != nil {
return r.body, nil
}
r.body = webkit_uri_scheme_request_get_http_body(r.req)
return r.body, nil
}
func (r *request) Response() ResponseWriter {
if r.rw != nil {
return r.rw
}
r.rw = &responseWriter{req: r.req}
return r.rw
}
func (r *request) Close() error {
var err error
if r.body != nil {
err = r.body.Close()
}
r.Response().Finish()
C.g_object_unref(C.gpointer(r.req))
return err
}

View File

@@ -0,0 +1,217 @@
//go:build windows
// +build windows
package webview
import (
"fmt"
"io"
"net/http"
"strings"
"github.com/wailsapp/go-webview2/pkg/edge"
)
// NewRequest creates as new WebViewRequest for chromium. This Method must be called from the Main-Thread!
func NewRequest(env *edge.ICoreWebView2Environment, args *edge.ICoreWebView2WebResourceRequestedEventArgs, invokeSync func(fn func())) (Request, error) {
req, err := args.GetRequest()
if err != nil {
return nil, fmt.Errorf("GetRequest failed: %s", err)
}
defer req.Release()
r := &request{
invokeSync: invokeSync,
}
code := http.StatusInternalServerError
r.response, err = env.CreateWebResourceResponse(nil, code, http.StatusText(code), "")
if err != nil {
return nil, fmt.Errorf("CreateWebResourceResponse failed: %s", err)
}
if err := args.PutResponse(r.response); err != nil {
r.finishResponse()
return nil, fmt.Errorf("PutResponse failed: %s", err)
}
r.deferral, err = args.GetDeferral()
if err != nil {
r.finishResponse()
return nil, fmt.Errorf("GetDeferral failed: %s", err)
}
r.url, r.urlErr = req.GetUri()
r.method, r.methodErr = req.GetMethod()
r.header, r.headerErr = getHeaders(req)
if content, err := req.GetContent(); err != nil {
r.bodyErr = err
} else if content != nil {
// It is safe to access Content from another Thread: https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/threading-model#thread-safety
r.body = &iStreamReleaseCloser{stream: content}
}
return r, nil
}
var _ Request = &request{}
type request struct {
response *edge.ICoreWebView2WebResourceResponse
deferral *edge.ICoreWebView2Deferral
url string
urlErr error
method string
methodErr error
header http.Header
headerErr error
body io.ReadCloser
bodyErr error
rw *responseWriter
invokeSync func(fn func())
}
func (r *request) URL() (string, error) {
return r.url, r.urlErr
}
func (r *request) Method() (string, error) {
return r.method, r.methodErr
}
func (r *request) Header() (http.Header, error) {
return r.header, r.headerErr
}
func (r *request) Body() (io.ReadCloser, error) {
return r.body, r.bodyErr
}
func (r *request) Response() ResponseWriter {
if r.rw != nil {
return r.rw
}
r.rw = &responseWriter{req: r}
return r.rw
}
func (r *request) Close() error {
var errs []error
if r.body != nil {
if err := r.body.Close(); err != nil {
errs = append(errs, err)
}
r.body = nil
}
if err := r.Response().Finish(); err != nil {
errs = append(errs, err)
}
return combineErrs(errs)
}
// finishResponse must be called on the main-thread
func (r *request) finishResponse() error {
var errs []error
if r.response != nil {
if err := r.response.Release(); err != nil {
errs = append(errs, err)
}
r.response = nil
}
if r.deferral != nil {
if err := r.deferral.Complete(); err != nil {
errs = append(errs, err)
}
if err := r.deferral.Release(); err != nil {
errs = append(errs, err)
}
r.deferral = nil
}
return combineErrs(errs)
}
type iStreamReleaseCloser struct {
stream *edge.IStream
closed bool
}
func (i *iStreamReleaseCloser) Read(p []byte) (int, error) {
if i.closed {
return 0, io.ErrClosedPipe
}
return i.stream.Read(p)
}
func (i *iStreamReleaseCloser) Close() error {
if i.closed {
return nil
}
i.closed = true
return i.stream.Release()
}
func getHeaders(req *edge.ICoreWebView2WebResourceRequest) (http.Header, error) {
header := http.Header{}
headers, err := req.GetHeaders()
if err != nil {
return nil, fmt.Errorf("GetHeaders Error: %s", err)
}
defer headers.Release()
headersIt, err := headers.GetIterator()
if err != nil {
return nil, fmt.Errorf("GetIterator Error: %s", err)
}
defer headersIt.Release()
for {
has, err := headersIt.HasCurrentHeader()
if err != nil {
return nil, fmt.Errorf("HasCurrentHeader Error: %s", err)
}
if !has {
break
}
name, value, err := headersIt.GetCurrentHeader()
if err != nil {
return nil, fmt.Errorf("GetCurrentHeader Error: %s", err)
}
header.Set(name, value)
if _, err := headersIt.MoveNext(); err != nil {
return nil, fmt.Errorf("MoveNext Error: %s", err)
}
}
// WebView2 has problems when a request returns a 304 status code and the WebView2 is going to hang for other
// requests including IPC calls.
// So prevent 304 status codes by removing the headers that are used in combinationwith caching.
header.Del("If-Modified-Since")
header.Del("If-None-Match")
return header, nil
}
func combineErrs(errs []error) error {
// TODO use Go1.20 errors.Join
if len(errs) == 0 {
return nil
}
errStrings := make([]string, len(errs))
for i, err := range errs {
errStrings[i] = err.Error()
}
return fmt.Errorf(strings.Join(errStrings, "\n"))
}

View File

@@ -0,0 +1,25 @@
package webview
import (
"errors"
"net/http"
)
const (
HeaderContentLength = "Content-Length"
HeaderContentType = "Content-Type"
)
var (
errRequestStopped = errors.New("request has been stopped")
errResponseFinished = errors.New("response has been finished")
)
// A ResponseWriter interface is used by an HTTP handler to
// construct an HTTP response for the WebView.
type ResponseWriter interface {
http.ResponseWriter
// Finish the response and flush all data. A Finish after the request has already been finished has no effect.
Finish() error
}

View File

@@ -0,0 +1,164 @@
//go:build darwin
package webview
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework WebKit
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
static bool urlSchemeTaskCall(void *wkUrlSchemeTask, schemeTaskCaller fn) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
if (urlSchemeTask == nil) {
return false;
}
@autoreleasepool {
@try {
fn(urlSchemeTask);
} @catch (NSException *exception) {
// This is very bad to detect a stopped schemeTask this should be implemented in a better way
// But it seems to be very tricky to not deadlock when keeping a lock curing executing fn()
// It seems like those call switch the thread back to the main thread and then deadlocks when they reentrant want
// to get the lock again to start another request or stop it.
if ([exception.reason isEqualToString: @"This task has already been stopped"]) {
return false;
}
@throw exception;
}
return true;
}
}
static bool URLSchemeTaskDidReceiveData(void *wkUrlSchemeTask, void* data, int datalength) {
return urlSchemeTaskCall(
wkUrlSchemeTask,
^(id<WKURLSchemeTask> urlSchemeTask) {
NSData *nsdata = [NSData dataWithBytes:data length:datalength];
[urlSchemeTask didReceiveData:nsdata];
});
}
static bool URLSchemeTaskDidFinish(void *wkUrlSchemeTask) {
return urlSchemeTaskCall(
wkUrlSchemeTask,
^(id<WKURLSchemeTask> urlSchemeTask) {
[urlSchemeTask didFinish];
});
}
static bool URLSchemeTaskDidReceiveResponse(void *wkUrlSchemeTask, int statusCode, void *headersString, int headersStringLength) {
return urlSchemeTaskCall(
wkUrlSchemeTask,
^(id<WKURLSchemeTask> urlSchemeTask) {
NSData *nsHeadersJSON = [NSData dataWithBytes:headersString length:headersStringLength];
NSDictionary *headerFields = [NSJSONSerialization JSONObjectWithData:nsHeadersJSON options: NSJSONReadingMutableContainers error: nil];
NSHTTPURLResponse *response = [[[NSHTTPURLResponse alloc] initWithURL:urlSchemeTask.request.URL statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:headerFields] autorelease];
[urlSchemeTask didReceiveResponse:response];
});
}
*/
import "C"
import (
"encoding/json"
"fmt"
"net/http"
"unsafe"
)
var _ ResponseWriter = &responseWriter{}
type responseWriter struct {
r *request
header http.Header
wroteHeader bool
finished bool
}
func (rw *responseWriter) Header() http.Header {
if rw.header == nil {
rw.header = http.Header{}
}
return rw.header
}
func (rw *responseWriter) Write(buf []byte) (int, error) {
if rw.finished {
return 0, errResponseFinished
}
rw.WriteHeader(http.StatusOK)
var contentLen int
if buf != nil {
contentLen = len(buf)
}
if contentLen > 0 {
// Create a C array to hold the data
cBuf := C.malloc(C.size_t(contentLen))
if cBuf == nil {
return 0, fmt.Errorf("memory allocation failed for %d bytes", contentLen)
}
defer C.free(cBuf)
// Copy the Go slice to the C array
C.memcpy(cBuf, unsafe.Pointer(&buf[0]), C.size_t(contentLen))
if !C.URLSchemeTaskDidReceiveData(rw.r.task, cBuf, C.int(contentLen)) {
return 0, errRequestStopped
}
} else {
if !C.URLSchemeTaskDidReceiveData(rw.r.task, nil, 0) {
return 0, errRequestStopped
}
}
return contentLen, nil
}
func (rw *responseWriter) WriteHeader(code int) {
if rw.wroteHeader || rw.finished {
return
}
rw.wroteHeader = true
header := map[string]string{}
for k := range rw.Header() {
header[k] = rw.Header().Get(k)
}
headerData, _ := json.Marshal(header)
var headers unsafe.Pointer
var headersLen int
if len(headerData) != 0 {
headers = unsafe.Pointer(&headerData[0])
headersLen = len(headerData)
}
C.URLSchemeTaskDidReceiveResponse(rw.r.task, C.int(code), headers, C.int(headersLen))
}
func (rw *responseWriter) Finish() error {
if !rw.wroteHeader {
rw.WriteHeader(http.StatusNotImplemented)
}
if rw.finished {
return nil
}
rw.finished = true
C.URLSchemeTaskDidFinish(rw.r.task)
return nil
}

View File

@@ -0,0 +1,132 @@
//go:build linux
// +build linux
package webview
/*
#cgo linux pkg-config: gtk+-3.0 gio-unix-2.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
#cgo webkit2_41 pkg-config: webkit2gtk-4.1
#include "gtk/gtk.h"
#include "webkit2/webkit2.h"
#include "gio/gunixinputstream.h"
*/
import "C"
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"syscall"
"unsafe"
)
type responseWriter struct {
req *C.WebKitURISchemeRequest
header http.Header
wroteHeader bool
finished bool
w io.WriteCloser
wErr error
}
func (rw *responseWriter) Header() http.Header {
if rw.header == nil {
rw.header = http.Header{}
}
return rw.header
}
func (rw *responseWriter) Write(buf []byte) (int, error) {
if rw.finished {
return 0, errResponseFinished
}
rw.WriteHeader(http.StatusOK)
if rw.wErr != nil {
return 0, rw.wErr
}
return rw.w.Write(buf)
}
func (rw *responseWriter) WriteHeader(code int) {
if rw.wroteHeader || rw.finished {
return
}
rw.wroteHeader = true
contentLength := int64(-1)
if sLen := rw.Header().Get(HeaderContentLength); sLen != "" {
if pLen, _ := strconv.ParseInt(sLen, 10, 64); pLen > 0 {
contentLength = pLen
}
}
// We can't use os.Pipe here, because that returns files with a finalizer for closing the FD. But the control over the
// read FD is given to the InputStream and will be closed there.
// Furthermore we especially don't want to have the FD_CLOEXEC
rFD, w, err := pipe()
if err != nil {
rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to open pipe: %s", err))
return
}
rw.w = w
stream := C.g_unix_input_stream_new(C.int(rFD), C.gboolean(1))
defer C.g_object_unref(C.gpointer(stream))
if err := webkit_uri_scheme_request_finish(rw.req, code, rw.Header(), stream, contentLength); err != nil {
rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to finish request: %s", err))
return
}
}
func (rw *responseWriter) Finish() error {
if !rw.wroteHeader {
rw.WriteHeader(http.StatusNotImplemented)
}
if rw.finished {
return nil
}
rw.finished = true
if rw.w != nil {
rw.w.Close()
}
return nil
}
func (rw *responseWriter) finishWithError(code int, err error) {
if rw.w != nil {
rw.w.Close()
rw.w = &nopCloser{io.Discard}
}
rw.wErr = err
msg := C.CString(err.Error())
gerr := C.g_error_new_literal(C.g_quark_from_string(msg), C.int(code), msg)
C.webkit_uri_scheme_request_finish_error(rw.req, gerr)
C.g_error_free(gerr)
C.free(unsafe.Pointer(msg))
}
type nopCloser struct {
io.Writer
}
func (nopCloser) Close() error { return nil }
func pipe() (r int, w *os.File, err error) {
var p [2]int
e := syscall.Pipe2(p[0:], 0)
if e != nil {
return 0, nil, fmt.Errorf("pipe2: %s", e)
}
return p[0], os.NewFile(uintptr(p[1]), "|1"), nil
}

View File

@@ -0,0 +1,105 @@
//go:build windows
// +build windows
package webview
import (
"bytes"
"fmt"
"net/http"
"strings"
)
var _ http.ResponseWriter = &responseWriter{}
type responseWriter struct {
req *request
header http.Header
wroteHeader bool
code int
body *bytes.Buffer
finished bool
}
func (rw *responseWriter) Header() http.Header {
if rw.header == nil {
rw.header = http.Header{}
}
return rw.header
}
func (rw *responseWriter) Write(buf []byte) (int, error) {
if rw.finished {
return 0, errResponseFinished
}
rw.WriteHeader(http.StatusOK)
return rw.body.Write(buf)
}
func (rw *responseWriter) WriteHeader(code int) {
if rw.wroteHeader || rw.finished {
return
}
rw.wroteHeader = true
if rw.body == nil {
rw.body = &bytes.Buffer{}
}
rw.code = code
}
func (rw *responseWriter) Finish() error {
if !rw.wroteHeader {
rw.WriteHeader(http.StatusNotImplemented)
}
if rw.finished {
return nil
}
rw.finished = true
var errs []error
code := rw.code
if code == http.StatusNotModified {
// WebView2 has problems when a request returns a 304 status code and the WebView2 is going to hang for other
// requests including IPC calls.
errs = append(errs, fmt.Errorf("AssetServer returned 304 - StatusNotModified which are going to hang WebView2, changed code to 505 - StatusInternalServerError"))
code = http.StatusInternalServerError
}
rw.req.invokeSync(func() {
resp := rw.req.response
hdrs, err := resp.GetHeaders()
if err != nil {
errs = append(errs, fmt.Errorf("Resp.GetHeaders failed: %s", err))
} else {
for k, v := range rw.header {
if err := hdrs.AppendHeader(k, strings.Join(v, ",")); err != nil {
errs = append(errs, fmt.Errorf("Resp.AppendHeader failed: %s", err))
}
}
hdrs.Release()
}
if err := resp.PutStatusCode(code); err != nil {
errs = append(errs, fmt.Errorf("Resp.PutStatusCode failed: %s", err))
}
if err := resp.PutByteContent(rw.body.Bytes()); err != nil {
errs = append(errs, fmt.Errorf("Resp.PutByteContent failed: %s", err))
}
if err := rw.req.finishResponse(); err != nil {
errs = append(errs, fmt.Errorf("Resp.finishResponse failed: %s", err))
}
})
return combineErrs(errs)
}

View File

@@ -0,0 +1,71 @@
//go:build linux && (webkit2_36 || webkit2_40 || webkit2_41 )
package webview
/*
#cgo linux pkg-config: gtk+-3.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 libsoup-2.4
#cgo webkit2_41 pkg-config: webkit2gtk-4.1 libsoup-3.0
#include "gtk/gtk.h"
#include "webkit2/webkit2.h"
#include "libsoup/soup.h"
*/
import "C"
import (
"net/http"
"strings"
"unsafe"
)
func webkit_uri_scheme_request_get_http_method(req *C.WebKitURISchemeRequest) string {
method := C.GoString(C.webkit_uri_scheme_request_get_http_method(req))
return strings.ToUpper(method)
}
func webkit_uri_scheme_request_get_http_headers(req *C.WebKitURISchemeRequest) http.Header {
hdrs := C.webkit_uri_scheme_request_get_http_headers(req)
var iter C.SoupMessageHeadersIter
C.soup_message_headers_iter_init(&iter, hdrs)
var name *C.char
var value *C.char
h := http.Header{}
for C.soup_message_headers_iter_next(&iter, &name, &value) != 0 {
h.Add(C.GoString(name), C.GoString(value))
}
return h
}
func webkit_uri_scheme_request_finish(req *C.WebKitURISchemeRequest, code int, header http.Header, stream *C.GInputStream, streamLength int64) error {
resp := C.webkit_uri_scheme_response_new(stream, C.gint64(streamLength))
defer C.g_object_unref(C.gpointer(resp))
cReason := C.CString(http.StatusText(code))
C.webkit_uri_scheme_response_set_status(resp, C.guint(code), cReason)
C.free(unsafe.Pointer(cReason))
cMimeType := C.CString(header.Get(HeaderContentType))
C.webkit_uri_scheme_response_set_content_type(resp, cMimeType)
C.free(unsafe.Pointer(cMimeType))
hdrs := C.soup_message_headers_new(C.SOUP_MESSAGE_HEADERS_RESPONSE)
for name, values := range header {
cName := C.CString(name)
for _, value := range values {
cValue := C.CString(value)
C.soup_message_headers_append(hdrs, cName, cValue)
C.free(unsafe.Pointer(cValue))
}
C.free(unsafe.Pointer(cName))
}
C.webkit_uri_scheme_response_set_http_headers(resp, hdrs)
C.webkit_uri_scheme_request_finish_with_response(req, resp)
return nil
}

View File

@@ -0,0 +1,21 @@
//go:build linux && webkit2_36
package webview
/*
#cgo linux pkg-config: webkit2gtk-4.0
#include "webkit2/webkit2.h"
*/
import "C"
import (
"io"
"net/http"
)
const Webkit2MinMinorVersion = 36
func webkit_uri_scheme_request_get_http_body(_ *C.WebKitURISchemeRequest) io.ReadCloser {
return http.NoBody
}

View File

@@ -0,0 +1,85 @@
//go:build linux && (webkit2_40 || webkit2_41)
package webview
/*
#cgo linux pkg-config: gtk+-3.0 gio-unix-2.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
#cgo webkit2_41 pkg-config: webkit2gtk-4.1
#include "gtk/gtk.h"
#include "webkit2/webkit2.h"
#include "gio/gunixinputstream.h"
*/
import "C"
import (
"fmt"
"io"
"net/http"
"unsafe"
)
func webkit_uri_scheme_request_get_http_body(req *C.WebKitURISchemeRequest) io.ReadCloser {
stream := C.webkit_uri_scheme_request_get_http_body(req)
if stream == nil {
return http.NoBody
}
return &webkitRequestBody{stream: stream}
}
type webkitRequestBody struct {
stream *C.GInputStream
closed bool
}
// Read implements io.Reader
func (r *webkitRequestBody) Read(p []byte) (int, error) {
if r.closed {
return 0, io.ErrClosedPipe
}
var content unsafe.Pointer
var contentLen int
if p != nil {
content = unsafe.Pointer(&p[0])
contentLen = len(p)
}
var n C.gsize
var gErr *C.GError
res := C.g_input_stream_read_all(r.stream, content, C.gsize(contentLen), &n, nil, &gErr)
if res == 0 {
return 0, formatGError("stream read failed", gErr)
} else if n == 0 {
return 0, io.EOF
}
return int(n), nil
}
func (r *webkitRequestBody) Close() error {
if r.closed {
return nil
}
r.closed = true
// https://docs.gtk.org/gio/method.InputStream.close.html
// Streams will be automatically closed when the last reference is dropped, but you might want to call this function
// to make sure resources are released as early as possible.
var err error
var gErr *C.GError
if C.g_input_stream_close(r.stream, nil, &gErr) == 0 {
err = formatGError("stream close failed", gErr)
}
C.g_object_unref(C.gpointer(r.stream))
r.stream = nil
return err
}
func formatGError(msg string, gErr *C.GError, args ...any) error {
if gErr != nil && gErr.message != nil {
msg += ": " + C.GoString(gErr.message)
C.g_error_free(gErr)
}
return fmt.Errorf(msg, args...)
}

View File

@@ -0,0 +1,5 @@
//go:build linux && webkit2_40
package webview
const Webkit2MinMinorVersion = 40

View File

@@ -0,0 +1,5 @@
//go:build linux && webkit2_41
package webview
const Webkit2MinMinorVersion = 41

View File

@@ -0,0 +1,48 @@
//go:build linux && !(webkit2_36 || webkit2_40 || webkit2_41)
package webview
/*
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0
#include "gtk/gtk.h"
#include "webkit2/webkit2.h"
*/
import "C"
import (
"fmt"
"io"
"net/http"
"unsafe"
)
const Webkit2MinMinorVersion = 0
func webkit_uri_scheme_request_get_http_method(_ *C.WebKitURISchemeRequest) string {
return http.MethodGet
}
func webkit_uri_scheme_request_get_http_headers(_ *C.WebKitURISchemeRequest) http.Header {
// Fake some basic default headers that are needed if e.g. request are being proxied to the an external sever, like
// we do in the devserver.
h := http.Header{}
h.Add("Accept", "*/*")
h.Add("User-Agent", "wails.io/605.1.15")
return h
}
func webkit_uri_scheme_request_get_http_body(_ *C.WebKitURISchemeRequest) io.ReadCloser {
return http.NoBody
}
func webkit_uri_scheme_request_finish(req *C.WebKitURISchemeRequest, code int, header http.Header, stream *C.GInputStream, streamLength int64) error {
if code != http.StatusOK {
return fmt.Errorf("StatusCodes not supported: %d - %s", code, http.StatusText(code))
}
cMimeType := C.CString(header.Get(HeaderContentType))
C.webkit_uri_scheme_request_finish(req, stream, C.gint64(streamLength), cMimeType)
C.free(unsafe.Pointer(cMimeType))
return nil
}

View File

@@ -0,0 +1,49 @@
package logger
import (
"os"
)
// DefaultLogger is a utility to log messages to a number of destinations
type DefaultLogger struct{}
// NewDefaultLogger creates a new Logger.
func NewDefaultLogger() Logger {
return &DefaultLogger{}
}
// Print works like Sprintf.
func (l *DefaultLogger) Print(message string) {
println(message)
}
// Trace level logging. Works like Sprintf.
func (l *DefaultLogger) Trace(message string) {
println("TRA | " + message)
}
// Debug level logging. Works like Sprintf.
func (l *DefaultLogger) Debug(message string) {
println("DEB | " + message)
}
// Info level logging. Works like Sprintf.
func (l *DefaultLogger) Info(message string) {
println("INF | " + message)
}
// Warning level logging. Works like Sprintf.
func (l *DefaultLogger) Warning(message string) {
println("WAR | " + message)
}
// Error level logging. Works like Sprintf.
func (l *DefaultLogger) Error(message string) {
println("ERR | " + message)
}
// Fatal level logging. Works like Sprintf.
func (l *DefaultLogger) Fatal(message string) {
println("FAT | " + message)
os.Exit(1)
}

View File

@@ -0,0 +1,66 @@
package logger
import (
"log"
"os"
)
// FileLogger is a utility to log messages to a number of destinations
type FileLogger struct {
filename string
}
// NewFileLogger creates a new Logger.
func NewFileLogger(filename string) Logger {
return &FileLogger{
filename: filename,
}
}
// Print works like Sprintf.
func (l *FileLogger) Print(message string) {
f, err := os.OpenFile(l.filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
log.Fatal(err)
}
if _, err := f.WriteString(message); err != nil {
f.Close()
log.Fatal(err)
}
f.Close()
}
func (l *FileLogger) Println(message string) {
l.Print(message + "\n")
}
// Trace level logging. Works like Sprintf.
func (l *FileLogger) Trace(message string) {
l.Println("TRACE | " + message)
}
// Debug level logging. Works like Sprintf.
func (l *FileLogger) Debug(message string) {
l.Println("DEBUG | " + message)
}
// Info level logging. Works like Sprintf.
func (l *FileLogger) Info(message string) {
l.Println("INFO | " + message)
}
// Warning level logging. Works like Sprintf.
func (l *FileLogger) Warning(message string) {
l.Println("WARN | " + message)
}
// Error level logging. Works like Sprintf.
func (l *FileLogger) Error(message string) {
l.Println("ERROR | " + message)
}
// Fatal level logging. Works like Sprintf.
func (l *FileLogger) Fatal(message string) {
l.Println("FATAL | " + message)
os.Exit(1)
}

View File

@@ -0,0 +1,72 @@
package logger
import (
"fmt"
"strings"
)
// LogLevel is an unsigned 8bit int
type LogLevel uint8
const (
// TRACE level
TRACE LogLevel = 1
// DEBUG level logging
DEBUG LogLevel = 2
// INFO level logging
INFO LogLevel = 3
// WARNING level logging
WARNING LogLevel = 4
// ERROR level logging
ERROR LogLevel = 5
)
var logLevelMap = map[string]LogLevel{
"trace": TRACE,
"debug": DEBUG,
"info": INFO,
"warning": WARNING,
"error": ERROR,
}
func StringToLogLevel(input string) (LogLevel, error) {
result, ok := logLevelMap[strings.ToLower(input)]
if !ok {
return ERROR, fmt.Errorf("invalid log level: %s", input)
}
return result, nil
}
// String returns the string representation of the LogLevel
func (l LogLevel) String() string {
switch l {
case TRACE:
return "trace"
case DEBUG:
return "debug"
case INFO:
return "info"
case WARNING:
return "warning"
case ERROR:
return "error"
default:
return "debug"
}
}
// Logger specifies the methods required to attach
// a logger to a Wails application
type Logger interface {
Print(message string)
Trace(message string)
Debug(message string)
Info(message string)
Warning(message string)
Error(message string)
Fatal(message string)
}

10
vendor/github.com/wailsapp/wails/v2/pkg/menu/README.md generated vendored Normal file
View File

@@ -0,0 +1,10 @@
# Menus
Menu support is heavily inspired by Electron's approach.
## Features
* Supports Text, Checkbox, Radio, Submenu and Separator
* Radio groups are defined as any number of adjacent radio items
* UTF-8 menu labels
* UTF-8 menu IDs

View File

@@ -0,0 +1,8 @@
package menu
type CallbackData struct {
MenuItem *MenuItem
// ContextData string
}
type Callback func(*CallbackData)

1559
vendor/github.com/wailsapp/wails/v2/pkg/menu/cols.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
package menu
type ContextMenu struct {
ID string
Menu *Menu
}
func NewContextMenu(ID string, menu *Menu) *ContextMenu {
return &ContextMenu{
ID: ID,
Menu: menu,
}
}

View File

@@ -0,0 +1,104 @@
package keys
import (
"fmt"
"strings"
)
// Modifier is actually a string
type Modifier string
const (
// CmdOrCtrlKey represents Command on Mac and Control on other platforms
CmdOrCtrlKey Modifier = "cmdorctrl"
// OptionOrAltKey represents Option on Mac and Alt on other platforms
OptionOrAltKey Modifier = "optionoralt"
// ShiftKey represents the shift key on all systems
ShiftKey Modifier = "shift"
// SuperKey represents Command on Mac and the Windows key on the other platforms
// SuperKey Modifier = "super"
// ControlKey represents the control key on all systems
ControlKey Modifier = "ctrl"
)
var modifierMap = map[string]Modifier{
"cmdorctrl": CmdOrCtrlKey,
"optionoralt": OptionOrAltKey,
"shift": ShiftKey,
//"super": SuperKey,
"ctrl": ControlKey,
}
func parseModifier(text string) (*Modifier, error) {
lowertext := strings.ToLower(text)
result, valid := modifierMap[lowertext]
if !valid {
return nil, fmt.Errorf("'%s' is not a valid modifier", text)
}
return &result, nil
}
// Accelerator holds the keyboard shortcut for a menu item
type Accelerator struct {
Key string
Modifiers []Modifier
}
// Key creates a standard key Accelerator
func Key(key string) *Accelerator {
return &Accelerator{
Key: strings.ToLower(key),
}
}
// CmdOrCtrl creates a 'CmdOrCtrl' Accelerator
func CmdOrCtrl(key string) *Accelerator {
return &Accelerator{
Key: strings.ToLower(key),
Modifiers: []Modifier{CmdOrCtrlKey},
}
}
// OptionOrAlt creates a 'OptionOrAlt' Accelerator
func OptionOrAlt(key string) *Accelerator {
return &Accelerator{
Key: strings.ToLower(key),
Modifiers: []Modifier{OptionOrAltKey},
}
}
// Shift creates a 'Shift' Accelerator
func Shift(key string) *Accelerator {
return &Accelerator{
Key: strings.ToLower(key),
Modifiers: []Modifier{ShiftKey},
}
}
// Control creates a 'Control' Accelerator
func Control(key string) *Accelerator {
return &Accelerator{
Key: strings.ToLower(key),
Modifiers: []Modifier{ControlKey},
}
}
//
//// Super creates a 'Super' Accelerator
//func Super(key string) *Accelerator {
// return &Accelerator{
// Key: strings.ToLower(key),
// Modifiers: []Modifier{SuperKey},
// }
//}
// Combo creates an Accelerator with multiple Modifiers
func Combo(key string, modifier1 Modifier, modifier2 Modifier, rest ...Modifier) *Accelerator {
result := &Accelerator{
Key: key,
Modifiers: []Modifier{modifier1, modifier2},
}
result.Modifiers = append(result.Modifiers, rest...)
return result
}

View File

@@ -0,0 +1,26 @@
package keys
const (
NSEventModifierFlagShift = 1 << 17 // Set if Shift key is pressed.
NSEventModifierFlagControl = 1 << 18 // Set if Control key is pressed.
NSEventModifierFlagOption = 1 << 19 // Set if Option or Alternate key is pressed.
NSEventModifierFlagCommand = 1 << 20 // Set if Command key is pressed.
)
var macModifierMap = map[Modifier]int{
CmdOrCtrlKey: NSEventModifierFlagCommand,
ControlKey: NSEventModifierFlagControl,
OptionOrAltKey: NSEventModifierFlagOption,
ShiftKey: NSEventModifierFlagShift,
}
func ToMacModifier(accelerator *Accelerator) int {
if accelerator == nil {
return 0
}
result := 0
for _, modifier := range accelerator.Modifiers {
result |= macModifierMap[modifier]
}
return result
}

View File

@@ -0,0 +1,87 @@
package keys
import (
"fmt"
"strconv"
"strings"
"github.com/leaanthony/slicer"
)
var namedKeys = slicer.String([]string{"backspace", "tab", "return", "enter", "escape", "left", "right", "up", "down", "space", "delete", "home", "end", "page up", "page down", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", "f20", "f21", "f22", "f23", "f24", "f25", "f26", "f27", "f28", "f29", "f30", "f31", "f32", "f33", "f34", "f35", "numlock"})
func parseKey(key string) (string, bool) {
// Lowercase!
key = strings.ToLower(key)
// Check special case
if key == "plus" {
return "+", true
}
// Handle named keys
if namedKeys.Contains(key) {
return key, true
}
// Check we only have a single character
if len(key) != 1 {
return "", false
}
runeKey := rune(key[0])
// This may be too inclusive
if strconv.IsPrint(runeKey) {
return key, true
}
return "", false
}
func Parse(shortcut string) (*Accelerator, error) {
var result Accelerator
// Split the shortcut by +
components := strings.Split(shortcut, "+")
// If we only have one it should be a key
// We require components
if len(components) == 0 {
return nil, fmt.Errorf("no components given to validateComponents")
}
// Keep track of modifiers we have processed
var modifiersProcessed slicer.StringSlicer
// Check components
for index, component := range components {
// If last component
if index == len(components)-1 {
processedkey, validKey := parseKey(component)
if !validKey {
return nil, fmt.Errorf("'%s' is not a valid key", component)
}
result.Key = processedkey
continue
}
// Not last component - needs to be modifier
lowercaseComponent := strings.ToLower(component)
thisModifier, valid := modifierMap[lowercaseComponent]
if !valid {
return nil, fmt.Errorf("'%s' is not a valid modifier", component)
}
// Needs to be unique
if modifiersProcessed.Contains(lowercaseComponent) {
return nil, fmt.Errorf("Modifier '%s' is defined twice for shortcut: %s", component, shortcut)
}
// Save this data
result.Modifiers = append(result.Modifiers, thisModifier)
modifiersProcessed.Add(lowercaseComponent)
}
return &result, nil
}

View File

@@ -0,0 +1,41 @@
package keys
import (
"strings"
"github.com/leaanthony/slicer"
)
var modifierStringMap = map[string]map[Modifier]string{
"windows": {
CmdOrCtrlKey: "Ctrl",
ControlKey: "Ctrl",
OptionOrAltKey: "Alt",
ShiftKey: "Shift",
// SuperKey: "Win",
},
"darwin": {
CmdOrCtrlKey: "Cmd",
ControlKey: "Ctrl",
OptionOrAltKey: "Option",
ShiftKey: "Shift",
// SuperKey: "Cmd",
},
"linux": {
CmdOrCtrlKey: "Ctrl",
ControlKey: "Ctrl",
OptionOrAltKey: "Alt",
ShiftKey: "Shift",
// SuperKey: "Super",
},
}
func Stringify(accelerator *Accelerator, platform string) string {
result := slicer.String()
for _, modifier := range accelerator.Modifiers {
result.Add(modifierStringMap[platform][modifier])
}
result.Deduplicate()
result.Add(strings.ToUpper(accelerator.Key))
return result.Join("+")
}

12
vendor/github.com/wailsapp/wails/v2/pkg/menu/mac.go generated vendored Normal file
View File

@@ -0,0 +1,12 @@
package menu
/*
// DefaultMacMenu returns a default menu including the default
// Application and Edit menus. Use `.Append()` to add to it.
func DefaultMacMenu() *Menu {
return NewMenuFromItems(
AppMenu(),
EditMenu(),
)
}
*/

75
vendor/github.com/wailsapp/wails/v2/pkg/menu/menu.go generated vendored Normal file
View File

@@ -0,0 +1,75 @@
package menu
import "github.com/wailsapp/wails/v2/pkg/menu/keys"
type Menu struct {
Items []*MenuItem
}
func NewMenu() *Menu {
return &Menu{}
}
func (m *Menu) Append(item *MenuItem) {
m.Items = append(m.Items, item)
}
// Merge will append the items in the given menu
// into this menu
func (m *Menu) Merge(menu *Menu) {
m.Items = append(m.Items, menu.Items...)
}
// AddText adds a TextMenu item to the menu
func (m *Menu) AddText(label string, accelerator *keys.Accelerator, click Callback) *MenuItem {
item := Text(label, accelerator, click)
m.Append(item)
return item
}
// AddCheckbox adds a CheckboxMenu item to the menu
func (m *Menu) AddCheckbox(label string, checked bool, accelerator *keys.Accelerator, click Callback) *MenuItem {
item := Checkbox(label, checked, accelerator, click)
m.Append(item)
return item
}
// AddRadio adds a radio item to the menu
func (m *Menu) AddRadio(label string, checked bool, accelerator *keys.Accelerator, click Callback) *MenuItem {
item := Radio(label, checked, accelerator, click)
m.Append(item)
return item
}
// AddSeparator adds a separator to the menu
func (m *Menu) AddSeparator() {
item := Separator()
m.Append(item)
}
func (m *Menu) AddSubmenu(label string) *Menu {
submenu := NewMenu()
item := SubMenu(label, submenu)
m.Append(item)
return submenu
}
func (m *Menu) Prepend(item *MenuItem) {
m.Items = append([]*MenuItem{item}, m.Items...)
}
func NewMenuFromItems(first *MenuItem, rest ...*MenuItem) *Menu {
result := NewMenu()
result.Append(first)
for _, item := range rest {
result.Append(item)
}
return result
}
func (m *Menu) setParent(menuItem *MenuItem) {
for _, item := range m.Items {
item.parent = menuItem
}
}

View File

@@ -0,0 +1,329 @@
package menu
import (
"sync"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
)
// MenuItem represents a menuitem contained in a menu
type MenuItem struct {
// Label is what appears as the menu text
Label string
// Role is a predefined menu type
Role Role
// Accelerator holds a representation of a key binding
Accelerator *keys.Accelerator
// Type of MenuItem, EG: Checkbox, Text, Separator, Radio, Submenu
Type Type
// Disabled makes the item unselectable
Disabled bool
// Hidden ensures that the item is not shown in the menu
Hidden bool
// Checked indicates if the item is selected (used by Checkbox and Radio types only)
Checked bool
// SubMenu contains a list of menu items that will be shown as a submenu
// SubMenu []*MenuItem `json:"SubMenu,omitempty"`
SubMenu *Menu
// Callback function when menu clicked
Click Callback
/*
// Text Colour
RGBA string
// Font
FontSize int
FontName string
// Image - base64 image data
Image string
// MacTemplateImage indicates that on a Mac, this image is a template image
MacTemplateImage bool
// MacAlternate indicates that this item is an alternative to the previous menu item
MacAlternate bool
// Tooltip
Tooltip string
*/
// This holds the menu item's parent.
parent *MenuItem
// Used for locking when removing elements
removeLock sync.Mutex
}
// Parent returns the parent of the menu item.
// If it is a top level menu then it returns nil.
func (m *MenuItem) Parent() *MenuItem {
return m.parent
}
// Append will attempt to append the given menu item to
// this item's submenu items. If this menu item is not a
// submenu, then this method will not add the item and
// simply return false.
func (m *MenuItem) Append(item *MenuItem) bool {
if !m.isSubMenu() {
return false
}
item.parent = m
m.SubMenu.Append(item)
return true
}
// Prepend will attempt to prepend the given menu item to
// this item's submenu items. If this menu item is not a
// submenu, then this method will not add the item and
// simply return false.
func (m *MenuItem) Prepend(item *MenuItem) bool {
if !m.isSubMenu() {
return false
}
item.parent = m
m.SubMenu.Prepend(item)
return true
}
func (m *MenuItem) Remove() {
// Iterate my parent's children
m.Parent().removeChild(m)
}
func (m *MenuItem) removeChild(item *MenuItem) {
m.removeLock.Lock()
for index, child := range m.SubMenu.Items {
if item == child {
m.SubMenu.Items = append(m.SubMenu.Items[:index], m.SubMenu.Items[index+1:]...)
}
}
m.removeLock.Unlock()
}
// InsertAfter attempts to add the given item after this item in the parent
// menu. If there is no parent menu (we are a top level menu) then false is
// returned
func (m *MenuItem) InsertAfter(item *MenuItem) bool {
// We need to find my parent
if m.parent == nil {
return false
}
// Get my parent to insert the item
return m.parent.insertNewItemAfterGivenItem(m, item)
}
// InsertBefore attempts to add the given item before this item in the parent
// menu. If there is no parent menu (we are a top level menu) then false is
// returned
func (m *MenuItem) InsertBefore(item *MenuItem) bool {
// We need to find my parent
if m.parent == nil {
return false
}
// Get my parent to insert the item
return m.parent.insertNewItemBeforeGivenItem(m, item)
}
// insertNewItemAfterGivenItem will insert the given item after the given target
// in this item's submenu. If we are not a submenu,
// then something bad has happened :/
func (m *MenuItem) insertNewItemAfterGivenItem(target *MenuItem,
newItem *MenuItem,
) bool {
if !m.isSubMenu() {
return false
}
// Find the index of the target
targetIndex := m.getItemIndex(target)
if targetIndex == -1 {
return false
}
// Insert element into slice
return m.insertItemAtIndex(targetIndex+1, newItem)
}
// insertNewItemBeforeGivenItem will insert the given item before the given
// target in this item's submenu. If we are not a submenu, then something bad
// has happened :/
func (m *MenuItem) insertNewItemBeforeGivenItem(target *MenuItem,
newItem *MenuItem,
) bool {
if !m.isSubMenu() {
return false
}
// Find the index of the target
targetIndex := m.getItemIndex(target)
if targetIndex == -1 {
return false
}
// Insert element into slice
return m.insertItemAtIndex(targetIndex, newItem)
}
func (m *MenuItem) isSubMenu() bool {
return m.Type == SubmenuType
}
// getItemIndex returns the index of the given target relative to this menu
func (m *MenuItem) getItemIndex(target *MenuItem) int {
// This should only be called on submenus
if !m.isSubMenu() {
return -1
}
// hunt down that bad boy
for index, item := range m.SubMenu.Items {
if item == target {
return index
}
}
return -1
}
// insertItemAtIndex attempts to insert the given item into the submenu at
// the given index
// Credit: https://stackoverflow.com/a/61822301
func (m *MenuItem) insertItemAtIndex(index int, target *MenuItem) bool {
// If index is OOB, return false
if index > len(m.SubMenu.Items) {
return false
}
// Save parent reference
target.parent = m
// If index is last item, then just regular append
if index == len(m.SubMenu.Items) {
m.SubMenu.Items = append(m.SubMenu.Items, target)
return true
}
m.SubMenu.Items = append(m.SubMenu.Items[:index+1], m.SubMenu.Items[index:]...)
m.SubMenu.Items[index] = target
return true
}
func (m *MenuItem) SetLabel(name string) {
if m.Label == name {
return
}
m.Label = name
}
func (m *MenuItem) IsSeparator() bool {
return m.Type == SeparatorType
}
func (m *MenuItem) IsCheckbox() bool {
return m.Type == CheckboxType
}
func (m *MenuItem) Disable() *MenuItem {
m.Disabled = true
return m
}
func (m *MenuItem) Enable() *MenuItem {
m.Disabled = false
return m
}
func (m *MenuItem) OnClick(click Callback) *MenuItem {
m.Click = click
return m
}
func (m *MenuItem) SetAccelerator(acc *keys.Accelerator) *MenuItem {
m.Accelerator = acc
return m
}
func (m *MenuItem) SetChecked(value bool) *MenuItem {
m.Checked = value
if m.Type != RadioType {
m.Type = CheckboxType
}
return m
}
func (m *MenuItem) Hide() *MenuItem {
m.Hidden = true
return m
}
func (m *MenuItem) Show() *MenuItem {
m.Hidden = false
return m
}
func (m *MenuItem) IsRadio() bool {
return m.Type == RadioType
}
func Label(label string) *MenuItem {
return &MenuItem{
Type: TextType,
Label: label,
}
}
// Text is a helper to create basic Text menu items
func Text(label string, accelerator *keys.Accelerator, click Callback) *MenuItem {
return &MenuItem{
Label: label,
Type: TextType,
Accelerator: accelerator,
Click: click,
}
}
// Separator provides a menu separator
func Separator() *MenuItem {
return &MenuItem{
Type: SeparatorType,
}
}
// Radio is a helper to create basic Radio menu items with an accelerator
func Radio(label string, selected bool, accelerator *keys.Accelerator, click Callback) *MenuItem {
return &MenuItem{
Label: label,
Type: RadioType,
Checked: selected,
Accelerator: accelerator,
Click: click,
}
}
// Checkbox is a helper to create basic Checkbox menu items
func Checkbox(label string, checked bool, accelerator *keys.Accelerator, click Callback) *MenuItem {
return &MenuItem{
Label: label,
Type: CheckboxType,
Checked: checked,
Accelerator: accelerator,
Click: click,
}
}
// SubMenu is a helper to create Submenus
func SubMenu(label string, menu *Menu) *MenuItem {
result := &MenuItem{
Label: label,
SubMenu: menu,
Type: SubmenuType,
}
menu.setParent(result)
return result
}

View File

@@ -0,0 +1,214 @@
// Package menu provides all the functions and structs related to menus in a Wails application.
// Heavily inspired by Electron (c) 2013-2020 Github Inc.
// Electron License: https://github.com/electron/electron/blob/master/LICENSE
package menu
// Role is a type to identify menu roles
type Role int
// These constants need to be kept in sync with `v2/internal/frontend/desktop/darwin/Role.h`
const (
AppMenuRole Role = 1
EditMenuRole = 2
WindowMenuRole = 3
// AboutRole Role = "about"
// UndoRole Role = "undo"
// RedoRole Role = "redo"
// CutRole Role = "cut"
// CopyRole Role = "copy"
// PasteRole Role = "paste"
// PasteAndMatchStyleRole Role = "pasteAndMatchStyle"
// SelectAllRole Role = "selectAll"
// DeleteRole Role = "delete"
// MinimizeRole Role = "minimize"
// QuitRole Role = "quit"
// TogglefullscreenRole Role = "togglefullscreen"
// FileMenuRole Role = "fileMenu"
// ViewMenuRole Role = "viewMenu"
// WindowMenuRole Role = "windowMenu"
// HideRole Role = "hide"
// HideOthersRole Role = "hideOthers"
// UnhideRole Role = "unhide"
// FrontRole Role = "front"
// ZoomRole Role = "zoom"
// WindowSubMenuRole Role = "windowSubMenu"
// HelpSubMenuRole Role = "helpSubMenu"
// SeparatorItemRole Role = "separatorItem"
)
/*
// About provides a MenuItem with the About role
func About() *MenuItem {
return &MenuItem{
Role: AboutRole,
}
}
// Undo provides a MenuItem with the Undo role
func Undo() *MenuItem {
return &MenuItem{
Role: UndoRole,
}
}
// Redo provides a MenuItem with the Redo role
func Redo() *MenuItem {
return &MenuItem{
Role: RedoRole,
}
}
// Cut provides a MenuItem with the Cut role
func Cut() *MenuItem {
return &MenuItem{
Role: CutRole,
}
}
// Copy provides a MenuItem with the Copy role
func Copy() *MenuItem {
return &MenuItem{
Role: CopyRole,
}
}
// Paste provides a MenuItem with the Paste role
func Paste() *MenuItem {
return &MenuItem{
Role: PasteRole,
}
}
// PasteAndMatchStyle provides a MenuItem with the PasteAndMatchStyle role
func PasteAndMatchStyle() *MenuItem {
return &MenuItem{
Role: PasteAndMatchStyleRole,
}
}
// SelectAll provides a MenuItem with the SelectAll role
func SelectAll() *MenuItem {
return &MenuItem{
Role: SelectAllRole,
}
}
// Delete provides a MenuItem with the Delete role
func Delete() *MenuItem {
return &MenuItem{
Role: DeleteRole,
}
}
// Minimize provides a MenuItem with the Minimize role
func Minimize() *MenuItem {
return &MenuItem{
Role: MinimizeRole,
}
}
// Quit provides a MenuItem with the Quit role
func Quit() *MenuItem {
return &MenuItem{
Role: QuitRole,
}
}
// ToggleFullscreen provides a MenuItem with the ToggleFullscreen role
func ToggleFullscreen() *MenuItem {
return &MenuItem{
Role: TogglefullscreenRole,
}
}
// FileMenu provides a MenuItem with the whole default "File" menu (Close / Quit)
func FileMenu() *MenuItem {
return &MenuItem{
Role: FileMenuRole,
}
}
*/
// EditMenu provides a MenuItem with the whole default "Edit" menu (Undo, Copy, etc.).
func EditMenu() *MenuItem {
return &MenuItem{
Role: EditMenuRole,
}
}
/*
// ViewMenu provides a MenuItem with the whole default "View" menu (Reload, Toggle Developer Tools, etc.)
func ViewMenu() *MenuItem {
return &MenuItem{
Role: ViewMenuRole,
}
}
*/
// WindowMenu provides a MenuItem with the whole default "Window" menu (Minimize, Zoom, etc.).
// On MacOS currently all options in there won't work if the window is frameless.
func WindowMenu() *MenuItem {
return &MenuItem{
Role: WindowMenuRole,
}
}
// These roles are Mac only
// AppMenu provides a MenuItem with the whole default "App" menu (About, Services, etc.)
func AppMenu() *MenuItem {
return &MenuItem{
Role: AppMenuRole,
}
}
/*
// Hide provides a MenuItem that maps to the hide action.
func Hide() *MenuItem {
return &MenuItem{
Role: HideRole,
}
}
// HideOthers provides a MenuItem that maps to the hideOtherApplications action.
func HideOthers() *MenuItem {
return &MenuItem{
Role: HideOthersRole,
}
}
// UnHide provides a MenuItem that maps to the unHideAllApplications action.
func UnHide() *MenuItem {
return &MenuItem{
Role: UnhideRole,
}
}
// Front provides a MenuItem that maps to the arrangeInFront action.
func Front() *MenuItem {
return &MenuItem{
Role: FrontRole,
}
}
// Zoom provides a MenuItem that maps to the performZoom action.
func Zoom() *MenuItem {
return &MenuItem{
Role: ZoomRole,
}
}
// WindowSubMenu provides a MenuItem with the "Window" submenu.
func WindowSubMenu() *MenuItem {
return &MenuItem{
Role: WindowSubMenuRole,
}
}
// HelpSubMenu provides a MenuItem with the "Help" submenu.
func HelpSubMenu() *MenuItem {
return &MenuItem{
Role: HelpSubMenuRole,
}
}
*/

View File

@@ -0,0 +1,261 @@
package menu
import (
"fmt"
"strconv"
"strings"
)
type TextStyle int
const (
Bold TextStyle = 1 << 0
Faint TextStyle = 1 << 1
Italic TextStyle = 1 << 2
Blinking TextStyle = 1 << 3
Inversed TextStyle = 1 << 4
Invisible TextStyle = 1 << 5
Underlined TextStyle = 1 << 6
Strikethrough TextStyle = 1 << 7
)
type StyledText struct {
Label string
FgCol *Col
BgCol *Col
Style TextStyle
}
func (s *StyledText) Bold() bool {
return s.Style&Bold == Bold
}
func (s *StyledText) Faint() bool {
return s.Style&Faint == Faint
}
func (s *StyledText) Italic() bool {
return s.Style&Italic == Italic
}
func (s *StyledText) Blinking() bool {
return s.Style&Blinking == Blinking
}
func (s *StyledText) Inversed() bool {
return s.Style&Inversed == Inversed
}
func (s *StyledText) Invisible() bool {
return s.Style&Invisible == Invisible
}
func (s *StyledText) Underlined() bool {
return s.Style&Underlined == Underlined
}
func (s *StyledText) Strikethrough() bool {
return s.Style&Strikethrough == Strikethrough
}
var ansiColorMap = map[string]map[string]*Col{
"Normal": {
"30": Cols[0],
"31": Cols[1],
"32": Cols[2],
"33": Cols[3],
"34": Cols[4],
"35": Cols[5],
"36": Cols[6],
"37": Cols[7],
},
"Bold": {
"30": Cols[8],
"31": Cols[9],
"32": Cols[10],
"33": Cols[11],
"34": Cols[12],
"35": Cols[13],
"36": Cols[14],
"37": Cols[15],
},
"Faint": {
"30": Cols[0],
"31": Cols[1],
"32": Cols[2],
"33": Cols[3],
"34": Cols[4],
"35": Cols[5],
"36": Cols[6],
"37": Cols[7],
},
}
func ParseANSI(input string) ([]*StyledText, error) {
var result []*StyledText
invalid := fmt.Errorf("invalid ansi string")
missingTerminator := fmt.Errorf("missing escape terminator 'm'")
invalidTrueColorSequence := fmt.Errorf("invalid TrueColor sequence")
invalid256ColSequence := fmt.Errorf("invalid 256 colour sequence")
index := 0
var currentStyledText *StyledText = &StyledText{}
if len(input) == 0 {
return nil, invalid
}
for {
// Read all chars to next escape code
esc := strings.Index(input, "\033[")
// If no more esc chars, save what's left and return
if esc == -1 {
text := input[index:]
if len(text) > 0 {
currentStyledText.Label = text
result = append(result, currentStyledText)
}
return result, nil
}
label := input[:esc]
if len(label) > 0 {
currentStyledText.Label = label
result = append(result, currentStyledText)
currentStyledText = &StyledText{
Label: "",
FgCol: currentStyledText.FgCol,
BgCol: currentStyledText.BgCol,
Style: currentStyledText.Style,
}
}
input = input[esc:]
// skip
input = input[2:]
// Read in params
endesc := strings.Index(input, "m")
if endesc == -1 {
return nil, missingTerminator
}
paramText := input[:endesc]
input = input[endesc+1:]
params := strings.Split(paramText, ";")
colourMap := ansiColorMap["Normal"]
skip := 0
for index, param := range params {
if skip > 0 {
skip--
continue
}
switch param {
case "0":
// Reset styles
if len(params) == 1 {
if len(currentStyledText.Label) > 0 {
result = append(result, currentStyledText)
currentStyledText = &StyledText{
Label: "",
FgCol: currentStyledText.FgCol,
BgCol: currentStyledText.BgCol,
Style: currentStyledText.Style,
}
continue
}
}
currentStyledText.Style = 0
currentStyledText.FgCol = nil
currentStyledText.BgCol = nil
case "1":
// Bold
colourMap = ansiColorMap["Bold"]
currentStyledText.Style |= Bold
case "2":
// Dim/Feint
colourMap = ansiColorMap["Faint"]
currentStyledText.Style |= Faint
case "3":
// Italic
currentStyledText.Style |= Italic
case "4":
// Underlined
currentStyledText.Style |= Underlined
case "5":
// Blinking
currentStyledText.Style |= Blinking
case "7":
// Inverse
currentStyledText.Style |= Inversed
case "8":
// Invisible
currentStyledText.Style |= Invisible
case "9":
// Strikethrough
currentStyledText.Style |= Strikethrough
case "30", "31", "32", "33", "34", "35", "36", "37":
currentStyledText.FgCol = colourMap[param]
case "40", "41", "42", "43", "44", "45", "46", "47":
currentStyledText.BgCol = colourMap[param]
case "38", "48":
if len(params)-index < 2 {
return nil, invalid
}
// 256 colours
if params[index+1] == "5" {
skip = 2
colIndexText := params[index+2]
colIndex, err := strconv.Atoi(colIndexText)
if err != nil {
return nil, invalid256ColSequence
}
if colIndex < 0 || colIndex > 255 {
return nil, invalid256ColSequence
}
if param == "38" {
currentStyledText.FgCol = Cols[colIndex]
continue
}
currentStyledText.BgCol = Cols[colIndex]
continue
}
// we must have 4 params left
if len(params)-index < 4 {
return nil, invalidTrueColorSequence
}
if params[index+1] != "2" {
return nil, invalidTrueColorSequence
}
var r, g, b uint8
ri, err := strconv.Atoi(params[index+2])
if err != nil {
return nil, invalidTrueColorSequence
}
gi, err := strconv.Atoi(params[index+3])
if err != nil {
return nil, invalidTrueColorSequence
}
bi, err := strconv.Atoi(params[index+4])
if err != nil {
return nil, invalidTrueColorSequence
}
if bi > 255 || gi > 255 || ri > 255 {
return nil, invalidTrueColorSequence
}
if bi < 0 || gi < 0 || ri < 0 {
return nil, invalidTrueColorSequence
}
r = uint8(ri)
g = uint8(gi)
b = uint8(bi)
skip = 4
colvalue := fmt.Sprintf("#%02x%02x%02x", r, g, b)
if param == "38" {
currentStyledText.FgCol = &Col{Hex: colvalue, Rgb: Rgb{r, g, b}}
continue
}
currentStyledText.BgCol = &Col{Hex: colvalue}
default:
return nil, invalid
}
}
}
}

42
vendor/github.com/wailsapp/wails/v2/pkg/menu/tray.go generated vendored Normal file
View File

@@ -0,0 +1,42 @@
package menu
// TrayMenu are the options
type TrayMenu struct {
// Label is the text we wish to display in the tray
Label string
// Image is the name of the tray icon we wish to display.
// These are read up during build from <projectdir>/trayicons and
// the filenames are used as IDs, minus the extension
// EG: <projectdir>/trayicons/main.png can be referenced here with "main"
// If the image is not a filename, it will be treated as base64 image data
Image string
// MacTemplateImage indicates that on a Mac, this image is a template image
MacTemplateImage bool
// Text Colour
RGBA string
// Font
FontSize int
FontName string
// Tooltip
Tooltip string
// Callback function when menu clicked
// Click Callback `json:"-"`
// Disabled makes the item unselectable
Disabled bool
// Menu is the initial menu we wish to use for the tray
Menu *Menu
// OnOpen is called when the Menu is opened
OnOpen func()
// OnClose is called when the Menu is closed
OnClose func()
}

17
vendor/github.com/wailsapp/wails/v2/pkg/menu/type.go generated vendored Normal file
View File

@@ -0,0 +1,17 @@
package menu
// Type of the menu item
type Type string
const (
// TextType is the text menuitem type
TextType Type = "Text"
// SeparatorType is the Separator menuitem type
SeparatorType Type = "Separator"
// SubmenuType is the Submenu menuitem type
SubmenuType Type = "Submenu"
// CheckboxType is the Checkbox menuitem type
CheckboxType Type = "Checkbox"
// RadioType is the Radio menuitem type
RadioType Type = "Radio"
)

View File

@@ -0,0 +1,13 @@
package menu
/*
// DefaultWindowsMenu returns a default menu including the default
// Application and Edit menus. Use `.Append()` to add to it.
func DefaultWindowsMenu() *Menu {
return NewMenuFromItems(
FileMenu(),
EditMenu(),
WindowMenu(),
)
}
*/

View File

@@ -0,0 +1,20 @@
package assetserver
import (
"net/http"
)
// Middleware defines a HTTP middleware that can be applied to the AssetServer.
// The handler passed as next is the next handler in the chain. One can decide to call the next handler
// or implement a specialized handling.
type Middleware func(next http.Handler) http.Handler
// ChainMiddleware allows chaining multiple middlewares to one middleware.
func ChainMiddleware(middleware ...Middleware) Middleware {
return func(h http.Handler) http.Handler {
for i := len(middleware) - 1; i >= 0; i-- {
h = middleware[i](h)
}
return h
}
}

View File

@@ -0,0 +1,45 @@
package assetserver
import (
"fmt"
"io/fs"
"net/http"
)
// Options defines the configuration of the AssetServer.
type Options struct {
// Assets defines the static assets to be used. A GET request is first tried to be served from this Assets. If the Assets returns
// `os.ErrNotExist` for that file, the request handling will fallback to the Handler and tries to serve the GET
// request from it.
//
// If set to nil, all GET requests will be forwarded to Handler.
Assets fs.FS
// Handler will be called for every GET request that can't be served from Assets, due to `os.ErrNotExist`. Furthermore all
// non GET requests will always be served from this Handler.
//
// If not defined, the result is the following in cases where the Handler would have been called:
// GET request: `http.StatusNotFound`
// Other request: `http.StatusMethodNotAllowed`
Handler http.Handler
// Middleware is a HTTP Middleware which allows to hook into the AssetServer request chain. It allows to skip the default
// request handler dynamically, e.g. implement specialized Routing etc.
// The Middleware is called to build a new `http.Handler` used by the AssetSever and it also receives the default
// handler used by the AssetServer as an argument.
//
// If not defined, the default AssetServer request chain is executed.
//
// Multiple Middlewares can be chained together with:
// ChainMiddleware(middleware ...Middleware) Middleware
Middleware Middleware
}
// Validate the options
func (o Options) Validate() error {
if o.Assets == nil && o.Handler == nil && o.Middleware == nil {
return fmt.Errorf("AssetServer options invalid: either Assets, Handler or Middleware must be set")
}
return nil
}

View File

@@ -0,0 +1,7 @@
package options
// Debug options which are taken into account in debug builds.
type Debug struct {
// OpenInspectorOnStartup opens the inspector on startup of the app.
OpenInspectorOnStartup bool
}

View File

@@ -0,0 +1,56 @@
package linux
// WebviewGpuPolicy values used for determining the webview's hardware acceleration policy.
type WebviewGpuPolicy int
const (
// WebviewGpuPolicyAlways Hardware acceleration is always enabled.
WebviewGpuPolicyAlways WebviewGpuPolicy = iota
// WebviewGpuPolicyOnDemand Hardware acceleration is enabled/disabled as request by web contents.
WebviewGpuPolicyOnDemand
// WebviewGpuPolicyNever Hardware acceleration is always disabled.
WebviewGpuPolicyNever
)
// Options specific to Linux builds
type Options struct {
// Icon Sets up the icon representing the window. This icon is used when the window is minimized
// (also known as iconified).
Icon []byte
// WindowIsTranslucent sets the window's background to transparent when enabled.
WindowIsTranslucent bool
// Messages are messages that can be customised
Messages *Messages
// WebviewGpuPolicy used for determining the hardware acceleration policy for the webview.
// - WebviewGpuPolicyAlways
// - WebviewGpuPolicyOnDemand
// - WebviewGpuPolicyNever
//
// Due to https://github.com/wailsapp/wails/issues/2977, if options.Linux is nil
// in the call to wails.Run(), WebviewGpuPolicy is set by default to WebviewGpuPolicyNever.
// Client code may override this behavior by passing a non-nil Options and set
// WebviewGpuPolicy as needed.
WebviewGpuPolicy WebviewGpuPolicy
// ProgramName is used to set the program's name for the window manager via GTK's g_set_prgname().
//This name should not be localized. [see the docs]
//
//When a .desktop file is created this value helps with window grouping and desktop icons when the .desktop file's Name
//property differs form the executable's filename.
//
//[see the docs]: https://docs.gtk.org/glib/func.set_prgname.html
ProgramName string
}
type Messages struct {
WebKit2GTKMinRequired string
}
func DefaultMessages() *Messages {
return &Messages{
WebKit2GTKMinRequired: "This application requires at least WebKit2GTK %s to be installed.",
}
}

View File

@@ -0,0 +1,23 @@
package mac
// AppearanceType is a type of Appearance for Cocoa windows
type AppearanceType string
const (
// DefaultAppearance uses the default system value
DefaultAppearance AppearanceType = ""
// NSAppearanceNameAqua - The standard light system appearance.
NSAppearanceNameAqua AppearanceType = "NSAppearanceNameAqua"
// NSAppearanceNameDarkAqua - The standard dark system appearance.
NSAppearanceNameDarkAqua AppearanceType = "NSAppearanceNameDarkAqua"
// NSAppearanceNameVibrantLight - The light vibrant appearance
NSAppearanceNameVibrantLight AppearanceType = "NSAppearanceNameVibrantLight"
// NSAppearanceNameAccessibilityHighContrastAqua - A high-contrast version of the standard light system appearance.
NSAppearanceNameAccessibilityHighContrastAqua AppearanceType = "NSAppearanceNameAccessibilityHighContrastAqua"
// NSAppearanceNameAccessibilityHighContrastDarkAqua - A high-contrast version of the standard dark system appearance.
NSAppearanceNameAccessibilityHighContrastDarkAqua AppearanceType = "NSAppearanceNameAccessibilityHighContrastDarkAqua"
// NSAppearanceNameAccessibilityHighContrastVibrantLight - A high-contrast version of the light vibrant appearance.
NSAppearanceNameAccessibilityHighContrastVibrantLight AppearanceType = "NSAppearanceNameAccessibilityHighContrastVibrantLight"
// NSAppearanceNameAccessibilityHighContrastVibrantDark - A high-contrast version of the dark vibrant appearance.
NSAppearanceNameAccessibilityHighContrastVibrantDark AppearanceType = "NSAppearanceNameAccessibilityHighContrastVibrantDark"
)

View File

@@ -0,0 +1,31 @@
package mac
//type ActivationPolicy int
//
//const (
// NSApplicationActivationPolicyRegular ActivationPolicy = 0
// NSApplicationActivationPolicyAccessory ActivationPolicy = 1
// NSApplicationActivationPolicyProhibited ActivationPolicy = 2
//)
type AboutInfo struct {
Title string
Message string
Icon []byte
}
// Options are options specific to Mac
type Options struct {
TitleBar *TitleBar
Appearance AppearanceType
ContentProtection bool
WebviewIsTransparent bool
WindowIsTranslucent bool
Preferences *Preferences
DisableZoom bool
// ActivationPolicy ActivationPolicy
About *AboutInfo
OnFileOpen func(filePath string) `json:"-"`
OnUrlOpen func(filePath string) `json:"-"`
// URLHandlers map[string]func(string)
}

View File

@@ -0,0 +1,21 @@
package mac
import "github.com/leaanthony/u"
var (
Enabled = u.True
Disabled = u.False
)
// Preferences allows to set webkit preferences
type Preferences struct {
// A Boolean value that indicates whether pressing the tab key changes the focus to links and form controls.
// Set to false by default.
TabFocusesLinks u.Bool
// A Boolean value that indicates whether to allow people to select or otherwise interact with text.
// Set to true by default.
TextInteractionEnabled u.Bool
// A Boolean value that indicates whether a web view can display content full screen.
// Set to false by default
FullscreenEnabled u.Bool
}

View File

@@ -0,0 +1,52 @@
package mac
// TitleBar contains options for the Mac titlebar
type TitleBar struct {
TitlebarAppearsTransparent bool
HideTitle bool
HideTitleBar bool
FullSizeContent bool
UseToolbar bool
HideToolbarSeparator bool
}
// TitleBarDefault results in the default Mac Titlebar
func TitleBarDefault() *TitleBar {
return &TitleBar{
TitlebarAppearsTransparent: false,
HideTitle: false,
HideTitleBar: false,
FullSizeContent: false,
UseToolbar: false,
HideToolbarSeparator: false,
}
}
// Credit: Comments from Electron site
// TitleBarHidden results in a hidden title bar and a full size content window,
// yet the title bar still has the standard window controls (“traffic lights”)
// in the top left.
func TitleBarHidden() *TitleBar {
return &TitleBar{
TitlebarAppearsTransparent: true,
HideTitle: true,
HideTitleBar: false,
FullSizeContent: true,
UseToolbar: false,
HideToolbarSeparator: false,
}
}
// TitleBarHiddenInset results in a hidden title bar with an alternative look where
// the traffic light buttons are slightly more inset from the window edge.
func TitleBarHiddenInset() *TitleBar {
return &TitleBar{
TitlebarAppearsTransparent: true,
HideTitle: true,
HideTitleBar: false,
FullSizeContent: true,
UseToolbar: true,
HideToolbarSeparator: true,
}
}

View File

@@ -0,0 +1,276 @@
package options
import (
"context"
"html"
"io/fs"
"net/http"
"os"
"path/filepath"
"runtime"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/linux"
"github.com/wailsapp/wails/v2/pkg/options/mac"
"github.com/wailsapp/wails/v2/pkg/options/windows"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/logger"
)
type WindowStartState int
const (
Normal WindowStartState = 0
Maximised WindowStartState = 1
Minimised WindowStartState = 2
Fullscreen WindowStartState = 3
)
type Experimental struct{}
// App contains options for creating the App
type App struct {
Title string
Width int
Height int
DisableResize bool
Fullscreen bool
Frameless bool
MinWidth int
MinHeight int
MaxWidth int
MaxHeight int
StartHidden bool
HideWindowOnClose bool
AlwaysOnTop bool
// BackgroundColour is the background colour of the window
// You can use the options.NewRGB and options.NewRGBA functions to create a new colour
BackgroundColour *RGBA
// Deprecated: Use AssetServer.Assets instead.
Assets fs.FS
// Deprecated: Use AssetServer.Handler instead.
AssetsHandler http.Handler
// AssetServer configures the Assets for the application
AssetServer *assetserver.Options
Menu *menu.Menu
Logger logger.Logger `json:"-"`
LogLevel logger.LogLevel
LogLevelProduction logger.LogLevel
OnStartup func(ctx context.Context) `json:"-"`
OnDomReady func(ctx context.Context) `json:"-"`
OnShutdown func(ctx context.Context) `json:"-"`
OnBeforeClose func(ctx context.Context) (prevent bool) `json:"-"`
Bind []interface{}
EnumBind []interface{}
WindowStartState WindowStartState
// ErrorFormatter overrides the formatting of errors returned by backend methods
ErrorFormatter ErrorFormatter
// CSS property to test for draggable elements. Default "--wails-draggable"
CSSDragProperty string
// The CSS Value that the CSSDragProperty must have to be draggable, EG: "drag"
CSSDragValue string
// EnableDefaultContextMenu enables the browser's default context-menu in production
// This menu is already enabled in development and debug builds
EnableDefaultContextMenu bool
// EnableFraudulentWebsiteDetection enables scan services for fraudulent content, such as malware or phishing attempts.
// These services might send information from your app like URLs navigated to and possibly other content to cloud
// services of Apple and Microsoft.
EnableFraudulentWebsiteDetection bool
SingleInstanceLock *SingleInstanceLock
Windows *windows.Options
Mac *mac.Options
Linux *linux.Options
// Experimental options
Experimental *Experimental
// Debug options for debug builds. These options will be ignored in a production build.
Debug Debug
// DragAndDrop options for drag and drop behavior
DragAndDrop *DragAndDrop
// DisablePanicRecovery disables the panic recovery system in messages processing
DisablePanicRecovery bool
// List of additional allowed origins for bindings in format "https://*.myapp.com,https://example.com"
BindingsAllowedOrigins string
}
type ErrorFormatter func(error) any
type RGBA struct {
R uint8 `json:"r"`
G uint8 `json:"g"`
B uint8 `json:"b"`
A uint8 `json:"a"`
}
// NewRGBA creates a new RGBA struct with the given values
func NewRGBA(r, g, b, a uint8) *RGBA {
return &RGBA{
R: r,
G: g,
B: b,
A: a,
}
}
// NewRGB creates a new RGBA struct with the given values and Alpha set to 255
func NewRGB(r, g, b uint8) *RGBA {
return &RGBA{
R: r,
G: g,
B: b,
A: 255,
}
}
// MergeDefaults will set the minimum default values for an application
func MergeDefaults(appoptions *App) {
// Do set defaults
if appoptions.Width <= 0 {
appoptions.Width = 1024
}
if appoptions.Height <= 0 {
appoptions.Height = 768
}
if appoptions.Logger == nil {
appoptions.Logger = logger.NewDefaultLogger()
}
if appoptions.LogLevel == 0 {
appoptions.LogLevel = logger.INFO
}
if appoptions.LogLevelProduction == 0 {
appoptions.LogLevelProduction = logger.ERROR
}
if appoptions.CSSDragProperty == "" {
appoptions.CSSDragProperty = "--wails-draggable"
}
if appoptions.CSSDragValue == "" {
appoptions.CSSDragValue = "drag"
}
if appoptions.DragAndDrop == nil {
appoptions.DragAndDrop = &DragAndDrop{}
}
if appoptions.DragAndDrop.CSSDropProperty == "" {
appoptions.DragAndDrop.CSSDropProperty = "--wails-drop-target"
}
if appoptions.DragAndDrop.CSSDropValue == "" {
appoptions.DragAndDrop.CSSDropValue = "drop"
}
if appoptions.BackgroundColour == nil {
appoptions.BackgroundColour = &RGBA{
R: 255,
G: 255,
B: 255,
A: 255,
}
}
// Ensure max and min are valid
processMinMaxConstraints(appoptions)
// Default menus
processMenus(appoptions)
// Process Drag Options
processDragOptions(appoptions)
}
type SingleInstanceLock struct {
// uniqueId that will be used for setting up messaging between instances
UniqueId string
OnSecondInstanceLaunch func(secondInstanceData SecondInstanceData)
}
type SecondInstanceData struct {
Args []string
WorkingDirectory string
}
type DragAndDrop struct {
// EnableFileDrop enables wails' drag and drop functionality that returns the dropped in files' absolute paths.
EnableFileDrop bool
// Disable webview's drag and drop functionality.
//
// It can be used to prevent accidental file opening of dragged in files in the webview, when there is no need for drag and drop.
DisableWebViewDrop bool
// CSS property to test for drag and drop target elements. Default "--wails-drop-target"
CSSDropProperty string
// The CSS Value that the CSSDropProperty must have to be a valid drop target. Default "drop"
CSSDropValue string
}
func NewSecondInstanceData() (*SecondInstanceData, error) {
ex, err := os.Executable()
if err != nil {
return nil, err
}
workingDirectory := filepath.Dir(ex)
return &SecondInstanceData{
Args: os.Args[1:],
WorkingDirectory: workingDirectory,
}, nil
}
func processMenus(appoptions *App) {
switch runtime.GOOS {
case "darwin":
if appoptions.Menu == nil {
items := []*menu.MenuItem{
menu.EditMenu(),
}
if !appoptions.Frameless {
items = append(items, menu.WindowMenu()) // Current options in Window Menu only work if not frameless
}
appoptions.Menu = menu.NewMenuFromItems(menu.AppMenu(), items...)
}
}
}
func processMinMaxConstraints(appoptions *App) {
if appoptions.MinWidth > 0 && appoptions.MaxWidth > 0 {
if appoptions.MinWidth > appoptions.MaxWidth {
appoptions.MinWidth = appoptions.MaxWidth
}
}
if appoptions.MinHeight > 0 && appoptions.MaxHeight > 0 {
if appoptions.MinHeight > appoptions.MaxHeight {
appoptions.MinHeight = appoptions.MaxHeight
}
}
// Ensure width and height are limited if max/min is set
if appoptions.Width < appoptions.MinWidth {
appoptions.Width = appoptions.MinWidth
}
if appoptions.MaxWidth > 0 && appoptions.Width > appoptions.MaxWidth {
appoptions.Width = appoptions.MaxWidth
}
if appoptions.Height < appoptions.MinHeight {
appoptions.Height = appoptions.MinHeight
}
if appoptions.MaxHeight > 0 && appoptions.Height > appoptions.MaxHeight {
appoptions.Height = appoptions.MaxHeight
}
}
func processDragOptions(appoptions *App) {
appoptions.CSSDragProperty = html.EscapeString(appoptions.CSSDragProperty)
appoptions.CSSDragValue = html.EscapeString(appoptions.CSSDragValue)
}

View File

@@ -0,0 +1,167 @@
package windows
type Theme int
type Messages struct {
InstallationRequired string
UpdateRequired string
MissingRequirements string
Webview2NotInstalled string
Error string
FailedToInstall string
DownloadPage string
PressOKToInstall string
ContactAdmin string
InvalidFixedWebview2 string
WebView2ProcessCrash string
}
const (
// SystemDefault will use whatever the system theme is. The application will follow system theme changes.
SystemDefault Theme = 0
// Dark Mode
Dark Theme = 1
// Light Mode
Light Theme = 2
)
type BackdropType int32
const (
Auto BackdropType = 0
None BackdropType = 1
Mica BackdropType = 2
Acrylic BackdropType = 3
Tabbed BackdropType = 4
)
const (
// Default is 0, which means no changes to the default Windows DLL search behavior
DLLSearchDefault uint32 = 0
// LoadLibrary flags for determining from where to search for a DLL
DLLSearchDontResolveDllReferences uint32 = 0x1 // windows.DONT_RESOLVE_DLL_REFERENCES
DLLSearchAsDataFile uint32 = 0x2 // windows.LOAD_LIBRARY_AS_DATAFILE
DLLSearchWithAlteredPath uint32 = 0x8 // windows.LOAD_WITH_ALTERED_SEARCH_PATH
DLLSearchIgnoreCodeAuthzLevel uint32 = 0x10 // windows.LOAD_IGNORE_CODE_AUTHZ_LEVEL
DLLSearchAsImageResource uint32 = 0x20 // windows.LOAD_LIBRARY_AS_IMAGE_RESOURCE
DLLSearchAsDataFileExclusive uint32 = 0x40 // windows.LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE
DLLSearchRequireSignedTarget uint32 = 0x80 // windows.LOAD_LIBRARY_REQUIRE_SIGNED_TARGET
DLLSearchDllLoadDir uint32 = 0x100 // windows.LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR
DLLSearchApplicationDir uint32 = 0x200 // windows.LOAD_LIBRARY_SEARCH_APPLICATION_DIR
DLLSearchUserDirs uint32 = 0x400 // windows.LOAD_LIBRARY_SEARCH_USER_DIRS
DLLSearchSystem32 uint32 = 0x800 // windows.LOAD_LIBRARY_SEARCH_SYSTEM32
DLLSearchDefaultDirs uint32 = 0x1000 // windows.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
DLLSearchSafeCurrentDirs uint32 = 0x2000 // windows.LOAD_LIBRARY_SAFE_CURRENT_DIRS
DLLSearchSystem32NoForwarder uint32 = 0x4000 // windows.LOAD_LIBRARY_SEARCH_SYSTEM32_NO_FORWARDER
DLLSearchOsIntegrityContinuity uint32 = 0x8000 // windows.LOAD_LIBRARY_OS_INTEGRITY_CONTINUITY
)
func RGB(r, g, b uint8) int32 {
col := int32(b)
col = col<<8 | int32(g)
col = col<<8 | int32(r)
return col
}
// ThemeSettings contains optional colours to use.
// They may be set using the hex values: 0x00BBGGRR
type ThemeSettings struct {
DarkModeTitleBar int32
DarkModeTitleBarInactive int32
DarkModeTitleText int32
DarkModeTitleTextInactive int32
DarkModeBorder int32
DarkModeBorderInactive int32
LightModeTitleBar int32
LightModeTitleBarInactive int32
LightModeTitleText int32
LightModeTitleTextInactive int32
LightModeBorder int32
LightModeBorderInactive int32
}
// Options are options specific to Windows
type Options struct {
ContentProtection bool
WebviewIsTransparent bool
WindowIsTranslucent bool
DisableWindowIcon bool
IsZoomControlEnabled bool
ZoomFactor float64
DisablePinchZoom bool
// Disable all window decorations in Frameless mode, which means no "Aero Shadow" and no "Rounded Corner" will be shown.
// "Rounded Corners" are only available on Windows 11.
DisableFramelessWindowDecorations bool
// Path where the WebView2 stores the user data. If empty %APPDATA%\[BinaryName.exe] will be used.
// If the path is not valid, a messagebox will be displayed with the error and the app will exit with error code.
WebviewUserDataPath string
// Path to the directory with WebView2 executables. If empty WebView2 installed in the system will be used.
WebviewBrowserPath string
// Dark/Light or System Default Theme
Theme Theme
// Custom settings for dark/light mode
CustomTheme *ThemeSettings
// Select the type of translucent backdrop. Requires Windows 11 22621 or later.
BackdropType BackdropType
// User messages that can be customised
Messages *Messages
// ResizeDebounceMS is the amount of time to debounce redraws of webview2
// when resizing the window
ResizeDebounceMS uint16
// OnSuspend is called when Windows enters low power mode
OnSuspend func()
// OnResume is called when Windows resumes from low power mode
OnResume func()
// WebviewGpuIsDisabled is used to enable / disable GPU acceleration for the webview
WebviewGpuIsDisabled bool
// WebviewDisableRendererCodeIntegrity disables the `RendererCodeIntegrity` of WebView2. Some Security Endpoint
// Protection Software inject themself into the WebView2 with unsigned or wrongly signed dlls, which is not allowed
// and will stop the WebView2 processes. Those security software need an update to fix this issue or one can disable
// the integrity check with this flag.
//
// The event viewer log contains `Code Integrity Errors` like mentioned here: https://github.com/MicrosoftEdge/WebView2Feedback/issues/2051
//
// !! Please keep in mind when disabling this feature, this also allows malicious software to inject into the WebView2 !!
WebviewDisableRendererCodeIntegrity bool
// Configure whether swipe gestures should be enabled
EnableSwipeGestures bool
// Class name for the window. If empty, 'wailsWindow' will be used.
WindowClassName string
// DLLSearchPaths controls which directories are searched when loading DLLs
// Set to 0 for default behavior, or combine multiple flags with bitwise OR
// Example: DLLSearchApplicationDir | DLLSearchSystem32
DLLSearchPaths uint32
}
func DefaultMessages() *Messages {
return &Messages{
InstallationRequired: "The WebView2 runtime is required. Press Ok to download and install. Note: The installer will download silently so please wait.",
UpdateRequired: "The WebView2 runtime needs updating. Press Ok to download and install. Note: The installer will download silently so please wait.",
MissingRequirements: "Missing Requirements",
Webview2NotInstalled: "WebView2 runtime not installed",
Error: "Error",
FailedToInstall: "The runtime failed to install correctly. Please try again.",
DownloadPage: "This application requires the WebView2 runtime. Press OK to open the download page. Minimum version required: ",
PressOKToInstall: "Press Ok to install.",
ContactAdmin: "The WebView2 runtime is required to run this application. Please contact your system administrator.",
InvalidFixedWebview2: "The WebView2 runtime is manually specified, but It is not valid. Check minimum required version and webview2 path.",
WebView2ProcessCrash: "The WebView2 process crashed and the application needs to be restarted.",
}
}

View File

@@ -0,0 +1,11 @@
package runtime
import (
"context"
)
// BrowserOpenURL uses the system default browser to open the url
func BrowserOpenURL(ctx context.Context, url string) {
appFrontend := getFrontend(ctx)
appFrontend.BrowserOpenURL(url)
}

View File

@@ -0,0 +1,13 @@
package runtime
import "context"
func ClipboardGetText(ctx context.Context) (string, error) {
appFrontend := getFrontend(ctx)
return appFrontend.ClipboardGetText()
}
func ClipboardSetText(ctx context.Context, text string) error {
appFrontend := getFrontend(ctx)
return appFrontend.ClipboardSetText(text)
}

View File

@@ -0,0 +1,80 @@
package runtime
import (
"context"
"fmt"
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/internal/fs"
)
// FileFilter defines a filter for dialog boxes
type FileFilter = frontend.FileFilter
// OpenDialogOptions contains the options for the OpenDialogOptions runtime method
type OpenDialogOptions = frontend.OpenDialogOptions
// SaveDialogOptions contains the options for the SaveDialog runtime method
type SaveDialogOptions = frontend.SaveDialogOptions
type DialogType = frontend.DialogType
const (
InfoDialog = frontend.InfoDialog
WarningDialog = frontend.WarningDialog
ErrorDialog = frontend.ErrorDialog
QuestionDialog = frontend.QuestionDialog
)
// MessageDialogOptions contains the options for the Message dialogs, EG Info, Warning, etc runtime methods
type MessageDialogOptions = frontend.MessageDialogOptions
// OpenDirectoryDialog prompts the user to select a directory
func OpenDirectoryDialog(ctx context.Context, dialogOptions OpenDialogOptions) (string, error) {
appFrontend := getFrontend(ctx)
if dialogOptions.DefaultDirectory != "" {
if !fs.DirExists(dialogOptions.DefaultDirectory) {
return "", fmt.Errorf("default directory '%s' does not exist", dialogOptions.DefaultDirectory)
}
}
return appFrontend.OpenDirectoryDialog(dialogOptions)
}
// OpenFileDialog prompts the user to select a file
func OpenFileDialog(ctx context.Context, dialogOptions OpenDialogOptions) (string, error) {
appFrontend := getFrontend(ctx)
if dialogOptions.DefaultDirectory != "" {
if !fs.DirExists(dialogOptions.DefaultDirectory) {
return "", fmt.Errorf("default directory '%s' does not exist", dialogOptions.DefaultDirectory)
}
}
return appFrontend.OpenFileDialog(dialogOptions)
}
// OpenMultipleFilesDialog prompts the user to select a file
func OpenMultipleFilesDialog(ctx context.Context, dialogOptions OpenDialogOptions) ([]string, error) {
appFrontend := getFrontend(ctx)
if dialogOptions.DefaultDirectory != "" {
if !fs.DirExists(dialogOptions.DefaultDirectory) {
return nil, fmt.Errorf("default directory '%s' does not exist", dialogOptions.DefaultDirectory)
}
}
return appFrontend.OpenMultipleFilesDialog(dialogOptions)
}
// SaveFileDialog prompts the user to select a file
func SaveFileDialog(ctx context.Context, dialogOptions SaveDialogOptions) (string, error) {
appFrontend := getFrontend(ctx)
if dialogOptions.DefaultDirectory != "" {
if !fs.DirExists(dialogOptions.DefaultDirectory) {
return "", fmt.Errorf("default directory '%s' does not exist", dialogOptions.DefaultDirectory)
}
}
return appFrontend.SaveFileDialog(dialogOptions)
}
// MessageDialog show a message dialog to the user
func MessageDialog(ctx context.Context, dialogOptions MessageDialogOptions) (string, error) {
appFrontend := getFrontend(ctx)
return appFrontend.MessageDialog(dialogOptions)
}

View File

@@ -0,0 +1,37 @@
package runtime
import (
"context"
"fmt"
)
// OnFileDrop returns a slice of file path strings when a drop is finished.
func OnFileDrop(ctx context.Context, callback func(x, y int, paths []string)) {
if callback == nil {
LogError(ctx, "OnFileDrop called with a nil callback")
return
}
EventsOn(ctx, "wails:file-drop", func(optionalData ...interface{}) {
if len(optionalData) != 3 {
callback(0, 0, nil)
}
x, ok := optionalData[0].(int)
if !ok {
LogError(ctx, fmt.Sprintf("invalid x coordinate in drag and drop: %v", optionalData[0]))
}
y, ok := optionalData[1].(int)
if !ok {
LogError(ctx, fmt.Sprintf("invalid y coordinate in drag and drop: %v", optionalData[1]))
}
paths, ok := optionalData[2].([]string)
if !ok {
LogError(ctx, fmt.Sprintf("invalid path data in drag and drop: %v", optionalData[2]))
}
callback(x, y, paths)
})
}
// OnFileDropOff removes the drag and drop listeners and handlers.
func OnFileDropOff(ctx context.Context) {
EventsOff(ctx, "wails:file-drop")
}

View File

@@ -0,0 +1,49 @@
package runtime
import (
"context"
)
// EventsOn registers a listener for the given event name. It returns a function to cancel the listener
func EventsOn(ctx context.Context, eventName string, callback func(optionalData ...interface{})) func() {
events := getEvents(ctx)
return events.On(eventName, callback)
}
// EventsOff unregisters a listener for the given event name, optionally multiple listeners can be unregistered via `additionalEventNames`
func EventsOff(ctx context.Context, eventName string, additionalEventNames ...string) {
events := getEvents(ctx)
events.Off(eventName)
if len(additionalEventNames) > 0 {
for _, eventName := range additionalEventNames {
events.Off(eventName)
}
}
}
// EventsOff unregisters a listener for the given event name, optionally multiple listeners can be unregistered via `additionalEventNames`
func EventsOffAll(ctx context.Context) {
events := getEvents(ctx)
events.OffAll()
}
// EventsOnce registers a listener for the given event name. After the first callback, the
// listener is deleted. It returns a function to cancel the listener
func EventsOnce(ctx context.Context, eventName string, callback func(optionalData ...interface{})) func() {
events := getEvents(ctx)
return events.Once(eventName, callback)
}
// EventsOnMultiple registers a listener for the given event name, that may be called a maximum of 'counter' times. It returns a function
// to cancel the listener
func EventsOnMultiple(ctx context.Context, eventName string, callback func(optionalData ...interface{}), counter int) func() {
events := getEvents(ctx)
return events.OnMultiple(eventName, callback, counter)
}
// EventsEmit pass through
func EventsEmit(ctx context.Context, eventName string, optionalData ...interface{}) {
events := getEvents(ctx)
events.Emit(eventName, optionalData...)
}

105
vendor/github.com/wailsapp/wails/v2/pkg/runtime/log.go generated vendored Normal file
View File

@@ -0,0 +1,105 @@
package runtime
import (
"context"
"fmt"
"github.com/wailsapp/wails/v2/pkg/logger"
)
// LogPrint prints a Print level message
func LogPrint(ctx context.Context, message string) {
myLogger := getLogger(ctx)
myLogger.Print(message)
}
// LogTrace prints a Trace level message
func LogTrace(ctx context.Context, message string) {
myLogger := getLogger(ctx)
myLogger.Trace(message)
}
// LogDebug prints a Debug level message
func LogDebug(ctx context.Context, message string) {
myLogger := getLogger(ctx)
myLogger.Debug(message)
}
// LogInfo prints a Info level message
func LogInfo(ctx context.Context, message string) {
myLogger := getLogger(ctx)
myLogger.Info(message)
}
// LogWarning prints a Warning level message
func LogWarning(ctx context.Context, message string) {
myLogger := getLogger(ctx)
myLogger.Warning(message)
}
// LogError prints a Error level message
func LogError(ctx context.Context, message string) {
myLogger := getLogger(ctx)
myLogger.Error(message)
}
// LogFatal prints a Fatal level message
func LogFatal(ctx context.Context, message string) {
myLogger := getLogger(ctx)
myLogger.Fatal(message)
}
// LogPrintf prints a Print level message
func LogPrintf(ctx context.Context, format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
myLogger := getLogger(ctx)
myLogger.Print(msg)
}
// LogTracef prints a Trace level message
func LogTracef(ctx context.Context, format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
myLogger := getLogger(ctx)
myLogger.Trace(msg)
}
// LogDebugf prints a Debug level message
func LogDebugf(ctx context.Context, format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
myLogger := getLogger(ctx)
myLogger.Debug(msg)
}
// LogInfof prints a Info level message
func LogInfof(ctx context.Context, format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
myLogger := getLogger(ctx)
myLogger.Info(msg)
}
// LogWarningf prints a Warning level message
func LogWarningf(ctx context.Context, format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
myLogger := getLogger(ctx)
myLogger.Warning(msg)
}
// LogErrorf prints a Error level message
func LogErrorf(ctx context.Context, format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
myLogger := getLogger(ctx)
myLogger.Error(msg)
}
// LogFatalf prints a Fatal level message
func LogFatalf(ctx context.Context, format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
myLogger := getLogger(ctx)
myLogger.Fatal(msg)
}
// LogSetLogLevel sets the log level
func LogSetLogLevel(ctx context.Context, level logger.LogLevel) {
myLogger := getLogger(ctx)
myLogger.SetLogLevel(level)
}

View File

@@ -0,0 +1,17 @@
package runtime
import (
"context"
"github.com/wailsapp/wails/v2/pkg/menu"
)
func MenuSetApplicationMenu(ctx context.Context, menu *menu.Menu) {
frontend := getFrontend(ctx)
frontend.MenuSetApplicationMenu(menu)
}
func MenuUpdateApplicationMenu(ctx context.Context) {
frontend := getFrontend(ctx)
frontend.MenuUpdateApplicationMenu()
}

View File

@@ -0,0 +1,136 @@
package runtime
import (
"context"
"github.com/wailsapp/wails/v2/internal/frontend"
)
// NotificationOptions contains configuration for a notification.
type NotificationOptions = frontend.NotificationOptions
// NotificationAction represents an action button for a notification.
type NotificationAction = frontend.NotificationAction
// NotificationCategory groups actions for notifications.
type NotificationCategory = frontend.NotificationCategory
// NotificationResponse represents the response sent by interacting with a notification.
type NotificationResponse = frontend.NotificationResponse
// NotificationResult represents the result of a notification response,
// returning the response or any errors that occurred.
type NotificationResult = frontend.NotificationResult
// InitializeNotifications initializes the notification service for the application.
// This must be called before sending any notifications. On macOS, this also ensures
// the notification delegate is properly initialized.
func InitializeNotifications(ctx context.Context) error {
fe := getFrontend(ctx)
return fe.InitializeNotifications()
}
// CleanupNotifications cleans up notification resources and releases any held connections.
// This should be called when shutting down the application to properly release resources
// (primarily needed on Linux to close D-Bus connections).
func CleanupNotifications(ctx context.Context) {
fe := getFrontend(ctx)
fe.CleanupNotifications()
}
// IsNotificationAvailable checks if notifications are available on the current platform.
func IsNotificationAvailable(ctx context.Context) bool {
fe := getFrontend(ctx)
return fe.IsNotificationAvailable()
}
// RequestNotificationAuthorization requests notification authorization from the user.
// On macOS, this prompts the user to allow notifications. On other platforms, this
// always returns true. Returns true if authorization was granted, false otherwise.
func RequestNotificationAuthorization(ctx context.Context) (bool, error) {
fe := getFrontend(ctx)
return fe.RequestNotificationAuthorization()
}
// CheckNotificationAuthorization checks the current notification authorization status.
// On macOS, this checks if the app has notification permissions. On other platforms,
// this always returns true.
func CheckNotificationAuthorization(ctx context.Context) (bool, error) {
fe := getFrontend(ctx)
return fe.CheckNotificationAuthorization()
}
// SendNotification sends a basic notification with the given options.
// The notification will display with the provided title, subtitle (if supported),
// and body text.
func SendNotification(ctx context.Context, options NotificationOptions) error {
fe := getFrontend(ctx)
return fe.SendNotification(options)
}
// SendNotificationWithActions sends a notification with action buttons.
// A NotificationCategory must be registered first using RegisterNotificationCategory.
// The options.CategoryID must match a previously registered category ID.
// If the category is not found, a basic notification will be sent instead.
func SendNotificationWithActions(ctx context.Context, options NotificationOptions) error {
fe := getFrontend(ctx)
return fe.SendNotificationWithActions(options)
}
// RegisterNotificationCategory registers a notification category that can be used
// with SendNotificationWithActions. Categories define the action buttons and optional
// reply fields that will appear on notifications.
func RegisterNotificationCategory(ctx context.Context, category NotificationCategory) error {
fe := getFrontend(ctx)
return fe.RegisterNotificationCategory(category)
}
// RemoveNotificationCategory removes a previously registered notification category.
func RemoveNotificationCategory(ctx context.Context, categoryId string) error {
fe := getFrontend(ctx)
return fe.RemoveNotificationCategory(categoryId)
}
// RemoveAllPendingNotifications removes all pending notifications from the notification center.
// On Windows, this is a no-op as the platform manages notification lifecycle automatically.
func RemoveAllPendingNotifications(ctx context.Context) error {
fe := getFrontend(ctx)
return fe.RemoveAllPendingNotifications()
}
// RemovePendingNotification removes a specific pending notification by its identifier.
// On Windows, this is a no-op as the platform manages notification lifecycle automatically.
func RemovePendingNotification(ctx context.Context, identifier string) error {
fe := getFrontend(ctx)
return fe.RemovePendingNotification(identifier)
}
// RemoveAllDeliveredNotifications removes all delivered notifications from the notification center.
// On Windows, this is a no-op as the platform manages notification lifecycle automatically.
func RemoveAllDeliveredNotifications(ctx context.Context) error {
fe := getFrontend(ctx)
return fe.RemoveAllDeliveredNotifications()
}
// RemoveDeliveredNotification removes a specific delivered notification by its identifier.
// On Windows, this is a no-op as the platform manages notification lifecycle automatically.
func RemoveDeliveredNotification(ctx context.Context, identifier string) error {
fe := getFrontend(ctx)
return fe.RemoveDeliveredNotification(identifier)
}
// RemoveNotification removes a notification by its identifier.
// This is a convenience function that works across platforms. On macOS, use the
// more specific RemovePendingNotification or RemoveDeliveredNotification functions.
func RemoveNotification(ctx context.Context, identifier string) error {
fe := getFrontend(ctx)
return fe.RemoveNotification(identifier)
}
// OnNotificationResponse registers a callback function that will be invoked when
// a user interacts with a notification (e.g., clicks an action button or the notification itself).
// The callback receives a NotificationResult containing the response details or any errors.
func OnNotificationResponse(ctx context.Context, callback func(result NotificationResult)) {
fe := getFrontend(ctx)
fe.OnNotificationResponse(callback)
}

View File

@@ -0,0 +1,107 @@
package runtime
import (
"context"
"log"
goruntime "runtime"
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/internal/logger"
)
const contextError = `An invalid context was passed. This method requires the specific context given in the lifecycle hooks:
https://wails.io/docs/reference/runtime/intro`
func getFrontend(ctx context.Context) frontend.Frontend {
if ctx == nil {
pc, _, _, _ := goruntime.Caller(1)
funcName := goruntime.FuncForPC(pc).Name()
log.Fatalf("cannot call '%s': %s", funcName, contextError)
}
result := ctx.Value("frontend")
if result != nil {
return result.(frontend.Frontend)
}
pc, _, _, _ := goruntime.Caller(1)
funcName := goruntime.FuncForPC(pc).Name()
log.Fatalf("cannot call '%s': %s", funcName, contextError)
return nil
}
func getLogger(ctx context.Context) *logger.Logger {
if ctx == nil {
pc, _, _, _ := goruntime.Caller(1)
funcName := goruntime.FuncForPC(pc).Name()
log.Fatalf("cannot call '%s': %s", funcName, contextError)
}
result := ctx.Value("logger")
if result != nil {
return result.(*logger.Logger)
}
pc, _, _, _ := goruntime.Caller(1)
funcName := goruntime.FuncForPC(pc).Name()
log.Fatalf("cannot call '%s': %s", funcName, contextError)
return nil
}
func getEvents(ctx context.Context) frontend.Events {
if ctx == nil {
pc, _, _, _ := goruntime.Caller(1)
funcName := goruntime.FuncForPC(pc).Name()
log.Fatalf("cannot call '%s': %s", funcName, contextError)
}
result := ctx.Value("events")
if result != nil {
return result.(frontend.Events)
}
pc, _, _, _ := goruntime.Caller(1)
funcName := goruntime.FuncForPC(pc).Name()
log.Fatalf("cannot call '%s': %s", funcName, contextError)
return nil
}
// Quit the application
func Quit(ctx context.Context) {
if ctx == nil {
log.Fatalf("Error calling 'runtime.Quit': %s", contextError)
}
appFrontend := getFrontend(ctx)
appFrontend.Quit()
}
// Hide the application
func Hide(ctx context.Context) {
if ctx == nil {
log.Fatalf("Error calling 'runtime.Hide': %s", contextError)
}
appFrontend := getFrontend(ctx)
appFrontend.Hide()
}
// Show the application if it is hidden
func Show(ctx context.Context) {
if ctx == nil {
log.Fatalf("Error calling 'runtime.Show': %s", contextError)
}
appFrontend := getFrontend(ctx)
appFrontend.Show()
}
// EnvironmentInfo contains information about the environment
type EnvironmentInfo struct {
BuildType string `json:"buildType"`
Platform string `json:"platform"`
Arch string `json:"arch"`
}
// Environment returns information about the environment
func Environment(ctx context.Context) EnvironmentInfo {
var result EnvironmentInfo
buildType := ctx.Value("buildtype")
if buildType != nil {
result.BuildType = buildType.(string)
}
result.Platform = goruntime.GOOS
result.Arch = goruntime.GOARCH
return result
}

View File

@@ -0,0 +1,15 @@
package runtime
import (
"context"
"github.com/wailsapp/wails/v2/internal/frontend"
)
type Screen = frontend.Screen
// ScreenGetAll returns all screens
func ScreenGetAll(ctx context.Context) ([]Screen, error) {
appFrontend := getFrontend(ctx)
return appFrontend.ScreenGetAll()
}

View File

@@ -0,0 +1,65 @@
//go:build linux
package runtime
/*
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
static void fix_signal(int signum)
{
struct sigaction st;
if (sigaction(signum, NULL, &st) < 0) {
return;
}
st.sa_flags |= SA_ONSTACK;
sigaction(signum, &st, NULL);
}
static void fix_all_signals()
{
#if defined(SIGSEGV)
fix_signal(SIGSEGV);
#endif
#if defined(SIGBUS)
fix_signal(SIGBUS);
#endif
#if defined(SIGFPE)
fix_signal(SIGFPE);
#endif
#if defined(SIGABRT)
fix_signal(SIGABRT);
#endif
}
*/
import "C"
// ResetSignalHandlers resets signal handlers to allow panic recovery.
//
// On Linux, WebKit (used for the webview) may install signal handlers without
// the SA_ONSTACK flag, which prevents Go from properly recovering from panics
// caused by nil pointer dereferences or other memory access violations.
//
// Call this function immediately before code that might panic to ensure
// the signal handlers are properly configured for Go's panic recovery mechanism.
//
// Example usage:
//
// go func() {
// defer func() {
// if err := recover(); err != nil {
// log.Printf("Recovered from panic: %v", err)
// }
// }()
// runtime.ResetSignalHandlers()
// // Code that might panic...
// }()
//
// Note: This function only has an effect on Linux. On other platforms,
// it is a no-op.
func ResetSignalHandlers() {
C.fix_all_signals()
}

View File

@@ -0,0 +1,18 @@
//go:build !linux
package runtime
// ResetSignalHandlers resets signal handlers to allow panic recovery.
//
// On Linux, WebKit (used for the webview) may install signal handlers without
// the SA_ONSTACK flag, which prevents Go from properly recovering from panics
// caused by nil pointer dereferences or other memory access violations.
//
// Call this function immediately before code that might panic to ensure
// the signal handlers are properly configured for Go's panic recovery mechanism.
//
// Note: This function only has an effect on Linux. On other platforms,
// it is a no-op.
func ResetSignalHandlers() {
// No-op on non-Linux platforms
}

View File

@@ -0,0 +1,186 @@
package runtime
import (
"context"
"github.com/wailsapp/wails/v2/pkg/options"
)
// WindowSetTitle sets the title of the window
func WindowSetTitle(ctx context.Context, title string) {
appFrontend := getFrontend(ctx)
appFrontend.WindowSetTitle(title)
}
// WindowFullscreen makes the window fullscreen
func WindowFullscreen(ctx context.Context) {
appFrontend := getFrontend(ctx)
appFrontend.WindowFullscreen()
}
// WindowUnfullscreen makes the window UnFullscreen
func WindowUnfullscreen(ctx context.Context) {
appFrontend := getFrontend(ctx)
appFrontend.WindowUnfullscreen()
}
// WindowCenter the window on the current screen
func WindowCenter(ctx context.Context) {
appFrontend := getFrontend(ctx)
appFrontend.WindowCenter()
}
// WindowReload will reload the window contents
func WindowReload(ctx context.Context) {
appFrontend := getFrontend(ctx)
appFrontend.WindowReload()
}
// WindowReloadApp will reload the application
func WindowReloadApp(ctx context.Context) {
appFrontend := getFrontend(ctx)
appFrontend.WindowReloadApp()
}
func WindowSetSystemDefaultTheme(ctx context.Context) {
appFrontend := getFrontend(ctx)
appFrontend.WindowSetSystemDefaultTheme()
}
func WindowSetLightTheme(ctx context.Context) {
appFrontend := getFrontend(ctx)
appFrontend.WindowSetLightTheme()
}
func WindowSetDarkTheme(ctx context.Context) {
appFrontend := getFrontend(ctx)
appFrontend.WindowSetDarkTheme()
}
// WindowShow shows the window if hidden
func WindowShow(ctx context.Context) {
appFrontend := getFrontend(ctx)
appFrontend.WindowShow()
}
// WindowHide the window
func WindowHide(ctx context.Context) {
appFrontend := getFrontend(ctx)
appFrontend.WindowHide()
}
// WindowSetSize sets the size of the window
func WindowSetSize(ctx context.Context, width int, height int) {
appFrontend := getFrontend(ctx)
appFrontend.WindowSetSize(width, height)
}
func WindowGetSize(ctx context.Context) (int, int) {
appFrontend := getFrontend(ctx)
return appFrontend.WindowGetSize()
}
// WindowSetMinSize sets the minimum size of the window
func WindowSetMinSize(ctx context.Context, width int, height int) {
appFrontend := getFrontend(ctx)
appFrontend.WindowSetMinSize(width, height)
}
// WindowSetMaxSize sets the maximum size of the window
func WindowSetMaxSize(ctx context.Context, width int, height int) {
appFrontend := getFrontend(ctx)
appFrontend.WindowSetMaxSize(width, height)
}
// WindowSetAlwaysOnTop sets the window AlwaysOnTop or not on top
func WindowSetAlwaysOnTop(ctx context.Context, b bool) {
appFrontend := getFrontend(ctx)
appFrontend.WindowSetAlwaysOnTop(b)
}
// WindowSetPosition sets the position of the window
func WindowSetPosition(ctx context.Context, x int, y int) {
appFrontend := getFrontend(ctx)
appFrontend.WindowSetPosition(x, y)
}
func WindowGetPosition(ctx context.Context) (int, int) {
appFrontend := getFrontend(ctx)
return appFrontend.WindowGetPosition()
}
// WindowMaximise the window
func WindowMaximise(ctx context.Context) {
appFrontend := getFrontend(ctx)
appFrontend.WindowMaximise()
}
// WindowToggleMaximise the window
func WindowToggleMaximise(ctx context.Context) {
appFrontend := getFrontend(ctx)
appFrontend.WindowToggleMaximise()
}
// WindowUnmaximise the window
func WindowUnmaximise(ctx context.Context) {
appFrontend := getFrontend(ctx)
appFrontend.WindowUnmaximise()
}
// WindowMinimise the window
func WindowMinimise(ctx context.Context) {
appFrontend := getFrontend(ctx)
appFrontend.WindowMinimise()
}
// WindowUnminimise the window
func WindowUnminimise(ctx context.Context) {
appFrontend := getFrontend(ctx)
appFrontend.WindowUnminimise()
}
// WindowIsFullscreen get the window state is window Fullscreen
func WindowIsFullscreen(ctx context.Context) bool {
appFrontend := getFrontend(ctx)
return appFrontend.WindowIsFullscreen()
}
// WindowIsMaximised get the window state is window Maximised
func WindowIsMaximised(ctx context.Context) bool {
appFrontend := getFrontend(ctx)
return appFrontend.WindowIsMaximised()
}
// WindowIsMinimised get the window state is window Minimised
func WindowIsMinimised(ctx context.Context) bool {
appFrontend := getFrontend(ctx)
return appFrontend.WindowIsMinimised()
}
// WindowIsNormal get the window state is window Normal
func WindowIsNormal(ctx context.Context) bool {
appFrontend := getFrontend(ctx)
return appFrontend.WindowIsNormal()
}
// WindowExecJS executes the given Js in the window
func WindowExecJS(ctx context.Context, js string) {
appFrontend := getFrontend(ctx)
appFrontend.ExecJS(js)
}
func WindowSetBackgroundColour(ctx context.Context, R, G, B, A uint8) {
appFrontend := getFrontend(ctx)
col := &options.RGBA{
R: R,
G: G,
B: B,
A: A,
}
appFrontend.WindowSetBackgroundColour(col)
}
func WindowPrint(ctx context.Context) {
appFrontend := getFrontend(ctx)
appFrontend.WindowPrint()
}