fix: add involves edge from task to agent:nomos at creation
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

Plus sync vendor directory for Docker build compatibility.
This commit is contained in:
2026-08-11 22:03:12 +02:00
parent 7d6a3320d4
commit febc153b7f
3384 changed files with 945212 additions and 2 deletions

21
vendor/github.com/wailsapp/wails/v3/LICENSE generated vendored Normal file
View File

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

View File

@@ -0,0 +1,161 @@
package assetserver
import (
"bytes"
"context"
"embed"
"errors"
"fmt"
"io"
iofs "io/fs"
"net/http"
"os"
"path"
"strings"
)
const (
indexHTML = "index.html"
)
type assetFileServer struct {
fs iofs.FS
err error
}
func newAssetFileServerFS(vfs iofs.FS) http.Handler {
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)
}
err = errors.New(msg)
}
} else {
vfs, err = iofs.Sub(vfs, path.Clean(subDir))
}
return &assetFileServer{fs: vfs, err: err}
}
func (d *assetFileServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
url := req.URL.Path
err := d.err
if err == nil {
filename := path.Clean(strings.TrimPrefix(url, "/"))
d.logInfo(ctx, "Handling request", "url", url, "file", filename)
err = d.serveFSFile(rw, req, filename)
if os.IsNotExist(err) {
rw.WriteHeader(http.StatusNotFound)
return
}
}
if err != nil {
d.logError(ctx, "Unable to handle request", "url", url, "err", err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
}
// serveFile will try to load the file from the fs.FS and write it to the response
func (d *assetFileServer) 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 {
if s := path.Ext(filename); s == "" {
filename = filename + ".html"
file, err = d.fs.Open(filename)
if err != nil {
return err
}
} else {
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
}
rw.Header().Set(HeaderContentLength, fmt.Sprintf("%d", statInfo.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 *assetFileServer) logInfo(ctx context.Context, message string, args ...interface{}) {
logInfo(ctx, "[AssetFileServerFS] "+message, args...)
}
func (d *assetFileServer) logError(ctx context.Context, message string, args ...interface{}) {
logError(ctx, "[AssetFileServerFS] "+message, args...)
}

View File

@@ -0,0 +1,175 @@
package assetserver
import (
"fmt"
"net"
"net/http"
"net/url"
"strings"
"time"
)
const (
webViewRequestHeaderWindowId = "x-wails-window-id"
webViewRequestHeaderWindowName = "x-wails-window-name"
HeaderAcceptLanguage = "accept-language"
)
type RuntimeHandler interface {
HandleRuntimeCall(w http.ResponseWriter, r *http.Request)
}
type service struct {
Route string
Handler http.Handler
}
type AssetServer struct {
options *Options
handler http.Handler
services []service
assetServerWebView
}
func NewAssetServer(options *Options) (*AssetServer, error) {
result := &AssetServer{
options: options,
}
userHandler := options.Handler
if userHandler == nil {
userHandler = http.NotFoundHandler()
}
handler := http.Handler(
http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
result.serveHTTP(w, r, userHandler)
}))
if middleware := options.Middleware; middleware != nil {
handler = middleware(handler)
}
result.handler = handler
return result, nil
}
func (a *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
start := time.Now()
wrapped := newContentTypeSniffer(rw)
defer func() {
if _, err := wrapped.complete(); err != nil {
a.options.Logger.Error("Error writing response data.", "uri", req.RequestURI, "error", err)
}
}()
req = req.WithContext(contextWithLogger(req.Context(), a.options.Logger))
a.handler.ServeHTTP(wrapped, req)
a.options.Logger.Debug(
"Asset Request:",
"windowName", req.Header.Get(webViewRequestHeaderWindowName),
"windowID", req.Header.Get(webViewRequestHeaderWindowId),
"code", wrapped.status,
"method", req.Method,
"path", req.URL.EscapedPath(),
"duration", time.Since(start),
)
}
func (a *AssetServer) serveHTTP(rw http.ResponseWriter, req *http.Request, userHandler http.Handler) {
if isWebSocket(req) {
// WebSockets are not supported by the AssetServer
rw.WriteHeader(http.StatusNotImplemented)
return
}
header := rw.Header()
// TODO: I don't think this is needed now?
//if a.servingFromDisk {
// header.Add(HeaderCacheControl, "no-cache")
//}
reqPath := req.URL.Path
switch reqPath {
case "", "/", "/index.html":
// Cache the accept-language header
// before passing the request down the chain.
acceptLanguage := req.Header.Get(HeaderAcceptLanguage)
if acceptLanguage == "" {
acceptLanguage = "en"
}
wrapped := &fallbackResponseWriter{
rw: rw,
req: req,
fallback: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
// Set content type for default index.html
header.Set(HeaderContentType, "text/html; charset=utf-8")
a.writeBlob(rw, indexHTML, defaultIndexHTML(acceptLanguage))
}),
}
userHandler.ServeHTTP(wrapped, req)
default:
// Check if the path matches a service route
for _, svc := range a.services {
if strings.HasPrefix(reqPath, svc.Route) {
req.URL.Path = strings.TrimPrefix(reqPath, svc.Route)
svc.Handler.ServeHTTP(rw, req)
return
}
}
// Forward to the user-provided handler
userHandler.ServeHTTP(rw, req)
}
}
func (a *AssetServer) AttachServiceHandler(route string, handler http.Handler) {
a.services = append(a.services, service{route, handler})
}
func (a *AssetServer) writeBlob(rw http.ResponseWriter, filename string, blob []byte) {
err := ServeFile(rw, filename, blob)
if err != nil {
a.serveError(rw, err, "Error writing file content.", "filename", filename)
}
}
func (a *AssetServer) serveError(rw http.ResponseWriter, err error, msg string, args ...interface{}) {
args = append(args, "error", err)
a.options.Logger.Error(msg, args...)
rw.WriteHeader(http.StatusInternalServerError)
}
func GetStartURL(userURL string) (string, error) {
devServerURL := GetDevServerURL()
startURL := baseURL.String()
if devServerURL != "" {
// Parse the port
parsedURL, err := url.Parse(devServerURL)
if err != nil {
return "", fmt.Errorf("error parsing environment variable `FRONTEND_DEVSERVER_URL`: %w. Please check your `Taskfile.yml` file", err)
}
port := parsedURL.Port()
if port != "" {
baseURL.Host = net.JoinHostPort(baseURL.Hostname(), port)
startURL = baseURL.String()
}
}
if userURL != "" {
parsedURL, err := baseURL.Parse(userURL)
if err != nil {
return "", fmt.Errorf("error parsing URL: %w", err)
}
startURL = parsedURL.String()
}
return startURL, nil
}

View File

@@ -0,0 +1,12 @@
//go:build android
package assetserver
import "net/url"
// Android uses https://wails.localhost as the base URL
// This matches the WebViewAssetLoader domain configuration
var baseURL = url.URL{
Scheme: "https",
Host: "wails.localhost",
}

View File

@@ -0,0 +1,10 @@
//go:build darwin && !ios
package assetserver
import "net/url"
var baseURL = url.URL{
Scheme: "wails",
Host: "localhost",
}

View File

@@ -0,0 +1,50 @@
//go:build !production
package assetserver
import (
"embed"
"io"
iofs "io/fs"
)
//go:embed defaults
var defaultHTML embed.FS
func defaultIndexHTML(language string) []byte {
result := []byte("index.html not found")
// Create an fs.Sub in the defaults directory
defaults, err := iofs.Sub(defaultHTML, "defaults")
if err != nil {
return result
}
// Get the 2 character language code
lang := "en"
if len(language) >= 2 {
lang = language[:2]
}
// Now we can read the index.html file in the format
// index.<lang>.html.
indexFile, err := defaults.Open("index." + lang + ".html")
if err != nil {
return result
}
indexBytes, err := io.ReadAll(indexFile)
if err != nil {
return result
}
return indexBytes
}
func (a *AssetServer) LogDetails() {
var info = []any{
"middleware", a.options.Middleware != nil,
"handler", a.options.Handler != nil,
}
if devServerURL := GetDevServerURL(); devServerURL != "" {
info = append(info, "devServerURL", devServerURL)
}
a.options.Logger.Info("AssetServer Info:", info...)
}

View File

@@ -0,0 +1,10 @@
//go:build ios
package assetserver
import "net/url"
var baseURL = url.URL{
Scheme: "wails",
Host: "localhost",
}

View File

@@ -0,0 +1,10 @@
//go:build linux && !android
package assetserver
import "net/url"
var baseURL = url.URL{
Scheme: "wails",
Host: "localhost",
}

View File

@@ -0,0 +1,9 @@
//go:build production
package assetserver
func defaultIndexHTML(_ string) []byte {
return []byte("index.html not found")
}
func (a *AssetServer) LogDetails() {}

View File

@@ -0,0 +1,198 @@
package assetserver
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"github.com/wailsapp/wails/v3/internal/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 (a *AssetServer) ServeWebViewRequest(req webview.Request) {
a.dispatchInit.Do(func() {
workers := a.dispatchWorkers
if workers <= 0 {
return
}
workerC := make(chan webview.Request, workers*2)
for i := 0; i < workers; i++ {
go func() {
for req := range workerC {
a.processWebViewRequest(req)
}
}()
}
dispatchC := make(chan webview.Request)
go queueingDispatcher(50, dispatchC, workerC)
a.dispatchReqC = dispatchC
})
if a.dispatchReqC == nil {
go a.processWebViewRequest(req)
} else {
a.dispatchReqC <- req
}
}
func (a *AssetServer) processWebViewRequest(r webview.Request) {
uri, _ := r.URL()
a.processWebViewRequestInternal(r)
if err := r.Close(); err != nil {
a.options.Logger.Error("Unable to call close for request for uri.", "uri", uri)
}
}
// processHTTPRequest 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 (a *AssetServer) processWebViewRequestInternal(r webview.Request) {
uri := "unknown"
var err error
wrw := r.Response()
defer func() {
if err := wrw.Finish(); err != nil {
a.options.Logger.Error("Error finishing request.", "uri", uri, "error", err)
}
}()
rw := newContentTypeSniffer(wrw) // Make sure we have a Content-Type sniffer
defer func() {
if _, err := rw.complete(); err != nil {
a.options.Logger.Error("Error writing response data.", "uri", uri, "error", err)
}
}()
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 {
a.webviewRequestErrorHandler(uri, rw, fmt.Errorf("URL: %w", err))
return
}
method, err := r.Method()
if err != nil {
a.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Method: %w", err))
return
}
header, err := r.Header()
if err != nil {
a.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Header: %w", err))
return
}
body, err := r.Body()
if err != nil {
a.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Body: %w", err))
return
}
if body == nil {
body = http.NoBody
}
defer body.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
req, err := http.NewRequestWithContext(ctx, method, uri, body)
if err != nil {
a.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 requestURL := req.URL; req.RequestURI == "" && requestURL != nil {
req.RequestURI = requestURL.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.RequestURI == "" && req.URL != nil {
req.RequestURI = req.URL.String()
}
if req.ContentLength == 0 {
req.ContentLength, _ = strconv.ParseInt(req.Header.Get(HeaderContentLength), 10, 64)
} else {
req.Header.Set(HeaderContentLength, fmt.Sprintf("%d", req.ContentLength))
}
if host := req.Header.Get(HeaderHost); host != "" {
req.Host = host
}
// iOS uses "localhost" while other platforms might use different hosts
// Skip host check for iOS requests from wails:// scheme
if expectedHost := a.ExpectedWebViewHost; expectedHost != "" && expectedHost != req.Host && !strings.HasPrefix(uri, "wails://") {
a.webviewRequestErrorHandler(uri, rw, fmt.Errorf("expected host '%s' in request, but was '%s'", expectedHost, req.Host))
return
}
a.ServeHTTP(rw, req)
}
func (a *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)
}
a.options.Logger.Error("Error processing request (HttpResponse=500)", "details", logInfo, "error", 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,8 @@
package assetserver
import "net/url"
var baseURL = url.URL{
Scheme: "http",
Host: "wails.localhost",
}

View File

@@ -0,0 +1,102 @@
//go:build !production
package assetserver
import (
"context"
"io/fs"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
"time"
)
// retryTransport implements http.RoundTripper with retry logic for transient connection failures.
// This is particularly useful when the Vite dev server temporarily rejects connections due to
// high concurrency with many dynamic imports.
type retryTransport struct {
base http.RoundTripper
maxRetries int
delay time.Duration
}
// RoundTrip executes a single HTTP transaction with retry logic.
func (t *retryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
var resp *http.Response
var err error
for i := 0; i < t.maxRetries; i++ {
resp, err = t.base.RoundTrip(req)
if err == nil {
return resp, nil
}
// Only retry on connection errors (e.g., connection refused)
if isConnectionError(err) && i < t.maxRetries-1 {
time.Sleep(t.delay)
continue
}
break
}
return resp, err
}
// isConnectionError checks if the error is a connection-related error that may be transient.
func isConnectionError(err error) bool {
if err == nil {
return false
}
errStr := strings.ToLower(err.Error())
return strings.Contains(errStr, "connection refused") ||
strings.Contains(errStr, "connection reset") ||
strings.Contains(errStr, "broken pipe") ||
strings.Contains(errStr, "connectex")
}
func NewAssetFileServer(vfs fs.FS) http.Handler {
devServerURL := GetDevServerURL()
if devServerURL == "" {
return newAssetFileServerFS(vfs)
}
parsedURL, err := url.Parse(devServerURL)
if err != nil {
return http.HandlerFunc(
func(rw http.ResponseWriter, req *http.Request) {
logError(req.Context(), "[ExternalAssetHandler] Invalid FRONTEND_DEVSERVER_URL. Should be valid URL", "error", err.Error())
http.Error(rw, err.Error(), http.StatusInternalServerError)
})
}
dialer := &net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}
proxy := httputil.NewSingleHostReverseProxy(parsedURL)
proxy.Transport = &retryTransport{
base: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
// Force IPv4 for localhost connections to avoid IPv6 issues on Windows
if parsedURL.Hostname() == "localhost" || parsedURL.Hostname() == "127.0.0.1" {
return dialer.DialContext(ctx, "tcp4", addr)
}
return dialer.DialContext(ctx, network, addr)
},
},
maxRetries: 50,
delay: 50 * time.Millisecond,
}
proxy.ErrorHandler = func(rw http.ResponseWriter, r *http.Request, err error) {
logError(r.Context(), "[ExternalAssetHandler] Proxy error", "error", err.Error())
rw.WriteHeader(http.StatusBadGateway)
}
return proxy
}
func GetDevServerURL() string {
return os.Getenv("FRONTEND_DEVSERVER_URL")
}

View File

@@ -0,0 +1,16 @@
//go:build production
package assetserver
import (
"io/fs"
"net/http"
)
func NewAssetFileServer(vfs fs.FS) http.Handler {
return newAssetFileServerFS(vfs)
}
func GetDevServerURL() string {
return ""
}

View File

