package ui import ( "context" "fmt" "os" "strings" "github.com/bnema/dumber/internal/bootstrap" "github.com/bnema/dumber/internal/logging" "github.com/bnema/dumber/internal/ui/component" "github.com/bnema/dumber/internal/ui/layout" "github.com/bnema/puregotk/v4/gdk" "github.com/bnema/puregotk/v4/gio" "github.com/bnema/puregotk/v4/glib" "github.com/bnema/puregotk/v4/gtk" "github.com/bnema/puregotk/v4/layershell" ) var layerShellLibraryAvailable = func() bool { if bootstrap.LayerShellPreloadPresent(bootstrap.CurrentEnvMap()) { return true } return bootstrap.LayerShellLibraryPath() != "" } const ( standaloneOmniboxNamespace = AppID + ".omnibox" standaloneOmniboxTitle = "Dumber Omnibox" standaloneOmniboxWidth = 1200 standaloneOmniboxHeight = 200 standaloneOmniboxWindowCSS = "standalone-omnibox-window" ) type StandaloneOmniboxWindowConfig struct { Namespace string Title string Width int Height int } type StandaloneOmniboxRuntime struct { OmniboxCfg component.OmniboxConfig ApplyTheme func(display *gdk.Display) } type standaloneOmniboxHostLayoutConfig struct { UseDefaultSize bool OmniboxSize component.ModalSizeConfig } var ( layerShellIsSupported = layershell.IsSupported layerShellInitForWindow = func(window *gtk.ApplicationWindow) { layershell.InitForWindow(&window.Window) } layerShellSetLayer = func(window *gtk.ApplicationWindow, layer layershell.Layer) { layershell.SetLayer(&window.Window, layer) } layerShellSetExclusiveZone = func(window *gtk.ApplicationWindow, zone int) { layershell.SetExclusiveZone(&window.Window, zone) } layerShellSetNamespace = func(window *gtk.ApplicationWindow, namespace *string) { layershell.SetNamespace(&window.Window, namespace) } layerShellSetKeyboardMode = func(window *gtk.ApplicationWindow, mode layershell.KeyboardMode) { layershell.SetKeyboardMode(&window.Window, mode) } buildStandaloneOmniboxHostFn = buildStandaloneOmniboxHost newStandaloneOmniboxWindow = gtk.NewApplicationWindow configureStandaloneOmniboxWindowFn = configureStandaloneOmniboxWindow presentStandaloneOmniboxWindow = func(window *gtk.ApplicationWindow) { window.Present() } logStandaloneOmniboxHostAllocationFn = logStandaloneOmniboxHostAllocation showStandaloneOmniboxFn = func(ctx context.Context, omnibox *component.Omnibox) { omnibox.Show(ctx, "") } ) type standaloneOmniboxActivationRetention struct { window *gtk.ApplicationWindow host *gtk.Widget omnibox *component.Omnibox } func (r *standaloneOmniboxActivationRetention) retain( window *gtk.ApplicationWindow, host *gtk.Widget, omnibox *component.Omnibox, ) { if r == nil { return } r.window = window r.host = host r.omnibox = omnibox } func (r *standaloneOmniboxActivationRetention) release() { if r == nil { return } r.window = nil r.host = nil r.omnibox = nil } func DefaultStandaloneOmniboxWindowConfig() StandaloneOmniboxWindowConfig { return StandaloneOmniboxWindowConfig{ Namespace: standaloneOmniboxNamespace, Title: standaloneOmniboxTitle, Width: standaloneOmniboxWidth, Height: standaloneOmniboxHeight, } } func standaloneOmniboxArgv(args []string) []string { if len(args) == 0 { return []string{"dumber-omnibox"} } filtered := []string{args[0]} subcommandSkipped := false for i := 1; i < len(args); i++ { arg := args[i] if arg == "omnibox" && !subcommandSkipped { subcommandSkipped = true continue } if strings.HasPrefix(arg, "-test.") { if !strings.Contains(arg, "=") && goTestFlagConsumesNextArg(arg) && i+1 < len(args) { i++ } continue } filtered = append(filtered, arg) } return filtered } func goTestFlagConsumesNextArg(arg string) bool { switch arg { case "-test.bench", "-test.blockprofile", "-test.count", "-test.coverprofile", "-test.cpu", "-test.cpuprofile", "-test.fuzz", "-test.fuzzcachedir", "-test.fuzzminimizetime", "-test.fuzztime", "-test.list", "-test.memprofile", "-test.memprofilerate", "-test.mutexprofile", "-test.mutexprofilefraction", "-test.outputdir", "-test.parallel", "-test.run", "-test.shuffle", "-test.skip", "-test.timeout", "-test.trace": return true default: return false } } func wrapStandaloneOmniboxNavigate(original func(context.Context, string) error, quit func()) func(context.Context, string) error { return func(ctx context.Context, url string) error { if original == nil { return fmt.Errorf("standalone omnibox navigate handler is not configured") } if err := original(ctx, url); err != nil { return err } if quit != nil { quit() } return nil } } func standaloneOmniboxCloseHandler(quit func()) func() { return func() { if quit != nil { quit() } } } func standaloneOmniboxWindowDecorated(layerShellApplied bool) bool { return !layerShellApplied } func standaloneOmniboxWindowResizable(layerShellApplied bool) bool { return !layerShellApplied } func standaloneOmniboxHostLayout(layerShellApplied bool) standaloneOmniboxHostLayoutConfig { return standaloneOmniboxHostLayoutConfig{ UseDefaultSize: !layerShellApplied, OmniboxSize: standaloneOmniboxSizeConfig(), } } func attachStandaloneOmniboxHostWidget(host layout.OverlayWidget, omniboxWidget layout.Widget) { if host == nil { return } host.SetChild(omniboxWidget) } func standaloneOmniboxSizeConfig() component.ModalSizeConfig { return component.ResolveModalSizeConfig(component.ModalSizeConfig{ FixedWidth: component.OmniboxSizeDefaults.MaxWidth, FixedTopMargin: 0, UseFixedTopMargin: true, UseMonitorHeight: true, }, component.OmniboxSizeDefaults) } func tryInitLayerShell(window *gtk.ApplicationWindow, cfg StandaloneOmniboxWindowConfig) bool { if window == nil { return false } if !safeLayerShellSupported() { return false } layerShellInitForWindow(window) layerShellSetLayer(window, layershell.LayerOverlayValue) layerShellSetExclusiveZone(window, 0) layerShellSetNamespace(window, &cfg.Namespace) layerShellSetKeyboardMode(window, layershell.KeyboardModeExclusiveValue) return true } func safeLayerShellSupported() (supported bool) { defer func() { if recover() != nil { supported = false } }() if bootstrap.LayerShellPreloadPresent(bootstrap.CurrentEnvMap()) || layerShellLibraryAvailable() { return layerShellIsSupported() } return false } func configureStandaloneOmniboxWindow(window *gtk.ApplicationWindow, windowCfg StandaloneOmniboxWindowConfig, child *gtk.Widget) { if window == nil { return } title := windowCfg.Title window.SetTitle(&title) layerShellApplied := tryInitLayerShell(window, windowCfg) hostLayout := standaloneOmniboxHostLayout(layerShellApplied) if hostLayout.UseDefaultSize { window.SetDefaultSize(windowCfg.Width, windowCfg.Height) } if layerShellApplied { window.AddCssClass(standaloneOmniboxWindowCSS) } window.SetDecorated(standaloneOmniboxWindowDecorated(layerShellApplied)) window.SetResizable(standaloneOmniboxWindowResizable(layerShellApplied)) window.SetChild(child) } func buildStandaloneOmniboxHost( ctx context.Context, runtimeCfg *StandaloneOmniboxRuntime, gtkApp *gtk.Application, ) (*gtk.Widget, *component.Omnibox) { factory := layout.NewGtkWidgetFactory() overlay := factory.NewOverlay() if overlay == nil { logging.FromContext(ctx).Error().Msg("failed to create standalone omnibox host widgets") return nil, nil } cfg := runtimeCfg.OmniboxCfg cfg.OnNavigate = wrapStandaloneOmniboxNavigate(cfg.OnNavigate, gtkApp.Quit) cfg.SizeConfig = standaloneOmniboxSizeConfig() omnibox := component.NewOmnibox(ctx, cfg) if omnibox == nil { logging.FromContext(ctx).Error().Msg("failed to create standalone omnibox") return nil, nil } overlay.SetVisible(true) omnibox.SetParentOverlay(overlay) omnibox.SetOnClose(standaloneOmniboxCloseHandler(gtkApp.Quit)) omniboxWidget := omnibox.WidgetAsLayout(factory) if omniboxWidget == nil { logging.FromContext(ctx).Error().Msg("failed to wrap standalone omnibox widget") return nil, nil } attachStandaloneOmniboxHostWidget(overlay, omniboxWidget) logging.FromContext(ctx).Debug().Msg("standalone omnibox host configured with primary child") return overlay.GtkWidget(), omnibox } func logStandaloneOmniboxHostAllocation(ctx context.Context, host *gtk.Widget) { if host == nil { return } var cb glib.SourceFunc = func(uintptr) bool { logging.FromContext(ctx).Debug(). Int("width", host.GetAllocatedWidth()). Int("height", host.GetAllocatedHeight()). Msg("standalone omnibox host allocation") return false } glib.IdleAdd(&cb, 0) } func activateStandaloneOmnibox( ctx context.Context, runtimeCfg *StandaloneOmniboxRuntime, gtkApp *gtk.Application, windowCfg StandaloneOmniboxWindowConfig, retention *standaloneOmniboxActivationRetention, ) { child, omnibox := buildStandaloneOmniboxHostFn(ctx, runtimeCfg, gtkApp) if child == nil || omnibox == nil { gtkApp.Quit() return } window := newStandaloneOmniboxWindow(gtkApp) if window == nil { logging.FromContext(ctx).Error().Msg("failed to create standalone omnibox window") gtkApp.Quit() return } retention.retain(window, child, omnibox) configureStandaloneOmniboxWindowFn(window, windowCfg, child) if runtimeCfg.ApplyTheme != nil { if display := window.GetDisplay(); display != nil { runtimeCfg.ApplyTheme(display) } } presentStandaloneOmniboxWindow(window) logStandaloneOmniboxHostAllocationFn(ctx, child) showStandaloneOmniboxFn(ctx, omnibox) } func RunStandaloneOmnibox(ctx context.Context, runtimeCfg *StandaloneOmniboxRuntime) int { if runtimeCfg == nil { logging.FromContext(ctx).Error().Msg("standalone omnibox runtime not configured") return 1 } EnsureAdwaitaInitialized() windowCfg := DefaultStandaloneOmniboxWindowConfig() appID := windowCfg.Namespace gtkApp := gtk.NewApplication(&appID, gtkApplicationFlags()) if gtkApp == nil { logging.FromContext(ctx).Error().Msg("failed to create standalone omnibox application") return 1 } defer gtkApp.Unref() retention := &standaloneOmniboxActivationRetention{} activateCb := func(_ gio.Application) { activateStandaloneOmnibox(ctx, runtimeCfg, gtkApp, windowCfg, retention) } gtkApp.ConnectActivate(&activateCb) argv := standaloneOmniboxArgv(os.Args) code := gtkApp.Run(len(argv), argv) retention.release() return code }