v0.33.1 9577fee

jsdialog_handler.go RawHistoryFolder

1package cef
2
3import (
4	"sync"
5
6	purecef "github.com/bnema/purego-cef/cef"
7
8	"github.com/bnema/dumber/internal/application/port"
9	"github.com/bnema/dumber/internal/logging"
10)
11
12// CEF JavaScript dialog support (alert/confirm/prompt/beforeunload).
13//
14// With a nil JsdialogHandler CEF falls back to its built-in runner, which has
15// no usable window in windowless (OSR) mode on Linux, so dialogs are never
16// shown to the user. We therefore implement the handler, forward requests to
17// the UI through port.WebViewCallbacks.OnJSDialog and answer the CEF callback
18// exactly once.
19//
20// Anti-abuse: while a dialog is open for a WebView, further OnJsdialog requests
21// are suppressed (suppress_message=true) instead of being queued, so a page
22// cannot stack or spam dialogs.
23//
24// CEF reset semantics (javascript_dialog_manager.cc): while CEF's handler_ is
25// set, OnResetDialogState is also delivered (a) right after a suppressed
26// OnJsdialog and (b) at the end of DialogClosed, i.e. inside/after a callback
27// Cont. Those are not real resets and must not cancel the dialog the user is
28// looking at, so jsDialogState carries a one-shot ignoreReset flag armed before
29// suppressing, rejecting or continuing (it may stay unconsumed when CEF's
30// handler_ was already cleared; begin resets it).
31//
32// After a suppress or a rejected beforeunload CEF clears handler_, so a later
33// navigation sends no reset for the still-open dialog, and Chromium's
34// main-frame commit does not reset state either. Main-frame OnLoadStart
35// therefore cancels a pending dialog too. Destroy and renderer termination
36// cancel unconditionally; Destroy also closes the state so no dialog can begin
37// afterwards.
38
39// continueCEFJSDialog resolves a CEF dialog callback. It is a seam for tests.
40// CEF's callback implementation re-posts to the CEF UI thread by itself.
41var continueCEFJSDialog = func(callback purecef.JsdialogCallback, ok bool, input string) {
42	success := int32(0)
43	if ok {
44		success = 1
45	}
46	callback.Cont(success, input)
47}
48
49var _ purecef.JsdialogHandler = (*handlerSet)(nil)
50
51// jsDialogCall is one in-flight dialog. done is guarded by jsDialogState.mu.
52type jsDialogCall struct {
53	callback purecef.JsdialogCallback
54	done     bool
55}
56
57// jsDialogState tracks the single pending dialog of a WebView.
58type jsDialogState struct {
59	mu          sync.Mutex
60	current     *jsDialogCall
61	ignoreReset bool // swallow the next OnResetDialogState (see file comment)
62	closed      bool // WebView destroyed: begin always fails
63}
64
65// jsDialogBegin is the outcome of jsDialogState.begin.
66type jsDialogBegin int
67
68const (
69	jsDialogBeginOK     jsDialogBegin = iota
70	jsDialogBeginBusy                 // another dialog is open
71	jsDialogBeginClosed               // WebView destroyed
72)
73
74// armIgnoreReset makes the next OnResetDialogState a no-op.
75func (s *jsDialogState) armIgnoreReset() {
76	s.mu.Lock()
77	s.ignoreReset = true
78	s.mu.Unlock()
79}
80
81// consumeIgnoreReset reports (and clears) a pending ignoreReset.
82func (s *jsDialogState) consumeIgnoreReset() bool {
83	s.mu.Lock()
84	defer s.mu.Unlock()
85	was := s.ignoreReset
86	s.ignoreReset = false
87	return was
88}
89
90// begin registers a new pending dialog.
91func (s *jsDialogState) begin(cb purecef.JsdialogCallback) (*jsDialogCall, jsDialogBegin) {
92	s.mu.Lock()
93	defer s.mu.Unlock()
94	if s.closed {
95		return nil, jsDialogBeginClosed
96	}
97	if s.current != nil {
98		return nil, jsDialogBeginBusy
99	}
100	call := &jsDialogCall{callback: cb}
101	s.current = call
102	// A new dialog means any reset owed by the previous one has been delivered
103	// (the page is blocked until then); never carry a stale flag across.
104	s.ignoreReset = false
105	return call, jsDialogBeginOK
106}
107
108// hasPending reports whether an unresolved dialog is registered.
109func (s *jsDialogState) hasPending() bool {
110	s.mu.Lock()
111	defer s.mu.Unlock()
112	return s.current != nil
113}
114
115func (s *jsDialogState) isDone(call *jsDialogCall) bool {
116	s.mu.Lock()
117	defer s.mu.Unlock()
118	return call.done
119}
120
121// resolve answers the CEF callback exactly once; later calls are no-ops.
122func (s *jsDialogState) resolve(call *jsDialogCall, ok bool, input string) bool {
123	s.mu.Lock()
124	if call == nil || call.done {
125		s.mu.Unlock()
126		return false
127	}
128	call.done = true
129	if s.current == call {
130		s.current = nil
131	}
132	// Armed before Cont: CEF may deliver DialogClosed + OnResetDialogState
133	// synchronously from inside it.
134	s.ignoreReset = true
135	s.mu.Unlock()
136	continueCEFJSDialog(call.callback, ok, input)
137	return true
138}
139
140// cancelPending resolves the pending dialog (if any) with a cancel. When
141// closing is true it also rejects every later begin (closing is atomic with
142// reading the pending call, so a concurrent begin cannot slip in).
143func (s *jsDialogState) cancelPending(closing bool) bool {
144	s.mu.Lock()
145	if closing {
146		s.closed = true
147	}
148	call := s.current
149	s.mu.Unlock()
150	if call == nil {
151		return false
152	}
153	return s.resolve(call, false, "")
154}
155
156func (wv *WebView) jsDialogUICallbacks() *port.WebViewCallbacks {
157	wv.mu.RLock()
158	defer wv.mu.RUnlock()
159	return wv.callbacks
160}
161
162// cancelJSDialogs resolves any pending dialog and tells the UI to hide it.
163func (wv *WebView) cancelJSDialogs() { wv.cancelJSDialogsClosing(false) }
164
165// cancelJSDialogsClosing is cancelJSDialogs; with closing it also forbids any
166// further dialog (WebView destruction).
167func (wv *WebView) cancelJSDialogsClosing(closing bool) {
168	if wv == nil {
169		return
170	}
171	if !wv.jsDialogs.cancelPending(closing) {
172		return
173	}
174	if cb := wv.jsDialogUICallbacks(); cb != nil && cb.OnJSDialogReset != nil {
175		wv.runOnGTK(func() { wv.resetJSDialogUI(cb.OnJSDialogReset) })
176	}
177}
178
179// resetJSDialogUI runs on the GTK thread, after a cancel posted it. The reset
180// carries no dialog identity, so if a newer dialog began in the meantime it is
181// stale and must be skipped, or it would hide that dialog. A dialog that
182// begins after this check has its presentation queued behind us on the GTK
183// thread, so it is never hidden either.
184func (wv *WebView) resetJSDialogUI(reset func()) {
185	if wv.jsDialogs.hasPending() {
186		return
187	}
188	reset()
189}
190
191func jsDialogTypeFromCEF(t purecef.JsdialogType) (port.JSDialogType, bool) {
192	switch t {
193	case purecef.JsdialogTypeJsdialogtypeAlert:
194		return port.JSDialogAlert, true
195	case purecef.JsdialogTypeJsdialogtypeConfirm:
196		return port.JSDialogConfirm, true
197	case purecef.JsdialogTypeJsdialogtypePrompt:
198		return port.JSDialogPrompt, true
199	default:
200		return 0, false
201	}
202}
203
204// present shows the dialog on the GTK thread and wires the user's answer back.
205func (wv *WebView) presentJSDialog(call *jsDialogCall, req port.JSDialogRequest, ui *port.WebViewCallbacks) {
206	wv.runOnGTK(func() {
207		if wv.jsDialogs.isDone(call) {
208			return // canceled before the UI got to run
209		}
210		respond := func(ok bool, input string) {
211			if req.Type != port.JSDialogPrompt {
212				input = ""
213			}
214			wv.jsDialogs.resolve(call, ok, input)
215		}
216		if !ui.OnJSDialog(req, respond) {
217			respond(false, "")
218		}
219	})
220}
221
222// OnJsdialog implements purecef.JsdialogHandler.
223func (h *handlerSet) OnJsdialog(
224	_ purecef.Browser,
225	originURL string,
226	dialogType purecef.JsdialogType,
227	messageText string,
228	defaultPromptText string,
229	callback purecef.JsdialogCallback,
230	suppressMessage *int32,
231) int32 {
232	if h == nil || h.wv == nil || callback == nil {
233		return 0
234	}
235	if h.wv.destroyed.Load() {
236		// Shutting down: the GTK loop may be blocked. Let CEF dismiss it.
237		return 0
238	}
239	typ, ok := jsDialogTypeFromCEF(dialogType)
240	if !ok {
241		return 0
242	}
243	ui := h.wv.jsDialogUICallbacks()
244	if ui == nil || ui.OnJSDialog == nil {
245		return 0 // no UI: let CEF apply its default behavior
246	}
247	call, begin := h.wv.jsDialogs.begin(callback)
248	if begin == jsDialogBeginClosed {
249		return 0
250	}
251	if begin == jsDialogBeginBusy {
252		// A dialog is already open: suppress rather than stack/spam. CEF will
253		// follow up with OnResetDialogState, which must not cancel that dialog.
254		h.wv.jsDialogs.armIgnoreReset()
255		if suppressMessage != nil {
256			*suppressMessage = 1
257		}
258		return 0
259	}
260	logging.FromContext(h.wv.ctx).Debug().Int32("type", dialogType).Msg("cef: js dialog requested")
261	h.wv.presentJSDialog(call, port.JSDialogRequest{
262		Type:          typ,
263		Origin:        originURL,
264		Message:       messageText,
265		DefaultPrompt: defaultPromptText,
266	}, ui)
267	return 1
268}
269
270// OnBeforeUnloadDialog implements purecef.JsdialogHandler.
271func (h *handlerSet) OnBeforeUnloadDialog(
272	_ purecef.Browser,
273	_ string, // CEF always passes a fixed string; the UI shows Dumber's own text.
274	isReload int32,
275	callback purecef.JsdialogCallback,
276) bool {
277	if h == nil || h.wv == nil || callback == nil {
278		return false
279	}
280	if h.wv.destroyed.Load() {
281		// CloseBrowser dispatches beforeunload; accept it instead of waiting
282		// on UI that may never run. CEF accepts the unload when we return false.
283		return false
284	}
285	ui := h.wv.jsDialogUICallbacks()
286	if ui == nil || ui.OnJSDialog == nil {
287		return false
288	}
289	call, begin := h.wv.jsDialogs.begin(callback)
290	if begin == jsDialogBeginClosed {
291		return false
292	}
293	if begin == jsDialogBeginBusy {
294		// Another dialog is open: stay on the page rather than stacking.
295		// The Cont below makes CEF send a reset that must not cancel it.
296		h.wv.jsDialogs.armIgnoreReset()
297		continueCEFJSDialog(callback, false, "")
298		return true
299	}
300	h.wv.presentJSDialog(call, port.JSDialogRequest{
301		Type:     port.JSDialogBeforeUnload,
302		Origin:   h.wv.URI(),
303		IsReload: isReload != 0,
304	}, ui)
305	return true
306}
307
308// OnResetDialogState implements purecef.JsdialogHandler: CEF canceled pending
309// dialogs (navigation, close). Close the UI and resolve the callback, unless
310// this is the echo of a suppress/reject/Cont (see file comment).
311//
312// The UI reset (OnJSDialogReset) carries no dialog identity: it hides whatever
313// the pane shows. That is safe because a pane shows at most one dialog and a
314// new one is only presented after the previous call was resolved.
315func (h *handlerSet) OnResetDialogState(_ purecef.Browser) {
316	if h == nil || h.wv == nil {
317		return
318	}
319	if h.wv.jsDialogs.consumeIgnoreReset() {
320		return
321	}
322	h.wv.cancelJSDialogs()
323}
324
325// OnDialogClosed implements purecef.JsdialogHandler. It intentionally does
326// nothing: resolve already cleared the pending state, and a late notification
327// must not clear a newer dialog.
328func (h *handlerSet) OnDialogClosed(_ purecef.Browser) {}

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