@@ -0,0 +1,33 @@
package assetserver
import (
"github.com/wailsapp/wails/v3/internal/assetserver/bundledassets"
"io/fs"
"net/http"
"strings"
)
type BundledAssetServer struct {
handler http.Handler
}
func NewBundledAssetFileServer(fs fs.FS) *BundledAssetServer {
return &BundledAssetServer{
handler: NewAssetFileServer(fs),
}
}
func (b *BundledAssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if strings.HasPrefix(req.URL.Path, "/wails/") {
// Strip the /wails prefix
req.URL.Path = req.URL.Path[6:]
switch req.URL.Path {
case "/runtime.js":
rw.Header().Set("Content-Type", "application/javascript")
rw.Write([]byte(bundledassets.RuntimeJS))
return
}
return
}
b.handler.ServeHTTP(rw, req)
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
//go:build !production
package bundledassets
import _ "embed"
//go:embed runtime.debug.js
var RuntimeJS []byte

View File

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

View File

@@ -0,0 +1,66 @@
package assetserver
import (
"context"
"fmt"
"log/slog"
"net/http"
"strings"
)
const (
HeaderHost = "Host"
HeaderContentType = "Content-Type"
HeaderContentLength = "Content-Length"
HeaderUserAgent = "User-Agent"
// TODO: Is this needed?
HeaderCacheControl = "Cache-Control"
HeaderUpgrade = "Upgrade"
WailsUserAgentValue = "wails.io"
)
type assetServerLogger struct{}
var assetServerLoggerKey assetServerLogger
// ServeFile writes the provided blob to rw as an HTTP 200 response, ensuring appropriate
// Content-Length and Content-Type headers are set.
//
// If the Content-Type header is not already present, ServeFile determines an appropriate
// MIME type from the filename and blob and sets the Content-Type header. It then writes
// the 200 status and the blob body to the response, returning any error encountered while
// writing the body.
func ServeFile(rw http.ResponseWriter, filename string, blob []byte) error {
header := rw.Header()
header.Set(HeaderContentLength, fmt.Sprintf("%d", len(blob)))
if mimeType := header.Get(HeaderContentType); mimeType == "" {
mimeType = GetMimetype(filename, blob)
header.Set(HeaderContentType, mimeType)
}
rw.WriteHeader(http.StatusOK)
_, err := rw.Write(blob)
return err
}
func isWebSocket(req *http.Request) bool {
upgrade := req.Header.Get(HeaderUpgrade)
return strings.EqualFold(upgrade, "websocket")
}
func contextWithLogger(ctx context.Context, logger *slog.Logger) context.Context {
return context.WithValue(ctx, assetServerLoggerKey, logger)
}
func logInfo(ctx context.Context, message string, args ...interface{}) {
if logger, _ := ctx.Value(assetServerLoggerKey).(*slog.Logger); logger != nil {
logger.Info(message, args...)
}
}
func logError(ctx context.Context, message string, args ...interface{}) {
if logger, _ := ctx.Value(assetServerLoggerKey).(*slog.Logger); logger != nil {
logger.Error(message, args...)
}
}

View File

@@ -0,0 +1,142 @@
package assetserver
import (
"net/http"
)
// newContentTypeSniffer creates a contentTypeSniffer that wraps the provided http.ResponseWriter.
// The returned sniffer does not allocate a close notification channel; it will be initialized lazily by CloseNotify.
func newContentTypeSniffer(rw http.ResponseWriter) *contentTypeSniffer {
return &contentTypeSniffer{
rw: rw,
}
}
type contentTypeSniffer struct {
rw http.ResponseWriter
prefix []byte
closeChannel chan bool // lazily allocated only if CloseNotify is called
status int
headerCommitted bool
headerWritten bool
}
// Unwrap returns the wrapped [http.ResponseWriter] for use with [http.ResponseController].
func (rw *contentTypeSniffer) Unwrap() http.ResponseWriter {
return rw.rw
}
func (rw *contentTypeSniffer) Header() http.Header {
return rw.rw.Header()
}
func (rw *contentTypeSniffer) Write(chunk []byte) (int, error) {
if !rw.headerCommitted {
rw.WriteHeader(http.StatusOK)
}
if rw.headerWritten {
return rw.rw.Write(chunk)
}
if len(chunk) == 0 {
return 0, nil
}
// Cut away at most 512 bytes from chunk, and not less than 0.
cut := max(min(len(chunk), 512-len(rw.prefix)), 0)
if cut >= 512 {
// Avoid copying data if a full prefix is available on first non-zero write.
cut = len(chunk)
rw.prefix = chunk
chunk = nil
} else if cut > 0 {
// First write had less than 512 bytes -- copy data to the prefix buffer.
if rw.prefix == nil {
// Preallocate space for the prefix to be used for sniffing.
rw.prefix = make([]byte, 0, 512)
}
rw.prefix = append(rw.prefix, chunk[:cut]...)
chunk = chunk[cut:]
}
if len(rw.prefix) < 512 {
return cut, nil
}
if _, err := rw.complete(); err != nil {
return cut, err
}
n, err := rw.rw.Write(chunk)
return cut + n, err
}
func (rw *contentTypeSniffer) WriteHeader(code int) {
if rw.headerCommitted {
return
}
rw.status = code
rw.headerCommitted = true
if _, hasType := rw.Header()[HeaderContentType]; hasType {
rw.rw.WriteHeader(rw.status)
rw.headerWritten = true
}
}
// sniff sniffs the content type from the stored prefix if necessary,
// then writes the header.
func (rw *contentTypeSniffer) sniff() {
if rw.headerWritten || !rw.headerCommitted {
return
}
m := rw.Header()
if _, hasType := m[HeaderContentType]; !hasType {
m.Set(HeaderContentType, http.DetectContentType(rw.prefix))
}
rw.rw.WriteHeader(rw.status)
rw.headerWritten = true
}
// complete sniffs the content type if necessary, writes the header
// and sends the data prefix that has been stored for sniffing.
//
// Whoever creates a contentTypeSniffer instance
// is responsible for calling complete after the nested handler has returned.
func (rw *contentTypeSniffer) complete() (n int, err error) {
rw.sniff()
if rw.headerWritten && len(rw.prefix) > 0 {
n, err = rw.rw.Write(rw.prefix)
rw.prefix = nil
}
return
}
// CloseNotify implements the http.CloseNotifier interface.
// The channel is lazily allocated to avoid allocation overhead for requests
// that don't use this deprecated interface.
func (rw *contentTypeSniffer) CloseNotify() <-chan bool {
if rw.closeChannel == nil {
rw.closeChannel = make(chan bool, 1)
}
return rw.closeChannel
}
func (rw *contentTypeSniffer) closeClient() {
if rw.closeChannel != nil {
rw.closeChannel <- true
}
}
// Flush implements the http.Flusher interface.
func (rw *contentTypeSniffer) Flush() {
if f, ok := rw.rw.(http.Flusher); ok {
f.Flush()
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,80 @@
package assetserver
import (
"maps"
"net/http"
)
// fallbackResponseWriter wraps a [http.ResponseWriter].
// If the main handler returns status code 404,
// its response is discarded
// and the request is forwarded to the fallback handler.
type fallbackResponseWriter struct {
rw http.ResponseWriter
req *http.Request
fallback http.Handler
header http.Header
headerWritten bool
complete bool
}
// Unwrap returns the wrapped [http.ResponseWriter] for use with [http.ResponseController].
func (fw *fallbackResponseWriter) Unwrap() http.ResponseWriter {
return fw.rw
}
func (fw *fallbackResponseWriter) Header() http.Header {
if fw.header == nil {
// Preserve original header in case we get a 404 response.
fw.header = fw.rw.Header().Clone()
}
return fw.header
}
func (fw *fallbackResponseWriter) Write(chunk []byte) (int, error) {
if fw.complete {
// Fallback triggered, discard further writes.
return len(chunk), nil
}
if !fw.headerWritten {
fw.WriteHeader(http.StatusOK)
}
return fw.rw.Write(chunk)
}
func (fw *fallbackResponseWriter) WriteHeader(statusCode int) {
if fw.headerWritten {
return
}
fw.headerWritten = true
if statusCode == http.StatusNotFound {
// Protect fallback header from external modifications.
if fw.header == nil {
fw.header = fw.rw.Header().Clone()
}
// Invoke fallback handler.
fw.complete = true
fw.fallback.ServeHTTP(fw.rw, fw.req)
return
}
if fw.header != nil {
// Apply headers and forward original map to the main handler.
maps.Copy(fw.rw.Header(), fw.header)
fw.header = fw.rw.Header()
}
fw.rw.WriteHeader(statusCode)
}
// Flush implements the http.Flusher interface.
func (rw *fallbackResponseWriter) Flush() {
if f, ok := rw.rw.(http.Flusher); ok {
f.Flush()
}
}

View File

@@ -0,0 +1,76 @@
package assetserver
import (
"embed"
"errors"
"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(fileSystem embed.FS) (string, error) {
stopErr := errors.New("files or multiple dirs found")
fPath := ""
err := fs.WalkDir(fileSystem, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
fPath = path
if entries, dErr := fs.ReadDir(fileSystem, path); dErr != nil {
return dErr
} else if len(entries) <= 1 {
return nil
}
}
return stopErr
})
if err != nil && !errors.Is(err, stopErr) {
return "", err
}
return fPath, nil
}
func findPathToFile(fileSystem fs.FS, file string) (string, error) {
stat, _ := fs.Stat(fileSystem, file)
if stat != nil {
return ".", nil
}
var indexFiles []string
err := fs.WalkDir(fileSystem, ".", 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,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,116 @@
package assetserver
import (
"net/http"
"path/filepath"
"sync"
)
var (
// mimeCache uses sync.Map for better concurrent read performance
// since reads are far more common than writes
mimeCache sync.Map
// mimeTypesByExt maps file extensions to MIME types for common web formats.
// This approach is preferred over content-based detection because:
// 1. Extension-based lookup is O(1) vs O(n) content scanning
// 2. Web assets typically have correct extensions
// 3. stdlib's http.DetectContentType handles remaining cases adequately
// 4. Saves ~208KB binary size by not using github.com/wailsapp/mimetype
mimeTypesByExt = map[string]string{
// HTML
".htm": "text/html; charset=utf-8",
".html": "text/html; charset=utf-8",
// CSS/JS
".css": "text/css; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".ts": "application/x-typescript; charset=utf-8",
".tsx": "application/x-typescript; charset=utf-8",
".jsx": "text/javascript; charset=utf-8",
// Data formats
".json": "application/json",
".xml": "text/xml; charset=utf-8",
".yaml": "text/yaml; charset=utf-8",
".yml": "text/yaml; charset=utf-8",
".toml": "text/toml; charset=utf-8",
// Images
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
".avif": "image/avif",
".svg": "image/svg+xml",
".ico": "image/x-icon",
".bmp": "image/bmp",
".tiff": "image/tiff",
".tif": "image/tiff",
// Fonts
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".otf": "font/otf",
".eot": "application/vnd.ms-fontobject",
// Audio
".mp3": "audio/mpeg",
".wav": "audio/wav",
".ogg": "audio/ogg",
".m4a": "audio/mp4",
".aac": "audio/aac",
".flac": "audio/flac",
".opus": "audio/opus",
// Video
".mp4": "video/mp4",
".webm": "video/webm",
".ogv": "video/ogg",
".mov": "video/quicktime",
".avi": "video/x-msvideo",
".mkv": "video/x-matroska",
".m4v": "video/mp4",
// Documents
".pdf": "application/pdf",
".txt": "text/plain; charset=utf-8",
".md": "text/markdown; charset=utf-8",
// Archives
".zip": "application/zip",
".gz": "application/gzip",
".tar": "application/x-tar",
// WebAssembly
".wasm": "application/wasm",
// Source maps
".map": "application/json",
}
)
// "application/octet-stream".
func GetMimetype(filename string, data []byte) string {
// Fast path: check extension map first (no lock needed)
if result := mimeTypesByExt[filepath.Ext(filename)]; result != "" {
return result
}
// Check cache (lock-free read)
if cached, ok := mimeCache.Load(filename); ok {
return cached.(string)
}
// Slow path: use stdlib content-based detection and cache
result := http.DetectContentType(data)
if result == "" {
result = "application/octet-stream"
}
mimeCache.Store(filename, result)
return result
}

View File

@@ -0,0 +1,38 @@
package assetserver
import (
"errors"
"log/slog"
"net/http"
)
// Options defines the configuration of the AssetServer.
type Options struct {
// Handler which serves all the content to the WebView.
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.
//
// This middleware injects itself before any of Wails internal middlewares.
//
// If not defined, the default AssetServer request chain is executed.
//
// Multiple Middlewares can be chained together with:
// ChainMiddleware(middleware ...Middleware) Middleware
Middleware Middleware
// Logger is the logger used by the AssetServer. If not defined, no logging will be done.
Logger *slog.Logger
}
// Validate the options
func (o Options) Validate() error {
if o.Handler == nil && o.Middleware == nil {
return errors.New("AssetServer options invalid: either Handler or Middleware must be set")
}
return nil
}

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,152 @@
//go:build linux && cgo && !android
package webview
/*
#cgo linux pkg-config: glib-2.0
#include <glib.h>
#include <stdint.h>
extern void webviewMainThreadCallback(uintptr_t id);
// webview_dispatch_mu serializes the enabled-check plus scheduling in
// webview_invoke_on_main_sync against the flag clear in
// webview_disable_main_dispatch. Without it a worker could read
// enabled == TRUE, then have the flag flipped before it scheduled its source,
// and still queue work onto the now-dead main loop — blocking forever. A
// statically allocated GMutex needs no g_mutex_init.
static GMutex webview_dispatch_mu;
// webview_main_dispatch_enabled gates whether webview_invoke_on_main_sync may
// schedule work onto the GTK main loop. It starts enabled and is cleared once
// the loop has stopped (see webview_disable_main_dispatch). It is only ever read
// or written while holding webview_dispatch_mu.
static gboolean webview_main_dispatch_enabled = TRUE;
static void webview_disable_main_dispatch(void) {
g_mutex_lock(&webview_dispatch_mu);
webview_main_dispatch_enabled = FALSE;
g_mutex_unlock(&webview_dispatch_mu);
}
typedef struct {
uintptr_t id;
GMutex mutex;
GCond cond;
gboolean done;
} webviewMainSyncCall;
// webview_main_sync_trampoline runs on the GTK main thread (scheduled via
// g_main_context_invoke). It invokes the Go callback, then signals the waiting
// worker. g_cond_signal happens while the mutex is held and before the unlock,
// so the waiter cannot re-acquire the mutex (and destroy the primitives) until
// the signal has completed — making the stack-allocated GMutex/GCond safe.
static gboolean webview_main_sync_trampoline(gpointer data) {
webviewMainSyncCall *call = (webviewMainSyncCall *)data;
webviewMainThreadCallback(call->id);
g_mutex_lock(&call->mutex);
call->done = TRUE;
g_cond_signal(&call->cond);
g_mutex_unlock(&call->mutex);
return G_SOURCE_REMOVE;
}
// webview_invoke_on_main_sync schedules the Go callback identified by id on the
// default GTK main context and blocks the calling thread until it has finished.
//
// WebKit2GTK objects may only be touched on the thread running the GTK main loop
// (g_application_run). Asset-server responses are produced on worker goroutines,
// so the WebKit calls that complete a request must hop here first. The wait is
// safe because webkit_uri_scheme_request_finish_with_response returns before the
// response stream is drained (WebKit reads it asynchronously), so the main loop
// never blocks waiting on the worker.
//
// If the caller is already the main thread, g_main_context_invoke runs the
// trampoline inline, so the wait completes immediately without deadlocking.
static void webview_invoke_on_main_sync(uintptr_t id) {
webviewMainSyncCall call;
call.id = id;
call.done = FALSE;
g_mutex_init(&call.mutex);
g_cond_init(&call.cond);
// The enabled-check and the g_main_context_invoke that acts on it must be
// atomic with respect to webview_disable_main_dispatch. Holding
// webview_dispatch_mu across both means a worker either schedules onto a live
// loop or sees the loop already stopped — it can never schedule onto a loop
// that stops in between (which would block it here forever). The trampoline
// only touches the per-call mutex/cond, never webview_dispatch_mu, so when
// g_main_context_invoke runs it inline (main-thread caller) there is no
// self-deadlock.
g_mutex_lock(&webview_dispatch_mu);
if (!webview_main_dispatch_enabled) {
// The GTK main loop has stopped: a scheduled source would never run. The
// loop is no longer iterating, so the cross-thread race that makes
// main-thread confinement necessary is gone — running the callback inline
// on the worker lets in-flight asset requests drain during shutdown
// instead of wedging. See #5631 (review question 5).
g_mutex_unlock(&webview_dispatch_mu);
webviewMainThreadCallback(id);
g_mutex_clear(&call.mutex);
g_cond_clear(&call.cond);
return;
}
g_main_context_invoke(NULL, webview_main_sync_trampoline, &call);
g_mutex_unlock(&webview_dispatch_mu);
g_mutex_lock(&call.mutex);
while (!call.done) {
g_cond_wait(&call.cond, &call.mutex);
}
g_mutex_unlock(&call.mutex);
g_mutex_clear(&call.mutex);
g_cond_clear(&call.cond);
}
*/
import "C"
import (
"sync"
)
var (
mainSyncMu sync.Mutex
mainSyncNextID uintptr
mainSyncCallbacks = map[uintptr]func(){}
)
// invokeOnMainSync runs fn on the GTK main thread and blocks until it returns.
// It is safe to call from any goroutine, including the main thread itself.
func invokeOnMainSync(fn func()) {
mainSyncMu.Lock()
mainSyncNextID++
id := mainSyncNextID
mainSyncCallbacks[id] = fn
mainSyncMu.Unlock()
C.webview_invoke_on_main_sync(C.uintptr_t(id))
}
// DisableMainThreadDispatch marks the GTK main loop as stopped. After it is
// called, invokeOnMainSync runs callbacks inline on the calling goroutine
// instead of scheduling them onto the now-dead main loop, so asset-server
// workers that complete a request during shutdown cannot block forever waiting
// for a source that will never be serviced. The application layer calls this
// once g_application_run has returned. See issue #5631.
func DisableMainThreadDispatch() {
C.webview_disable_main_dispatch()
}
//export webviewMainThreadCallback
func webviewMainThreadCallback(id C.uintptr_t) {
mainSyncMu.Lock()
fn := mainSyncCallbacks[uintptr(id)]
delete(mainSyncCallbacks, uintptr(id))
mainSyncMu.Unlock()
if fn != nil {
fn()
}
}

View File

@@ -0,0 +1,90 @@
//go:build linux && cgo && !android
package webview
// This file provides cgo helpers for mainthread_linux_test.go. Go forbids cgo
// (an `import "C"` preamble) inside _test.go files, so the small GLib main-loop
// scaffolding the test needs lives here instead. None of it is referenced by
// production code paths.
/*
#cgo linux pkg-config: glib-2.0
#include <glib.h>
// All shared loop state is published and read under webview_test_mu so the
// worker goroutines that call the helpers below never observe it across threads
// without synchronization. webview_test_ready is signalled from inside the loop
// (via an idle source) so waiters block on the cond instead of spinning on an
// unsynchronized flag.
static GMutex webview_test_mu;
static GCond webview_test_cond;
static GMainLoop *webview_test_loop; // guarded by webview_test_mu
static GThread *webview_test_loop_thread; // guarded by webview_test_mu
static gboolean webview_test_ready; // guarded by webview_test_mu
// webview_test_mark_ready runs on the loop thread once the loop starts
// iterating, publishing that it is live.
static gboolean webview_test_mark_ready(gpointer data) {
g_mutex_lock(&webview_test_mu);
webview_test_ready = TRUE;
g_cond_signal(&webview_test_cond);
g_mutex_unlock(&webview_test_mu);
return G_SOURCE_REMOVE;
}
// webview_test_run_loop records the running thread and drives the default GLib
// main context, mimicking the GTK main loop that invokeOnMainSync dispatches to.
static void webview_test_run_loop(void) {
GMainLoop *loop = g_main_loop_new(NULL, FALSE);
g_mutex_lock(&webview_test_mu);
webview_test_loop = loop;
webview_test_loop_thread = g_thread_self();
g_mutex_unlock(&webview_test_mu);
g_idle_add(webview_test_mark_ready, NULL);
g_main_loop_run(loop);
g_mutex_lock(&webview_test_mu);
webview_test_loop = NULL;
webview_test_ready = FALSE;
g_mutex_unlock(&webview_test_mu);
g_main_loop_unref(loop);
}
static void webview_test_wait_running(void) {
g_mutex_lock(&webview_test_mu);
while (!webview_test_ready) {
g_cond_wait(&webview_test_cond, &webview_test_mu);
}
g_mutex_unlock(&webview_test_mu);
}
static void webview_test_quit_loop(void) {
g_mutex_lock(&webview_test_mu);
GMainLoop *loop = webview_test_loop;
g_mutex_unlock(&webview_test_mu);
if (loop != NULL) {
g_main_loop_quit(loop);
}
}
// webview_test_on_loop_thread reports whether the caller is the thread that runs
// the main loop.
static int webview_test_on_loop_thread(void) {
g_mutex_lock(&webview_test_mu);
GThread *loopThread = webview_test_loop_thread;
g_mutex_unlock(&webview_test_mu);
return g_thread_self() == loopThread ? 1 : 0;
}
*/
import "C"
func testRunMainLoop() { C.webview_test_run_loop() }
func testWaitLoopRunning() { C.webview_test_wait_running() }
func testQuitMainLoop() { C.webview_test_quit_loop() }
func testOnLoopThread() bool {
return C.webview_test_on_loop_thread() != 0
}

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,102 @@
//go:build android
package webview
import (
"bytes"
"io"
"net/http"
)
// Request interface for Android asset requests
// On Android, requests are handled via JNI from Java's WebViewAssetLoader
// androidRequest implements the Request interface for Android
type androidRequest struct {
url string
method string
headers http.Header
body io.ReadCloser
rw *androidResponseWriter
}
// NewRequestFromJNI creates a new request from JNI parameters
func NewRequestFromJNI(url string, method string, headersJSON string) Request {
return &androidRequest{
url: url,
method: method,
headers: http.Header{},
body: http.NoBody,
}
}
func (r *androidRequest) URL() (string, error) {
return r.url, nil
}
func (r *androidRequest) Method() (string, error) {
return r.method, nil
}
func (r *androidRequest) Header() (http.Header, error) {
return r.headers, nil
}
func (r *androidRequest) Body() (io.ReadCloser, error) {
return r.body, nil
}
func (r *androidRequest) Response() ResponseWriter {
if r.rw == nil {
r.rw = &androidResponseWriter{}
}
return r.rw
}
func (r *androidRequest) Close() error {
if r.body != nil {
return r.body.Close()
}
return nil
}
// androidResponseWriter implements ResponseWriter for Android
type androidResponseWriter struct {
statusCode int
headers http.Header
body bytes.Buffer
finished bool
}
func (w *androidResponseWriter) Header() http.Header {
if w.headers == nil {
w.headers = http.Header{}
}
return w.headers
}
func (w *androidResponseWriter) Write(data []byte) (int, error) {
return w.body.Write(data)
}
func (w *androidResponseWriter) WriteHeader(statusCode int) {
w.statusCode = statusCode
}
func (w *androidResponseWriter) Finish() error {
w.finished = true
return nil
}
// Code returns the HTTP status code of the response
func (w *androidResponseWriter) Code() int {
if w.statusCode == 0 {
return 200
}
return w.statusCode
}
// GetResponseData returns the response data for JNI
func (w *androidResponseWriter) GetResponseData() []byte {
return w.body.Bytes()
}

View File

@@ -0,0 +1,250 @@
//go:build darwin && !ios
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"
"errors"
"fmt"
"io"
"net/http"
"unsafe"
"encoding/json"
)
// 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()
}
r.Response().Finish()
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, errors.New("body: stream error")
case -2:
return 0, errors.New("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,248 @@
//go:build ios
package webview
/*
#cgo CFLAGS: -x objective-c -fobjc-arc
#cgo LDFLAGS: -framework Foundation -framework WebKit -framework CoreFoundation
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
#import <CoreFoundation/CoreFoundation.h>
#include <string.h>
static void URLSchemeTaskRetain(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (__bridge id<WKURLSchemeTask>) wkUrlSchemeTask;
CFRetain((CFTypeRef)urlSchemeTask);
}
static void URLSchemeTaskRelease(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (__bridge id<WKURLSchemeTask>) wkUrlSchemeTask;
CFRelease((CFTypeRef)urlSchemeTask);
}
static const char * URLSchemeTaskRequestURL(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (__bridge id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
return [urlSchemeTask.request.URL.absoluteString UTF8String];
}
}
static const char * URLSchemeTaskRequestMethod(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (__bridge id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
return [urlSchemeTask.request.HTTPMethod UTF8String];
}
}
static const char * URLSchemeTaskRequestHeadersJSON(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (__bridge 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];
const char *headerJSON = [headerString UTF8String];
return strdup(headerJSON);
}
}
static bool URLSchemeTaskRequestBodyBytes(void *wkUrlSchemeTask, const void **body, int *bodyLen) {
id<WKURLSchemeTask> urlSchemeTask = (__bridge 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 = (__bridge id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
if (!urlSchemeTask.request.HTTPBodyStream) {
return false;
}
[urlSchemeTask.request.HTTPBodyStream open];
return true;
}
}
static void URLSchemeTaskRequestBodyStreamClose(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (__bridge 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 = (__bridge 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"
"errors"
"fmt"
"io"
"net/http"
"unsafe"
"encoding/json"
)
// 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()
}
r.Response().Finish()
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, errors.New("body: stream error")
case -2:
return 0, errors.New("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,106 @@
//go:build linux && cgo && !gtk3 && !android
package webview
/*
#cgo linux pkg-config: gtk4 webkitgtk-6.0 gio-unix-2.0
#include <gtk/gtk.h>
#include <webkit/webkit.h>
static gboolean unref_request_on_main(gpointer data) {
if (data != NULL) {
g_object_unref(data);
}
return G_SOURCE_REMOVE;
}
// releaseRequestOnMainThread schedules the WebKitURISchemeRequest unref on the
// GTK main context. Close() runs on the assetserver goroutine, and dropping
// what may be the last reference finalizes a WebKit GObject — only safe on the
// UI thread (see #5557).
static void releaseRequestOnMainThread(WebKitURISchemeRequest *request) {
if (request == NULL) {
return;
}
g_main_context_invoke(NULL, unref_request_on_main, request);
}
*/
import "C"
import (
"io"
"net/http"
"unsafe"
)
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) {
// Reading the URI touches the WebKit-owned request on the GTK main loop;
// this runs on a worker goroutine, so it must hop to the main thread.
// See mainthread_linux.go and issue #5631.
var uri string
invokeOnMainSync(func() {
uri = C.GoString(C.webkit_uri_scheme_request_get_uri(r.req))
})
return uri, 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.releaseRequestOnMainThread(r.req)
return err
}

View File

@@ -0,0 +1,107 @@
//go:build linux && cgo && gtk3 && !android
package webview
/*
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.1 gio-unix-2.0
#include "gtk/gtk.h"
#include "webkit2/webkit2.h"
static gboolean unref_request_on_main(gpointer data) {
if (data != NULL) {
g_object_unref(data);
}
return G_SOURCE_REMOVE;
}
// releaseRequestOnMainThread schedules the WebKitURISchemeRequest unref on the
// GTK main context. Close() runs on the assetserver goroutine, and dropping
// what may be the last reference finalizes a WebKit GObject — only safe on the
// UI thread (see #5557).
static void releaseRequestOnMainThread(WebKitURISchemeRequest *request) {
if (request == NULL) {
return;
}
g_main_context_invoke(NULL, unref_request_on_main, request);
}
*/
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) {
// Reading the URI touches the WebKit-owned request on the GTK main loop;
// this runs on a worker goroutine, so it must hop to the main thread.
// See mainthread_linux.go and issue #5631.
var uri string
invokeOnMainSync(func() {
uri = C.GoString(C.webkit_uri_scheme_request_get_uri(r.req))
})
return uri, 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.releaseRequestOnMainThread(r.req)
return err
}

