v0.33.1 9577fee

standalone_omnibox.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close