mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-16 15:45:50 +01:00
* fix: prevent window menu crash on Wayland by realizing window before showing On Wayland with GTK3, the appmenu-gtk-module tries to set DBus properties for global menu integration before the window is fully realized, causing a crash with the error "GDK_IS_WAYLAND_WINDOW (window) assertion failed". The fix calls gtk_widget_realize() before gtk_widget_show_all() to ensure the window has a valid GdkWindow when the menu system accesses it. This fix is applied to both the CGO and purego implementations. Fixes wailsapp/wails#4769 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add Wayland detection to wails3 doctor and test for #4769 - Add Wayland session detection row to `wails3 doctor` output on Linux - Create test project v3/test/4769-menu to manually verify the menu fix 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: sanitize GTK application name to prevent crashes with invalid characters Application names containing spaces, parentheses, hash symbols, or other invalid characters would cause GTK to fail with an assertion error. Added sanitizeAppName() function that: - Replaces invalid characters with underscores - Handles leading digits - Removes consecutive underscores - Defaults to "wailsapp" if empty 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
347 lines
9.1 KiB
Go
347 lines
9.1 KiB
Go
//go:build linux && !android
|
|
|
|
package application
|
|
|
|
/*
|
|
#include "gtk/gtk.h"
|
|
#include "webkit2/webkit2.h"
|
|
static guint get_compiled_gtk_major_version() { return GTK_MAJOR_VERSION; }
|
|
static guint get_compiled_gtk_minor_version() { return GTK_MINOR_VERSION; }
|
|
static guint get_compiled_gtk_micro_version() { return GTK_MICRO_VERSION; }
|
|
static guint get_compiled_webkit_major_version() { return WEBKIT_MAJOR_VERSION; }
|
|
static guint get_compiled_webkit_minor_version() { return WEBKIT_MINOR_VERSION; }
|
|
static guint get_compiled_webkit_micro_version() { return WEBKIT_MICRO_VERSION; }
|
|
*/
|
|
import "C"
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
|
|
"path/filepath"
|
|
|
|
"github.com/godbus/dbus/v5"
|
|
"github.com/wailsapp/wails/v3/internal/operatingsystem"
|
|
"github.com/wailsapp/wails/v3/pkg/events"
|
|
)
|
|
|
|
// sanitizeAppName sanitizes the application name to be a valid GTK/D-Bus application ID.
|
|
// Valid IDs contain only alphanumeric characters, hyphens, and underscores.
|
|
// They must not start with a digit.
|
|
var invalidAppNameChars = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
|
|
var leadingDigits = regexp.MustCompile(`^[0-9]+`)
|
|
|
|
func sanitizeAppName(name string) string {
|
|
// Replace invalid characters with underscores
|
|
name = invalidAppNameChars.ReplaceAllString(name, "_")
|
|
// Prefix with underscore if starts with digit
|
|
name = leadingDigits.ReplaceAllString(name, "_$0")
|
|
// Remove consecutive underscores
|
|
for strings.Contains(name, "__") {
|
|
name = strings.ReplaceAll(name, "__", "_")
|
|
}
|
|
// Trim leading/trailing underscores
|
|
name = strings.Trim(name, "_")
|
|
if name == "" {
|
|
name = "wailsapp"
|
|
}
|
|
return strings.ToLower(name)
|
|
}
|
|
|
|
func init() {
|
|
// FIXME: This should be handled appropriately in the individual files most likely.
|
|
// Set GDK_BACKEND=x11 if currently unset and XDG_SESSION_TYPE is unset, unspecified or x11 to prevent warnings
|
|
if os.Getenv("GDK_BACKEND") == "" &&
|
|
(os.Getenv("XDG_SESSION_TYPE") == "" || os.Getenv("XDG_SESSION_TYPE") == "unspecified" || os.Getenv("XDG_SESSION_TYPE") == "x11") {
|
|
_ = os.Setenv("GDK_BACKEND", "x11")
|
|
}
|
|
|
|
// Disable DMA-BUF renderer on Wayland with NVIDIA to prevent "Error 71 (Protocol error)" crashes.
|
|
// This is a known WebKitGTK issue with NVIDIA proprietary drivers on Wayland.
|
|
// See: https://bugs.webkit.org/show_bug.cgi?id=262607
|
|
if os.Getenv("WEBKIT_DISABLE_DMABUF_RENDERER") == "" &&
|
|
os.Getenv("XDG_SESSION_TYPE") == "wayland" &&
|
|
isNVIDIAGPU() {
|
|
_ = os.Setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1")
|
|
}
|
|
}
|
|
|
|
// isNVIDIAGPU checks if an NVIDIA GPU is present by looking for the nvidia kernel module.
|
|
func isNVIDIAGPU() bool {
|
|
// Check if nvidia module is loaded (most reliable for proprietary driver)
|
|
if _, err := os.Stat("/sys/module/nvidia"); err == nil {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
type linuxApp struct {
|
|
application pointer
|
|
parent *App
|
|
|
|
startupActions []func()
|
|
|
|
// Native -> uint
|
|
windowMap map[windowPointer]uint
|
|
windowMapLock sync.Mutex
|
|
|
|
theme string
|
|
|
|
icon pointer
|
|
}
|
|
|
|
func (a *linuxApp) GetFlags(options Options) map[string]any {
|
|
if options.Flags == nil {
|
|
options.Flags = make(map[string]any)
|
|
}
|
|
return options.Flags
|
|
}
|
|
|
|
func getNativeApplication() *linuxApp {
|
|
return globalApplication.impl.(*linuxApp)
|
|
}
|
|
|
|
func (a *linuxApp) hide() {
|
|
a.hideAllWindows()
|
|
}
|
|
|
|
func (a *linuxApp) show() {
|
|
a.showAllWindows()
|
|
}
|
|
|
|
func (a *linuxApp) on(eventID uint) {
|
|
// TODO: Test register/unregister events
|
|
//C.registerApplicationEvent(l.application, C.uint(eventID))
|
|
}
|
|
|
|
func (a *linuxApp) name() string {
|
|
return appName()
|
|
}
|
|
|
|
type rnr struct {
|
|
f func()
|
|
}
|
|
|
|
func (r rnr) run() {
|
|
r.f()
|
|
}
|
|
|
|
func (a *linuxApp) setApplicationMenu(menu *Menu) {
|
|
// FIXME: How do we avoid putting a menu?
|
|
if menu == nil {
|
|
// Create a default menu
|
|
menu = DefaultApplicationMenu()
|
|
globalApplication.applicationMenu = menu
|
|
}
|
|
}
|
|
|
|
func (a *linuxApp) run() error {
|
|
|
|
if len(os.Args) == 2 { // Case: program + 1 argument
|
|
arg1 := os.Args[1]
|
|
// Check if the argument is likely a URL from a custom protocol invocation
|
|
if strings.Contains(arg1, "://") {
|
|
a.parent.info("Application launched with argument, potentially a URL from custom protocol", "url", arg1)
|
|
eventContext := newApplicationEventContext()
|
|
eventContext.setURL(arg1)
|
|
applicationEvents <- &ApplicationEvent{
|
|
Id: uint(events.Common.ApplicationLaunchedWithUrl),
|
|
ctx: eventContext,
|
|
}
|
|
} else {
|
|
// Check if the argument matches any file associations
|
|
if a.parent.options.FileAssociations != nil {
|
|
ext := filepath.Ext(arg1)
|
|
if slices.Contains(a.parent.options.FileAssociations, ext) {
|
|
a.parent.info("File opened via file association", "file", arg1, "extension", ext)
|
|
eventContext := newApplicationEventContext()
|
|
eventContext.setOpenedWithFile(arg1)
|
|
applicationEvents <- &ApplicationEvent{
|
|
Id: uint(events.Common.ApplicationOpenedWithFile),
|
|
ctx: eventContext,
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
a.parent.info("Application launched with single argument (not a URL), potential file open?", "arg", arg1)
|
|
}
|
|
} else if len(os.Args) > 2 {
|
|
// Log if multiple arguments are passed
|
|
a.parent.info("Application launched with multiple arguments", "args", os.Args[1:])
|
|
}
|
|
|
|
a.parent.Event.OnApplicationEvent(events.Linux.ApplicationStartup, func(evt *ApplicationEvent) {
|
|
// TODO: What should happen here?
|
|
})
|
|
a.setupCommonEvents()
|
|
a.monitorThemeChanges()
|
|
return appRun(a.application)
|
|
}
|
|
|
|
func (a *linuxApp) unregisterWindow(w windowPointer) {
|
|
a.windowMapLock.Lock()
|
|
delete(a.windowMap, w)
|
|
a.windowMapLock.Unlock()
|
|
|
|
// If this was the last window...
|
|
if len(a.windowMap) == 0 && !a.parent.options.Linux.DisableQuitOnLastWindowClosed {
|
|
a.destroy()
|
|
}
|
|
}
|
|
|
|
func (a *linuxApp) destroy() {
|
|
if !globalApplication.shouldQuit() {
|
|
return
|
|
}
|
|
globalApplication.cleanup()
|
|
appDestroy(a.application)
|
|
}
|
|
|
|
func (a *linuxApp) isOnMainThread() bool {
|
|
return isOnMainThread()
|
|
}
|
|
|
|
// register our window to our parent mapping
|
|
func (a *linuxApp) registerWindow(window pointer, id uint) {
|
|
a.windowMapLock.Lock()
|
|
a.windowMap[windowPointer(window)] = id
|
|
a.windowMapLock.Unlock()
|
|
}
|
|
|
|
func (a *linuxApp) isDarkMode() bool {
|
|
return strings.Contains(a.theme, "dark")
|
|
}
|
|
|
|
func (a *linuxApp) getAccentColor() string {
|
|
// Linux doesn't have a unified system accent color API
|
|
// Return a default blue color
|
|
return "rgb(0,122,255)"
|
|
}
|
|
|
|
func (a *linuxApp) monitorThemeChanges() {
|
|
go func() {
|
|
defer handlePanic()
|
|
conn, err := dbus.ConnectSessionBus()
|
|
if err != nil {
|
|
a.parent.info(
|
|
"[WARNING] Failed to connect to session bus; monitoring for theme changes will not function:",
|
|
err,
|
|
)
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
|
|
if err = conn.AddMatchSignal(
|
|
dbus.WithMatchObjectPath("/org/freedesktop/portal/desktop"),
|
|
); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
c := make(chan *dbus.Signal, 10)
|
|
conn.Signal(c)
|
|
|
|
getTheme := func(body []interface{}) (string, bool) {
|
|
if len(body) < 2 {
|
|
return "", false
|
|
}
|
|
if entry, ok := body[0].(string); !ok || entry != "org.gnome.desktop.interface" {
|
|
return "", false
|
|
}
|
|
if entry, ok := body[1].(string); ok && entry == "color-scheme" {
|
|
return body[2].(dbus.Variant).Value().(string), true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
for v := range c {
|
|
theme, ok := getTheme(v.Body)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if theme != a.theme {
|
|
a.theme = theme
|
|
event := newApplicationEvent(events.Linux.SystemThemeChanged)
|
|
event.Context().setIsDarkMode(a.isDarkMode())
|
|
applicationEvents <- event
|
|
}
|
|
|
|
}
|
|
}()
|
|
}
|
|
|
|
func newPlatformApp(parent *App) *linuxApp {
|
|
name := sanitizeAppName(parent.options.Name)
|
|
app := &linuxApp{
|
|
parent: parent,
|
|
application: appNew(name),
|
|
windowMap: map[windowPointer]uint{},
|
|
}
|
|
|
|
if parent.options.Linux.ProgramName != "" {
|
|
setProgramName(parent.options.Linux.ProgramName)
|
|
}
|
|
|
|
return app
|
|
}
|
|
|
|
// logPlatformInfo logs the platform information to the console
|
|
func (a *App) logPlatformInfo() {
|
|
info, err := operatingsystem.Info()
|
|
if err != nil {
|
|
a.error("error getting OS info: %w", err)
|
|
return
|
|
}
|
|
|
|
wkVersion := operatingsystem.GetWebkitVersion()
|
|
platformInfo := info.AsLogSlice()
|
|
platformInfo = append(platformInfo, "Webkit2Gtk", wkVersion)
|
|
|
|
a.info("Platform Info:", platformInfo...)
|
|
}
|
|
|
|
//export processWindowEvent
|
|
func processWindowEvent(windowID C.uint, eventID C.uint) {
|
|
windowEvents <- &windowEvent{
|
|
WindowID: uint(windowID),
|
|
EventID: uint(eventID),
|
|
}
|
|
}
|
|
|
|
func buildVersionString(major, minor, micro C.uint) string {
|
|
return fmt.Sprintf("%d.%d.%d", uint(major), uint(minor), uint(micro))
|
|
}
|
|
|
|
func (a *App) platformEnvironment() map[string]any {
|
|
result := map[string]any{}
|
|
result["gtk3-compiled"] = buildVersionString(
|
|
C.get_compiled_gtk_major_version(),
|
|
C.get_compiled_gtk_minor_version(),
|
|
C.get_compiled_gtk_micro_version(),
|
|
)
|
|
result["gtk3-runtime"] = buildVersionString(
|
|
C.gtk_get_major_version(),
|
|
C.gtk_get_minor_version(),
|
|
C.gtk_get_micro_version(),
|
|
)
|
|
|
|
result["webkit2gtk-compiled"] = buildVersionString(
|
|
C.get_compiled_webkit_major_version(),
|
|
C.get_compiled_webkit_minor_version(),
|
|
C.get_compiled_webkit_micro_version(),
|
|
)
|
|
result["webkit2gtk-runtime"] = buildVersionString(
|
|
C.webkit_get_major_version(),
|
|
C.webkit_get_minor_version(),
|
|
C.webkit_get_micro_version(),
|
|
)
|
|
return result
|
|
}
|
|
|
|
func fatalHandler(errFunc func(error)) {
|
|
// Stub for windows function
|
|
return
|
|
}
|