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
173 lines
4.7 KiB
Go
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
|
|
}
|