View File

@@ -0,0 +1,218 @@
//go:build windows
package webview
import (
"errors"
"fmt"
"io"
"net/http"
"github.com/wailsapp/wails/v3/internal/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 {
err := errors.Join(errs...)
if err != nil {
// errors.Join wraps even a single error.
// Check the filtered error list,
// and if it has just one element return it directly.
errs = err.(interface{ Unwrap() []error }).Unwrap()
if len(errs) == 1 {
return errs[0]
}
}
return err
}

View File

@@ -0,0 +1,28 @@
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
// Code returns the HTTP status code of the response
Code() int
}

View File

@@ -0,0 +1,156 @@
//go:build darwin && !ios
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 (
"net/http"
"unsafe"
"encoding/json"
)
var _ ResponseWriter = &responseWriter{}
type responseWriter struct {
r *request
header http.Header
wroteHeader bool
code int
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 content unsafe.Pointer
var contentLen int
if buf != nil && len(buf) > 0 {
content = unsafe.Pointer(&buf[0])
contentLen = len(buf)
}
if !C.URLSchemeTaskDidReceiveData(rw.r.task, content, C.int(contentLen)) {
return 0, errRequestStopped
}
return contentLen, nil
}
func (rw *responseWriter) WriteHeader(code int) {
rw.code = code
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
}
func (rw *responseWriter) Code() int {
return rw.code
}

View File

@@ -0,0 +1,161 @@
//go:build ios
package webview
/*
#cgo CFLAGS: -x objective-c -fobjc-arc
#cgo LDFLAGS: -framework Foundation -framework WebKit
#include <stdlib.h>
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
static bool urlSchemeTaskCall(void *wkUrlSchemeTask, schemeTaskCaller fn) {
id<WKURLSchemeTask> urlSchemeTask = (__bridge 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:nil headerFields:headerFields];
[urlSchemeTask didReceiveResponse:response];
});
}
*/
import "C"
import (
"net/http"
"unsafe"
"encoding/json"
)
var _ ResponseWriter = &responseWriter{}
type responseWriter struct {
r *request
header http.Header
wroteHeader bool
code int
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 content unsafe.Pointer
var contentLen int
// Guard on length, not just nil: a non-nil but empty slice (e.g. the body of
// a bound method that returned "") would make &buf[0] panic with an
// index-out-of-range, aborting the Go runtime. An empty body is valid — pass
// a nil pointer with length 0, which yields an empty NSData on the C side.
if len(buf) != 0 {
content = unsafe.Pointer(&buf[0])
contentLen = len(buf)
}
if !C.URLSchemeTaskDidReceiveData(rw.r.task, content, C.int(contentLen)) {
return 0, errRequestStopped
}
return contentLen, nil
}
func (rw *responseWriter) WriteHeader(code int) {
rw.code = code
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
}
func (rw *responseWriter) Code() int {
return rw.code
}

View File

@@ -0,0 +1,146 @@
//go:build linux && cgo && !gtk3 && !android
package webview
/*
#cgo linux pkg-config: gtk4 webkitgtk-6.0
#include <gtk/gtk.h>
#include <webkit/webkit.h>
// webview_asset_error_quark returns a stable GError domain for asset-server
// failures. The string literal is static storage, so g_quark_from_static_string
// interns it once and never leaks. Interning the per-request error message
// instead (the previous behaviour) grew the global quark table unboundedly on
// long-running apps, since GQuarks are never freed.
static GQuark webview_asset_error_quark(void) {
return g_quark_from_static_string("wails-webview-assetserver");
}
*/
import "C"
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"syscall"
"unsafe"
)
type responseWriter struct {
req *C.WebKitURISchemeRequest
header http.Header
wroteHeader bool
finished bool
code int
w io.WriteCloser
wErr error
}
func (rw *responseWriter) Code() int {
return rw.code
}
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) {
rw.code = code
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
}
}
rFD, w, err := pipe()
if err != nil {
rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to open pipe: %s", err))
return
}
rw.w = w
// webkit_uri_scheme_request_finish wraps the read end of the pipe in a
// GUnixInputStream and completes the request on the GTK main thread; the
// stream is created and released there too. See webkit_linux.go and #5631.
if err := webkit_uri_scheme_request_finish(rw.req, code, rw.Header(), rFD, 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())
defer C.free(unsafe.Pointer(msg))
// webkit_uri_scheme_request_finish_error touches the WebKit-owned request
// on the GTK main loop; this runs on an asset-server worker goroutine, so
// it must hop to the main thread. See mainthread_linux.go and issue #5631.
invokeOnMainSync(func() {
gerr := C.g_error_new_literal(C.webview_asset_error_quark(), C.int(code), msg)
C.webkit_uri_scheme_request_finish_error(rw.req, gerr)
C.g_error_free(gerr)
})
}
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,149 @@
//go:build linux && cgo && gtk3 && !android
package webview
/*
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.1
#include "gtk/gtk.h"
#include "webkit2/webkit2.h"
// webview_asset_error_quark returns a stable GError domain for asset-server
// failures. The string literal is static storage, so g_quark_from_static_string
// interns it once and never leaks. Interning the per-request error message
// instead (the previous behaviour) grew the global quark table unboundedly on
// long-running apps, since GQuarks are never freed.
static GQuark webview_asset_error_quark(void) {
return g_quark_from_static_string("wails-webview-assetserver");
}
*/
import "C"
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"syscall"
"unsafe"
)
type responseWriter struct {
req *C.WebKitURISchemeRequest
header http.Header
wroteHeader bool
finished bool
code int
w io.WriteCloser
wErr error
}
func (rw *responseWriter) Code() int {
return rw.code
}
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) {
rw.code = code
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
// webkit_uri_scheme_request_finish wraps the read end of the pipe in a
// GUnixInputStream and completes the request on the GTK main thread; the
// stream is created and released there too. See webkit_linux_gtk3.go and #5631.
if err := webkit_uri_scheme_request_finish(rw.req, code, rw.Header(), rFD, 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())
defer C.free(unsafe.Pointer(msg))
// webkit_uri_scheme_request_finish_error touches the WebKit-owned request
// on the GTK main loop; this runs on an asset-server worker goroutine, so
// it must hop to the main thread. See mainthread_linux.go and issue #5631.
invokeOnMainSync(func() {
gerr := C.g_error_new_literal(C.webview_asset_error_quark(), C.int(code), msg)
C.webkit_uri_scheme_request_finish_error(rw.req, gerr)
C.g_error_free(gerr)
})
}
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,109 @@
//go:build windows
package webview
import (
"bytes"
"errors"
"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, errors.New("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)
}
func (rw *responseWriter) Code() int {
return rw.code
}

View File

