renderer_bridge.go RawHistoryFolder

1package cef
2
3import (
4	"context"
5	"encoding/json"
6	"fmt"
7	"sync"
8	"unsafe"
9
10	purecef "github.com/bnema/purego-cef/cef"
11
12	"github.com/bnema/dumber/internal/logging"
13)
14
15const (
16	rendererBridgeMessageName                = "dumber.renderer_bridge"
17	rendererBridgeActionExplicitTextCopy     = "explicit_text_copy"
18	rendererBridgeActionFocusSync            = "focus_sync"
19	rendererBridgeActionEditableFocusChanged = "editable_focus_changed"
20	rendererBridgeActionPopupOpen            = "popup_open"
21	rendererBridgeActionPopupNavigate        = "popup_navigate"
22	rendererBridgeActionPopupClose           = "popup_close"
23	rendererBridgeActionReady                = "bridge_ready"
24	rendererBridgeExtensionName              = "dumber.renderer_bridge"
25)
26
27var newRendererBridgeProcessMessage = purecef.ProcessMessageCreate
28
29type rendererBridgeProcessHandler struct {
30	registerOnce  sync.Once
31	bridgeHandler purecef.V8Handler
32}
33
34func (h *rendererBridgeProcessHandler) ensureExtensionRegistered() {
35	if h == nil {
36		return
37	}
38	h.registerOnce.Do(func() {
39		if h.bridgeHandler == nil {
40			h.bridgeHandler = &rendererBridgeV8Handler{}
41		}
42		if result := purecef.RegisterExtension(rendererBridgeExtensionName, rendererBridgeExtensionJS, h.bridgeHandler); result != 1 {
43			logging.FromContext(context.Background()).Error().Int32("result", result).Msg("cef: failed to register renderer bridge extension")
44		}
45	})
46}
47
48func (h *rendererBridgeProcessHandler) OnWebKitInitialized() {
49	h.ensureExtensionRegistered()
50}
51
52func (*rendererBridgeProcessHandler) OnBrowserCreated(_ purecef.Browser, _ purecef.DictionaryValue) {}
53func (*rendererBridgeProcessHandler) OnBrowserDestroyed(_ purecef.Browser)                          {}
54func (*rendererBridgeProcessHandler) GetLoadHandler() purecef.LoadHandler                           { return nil }
55func (*rendererBridgeProcessHandler) OnContextCreated(_ purecef.Browser, _ purecef.Frame, _ purecef.V8Context) {
56}
57func (*rendererBridgeProcessHandler) OnContextReleased(_ purecef.Browser, _ purecef.Frame, _ purecef.V8Context) {
58}
59func (*rendererBridgeProcessHandler) OnUncaughtException(
60	_ purecef.Browser, _ purecef.Frame, _ purecef.V8Context, _ purecef.V8Exception, _ purecef.V8StackTrace,
61) {
62}
63func (*rendererBridgeProcessHandler) OnFocusedNodeChanged(_ purecef.Browser, frame purecef.Frame, node purecef.Domnode) {
64	if frame == nil {
65		return
66	}
67	message := newRendererBridgeProcessMessage(rendererBridgeMessageName)
68	if message == nil {
69		return
70	}
71	args := message.GetArgumentList()
72	if args == nil {
73		return
74	}
75	args.SetString(0, rendererBridgeActionEditableFocusChanged)
76	if node != nil && node.IsEditable() {
77		args.SetString(1, "1")
78	} else {
79		args.SetString(1, "0")
80	}
81	frame.SendProcessMessage(purecef.ProcessIDPidBrowser, message)
82}
83func (*rendererBridgeProcessHandler) OnProcessMessageReceived(
84	_ purecef.Browser, _ purecef.Frame, _ purecef.ProcessID, _ purecef.ProcessMessage,
85) int32 {
86	return 0
87}
88
89type rendererBridgeV8Handler struct{}
90
91func (*rendererBridgeV8Handler) Execute(
92	name string, _ purecef.V8Value, arguments []purecef.V8Value, _ unsafe.Pointer, _ uintptr,
93) int32 {
94	if name != "Dispatch" || len(arguments) < 2 {
95		return 0
96	}
97	if arguments[0] == nil || arguments[1] == nil || !arguments[0].IsString() || !arguments[1].IsString() {
98		return 0
99	}
100	action := arguments[0].GetStringValue()
101	payload := arguments[1].GetStringValue()
102	if action == "" {
103		return 0
104	}
105
106	ctx := purecef.V8ContextGetCurrentContext()
107	if ctx == nil || !ctx.IsValid() {
108		return 0
109	}
110	frame := ctx.GetFrame()
111	if frame == nil {
112		return 0
113	}
114	message := purecef.ProcessMessageCreate(rendererBridgeMessageName)
115	if message == nil {
116		return 0
117	}
118	args := message.GetArgumentList()
119	if args == nil {
120		return 0
121	}
122	args.SetString(0, action)
123	args.SetString(1, payload)
124	frame.SendProcessMessage(purecef.ProcessIDPidBrowser, message)
125	return 1
126}
127
128func decodeRendererBridgeProcessMessage(message purecef.ProcessMessage) (string, string, bool) {
129	if message == nil || message.GetName() != rendererBridgeMessageName {
130		return "", "", false
131	}
132	args := message.GetArgumentList()
133	if args == nil || args.GetSize() < 2 {
134		return "", "", false
135	}
136	action := args.GetString(0)
137	payload := args.GetString(1)
138	if action == "" {
139		return "", "", false
140	}
141	return action, payload, true
142}
143
144type rendererBridgeExplicitTextCopyPayload struct {
145	Text   string `json:"text"`
146	Action string `json:"action"`
147}
148
149type rendererBridgePopupOpenPayload struct {
150	ProxyID            string `json:"proxy_id"`
151	URL                string `json:"url"`
152	FrameName          string `json:"frame_name"`
153	UserGesture        bool   `json:"user_gesture"`
154	NoJavaScriptAccess bool   `json:"no_javascript_access"`
155}
156
157type rendererBridgePopupNavigatePayload struct {
158	ProxyID string `json:"proxy_id"`
159	URL     string `json:"url"`
160}
161
162type rendererBridgePopupClosePayload struct {
163	ProxyID string `json:"proxy_id"`
164}
165
166func decodeRendererBridgeExplicitTextCopyPayload(payload []byte) (rendererBridgeExplicitTextCopyPayload, error) {
167	var req rendererBridgeExplicitTextCopyPayload
168	if len(payload) == 0 {
169		return req, fmt.Errorf("empty payload")
170	}
171	if err := json.Unmarshal(payload, &req); err != nil {
172		return req, err
173	}
174	if req.Action == "" {
175		req.Action = "copy"
176	}
177	if req.Text == "" {
178		return req, fmt.Errorf("missing text")
179	}
180	return req, nil
181}
182
183func decodeRendererBridgePopupPayload[T any](payload []byte, proxyID func(T) string) (T, error) {
184	var req T
185	if len(payload) == 0 {
186		return req, fmt.Errorf("empty payload")
187	}
188	if err := json.Unmarshal(payload, &req); err != nil {
189		return req, err
190	}
191	if proxyID(req) == "" {
192		return req, fmt.Errorf("missing proxy_id")
193	}
194	return req, nil
195}
196
197func decodeRendererBridgePopupOpenPayload(payload []byte) (rendererBridgePopupOpenPayload, error) {
198	return decodeRendererBridgePopupPayload(payload, func(req rendererBridgePopupOpenPayload) string {
199		return req.ProxyID
200	})
201}
202
203func decodeRendererBridgePopupNavigatePayload(payload []byte) (rendererBridgePopupNavigatePayload, error) {
204	return decodeRendererBridgePopupPayload(payload, func(req rendererBridgePopupNavigatePayload) string {
205		return req.ProxyID
206	})
207}
208
209func decodeRendererBridgePopupClosePayload(payload []byte) (rendererBridgePopupClosePayload, error) {
210	return decodeRendererBridgePopupPayload(payload, func(req rendererBridgePopupClosePayload) string {
211		return req.ProxyID
212	})
213}
214
215func (e *Engine) handleEditableFocusBridge(browser purecef.Browser) {
216	logger := logging.FromContext(e.currentContext())
217	task := purecef.NewTask(cefTaskFunc(func() {
218		if browser == nil {
219			logger.Debug().Msg("cef: editable focus sync skipped — browser nil")
220			return
221		}
222		host := browser.GetHost()
223		if host == nil {
224			logger.Debug().Msg("cef: editable focus sync skipped — host nil")
225			return
226		}
227		syncWindowlessBrowserFocus(host)
228		logger.Debug().Msg("cef: editable focus synchronized")
229	}))
230	if result := purecef.PostTask(purecef.ThreadIDTidUi, task); result != 1 {
231		logger.Warn().Int32("result", result).Msg("cef: failed to post editable focus sync to UI thread")
232	}
233}

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