Align Wails stub bridge with RFC parity
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

This commit is contained in:
Snider 2026-04-15 11:13:55 +01:00
parent ae02c8574b
commit cefcdfbb19
6 changed files with 196 additions and 20 deletions

View file

@ -0,0 +1,8 @@
package operatingsystem
// OS is the minimal host metadata exposed by the Wails environment API.
type OS struct {
Name string
Version string
Build string
}

View file

@ -1,6 +1,7 @@
package application
import (
"log/slog"
"sync"
"unsafe"
@ -216,9 +217,13 @@ func (c *WindowEventContext) DropTargetDetails() *DropTargetDetails {
return &details
}
// DropTargetDetails mirrors the fields consumed by the GUI wrappers.
// DropTargetDetails mirrors the Wails drop-target payload.
type DropTargetDetails struct {
ElementID string
X int `json:"x"`
Y int `json:"y"`
ElementID string `json:"id"`
ClassList []string `json:"classList"`
Attributes map[string]string `json:"attributes,omitempty"`
}
// WindowEvent mirrors the event object passed to window callbacks.
@ -873,18 +878,19 @@ func (wm *WindowManager) RemoveByName(name string) bool {
// app := &application.App{}
// win := app.Window.NewWithOptions(application.WebviewWindowOptions{Title: "Main"})
type App struct {
Logger Logger
Window WindowManager
Menu MenuManager
SystemTray SystemTrayManager
Dialog DialogManager
Event EventManager
Browser BrowserManager
Clipboard ClipboardManager
ContextMenu ContextMenuManager
Environment EnvironmentManager
Screen ScreenManager
KeyBinding KeyBindingManager
Logger *slog.Logger
Window *WindowManager
Menu *MenuManager
SystemTray *SystemTrayManager
Dialog *DialogManager
Event *EventManager
Browser *BrowserManager
Clipboard *ClipboardManager
ContextMenu *ContextMenuManager
Env *EnvironmentManager
Environment *EnvironmentManager
Screen *ScreenManager
KeyBinding *KeyBindingManager
}
// NewApp creates a zero-config in-memory application stub.

View file

@ -1,6 +1,8 @@
package application
import (
"context"
"log/slog"
"net/http"
"time"
)
@ -43,6 +45,12 @@ type Options struct {
// Example: map[uint32]uint32{1: 1411160069}
BindAliases map[uint32]uint32
// Logger is the Wails system logger used by the runtime.
Logger *slog.Logger
// LogLevel defines the desired system log level when Logger is nil.
LogLevel slog.Level
// Assets configures the embedded asset server.
Assets AssetOptions
@ -85,6 +93,9 @@ type Options struct {
// SingleInstance configures single-instance enforcement.
SingleInstance *SingleInstanceOptions
// Transport configures the IPC transport implementation.
Transport Transport
// Server configures the headless HTTP server (enabled via the "server" build tag).
Server ServerOptions
}
@ -141,6 +152,27 @@ type AssetOptions struct {
// type Middleware func(next http.Handler) http.Handler
type Middleware func(next http.Handler) http.Handler
// MessageProcessor is the placeholder type passed to Transport.Start.
// The stub does not perform runtime IPC processing, but the type exists so
// callers can satisfy the same API surface as real Wails.
type MessageProcessor struct{}
// Transport is the custom IPC transport contract supported by Wails.
//
// type MyTransport struct{}
// func (t *MyTransport) Start(ctx context.Context, processor *application.MessageProcessor) error { return nil }
// func (t *MyTransport) Stop() error { return nil }
type Transport interface {
Start(ctx context.Context, processor *MessageProcessor) error
Stop() error
}
// AssetServerTransport optionally allows a custom transport to serve app assets.
type AssetServerTransport interface {
Transport
ServeAssets(assetHandler http.Handler) error
}
// ChainMiddleware composes multiple Middleware values into a single Middleware.
// chain := application.ChainMiddleware(authMiddleware, loggingMiddleware)
func ChainMiddleware(middleware ...Middleware) Middleware {

View file

@ -2,6 +2,8 @@ package application
import (
"context"
"io"
"log/slog"
"sync"
"time"
)
@ -53,19 +55,18 @@ func ensureAppCompatState(app *App) *appCompatState {
if app == nil {
return &appCompatState{ctx: context.Background()}
}
initialiseAppManagers(app, Options{})
if state, ok := appCompatStates.Load(app); ok {
return state.(*appCompatState)
}
state := &appCompatState{ctx: context.Background()}
actual, _ := appCompatStates.LoadOrStore(app, state)
if app.KeyBinding.bindings == nil {
app.KeyBinding = *NewKeyBindingManager()
}
return actual.(*appCompatState)
}
func newStubApp(options Options) *App {
app := &App{}
initialiseAppManagers(app, options)
state := ensureAppCompatState(app)
state.mu.Lock()
state.options = options
@ -77,6 +78,59 @@ func newStubApp(options Options) *App {
return app
}
func initialiseAppManagers(app *App, options Options) {
if app == nil {
return
}
if app.Logger == nil {
if options.Logger != nil {
app.Logger = options.Logger
} else {
handlerOptions := &slog.HandlerOptions{Level: options.LogLevel}
app.Logger = slog.New(slog.NewTextHandler(io.Discard, handlerOptions))
}
}
if app.Window == nil {
app.Window = &WindowManager{}
}
if app.Menu == nil {
app.Menu = &MenuManager{}
}
if app.SystemTray == nil {
app.SystemTray = &SystemTrayManager{}
}
if app.Dialog == nil {
app.Dialog = &DialogManager{}
}
if app.Event == nil {
app.Event = &EventManager{}
}
if app.Browser == nil {
app.Browser = &BrowserManager{}
}
if app.Clipboard == nil {
app.Clipboard = &ClipboardManager{}
}
if app.ContextMenu == nil {
app.ContextMenu = &ContextMenuManager{}
}
if app.Env == nil && app.Environment != nil {
app.Env = app.Environment
}
if app.Env == nil {
app.Env = &EnvironmentManager{}
}
if app.Environment == nil {
app.Environment = app.Env
}
if app.Screen == nil {
app.Screen = NewScreenManager()
}
if app.KeyBinding == nil {
app.KeyBinding = NewKeyBindingManager()
}
}
func ensureWebviewCompatState(window *WebviewWindow) *webviewCompatState {
if state, ok := webviewCompatStates.Load(window); ok {
return state.(*webviewCompatState)
@ -107,7 +161,7 @@ func ensureTrayCompatState(tray *SystemTray) *trayCompatState {
func appScreens() *ScreenManager {
if globalApplication != nil {
return &globalApplication.Screen
return globalApplication.Screen
}
return defaultScreenManager
}

View file

@ -1,5 +1,11 @@
package application
import (
"runtime"
"github.com/wailsapp/wails/v3/internal/operatingsystem"
)
// EnvironmentInfo holds runtime information about the host OS and build.
//
// info := manager.Info()
@ -8,6 +14,7 @@ type EnvironmentInfo struct {
OS string
Arch string
Debug bool
OSInfo *operatingsystem.OS
PlatformInfo map[string]any
}
@ -42,6 +49,9 @@ func (em *EnvironmentManager) GetAccentColor() string {
// info := manager.Info()
func (em *EnvironmentManager) Info() EnvironmentInfo {
return EnvironmentInfo{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
OSInfo: &operatingsystem.OS{Name: runtime.GOOS, Version: "stub"},
PlatformInfo: make(map[string]any),
}
}

View file

@ -1,16 +1,50 @@
package application
import "testing"
import (
"context"
"log/slog"
"testing"
)
type noopTransport struct{}
func (noopTransport) Start(context.Context, *MessageProcessor) error { return nil }
func (noopTransport) Stop() error { return nil }
func TestNewAndGetExposeSingletonState(t *testing.T) {
globalApplication = nil
app := New(Options{Name: "Parity"})
logger := slog.Default()
transport := noopTransport{}
app := New(Options{
Name: "Parity",
Logger: logger,
LogLevel: slog.LevelDebug,
Transport: transport,
})
if Get() != app {
t.Fatalf("Get() did not return singleton app")
}
if app.Config().Name != "Parity" {
t.Fatalf("unexpected app name: %q", app.Config().Name)
}
if app.Config().Logger != logger {
t.Fatalf("expected logger to round-trip through config")
}
if app.Config().LogLevel != slog.LevelDebug {
t.Fatalf("expected log level to round-trip through config")
}
if app.Config().Transport == nil {
t.Fatalf("expected transport to round-trip through config")
}
if app.Logger == nil {
t.Fatalf("expected app logger to be initialised")
}
if app.Env == nil {
t.Fatalf("expected Env manager to be initialised")
}
if app.Environment != app.Env {
t.Fatalf("expected Environment alias to point at Env manager")
}
if err := app.Run(); err != nil {
t.Fatalf("Run() failed: %v", err)
}
@ -61,3 +95,35 @@ func TestMenuAndWindowParityHelpers(t *testing.T) {
t.Fatalf("expected devtools to be marked closed")
}
}
func TestEnvironmentAndDropTargetParity(t *testing.T) {
info := (&EnvironmentManager{}).Info()
if info.OS == "" || info.Arch == "" {
t.Fatalf("expected runtime environment metadata, got %+v", info)
}
if info.OSInfo == nil {
t.Fatalf("expected OSInfo to be populated")
}
ctx := &WindowEventContext{
dropDetails: &DropTargetDetails{
X: 10,
Y: 20,
ElementID: "dropzone",
ClassList: []string{"primary", "drop-target"},
Attributes: map[string]string{
"data-file-drop-target": "true",
},
},
}
details := ctx.DropTargetDetails()
if details == nil {
t.Fatalf("expected drop target details")
}
if details.X != 10 || details.Y != 20 || details.ElementID != "dropzone" {
t.Fatalf("unexpected drop details: %+v", details)
}
if len(details.ClassList) != 2 || details.Attributes["data-file-drop-target"] != "true" {
t.Fatalf("drop target metadata was not preserved: %+v", details)
}
}