@@ -0,0 +1,181 @@
//go:build linux && cgo && !gtk3 && !android
package webview
/*
#cgo linux pkg-config: gtk4 webkitgtk-6.0 libsoup-3.0 gio-unix-2.0
#include <gtk/gtk.h>
#include <webkit/webkit.h>
#include <libsoup/soup.h>
#include <gio/gunixinputstream.h>
*/
import "C"
import (
"fmt"
"io"
"net/http"
"strings"
"unsafe"
)
const Webkit2MinMinorVersion = 0
func webkit_uri_scheme_request_get_http_method(req *C.WebKitURISchemeRequest) string {
// Reading request metadata touches the WebKit-owned request object, which
// belongs to the GTK main loop; this runs on a worker goroutine, so it must
// hop to the main thread. See mainthread_linux.go and issue #5631.
var method string
invokeOnMainSync(func() {
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 {
h := http.Header{}
// Reading and iterating the request's libsoup headers touches WebKit-owned
// state on the GTK main loop; this runs on a worker goroutine, so it must hop
// to the main thread. See mainthread_linux.go and issue #5631.
invokeOnMainSync(func() {
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
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, rFD int, streamLength int64) error {
// Completing the request touches WebKit/libsoup objects owned by the GTK
// main loop, but this runs on an asset-server worker goroutine. WebKit2GTK
// is not thread-safe, so the whole sequence must hop to the main thread.
//
// The response input stream is created and unref'd inside the same hop: it is
// ref-taken by webkit_uri_scheme_response_new on the main thread, so creating
// and releasing our reference here too keeps every refcount operation on a
// single thread. Previously the stream was built and unref'd on the worker
// while WebKit took its ref on the main thread, splitting the stream's
// refcount across threads. See mainthread_linux.go and issue #5631.
invokeOnMainSync(func() {
stream := C.g_unix_input_stream_new(C.int(rFD), C.gboolean(1))
defer C.g_object_unref(C.gpointer(stream))
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))
// Ownership of hdrs is transferred to the response by
// webkit_uri_scheme_response_set_http_headers (transfer full), so we must
// not unref it here — doing so frees the headers while WebKit/libsoup still
// reference them, crashing in soup_message_headers_iter_next on render.
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
}
func webkit_uri_scheme_request_get_http_body(req *C.WebKitURISchemeRequest) io.ReadCloser {
// Fetching the request body stream touches the WebKit-owned request on the
// GTK main loop; this runs on a worker goroutine, so it must hop to the main
// thread. See mainthread_linux.go and issue #5631.
var stream *C.GInputStream
invokeOnMainSync(func() {
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
}
func (r *webkitRequestBody) Read(p []byte) (int, error) {
if r.closed {
return 0, io.ErrClosedPipe
}
// io.Reader allows a zero-length read; taking &p[0] on an empty slice would
// panic, so return early before touching the backing array.
if len(p) == 0 {
return 0, nil
}
content := unsafe.Pointer(&p[0])
contentLen := len(p)
var n C.gsize
var gErr *C.GError
var res C.gboolean
// Reading the WebKit-owned request body stream must happen on the GTK main
// loop thread; this runs on a worker goroutine. See issue #5631.
invokeOnMainSync(func() {
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
var err error
var gErr *C.GError
// Closing and unref-ing the WebKit-owned request body stream finalizes a
// GObject tied to the GTK main loop; this runs on a worker goroutine, so it
// must hop to the main thread. See issue #5631.
invokeOnMainSync(func() {
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,185 @@
//go:build linux && cgo && gtk3 && !android
package webview
/*
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.1 libsoup-3.0 gio-unix-2.0
#include "gtk/gtk.h"
#include "webkit2/webkit2.h"
#include "libsoup/soup.h"
#include "gio/gunixinputstream.h"
*/
import "C"
import (
"fmt"
"io"
"net/http"
"strings"
"unsafe"
)
const Webkit2MinMinorVersion = 40
func webkit_uri_scheme_request_get_http_method(req *C.WebKitURISchemeRequest) string {
// Reading request metadata touches the WebKit-owned request object, which
// belongs to the GTK main loop; this runs on a worker goroutine, so it must
// hop to the main thread. See mainthread_linux.go and issue #5631.
var method string
invokeOnMainSync(func() {
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 {
h := http.Header{}
// Reading and iterating the request's libsoup headers touches WebKit-owned
// state on the GTK main loop; this runs on a worker goroutine, so it must hop
// to the main thread. See mainthread_linux.go and issue #5631.
invokeOnMainSync(func() {
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
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, rFD int, streamLength int64) error {
// Completing the request touches WebKit/libsoup objects owned by the GTK
// main loop, but this runs on an asset-server worker goroutine. WebKit2GTK
// is not thread-safe, so the whole sequence must hop to the main thread.
//
// The response input stream is created and unref'd inside the same hop: it is
// ref-taken by webkit_uri_scheme_response_new on the main thread, so creating
// and releasing our reference here too keeps every refcount operation on a
// single thread. Previously the stream was built and unref'd on the worker
// while WebKit took its ref on the main thread, splitting the stream's
// refcount across threads. See mainthread_linux.go and issue #5631.
invokeOnMainSync(func() {
stream := C.g_unix_input_stream_new(C.int(rFD), C.gboolean(1))
defer C.g_object_unref(C.gpointer(stream))
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))
// Ownership of hdrs is transferred to the response by
// webkit_uri_scheme_response_set_http_headers (transfer full), so we must
// not unref it here — doing so frees the headers while WebKit/libsoup still
// reference them, crashing in soup_message_headers_iter_next on render.
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
}
func webkit_uri_scheme_request_get_http_body(req *C.WebKitURISchemeRequest) io.ReadCloser {
// Fetching the request body stream touches the WebKit-owned request on the
// GTK main loop; this runs on a worker goroutine, so it must hop to the main
// thread. See mainthread_linux.go and issue #5631.
var stream *C.GInputStream
invokeOnMainSync(func() {
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
}
// io.Reader allows a zero-length read; taking &p[0] on an empty slice would
// panic, so return early before touching the backing array.
if len(p) == 0 {
return 0, nil
}
content := unsafe.Pointer(&p[0])
contentLen := len(p)
var n C.gsize
var gErr *C.GError
var res C.gboolean
// Reading the WebKit-owned request body stream must happen on the GTK main
// loop thread; this runs on a worker goroutine. See issue #5631.
invokeOnMainSync(func() {
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
// Closing and unref-ing the WebKit-owned request body stream finalizes a
// GObject tied to the GTK main loop; this runs on a worker goroutine, so it
// must hop to the main thread. See issue #5631.
invokeOnMainSync(func() {
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,12 @@
// Package browser provides functions to open URLs and files in the default browser.
package browser
// OpenURL opens the named URL in the default browser.
func OpenURL(url string) error {
return open(url)
}
// OpenFile opens the named file in the default browser or file handler.
func OpenFile(path string) error {
return open(path)
}

View File

@@ -0,0 +1,18 @@
//go:build darwin
package browser
import "os/exec"
var openCmd = func(target string) *exec.Cmd {
return exec.Command("open", target)
}
func open(target string) error {
cmd := openCmd(target)
if err := cmd.Start(); err != nil {
return err
}
go cmd.Wait() //nolint:errcheck
return nil
}

View File

@@ -0,0 +1,18 @@
//go:build !darwin && !windows
package browser
import "os/exec"
var openCmd = func(target string) *exec.Cmd {
return exec.Command("xdg-open", target)
}
func open(target string) error {
cmd := openCmd(target)
if err := cmd.Start(); err != nil {
return err
}
go cmd.Wait() //nolint:errcheck
return nil
}

View File

@@ -0,0 +1,18 @@
//go:build windows
package browser
import "os/exec"
var openCmd = func(target string) *exec.Cmd {
return exec.Command("rundll32", "url.dll,FileProtocolHandler", target)
}
func open(target string) error {
cmd := openCmd(target)
if err := cmd.Start(); err != nil {
return err
}
go cmd.Wait() //nolint:errcheck
return nil
}

View File

@@ -0,0 +1,18 @@
package capabilities
import "encoding/json"
type Capabilities struct {
HasNativeDrag bool `json:"HasNativeDrag"`
GTKVersion int `json:"GTKVersion"`
WebKitVersion string `json:"WebKitVersion"`
}
func (c Capabilities) AsBytes() []byte {
// JSON encode
result, err := json.Marshal(c)
if err != nil {
return []byte("{}")
}
return result
}

View File

@@ -0,0 +1,9 @@
//go:build darwin
package capabilities
func newCapabilities(_ string) Capabilities {
c := Capabilities{}
c.HasNativeDrag = false
return c
}

View File

@@ -0,0 +1,11 @@
//go:build linux && !gtk3
package capabilities
func NewCapabilities() Capabilities {
return Capabilities{
HasNativeDrag: true,
GTKVersion: 4,
WebKitVersion: "6.0",
}
}

View File

@@ -0,0 +1,11 @@
//go:build linux && gtk3
package capabilities
func NewCapabilities() Capabilities {
return Capabilities{
HasNativeDrag: true,
GTKVersion: 3,
WebKitVersion: "4.1",
}
}

View File

@@ -0,0 +1,22 @@
//go:build windows
package capabilities
import "github.com/wailsapp/wails/v3/internal/webview2/webviewloader"
type version string
func (v version) IsAtLeast(input string) bool {
result, err := webviewloader.CompareBrowserVersions(string(v), input)
if err != nil {
return false
}
return result >= 0
}
func NewCapabilities(webview2version string) Capabilities {
webview2 := version(webview2version)
c := Capabilities{}
c.HasNativeDrag = webview2.IsAtLeast("113.0.0.0")
return c
}

View File

View File

@@ -0,0 +1,483 @@
// Code generated by dbus-codegen-go DO NOT EDIT.
package menu
import (
"context"
"errors"
"fmt"
"github.com/godbus/dbus/v5"
"github.com/godbus/dbus/v5/introspect"
)
var (
// Introspection for com.canonical.dbusmenu
IntrospectDataDbusmenu = introspect.Interface{
Name: "com.canonical.dbusmenu",
Methods: []introspect.Method{{Name: "GetLayout", Args: []introspect.Arg{
{Name: "parentId", Type: "i", Direction: "in"},
{Name: "recursionDepth", Type: "i", Direction: "in"},
{Name: "propertyNames", Type: "as", Direction: "in"},
{Name: "revision", Type: "u", Direction: "out"},
{Name: "layout", Type: "(ia{sv}av)", Direction: "out"},
}},
{Name: "GetGroupProperties", Args: []introspect.Arg{
{Name: "ids", Type: "ai", Direction: "in"},
{Name: "propertyNames", Type: "as", Direction: "in"},
{Name: "properties", Type: "a(ia{sv})", Direction: "out"},
}},
{Name: "GetProperty", Args: []introspect.Arg{
{Name: "id", Type: "i", Direction: "in"},
{Name: "name", Type: "s", Direction: "in"},
{Name: "value", Type: "v", Direction: "out"},
}},
{Name: "Event", Args: []introspect.Arg{
{Name: "id", Type: "i", Direction: "in"},
{Name: "eventId", Type: "s", Direction: "in"},
{Name: "data", Type: "v", Direction: "in"},
{Name: "timestamp", Type: "u", Direction: "in"},
}},
{Name: "EventGroup", Args: []introspect.Arg{
{Name: "events", Type: "a(isvu)", Direction: "in"},
{Name: "idErrors", Type: "ai", Direction: "out"},
}},
{Name: "AboutToShow", Args: []introspect.Arg{
{Name: "id", Type: "i", Direction: "in"},
{Name: "needUpdate", Type: "b", Direction: "out"},
}},
{Name: "AboutToShowGroup", Args: []introspect.Arg{
{Name: "ids", Type: "ai", Direction: "in"},
{Name: "updatesNeeded", Type: "ai", Direction: "out"},
{Name: "idErrors", Type: "ai", Direction: "out"},
}},
},
Signals: []introspect.Signal{{Name: "ItemsPropertiesUpdated", Args: []introspect.Arg{
{Name: "updatedProps", Type: "a(ia{sv})", Direction: "out"},
{Name: "removedProps", Type: "a(ias)", Direction: "out"},
}},
{Name: "LayoutUpdated", Args: []introspect.Arg{
{Name: "revision", Type: "u", Direction: "out"},
{Name: "parent", Type: "i", Direction: "out"},
}},
{Name: "ItemActivationRequested", Args: []introspect.Arg{
{Name: "id", Type: "i", Direction: "out"},
{Name: "timestamp", Type: "u", Direction: "out"},
}},
},
Properties: []introspect.Property{{Name: "Version", Type: "u", Access: "read"},
{Name: "TextDirection", Type: "s", Access: "read"},
{Name: "Status", Type: "s", Access: "read"},
{Name: "IconThemePath", Type: "as", Access: "read"},
},
Annotations: []introspect.Annotation{},
}
)
// Signal is a common interface for all signals.
type Signal interface {
Name() string
Interface() string
Sender() string
path() dbus.ObjectPath
values() []interface{}
}
// Emit sends the given signal to the bus.
func Emit(conn *dbus.Conn, s Signal) error {
return conn.Emit(s.path(), s.Interface()+"."+s.Name(), s.values()...)
}
// ErrUnknownSignal is returned by LookupSignal when a signal cannot be resolved.
var ErrUnknownSignal = errors.New("unknown signal")
// LookupSignal converts the given raw D-Bus signal with variable body
// into one with typed structured body or returns ErrUnknownSignal error.
func LookupSignal(signal *dbus.Signal) (Signal, error) {
switch signal.Name {
case InterfaceDbusmenu + "." + "ItemsPropertiesUpdated":
v0, ok := signal.Body[0].([]struct {
V0 int32
V1 map[string]dbus.Variant
})
if !ok {
return nil, fmt.Errorf("prop .UpdatedProps is %T, not []struct {V0 int32;V1 map[string]dbus.Variant}", signal.Body[0])
}
v1, ok := signal.Body[1].([]struct {
V0 int32
V1 []string
})
if !ok {
return nil, fmt.Errorf("prop .RemovedProps is %T, not []struct {V0 int32;V1 []string}", signal.Body[1])
}
return &Dbusmenu_ItemsPropertiesUpdatedSignal{
sender: signal.Sender,
Path: signal.Path,
Body: &Dbusmenu_ItemsPropertiesUpdatedSignalBody{
UpdatedProps: v0,
RemovedProps: v1,
},
}, nil
case InterfaceDbusmenu + "." + "LayoutUpdated":
v0, ok := signal.Body[0].(uint32)
if !ok {
return nil, fmt.Errorf("prop .Revision is %T, not uint32", signal.Body[0])
}
v1, ok := signal.Body[1].(int32)
if !ok {
return nil, fmt.Errorf("prop .Parent is %T, not int32", signal.Body[1])
}
return &Dbusmenu_LayoutUpdatedSignal{
sender: signal.Sender,
Path: signal.Path,
Body: &Dbusmenu_LayoutUpdatedSignalBody{
Revision: v0,
Parent: v1,
},
}, nil
case InterfaceDbusmenu + "." + "ItemActivationRequested":
v0, ok := signal.Body[0].(int32)
if !ok {
return nil, fmt.Errorf("prop .Id is %T, not int32", signal.Body[0])
}
v1, ok := signal.Body[1].(uint32)
if !ok {
return nil, fmt.Errorf("prop .Timestamp is %T, not uint32", signal.Body[1])
}
return &Dbusmenu_ItemActivationRequestedSignal{
sender: signal.Sender,
Path: signal.Path,
Body: &Dbusmenu_ItemActivationRequestedSignalBody{
Id: v0,
Timestamp: v1,
},
}, nil
default:
return nil, ErrUnknownSignal
}
}
// AddMatchSignal registers a match rule for the given signal,
// opts are appended to the automatically generated signal's rules.
func AddMatchSignal(conn *dbus.Conn, s Signal, opts ...dbus.MatchOption) error {
return conn.AddMatchSignal(append([]dbus.MatchOption{
dbus.WithMatchInterface(s.Interface()),
dbus.WithMatchMember(s.Name()),
}, opts...)...)
}
// RemoveMatchSignal unregisters the previously registered subscription.
func RemoveMatchSignal(conn *dbus.Conn, s Signal, opts ...dbus.MatchOption) error {
return conn.RemoveMatchSignal(append([]dbus.MatchOption{
dbus.WithMatchInterface(s.Interface()),
dbus.WithMatchMember(s.Name()),
}, opts...)...)
}
// Interface name constants.
const (
InterfaceDbusmenu = "com.canonical.dbusmenu"
)
// Dbusmenuer is com.canonical.dbusmenu interface.
type Dbusmenuer interface {
// GetLayout is com.canonical.dbusmenu.GetLayout method.
GetLayout(parentId int32, recursionDepth int32, propertyNames []string) (revision uint32, layout struct {
V0 int32
V1 map[string]dbus.Variant
V2 []dbus.Variant
}, err *dbus.Error)
// GetGroupProperties is com.canonical.dbusmenu.GetGroupProperties method.
GetGroupProperties(ids []int32, propertyNames []string) (properties []struct {
V0 int32
V1 map[string]dbus.Variant
}, err *dbus.Error)
// GetProperty is com.canonical.dbusmenu.GetProperty method.
GetProperty(id int32, name string) (value dbus.Variant, err *dbus.Error)
// Event is com.canonical.dbusmenu.Event method.
Event(id int32, eventId string, data dbus.Variant, timestamp uint32) (err *dbus.Error)
// EventGroup is com.canonical.dbusmenu.EventGroup method.
EventGroup(events []struct {
V0 int32
V1 string
V2 dbus.Variant
V3 uint32
}) (idErrors []int32, err *dbus.Error)
// AboutToShow is com.canonical.dbusmenu.AboutToShow method.
AboutToShow(id int32) (needUpdate bool, err *dbus.Error)
// AboutToShowGroup is com.canonical.dbusmenu.AboutToShowGroup method.
AboutToShowGroup(ids []int32) (updatesNeeded []int32, idErrors []int32, err *dbus.Error)
}
// ExportDbusmenu exports the given object that implements com.canonical.dbusmenu on the bus.
func ExportDbusmenu(conn *dbus.Conn, path dbus.ObjectPath, v Dbusmenuer) error {
return conn.ExportSubtreeMethodTable(map[string]interface{}{
"GetLayout": v.GetLayout,
"GetGroupProperties": v.GetGroupProperties,
"GetProperty": v.GetProperty,
"Event": v.Event,
"EventGroup": v.EventGroup,
"AboutToShow": v.AboutToShow,
"AboutToShowGroup": v.AboutToShowGroup,
}, path, InterfaceDbusmenu)
}
// UnexportDbusmenu unexports com.canonical.dbusmenu interface on the named path.
func UnexportDbusmenu(conn *dbus.Conn, path dbus.ObjectPath) error {
return conn.Export(nil, path, InterfaceDbusmenu)
}
// UnimplementedDbusmenu can be embedded to have forward compatible server implementations.
type UnimplementedDbusmenu struct{}
func (*UnimplementedDbusmenu) iface() string {
return InterfaceDbusmenu
}
func (*UnimplementedDbusmenu) GetLayout(parentId int32, recursionDepth int32, propertyNames []string) (revision uint32, layout struct {
V0 int32
V1 map[string]dbus.Variant
V2 []dbus.Variant
}, err *dbus.Error) {
err = &dbus.ErrMsgUnknownMethod
return
}
func (*UnimplementedDbusmenu) GetGroupProperties(ids []int32, propertyNames []string) (properties []struct {
V0 int32
V1 map[string]dbus.Variant
}, err *dbus.Error) {
err = &dbus.ErrMsgUnknownMethod
return
}
func (*UnimplementedDbusmenu) GetProperty(id int32, name string) (value dbus.Variant, err *dbus.Error) {
err = &dbus.ErrMsgUnknownMethod
return
}
func (*UnimplementedDbusmenu) Event(id int32, eventId string, data dbus.Variant, timestamp uint32) (err *dbus.Error) {
err = &dbus.ErrMsgUnknownMethod
return
}
func (*UnimplementedDbusmenu) EventGroup(events []struct {
V0 int32
V1 string
V2 dbus.Variant
V3 uint32
}) (idErrors []int32, err *dbus.Error) {
err = &dbus.ErrMsgUnknownMethod
return
}
func (*UnimplementedDbusmenu) AboutToShow(id int32) (needUpdate bool, err *dbus.Error) {
err = &dbus.ErrMsgUnknownMethod
return
}
func (*UnimplementedDbusmenu) AboutToShowGroup(ids []int32) (updatesNeeded []int32, idErrors []int32, err *dbus.Error) {
err = &dbus.ErrMsgUnknownMethod
return
}
// NewDbusmenu creates and allocates com.canonical.dbusmenu.
func NewDbusmenu(object dbus.BusObject) *Dbusmenu {
return &Dbusmenu{object}
}
// Dbusmenu implements com.canonical.dbusmenu D-Bus interface.
type Dbusmenu struct {
object dbus.BusObject
}
// GetLayout calls com.canonical.dbusmenu.GetLayout method.
func (o *Dbusmenu) GetLayout(ctx context.Context, parentId int32, recursionDepth int32, propertyNames []string) (revision uint32, layout struct {
V0 int32
V1 map[string]dbus.Variant
V2 []dbus.Variant
}, err error) {
err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".GetLayout", 0, parentId, recursionDepth, propertyNames).Store(&revision, &layout)
return
}
// GetGroupProperties calls com.canonical.dbusmenu.GetGroupProperties method.
func (o *Dbusmenu) GetGroupProperties(ctx context.Context, ids []int32, propertyNames []string) (properties []struct {
V0 int32
V1 map[string]dbus.Variant
}, err error) {
err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".GetGroupProperties", 0, ids, propertyNames).Store(&properties)
return
}
// GetProperty calls com.canonical.dbusmenu.GetProperty method.
func (o *Dbusmenu) GetProperty(ctx context.Context, id int32, name string) (value dbus.Variant, err error) {
err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".GetProperty", 0, id, name).Store(&value)
return
}
// Event calls com.canonical.dbusmenu.Event method.
func (o *Dbusmenu) Event(ctx context.Context, id int32, eventId string, data dbus.Variant, timestamp uint32) (err error) {
err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".Event", 0, id, eventId, data, timestamp).Store()
return
}
// EventGroup calls com.canonical.dbusmenu.EventGroup method.
func (o *Dbusmenu) EventGroup(ctx context.Context, events []struct {
V0 int32
V1 string
V2 dbus.Variant
V3 uint32
}) (idErrors []int32, err error) {
err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".EventGroup", 0, events).Store(&idErrors)
return
}
// AboutToShow calls com.canonical.dbusmenu.AboutToShow method.
func (o *Dbusmenu) AboutToShow(ctx context.Context, id int32) (needUpdate bool, err error) {
err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".AboutToShow", 0, id).Store(&needUpdate)
return
}
// AboutToShowGroup calls com.canonical.dbusmenu.AboutToShowGroup method.
func (o *Dbusmenu) AboutToShowGroup(ctx context.Context, ids []int32) (updatesNeeded []int32, idErrors []int32, err error) {
err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".AboutToShowGroup", 0, ids).Store(&updatesNeeded, &idErrors)
return
}
// GetVersion gets com.canonical.dbusmenu.Version property.
func (o *Dbusmenu) GetVersion(ctx context.Context) (version uint32, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceDbusmenu, "Version").Store(&version)
return
}
// GetTextDirection gets com.canonical.dbusmenu.TextDirection property.
func (o *Dbusmenu) GetTextDirection(ctx context.Context) (textDirection string, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceDbusmenu, "TextDirection").Store(&textDirection)
return
}
// GetStatus gets com.canonical.dbusmenu.Status property.
func (o *Dbusmenu) GetStatus(ctx context.Context) (status string, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceDbusmenu, "Status").Store(&status)
return
}
// GetIconThemePath gets com.canonical.dbusmenu.IconThemePath property.
func (o *Dbusmenu) GetIconThemePath(ctx context.Context) (iconThemePath []string, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceDbusmenu, "IconThemePath").Store(&iconThemePath)
return
}
// Dbusmenu_ItemsPropertiesUpdatedSignal represents com.canonical.dbusmenu.ItemsPropertiesUpdated signal.
type Dbusmenu_ItemsPropertiesUpdatedSignal struct {
sender string
Path dbus.ObjectPath
Body *Dbusmenu_ItemsPropertiesUpdatedSignalBody
}
// Name returns the signal's name.
func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) Name() string {
return "ItemsPropertiesUpdated"
}
// Interface returns the signal's interface.
func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) Interface() string {
return InterfaceDbusmenu
}
// Sender returns the signal's sender unique name.
func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) Sender() string {
return s.sender
}
func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) path() dbus.ObjectPath {
return s.Path
}
func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) values() []interface{} {
return []interface{}{s.Body.UpdatedProps, s.Body.RemovedProps}
}
// Dbusmenu_ItemsPropertiesUpdatedSignalBody is body container.
type Dbusmenu_ItemsPropertiesUpdatedSignalBody struct {
UpdatedProps []struct {
V0 int32
V1 map[string]dbus.Variant
}
RemovedProps []struct {
V0 int32
V1 []string
}
}
// Dbusmenu_LayoutUpdatedSignal represents com.canonical.dbusmenu.LayoutUpdated signal.
type Dbusmenu_LayoutUpdatedSignal struct {
sender string
Path dbus.ObjectPath
Body *Dbusmenu_LayoutUpdatedSignalBody
}
// Name returns the signal's name.
func (s *Dbusmenu_LayoutUpdatedSignal) Name() string {
return "LayoutUpdated"
}
// Interface returns the signal's interface.
func (s *Dbusmenu_LayoutUpdatedSignal) Interface() string {
return InterfaceDbusmenu
}
// Sender returns the signal's sender unique name.
func (s *Dbusmenu_LayoutUpdatedSignal) Sender() string {
return s.sender
}
func (s *Dbusmenu_LayoutUpdatedSignal) path() dbus.ObjectPath {
return s.Path
}
func (s *Dbusmenu_LayoutUpdatedSignal) values() []interface{} {
return []interface{}{s.Body.Revision, s.Body.Parent}
}
// Dbusmenu_LayoutUpdatedSignalBody is body container.
type Dbusmenu_LayoutUpdatedSignalBody struct {
Revision uint32
Parent int32
}
// Dbusmenu_ItemActivationRequestedSignal represents com.canonical.dbusmenu.ItemActivationRequested signal.
type Dbusmenu_ItemActivationRequestedSignal struct {
sender string
Path dbus.ObjectPath
Body *Dbusmenu_ItemActivationRequestedSignalBody
}
// Name returns the signal's name.
func (s *Dbusmenu_ItemActivationRequestedSignal) Name() string {
return "ItemActivationRequested"
}
// Interface returns the signal's interface.
func (s *Dbusmenu_ItemActivationRequestedSignal) Interface() string {
return InterfaceDbusmenu
}
// Sender returns the signal's sender unique name.
func (s *Dbusmenu_ItemActivationRequestedSignal) Sender() string {
return s.sender
}
func (s *Dbusmenu_ItemActivationRequestedSignal) path() dbus.ObjectPath {
return s.Path
}
func (s *Dbusmenu_ItemActivationRequestedSignal) values() []interface{} {
return []interface{}{s.Body.Id, s.Body.Timestamp}
}
// Dbusmenu_ItemActivationRequestedSignalBody is body container.
type Dbusmenu_ItemActivationRequestedSignalBody struct {
Id int32
Timestamp uint32
}

View File

View File

@@ -0,0 +1,636 @@
// Code generated by dbus-codegen-go DO NOT EDIT.
package notifier
import (
"context"
"errors"
"fmt"
"github.com/godbus/dbus/v5"
"github.com/godbus/dbus/v5/introspect"
)
var (
// Introspection for org.kde.StatusNotifierItem
IntrospectDataStatusNotifierItem = introspect.Interface{
Name: "org.kde.StatusNotifierItem",
Methods: []introspect.Method{{Name: "ContextMenu", Args: []introspect.Arg{
{Name: "x", Type: "i", Direction: "in"},
{Name: "y", Type: "i", Direction: "in"},
}},
{Name: "Activate", Args: []introspect.Arg{
{Name: "x", Type: "i", Direction: "in"},
{Name: "y", Type: "i", Direction: "in"},
}},
{Name: "SecondaryActivate", Args: []introspect.Arg{
{Name: "x", Type: "i", Direction: "in"},
{Name: "y", Type: "i", Direction: "in"},
}},
{Name: "Scroll", Args: []introspect.Arg{
{Name: "delta", Type: "i", Direction: "in"},
{Name: "orientation", Type: "s", Direction: "in"},
}},
},
Signals: []introspect.Signal{{Name: "NewTitle"},
{Name: "NewIcon"},
{Name: "NewAttentionIcon"},
{Name: "NewOverlayIcon"},
{Name: "NewStatus", Args: []introspect.Arg{
{Name: "status", Type: "s", Direction: ""},
}},
{Name: "NewIconThemePath", Args: []introspect.Arg{
{Name: "icon_theme_path", Type: "s", Direction: "out"},
}},
{Name: "NewMenu"},
},
Properties: []introspect.Property{{Name: "Category", Type: "s", Access: "read"},
{Name: "Id", Type: "s", Access: "read"},
{Name: "Title", Type: "s", Access: "read"},
{Name: "Status", Type: "s", Access: "read"},
{Name: "WindowId", Type: "i", Access: "read"},
{Name: "IconThemePath", Type: "s", Access: "read"},
{Name: "Menu", Type: "o", Access: "read"},
{Name: "ItemIsMenu", Type: "b", Access: "read"},
{Name: "IconName", Type: "s", Access: "read"},
{Name: "IconPixmap", Type: "a(iiay)", Access: "read", Annotations: []introspect.Annotation{
{Name: "org.qtproject.QtDBus.QtTypeName", Value: "KDbusImageVector"},
}},
{Name: "OverlayIconName", Type: "s", Access: "read"},
{Name: "OverlayIconPixmap", Type: "a(iiay)", Access: "read", Annotations: []introspect.Annotation{
{Name: "org.qtproject.QtDBus.QtTypeName", Value: "KDbusImageVector"},
}},
{Name: "AttentionIconName", Type: "s", Access: "read"},
{Name: "AttentionIconPixmap", Type: "a(iiay)", Access: "read", Annotations: []introspect.Annotation{
{Name: "org.qtproject.QtDBus.QtTypeName", Value: "KDbusImageVector"},
}},
{Name: "AttentionMovieName", Type: "s", Access: "read"},
{Name: "ToolTip", Type: "(sa(iiay)ss)", Access: "read", Annotations: []introspect.Annotation{
{Name: "org.qtproject.QtDBus.QtTypeName", Value: "KDbusToolTipStruct"},
}},
},
Annotations: []introspect.Annotation{},
}
)
// Signal is a common interface for all signals.
type Signal interface {
Name() string
Interface() string
Sender() string
path() dbus.ObjectPath
values() []interface{}
}
// Emit sends the given signal to the bus.
func Emit(conn *dbus.Conn, s Signal) error {
return conn.Emit(s.path(), s.Interface()+"."+s.Name(), s.values()...)
}
// ErrUnknownSignal is returned by LookupSignal when a signal cannot be resolved.
var ErrUnknownSignal = errors.New("unknown signal")
// LookupSignal converts the given raw D-Bus signal with variable body
// into one with typed structured body or returns ErrUnknownSignal error.
func LookupSignal(signal *dbus.Signal) (Signal, error) {
switch signal.Name {
case InterfaceStatusNotifierItem + "." + "NewTitle":
return &StatusNotifierItem_NewTitleSignal{
sender: signal.Sender,
Path: signal.Path,
Body: &StatusNotifierItem_NewTitleSignalBody{},
}, nil
case InterfaceStatusNotifierItem + "." + "NewIcon":
return &StatusNotifierItem_NewIconSignal{
sender: signal.Sender,
Path: signal.Path,
Body: &StatusNotifierItem_NewIconSignalBody{},
}, nil
case InterfaceStatusNotifierItem + "." + "NewAttentionIcon":
return &StatusNotifierItem_NewAttentionIconSignal{
sender: signal.Sender,
Path: signal.Path,
Body: &StatusNotifierItem_NewAttentionIconSignalBody{},
}, nil
case InterfaceStatusNotifierItem + "." + "NewOverlayIcon":
return &StatusNotifierItem_NewOverlayIconSignal{
sender: signal.Sender,
Path: signal.Path,
Body: &StatusNotifierItem_NewOverlayIconSignalBody{},
}, nil
case InterfaceStatusNotifierItem + "." + "NewStatus":
v0, ok := signal.Body[0].(string)
if !ok {
return nil, fmt.Errorf("prop .Status is %T, not string", signal.Body[0])
}
return &StatusNotifierItem_NewStatusSignal{
sender: signal.Sender,
Path: signal.Path,
Body: &StatusNotifierItem_NewStatusSignalBody{
Status: v0,
},
}, nil
case InterfaceStatusNotifierItem + "." + "NewIconThemePath":
v0, ok := signal.Body[0].(string)
if !ok {
return nil, fmt.Errorf("prop .IconThemePath is %T, not string", signal.Body[0])
}
return &StatusNotifierItem_NewIconThemePathSignal{
sender: signal.Sender,
Path: signal.Path,
Body: &StatusNotifierItem_NewIconThemePathSignalBody{
IconThemePath: v0,
},
}, nil
case InterfaceStatusNotifierItem + "." + "NewMenu":
return &StatusNotifierItem_NewMenuSignal{
sender: signal.Sender,
Path: signal.Path,
Body: &StatusNotifierItem_NewMenuSignalBody{},
}, nil
default:
return nil, ErrUnknownSignal
}
}
// AddMatchSignal registers a match rule for the given signal,
// opts are appended to the automatically generated signal's rules.
func AddMatchSignal(conn *dbus.Conn, s Signal, opts ...dbus.MatchOption) error {
return conn.AddMatchSignal(append([]dbus.MatchOption{
dbus.WithMatchInterface(s.Interface()),
dbus.WithMatchMember(s.Name()),
}, opts...)...)
}
// RemoveMatchSignal unregisters the previously registered subscription.
func RemoveMatchSignal(conn *dbus.Conn, s Signal, opts ...dbus.MatchOption) error {
return conn.RemoveMatchSignal(append([]dbus.MatchOption{
dbus.WithMatchInterface(s.Interface()),
dbus.WithMatchMember(s.Name()),
}, opts...)...)
}
// Interface name constants.
const (
InterfaceStatusNotifierItem = "org.kde.StatusNotifierItem"
)
// StatusNotifierItemer is org.kde.StatusNotifierItem interface.
type StatusNotifierItemer interface {
// ContextMenu is org.kde.StatusNotifierItem.ContextMenu method.
ContextMenu(x int32, y int32) (err *dbus.Error)
// Activate is org.kde.StatusNotifierItem.Activate method.
Activate(x int32, y int32) (err *dbus.Error)
// SecondaryActivate is org.kde.StatusNotifierItem.SecondaryActivate method.
SecondaryActivate(x int32, y int32) (err *dbus.Error)
// Scroll is org.kde.StatusNotifierItem.Scroll method.
Scroll(delta int32, orientation string) (err *dbus.Error)
}
// ExportStatusNotifierItem exports the given object that implements org.kde.StatusNotifierItem on the bus.
func ExportStatusNotifierItem(conn *dbus.Conn, path dbus.ObjectPath, v StatusNotifierItemer) error {
return conn.ExportSubtreeMethodTable(map[string]interface{}{
"ContextMenu": v.ContextMenu,
"Activate": v.Activate,
"SecondaryActivate": v.SecondaryActivate,
"Scroll": v.Scroll,
}, path, InterfaceStatusNotifierItem)
}
// UnexportStatusNotifierItem unexports org.kde.StatusNotifierItem interface on the named path.
func UnexportStatusNotifierItem(conn *dbus.Conn, path dbus.ObjectPath) error {
return conn.Export(nil, path, InterfaceStatusNotifierItem)
}
// UnimplementedStatusNotifierItem can be embedded to have forward compatible server implementations.
type UnimplementedStatusNotifierItem struct{}
func (*UnimplementedStatusNotifierItem) iface() string {
return InterfaceStatusNotifierItem
}
func (*UnimplementedStatusNotifierItem) ContextMenu(x int32, y int32) (err *dbus.Error) {
err = &dbus.ErrMsgUnknownMethod
return
}
func (*UnimplementedStatusNotifierItem) Activate(x int32, y int32) (err *dbus.Error) {
err = &dbus.ErrMsgUnknownMethod
return
}
func (*UnimplementedStatusNotifierItem) SecondaryActivate(x int32, y int32) (err *dbus.Error) {
err = &dbus.ErrMsgUnknownMethod
return
}
func (*UnimplementedStatusNotifierItem) Scroll(delta int32, orientation string) (err *dbus.Error) {
err = &dbus.ErrMsgUnknownMethod
return
}
// NewStatusNotifierItem creates and allocates org.kde.StatusNotifierItem.
func NewStatusNotifierItem(object dbus.BusObject) *StatusNotifierItem {
return &StatusNotifierItem{object}
}
// StatusNotifierItem implements org.kde.StatusNotifierItem D-Bus interface.
type StatusNotifierItem struct {
object dbus.BusObject
}
// ContextMenu calls org.kde.StatusNotifierItem.ContextMenu method.
func (o *StatusNotifierItem) ContextMenu(ctx context.Context, x int32, y int32) (err error) {
err = o.object.CallWithContext(ctx, InterfaceStatusNotifierItem+".ContextMenu", 0, x, y).Store()
return
}
// Activate calls org.kde.StatusNotifierItem.Activate method.
func (o *StatusNotifierItem) Activate(ctx context.Context, x int32, y int32) (err error) {
err = o.object.CallWithContext(ctx, InterfaceStatusNotifierItem+".Activate", 0, x, y).Store()
return
}
// SecondaryActivate calls org.kde.StatusNotifierItem.SecondaryActivate method.
func (o *StatusNotifierItem) SecondaryActivate(ctx context.Context, x int32, y int32) (err error) {
err = o.object.CallWithContext(ctx, InterfaceStatusNotifierItem+".SecondaryActivate", 0, x, y).Store()
return
}
// Scroll calls org.kde.StatusNotifierItem.Scroll method.
func (o *StatusNotifierItem) Scroll(ctx context.Context, delta int32, orientation string) (err error) {
err = o.object.CallWithContext(ctx, InterfaceStatusNotifierItem+".Scroll", 0, delta, orientation).Store()
return
}
// GetCategory gets org.kde.StatusNotifierItem.Category property.
func (o *StatusNotifierItem) GetCategory(ctx context.Context) (category string, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "Category").Store(&category)
return
}
// GetId gets org.kde.StatusNotifierItem.Id property.
func (o *StatusNotifierItem) GetId(ctx context.Context) (id string, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "Id").Store(&id)
return
}
// GetTitle gets org.kde.StatusNotifierItem.Title property.
func (o *StatusNotifierItem) GetTitle(ctx context.Context) (title string, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "Title").Store(&title)
return
}
// GetStatus gets org.kde.StatusNotifierItem.Status property.
func (o *StatusNotifierItem) GetStatus(ctx context.Context) (status string, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "Status").Store(&status)
return
}
// GetWindowId gets org.kde.StatusNotifierItem.WindowId property.
func (o *StatusNotifierItem) GetWindowId(ctx context.Context) (windowId int32, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "WindowId").Store(&windowId)
return
}
// GetIconThemePath gets org.kde.StatusNotifierItem.IconThemePath property.
func (o *StatusNotifierItem) GetIconThemePath(ctx context.Context) (iconThemePath string, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "IconThemePath").Store(&iconThemePath)
return
}
// GetMenu gets org.kde.StatusNotifierItem.Menu property.
func (o *StatusNotifierItem) GetMenu(ctx context.Context) (menu dbus.ObjectPath, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "Menu").Store(&menu)
return
}
// GetItemIsMenu gets org.kde.StatusNotifierItem.ItemIsMenu property.
func (o *StatusNotifierItem) GetItemIsMenu(ctx context.Context) (itemIsMenu bool, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "ItemIsMenu").Store(&itemIsMenu)
return
}
// GetIconName gets org.kde.StatusNotifierItem.IconName property.
func (o *StatusNotifierItem) GetIconName(ctx context.Context) (iconName string, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "IconName").Store(&iconName)
return
}
// GetIconPixmap gets org.kde.StatusNotifierItem.IconPixmap property.
//
// Annotations:
//
// @org.qtproject.QtDBus.QtTypeName = KDbusImageVector
func (o *StatusNotifierItem) GetIconPixmap(ctx context.Context) (iconPixmap []struct {
V0 int32
V1 int32
V2 []byte
}, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "IconPixmap").Store(&iconPixmap)
return
}
// GetOverlayIconName gets org.kde.StatusNotifierItem.OverlayIconName property.
func (o *StatusNotifierItem) GetOverlayIconName(ctx context.Context) (overlayIconName string, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "OverlayIconName").Store(&overlayIconName)
return
}
// GetOverlayIconPixmap gets org.kde.StatusNotifierItem.OverlayIconPixmap property.
//
// Annotations:
//
// @org.qtproject.QtDBus.QtTypeName = KDbusImageVector
func (o *StatusNotifierItem) GetOverlayIconPixmap(ctx context.Context) (overlayIconPixmap []struct {
V0 int32
V1 int32
V2 []byte
}, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "OverlayIconPixmap").Store(&overlayIconPixmap)
return
}
// GetAttentionIconName gets org.kde.StatusNotifierItem.AttentionIconName property.
func (o *StatusNotifierItem) GetAttentionIconName(ctx context.Context) (attentionIconName string, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "AttentionIconName").Store(&attentionIconName)
return
}
// GetAttentionIconPixmap gets org.kde.StatusNotifierItem.AttentionIconPixmap property.
//
// Annotations:
//
// @org.qtproject.QtDBus.QtTypeName = KDbusImageVector
func (o *StatusNotifierItem) GetAttentionIconPixmap(ctx context.Context) (attentionIconPixmap []struct {
V0 int32
V1 int32
V2 []byte
}, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "AttentionIconPixmap").Store(&attentionIconPixmap)
return
}
// GetAttentionMovieName gets org.kde.StatusNotifierItem.AttentionMovieName property.
func (o *StatusNotifierItem) GetAttentionMovieName(ctx context.Context) (attentionMovieName string, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "AttentionMovieName").Store(&attentionMovieName)
return
}
// GetToolTip gets org.kde.StatusNotifierItem.ToolTip property.
//
// Annotations:
//
// @org.qtproject.QtDBus.QtTypeName = KDbusToolTipStruct
func (o *StatusNotifierItem) GetToolTip(ctx context.Context) (toolTip struct {
V0 string
V1 []struct {
V0 int32
V1 int32
V2 []byte
}
V2 string
V3 string
}, err error) {
err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "ToolTip").Store(&toolTip)
return
}
// StatusNotifierItem_NewTitleSignal represents org.kde.StatusNotifierItem.NewTitle signal.
type StatusNotifierItem_NewTitleSignal struct {
sender string
Path dbus.ObjectPath
Body *StatusNotifierItem_NewTitleSignalBody
}
// Name returns the signal's name.
func (s *StatusNotifierItem_NewTitleSignal) Name() string {
return "NewTitle"
}
// Interface returns the signal's interface.
func (s *StatusNotifierItem_NewTitleSignal) Interface() string {
return InterfaceStatusNotifierItem
}
// Sender returns the signal's sender unique name.
func (s *StatusNotifierItem_NewTitleSignal) Sender() string {
return s.sender
}
func (s *StatusNotifierItem_NewTitleSignal) path() dbus.ObjectPath {
return s.Path
}
func (s *StatusNotifierItem_NewTitleSignal) values() []interface{} {
return []interface{}{}
}
// StatusNotifierItem_NewTitleSignalBody is body container.
type StatusNotifierItem_NewTitleSignalBody struct {
}
// StatusNotifierItem_NewIconSignal represents org.kde.StatusNotifierItem.NewIcon signal.
type StatusNotifierItem_NewIconSignal struct {
sender string
Path dbus.ObjectPath
Body *StatusNotifierItem_NewIconSignalBody
}
// Name returns the signal's name.
func (s *StatusNotifierItem_NewIconSignal) Name() string {
return "NewIcon"
}
// Interface returns the signal's interface.
func (s *StatusNotifierItem_NewIconSignal) Interface() string {
return InterfaceStatusNotifierItem
}
// Sender returns the signal's sender unique name.
func (s *StatusNotifierItem_NewIconSignal) Sender() string {
return s.sender
}
func (s *StatusNotifierItem_NewIconSignal) path() dbus.ObjectPath {
return s.Path
}
func (s *StatusNotifierItem_NewIconSignal) values() []interface{} {
return []interface{}{}
}
// StatusNotifierItem_NewIconSignalBody is body container.
type StatusNotifierItem_NewIconSignalBody struct {
}
// StatusNotifierItem_NewAttentionIconSignal represents org.kde.StatusNotifierItem.NewAttentionIcon signal.
type StatusNotifierItem_NewAttentionIconSignal struct {
sender string
Path dbus.ObjectPath
Body *StatusNotifierItem_NewAttentionIconSignalBody
}
// Name returns the signal's name.
func (s *StatusNotifierItem_NewAttentionIconSignal) Name() string {
return "NewAttentionIcon"
}
// Interface returns the signal's interface.
func (s *StatusNotifierItem_NewAttentionIconSignal) Interface() string {
return InterfaceStatusNotifierItem
}
// Sender returns the signal's sender unique name.
func (s *StatusNotifierItem_NewAttentionIconSignal) Sender() string {
return s.sender
}
func (s *StatusNotifierItem_NewAttentionIconSignal) path() dbus.ObjectPath {
return s.Path
}
func (s *StatusNotifierItem_NewAttentionIconSignal) values() []interface{} {
return []interface{}{}
}
// StatusNotifierItem_NewAttentionIconSignalBody is body container.
type StatusNotifierItem_NewAttentionIconSignalBody struct {
}
// StatusNotifierItem_NewOverlayIconSignal represents org.kde.StatusNotifierItem.NewOverlayIcon signal.
type StatusNotifierItem_NewOverlayIconSignal struct {
sender string
Path dbus.ObjectPath
Body *StatusNotifierItem_NewOverlayIconSignalBody
}
// Name returns the signal's name.
func (s *StatusNotifierItem_NewOverlayIconSignal) Name() string {
return "NewOverlayIcon"
}
// Interface returns the signal's interface.
func (s *StatusNotifierItem_NewOverlayIconSignal) Interface() string {
return InterfaceStatusNotifierItem
}
// Sender returns the signal's sender unique name.
func (s *StatusNotifierItem_NewOverlayIconSignal) Sender() string {
return s.sender
}
func (s *StatusNotifierItem_NewOverlayIconSignal) path() dbus.ObjectPath {
return s.Path
}
func (s *StatusNotifierItem_NewOverlayIconSignal) values() []interface{} {
return []interface{}{}
}
// StatusNotifierItem_NewOverlayIconSignalBody is body container.
type StatusNotifierItem_NewOverlayIconSignalBody struct {
}
// StatusNotifierItem_NewStatusSignal represents org.kde.StatusNotifierItem.NewStatus signal.
type StatusNotifierItem_NewStatusSignal struct {
sender string
Path dbus.ObjectPath
Body *StatusNotifierItem_NewStatusSignalBody
}
// Name returns the signal's name.
func (s *StatusNotifierItem_NewStatusSignal) Name() string {
return "NewStatus"
}
// Interface returns the signal's interface.
func (s *StatusNotifierItem_NewStatusSignal) Interface() string {
return InterfaceStatusNotifierItem
}
// Sender returns the signal's sender unique name.
func (s *StatusNotifierItem_NewStatusSignal) Sender() string {
return s.sender
}
func (s *StatusNotifierItem_NewStatusSignal) path() dbus.ObjectPath {
return s.Path
}
func (s *StatusNotifierItem_NewStatusSignal) values() []interface{} {
return []interface{}{s.Body.Status}
}
// StatusNotifierItem_NewStatusSignalBody is body container.
type StatusNotifierItem_NewStatusSignalBody struct {
Status string
}
// StatusNotifierItem_NewIconThemePathSignal represents org.kde.StatusNotifierItem.NewIconThemePath signal.
type StatusNotifierItem_NewIconThemePathSignal struct {
sender string
Path dbus.ObjectPath
Body *StatusNotifierItem_NewIconThemePathSignalBody
}
// Name returns the signal's name.
func (s *StatusNotifierItem_NewIconThemePathSignal) Name() string {
return "NewIconThemePath"
}
// Interface returns the signal's interface.
func (s *StatusNotifierItem_NewIconThemePathSignal) Interface() string {
return InterfaceStatusNotifierItem
}
// Sender returns the signal's sender unique name.
func (s *StatusNotifierItem_NewIconThemePathSignal) Sender() string {
return s.sender
}
func (s *StatusNotifierItem_NewIconThemePathSignal) path() dbus.ObjectPath {
return s.Path
}
func (s *StatusNotifierItem_NewIconThemePathSignal) values() []interface{} {
return []interface{}{s.Body.IconThemePath}
}
// StatusNotifierItem_NewIconThemePathSignalBody is body container.
type StatusNotifierItem_NewIconThemePathSignalBody struct {
IconThemePath string
}
// StatusNotifierItem_NewMenuSignal represents org.kde.StatusNotifierItem.NewMenu signal.
type StatusNotifierItem_NewMenuSignal struct {
sender string
Path dbus.ObjectPath
Body *StatusNotifierItem_NewMenuSignalBody
}
// Name returns the signal's name.
func (s *StatusNotifierItem_NewMenuSignal) Name() string {
return "NewMenu"
}
// Interface returns the signal's interface.
func (s *StatusNotifierItem_NewMenuSignal) Interface() string {
return InterfaceStatusNotifierItem
}
// Sender returns the signal's sender unique name.
func (s *StatusNotifierItem_NewMenuSignal) Sender() string {
return s.sender
}
func (s *StatusNotifierItem_NewMenuSignal) path() dbus.ObjectPath {
return s.Path
}
func (s *StatusNotifierItem_NewMenuSignal) values() []interface{} {
return []interface{}{}
}
// StatusNotifierItem_NewMenuSignalBody is body container.
type StatusNotifierItem_NewMenuSignalBody struct {
}

