bridge_script_templates.go RawHistoryFolder

1package cef
2
3import (
4	"strings"
5
6	"github.com/bnema/dumber/internal/infrastructure/webutil"
7)
8
9// trustedPageFetchBridgeBaseJS is the invariant fetch-bridge bundle injected on
10// external pages. It is assembled once from shared templates and then only the
11// bridge nonce placeholder is replaced per navigation.
12var trustedPageFetchBridgeBaseJS = buildTrustedPageFetchBridgeBaseJS()
13
14// rendererBridgeExtensionJS is assembled once for renderer-process bridge helpers.
15var rendererBridgeExtensionJS = buildRendererBridgeExtensionJS()
16
17func buildTrustedPageFetchBridgeBaseJS() string {
18	return clipboardSelectionFetchBridgeJSTemplate
19}
20
21const bridgeNoncePlaceholder = "__DUMBER_BRIDGE_NONCE__"
22
23func buildTrustedPageFetchBridgeJS(bridgeNonce string) string {
24	if strings.Contains(bridgeNonce, bridgeNoncePlaceholder) {
25		panic("bridge nonce contains reserved placeholder")
26	}
27	return strings.ReplaceAll(trustedPageFetchBridgeBaseJS, bridgeNoncePlaceholder, bridgeNonce)
28}
29
30func buildRendererBridgeExtensionJS() string {
31	return strings.ReplaceAll(rendererBridgeExtensionTemplateJS, "__DUMBER_POPUP_PROXY_BRIDGE_BODY__", "")
32}
33
34func buildPopupOpenerBridgeJS(bridgeNonce, parentURI string) string {
35	replacer := strings.NewReplacer(
36		"__DUMBER_BRIDGE_NONCE__", webutil.EscapeForJSString(bridgeNonce),
37		"__DUMBER_PARENT_URI__", webutil.EscapeForJSString(parentURI),
38	)
39	return replacer.Replace(popupOpenerBridgeTemplateJS)
40}
41
42// clipboardSelectionFetchBridgeJSTemplate keeps the lightweight fetch bridge
43// enabled while the native renderer bridge remains disabled due to an OSR
44// startup regression. It mirrors explicit clipboard text from DOM selections,
45// safe input/textarea selections, and async Clipboard API writes, and also
46// reasserts browser focus when an editable element gains DOM focus.
47const clipboardSelectionFetchBridgeJSTemplate = `(function(){
48  var bridgeNonce = '__DUMBER_BRIDGE_NONCE__';
49
50  function encodeBody(body) {
51    return typeof btoa === 'function' ? btoa(unescape(encodeURIComponent(body))) : '';
52  }
53
54  function inputType(node) {
55    return node && node.type ? String(node.type).toLowerCase() : 'text';
56  }
57
58  function isTextEditableInputType(type) {
59    switch ((type || 'text').toLowerCase()) {
60    case 'button':
61    case 'submit':
62    case 'reset':
63    case 'checkbox':
64    case 'radio':
65    case 'range':
66    case 'color':
67    case 'file':
68    case 'image':
69    case 'hidden':
70      return false;
71    default:
72      return true;
73    }
74  }
75
76  function isSelectionReadableInputType(type) {
77    return isTextEditableInputType(type) && type !== 'password';
78  }
79
80  function getActiveElementSelection() {
81    var el = document.activeElement;
82    if (!el || el.nodeType !== 1 || typeof el.value !== 'string') return '';
83    var tag = el.tagName;
84    if (el.disabled || el.readOnly) return '';
85    if (tag === 'INPUT' && !isSelectionReadableInputType(inputType(el))) return '';
86    if (tag !== 'INPUT' && tag !== 'TEXTAREA') return '';
87    var start = typeof el.selectionStart === 'number' ? el.selectionStart : 0;
88    var end = typeof el.selectionEnd === 'number' ? el.selectionEnd : 0;
89    if (end > start) return el.value.slice(start, end);
90    return '';
91  }
92
93  function getSelectedText() {
94    var activeSelection = getActiveElementSelection();
95    if (activeSelection) return activeSelection;
96    var sel = window.getSelection();
97    return sel ? sel.toString() : '';
98  }
99
100  function isEditable(node) {
101    if (!node || node.nodeType !== 1) return false;
102    if (node.isContentEditable) return true;
103    var tag = node.tagName;
104    if (tag === 'TEXTAREA') return !node.disabled && !node.readOnly;
105    if (tag !== 'INPUT') return false;
106    if (node.disabled || node.readOnly) return false;
107    return isTextEditableInputType(inputType(node));
108  }
109
110  function sendToClipboard(text) {
111    if (!text) return;
112    var body = JSON.stringify({text: text});
113    var encoded = encodeBody(body);
114    if (!encoded) return;
115    fetch('dumb:///api/clipboard-set', {
116      method: 'POST',
117      headers: {
118        'X-Dumber-Body': encoded,
119        'X-Dumber-Bridge-Nonce': bridgeNonce
120      }
121    }).catch(function(){});
122  }
123
124  function sendFocusSync() {
125    fetch('dumb:///api/focus-sync', {
126      method: 'POST',
127      headers: {
128        'X-Dumber-Bridge-Action': 'focus-sync',
129        'X-Dumber-Bridge-Nonce': bridgeNonce
130      }
131    }).catch(function(){});
132  }
133
134  function mirrorClipboardItems(items) {
135    try {
136      if (!items || typeof items.length !== 'number') return;
137      Array.prototype.forEach.call(items, function(item) {
138        if (!item || !item.types || item.types.indexOf('text/plain') === -1 || typeof item.getType !== 'function') return;
139        Promise.resolve(item.getType('text/plain'))
140          .then(function(blob) {
141            if (!blob || typeof blob.text !== 'function') return '';
142            return blob.text();
143          })
144          .then(function(text) {
145            if (text) sendToClipboard(text);
146          })
147          .catch(function(){});
148      });
149    } catch (_) {}
150  }
151
152  document.addEventListener('copy', function(event) {
153    if (event && event.isTrusted === false) return;
154    var text = getSelectedText();
155    if (text) sendToClipboard(text);
156  });
157  document.addEventListener('cut', function(event) {
158    if (event && event.isTrusted === false) return;
159    var text = getSelectedText();
160    if (text) sendToClipboard(text);
161  });
162  document.addEventListener('focusin', function(event) {
163    if (event && event.isTrusted === false) return;
164    if (isEditable(event && event.target)) sendFocusSync();
165  }, true);
166
167  try {
168    var clipboardObj = navigator && navigator.clipboard ? navigator.clipboard : null;
169    var clipboardProto = (typeof Clipboard !== 'undefined' && Clipboard.prototype) ||
170      (clipboardObj && typeof Object.getPrototypeOf === 'function' ? Object.getPrototypeOf(clipboardObj) : null);
171
172    if (clipboardProto && typeof clipboardProto.writeText === 'function' && !clipboardProto.__dumberWriteTextPatched) {
173      clipboardProto.__dumberWriteTextPatched = true;
174      var originalProtoWriteText = clipboardProto.writeText;
175      var wrappedProtoWriteText = function(text) {
176        var normalized = text == null ? '' : String(text);
177        var result = originalProtoWriteText.call(this, normalized);
178        if (result && typeof result.then === 'function') {
179          return result.then(function(value) {
180            if (normalized) sendToClipboard(normalized);
181            return value;
182          });
183        }
184        if (normalized) sendToClipboard(normalized);
185        return result;
186      };
187      try {
188        Object.defineProperty(clipboardProto, 'writeText', {
189          configurable: true,
190          writable: true,
191          value: wrappedProtoWriteText
192        });
193      } catch (_) {
194        clipboardProto.writeText = wrappedProtoWriteText;
195      }
196    } else if (clipboardObj && typeof clipboardObj.writeText === 'function' && !window.__dumberClipboardWriteTextPatched) {
197      window.__dumberClipboardWriteTextPatched = true;
198      var originalWriteText = clipboardObj.writeText.bind(clipboardObj);
199      var wrappedWriteText = function(text) {
200        var normalized = text == null ? '' : String(text);
201        var result = originalWriteText(normalized);
202        if (result && typeof result.then === 'function') {
203          return result.then(function(value) {
204            if (normalized) sendToClipboard(normalized);
205            return value;
206          });
207        }
208        if (normalized) sendToClipboard(normalized);
209        return result;
210      };
211      try {
212        Object.defineProperty(clipboardObj, 'writeText', {
213          configurable: true,
214          writable: true,
215          value: wrappedWriteText
216        });
217      } catch (_) {
218        clipboardObj.writeText = wrappedWriteText;
219      }
220    }
221
222    if (clipboardProto && typeof clipboardProto.write === 'function' && !clipboardProto.__dumberWritePatched) {
223      clipboardProto.__dumberWritePatched = true;
224      var originalProtoWrite = clipboardProto.write;
225      var wrappedProtoWrite = function(items) {
226        var result = originalProtoWrite.apply(this, arguments);
227        if (result && typeof result.then === 'function') {
228          return result.then(function(value) {
229            mirrorClipboardItems(items);
230            return value;
231          });
232        }
233        mirrorClipboardItems(items);
234        return result;
235      };
236      try {
237        Object.defineProperty(clipboardProto, 'write', {
238          configurable: true,
239          writable: true,
240          value: wrappedProtoWrite
241        });
242      } catch (_) {
243        clipboardProto.write = wrappedProtoWrite;
244      }
245    } else if (clipboardObj && typeof clipboardObj.write === 'function' && !window.__dumberClipboardWritePatched) {
246      window.__dumberClipboardWritePatched = true;
247      var originalWrite = clipboardObj.write.bind(clipboardObj);
248      var wrappedWrite = function(items) {
249        var result = originalWrite(items);
250        if (result && typeof result.then === 'function') {
251          return result.then(function(value) {
252            mirrorClipboardItems(items);
253            return value;
254          });
255        }
256        mirrorClipboardItems(items);
257        return result;
258      };
259      try {
260        Object.defineProperty(clipboardObj, 'write', {
261          configurable: true,
262          writable: true,
263          value: wrappedWrite
264        });
265      } catch (_) {
266        clipboardObj.write = wrappedWrite;
267      }
268    }
269  } catch (_) {}
270
271  if (isEditable(document.activeElement)) sendFocusSync();
272})();`
273
274const rendererBridgeExtensionTemplateJS = `
275(function() {
276  native function Dispatch(action, payload);
277
278  function send(action, payload) {
279    return Dispatch(action, payload == null ? '' : String(payload));
280  }
281
282  function isEditable(node) {
283    if (!node || node.nodeType !== 1) return false;
284    if (node.isContentEditable) return true;
285    var tag = node.tagName;
286    if (tag !== 'INPUT' && tag !== 'TEXTAREA') return false;
287    if (node.disabled || node.readOnly) return false;
288    return true;
289  }
290
291  function getActiveElementSelection() {
292    var el = document.activeElement;
293    if (!el) return '';
294    var tag = el.tagName;
295    if ((tag === 'INPUT' || tag === 'TEXTAREA') && typeof el.value === 'string') {
296      var start = typeof el.selectionStart === 'number' ? el.selectionStart : 0;
297      var end = typeof el.selectionEnd === 'number' ? el.selectionEnd : 0;
298      if (end > start) return el.value.slice(start, end);
299    }
300    return '';
301  }
302
303  function getSelectedText() {
304    var activeSelection = getActiveElementSelection();
305    if (activeSelection) return activeSelection;
306    var sel = window.getSelection();
307    return sel ? sel.toString() : '';
308  }
309
310  function notifyExplicitTextCopy(action, text) {
311    var normalizedText = text == null ? '' : String(text);
312    var normalizedAction = action == null || action === '' ? 'copy' : String(action);
313    return send('explicit_text_copy', JSON.stringify({ text: normalizedText, action: normalizedAction }));
314  }
315
316__DUMBER_POPUP_PROXY_BRIDGE_BODY__
317
318  if (typeof document !== 'undefined' && document.addEventListener) {
319    document.addEventListener('focusin', function(event) {
320      if (isEditable(event.target)) {
321        send('focus_sync', '');
322      }
323    }, true);
324
325    function mirrorClipboardEvent(action, e) {
326      if (!e.isTrusted) return;
327      var capturedText = '';
328      if (e.clipboardData && typeof e.clipboardData.getData === 'function') {
329        try { capturedText = e.clipboardData.getData('text/plain'); } catch(_) {}
330      }
331      setTimeout(function() {
332        var text = capturedText;
333        if (!text && !e.defaultPrevented) text = getSelectedText();
334        if (text) notifyExplicitTextCopy(action, text);
335      }, 0);
336    }
337
338    document.addEventListener('copy', function(e) {
339      mirrorClipboardEvent('copy', e);
340    }, true);
341    document.addEventListener('cut', function(e) {
342      mirrorClipboardEvent('cut', e);
343    }, true);
344  }
345  try {
346    var clipboardObj = navigator && navigator.clipboard ? navigator.clipboard : null;
347    var clipboardProto = (typeof Clipboard !== 'undefined' && Clipboard.prototype) ||
348      (clipboardObj && typeof Object.getPrototypeOf === 'function' ? Object.getPrototypeOf(clipboardObj) : null);
349
350    function mirrorClipboardItems(items) {
351      try {
352        if (!items || typeof items.length !== 'number') return;
353        Array.prototype.forEach.call(items, function(item) {
354          if (!item || !item.types || item.types.indexOf('text/plain') === -1 || typeof item.getType !== 'function') return;
355          Promise.resolve(item.getType('text/plain'))
356            .then(function(blob) {
357              if (!blob || typeof blob.text !== 'function') return '';
358              return blob.text();
359            })
360            .then(function(text) {
361              if (text) notifyExplicitTextCopy('copy', text);
362            })
363            .catch(function() {});
364        });
365      } catch (_) {}
366    }
367
368    if (clipboardProto && typeof clipboardProto.writeText === 'function' && !clipboardProto.__dumberWriteTextPatched) {
369      clipboardProto.__dumberWriteTextPatched = true;
370      var originalProtoWriteText = clipboardProto.writeText;
371      var wrappedProtoWriteText = function(text) {
372        var normalized = text == null ? '' : String(text);
373        var result = originalProtoWriteText.call(this, normalized);
374        if (result && typeof result.then === 'function') {
375          return result.then(function(value) {
376            if (normalized) notifyExplicitTextCopy('copy', normalized);
377            return value;
378          });
379        }
380        if (normalized) notifyExplicitTextCopy('copy', normalized);
381        return result;
382      };
383      try {
384        Object.defineProperty(clipboardProto, 'writeText', {
385          configurable: true,
386          writable: true,
387          value: wrappedProtoWriteText
388        });
389      } catch (_) {
390        clipboardProto.writeText = wrappedProtoWriteText;
391      }
392    } else if (clipboardObj && typeof clipboardObj.writeText === 'function' && !window.__dumberClipboardWriteTextPatched) {
393      window.__dumberClipboardWriteTextPatched = true;
394      var originalWriteText = clipboardObj.writeText.bind(clipboardObj);
395      var wrappedWriteText = function(text) {
396        var normalized = text == null ? '' : String(text);
397        var result = originalWriteText(normalized);
398        if (result && typeof result.then === 'function') {
399          return result.then(function(value) {
400            if (normalized) notifyExplicitTextCopy('copy', normalized);
401            return value;
402          });
403        }
404        if (normalized) notifyExplicitTextCopy('copy', normalized);
405        return result;
406      };
407      try {
408        Object.defineProperty(clipboardObj, 'writeText', {
409          configurable: true,
410          writable: true,
411          value: wrappedWriteText
412        });
413      } catch (_) {
414        clipboardObj.writeText = wrappedWriteText;
415      }
416    }
417
418    if (clipboardProto && typeof clipboardProto.write === 'function' && !clipboardProto.__dumberWritePatched) {
419      clipboardProto.__dumberWritePatched = true;
420      var originalProtoWrite = clipboardProto.write;
421      var wrappedProtoWrite = function(items) {
422        var result = originalProtoWrite.apply(this, arguments);
423        if (result && typeof result.then === 'function') {
424          return result.then(function(value) {
425            mirrorClipboardItems(items);
426            return value;
427          });
428        }
429        mirrorClipboardItems(items);
430        return result;
431      };
432      try {
433        Object.defineProperty(clipboardProto, 'write', {
434          configurable: true,
435          writable: true,
436          value: wrappedProtoWrite
437        });
438      } catch (_) {
439        clipboardProto.write = wrappedProtoWrite;
440      }
441    } else if (clipboardObj && typeof clipboardObj.write === 'function' && !window.__dumberClipboardWritePatched) {
442      window.__dumberClipboardWritePatched = true;
443      var originalWrite = clipboardObj.write.bind(clipboardObj);
444      var wrappedWrite = function(items) {
445        var result = originalWrite(items);
446        if (result && typeof result.then === 'function') {
447          return result.then(function(value) {
448            mirrorClipboardItems(items);
449            return value;
450          });
451        }
452        mirrorClipboardItems(items);
453        return result;
454      };
455      try {
456        Object.defineProperty(clipboardObj, 'write', {
457          configurable: true,
458          writable: true,
459          value: wrappedWrite
460        });
461      } catch (_) {
462        clipboardObj.write = wrappedWrite;
463      }
464    }
465  } catch (_) {}
466
467  send('bridge_ready', (typeof location !== 'undefined' && location && location.href) ? location.href : '');
468})();
469`
470
471const popupOpenerBridgeTemplateJS = `(function() {
472  if (typeof window === 'undefined') return;
473  if (window.__dumberPopupOpenerBridgeInstalled) return;
474  window.__dumberPopupOpenerBridgeInstalled = true;
475  if (window.opener != null) return;
476
477  var bridgeNonce = '__DUMBER_BRIDGE_NONCE__';
478  var openerHref = '__DUMBER_PARENT_URI__';
479
480  function postBridge(path, payload) {
481    try {
482      fetch('dumb:///api/' + path, {
483        method: 'POST',
484        headers: {
485          'Content-Type': 'application/json',
486          'X-Dumber-Body': btoa(unescape(encodeURIComponent(JSON.stringify(payload)))),
487          'X-Dumber-Bridge-Nonce': bridgeNonce
488        }
489      }).catch(function() {});
490    } catch (_) {}
491  }
492
493  function normalizeURLValue(rawURL) {
494    if (rawURL == null) return '';
495    try {
496      return String(rawURL);
497    } catch (_) {
498      return '';
499    }
500  }
501
502  function navigateOpener(nextURL) {
503    var rawURL = normalizeURLValue(nextURL);
504    if (rawURL !== '') openerHref = rawURL;
505    postBridge('popup-opener-navigate', { url: rawURL });
506    return openerHref;
507  }
508
509  function serializeMessage(value) {
510    try {
511      return { kind: 'json', value: JSON.stringify(value) };
512    } catch (_) {
513      return { kind: 'string', value: String(value) };
514    }
515  }
516
517  var locationProxy = {
518    assign: function(nextURL) { return navigateOpener(nextURL); },
519    replace: function(nextURL) { return navigateOpener(nextURL); },
520    toString: function() { return openerHref; }
521  };
522  try {
523    Object.defineProperty(locationProxy, 'href', {
524      configurable: true,
525      enumerable: true,
526      get: function() { return openerHref; },
527      set: function(nextURL) { navigateOpener(nextURL); }
528    });
529  } catch (_) {
530    locationProxy.href = openerHref;
531  }
532
533  var openerProxy = {
534    blur: function() { return undefined; },
535    close: function() { return undefined; },
536    focus: function() { return undefined; },
537    postMessage: function(message, targetOrigin) {
538      var serialized = serializeMessage(message);
539      postBridge('popup-opener-post-message', {
540        data: serialized.value,
541        data_kind: serialized.kind,
542        target_origin: targetOrigin == null ? '*' : String(targetOrigin),
543        source_origin: (typeof location !== 'undefined' && location && location.origin) ? location.origin : '',
544        source_href: (typeof location !== 'undefined' && location && location.href) ? location.href : ''
545      });
546      return undefined;
547    }
548  };
549  openerProxy.self = openerProxy;
550  openerProxy.window = openerProxy;
551  try {
552    Object.defineProperty(openerProxy, 'closed', {
553      configurable: true,
554      enumerable: true,
555      get: function() { return false; }
556    });
557  } catch (_) {
558    openerProxy.closed = false;
559  }
560  try {
561    Object.defineProperty(openerProxy, 'location', {
562      configurable: true,
563      enumerable: true,
564      get: function() { return locationProxy; },
565      set: function(nextURL) { navigateOpener(nextURL); }
566    });
567  } catch (_) {
568    openerProxy.location = locationProxy;
569  }
570
571  try {
572    Object.defineProperty(window, 'opener', {
573      configurable: true,
574      enumerable: true,
575      get: function() { return openerProxy; }
576    });
577  } catch (_) {
578    try { window.opener = openerProxy; } catch (_) {}
579  }
580})();`

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