Files
yellowjacket/main.go
T
yonluandClaude Opus 5 4471db3aef feat(wails): move the Go side to v3
Phases 2 and 3 of plan 009, plus the parts of phase 1 that could not
land before them. Nothing in the tree imports wails/v2 any more; all
three lint and test configurations are green and `go build .` produces
a running binary.

The point of the migration is one file. backend/events/emit.go probed
ctx.Value("events") — a v2-*private* context key — to decide whether
emitting was safe, because runtime.EventsEmit called log.Fatalf on a
context without the runtime and took the process down with it. v3's
emit takes no context, so that is now application.Get() == nil. D1
held: events.Emit keeps its ctx as the WithSink test seam, and all 45
call sites and 7 test files are untouched.

The bootstrap splits into application.New + Window.NewWithOptions +
Run. Ten bound services implement ServiceStartup instead of being
handed a context by hand from OnStartup, which also stops ten
SetContext methods being exported as bindings. jobs.Registry and
explore.SearchIndex keep theirs — neither is bound, so converting them
would be churn for no binding removed.

Four things differed from the plan and are written up in it: GPU policy
moved to the per-window LinuxWindow options rather than surviving on
LinuxOptions; there is no OnStartup/OnDomReady option, so app-level
wiring hangs off ApplicationStarted; application.NewService is generic,
so FEBindings []any could not survive (the binding generator is a
static analyser and would have seen nothing); and the quit veto had to
be restructured, because v3's dialog answers on a callback rather than
returning the button, so ShouldQuit vetoes, asks, and quits again from
the callback.

Window state saving moves to a WindowClosing hook — the size has to be
read while the window still exists, and v3's OnShutdown has neither
context nor window. backend/logging is deleted rather than ported:
v3 takes a *slog.Logger directly, so the v2 logger.Logger adapter had
no caller left.

Phase 1's tail rides along, now that it can: the Makefile's wails
invocations, all 50 webkit2_41 sites, lefthook, both packaging recipes
and ci.yml's apt lists. v3 builds against GTK4 + WebKitGTK 6.0, which
Arch and ubuntu:24.04 both ship, so the tag is a deletion rather than
a translation.

Phase 4 is next and the branch is not usable until it lands: the app
builds, but frontend/wailsjs/ is v2's tree and nothing regenerates it,
so the frontend cannot reach the backend yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm
2026-08-14 14:01:02 -04:00

173 lines
4.7 KiB
Go

// Package main is the entry point for the Yellowjacket application.
package main
import (
"embed"
"log/slog"
"os"
"strings"
"github.com/golang-cz/devslog"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
"yellowjacket/backend"
"yellowjacket/backend/assets"
"yellowjacket/backend/config"
"yellowjacket/backend/profiling"
"yellowjacket/internal/dev"
)
// version and commit are set at build time via ldflags.
var (
version = "dev"
commit = "unknown"
)
//go:embed all:frontend/dist
var frontendDistAssets embed.FS
func main() {
// WebKitGTK's DMABuf renderer crashes on NVIDIA GPUs under Wayland.
// Only disable it for that specific combo so AMD/Intel and X11 users
// keep full hardware-accelerated buffer sharing. Users can also
// force the workaround with WEBKIT_DISABLE_DMABUF_RENDERER=1.
if os.Getenv("WEBKIT_DISABLE_DMABUF_RENDERER") == "" && isNVIDIAWayland() {
_ = os.Setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1")
}
isDev := dev.IsDev
// create sLogger
loglevel := resolveLogLevel(isDev)
sLogger := slog.New(devslog.NewHandler(os.Stdout, &devslog.Options{
HandlerOptions: &slog.HandlerOptions{
Level: loglevel,
},
}))
slog.SetDefault(sLogger)
sLogger.Info("starting yellowjacket", "version", version, "commit", commit)
// Start profiling server (pprof + trace). In production builds this
// is a no-op — the compiler eliminates all profiling code.
stopProfiler := profiling.Start(sLogger)
// create asset handler
assetHandler, err := assets.NewAssetHandler(sLogger, frontendDistAssets)
if err != nil {
sLogger.Error("could not create asset handler", "err", err.Error())
stopProfiler()
os.Exit(1)
}
yjApp, err := backend.NewYellowJacketApp(sLogger, assetHandler)
if err != nil {
sLogger.Error("problem initializing yellowjacket", "err", err.Error())
stopProfiler()
os.Exit(1)
}
winCfg := yjApp.WindowConfig()
app := application.New(application.Options{
Name: "yellowjacket",
Description: "A cross-platform desktop music player",
Services: yjApp.Services,
Assets: assetHandler.Options,
// v3 logs through slog directly, so v2's logger.Logger adapter
// (backend/logging) is gone rather than ported.
Logger: sLogger,
ShouldQuit: yjApp.ShouldQuit,
OnShutdown: yjApp.OnShutdown,
})
// The services' own ServiceStartup hooks have run by the time this
// fires; what is left is the wiring between them.
app.Event.OnApplicationEvent(
events.Common.ApplicationStarted,
func(*application.ApplicationEvent) {
yjApp.OnStartup(app.Context())
yjApp.OnDomReady(app.Context())
},
)
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "yellowjacket",
Width: winCfg.Width,
Height: winCfg.Height,
MinWidth: config.MinWidth,
MinHeight: config.MinHeight,
BackgroundColour: application.NewRGB(27, 38, 54),
URL: "/",
Linux: application.LinuxWindow{
WebviewGpuPolicy: application.WebviewGpuPolicyAlways,
},
})
// The window's size has to be read while the window still exists,
// which OnShutdown is too late for.
window.OnWindowEvent(
events.Common.WindowClosing,
func(*application.WindowEvent) { yjApp.SaveWindowState(window) },
)
err = app.Run()
stopProfiler()
if err != nil {
sLogger.Error("application error", "err", err.Error())
os.Exit(1)
}
}
// resolveLogLevel determines the slog level. In dev mode the default
// is Info (not Debug) to avoid flooding stdout during library scans.
// Set YJ_LOG_LEVEL=debug to restore verbose logging.
//
// Accepted values: debug, info, warn, error (case-insensitive).
// Production builds always default to Info.
func resolveLogLevel(_ bool) slog.Level {
if env := os.Getenv("YJ_LOG_LEVEL"); env != "" {
switch strings.ToLower(env) {
case "debug":
return slog.LevelDebug
case "info":
return slog.LevelInfo
case "warn":
return slog.LevelWarn
case "error":
return slog.LevelError
}
}
// Default: Info for both dev and prod.
return slog.LevelInfo
}
// isNVIDIAWayland returns true when running under a Wayland session with
// an NVIDIA GPU. This combination triggers DMABuf rendering crashes in
// WebKitGTK, so we need to disable the DMABuf renderer for it.
func isNVIDIAWayland() bool {
// Not Wayland → safe.
if os.Getenv("WAYLAND_DISPLAY") == "" && os.Getenv("XDG_SESSION_TYPE") != "wayland" {
return false
}
// Check for NVIDIA kernel modules (works even without nvidia-smi).
if data, err := os.ReadFile("/proc/driver/nvidia/version"); err == nil {
_ = data
return true
}
// Fallback: check if the nvidia module is loaded.
if data, err := os.ReadFile("/proc/modules"); err == nil {
if strings.Contains(string(data), "nvidia") {
return true
}
}
return false
}