View File

@@ -0,0 +1,41 @@
package debounce
import (
"sync"
"time"
)
// New returns a debounced function that calls f after it stops being invoked
// for the given duration. The last invocation wins if called with different functions.
func New(after time.Duration) func(f func()) {
d := &debouncer{after: after}
return func(f func()) {
d.add(f)
}
}
type debouncer struct {
mu sync.Mutex
after time.Duration
timer *time.Timer
generation uint64
}
func (d *debouncer) add(f func()) {
d.mu.Lock()
defer d.mu.Unlock()
if d.timer != nil {
d.timer.Stop()
}
d.generation++
gen := d.generation
d.timer = time.AfterFunc(d.after, func() {
d.mu.Lock()
if d.generation != gen {
d.mu.Unlock()
return
}
d.mu.Unlock()
f()
})
}

View File

@@ -0,0 +1,42 @@
package debug
import (
"os"
"path/filepath"
"runtime"
)
var LocalModulePath = ""
func init() {
// Check if .git exists in the relative directory from here: ../../..
// If it does, we are in a local build
gitDir := RelativePath("..", "..", "..", ".git")
if _, err := os.Stat(gitDir); err == nil {
modulePath := RelativePath("..", "..", "..")
LocalModulePath, _ = filepath.Abs(modulePath)
}
}
// RelativePath returns a qualified path created by joining the
// directory of the calling file and the given relative path.
func RelativePath(relativepath string, optionalpaths ...string) string {
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// If we have optional paths, join them to the relativepath
if len(optionalpaths) > 0 {
paths := []string{relativepath}
paths = append(paths, optionalpaths...)
relativepath = filepath.Join(paths...)
}
result, err := filepath.Abs(filepath.Join(localDir, relativepath))
if err != nil {
// I'm allowing this for 1 reason only: It's fatal if the path
// supplied is wrong as it's only used internally in Wails. If we get
// that path wrong, we should know about it immediately. The other reason is
// that it cuts down a ton of unnecassary error handling.
panic(err)
}
return result
}

