Merge v3-alpha into fix/security-issues-bundle and address review comments

Resolve merge conflicts:
- build-and-test-v3.yml: take v3-alpha step names (non-experimental GTK4)
- linux_cgo_gtk4.h: remove WailsScreen struct (deleted in v3-alpha)

Address PR review comments:
- screen/main.go: use path.Clean for HTTP paths instead of filepath.Clean,
  fix Windows drive-letter check (was dead code), add filepath.FromSlash
- UNRELEASED_CHANGELOG.md: remove duplicate changelog entry
- build-and-test-v3.yml: add actions:write permission to cleanup job

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lea Anthony 2026-02-28 15:38:47 +11:00
commit d15d35a25e
39 changed files with 880 additions and 144 deletions

View file

@ -159,7 +159,7 @@ jobs:
task test:examples
echo "Example compilation tests (GTK3) completed successfully"
- name: Build Examples (GTK4 experimental)
- name: Build Examples (GTK4)
if: matrix.os == 'ubuntu-latest'
working-directory: v3
run: |
@ -189,11 +189,10 @@ jobs:
go test -v ./...
'
- name: Run tests (ubuntu) - GTK4 experimental
- name: Run tests (ubuntu) - GTK4
if: matrix.os == 'ubuntu-latest'
working-directory: v3
# Skip all service tests that hang in CI due to GTK4 display requirements
# The services tests require a fully functional GTK4 display which xvfb cannot provide
# Skip service tests that hang in CI due to GTK4 display requirements
run: >
xvfb-run --auto-servernum
sh -c '
@ -210,7 +209,8 @@ jobs:
if: always()
needs: [test_js, test_go, test_templates]
runs-on: ubuntu-latest
permissions: {}
permissions:
actions: write
steps:
- uses: geekyeggo/delete-artifact@v5
with:
@ -304,8 +304,7 @@ jobs:
cd ..
wails3 build
# Note: GTK4 template builds are not tested here as wails build doesn't
# support -tags flag yet. GTK4 compilation is verified by Go tests.
# GTK4 template builds are covered by the Go example compilation tests above.
build_results:
if: ${{ always() }}

View file

