wails/v3/pkg/application/menu_windows.go
Lea Anthony 9a363d7be5
feat(v3): add server mode for headless HTTP deployment (#4903)
* feat(v3): add server mode for headless HTTP deployment

Server mode allows Wails applications to run as pure HTTP servers
without native GUI dependencies. Enable with `-tags server` build tag.

Features:
- HTTP server with configurable host/port via ServerOptions
- WAILS_SERVER_HOST and WAILS_SERVER_PORT env var overrides
- WebSocket event broadcasting to connected browsers
- Browser clients represented as BrowserWindow (Window interface)
- Health check endpoint at /health
- Graceful shutdown with configurable timeout
- Docker support with Dockerfile.server template and tasks

Build and run:
  wails3 task build:server
  wails3 task run:server
  wails3 task build:docker
  wails3 task run:docker

Documentation at docs/guides/server-build.mdx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(v3): add server mode for headless HTTP deployment

Server mode allows Wails applications to run as pure HTTP servers
without native GUI dependencies. Enable with `-tags server` build tag.

Features:
- HTTP server with configurable host/port via ServerOptions
- WAILS_SERVER_HOST and WAILS_SERVER_PORT env var overrides
- WebSocket event broadcasting to connected browsers
- Browser clients represented as BrowserWindow (Window interface)
- Health check endpoint at /health
- Graceful shutdown with configurable timeout
- Docker support with Dockerfile.server template and tasks

Build and run:
  wails3 task build:server
  wails3 task run:server
  wails3 task build:docker
  wails3 task run:docker

Documentation at docs/guides/server-build.mdx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address CodeRabbit review comments

- Fix corrupted test file with embedded terminal output
- Fix module name mismatch in gin-routing (was gin-example)
- Fix replace directive version mismatch in gin-service
- Fix placeholder module name in ios example (was changeme)
- Fix Dockerfile COPY path to work from both build contexts
- Fix bare URL in README (MD034 compliance)
- Fix comment accuracy in getScreens (returns error, not empty slice)
- Remove deprecated docker-compose version field
- Add port documentation in Taskfile template

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address CodeRabbit review comments

- Add note about healthcheck wget not being available in distroless images
- Add !server build constraint to menu_windows.go and menu_darwin.go
- Downgrade window-visibility-test go.mod from 1.25 to 1.24 to match CI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 14:33:44 +11:00

142 lines
3 KiB
Go

//go:build windows && !server
package application
import (
"github.com/wailsapp/wails/v3/pkg/w32"
)
type windowsMenu struct {
menu *Menu
parentWindow *windowsWebviewWindow
hWnd w32.HWND
hMenu w32.HMENU
currentMenuID int
menuMapping map[int]*MenuItem
checkboxItems []*Menu
}
func newMenuImpl(menu *Menu) *windowsMenu {
result := &windowsMenu{
menu: menu,
menuMapping: make(map[int]*MenuItem),
}
return result
}
func (w *windowsMenu) update() {
if w.hMenu != 0 {
w32.DestroyMenu(w.hMenu)
}
w.hMenu = w32.NewPopupMenu()
w.processMenu(w.hMenu, w.menu)
}
func (w *windowsMenu) processMenu(parentMenu w32.HMENU, inputMenu *Menu) {
for _, item := range inputMenu.items {
w.currentMenuID++
itemID := w.currentMenuID
w.menuMapping[itemID] = item
menuItemImpl := newMenuItemImpl(item, parentMenu, itemID)
menuItemImpl.parent = inputMenu
item.impl = menuItemImpl
if item.Hidden() {
if item.accelerator != nil && item.callback != nil {
if w.parentWindow != nil {
w.parentWindow.parent.removeMenuBinding(item.accelerator)
} else {
globalApplication.KeyBinding.Remove(item.accelerator.String())
}
}
}
flags := uint32(w32.MF_STRING)
if item.disabled {
flags = flags | w32.MF_GRAYED
}
if item.checked {
flags = flags | w32.MF_CHECKED
}
if item.IsSeparator() {
flags = flags | w32.MF_SEPARATOR
}
if item.itemType == radio {
flags = flags | w32.MFT_RADIOCHECK
}
if item.submenu != nil {
flags = flags | w32.MF_POPUP
newSubmenu := w32.CreateMenu()
w.processMenu(newSubmenu, item.submenu)
itemID = int(newSubmenu)
}
thisText := item.Label()
if item.accelerator != nil && item.callback != nil {
if w.parentWindow != nil {
w.parentWindow.parent.addMenuBinding(item.accelerator, item)
} else {
globalApplication.KeyBinding.Add(item.accelerator.String(), func(w Window) {
item.handleClick()
})
}
thisText = thisText + "\t" + item.accelerator.String()
}
var menuText = w32.MustStringToUTF16Ptr(thisText)
// If the item is hidden, don't append
if item.Hidden() {
continue
}
w32.AppendMenu(parentMenu, flags, uintptr(itemID), menuText)
if item.bitmap != nil {
if err := w32.SetMenuIcons(parentMenu, itemID, item.bitmap, nil); err != nil {
globalApplication.fatal("error setting menu icons: %w", err)
}
}
}
}
func (w *windowsMenu) ShowAtCursor() {
InvokeSync(func() {
x, y, ok := w32.GetCursorPos()
if !ok {
return
}
w.ShowAt(x, y)
})
}
func (w *windowsMenu) ShowAt(x int, y int) {
w.update()
w32.TrackPopupMenuEx(w.hMenu,
w32.TPM_LEFTALIGN,
int32(x),
int32(y),
w.hWnd,
nil)
w32.PostMessage(w.hWnd, w32.WM_NULL, 0, 0)
}
func (w *windowsMenu) ProcessCommand(cmdMsgID int) {
item := w.menuMapping[cmdMsgID]
if item == nil {
return
}
item.handleClick()
}
func DefaultApplicationMenu() *Menu {
menu := NewMenu()
menu.AddRole(FileMenu)
menu.AddRole(EditMenu)
menu.AddRole(ViewMenu)
menu.AddRole(WindowMenu)
menu.AddRole(HelpMenu)
return menu
}