View File

@@ -0,0 +1,96 @@
package fileexplorer
import (
"bufio"
"io"
"os"
"strings"
)
// DesktopEntry represents a parsed .desktop file's [Desktop Entry] section.
// This is a minimal parser that only extracts the fields we need,
// replacing the full gopkg.in/ini.v1 dependency (~34KB + 68 transitive deps).
type DesktopEntry struct {
Exec string
}
// ParseDesktopFile parses a .desktop file and returns the Desktop Entry section.
// It follows the Desktop Entry Specification:
// ParseDesktopFile parses the `[Desktop Entry]` section of the desktop file at path and returns a DesktopEntry.
// It returns an error if the file cannot be opened or if parsing the file fails.
func ParseDesktopFile(path string) (*DesktopEntry, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return ParseDesktopReader(f)
}
// ParseDesktopReader parses the [Desktop Entry] section of a .desktop file from r and extracts the Exec value.
// It ignores empty lines and lines starting with '#', treats section names as case-sensitive, and stops parsing after leaving the [Desktop Entry] section.
// The returned *DesktopEntry has Exec set to the exact value of the Exec key if present (whitespace preserved).
// An error is returned if reading from r fails.
func ParseDesktopReader(r io.Reader) (*DesktopEntry, error) {
scanner := bufio.NewScanner(r)
entry := &DesktopEntry{}
inDesktopEntry := false
for scanner.Scan() {
line := scanner.Text()
// Skip empty lines
if len(line) == 0 {
continue
}
// Skip comments (# at start of line)
if line[0] == '#' {
continue
}
// Handle section headers
if line[0] == '[' {
// Check if this is the [Desktop Entry] section
// The spec says section names are case-sensitive
trimmed := strings.TrimSpace(line)
if trimmed == "[Desktop Entry]" {
inDesktopEntry = true
} else if inDesktopEntry {
// We've left the [Desktop Entry] section
// (e.g., entering [Desktop Action new-window])
// We already have what we need, so we can stop
break
}
continue
}
// Only process key=value pairs in [Desktop Entry] section
if !inDesktopEntry {
continue
}
// Parse key=value (spec says no spaces around =, but be lenient)
eqIdx := strings.Index(line, "=")
if eqIdx == -1 {
continue
}
key := strings.TrimSpace(line[:eqIdx])
value := line[eqIdx+1:] // Don't trim value - preserve intentional whitespace
// We only need the Exec key
// Per spec, keys are case-sensitive and Exec is always "Exec"
if key == "Exec" {
entry.Exec = value
// Continue parsing in case there are multiple Exec lines (shouldn't happen but be safe)
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return entry, nil
}

View File

@@ -0,0 +1,61 @@
package fileexplorer
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"time"
)
func OpenFileManager(path string, selectFile bool) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
path = os.ExpandEnv(path)
path = filepath.Clean(path)
absPath, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("failed to resolve the absolute path: %w", err)
}
path = absPath
if pathInfo, err := os.Stat(path); err != nil {
return fmt.Errorf("failed to access the specified path: %w", err)
} else {
selectFile = selectFile && !pathInfo.IsDir()
}
var (
ignoreExitCode bool = false
)
switch runtime.GOOS {
case "windows":
// NOTE: Disabling the exit code check on Windows system. Workaround for explorer.exe
// exit code handling (https://github.com/microsoft/WSL/issues/6565)
ignoreExitCode = true
case "darwin", "linux":
default:
return errors.New("unsupported platform: " + runtime.GOOS)
}
explorerBin, explorerArgs, err := explorerBinArgs(path, selectFile)
if err != nil {
return fmt.Errorf("failed to determine the file explorer binary: %w", err)
}
cmd := exec.CommandContext(ctx, explorerBin, explorerArgs...)
cmd.SysProcAttr = sysProcAttr(path, selectFile)
cmd.Stdout = nil
cmd.Stderr = nil
if err := cmd.Run(); err != nil {
if !ignoreExitCode {
return fmt.Errorf("failed to open the file explorer: %w", err)
}
}
return nil
}

View File

@@ -0,0 +1,19 @@
//go:build darwin
package fileexplorer
import "syscall"
func explorerBinArgs(path string, selectFile bool) (string, []string, error) {
args := []string{}
if selectFile {
args = append(args, "-R")
}
args = append(args, path)
return "open", args, nil
}
func sysProcAttr(path string, selectFile bool) *syscall.SysProcAttr {
return &syscall.SysProcAttr{}
}

View File

@@ -0,0 +1,113 @@
//go:build linux
package fileexplorer
import (
"bytes"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
)
// when possible; the fallback method does not support selecting a file.
func explorerBinArgs(path string, selectFile bool) (string, []string, error) {
// Map of field codes to their replacements
var fieldCodes = map[string]string{
"%d": "",
"%D": "",
"%n": "",
"%N": "",
"%v": "",
"%m": "",
"%f": path,
"%F": path,
"%u": pathToURI(path),
"%U": pathToURI(path),
}
fileManagerQuery := exec.Command("xdg-mime", "query", "default", "inode/directory")
buf := new(bytes.Buffer)
fileManagerQuery.Stdout = buf
fileManagerQuery.Stderr = nil
if err := fileManagerQuery.Run(); err != nil {
return fallbackExplorerBinArgs(path, selectFile)
}
desktopFilePath, err := findDesktopFile(strings.TrimSpace((buf.String())))
if err != nil {
return fallbackExplorerBinArgs(path, selectFile)
}
entry, err := ParseDesktopFile(desktopFilePath)
if err != nil {
// Opting to fallback rather than fail
return fallbackExplorerBinArgs(path, selectFile)
}
execCmd := entry.Exec
for fieldCode, replacement := range fieldCodes {
execCmd = strings.ReplaceAll(execCmd, fieldCode, replacement)
}
args := strings.Fields(execCmd)
if !strings.Contains(strings.Join(args, " "), path) {
args = append(args, path)
}
return args[0], args[1:], nil
}
func sysProcAttr(path string, selectFile bool) *syscall.SysProcAttr {
return &syscall.SysProcAttr{}
}
func fallbackExplorerBinArgs(path string, selectFile bool) (string, []string, error) {
// NOTE: The linux fallback explorer opening does not support file selection
stat, err := os.Stat(path)
if err != nil {
return "", []string{}, fmt.Errorf("stat path: %w", err)
}
// If the path is a file, we want to open the directory containing the file
if !stat.IsDir() {
path = filepath.Dir(path)
}
return "xdg-open", []string{path}, nil
}
func pathToURI(path string) string {
absPath, err := filepath.Abs(path)
if err != nil {
return path
}
// Use url.URL to properly construct file URIs.
// url.PathEscape incorrectly escapes forward slashes (/ -> %2F),
// which breaks file manager path parsing.
u := &url.URL{
Scheme: "file",
Path: absPath,
}
return u.String()
}
func findDesktopFile(xdgFileName string) (string, error) {
paths := []string{
filepath.Join(os.Getenv("XDG_DATA_HOME"), "applications"),
filepath.Join(os.Getenv("HOME"), ".local", "share", "applications"),
"/usr/share/applications",
}
for _, path := range paths {
desktopFile := filepath.Join(path, xdgFileName)
if _, err := os.Stat(desktopFile); err == nil {
return desktopFile, nil
}
}
err := fmt.Errorf("desktop file not found: %s", xdgFileName)
return "", err
}

View File

@@ -0,0 +1,24 @@
//go:build windows
package fileexplorer
import (
"fmt"
"syscall"
)
func explorerBinArgs(path string, selectFile bool) (string, []string, error) {
return "explorer.exe", []string{}, nil
}
func sysProcAttr(path string, selectFile bool) *syscall.SysProcAttr {
if selectFile {
return &syscall.SysProcAttr{
CmdLine: fmt.Sprintf("explorer.exe /select,\"%s\"", path),
}
} else {
return &syscall.SysProcAttr{
CmdLine: fmt.Sprintf("explorer.exe \"%s\"", path),
}
}
}

View File

@@ -0,0 +1,91 @@
package git
import (
"bytes"
"errors"
"fmt"
"net/url"
"os/exec"
"strings"
)
// ErrNotInstalled is returned when git is not found in PATH.
var ErrNotInstalled = errors.New("git is not installed; please install git from https://git-scm.com")
func isNotFound(err error) bool {
var execErr *exec.Error
return errors.As(err, &execErr) && errors.Is(execErr.Err, exec.ErrNotFound)
}
// redactArgs returns a copy of args with any URL credentials (user:pass@host)
// replaced by user:***@host so tokens are not leaked in error messages.
func redactArgs(args []string) []string {
out := make([]string, len(args))
for i, a := range args {
if u, err := url.Parse(a); err == nil && u.User != nil {
u.User = url.UserPassword(u.User.Username(), "***")
a = u.String()
}
out[i] = a
}
return out
}
func run(args ...string) error {
out, err := exec.Command("git", args...).CombinedOutput()
if err != nil {
if isNotFound(err) {
return ErrNotInstalled
}
return fmt.Errorf("git %s: %w\n%s", strings.Join(redactArgs(args), " "), err, bytes.TrimSpace(out))
}
return nil
}
func output(args ...string) (string, error) {
out, err := exec.Command("git", args...).CombinedOutput()
if err != nil {
if isNotFound(err) {
return "", ErrNotInstalled
}
return "", fmt.Errorf("git %s: %w\n%s", strings.Join(redactArgs(args), " "), err, bytes.TrimSpace(out))
}
return strings.TrimSpace(string(out)), nil
}
// HeadHash returns the short (8-character) commit hash of HEAD in dir.
func HeadHash(dir string) (string, error) {
hash, err := output("-C", dir, "rev-parse", "HEAD")
if err != nil {
return "", err
}
if len(hash) < 8 {
return "", fmt.Errorf("git rev-parse returned unexpected output %q", hash)
}
return hash[:8], nil
}
// Clone clones url into dir. If tag is non-empty, checks out that tag or branch.
func Clone(url, dir, tag string) error {
args := []string{"clone", "--quiet"}
if tag != "" {
args = append(args, "--branch", tag)
}
args = append(args, url, dir)
return run(args...)
}
// Init initializes a new git repository at dir.
func Init(dir string) error {
return run("-C", dir, "init", "--quiet")
}
// RemoteAdd adds a named remote to the repository at dir.
func RemoteAdd(dir, name, url string) error {
return run("-C", dir, "remote", "add", name, url)
}
// AddAll stages all files in the repository at dir.
func AddAll(dir string) error {
return run("-C", dir, "add", ".")
}

View File

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

View File

@@ -0,0 +1,72 @@
// Cross-platform.
// Common File Dialogs
package cfd
type Dialog interface {
// Show the dialog to the user.
// Blocks until the user has closed the dialog.
Show() error
// Sets the dialog's parent window. Use 0 to set the dialog to have no parent window.
SetParentWindowHandle(hwnd uintptr)
// Show the dialog to the user.
// Blocks until the user has closed the dialog and returns their selection.
// Returns an error if the user cancelled the dialog.
// Do not use for the Open Multiple Files dialog. Use ShowAndGetResults instead.
ShowAndGetResult() (string, error)
// Sets the title of the dialog window.
SetTitle(title string) error
// Sets the "role" of the dialog. This is used to derive the dialog's GUID, which the
// OS will use to differentiate it from dialogs that are intended for other purposes.
// This means that, for example, a dialog with role "Import" will have a different
// previous location that it will open to than a dialog with role "Open". Can be any string.
SetRole(role string) error
// Sets the folder used as a default if there is not a recently used folder value available
SetDefaultFolder(defaultFolder string) error
// Sets the folder that the dialog always opens to.
// If this is set, it will override the "default folder" behaviour and the dialog will always open to this folder.
SetFolder(folder string) error
// Gets the selected file or folder path, as an absolute path eg. "C:\Folder\file.txt"
// Do not use for the Open Multiple Files dialog. Use GetResults instead.
GetResult() (string, error)
// Sets the file name, I.E. the contents of the file name text box.
// For Select Folder Dialog, sets folder name.
SetFileName(fileName string) error
// Release the resources allocated to this Dialog.
// Should be called when the dialog is finished with.
Release() error
}
type FileDialog interface {
Dialog
// Set the list of file filters that the user can select.
SetFileFilters(fileFilter []FileFilter) error
// Set the selected item from the list of file filters (set using SetFileFilters) by its index. Defaults to 0 (the first item in the list) if not called.
SetSelectedFileFilterIndex(index uint) error
// Sets the default extension applied when a user does not provide one as part of the file name.
// If the user selects a different file filter, the default extension will be automatically updated to match the new file filter.
// For Open / Open Multiple File Dialog, this only has an effect when the user specifies a file name with no extension and a file with the default extension exists.
// For Save File Dialog, this extension will be used whenever a user does not specify an extension.
SetDefaultExtension(defaultExtension string) error
}
type OpenFileDialog interface {
FileDialog
}
type OpenMultipleFilesDialog interface {
FileDialog
// Show the dialog to the user.
// Blocks until the user has closed the dialog and returns the selected files.
ShowAndGetResults() ([]string, error)
// Gets the selected file paths, as absolute paths eg. "C:\Folder\file.txt"
GetResults() ([]string, error)
}
type SelectFolderDialog interface {
Dialog
}
type SaveFileDialog interface { // TODO Properties
FileDialog
}

View File

@@ -0,0 +1,28 @@
//go:build !windows
// +build !windows
package cfd
import "fmt"
var unsupportedError = fmt.Errorf("common file dialogs are only available on windows")
// TODO doc
func NewOpenFileDialog(config DialogConfig) (OpenFileDialog, error) {
return nil, unsupportedError
}
// TODO doc
func NewOpenMultipleFilesDialog(config DialogConfig) (OpenMultipleFilesDialog, error) {
return nil, unsupportedError
}
// TODO doc
func NewSelectFolderDialog(config DialogConfig) (SelectFolderDialog, error) {
return nil, unsupportedError
}
// TODO doc
func NewSaveFileDialog(config DialogConfig) (SaveFileDialog, error) {
return nil, unsupportedError
}

View File

@@ -0,0 +1,79 @@
//go:build windows
// +build windows
package cfd
import "github.com/go-ole/go-ole"
func initialize() {
// Swallow error
_ = ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED|ole.COINIT_DISABLE_OLE1DDE)
}
// TODO doc
func NewOpenFileDialog(config DialogConfig) (OpenFileDialog, error) {
initialize()
openDialog, err := newIFileOpenDialog()
if err != nil {
return nil, err
}
err = config.apply(openDialog)
if err != nil {
return nil, err
}
return openDialog, nil
}
// TODO doc
func NewOpenMultipleFilesDialog(config DialogConfig) (OpenMultipleFilesDialog, error) {
initialize()
openDialog, err := newIFileOpenDialog()
if err != nil {
return nil, err
}
err = config.apply(openDialog)
if err != nil {
return nil, err
}
err = openDialog.setIsMultiselect(true)
if err != nil {
return nil, err
}
return openDialog, nil
}
// TODO doc
func NewSelectFolderDialog(config DialogConfig) (SelectFolderDialog, error) {
initialize()
openDialog, err := newIFileOpenDialog()
if err != nil {
return nil, err
}
err = config.apply(openDialog)
if err != nil {
return nil, err
}
err = openDialog.setPickFolders(true)
if err != nil {
return nil, err
}
return openDialog, nil
}
// TODO doc
func NewSaveFileDialog(config DialogConfig) (SaveFileDialog, error) {
initialize()
saveDialog, err := newIFileSaveDialog()
if err != nil {
return nil, err
}
err = config.apply(saveDialog)
if err != nil {
return nil, err
}
return saveDialog, nil
}

View File

@@ -0,0 +1,141 @@
// Cross-platform.
package cfd
import (
"reflect"
"fmt"
"os"
)
type FileFilter struct {
// The display name of the filter (That is shown to the user)
DisplayName string
// The filter pattern. Eg. "*.txt;*.png" to select all txt and png files, "*.*" to select any files, etc.
Pattern string
}
// Never obfuscate the FileFilter type.
var _ = reflect.TypeOf(FileFilter{})
type DialogConfig struct {
// The title of the dialog
Title string
// The role of the dialog. This is used to derive the dialog's GUID, which the
// OS will use to differentiate it from dialogs that are intended for other purposes.
// This means that, for example, a dialog with role "Import" will have a different
// previous location that it will open to than a dialog with role "Open". Can be any string.
Role string
// The default folder - the folder that is used the first time the user opens it
// (after the first time their last used location is used).
DefaultFolder string
// The initial folder - the folder that the dialog always opens to if not empty.
// If this is not empty, it will override the "default folder" behaviour and
// the dialog will always open to this folder.
Folder string
// The file filters that restrict which types of files the dialog is able to choose.
// Ignored by Select Folder Dialog.
FileFilters []FileFilter
// Sets the initially selected file filter. This is an index of FileFilters.
// Ignored by Select Folder Dialog.
SelectedFileFilterIndex uint
// The initial name of the file (I.E. the text in the file name text box) when the user opens the dialog.
// For the Select Folder Dialog, this sets the initial folder name.
FileName string
// The default extension applied when a user does not provide one as part of the file name.
// If the user selects a different file filter, the default extension will be automatically updated to match the new file filter.
// For Open / Open Multiple File Dialog, this only has an effect when the user specifies a file name with no extension and a file with the default extension exists.
// For Save File Dialog, this extension will be used whenever a user does not specify an extension.
// Ignored by Select Folder Dialog.
DefaultExtension string
// ParentWindowHandle is the handle (HWND) to the parent window of the dialog.
// If left as 0 / nil, the dialog will have no parent window.
ParentWindowHandle uintptr
}
var defaultFilters = []FileFilter{
{
DisplayName: "All Files (*.*)",
Pattern: "*.*",
},
}
func (config *DialogConfig) apply(dialog Dialog) (err error) {
if config.Title != "" {
err = dialog.SetTitle(config.Title)
if err != nil {
return
}
}
if config.Role != "" {
err = dialog.SetRole(config.Role)
if err != nil {
return
}
}
if config.Folder != "" {
_, err = os.Stat(config.Folder)
if err != nil {
return
}
err = dialog.SetFolder(config.Folder)
if err != nil {
return
}
}
if config.DefaultFolder != "" {
_, err = os.Stat(config.DefaultFolder)
if err != nil {
return
}
err = dialog.SetDefaultFolder(config.DefaultFolder)
if err != nil {
return
}
}
if config.FileName != "" {
err = dialog.SetFileName(config.FileName)
if err != nil {
return
}
}
dialog.SetParentWindowHandle(config.ParentWindowHandle)
if dialog, ok := dialog.(FileDialog); ok {
var fileFilters []FileFilter
if config.FileFilters != nil && len(config.FileFilters) > 0 {
fileFilters = config.FileFilters
} else {
fileFilters = defaultFilters
}
err = dialog.SetFileFilters(fileFilters)
if err != nil {
return
}
if config.SelectedFileFilterIndex != 0 {
if config.SelectedFileFilterIndex > uint(len(fileFilters)) {
err = fmt.Errorf("selected file filter index out of range")
return
}
err = dialog.SetSelectedFileFilterIndex(config.SelectedFileFilterIndex)
if err != nil {
return
}
}
if config.DefaultExtension != "" {
err = dialog.SetDefaultExtension(config.DefaultExtension)
if err != nil {
return
}
}
}
return
}

View File

@@ -0,0 +1,7 @@
package cfd
import "errors"
var (
ErrorCancelled = errors.New("cancelled by user")
)

View File

