Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/src/content/docs/reference/application.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -463,12 +463,59 @@ app := application.New(application.Options{
app := application.New(application.Options{
Name: "My App",
Linux: application.LinuxOptions{
ApplicationID: "com.myapp.myapplication",
ProgramName: "my-app",
DisableQuitOnLastWindowClosed: false,
},
})
```

- `ApplicationID` - The GTK application id. Defaults to `org.wails.` followed by a sanitised `Name`.
- `ProgramName` - Sets the program name for the window manager via `g_set_prgname()`. Defaults to `ApplicationID` when that is set.
- `DisableQuitOnLastWindowClosed` - Keeps the application running after its last window closes.

Set `ApplicationID` to match the identity your packaging declares. It should be
the same reverse-DNS id you use for the `.desktop` file, so the desktop
environment can associate the application's windows with its launcher.

The id has to satisfy [`g_application_id_is_valid()`](https://docs.gtk.org/gio/type_func.Application.id_is_valid.html):
two or more non-empty elements separated by a `.`, each holding only the ASCII
characters `A-Z`, `a-z`, `0-9`, `_` and `-`, none of them starting with a digit,
and at most 255 characters in total. So `com.example.MyApp` is fine, while
`MyApp`, `com.example.2ndApp` and `com.example.My App` are not. An id that GTK
would reject is reported through the application's error handler and replaced
with the derived default, because GTK only asserts on the id and would otherwise
abort the process during startup.

On Wayland, GTK takes the surface `app_id` from the program name rather than the
application id, so `ProgramName` defaults to `ApplicationID` when you set one.
Setting the same string twice is not needed, and setting `ProgramName`
explicitly still wins:

```go
app := application.New(application.Options{
Name: "My App",
Linux: application.LinuxOptions{
ApplicationID: "com.example.MyApp", // also becomes the program name
},
})
```

:::note
Sandboxed builds must set this. A flatpak may only own D-Bus names prefixed with
its own app id, and WebKit asks the portal to own
`<application id>.Sandboxed.WebProcess-<uuid>` for the accessibility bus. With
the default id that request is refused and the web process aborts, taking the
application down during startup:

```
Portal call failed: Invalid sandbox a11y own name:
'org.wails.myapp.Sandboxed.WebProcess-<uuid>' doesn't match app id
```

Setting `ApplicationID` to the `app-id` in your flatpak manifest resolves it.
:::

## Complete Application Example

```go
Expand Down
29 changes: 8 additions & 21 deletions v3/pkg/application/application_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"sync"
Expand All @@ -27,22 +26,6 @@ import (
"github.com/wailsapp/wails/v3/pkg/events"
)

var invalidAppNameChars = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
var leadingDigits = regexp.MustCompile(`^[0-9]+`)

func sanitizeAppName(name string) string {
name = invalidAppNameChars.ReplaceAllString(name, "_")
name = leadingDigits.ReplaceAllString(name, "_$0")
for strings.Contains(name, "__") {
name = strings.ReplaceAll(name, "__", "_")
}
name = strings.Trim(name, "_")
if name == "" {
name = "wailsapp"
}
return strings.ToLower(name)
}

func init() {
// Disable DMA-BUF renderer on any session type with NVIDIA to prevent blank windows and
// "Error 71 (Protocol error)" crashes. NVIDIA proprietary drivers fail gbm_bo_map() when
Expand Down Expand Up @@ -232,16 +215,20 @@ func (a *linuxApp) unregisterWindow(window windowPointer) {
}

func newPlatformApp(parent *App) *linuxApp {
name := sanitizeAppName(parent.options.Name)
appID, err := applicationID(parent.options)
if err != nil {
parent.error("invalid Linux.ApplicationID: %w; falling back to %q", err, appID)
}

app := &linuxApp{
parent: parent,
application: appNew(name),
application: appNew(appID),
activated: make(chan struct{}),
windowMap: map[windowPointer]uint{},
}

if parent.options.Linux.ProgramName != "" {
setProgramName(parent.options.Linux.ProgramName)
if name := programName(parent.options, appID); name != "" {
setProgramName(name)
}

return app
Expand Down
136 changes: 136 additions & 0 deletions v3/pkg/application/application_linux_appid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//go:build linux && cgo && !android && !server

package application

import (
"errors"
"fmt"
"regexp"
"strings"
)

var invalidAppNameChars = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
var leadingDigits = regexp.MustCompile(`^[0-9]+`)

// sanitizeAppName sanitizes the application name into a single element of a
// GTK/D-Bus application id: only alphanumeric characters, hyphens and
// underscores, and never a leading digit.
func sanitizeAppName(name string) string {
// Replace invalid characters with underscores
name = invalidAppNameChars.ReplaceAllString(name, "_")
// Remove consecutive underscores
for strings.Contains(name, "__") {
name = strings.ReplaceAll(name, "__", "_")
}
// Trim leading/trailing underscores
name = strings.Trim(name, "_")
if name == "" {
name = "wailsapp"
}
// Prefix with underscore if starts with digit. This has to happen after the
// trim, which would otherwise strip the prefix again and leave an element
// GTK refuses, e.g. "1Password" -> "org.wails.1password".
name = leadingDigits.ReplaceAllString(name, "_$0")
return strings.ToLower(name)
}

// maxApplicationIDLength is the longest id GTK accepts, inherited from the
// D-Bus bus name limit.
const maxApplicationIDLength = 255

// validateApplicationID returns an error describing why GTK would refuse id,
// following the same contract as g_application_id_is_valid():
//
// - the id is composed of two or more elements separated by a '.', and every
// element holds at least one character;
// - every element contains only the ASCII characters A-Z, a-z, 0-9, '_' and
// '-', and does not begin with a digit;
// - the id is at most 255 characters long.
//
// GTK only asserts on this, so an invalid id makes gtk_application_new() return
// NULL and takes the process down later, far away from the option that caused it.
//
// See: https://docs.gtk.org/gio/type_func.Application.id_is_valid.html
func validateApplicationID(id string) error {
if id == "" {
return errors.New("application id is empty")
}
if len(id) > maxApplicationIDLength {
return fmt.Errorf("application id %q is %d characters long, the maximum is %d", id, len(id), maxApplicationIDLength)
}

elements := strings.Split(id, ".")
if len(elements) < 2 {
return fmt.Errorf("application id %q needs at least two elements separated by a '.', for example \"com.example.MyApp\"", id)
}

for _, element := range elements {
if element == "" {
return fmt.Errorf("application id %q has an empty element: it must not start or end with a '.', or contain \"..\"", id)
}
if element[0] >= '0' && element[0] <= '9' {
return fmt.Errorf("application id %q has the element %q starting with a digit", id, element)
}
for i := 0; i < len(element); i++ {
if !isApplicationIDChar(element[i]) {
return fmt.Errorf("application id %q contains the invalid character %q: only A-Z, a-z, 0-9, '_' and '-' are allowed", id, rune(element[i]))
}
}
}

return nil
}

func isApplicationIDChar(c byte) bool {
return c >= 'A' && c <= 'Z' ||
c >= 'a' && c <= 'z' ||
c >= '0' && c <= '9' ||
c == '_' || c == '-'
}

// applicationID returns the id to build the GtkApplication with. Options.Linux
// wins when it sets one, so sandboxed builds can match the id their runtime
// expects; everything else keeps the derived "org.wails.<name>".
//
// An id GTK would reject is reported as an error together with the derived id,
// so callers can carry on with an id that works instead of crashing inside GTK.
func applicationID(options Options) (string, error) {
derived := "org.wails." + sanitizeAppName(options.Name)
if len(derived) > maxApplicationIDLength {
// sanitizeAppName never emits a '.', so cutting the tail can only leave
// characters that are legal in the middle of an element.
derived = derived[:maxApplicationIDLength]
}

id := options.Linux.ApplicationID
if id == "" {
return derived, nil
}
if err := validateApplicationID(id); err != nil {
return derived, err
}
return id, nil
}

// programName returns the name to hand to g_set_prgname, or "" to leave the
// program name at whatever GTK picked up from the executable.
//
// GTK takes the Wayland surface app_id from g_get_prgname(), so a window is only
// matched with its .desktop file when the program name carries the application
// id as well. An application that sets Options.Linux.ApplicationID inherits it
// here rather than having to repeat the same string in ProgramName.
//
// What is inherited is appID, the id the GtkApplication was built with, so the
// program name cannot disagree with it: an ApplicationID that validation
// rejected falls back to the derived id in both places. Without an
// ApplicationID nothing is derived, keeping the program name of applications
// that set neither option as it was.
func programName(options Options, appID string) string {
if options.Linux.ProgramName != "" {
return options.Linux.ProgramName
}
if options.Linux.ApplicationID != "" {
return appID
}
return ""
}
Loading
Loading