@ -138,6 +138,8 @@ export default defineConfig({
items: [
{ label: "Method Binding", link: "/features/bindings/methods" },
{ label: "Services", link: "/features/bindings/services" },
{ label: "Data Models", link: "/features/bindings/models" },
{ label: "Enums", link: "/features/bindings/enums" },
{ label: "Advanced Binding", link: "/features/bindings/advanced" },
{ label: "Best Practices", link: "/features/bindings/best-practices" },
],

View file

@ -30,6 +30,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
*/
## [Unreleased]
## v3.0.0-alpha.73 - 2026-02-27
## Fixed
- Fix frameless window cannot be minimized on darwin (#4294)
## v3.0.0-alpha.72 - 2026-02-16
## Fixed
- Fix 20-30 minute hangs during `wails3 build` and `wails3 dev` by excluding `node_modules/` from go-task's up-to-date checking. Previously, the `sources: "**/*"` glob caused go-task to enumerate and checksum every file in `node_modules/` (50-100k+ files with heavy dependencies like MUI), especially slow on Windows/NTFS (#4939)
## v3.0.0-alpha.71 - 2026-02-10
## Added
- Bumped ghw version for better Apple device support by @leaanthony (#4977)
- Add `GetBadge` method to the dock service
## Fixed
- Fix GTK4 build failure caused by C `Screen` typedef colliding with X11 Xlib.h (#4957)
- Fix dock badge methods consistency on macOS
## v3.0.0-alpha.70 - 2026-02-09
## Added
- Add `-tags` flag to `wails3 build` command for passing custom Go build tags (e.g., `wails3 build -tags gtk4`) (#4957)
- Add documentation for automatic enum generation in binding generator, including dedicated Enums page and sidebar navigation (#4972)
## Fixed
- Fix `InvisibleTitleBarHeight` being applied to all macOS windows instead of only frameless or transparent title bar windows (#4960)
- Fix window shaking/jitter when resizing from top corners with `InvisibleTitleBarHeight` enabled, by skipping drag initiation near window edges (#4960)
- Fix generation of mapped types with enum keys in JS/TS bindings (#4437) by @fbbdev
## v3.0.0-alpha.69 - 2026-02-08
## Added
- Add `-tags` flag to `wails3 build` command for passing custom Go build tags (e.g., `wails3 build -tags gtk4`) (#4957)
## Fixed
- Fix file drag-and-drop on Windows not working at non-100% display scaling
- Fix HTML5 internal drag-and-drop being broken when file drop was enabled on Windows
- Fix file drop coordinates being in wrong pixel space on Windows (physical vs CSS pixels)
- Fix file drag-and-drop on Linux not working reliably with hover effects
- Fix HTML5 internal drag-and-drop being broken when file drop was enabled on Linux
- Fix window show/hide on Linux/GTK4 sometimes restoring to minimized state by using `gtk_window_present()` (#4957)
- Fix window position get/set on Linux/GTK4 always returning 0,0 by adding X11-conditional support via `XTranslateCoordinates`/`XMoveWindow` (#4957)
- Fix max window size not being enforced on Linux/GTK4 by adding signal-based size clamping to replace removed `gtk_window_set_geometry_hints` (#4957)
- Fix DPI scaling on Linux/GTK4 by implementing proper PhysicalBounds calculation and fractional scaling support via `gdk_monitor_get_scale` (GTK 4.14+)
- Fix menu items duplicating when creating new windows on Linux/GTK4
- Fix generation of mapped types with enum keys in JS/TS bindings (#4437) by @fbbdev
## v3.0.0-alpha.68 - 2026-02-07
## Added

View file

@ -0,0 +1,529 @@
---
title: Enums
description: Automatic enum generation from Go constants
sidebar:
order: 4
---
import { Card, CardGrid } from "@astrojs/starlight/components";
## Enum Bindings
The Wails v3 binding generator **automatically detects Go constant types and generates TypeScript enums or JavaScript const objects**. No registration, no configuration — just define your types and constants in Go, and the generator handles the rest.
:::note
Unlike Wails v2, there is **no need to call `EnumBind`** or register enums manually. The generator discovers them automatically from your source code.
:::
## Quick Start
**Define a named type with constants in Go:**
```go
type Status string
const (
StatusActive Status = "active"
StatusPending Status = "pending"
StatusClosed Status = "closed"
)
```
**Use the type in a struct or service method:**
```go
type Ticket struct {
ID int `json:"id"`
Title string `json:"title"`
Status Status `json:"status"`
}
```
**Generate bindings:**
```bash
wails3 generate bindings
```
The generator output will report enum counts alongside models:
```
3 Enums, 5 Models
```
**Use in your frontend:**
```javascript
import { Ticket, Status } from './bindings/myapp/models'
const ticket = new Ticket({
id: 1,
title: "Bug report",
status: Status.StatusActive
})
```
**That's it!** The enum type is enforced in both Go and JavaScript/TypeScript.
## Defining Enums
An enum in Wails is a **named type** with an underlying basic type, combined with **const declarations** of that type.
### String Enums
```go
// Title is a title
type Title string
const (
// Mister is a title
Mister Title = "Mr"
Miss Title = "Miss"
Ms Title = "Ms"
Mrs Title = "Mrs"
Dr Title = "Dr"
)
```
**Generated TypeScript:**
```typescript
/**
* Title is a title
*/
export enum Title {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* Mister is a title
*/
Mister = "Mr",
Miss = "Miss",
Ms = "Ms",
Mrs = "Mrs",
Dr = "Dr",
}
```
**Generated JavaScript:**
```javascript
/**
* Title is a title
* @readonly
* @enum {string}
*/
export const Title = {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero: "",
/**
* Mister is a title
*/
Mister: "Mr",
Miss: "Miss",
Ms: "Ms",
Mrs: "Mrs",
Dr: "Dr",
};
```
### Integer Enums
```go
type Priority int
const (
PriorityLow Priority = 0
PriorityMedium Priority = 1
PriorityHigh Priority = 2
)
```
**Generated TypeScript:**
```typescript
export enum Priority {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = 0,
PriorityLow = 0,
PriorityMedium = 1,
PriorityHigh = 2,
}
```
### Type Alias Enums
Go type aliases (`=`) also work, but generate a slightly different output — a type definition plus a const object, rather than a native TypeScript `enum`:
```go
// Age is an integer with some predefined values
type Age = int
const (
NewBorn Age = 0
Teenager Age = 12
YoungAdult Age = 18
// Oh no, some grey hair!
MiddleAged Age = 50
Mathusalem Age = 1000 // Unbelievable!
)
```
**Generated TypeScript:**
```typescript
/**
* Age is an integer with some predefined values
*/
export type Age = number;
/**
* Predefined constants for type Age.
* @namespace
*/
export const Age = {
NewBorn: 0,
Teenager: 12,
YoungAdult: 18,
/**
* Oh no, some grey hair!
*/
MiddleAged: 50,
/**
* Unbelievable!
*/
Mathusalem: 1000,
};
```
**Generated JavaScript:**
```javascript
/**
* Age is an integer with some predefined values
* @typedef {number} Age
*/
/**
* Predefined constants for type Age.
* @namespace
*/
export const Age = {
NewBorn: 0,
Teenager: 12,
YoungAdult: 18,
/**
* Oh no, some grey hair!
*/
MiddleAged: 50,
/**
* Unbelievable!
*/
Mathusalem: 1000,
};
```
:::tip
**Named types** (`type Title string`) generate native TypeScript `enum` declarations with a `$zero` member.
**Type aliases** (`type Age = int`) generate a `type` + `const` namespace pair without `$zero`.
:::
## The `$zero` Value
Every named-type enum includes a special `$zero` member representing the **Go zero value** for the underlying type:
| Underlying Type | `$zero` Value |
|----------------|---------------|
| `string` | `""` |
| `int`, `int8`, `int16`, `int32`, `int64` | `0` |
| `uint`, `uint8`, `uint16`, `uint32`, `uint64` | `0` |
| `float32`, `float64` | `0` |
| `bool` | `false` |
When a struct field uses an enum type and no value is provided, the constructor defaults to `$zero`:
```typescript
export class Person {
"Title": Title;
constructor($$source: Partial<Person> = {}) {
if (!("Title" in $$source)) {
this["Title"] = Title.$zero; // defaults to ""
}
Object.assign(this, $$source);
}
}
```
This ensures type-safe initialisation when generating classes — enum fields are never `undefined`. When generating TypeScript interfaces (using `-i`), there is no constructor and fields may be absent as usual.
## Using Enums in Structs
When a struct field has an enum type, the generated code **preserves that type** rather than falling back to the primitive:
```go
type Person struct {
Title Title
Name string
Age Age
}
```
**Generated TypeScript:**
```typescript
export class Person {
"Title": Title;
"Name": string;
"Age": Age;
constructor($$source: Partial<Person> = {}) {
if (!("Title" in $$source)) {
this["Title"] = Title.$zero;
}
if (!("Name" in $$source)) {
this["Name"] = "";
}
if (!("Age" in $$source)) {
this["Age"] = 0;
}
Object.assign(this, $$source);
}
}
```
The `Title` field is typed as `Title`, not `string`. This gives your IDE full autocompletion and type checking on enum values.
## Enums from Imported Packages
Enums defined in separate packages are fully supported. They are generated into the corresponding package directory:
```go
// services/types.go
package services
type Title string
const (
Mister Title = "Mr"
Miss Title = "Miss"
Ms Title = "Ms"
)
```
```go
// main.go
package main
import "myapp/services"
func (*GreetService) Greet(name string, title services.Title) string {
return "Hello " + string(title) + " " + name
}
```
The `Title` enum is generated in the `services` models file, and import paths are resolved automatically:
```typescript
// bindings/myapp/services/models.ts
export enum Title {
$zero = "",
Mister = "Mr",
Miss = "Miss",
Ms = "Ms",
}
```
## Enum Methods
You can add methods to your enum types in Go. These don't affect binding generation but provide useful server-side functionality:
```go
type Title string
func (t Title) String() string {
return string(t)
}
const (
Mister Title = "Mr"
Miss Title = "Miss"
)
```
The generated enum is identical whether or not Go methods exist on the type.
## Comments and Documentation
The generator preserves Go comments as JSDoc in the generated output:
- **Type comments** become the enum's doc comment
- **Const group comments** become section separators
- **Individual const comments** become member doc comments
- **Inline comments** are preserved where possible
This means your IDE will show documentation for enum values on hover.
## Supported Underlying Types
The binding generator supports enums with the following Go underlying types:
| Go Type | Works as Enum |
|---------|:---:|
| `string` | Yes |
| `int`, `int8`, `int16`, `int32`, `int64` | Yes |
| `uint`, `uint8`, `uint16`, `uint32`, `uint64` | Yes |
| `float32`, `float64` | Yes |
| `byte` (`uint8`) | Yes |
| `rune` (`int32`) | Yes |
| `bool` | Yes |
| `complex64`, `complex128` | No |
## Limitations
The following are **not** supported for enum generation:
- **Generic types** — Type parameters prevent constant detection
- **Types with custom `json.Marshaler` or `encoding.TextMarshaler`** — Custom serialisation means the generated values may not match runtime behaviour, so the generator skips these
- **Constants whose values cannot be statically evaluated or represented** — Constants must have known, representable values in their underlying type. Standard `iota` patterns work fine since the compiler resolves them to concrete values
- **Complex number types** — `complex64` and `complex128` cannot be enum underlying types
## Complete Example
**Go:**
```go
package main
import (
"github.com/wailsapp/wails/v3/pkg/application"
)
// BackgroundType defines the type of background
type BackgroundType string
const (
BackgroundSolid BackgroundType = "solid"
BackgroundGradient BackgroundType = "gradient"
BackgroundImage BackgroundType = "image"
)
type BackgroundConfig struct {
Type BackgroundType `json:"type"`
Value string `json:"value"`
}
type ThemeService struct{}
func (*ThemeService) GetBackground() BackgroundConfig {
return BackgroundConfig{
Type: BackgroundSolid,
Value: "#ffffff",
}
}
func (*ThemeService) SetBackground(config BackgroundConfig) error {
// Apply background
return nil
}
func main() {
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&ThemeService{}),
},
})
app.Window.New()
app.Run()
}
```
**Frontend (TypeScript):**
```typescript
import { GetBackground, SetBackground } from './bindings/myapp/themeservice'
import { BackgroundConfig, BackgroundType } from './bindings/myapp/models'
// Get current background
const bg = await GetBackground()
// Check the type using enum values
if (bg.type === BackgroundType.BackgroundSolid) {
console.log("Solid background:", bg.value)
}
// Set a new background
await SetBackground(new BackgroundConfig({
type: BackgroundType.BackgroundGradient,
value: "linear-gradient(to right, #000, #fff)"
}))
```
**Frontend (JavaScript):**
```javascript
import { GetBackground, SetBackground } from './bindings/myapp/themeservice'
import { BackgroundConfig, BackgroundType } from './bindings/myapp/models'
// Use enum values for type-safe comparisons
const bg = await GetBackground()
switch (bg.type) {
case BackgroundType.BackgroundSolid:
applySolid(bg.value)
break
case BackgroundType.BackgroundGradient:
applyGradient(bg.value)
break
case BackgroundType.BackgroundImage:
applyImage(bg.value)
break
}
```
## Next Steps
<CardGrid>
<Card title="Data Models" icon="document">
Structs, type mapping, and model generation.
[Learn More →](/features/bindings/models)
</Card>
<Card title="Method Binding" icon="rocket">
Bind Go methods to the frontend.
[Learn More →](/features/bindings/methods)
</Card>
<Card title="Advanced Binding" icon="setting">
Directives, code injection, and custom IDs.
[Learn More →](/features/bindings/advanced)
</Card>
<Card title="Best Practices" icon="approve-check">
Binding design patterns.
[Learn More →](/features/bindings/best-practices)
</Card>
</CardGrid>
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [binding examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/binding).

View file

@ -480,6 +480,8 @@ if (user.nickname) {
### Enums
The binding generator automatically detects Go named types with constants and generates TypeScript enums or JavaScript const objects — including a `$zero` member for Go's zero value and full JSDoc preservation.
```go
type UserRole string
@ -488,26 +490,16 @@ const (
RoleUser UserRole = "user"
RoleGuest UserRole = "guest"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Role UserRole `json:"role"`
}
```
**Generated:**
**Generated TypeScript:**
```javascript
export const UserRole = {
Admin: "admin",
User: "user",
Guest: "guest"
}
export class User {
/** @type {string} */
role = UserRole.User
```typescript
export enum UserRole {
$zero = "",
RoleAdmin = "admin",
RoleUser = "user",
RoleGuest = "guest",
}
```
@ -518,10 +510,12 @@ import { User, UserRole } from './bindings/myapp/models'
const admin = new User({
name: "Admin",
role: UserRole.Admin
role: UserRole.RoleAdmin
})
```
For comprehensive coverage of string enums, integer enums, type aliases, imported package enums, and limitations, see the dedicated **[Enums](/features/bindings/enums)** page.
### Validation
```javascript

View file

@ -131,6 +131,11 @@ Remove the badge from the application icon:
dockService.RemoveBadge()
```
### Getting the set badge
```go
dockService.GetBadge()
```
## Platform Considerations
<Tabs>
@ -220,6 +225,7 @@ dockService.RemoveBadge()
| `SetBadge(label string) error` | Sets a badge with the specified label |
| `SetCustomBadge(label string, options BadgeOptions) error` | Sets a badge with the specified label and custom styling options (Windows only) |
| `RemoveBadge() error` | Removes the badge from the application icon |
| `GetBadge() *string` | Gets the current badge |
### Structs and Types

View file

@ -394,7 +394,7 @@ body {
```
**Invisible title bar:**
Allows dragging whilst hiding the title bar:
Allows dragging whilst hiding the title bar. This only takes effect when the window is frameless or uses `AppearsTransparent`:
```go
Mac: application.MacOptions{
InvisibleTitleBarHeight: 40,

View file

@ -288,7 +288,7 @@ func CreateToolPalette(app *application.Application) *application.WebviewWindow
}
```
### Pattern 4: Modal dialogs
### Pattern 4: Modal dialogs (macOS only)
Child windows that block parent:
@ -303,14 +303,7 @@ func ShowModaldialog(parent *application.WebviewWindow, title string) {
Resizable: false,
})
// Disable parent (platform-specific)
parent.SetEnabled(false)
// Re-enable parent on close
dialog.OnDestroy(func() {
parent.SetEnabled(true)
parent.SetFocus()
})
parent.AttachModal(dialog)
}
```
@ -359,51 +352,38 @@ func (e *EditorApp) TogglePreview() {
```go
childWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Child Window",
Parent: parentWindow,
})
parentWindow.AttachModal(childWindow)
```
**Behaviour:**
- Child closes when parent closes
- Child stays above parent (on some platforms)
- Child minimises with parent (on some platforms)
- Child stays above parent
- Child moves with parent
- Child blocks interaction to parent
**Platform support:**
| Feature | macOS | Windows | Linux |
|---------|-------|---------|-------|
| Auto-close | ✅ | ✅ | ⚠️ Varies |
| Stay above | ✅ | ⚠️ Partial | ⚠️ Varies |
| Minimise together | ✅ | ❌ | ⚠️ Varies |
| macOS | Windows | Linux |
|-------|---------|-------|
| ✅ | ❌ | ❌ |
### Modal Behaviour
Create modal-like behaviour:
```go
func ShowModal(parent *application.WebviewWindow) {
modal := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Modal dialog",
func ShowModaldialog(parent *application.WebviewWindow, title string) {
dialog := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: title,
Width: 400,
Height: 200,
Parent: parent,
AlwaysOnTop: true,
})
// Disable parent interaction
parent.SetEnabled(false)
// Re-enable on close
modal.OnClose(func() bool {
parent.SetEnabled(true)
parent.SetFocus()
return true
})
parent.AttachModal(dialog)
}
```
**Note:** True modal behaviour (blocking) varies by platform.
## Window Lifecycle Management
### Creation Callbacks

View file

@ -792,7 +792,8 @@ Mac: application.MacWindow{
**InvisibleTitleBarHeight** (`int`)
- Height of invisible title bar area (for dragging)
- Useful when title bar is hidden
- Only takes effect when the native title bar drag area is hidden — i.e. when the window is frameless (`Frameless: true`) or uses a transparent title bar (`AppearsTransparent: true`)
- Has no effect on standard windows with a visible title bar
**WindowLevel** (`MacWindowLevel`)
- `MacWindowLevelNormal` - Standard window level (default)

View file

@ -597,12 +597,27 @@ if err != nil {
}
```
**Platform support:**
- **macOS**: Full support
- **Windows**: Full support
- **Linux**: Full support
### AttachModal()
**Note:** This triggers the native OS print dialog, allowing the user to select printer settings and print the current window content.
Attaches a second Window as a sheet modal.
```go
func (w *Window) AttachModal(modalWindow Window)
```
**Parameters:**
- `modalWindow` - The window to attach as a modal
**Platform support:**
- **macOS**: Full support (presents as a sheet)
- **Windows**: No support
- **Linux**: No support
**Example:**
```go
modalWindow := app.Window.New()
window.AttachModal(modalWindow)
```
## Platform-Specific Options

View file

@ -92,14 +92,14 @@ tasks:
GOARCH: amd64
test:example:linux:
summary: Build example for Linux with GTK3 (default)
summary: Build example for Linux (pass BUILD_TAGS env var to add -tags, e.g. BUILD_TAGS=gtk4)
dir: 'examples/{{.DIR}}'
platforms:
- linux
cmds:
- echo "Building example {{.DIR}} for Linux (GTK3)"
- echo "Building example {{.DIR}} for Linux (${BUILD_TAGS:-GTK3})"
- go mod tidy
- go build -o "testbuild-{{.DIR}}-linux"
- go build ${BUILD_TAGS:+-tags $BUILD_TAGS} -o "testbuild-{{.DIR}}-linux"
test:example:linux:gtk4:
summary: Build example for Linux with GTK4 (experimental, opt-in via -tags gtk4)
@ -365,12 +365,7 @@ tasks:
vars:
DIR: "{{.ITEM}}"
platforms: [linux]
# GTK4 tests are run separately via Docker tasks since CI doesn't have GTK4 deps
# - for: { var: EXAMPLEDIRS }
# task: test:example:linux:gtk4
# vars:
# DIR: "{{.ITEM}}"
# platforms: [linux]
# GTK4 example builds are handled in CI via: BUILD_TAGS=gtk4 task test:examples
- for: { var: EXAMPLEDIRS }
task: test:example:windows
vars:

View file

@ -17,24 +17,13 @@ After processing, the content will be moved to the main changelog and this file
## Added
<!-- New features, capabilities, or enhancements -->
- Add `-tags` flag to `wails3 build` command for passing custom Go build tags (e.g., `wails3 build -tags gtk4`) (#4957)
- Add support for modal sheets (macOS)
## Changed
<!-- Changes in existing functionality -->
## Fixed
<!-- Bug fixes -->
- Fix file drag-and-drop on Windows not working at non-100% display scaling
- Fix HTML5 internal drag-and-drop being broken when file drop was enabled on Windows
- Fix file drop coordinates being in wrong pixel space on Windows (physical vs CSS pixels)
- Fix file drag-and-drop on Linux not working reliably with hover effects
- Fix HTML5 internal drag-and-drop being broken when file drop was enabled on Linux
- Fix window show/hide on Linux/GTK4 sometimes restoring to minimized state by using `gtk_window_present()` (#4957)
- Fix window position get/set on Linux/GTK4 always returning 0,0 by adding X11-conditional support via `XTranslateCoordinates`/`XMoveWindow` (#4957)
- Fix max window size not being enforced on Linux/GTK4 by adding signal-based size clamping to replace removed `gtk_window_set_geometry_hints` (#4957)
- Fix DPI scaling on Linux/GTK4 by implementing proper PhysicalBounds calculation and fractional scaling support via `gdk_monitor_get_scale` (GTK 4.14+)
- Fix menu items duplicating when creating new windows on Linux/GTK4
- Fix generation of mapped types with enum keys in JS/TS bindings (#4437) by @fbbdev
## Deprecated
<!-- Soon-to-be removed features -->
@ -47,7 +36,6 @@ After processing, the content will be moved to the main changelog and this file
- Restrict GITHUB_TOKEN permissions in workflow files to follow principle of least privilege
- Fix path traversal vulnerability in screen example asset middleware
- Fix command injection vulnerability in setup wizard dependency installation endpoint
- Fix command injection vulnerability in setup wizard dependency installation endpoint
- Update rollup to 3.29.5 to fix XSS vulnerability (CVE-2024-47068)
---

View file

@ -27,6 +27,7 @@ tasks:
dir: frontend
sources:
- "**/*"
- exclude: node_modules/**/*
generates:
- dist/**/*
deps:

View file

@ -27,6 +27,7 @@ tasks:
dir: frontend
sources:
- "**/*"
- exclude: node_modules/**/*
generates:
- dist/**/*
deps:

View file

@ -27,6 +27,7 @@ tasks:
dir: frontend
sources:
- "**/*"
- exclude: node_modules/**/*
generates:
- dist/**/*
deps:

View file

@ -26,6 +26,7 @@ tasks:
dir: frontend
sources:
- "**/*"
- exclude: node_modules/**/*
generates:
- dist/**/*
deps:

View file

@ -27,6 +27,7 @@ tasks:
dir: frontend
sources:
- "**/*"
- exclude: node_modules/**/*
generates:
- dist/**/*
deps:

View file

@ -30,6 +30,7 @@ tasks:
dir: frontend
sources:
- "**/*"
- exclude: node_modules/**/*
generates:
- dist/*
deps:

View file

@ -27,6 +27,7 @@ tasks:
dir: frontend
sources:
- "**/*"
- exclude: node_modules/**/*
generates:
- dist/**/*
deps:

View file

@ -27,6 +27,7 @@ tasks:
dir: frontend
sources:
- "**/*"
- exclude: node_modules/**/*
generates:
- dist/**/*
deps:

View file

@ -6,6 +6,7 @@ import (
"log/slog"
"net/http"
"os"
"path"
"path/filepath"
"runtime"
"strings"
@ -55,22 +56,19 @@ func main() {
return
}
// Clean the requested URL path and make it relative, to prevent directory traversal
cleanPath := filepath.Clean(r.URL.Path)
// Clean the requested URL path using path.Clean (HTTP paths always use forward slashes).
cleanPath := path.Clean("/" + r.URL.Path)
// Normalize to use forward slashes for leading-separator handling.
normalized := strings.ReplaceAll(cleanPath, "\\", "/")
// Strip all leading slashes so the path is always treated as relative.
normalized = strings.TrimLeft(normalized, "/")
// On Windows, also reject drive-letter or UNC-style absolute paths outright.
if strings.HasPrefix(normalized, ":") || strings.HasPrefix(normalized, "\\") {
// Reject Windows drive-letter (e.g. "C:/...") or UNC-style absolute paths.
if len(cleanPath) >= 2 && cleanPath[1] == ':' {
next.ServeHTTP(w, r)
return
}
relativePath := normalized
// Treat the request path as relative by stripping the leading forward slash.
relativePath := strings.TrimPrefix(cleanPath, "/")
// Convert to OS-specific path separators for filesystem operations.
relativePath = filepath.FromSlash(relativePath)
// Resolve the requested path against the absolute assets directory.
resolvedPath, err := filepath.Abs(filepath.Join(assetsDirAbs, relativePath))

View file

@ -6,8 +6,11 @@ require (
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3
github.com/Masterminds/semver v1.5.0
github.com/adrg/xdg v0.5.3
github.com/atotto/clipboard v0.1.4
github.com/atterpac/refresh v0.8.6
github.com/bep/debounce v1.2.1
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/huh v0.8.0
github.com/coder/websocket v1.8.14
@ -22,7 +25,7 @@ require (
github.com/goreleaser/nfpm/v2 v2.44.1
github.com/gorilla/websocket v1.5.3
github.com/jackmordaunt/icns/v2 v2.2.7
github.com/jaypipes/ghw v0.21.2
github.com/jaypipes/ghw v0.21.3
github.com/konoui/lipo v0.10.0
github.com/leaanthony/clir v1.7.0
github.com/leaanthony/go-ansi-parser v1.6.1
@ -52,10 +55,7 @@ require (
require (
al.essio.dev/pkg/shellescape v1.6.0 // indirect
atomicgo.dev/schedule v0.1.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
github.com/charmbracelet/bubbletea v1.3.10 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20260122224438-b01af16209d9 // indirect
@ -93,7 +93,7 @@ require (
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
github.com/cavaliergopher/cpio v1.0.1 // indirect
github.com/chainguard-dev/git-urls v1.0.2 // indirect
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/charmbracelet/x/ansi v0.11.4 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/containerd/console v1.0.5 // indirect

View file

@ -218,8 +218,8 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/jackmordaunt/icns/v2 v2.2.7 h1:K/RbfvuzjmjVY5y4g+XENRs8ZZatwz4YnLHypa2KwQg=
github.com/jackmordaunt/icns/v2 v2.2.7/go.mod h1:ovoTxGguSuoUGKMk5Nn3R7L7BgMQkylsO+bblBuI22A=
github.com/jaypipes/ghw v0.21.2 h1:woW0lqNMPbYk59sur6thOVM8YFP9Hxxr8PM+JtpUrNU=
github.com/jaypipes/ghw v0.21.2/go.mod h1:GPrvwbtPoxYUenr74+nAnWbardIZq600vJDD5HnPsPE=
github.com/jaypipes/ghw v0.21.3 h1:v5mUHM+RN854Vqmk49Uh213jyUA4+8uqaRajlYESsh8=
github.com/jaypipes/ghw v0.21.3/go.mod h1:GPrvwbtPoxYUenr74+nAnWbardIZq600vJDD5HnPsPE=
github.com/jaypipes/pcidb v1.1.1 h1:QmPhpsbmmnCwZmHeYAATxEaoRuiMAJusKYkUncMC0ro=
github.com/jaypipes/pcidb v1.1.1/go.mod h1:x27LT2krrUgjf875KxQXKB0Ha/YXLdZRVmw6hH0G7g8=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=

View file

@ -27,6 +27,7 @@ tasks:
dir: frontend
sources:
- "**/*"
- exclude: node_modules/**/*
generates:
- dist/**/*
deps:

View file

@ -1 +1 @@
v3.0.0-alpha.68
v3.0.0-alpha.73

View file

@ -29,22 +29,6 @@ typedef struct WindowEvent {
uint event;
} WindowEvent;
typedef struct WailsScreen {
const char* id;
const char* name;
int p_width;
int p_height;
int x;
int y;
int w_width;
int w_height;
int w_x;
int w_y;
float scaleFactor;
double rotation;
bool isPrimary;
} WailsScreen;
typedef struct MenuItemData {
guint id;
GSimpleAction *action;

View file

@ -113,6 +113,7 @@ type (
setMenu(menu *Menu)
snapAssist()
setContentProtection(enabled bool)
attachModal(modalWindow *WebviewWindow)
}
)
@ -1284,6 +1285,22 @@ func (w *WebviewWindow) NativeWindow() unsafe.Pointer {
return w.impl.nativeWindow()
}
// AttachModal attaches a modal window to this window, presenting it as a sheet on macOS.
func (w *WebviewWindow) AttachModal(modalWindow Window) {
if w.impl == nil || w.isDestroyed() {
return
}
modalWebviewWindow, ok := modalWindow.(*WebviewWindow)
if !ok || modalWebviewWindow == nil {
return
}
InvokeSync(func() {
w.impl.attachModal(modalWebviewWindow)
})
}
// shouldUnconditionallyClose returns whether the window should close unconditionally
func (w *WebviewWindow) shouldUnconditionallyClose() bool {
return atomic.LoadUint32(&w.unconditionallyClose) != 0

View file

@ -296,6 +296,10 @@ func (w *androidWebviewWindow) nativeWindow() unsafe.Pointer {
return nil
}
func (w *androidWebviewWindow) attachModal(modalWindow *WebviewWindow) {
// Modal windows are not supported on Android
}
func (w *androidWebviewWindow) on(eventID uint) {
// Android event handling
}

View file

@ -27,7 +27,7 @@ extern void registerListener(unsigned int event);
void* windowNew(unsigned int id, int width, int height, bool fraudulentWebsiteWarningEnabled, bool frameless, bool enableDragAndDrop, struct WebviewPreferences preferences) {
NSWindowStyleMask styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
if (frameless) {
styleMask = NSWindowStyleMaskBorderless | NSWindowStyleMaskResizable;
styleMask = NSWindowStyleMaskBorderless | NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable;
}
WebviewWindow* window = [[WebviewWindow alloc] initWithContentRect:NSMakeRect(0, 0, width-1, height-1)
styleMask:styleMask
@ -331,6 +331,24 @@ void windowZoomOut(void* nsWindow) {
}
}
// createModalWindow presents a modal window as a sheet attached to the parent window
void createModalWindow(void* parentWindowPtr, void* modalWindowPtr) {
if (parentWindowPtr == NULL || modalWindowPtr == NULL) {
return;
}
NSWindow* parentWindow = (NSWindow*)parentWindowPtr;
NSWindow* modalWindow = (NSWindow*)modalWindowPtr;
// Present the modal window as a sheet attached to the parent window
// Must be dispatched to the main thread for UI thread safety
dispatch_async(dispatch_get_main_queue(), ^{
[parentWindow beginSheet:modalWindow completionHandler:^(NSModalResponse returnCode) {
// Sheet was dismissed - window will be released automatically
}];
});
}
// set the window position relative to the screen
void windowSetRelativePosition(void* nsWindow, int x, int y) {
WebviewWindow* window = (WebviewWindow*)nsWindow;
@ -1334,7 +1352,10 @@ func (w *macosWebviewWindow) run() {
C.windowSetAppearanceTypeByName(w.nsWindow, C.CString(string(macOptions.Appearance)))
}
if macOptions.InvisibleTitleBarHeight != 0 {
// Only apply invisible title bar when the native drag area is hidden
// (frameless window or transparent/hidden title bar presets like HiddenInset)
if macOptions.InvisibleTitleBarHeight != 0 &&
(w.parent.options.Frameless || titleBarOptions.AppearsTransparent) {
C.windowSetInvisibleTitleBar(w.nsWindow, C.uint(macOptions.InvisibleTitleBarHeight))
}
@ -1548,6 +1569,17 @@ func (w *macosWebviewWindow) setContentProtection(enabled bool) {
C.setContentProtection(w.nsWindow, C.bool(enabled))
}
func (w *macosWebviewWindow) attachModal(modalWindow *WebviewWindow) {
if modalWindow == nil || modalWindow.impl == nil || modalWindow.isDestroyed() {
return
}
modalNativeWindow := modalWindow.impl.nativeWindow()
if modalNativeWindow == nil {
return
}
C.createModalWindow(w.nsWindow, modalNativeWindow)
}
func (w *macosWebviewWindow) cut() {
}

View file

@ -345,6 +345,15 @@ typedef NS_ENUM(NSInteger, MacLiquidGlassStyle) {
NSPoint location = [event locationInWindow];
NSRect frame = [window frame];
if( location.y > frame.size.height - self.invisibleTitleBarHeight ) {
// Skip drag if the click is near a window edge (resize zone).
// This prevents conflict between dragging and native top-corner resizing,
// which causes window content to shake/jitter (#4960).
CGFloat resizeThreshold = 5.0;
BOOL nearLeftEdge = location.x < resizeThreshold;
BOOL nearRightEdge = location.x > frame.size.width - resizeThreshold;
if( nearLeftEdge || nearRightEdge ) {
return;
}
[window performWindowDragWithEvent:event];
return;
}

View file

@ -330,6 +330,10 @@ func (w *iosWebviewWindow) nativeWindow() unsafe.Pointer {
return w.nativeHandle
}
func (w *iosWebviewWindow) attachModal(modalWindow *WebviewWindow) {
// Modal windows are not supported on iOS
}
func (w *iosWebviewWindow) on(eventID uint) {
// iOS event handling
}

View file

@ -410,6 +410,10 @@ func (w *linuxWebviewWindow) nativeWindow() unsafe.Pointer {
return unsafe.Pointer(w.window)
}
func (w *linuxWebviewWindow) attachModal(modalWindow *WebviewWindow) {
// Modal windows are not supported on Linux
}
func (w *linuxWebviewWindow) print() error {
w.execJS("window.print();")
return nil

View file

@ -78,6 +78,9 @@ type windowsWebviewWindow struct {
// menubarTheme is the theme for the menubar
menubarTheme *w32.MenuBarTheme
// Modal window tracking
parentHWND w32.HWND // Parent window HWND when this window is a modal
}
func (w *windowsWebviewWindow) setMenu(menu *Menu) {
@ -233,6 +236,37 @@ func (w *windowsWebviewWindow) startDrag() error {
return nil
}
func (w *windowsWebviewWindow) attachModal(modalWindow *WebviewWindow) {
if modalWindow == nil || modalWindow.impl == nil || modalWindow.isDestroyed() {
return
}
// Get the modal window's Windows implementation
modalWindowsImpl, ok := modalWindow.impl.(*windowsWebviewWindow)
if !ok {
return
}
parentHWND := w.hwnd
modalHWND := modalWindowsImpl.hwnd
// Set parent-child relationship using GWLP_HWNDPARENT
// This ensures the modal stays above parent and moves with it
w32.SetWindowLongPtr(modalHWND, w32.GWLP_HWNDPARENT, uintptr(parentHWND))
// Track the parent HWND in the modal window for cleanup
modalWindowsImpl.parentHWND = parentHWND
// Disable the parent window to block interaction (Microsoft's recommended approach)
// This follows Windows modal dialog best practices
w32.EnableWindow(parentHWND, false)
// Ensure modal window is shown and brought to front
w32.ShowWindow(modalHWND, w32.SW_SHOW)
w32.SetForegroundWindow(modalHWND)
w32.BringWindowToTop(modalHWND)
}
func (w *windowsWebviewWindow) nativeWindow() unsafe.Pointer {
return unsafe.Pointer(w.hwnd)
}
@ -725,6 +759,12 @@ func (w *windowsWebviewWindow) setRelativePosition(x int, y int) {
}
func (w *windowsWebviewWindow) destroy() {
// Re-enable parent window if this was a modal window
if w.parentHWND != 0 {
w32.EnableWindow(w.parentHWND, true)
w.parentHWND = 0
}
w.parent.markAsDestroyed()
// destroy the window
w32.DestroyWindow(w.hwnd)
@ -1420,6 +1460,12 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp
}
defer func() {
// Re-enable parent window if this was a modal window
if w.parentHWND != 0 {
w32.EnableWindow(w.parentHWND, true)
w.parentHWND = 0
}
windowsApp := globalApplication.impl.(*windowsApp)
windowsApp.unregisterWindow(w)

View file

@ -91,6 +91,7 @@ type Window interface {
Flash(enabled bool)
Print() error
RegisterHook(eventType events.WindowEventType, callback func(event *WindowEvent)) func()
AttachModal(modalWindow Window)
shouldUnconditionallyClose() bool
// Editing methods

View file

@ -8,7 +8,8 @@ import (
"github.com/wailsapp/wails/v3/pkg/application"
)
type iosDock struct{}
type iosDock struct {
}
// New creates a new Dock Service.
// On iOS, this returns a stub implementation.
@ -61,4 +62,10 @@ func (d *iosDock) SetCustomBadge(label string, options BadgeOptions) error {
func (d *iosDock) RemoveBadge() error {
// iOS badge removal would go here via native bridge
return nil
}
}
// GetBadge retrieves the badge from the iOS app icon.
func (d *iosDock) GetBadge() *string {
// iOS badge retrieval would go here via native bridge
return nil
}

View file

@ -20,6 +20,7 @@ type platformDock interface {
SetBadge(label string) error
SetCustomBadge(label string, options BadgeOptions) error
RemoveBadge() error
GetBadge() *string
}
// Service represents the dock service
@ -75,3 +76,8 @@ func (d *DockService) SetCustomBadge(label string, options BadgeOptions) error {
func (d *DockService) RemoveBadge() error {
return d.impl.RemoveBadge()
}
// GetBadge returns the badge label on the application icon.
func (d *DockService) GetBadge() *string {
return d.impl.GetBadge()
}

View file

@ -8,42 +8,59 @@ package dock
#import <Cocoa/Cocoa.h>
void hideDockIcon() {
dispatch_async(dispatch_get_main_queue(), ^{
dispatch_sync(dispatch_get_main_queue(), ^{
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
});
}
void showDockIcon() {
dispatch_async(dispatch_get_main_queue(), ^{
dispatch_sync(dispatch_get_main_queue(), ^{
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
});
}
static void setBadge(const char *label) {
dispatch_async(dispatch_get_main_queue(), ^{
bool setBadge(const char *label) {
__block bool success = false;
dispatch_sync(dispatch_get_main_queue(), ^{
// Ensure the app is in Regular activation policy (dock icon visible)
NSApplicationActivationPolicy currentPolicy = [NSApp activationPolicy];
if (currentPolicy != NSApplicationActivationPolicyRegular) {
success = false;
return;
}
NSString *nsLabel = nil;
if (label != NULL) {
nsLabel = [NSString stringWithUTF8String:label];
}
[[NSApp dockTile] setBadgeLabel:nsLabel];
[[NSApp dockTile] display];
success = true;
});
return success;
}
*/
import "C"
import (
"context"
"fmt"
"sync"
"unsafe"
"github.com/wailsapp/wails/v3/pkg/application"
)
type darwinDock struct{}
type darwinDock struct {
mu sync.RWMutex
Badge *string
}
// Creates a new Dock Service.
func New() *DockService {
return &DockService{
impl: &darwinDock{},
impl: &darwinDock{
Badge: nil,
},
}
}
@ -67,22 +84,42 @@ func (d *darwinDock) HideAppIcon() {
}
// ShowAppIcon shows the app icon in the macOS Dock.
// Note: After showing the dock icon, you may need to call SetBadge again
// to reapply any previously set badge, as changing activation policies clears the badge.
func (d *darwinDock) ShowAppIcon() {
C.showDockIcon()
}
// SetBadge sets the badge label on the application icon.
func (d *darwinDock) SetBadge(label string) error {
// Always pick a label (use “●” if empty), then allocate + free exactly once.
value := label
if value == "" {
value = "●" // Default badge character
}
cLabel := C.CString(value)
defer C.free(unsafe.Pointer(cLabel))
// setBadge handles the C call and updates the internal badge state with locking.
func (d *darwinDock) setBadge(label *string) error {
var cLabel *C.char
if label != nil {
cLabel = C.CString(*label)
defer C.free(unsafe.Pointer(cLabel))
}
C.setBadge(cLabel)
return nil
success := C.setBadge(cLabel)
if !success {
return fmt.Errorf("failed to set badge")
}
d.mu.Lock()
d.Badge = label
d.mu.Unlock()
return nil
}
// SetBadge sets the badge label on the application icon.
// Available default badge labels:
// Single space " " empty badge
// Empty string "" dot "●" indeterminate badge
func (d *darwinDock) SetBadge(label string) error {
// Always pick a label (use "●" if empty), then allocate + free exactly once.
if label == "" {
label = "●" // Default badge character
}
return d.setBadge(&label)
}
// SetCustomBadge is not supported on macOS, SetBadge is called instead.
@ -92,6 +129,12 @@ func (d *darwinDock) SetCustomBadge(label string, options BadgeOptions) error {
// RemoveBadge removes the badge label from the application icon.
func (d *darwinDock) RemoveBadge() error {
C.setBadge(nil)
return nil
return d.setBadge(nil)
}
// GetBadge returns the badge label on the application icon.
func (d *darwinDock) GetBadge() *string {
d.mu.RLock()
defer d.mu.RUnlock()
return d.Badge
}

View file

@ -68,3 +68,8 @@ func (l *linuxDock) RemoveBadge() error {
// No-op: Linux doesn't have standardized badge support
return nil
}
func (l *linuxDock) GetBadge() *string {
// No-op: Linux doesn't have standardized badge support
return nil
}

View file

@ -24,6 +24,7 @@ type windowsDock struct {
badgeSize int
fontManager *FontManager
badgeOptions BadgeOptions
badge *string
}
var defaultOptions = BadgeOptions{
@ -48,6 +49,7 @@ func NewWithOptions(options BadgeOptions) *DockService {
return &DockService{
impl: &windowsDock{
badgeOptions: options,
badge: nil,
},
}
}
@ -121,6 +123,7 @@ func (w *windowsDock) SetBadge(label string) error {
}
defer w32.DestroyIcon(hicon)
w.badge = &label
return w.taskbar.SetOverlayIcon(hwnd, hicon, nil)
})
}
@ -182,6 +185,7 @@ func (w *windowsDock) SetCustomBadge(label string, options BadgeOptions) error {
}
defer w32.DestroyIcon(hicon)
w.badge = &label
return w.taskbar.SetOverlayIcon(hwnd, hicon, nil)
})
}
@ -209,6 +213,7 @@ func (w *windowsDock) RemoveBadge() error {
}
hwnd := uintptr(nativeWindow)
w.badge = nil
return w.taskbar.SetOverlayIcon(hwnd, 0, nil)
})
}
@ -394,3 +399,7 @@ func (w *windowsDock) createBadge() {
w.badgeImg = img
}
func (w *windowsDock) GetBadge() *string {
return w.badge
}