@@ -0,0 +1,200 @@
//go:build windows
// +build windows
package cfd
import (
"github.com/go-ole/go-ole"
"github.com/wailsapp/wails/v3/internal/uuid"
"syscall"
"unsafe"
)
var (
fileOpenDialogCLSID = ole.NewGUID("{DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7}")
fileOpenDialogIID = ole.NewGUID("{d57c7288-d4ad-4768-be02-9d969532d960}")
)
type iFileOpenDialog struct {
vtbl *iFileOpenDialogVtbl
parentWindowHandle uintptr
}
type iFileOpenDialogVtbl struct {
iFileDialogVtbl
GetResults uintptr // func (ppenum **IShellItemArray) HRESULT
GetSelectedItems uintptr
}
func newIFileOpenDialog() (*iFileOpenDialog, error) {
if unknown, err := ole.CreateInstance(fileOpenDialogCLSID, fileOpenDialogIID); err == nil {
return (*iFileOpenDialog)(unsafe.Pointer(unknown)), nil
} else {
return nil, err
}
}
func (fileOpenDialog *iFileOpenDialog) Show() error {
return fileOpenDialog.vtbl.show(unsafe.Pointer(fileOpenDialog), fileOpenDialog.parentWindowHandle)
}
func (fileOpenDialog *iFileOpenDialog) SetParentWindowHandle(hwnd uintptr) {
fileOpenDialog.parentWindowHandle = hwnd
}
func (fileOpenDialog *iFileOpenDialog) ShowAndGetResult() (string, error) {
isMultiselect, err := fileOpenDialog.isMultiselect()
if err != nil {
return "", err
}
if isMultiselect {
// We should panic as this error is caused by the developer using the library
panic("use ShowAndGetResults for open multiple files dialog")
}
if err := fileOpenDialog.Show(); err != nil {
return "", err
}
return fileOpenDialog.GetResult()
}
func (fileOpenDialog *iFileOpenDialog) ShowAndGetResults() ([]string, error) {
isMultiselect, err := fileOpenDialog.isMultiselect()
if err != nil {
return nil, err
}
if !isMultiselect {
// We should panic as this error is caused by the developer using the library
panic("use ShowAndGetResult for open single file dialog")
}
if err := fileOpenDialog.Show(); err != nil {
return nil, err
}
return fileOpenDialog.GetResults()
}
func (fileOpenDialog *iFileOpenDialog) SetTitle(title string) error {
return fileOpenDialog.vtbl.setTitle(unsafe.Pointer(fileOpenDialog), title)
}
func (fileOpenDialog *iFileOpenDialog) GetResult() (string, error) {
isMultiselect, err := fileOpenDialog.isMultiselect()
if err != nil {
return "", err
}
if isMultiselect {
// We should panic as this error is caused by the developer using the library
panic("use GetResults for open multiple files dialog")
}
return fileOpenDialog.vtbl.getResultString(unsafe.Pointer(fileOpenDialog))
}
func (fileOpenDialog *iFileOpenDialog) Release() error {
return fileOpenDialog.vtbl.release(unsafe.Pointer(fileOpenDialog))
}
func (fileOpenDialog *iFileOpenDialog) SetDefaultFolder(defaultFolderPath string) error {
return fileOpenDialog.vtbl.setDefaultFolder(unsafe.Pointer(fileOpenDialog), defaultFolderPath)
}
func (fileOpenDialog *iFileOpenDialog) SetFolder(defaultFolderPath string) error {
return fileOpenDialog.vtbl.setFolder(unsafe.Pointer(fileOpenDialog), defaultFolderPath)
}
func (fileOpenDialog *iFileOpenDialog) SetFileFilters(filter []FileFilter) error {
return fileOpenDialog.vtbl.setFileTypes(unsafe.Pointer(fileOpenDialog), filter)
}
func (fileOpenDialog *iFileOpenDialog) SetRole(role string) error {
return fileOpenDialog.vtbl.setClientGuid(unsafe.Pointer(fileOpenDialog), StringToUUID(role))
}
// This should only be callable when the user asks for a multi select because
// otherwise they will be given the Dialog interface which does not expose this function.
func (fileOpenDialog *iFileOpenDialog) GetResults() ([]string, error) {
isMultiselect, err := fileOpenDialog.isMultiselect()
if err != nil {
return nil, err
}
if !isMultiselect {
// We should panic as this error is caused by the developer using the library
panic("use GetResult for open single file dialog")
}
return fileOpenDialog.vtbl.getResultsStrings(unsafe.Pointer(fileOpenDialog))
}
func (fileOpenDialog *iFileOpenDialog) SetDefaultExtension(defaultExtension string) error {
return fileOpenDialog.vtbl.setDefaultExtension(unsafe.Pointer(fileOpenDialog), defaultExtension)
}
func (fileOpenDialog *iFileOpenDialog) SetFileName(initialFileName string) error {
return fileOpenDialog.vtbl.setFileName(unsafe.Pointer(fileOpenDialog), initialFileName)
}
func (fileOpenDialog *iFileOpenDialog) SetSelectedFileFilterIndex(index uint) error {
return fileOpenDialog.vtbl.setSelectedFileFilterIndex(unsafe.Pointer(fileOpenDialog), index)
}
func (fileOpenDialog *iFileOpenDialog) setPickFolders(pickFolders bool) error {
const FosPickfolders = 0x20
if pickFolders {
return fileOpenDialog.vtbl.addOption(unsafe.Pointer(fileOpenDialog), FosPickfolders)
} else {
return fileOpenDialog.vtbl.removeOption(unsafe.Pointer(fileOpenDialog), FosPickfolders)
}
}
const FosAllowMultiselect = 0x200
func (fileOpenDialog *iFileOpenDialog) isMultiselect() (bool, error) {
options, err := fileOpenDialog.vtbl.getOptions(unsafe.Pointer(fileOpenDialog))
if err != nil {
return false, err
}
return options&FosAllowMultiselect != 0, nil
}
func (fileOpenDialog *iFileOpenDialog) setIsMultiselect(isMultiselect bool) error {
if isMultiselect {
return fileOpenDialog.vtbl.addOption(unsafe.Pointer(fileOpenDialog), FosAllowMultiselect)
} else {
return fileOpenDialog.vtbl.removeOption(unsafe.Pointer(fileOpenDialog), FosAllowMultiselect)
}
}
func (vtbl *iFileOpenDialogVtbl) getResults(objPtr unsafe.Pointer) (*iShellItemArray, error) {
var shellItemArray *iShellItemArray
ret, _, _ := syscall.SyscallN(vtbl.GetResults,
uintptr(objPtr),
uintptr(unsafe.Pointer(&shellItemArray)),
0)
return shellItemArray, hresultToError(ret)
}
func (vtbl *iFileOpenDialogVtbl) getResultsStrings(objPtr unsafe.Pointer) ([]string, error) {
shellItemArray, err := vtbl.getResults(objPtr)
if err != nil {
return nil, err
}
if shellItemArray == nil {
return nil, ErrorCancelled
}
defer shellItemArray.vtbl.release(unsafe.Pointer(shellItemArray))
count, err := shellItemArray.vtbl.getCount(unsafe.Pointer(shellItemArray))
if err != nil {
return nil, err
}
var results []string
for i := uintptr(0); i < count; i++ {
newItem, err := shellItemArray.vtbl.getItemAt(unsafe.Pointer(shellItemArray), i)
if err != nil {
return nil, err
}
results = append(results, newItem)
}
return results, nil
}
func StringToUUID(str string) *ole.GUID {
return ole.NewGUID(uuid.NewSHA1(uuid.Nil, []byte(str)).String())
}

View File

@@ -0,0 +1,92 @@
//go:build windows
// +build windows
package cfd
import (
"github.com/go-ole/go-ole"
"unsafe"
)
var (
saveFileDialogCLSID = ole.NewGUID("{C0B4E2F3-BA21-4773-8DBA-335EC946EB8B}")
saveFileDialogIID = ole.NewGUID("{84bccd23-5fde-4cdb-aea4-af64b83d78ab}")
)
type iFileSaveDialog struct {
vtbl *iFileSaveDialogVtbl
parentWindowHandle uintptr
}
type iFileSaveDialogVtbl struct {
iFileDialogVtbl
SetSaveAsItem uintptr
SetProperties uintptr
SetCollectedProperties uintptr
GetProperties uintptr
ApplyProperties uintptr
}
func newIFileSaveDialog() (*iFileSaveDialog, error) {
if unknown, err := ole.CreateInstance(saveFileDialogCLSID, saveFileDialogIID); err == nil {
return (*iFileSaveDialog)(unsafe.Pointer(unknown)), nil
} else {
return nil, err
}
}
func (fileSaveDialog *iFileSaveDialog) Show() error {
return fileSaveDialog.vtbl.show(unsafe.Pointer(fileSaveDialog), fileSaveDialog.parentWindowHandle)
}
func (fileSaveDialog *iFileSaveDialog) SetParentWindowHandle(hwnd uintptr) {
fileSaveDialog.parentWindowHandle = hwnd
}
func (fileSaveDialog *iFileSaveDialog) ShowAndGetResult() (string, error) {
if err := fileSaveDialog.Show(); err != nil {
return "", err
}
return fileSaveDialog.GetResult()
}
func (fileSaveDialog *iFileSaveDialog) SetTitle(title string) error {
return fileSaveDialog.vtbl.setTitle(unsafe.Pointer(fileSaveDialog), title)
}
func (fileSaveDialog *iFileSaveDialog) GetResult() (string, error) {
return fileSaveDialog.vtbl.getResultString(unsafe.Pointer(fileSaveDialog))
}
func (fileSaveDialog *iFileSaveDialog) Release() error {
return fileSaveDialog.vtbl.release(unsafe.Pointer(fileSaveDialog))
}
func (fileSaveDialog *iFileSaveDialog) SetDefaultFolder(defaultFolderPath string) error {
return fileSaveDialog.vtbl.setDefaultFolder(unsafe.Pointer(fileSaveDialog), defaultFolderPath)
}
func (fileSaveDialog *iFileSaveDialog) SetFolder(defaultFolderPath string) error {
return fileSaveDialog.vtbl.setFolder(unsafe.Pointer(fileSaveDialog), defaultFolderPath)
}
func (fileSaveDialog *iFileSaveDialog) SetFileFilters(filter []FileFilter) error {
return fileSaveDialog.vtbl.setFileTypes(unsafe.Pointer(fileSaveDialog), filter)
}
func (fileSaveDialog *iFileSaveDialog) SetRole(role string) error {
return fileSaveDialog.vtbl.setClientGuid(unsafe.Pointer(fileSaveDialog), StringToUUID(role))
}
func (fileSaveDialog *iFileSaveDialog) SetDefaultExtension(defaultExtension string) error {
return fileSaveDialog.vtbl.setDefaultExtension(unsafe.Pointer(fileSaveDialog), defaultExtension)
}
func (fileSaveDialog *iFileSaveDialog) SetFileName(initialFileName string) error {
return fileSaveDialog.vtbl.setFileName(unsafe.Pointer(fileSaveDialog), initialFileName)
}
func (fileSaveDialog *iFileSaveDialog) SetSelectedFileFilterIndex(index uint) error {
return fileSaveDialog.vtbl.setSelectedFileFilterIndex(unsafe.Pointer(fileSaveDialog), index)
}

View File

@@ -0,0 +1,56 @@
//go:build windows
// +build windows
package cfd
import (
"github.com/go-ole/go-ole"
"syscall"
"unsafe"
)
var (
procSHCreateItemFromParsingName = syscall.NewLazyDLL("Shell32.dll").NewProc("SHCreateItemFromParsingName")
iidShellItem = ole.NewGUID("43826d1e-e718-42ee-bc55-a1e261c37bfe")
)
type iShellItem struct {
vtbl *iShellItemVtbl
}
type iShellItemVtbl struct {
iUnknownVtbl
BindToHandler uintptr
GetParent uintptr
GetDisplayName uintptr // func (sigdnName SIGDN, ppszName *LPWSTR) HRESULT
GetAttributes uintptr
Compare uintptr
}
func newIShellItem(path string) (*iShellItem, error) {
var shellItem *iShellItem
pathPtr := ole.SysAllocString(path)
defer func(v *int16) {
_ = ole.SysFreeString(v)
}(pathPtr)
ret, _, _ := procSHCreateItemFromParsingName.Call(
uintptr(unsafe.Pointer(pathPtr)),
0,
uintptr(unsafe.Pointer(iidShellItem)),
uintptr(unsafe.Pointer(&shellItem)))
return shellItem, hresultToError(ret)
}
func (vtbl *iShellItemVtbl) getDisplayName(objPtr unsafe.Pointer) (string, error) {
var ptr *uint16
ret, _, _ := syscall.SyscallN(vtbl.GetDisplayName,
uintptr(objPtr),
0x80058000, // SIGDN_FILESYSPATH,
uintptr(unsafe.Pointer(&ptr)))
if err := hresultToError(ret); err != nil {
return "", err
}
defer ole.CoTaskMemFree(uintptr(unsafe.Pointer(ptr)))
return ole.LpOleStrToString(ptr), nil
}

View File

@@ -0,0 +1,65 @@
//go:build windows
// +build windows
package cfd
import (
"fmt"
"github.com/go-ole/go-ole"
"syscall"
"unsafe"
)
const (
iidShellItemArrayGUID = "{b63ea76d-1f85-456f-a19c-48159efa858b}"
)
var (
iidShellItemArray *ole.GUID
)
func init() {
iidShellItemArray, _ = ole.IIDFromString(iidShellItemArrayGUID)
}
type iShellItemArray struct {
vtbl *iShellItemArrayVtbl
}
type iShellItemArrayVtbl struct {
iUnknownVtbl
BindToHandler uintptr
GetPropertyStore uintptr
GetPropertyDescriptionList uintptr
GetAttributes uintptr
GetCount uintptr // func (pdwNumItems *DWORD) HRESULT
GetItemAt uintptr // func (dwIndex DWORD, ppsi **IShellItem) HRESULT
EnumItems uintptr
}
func (vtbl *iShellItemArrayVtbl) getCount(objPtr unsafe.Pointer) (uintptr, error) {
var count uintptr
ret, _, _ := syscall.SyscallN(vtbl.GetCount,
uintptr(objPtr),
uintptr(unsafe.Pointer(&count)))
if err := hresultToError(ret); err != nil {
return 0, err
}
return count, nil
}
func (vtbl *iShellItemArrayVtbl) getItemAt(objPtr unsafe.Pointer, index uintptr) (string, error) {
var shellItem *iShellItem
ret, _, _ := syscall.SyscallN(vtbl.GetItemAt,
uintptr(objPtr),
index,
uintptr(unsafe.Pointer(&shellItem)))
if err := hresultToError(ret); err != nil {
return "", err
}
if shellItem == nil {
return "", fmt.Errorf("shellItem is nil")
}
defer shellItem.vtbl.release(unsafe.Pointer(shellItem))
return shellItem.vtbl.getDisplayName(unsafe.Pointer(shellItem))
}

View File

@@ -0,0 +1,48 @@
//go:build windows
// +build windows
package cfd
type comDlgFilterSpec struct {
pszName *int16
pszSpec *int16
}
type iUnknownVtbl struct {
QueryInterface uintptr
AddRef uintptr
Release uintptr
}
type iModalWindowVtbl struct {
iUnknownVtbl
Show uintptr // func (hwndOwner HWND) HRESULT
}
type iFileDialogVtbl struct {
iModalWindowVtbl
SetFileTypes uintptr // func (cFileTypes UINT, rgFilterSpec *COMDLG_FILTERSPEC) HRESULT
SetFileTypeIndex uintptr // func(iFileType UINT) HRESULT
GetFileTypeIndex uintptr
Advise uintptr
Unadvise uintptr
SetOptions uintptr // func (fos FILEOPENDIALOGOPTIONS) HRESULT
GetOptions uintptr // func (pfos *FILEOPENDIALOGOPTIONS) HRESULT
SetDefaultFolder uintptr // func (psi *IShellItem) HRESULT
SetFolder uintptr // func (psi *IShellItem) HRESULT
GetFolder uintptr
GetCurrentSelection uintptr
SetFileName uintptr // func (pszName LPCWSTR) HRESULT
GetFileName uintptr
SetTitle uintptr // func(pszTitle LPCWSTR) HRESULT
SetOkButtonLabel uintptr
SetFileNameLabel uintptr
GetResult uintptr // func (ppsi **IShellItem) HRESULT
AddPlace uintptr
SetDefaultExtension uintptr // func (pszDefaultExtension LPCWSTR) HRESULT
// This can only be used from a callback.
Close uintptr
SetClientGuid uintptr // func (guid REFGUID) HRESULT
ClearClientData uintptr
SetFilter uintptr
}

View File

