1package ui
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "strings"
8
9 "github.com/bnema/dumber/internal/bootstrap"
10 "github.com/bnema/dumber/internal/logging"
11 "github.com/bnema/dumber/internal/ui/component"
12 "github.com/bnema/dumber/internal/ui/layout"
13 "github.com/bnema/puregotk/v4/gdk"
14 "github.com/bnema/puregotk/v4/gio"
15 "github.com/bnema/puregotk/v4/glib"
16 "github.com/bnema/puregotk/v4/gtk"
17 "github.com/bnema/puregotk/v4/layershell"
18)
19
20var layerShellLibraryAvailable = func() bool {
21 if bootstrap.LayerShellPreloadPresent(bootstrap.CurrentEnvMap()) {
22 return true
23 }
24
25 return bootstrap.LayerShellLibraryPath() != ""
26}
27
28const (
29 standaloneOmniboxNamespace = AppID + ".omnibox"
30 standaloneOmniboxTitle = "Dumber Omnibox"
31 standaloneOmniboxWidth = 1200
32 standaloneOmniboxHeight = 200
33 standaloneOmniboxWindowCSS = "standalone-omnibox-window"
34)
35
36type StandaloneOmniboxWindowConfig struct {
37 Namespace string
38 Title string
39 Width int
40 Height int
41}
42
43type StandaloneOmniboxRuntime struct {
44 OmniboxCfg component.OmniboxConfig
45 ApplyTheme func(display *gdk.Display)
46}
47
48type standaloneOmniboxHostLayoutConfig struct {
49 UseDefaultSize bool
50 OmniboxSize component.ModalSizeConfig
51}
52
53var (
54 layerShellIsSupported = layershell.IsSupported
55 layerShellInitForWindow = func(window *gtk.ApplicationWindow) {
56 layershell.InitForWindow(&window.Window)
57 }
58 layerShellSetLayer = func(window *gtk.ApplicationWindow, layer layershell.Layer) {
59 layershell.SetLayer(&window.Window, layer)
60 }
61 layerShellSetExclusiveZone = func(window *gtk.ApplicationWindow, zone int) {
62 layershell.SetExclusiveZone(&window.Window, zone)
63 }
64 layerShellSetNamespace = func(window *gtk.ApplicationWindow, namespace *string) {
65 layershell.SetNamespace(&window.Window, namespace)
66 }
67 layerShellSetKeyboardMode = func(window *gtk.ApplicationWindow, mode layershell.KeyboardMode) {
68 layershell.SetKeyboardMode(&window.Window, mode)
69 }
70 buildStandaloneOmniboxHostFn = buildStandaloneOmniboxHost
71 newStandaloneOmniboxWindow = gtk.NewApplicationWindow
72 configureStandaloneOmniboxWindowFn = configureStandaloneOmniboxWindow
73 presentStandaloneOmniboxWindow = func(window *gtk.ApplicationWindow) {
74 window.Present()
75 }
76 logStandaloneOmniboxHostAllocationFn = logStandaloneOmniboxHostAllocation
77 showStandaloneOmniboxFn = func(ctx context.Context, omnibox *component.Omnibox) {
78 omnibox.Show(ctx, "")
79 }
80)
81
82type standaloneOmniboxActivationRetention struct {
83 window *gtk.ApplicationWindow
84 host *gtk.Widget
85 omnibox *component.Omnibox
86}
87
88func (r *standaloneOmniboxActivationRetention) retain(
89 window *gtk.ApplicationWindow,
90 host *gtk.Widget,
91 omnibox *component.Omnibox,
92) {
93 if r == nil {
94 return
95 }
96 r.window = window
97 r.host = host
98 r.omnibox = omnibox
99}
100
101func (r *standaloneOmniboxActivationRetention) release() {
102 if r == nil {
103 return
104 }
105 r.window = nil
106 r.host = nil
107 r.omnibox = nil
108}
109
110func DefaultStandaloneOmniboxWindowConfig() StandaloneOmniboxWindowConfig {
111 return StandaloneOmniboxWindowConfig{
112 Namespace: standaloneOmniboxNamespace,
113 Title: standaloneOmniboxTitle,
114 Width: standaloneOmniboxWidth,
115 Height: standaloneOmniboxHeight,
116 }
117}
118
119func standaloneOmniboxArgv(args []string) []string {
120 if len(args) == 0 {
121 return []string{"dumber-omnibox"}
122 }
123
124 filtered := []string{args[0]}
125 subcommandSkipped := false
126 for i := 1; i < len(args); i++ {
127 arg := args[i]
128 if arg == "omnibox" && !subcommandSkipped {
129 subcommandSkipped = true
130 continue
131 }
132 if strings.HasPrefix(arg, "-test.") {
133 if !strings.Contains(arg, "=") && goTestFlagConsumesNextArg(arg) && i+1 < len(args) {
134 i++
135 }
136 continue
137 }
138 filtered = append(filtered, arg)
139 }
140
141 return filtered
142}
143
144func goTestFlagConsumesNextArg(arg string) bool {
145 switch arg {
146 case "-test.bench", "-test.blockprofile", "-test.count", "-test.coverprofile",
147 "-test.cpu", "-test.cpuprofile", "-test.fuzz", "-test.fuzzcachedir",
148 "-test.fuzzminimizetime", "-test.fuzztime", "-test.list", "-test.memprofile",
149 "-test.memprofilerate", "-test.mutexprofile", "-test.mutexprofilefraction",
150 "-test.outputdir", "-test.parallel", "-test.run", "-test.shuffle",
151 "-test.skip", "-test.timeout", "-test.trace":
152 return true
153 default:
154 return false
155 }
156}
157
158func wrapStandaloneOmniboxNavigate(original func(context.Context, string) error, quit func()) func(context.Context, string) error {
159 return func(ctx context.Context, url string) error {
160 if original == nil {
161 return fmt.Errorf("standalone omnibox navigate handler is not configured")
162 }
163 if err := original(ctx, url); err != nil {
164 return err
165 }
166 if quit != nil {
167 quit()
168 }
169 return nil
170 }
171}
172
173func standaloneOmniboxCloseHandler(quit func()) func() {
174 return func() {
175 if quit != nil {
176 quit()
177 }
178 }
179}
180
181func standaloneOmniboxWindowDecorated(layerShellApplied bool) bool {
182 return !layerShellApplied
183}
184
185func standaloneOmniboxWindowResizable(layerShellApplied bool) bool {
186 return !layerShellApplied
187}
188
189func standaloneOmniboxHostLayout(layerShellApplied bool) standaloneOmniboxHostLayoutConfig {
190 return standaloneOmniboxHostLayoutConfig{
191 UseDefaultSize: !layerShellApplied,
192 OmniboxSize: standaloneOmniboxSizeConfig(),
193 }
194}
195
196func attachStandaloneOmniboxHostWidget(host layout.OverlayWidget, omniboxWidget layout.Widget) {
197 if host == nil {
198 return
199 }
200
201 host.SetChild(omniboxWidget)
202}
203
204func standaloneOmniboxSizeConfig() component.ModalSizeConfig {
205 return component.ResolveModalSizeConfig(component.ModalSizeConfig{
206 FixedWidth: component.OmniboxSizeDefaults.MaxWidth,
207 FixedTopMargin: 0,
208 UseFixedTopMargin: true,
209 UseMonitorHeight: true,
210 }, component.OmniboxSizeDefaults)
211}
212
213func tryInitLayerShell(window *gtk.ApplicationWindow, cfg StandaloneOmniboxWindowConfig) bool {
214 if window == nil {
215 return false
216 }
217 if !safeLayerShellSupported() {
218 return false
219 }
220
221 layerShellInitForWindow(window)
222 layerShellSetLayer(window, layershell.LayerOverlayValue)
223 layerShellSetExclusiveZone(window, 0)
224 layerShellSetNamespace(window, &cfg.Namespace)
225 layerShellSetKeyboardMode(window, layershell.KeyboardModeExclusiveValue)
226 return true
227}
228
229func safeLayerShellSupported() (supported bool) {
230 defer func() {
231 if recover() != nil {
232 supported = false
233 }
234 }()
235
236 if bootstrap.LayerShellPreloadPresent(bootstrap.CurrentEnvMap()) || layerShellLibraryAvailable() {
237 return layerShellIsSupported()
238 }
239
240 return false
241}
242
243func configureStandaloneOmniboxWindow(window *gtk.ApplicationWindow, windowCfg StandaloneOmniboxWindowConfig, child *gtk.Widget) {
244 if window == nil {
245 return
246 }
247
248 title := windowCfg.Title
249 window.SetTitle(&title)
250 layerShellApplied := tryInitLayerShell(window, windowCfg)
251 hostLayout := standaloneOmniboxHostLayout(layerShellApplied)
252 if hostLayout.UseDefaultSize {
253 window.SetDefaultSize(windowCfg.Width, windowCfg.Height)
254 }
255 if layerShellApplied {
256 window.AddCssClass(standaloneOmniboxWindowCSS)
257 }
258 window.SetDecorated(standaloneOmniboxWindowDecorated(layerShellApplied))
259 window.SetResizable(standaloneOmniboxWindowResizable(layerShellApplied))
260 window.SetChild(child)
261}
262
263func buildStandaloneOmniboxHost(
264 ctx context.Context,
265 runtimeCfg *StandaloneOmniboxRuntime,
266 gtkApp *gtk.Application,
267) (*gtk.Widget, *component.Omnibox) {
268 factory := layout.NewGtkWidgetFactory()
269 overlay := factory.NewOverlay()
270 if overlay == nil {
271 logging.FromContext(ctx).Error().Msg("failed to create standalone omnibox host widgets")
272 return nil, nil
273 }
274
275 cfg := runtimeCfg.OmniboxCfg
276 cfg.OnNavigate = wrapStandaloneOmniboxNavigate(cfg.OnNavigate, gtkApp.Quit)
277 cfg.SizeConfig = standaloneOmniboxSizeConfig()
278
279 omnibox := component.NewOmnibox(ctx, cfg)
280 if omnibox == nil {
281 logging.FromContext(ctx).Error().Msg("failed to create standalone omnibox")
282 return nil, nil
283 }
284
285 overlay.SetVisible(true)
286 omnibox.SetParentOverlay(overlay)
287 omnibox.SetOnClose(standaloneOmniboxCloseHandler(gtkApp.Quit))
288
289 omniboxWidget := omnibox.WidgetAsLayout(factory)
290 if omniboxWidget == nil {
291 logging.FromContext(ctx).Error().Msg("failed to wrap standalone omnibox widget")
292 return nil, nil
293 }
294
295 attachStandaloneOmniboxHostWidget(overlay, omniboxWidget)
296 logging.FromContext(ctx).Debug().Msg("standalone omnibox host configured with primary child")
297 return overlay.GtkWidget(), omnibox
298}
299
300func logStandaloneOmniboxHostAllocation(ctx context.Context, host *gtk.Widget) {
301 if host == nil {
302 return
303 }
304
305 var cb glib.SourceFunc = func(uintptr) bool {
306 logging.FromContext(ctx).Debug().
307 Int("width", host.GetAllocatedWidth()).
308 Int("height", host.GetAllocatedHeight()).
309 Msg("standalone omnibox host allocation")
310 return false
311 }
312 glib.IdleAdd(&cb, 0)
313}
314
315func activateStandaloneOmnibox(
316 ctx context.Context,
317 runtimeCfg *StandaloneOmniboxRuntime,
318 gtkApp *gtk.Application,
319 windowCfg StandaloneOmniboxWindowConfig,
320 retention *standaloneOmniboxActivationRetention,
321) {
322 child, omnibox := buildStandaloneOmniboxHostFn(ctx, runtimeCfg, gtkApp)
323 if child == nil || omnibox == nil {
324 gtkApp.Quit()
325 return
326 }
327
328 window := newStandaloneOmniboxWindow(gtkApp)
329 if window == nil {
330 logging.FromContext(ctx).Error().Msg("failed to create standalone omnibox window")
331 gtkApp.Quit()
332 return
333 }
334
335 retention.retain(window, child, omnibox)
336 configureStandaloneOmniboxWindowFn(window, windowCfg, child)
337 if runtimeCfg.ApplyTheme != nil {
338 if display := window.GetDisplay(); display != nil {
339 runtimeCfg.ApplyTheme(display)
340 }
341 }
342 presentStandaloneOmniboxWindow(window)
343 logStandaloneOmniboxHostAllocationFn(ctx, child)
344 showStandaloneOmniboxFn(ctx, omnibox)
345}
346
347func RunStandaloneOmnibox(ctx context.Context, runtimeCfg *StandaloneOmniboxRuntime) int {
348 if runtimeCfg == nil {
349 logging.FromContext(ctx).Error().Msg("standalone omnibox runtime not configured")
350 return 1
351 }
352
353 EnsureAdwaitaInitialized()
354 windowCfg := DefaultStandaloneOmniboxWindowConfig()
355 appID := windowCfg.Namespace
356 gtkApp := gtk.NewApplication(&appID, gtkApplicationFlags())
357 if gtkApp == nil {
358 logging.FromContext(ctx).Error().Msg("failed to create standalone omnibox application")
359 return 1
360 }
361 defer gtkApp.Unref()
362 retention := &standaloneOmniboxActivationRetention{}
363
364 activateCb := func(_ gio.Application) {
365 activateStandaloneOmnibox(ctx, runtimeCfg, gtkApp, windowCfg, retention)
366 }
367 gtkApp.ConnectActivate(&activateCb)
368
369 argv := standaloneOmniboxArgv(os.Args)
370 code := gtkApp.Run(len(argv), argv)
371 retention.release()
372 return code
373}