v0.15.0 9006957

shortcuts.go RawHistoryFolder

1package webkit
2
3import (
4	"fmt"
5	"strings"
6
7	"github.com/bnema/dumber/internal/logging"
8	"github.com/diamondburned/gotk4/pkg/gdk/v4"
9	"github.com/diamondburned/gotk4/pkg/glib/v2"
10	"github.com/diamondburned/gotk4/pkg/gtk/v4"
11)
12
13// WindowShortcuts manages keyboard shortcuts for a window
14type WindowShortcuts struct {
15	window     *Window
16	shortcuts  map[string]*gtk.Shortcut
17	controller *gtk.ShortcutController
18}
19
20// NewWindowShortcuts creates a new WindowShortcuts manager
21func NewWindowShortcuts(win *Window) *WindowShortcuts {
22	controller := gtk.NewShortcutController()
23
24	// Set scope to GLOBAL so shortcuts work at the window level
25	// and aren't captured by child widgets (like the WebView)
26	controller.SetScope(gtk.ShortcutScopeGlobal)
27
28	if win != nil && win.win != nil {
29		win.win.AddController(controller)
30	}
31
32	return &WindowShortcuts{
33		window:     win,
34		shortcuts:  make(map[string]*gtk.Shortcut),
35		controller: controller,
36	}
37}
38
39// InitializeGlobalShortcuts initializes global keyboard shortcuts for a window
40func (w *Window) InitializeGlobalShortcuts(shortcuts map[string]func()) {
41	if w == nil || w.win == nil {
42		return
43	}
44
45	// Create event controller for keyboard events
46	controller := gtk.NewEventControllerKey()
47
48	controller.ConnectKeyPressed(func(keyval, keycode uint, state gdk.ModifierType) bool {
49		// Handle keyboard shortcuts
50		// TODO: Implement proper shortcut matching
51		return false
52	})
53
54	w.win.AddController(controller)
55}
56
57// RegisterShortcut registers a keyboard shortcut
58func (ws *WindowShortcuts) RegisterShortcut(accelerator string, handler func()) {
59	if ws == nil || ws.controller == nil {
60		return
61	}
62
63	// Convert to GTK format (e.g., "ctrl+plus" -> "<Control>plus")
64	gtkFormat := convertToGtkFormat(accelerator)
65	if gtkFormat == "" {
66		logging.Warn(fmt.Sprintf("[shortcuts] Invalid key format: %s", accelerator))
67		return
68	}
69
70	// Create a callback action that will trigger our handler
71	// ShortcutFunc signature: func(widget Widgetter, args *glib.Variant) (ok bool)
72	action := gtk.NewCallbackAction(func(widget gtk.Widgetter, args *glib.Variant) bool {
73		if handler != nil {
74			logging.Debug(fmt.Sprintf("[shortcuts] Shortcut triggered: %s", accelerator))
75			handler()
76		}
77		// Return false to allow event propagation to WebView
78		// This lets JavaScript KeyboardService also handle events if needed
79		return false
80	})
81
82	// Parse the accelerator string into a GDK key trigger
83	// GTK4 uses gtk.ShortcutTrigger for key combinations
84	trigger := gtk.NewShortcutTriggerParseString(gtkFormat)
85	if trigger == nil {
86		logging.Error(fmt.Sprintf("[shortcuts] Failed to parse trigger: %s", gtkFormat))
87		return
88	}
89
90	// Create the shortcut with trigger and action
91	shortcut := gtk.NewShortcut(trigger, action)
92
93	// Add shortcut to the shared controller
94	ws.controller.AddShortcut(shortcut)
95
96	// Store the shortcut for potential cleanup
97	ws.shortcuts[accelerator] = shortcut
98
99	logging.Debug(fmt.Sprintf("[shortcuts] Registered: %s -> %s", accelerator, gtkFormat))
100}
101
102// convertToGtkFormat converts common key formats to GTK shortcut format
103// Examples: "ctrl+plus" -> "<Control>plus", "ctrl+l" -> "<Control>l"
104func convertToGtkFormat(key string) string {
105	key = strings.ToLower(key)
106
107	// Handle cmdorctrl -> Control
108	key = strings.ReplaceAll(key, "cmdorctrl+", "ctrl+")
109	key = strings.ReplaceAll(key, "cmd+", "ctrl+")
110
111	// Split by + to get components
112	components := strings.Split(key, "+")
113	if len(components) == 0 {
114		return ""
115	}
116
117	var parts []string
118	for i, part := range components {
119		if i == len(components)-1 {
120			// Last part is the actual key
121			switch part {
122			case "f12":
123				parts = append(parts, "F12")
124			case "f5":
125				parts = append(parts, "F5")
126			case "plus":
127				parts = append(parts, "plus")
128			case "equal":
129				parts = append(parts, "equal")
130			case "minus":
131				parts = append(parts, "minus")
132			case "0":
133				parts = append(parts, "0")
134			case "left", "arrowleft":
135				parts = append(parts, "Left")
136			case "right", "arrowright":
137				parts = append(parts, "Right")
138			case "up", "arrowup":
139				parts = append(parts, "Up")
140			case "down", "arrowdown":
141				parts = append(parts, "Down")
142			case "tab":
143				parts = append(parts, "Tab")
144			case "escape":
145				parts = append(parts, "Escape")
146			case "return", "enter":
147				parts = append(parts, "Return")
148			default:
149				// Single letter keys like l, f, c, w, etc.
150				parts = append(parts, part)
151			}
152		} else {
153			// Modifier keys
154			switch part {
155			case "ctrl":
156				parts = append(parts, "<Control>")
157			case "shift":
158				parts = append(parts, "<Shift>")
159			case "alt":
160				parts = append(parts, "<Alt>")
161			case "super", "meta":
162				parts = append(parts, "<Super>")
163			}
164		}
165	}
166
167	return strings.Join(parts, "")
168}

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