@@ -0,0 +1,226 @@
//go:build windows
package cfd
import (
"fmt"
"strings"
"syscall"
"unsafe"
"github.com/go-ole/go-ole"
)
func hresultToError(hr uintptr) error {
if hr < 0 {
return ole.NewError(hr)
}
return nil
}
func (vtbl *iUnknownVtbl) release(objPtr unsafe.Pointer) error {
ret, _, _ := syscall.SyscallN(vtbl.Release,
uintptr(objPtr),
0)
return hresultToError(ret)
}
func (vtbl *iModalWindowVtbl) show(objPtr unsafe.Pointer, hwnd uintptr) error {
ret, _, _ := syscall.SyscallN(vtbl.Show,
uintptr(objPtr),
hwnd)
return hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) setFileTypes(objPtr unsafe.Pointer, filters []FileFilter) error {
cFileTypes := len(filters)
if cFileTypes < 0 {
return fmt.Errorf("must specify at least one filter")
}
comDlgFilterSpecs := make([]comDlgFilterSpec, cFileTypes)
for i := 0; i < cFileTypes; i++ {
filter := &filters[i]
comDlgFilterSpecs[i] = comDlgFilterSpec{
pszName: ole.SysAllocString(filter.DisplayName),
pszSpec: ole.SysAllocString(filter.Pattern),
}
}
// Ensure memory is freed after use
defer func() {
for _, spec := range comDlgFilterSpecs {
ole.SysFreeString(spec.pszName)
ole.SysFreeString(spec.pszSpec)
}
}()
ret, _, _ := syscall.SyscallN(vtbl.SetFileTypes,
uintptr(objPtr),
uintptr(cFileTypes),
uintptr(unsafe.Pointer(&comDlgFilterSpecs[0])))
return hresultToError(ret)
}
// Options are:
// FOS_OVERWRITEPROMPT = 0x2,
// FOS_STRICTFILETYPES = 0x4,
// FOS_NOCHANGEDIR = 0x8,
// FOS_PICKFOLDERS = 0x20,
// FOS_FORCEFILESYSTEM = 0x40,
// FOS_ALLNONSTORAGEITEMS = 0x80,
// FOS_NOVALIDATE = 0x100,
// FOS_ALLOWMULTISELECT = 0x200,
// FOS_PATHMUSTEXIST = 0x800,
// FOS_FILEMUSTEXIST = 0x1000,
// FOS_CREATEPROMPT = 0x2000,
// FOS_SHAREAWARE = 0x4000,
// FOS_NOREADONLYRETURN = 0x8000,
// FOS_NOTESTFILECREATE = 0x10000,
// FOS_HIDEMRUPLACES = 0x20000,
// FOS_HIDEPINNEDPLACES = 0x40000,
// FOS_NODEREFERENCELINKS = 0x100000,
// FOS_OKBUTTONNEEDSINTERACTION = 0x200000,
// FOS_DONTADDTORECENT = 0x2000000,
// FOS_FORCESHOWHIDDEN = 0x10000000,
// FOS_DEFAULTNOMINIMODE = 0x20000000,
// FOS_FORCEPREVIEWPANEON = 0x40000000,
// FOS_SUPPORTSTREAMABLEITEMS = 0x80000000
func (vtbl *iFileDialogVtbl) setOptions(objPtr unsafe.Pointer, options uint32) error {
ret, _, _ := syscall.SyscallN(vtbl.SetOptions,
uintptr(objPtr),
uintptr(options))
return hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) getOptions(objPtr unsafe.Pointer) (uint32, error) {
var options uint32
ret, _, _ := syscall.SyscallN(vtbl.GetOptions,
uintptr(objPtr),
uintptr(unsafe.Pointer(&options)))
return options, hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) addOption(objPtr unsafe.Pointer, option uint32) error {
if options, err := vtbl.getOptions(objPtr); err == nil {
return vtbl.setOptions(objPtr, options|option)
} else {
return err
}
}
func (vtbl *iFileDialogVtbl) removeOption(objPtr unsafe.Pointer, option uint32) error {
if options, err := vtbl.getOptions(objPtr); err == nil {
return vtbl.setOptions(objPtr, options&^option)
} else {
return err
}
}
func (vtbl *iFileDialogVtbl) setDefaultFolder(objPtr unsafe.Pointer, path string) error {
shellItem, err := newIShellItem(path)
if err != nil {
return err
}
defer shellItem.vtbl.release(unsafe.Pointer(shellItem))
ret, _, _ := syscall.SyscallN(vtbl.SetDefaultFolder,
uintptr(objPtr),
uintptr(unsafe.Pointer(shellItem)))
return hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) setFolder(objPtr unsafe.Pointer, path string) error {
shellItem, err := newIShellItem(path)
if err != nil {
return err
}
defer shellItem.vtbl.release(unsafe.Pointer(shellItem))
ret, _, _ := syscall.SyscallN(vtbl.SetFolder,
uintptr(objPtr),
uintptr(unsafe.Pointer(shellItem)))
return hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) setTitle(objPtr unsafe.Pointer, title string) error {
titlePtr := ole.SysAllocString(title)
defer ole.SysFreeString(titlePtr) // Ensure the string is freed
ret, _, _ := syscall.SyscallN(vtbl.SetTitle,
uintptr(objPtr),
uintptr(unsafe.Pointer(titlePtr)))
return hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) close(objPtr unsafe.Pointer) error {
ret, _, _ := syscall.SyscallN(vtbl.Close,
uintptr(objPtr))
return hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) getResult(objPtr unsafe.Pointer) (*iShellItem, error) {
var shellItem *iShellItem
ret, _, _ := syscall.SyscallN(vtbl.GetResult,
uintptr(objPtr),
uintptr(unsafe.Pointer(&shellItem)))
return shellItem, hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) getResultString(objPtr unsafe.Pointer) (string, error) {
shellItem, err := vtbl.getResult(objPtr)
if err != nil {
return "", err
}
if shellItem == nil {
return "", ErrorCancelled
}
defer shellItem.vtbl.release(unsafe.Pointer(shellItem))
return shellItem.vtbl.getDisplayName(unsafe.Pointer(shellItem))
}
func (vtbl *iFileDialogVtbl) setClientGuid(objPtr unsafe.Pointer, guid *ole.GUID) error {
// Ensure the GUID is not nil
if guid == nil {
return fmt.Errorf("guid cannot be nil")
}
// Call the SetClientGuid method
ret, _, _ := syscall.SyscallN(vtbl.SetClientGuid,
uintptr(objPtr),
uintptr(unsafe.Pointer(guid)))
// Convert the HRESULT to a Go error
return hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) setDefaultExtension(objPtr unsafe.Pointer, defaultExtension string) error {
// Ensure the string is not empty before accessing the first character
if len(defaultExtension) > 0 && defaultExtension[0] == '.' {
defaultExtension = strings.TrimPrefix(defaultExtension, ".")
}
// Allocate memory for the default extension string
defaultExtensionPtr := ole.SysAllocString(defaultExtension)
defer ole.SysFreeString(defaultExtensionPtr) // Ensure the string is freed
// Call the SetDefaultExtension method
ret, _, _ := syscall.SyscallN(vtbl.SetDefaultExtension,
uintptr(objPtr),
uintptr(unsafe.Pointer(defaultExtensionPtr)))
// Convert the HRESULT to a Go error
return hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) setFileName(objPtr unsafe.Pointer, fileName string) error {
fileNamePtr := ole.SysAllocString(fileName)
defer ole.SysFreeString(fileNamePtr) // Ensure the string is freed
ret, _, _ := syscall.SyscallN(vtbl.SetFileName,
uintptr(objPtr),
uintptr(unsafe.Pointer(fileNamePtr)))
return hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) setSelectedFileFilterIndex(objPtr unsafe.Pointer, index uint) error {
ret, _, _ := syscall.SyscallN(vtbl.SetFileTypeIndex,
uintptr(objPtr),
uintptr(index+1)) // SetFileTypeIndex counts from 1
return hresultToError(ret)
}

View File

@@ -0,0 +1,9 @@
package hash
import "hash/fnv"
func Fnv(s string) uint32 {
h := fnv.New32a()
_, _ = h.Write([]byte(s)) // Hash implementations never return errors (see https://pkg.go.dev/hash#Hash)
return h.Sum32()
}

86
vendor/github.com/wailsapp/wails/v3/internal/lo/lo.go generated vendored Normal file
View File

@@ -0,0 +1,86 @@
package lo
// Associate converts a slice to a map by applying keyFn to each element.
func Associate[T any, K comparable, V any](collection []T, keyFn func(T) (K, V)) map[K]V {
result := make(map[K]V, len(collection))
for _, item := range collection {
k, v := keyFn(item)
result[k] = v
}
return result
}
// Contains reports whether v is present in collection.
func Contains[T comparable](collection []T, v T) bool {
for _, item := range collection {
if item == v {
return true
}
}
return false
}
// ContainsBy reports whether any element of collection satisfies predicate.
func ContainsBy[T any](collection []T, predicate func(T) bool) bool {
for _, item := range collection {
if predicate(item) {
return true
}
}
return false
}
// Find returns the first element satisfying predicate and true, or the zero
// value and false if no element matches.
func Find[T any](collection []T, predicate func(T) bool) (T, bool) {
for _, item := range collection {
if predicate(item) {
return item, true
}
}
var zero T
return zero, false
}
// Keys returns the keys of the map in an unspecified order.
func Keys[K comparable, V any](m map[K]V) []K {
result := make([]K, 0, len(m))
for k := range m {
result = append(result, k)
}
return result
}
// Must returns val and panics if err is non-nil.
func Must[T any](val T, err error) T {
if err != nil {
panic(err)
}
return val
}
// Ternary returns ifTrue when condition is true, otherwise ifFalse.
func Ternary[T any](condition bool, ifTrue, ifFalse T) T {
if condition {
return ifTrue
}
return ifFalse
}
// Without returns a copy of collection with all occurrences of exclude removed.
func Without[T comparable](collection []T, exclude ...T) []T {
result := make([]T, 0, len(collection))
for _, item := range collection {
excluded := false
for _, ex := range exclude {
if item == ex {
excluded = true
break
}
}
if !excluded {
result = append(result, item)
}
}
return result
}

View File

@@ -0,0 +1,23 @@
package operatingsystem
// OS contains information about the operating system
type OS struct {
ID string `json:"ID"`
Name string `json:"Name"`
Version string `json:"Version"`
Branding string `json:"Branding"`
}
func (o *OS) AsLogSlice() []any {
return []any{
"ID", o.ID,
"Name", o.Name,
"Version", o.Version,
"Branding", o.Branding,
}
}
// Info retrieves information about the current platform
func Info() (*OS, error) {
return platformInfo()
}

View File

@@ -0,0 +1,17 @@
//go:build android
package operatingsystem
import (
"fmt"
"runtime"
)
func platformInfo() (*OS, error) {
return &OS{
ID: "android",
Name: "Android",
Version: fmt.Sprintf("Go %s", runtime.Version()),
Branding: "Android",
}, nil
}

View File

@@ -0,0 +1,72 @@
//go:build darwin
package operatingsystem
import (
"os/exec"
"strings"
)
var macOSNames = map[string]string{
"10.10": "Yosemite",
"10.11": "El Capitan",
"10.12": "Sierra",
"10.13": "High Sierra",
"10.14": "Mojave",
"10.15": "Catalina",
"11": "Big Sur",
"12": "Monterey",
"13": "Ventura",
"14": "Sonoma",
"15": "Sequoia",
// Add newer versions as they are released...
}
func getOSName(version string) string {
trimmedVersion := version
if !strings.HasPrefix(version, "10.") {
trimmedVersion = strings.SplitN(version, ".", 2)[0]
}
name, ok := macOSNames[trimmedVersion]
if ok {
return name
}
return "MacOS " + version
}
func getSysctlValue(key string) (string, error) {
// Run "sysctl" command
command := exec.Command("sysctl", key)
// Capture stdout
var stdout strings.Builder
command.Stdout = &stdout
// Run command
err := command.Run()
if err != nil {
return "", err
}
version := strings.TrimPrefix(stdout.String(), key+": ")
return strings.TrimSpace(version), nil
}
func platformInfo() (*OS, error) {
// Default value
var result OS
result.ID = "Unknown"
result.Name = "MacOS"
result.Version = "Unknown"
version, err := getSysctlValue("kern.osproductversion")
if err != nil {
return nil, err
}
result.Version = version
ID, err := getSysctlValue("kern.osversion")
if err != nil {
return nil, err
}
result.ID = ID
result.Branding = getOSName(result.Version)
return &result, nil
}

View File

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

View File

@@ -0,0 +1,34 @@
//go:build windows
package operatingsystem
import (
"fmt"
"github.com/wailsapp/wails/v3/pkg/w32"
"golang.org/x/sys/windows/registry"
)
func platformInfo() (*OS, error) {
// Default value
var result OS
result.ID = "Unknown"
result.Name = "Windows"
result.Version = "Unknown"
// Credit: https://stackoverflow.com/a/33288328
// Ignore errors as it isn't a showstopper
key, _ := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
productName, _, _ := key.GetStringValue("ProductName")
currentBuild, _, _ := key.GetStringValue("CurrentBuildNumber")
displayVersion, _, _ := key.GetStringValue("DisplayVersion")
releaseId, _, _ := key.GetStringValue("ReleaseId")
result.Name = productName
result.Version = fmt.Sprintf("%s (Build: %s)", releaseId, currentBuild)
result.ID = displayVersion
result.Branding = w32.GetBranding()
return &result, key.Close()
}

View File

@@ -0,0 +1,62 @@
//go:build windows
package operatingsystem
import (
"strconv"
"golang.org/x/sys/windows/registry"
)
type WindowsVersionInfo struct {
Major int
Minor int
Build int
DisplayVersion string
}
func (w *WindowsVersionInfo) IsWindowsVersionAtLeast(major, minor, buildNumber int) bool {
return w.Major >= major && w.Minor >= minor && w.Build >= buildNumber
}
func GetWindowsVersionInfo() (*WindowsVersionInfo, error) {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
if err != nil {
return nil, err
}
return &WindowsVersionInfo{
Major: regDWORDKeyAsInt(key, "CurrentMajorVersionNumber"),
Minor: regDWORDKeyAsInt(key, "CurrentMinorVersionNumber"),
Build: regStringKeyAsInt(key, "CurrentBuildNumber"),
DisplayVersion: regKeyAsString(key, "DisplayVersion"),
}, nil
}
func regDWORDKeyAsInt(key registry.Key, name string) int {
result, _, err := key.GetIntegerValue(name)
if err != nil {
return -1
}
return int(result)
}
func regStringKeyAsInt(key registry.Key, name string) int {
resultStr, _, err := key.GetStringValue(name)
if err != nil {
return -1
}
result, err := strconv.Atoi(resultStr)
if err != nil {
return -1
}
return result
}
func regKeyAsString(key registry.Key, name string) string {
resultStr, _, err := key.GetStringValue(name)
if err != nil {
return ""
}
return resultStr
}

View File

@@ -0,0 +1,42 @@
//go:build linux && cgo && !gtk3 && !android
package operatingsystem
/*
#cgo linux pkg-config: gtk4 webkitgtk-6.0
#include <webkit/webkit.h>
*/
import "C"
import "fmt"
type WebkitVersion struct {
Major uint
Minor uint
Micro uint
}
func GetWebkitVersion() WebkitVersion {
var major, minor, micro C.uint
major = C.webkit_get_major_version()
minor = C.webkit_get_minor_version()
micro = C.webkit_get_micro_version()
return WebkitVersion{
Major: uint(major),
Minor: uint(minor),
Micro: uint(micro),
}
}
func (v WebkitVersion) String() string {
return fmt.Sprintf("v%d.%d.%d", v.Major, v.Minor, v.Micro)
}
func (v WebkitVersion) IsAtLeast(major int, minor int, micro int) bool {
if v.Major != uint(major) {
return v.Major > uint(major)
}
if v.Minor != uint(minor) {
return v.Minor > uint(minor)
}
return v.Micro >= uint(micro)
}

View File

@@ -0,0 +1,42 @@
//go:build linux && cgo && gtk3 && !android
package operatingsystem
/*
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.1
#include <webkit2/webkit2.h>
*/
import "C"
import "fmt"
type WebkitVersion struct {
Major uint
Minor uint
Micro uint
}
func GetWebkitVersion() WebkitVersion {
var major, minor, micro C.uint
major = C.webkit_get_major_version()
minor = C.webkit_get_minor_version()
micro = C.webkit_get_micro_version()
return WebkitVersion{
Major: uint(major),
Minor: uint(minor),
Micro: uint(micro),
}
}
func (v WebkitVersion) String() string {
return fmt.Sprintf("v%d.%d.%d", v.Major, v.Minor, v.Micro)
}
func (v WebkitVersion) IsAtLeast(major int, minor int, micro int) bool {
if v.Major != uint(major) {
return v.Major > uint(major)
}
if v.Minor != uint(minor) {
return v.Minor > uint(minor)
}
return v.Micro >= uint(micro)
}

View File

@@ -0,0 +1,49 @@
package optional
// True is a Bool set to true.
var True = NewBool(true)
// False is a Bool set to false.
var False = NewBool(false)
// Bool is an optional bool value.
type Bool = Var[bool]
// NewBool creates a new Bool with the given value.
func NewBool(val bool) Bool {
return NewVar(val)
}
// Var is a generic optional value that tracks whether it has been set.
type Var[T any] struct {
val T
set bool
}
// Get returns the value, or the zero value if unset.
func (v *Var[T]) Get() T {
return v.val
}
// Set assigns a value and marks the variable as set.
func (v *Var[T]) Set(val T) {
v.val = val
v.set = true
}
// IsSet reports whether a value has been assigned.
func (v *Var[T]) IsSet() bool {
return v.set
}
// Unset resets the variable to the zero value and marks it as unset.
func (v *Var[T]) Unset() {
v.set = false
var zero T
v.val = zero
}
// NewVar creates a new Var with the given value, marked as set.
func NewVar[T any](val T) Var[T] {
return Var[T]{val: val, set: true}
}

View File

@@ -0,0 +1,5 @@
node_modules
.task
*.tsbuildinfo
desktop/@wailsio/runtime/dist/
desktop/@wailsio/runtime/types/

View File

@@ -0,0 +1,3 @@
# Runtime
To rebuild the runtime run `task build` or if you have Wails v3 CLI, you can use `wails3 task build`.

View File

@@ -0,0 +1,128 @@
# https://taskfile.dev
version: "3"
vars:
ESBUILD: desktop/@wailsio/runtime/node_modules/.bin/esbuild
tasks:
install-deps:
internal: true
# once: build:debug and build:production dep on this in parallel; two
# concurrent npm installs in the same directory corrupt node_modules.
run: once
dir: desktop/@wailsio/runtime
sources:
- package.json
cmds:
- npm install
check:
dir: desktop/@wailsio/runtime
deps:
- install-deps
cmds:
- npm run check
test:
dir: desktop/@wailsio/runtime
deps:
- install-deps
cmds:
- npm test
build:debug:
internal: true
deps:
- install-deps
cmds:
- "{{.ESBUILD}} desktop/@wailsio/runtime/src/index.ts --inject:desktop/compiled/main.js --format=esm --target=safari11 --bundle --ignore-annotations --tree-shaking=true --sourcemap=inline --outfile=../assetserver/bundledassets/runtime.debug.js --define:DEBUG=true"
build:production:
internal: true
deps:
- install-deps
cmds:
- "{{.ESBUILD}} desktop/@wailsio/runtime/src/index.ts --inject:desktop/compiled/main.js --format=esm --target=safari11 --bundle --ignore-annotations --tree-shaking=true --minify --outfile=../assetserver/bundledassets/runtime.js --define:DEBUG=false --drop:console"
build:docs:
internal: true
dir: desktop/@wailsio/runtime
deps:
- install-deps
cmds:
- npm run build:docs
build:docs:md:
internal: true
dir: desktop/@wailsio/runtime
deps:
- install-deps
cmds:
- npm run build:docs:md
build:assets:
desc: Rebuild only the embedded runtime bundles (bundledassets); CI verifies PR bundles against this exact output.
deps:
- build:debug
- build:production
build:runtime:
internal: true
deps:
- build:debug
- build:production
cmds:
- cmd: echo "Runtime build complete."
build:all:
internal: true
cmds:
- task: generate:events
- task: build:docs
- task: build:runtime
- echo "Build Complete."
build:
deps:
- install-deps
cmds:
- task: build:all
docs:
summary: Generate TypeDoc documentation for the runtime
dir: desktop/@wailsio/runtime
deps:
- install-deps
cmds:
- npm run build:docs
- echo "Documentation generated at desktop/@wailsio/runtime/docs/"
docs:md:
summary: Generate markdown documentation for the runtime
dir: desktop/@wailsio/runtime
deps:
- install-deps
cmds:
- npm run build:docs:md
- echo "Markdown documentation generated"
generate:events:
dir: ../../tasks/events
cmds:
- go run generate.go
- go fmt ../../pkg/events/events.go
clean:
summary: Clean built artifacts and documentation
dir: desktop/@wailsio/runtime
cmds:
- npm run clean
- echo "Cleaned runtime artifacts"
generate:
summary: Generate events only (use runtime:build to rebuild everything)
cmds:
- task: generate:events
- echo "Events generated. Run 'wails3 task runtime:build' to rebuild runtime with updated documentation"

View File

@@ -0,0 +1,6 @@
{
"name": "runtime",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@@ -0,0 +1,22 @@
package runtime
import (
"fmt"
"encoding/json"
)
var runtimeInit = `window._wails=window._wails||{};window._wails.flags=window._wails.flags||{};window.wails=window.wails||{};`
var runtimeConfigReady = `Promise.resolve().then(function(){window.dispatchEvent(new Event("wails:runtime-config-ready"));});`
func Core(flags map[string]any) string {
flagsStr := ""
if len(flags) > 0 {
f, err := json.Marshal(flags)
if err == nil {
flagsStr += fmt.Sprintf("window._wails.flags=%s;", f)
}
}
return runtimeInit + flagsStr + invoke + environment + runtimeConfigReady
}

View File

@@ -0,0 +1,16 @@
//go:build android
package runtime
// Android uses window.wails.invoke which is set up via addJavascriptInterface in WailsJSBridge
// We need to log the state to debug why it's not being detected
var invoke = `
console.log('[Wails Android Runtime] Injecting runtime, window.wails exists:', !!window.wails);
console.log('[Wails Android Runtime] window.wails.invoke exists:', !!(window.wails && window.wails.invoke));
window._wails.invoke=function(m){
console.log('[Wails Android Runtime] _wails.invoke called:', m);
return window.wails.invoke(typeof m==='string'?m:JSON.stringify(m));
};
console.log('[Wails Android Runtime] Runtime injection complete');
`
var flags = ""

View File

@@ -0,0 +1,5 @@
//go:build darwin
package runtime
var invoke = "window._wails.invoke=function(msg){window.webkit.messageHandlers.external.postMessage(msg);};"

View File

@@ -0,0 +1,10 @@
//go:build !production
package runtime
import (
"fmt"
"runtime"
)
var environment = fmt.Sprintf(`window._wails.environment={"OS":"%s","Arch":"%s","Debug":true};`, runtime.GOOS, runtime.GOARCH)

View File

@@ -0,0 +1,11 @@
//go:build linux && !android
package runtime
// On webkit2gtk, `messageHandlers.external.postMessage` only works when
// `this` is bound to the handler object. Assigning the bare function
// reference (as we did historically) silently swallows messages when
// called as `window._wails.invoke(msg)` — the page's invoke loses the
// receiver. Wrap it like darwin does so callers can invoke without
// thinking about receiver binding.
var invoke = "window._wails.invoke=function(msg){window.webkit.messageHandlers.external.postMessage(msg);};"

View File

@@ -0,0 +1,8 @@
//go:build production
package runtime
import "fmt"
import goruntime "runtime"
var environment = fmt.Sprintf(`window._wails.environment={"OS":"%s","Arch":"%s","Debug":false};`, goruntime.GOOS, goruntime.GOARCH)

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