detached 107ed41

refactor: inline redundant wrapper functions for webutil calls

- Replace escapeForJSString() wrapper with direct webutil.EscapeForJSString() calls in content_injector.go and input_bridge.go
- Replace getMimeType() wrapper with direct webutil.GetMimeType() call in scheme_handler.go
commit
107ed41ed26ff0cb978cb9451d0f7d63ceeac7fa
author
brice · · 2026-04-05 06:15 UTC
parent
630eef1
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +12 -246
M internal/infrastructure/cef/content_injector.go +6 -68
@@ -4,10 +4,11 @@ import (
44 "context"
55 "fmt"
66 "net/url"
7- "os"
87 "strings"
98 "sync"
109
10+ "github.com/bnema/dumber/internal/infrastructure/webutil"
11+
1112 "github.com/bnema/dumber/internal/application/port"
1213 "github.com/bnema/dumber/internal/logging"
1314 )
@@ -430,11 +431,7 @@ func (ci *contentInjector) setColorResolver(resolver port.ColorSchemeResolver) {
430431 // newContentInjector creates a content injector wired to the given engine.
431432 // Video diagnostics are enabled when DUMBER_VIDEO_DIAG=1 is set.
432433 func newContentInjector(engine *Engine, resolver port.ColorSchemeResolver) *contentInjector {
433- diagEnabled := false
434- switch strings.ToLower(strings.TrimSpace(os.Getenv("DUMBER_VIDEO_DIAG"))) {
435- case "1", "true", "yes", "on":
436- diagEnabled = true
437- }
434+ diagEnabled := envBoolEnabled("DUMBER_VIDEO_DIAG")
438435 return &contentInjector{
439436 engine: engine,
440437 colorResolver: resolver,
@@ -550,8 +547,8 @@ func (ci *contentInjector) onLoadEnd(wv *WebView) {
550547
551548 // injectCSS injects a CSS string as a <style> element via JavaScript.
552549 func (ci *contentInjector) injectCSS(wv *WebView, id, css string) {
553- escapedID := escapeForJSString(id)
554- escaped := escapeForJSString(css)
550+ escapedID := webutil.EscapeForJSString(id)
551+ escaped := webutil.EscapeForJSString(css)
555552 script := fmt.Sprintf(`(function(){
556553 var el = document.getElementById('%s');
557554 if (!el) { el = document.createElement('style'); el.id = '%s'; document.head.appendChild(el); }
@@ -564,55 +561,7 @@ func (ci *contentInjector) injectCSS(wv *WebView, id, css string) {
564561 // injectDarkModeScript sets dark/light class on <html> and patches matchMedia
565562 // for prefers-color-scheme queries on internal pages.
566563 func (ci *contentInjector) injectDarkModeScript(wv *WebView, prefersDark bool) {
567- script := fmt.Sprintf(`(function() {
568- var prefersDark = %t;
569- window.__dumber_cef_prefers_dark = prefersDark;
570- var originalMatchMedia = window.matchMedia.bind(window);
571-
572- if (prefersDark) {
573- document.documentElement.classList.add('dark');
574- document.documentElement.classList.remove('light');
575- } else {
576- document.documentElement.classList.add('light');
577- document.documentElement.classList.remove('dark');
578- }
579-
580- function isColorSchemeQuery(query, scheme) {
581- if (typeof query !== 'string') return false;
582- var normalized = query.replace(/\s+/g, '').toLowerCase();
583- return normalized.indexOf('prefers-color-scheme:' + scheme) !== -1;
584- }
585-
586- function createFakeMediaQueryList(query, matches) {
587- var listeners = [];
588- var onchangeHandler = null;
589- return {
590- matches: matches,
591- media: query,
592- get onchange() { return onchangeHandler; },
593- set onchange(fn) { onchangeHandler = fn; },
594- addListener: function(cb) { if (typeof cb === 'function') listeners.push(cb); },
595- removeListener: function(cb) { var idx = listeners.indexOf(cb); if (idx !== -1) listeners.splice(idx, 1); },
596- addEventListener: function(type, cb) { if (type === 'change' && typeof cb === 'function') listeners.push(cb); },
597- removeEventListener: function(type, cb) {
598- if (type === 'change') { var idx = listeners.indexOf(cb); if (idx !== -1) listeners.splice(idx, 1); }
599- },
600- dispatchEvent: function(event) {
601- for (var i = 0; i < listeners.length; i++) { try { listeners[i](event); } catch (e) {} }
602- if (onchangeHandler) { try { onchangeHandler(event); } catch (e) {} }
603- return true;
604- }
605- };
606- }
607-
608- window.matchMedia = function(query) {
609- if (isColorSchemeQuery(query, 'dark')) return createFakeMediaQueryList(query, prefersDark);
610- if (isColorSchemeQuery(query, 'light')) return createFakeMediaQueryList(query, !prefersDark);
611- return originalMatchMedia(query);
612- };
613-})();`, prefersDark)
614-
615- wv.RunJavaScript(context.Background(), script)
564+ wv.RunJavaScript(context.Background(), webutil.DarkModeScript(prefersDark, "__dumber_cef_prefers_dark"))
616565 }
617566
618567 // injectMessageBridgeShim injects the window.dumber.postMessage JS client shim
@@ -622,14 +571,3 @@ func (ci *contentInjector) injectDarkModeScript(wv *WebView, prefersDark bool) {
622571 func (ci *contentInjector) injectMessageBridgeShim(wv *WebView) {
623572 wv.RunJavaScript(context.Background(), MessageBridgeJS)
624573 }
625-
626-// escapeForJSString escapes a string for use inside a JS single-quoted string literal.
627-func escapeForJSString(s string) string {
628- s = strings.ReplaceAll(s, "\\", "\\\\")
629- s = strings.ReplaceAll(s, "'", "\\'")
630- s = strings.ReplaceAll(s, "\n", "\\n")
631- s = strings.ReplaceAll(s, "\r", "\\r")
632- s = strings.ReplaceAll(s, "\u2028", "\\u2028")
633- s = strings.ReplaceAll(s, "\u2029", "\\u2029")
634- return s
635-}
@@ -4,10 +4,11 @@ import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6 "net/url"6 "net/url"
7- "os"
8 "strings"7 "strings"
9 "sync"8 "sync"
10 9
10+ "github.com/bnema/dumber/internal/infrastructure/webutil"
11+
11 "github.com/bnema/dumber/internal/application/port"12 "github.com/bnema/dumber/internal/application/port"
12 "github.com/bnema/dumber/internal/logging"13 "github.com/bnema/dumber/internal/logging"
13 )14 )
@@ -430,11 +431,7 @@ func (ci *contentInjector) setColorResolver(resolver port.ColorSchemeResolver) {
430 // newContentInjector creates a content injector wired to the given engine.431 // newContentInjector creates a content injector wired to the given engine.
431 // Video diagnostics are enabled when DUMBER_VIDEO_DIAG=1 is set.432 // Video diagnostics are enabled when DUMBER_VIDEO_DIAG=1 is set.
432 func newContentInjector(engine *Engine, resolver port.ColorSchemeResolver) *contentInjector {433 func newContentInjector(engine *Engine, resolver port.ColorSchemeResolver) *contentInjector {
433- diagEnabled := false434+ diagEnabled := envBoolEnabled("DUMBER_VIDEO_DIAG")
434- switch strings.ToLower(strings.TrimSpace(os.Getenv("DUMBER_VIDEO_DIAG"))) {
435- case "1", "true", "yes", "on":
436- diagEnabled = true
437- }
438 return &contentInjector{435 return &contentInjector{
439 engine: engine,436 engine: engine,
440 colorResolver: resolver,437 colorResolver: resolver,
@@ -550,8 +547,8 @@ func (ci *contentInjector) onLoadEnd(wv *WebView) {
550 547
551 // injectCSS injects a CSS string as a <style> element via JavaScript.548 // injectCSS injects a CSS string as a <style> element via JavaScript.
552 func (ci *contentInjector) injectCSS(wv *WebView, id, css string) {549 func (ci *contentInjector) injectCSS(wv *WebView, id, css string) {
553- escapedID := escapeForJSString(id)550+ escapedID := webutil.EscapeForJSString(id)
554- escaped := escapeForJSString(css)551+ escaped := webutil.EscapeForJSString(css)
555 script := fmt.Sprintf(`(function(){552 script := fmt.Sprintf(`(function(){
556 var el = document.getElementById('%s');553 var el = document.getElementById('%s');
557 if (!el) { el = document.createElement('style'); el.id = '%s'; document.head.appendChild(el); }554 if (!el) { el = document.createElement('style'); el.id = '%s'; document.head.appendChild(el); }
@@ -564,55 +561,7 @@ func (ci *contentInjector) injectCSS(wv *WebView, id, css string) {
564 // injectDarkModeScript sets dark/light class on <html> and patches matchMedia561 // injectDarkModeScript sets dark/light class on <html> and patches matchMedia
565 // for prefers-color-scheme queries on internal pages.562 // for prefers-color-scheme queries on internal pages.
566 func (ci *contentInjector) injectDarkModeScript(wv *WebView, prefersDark bool) {563 func (ci *contentInjector) injectDarkModeScript(wv *WebView, prefersDark bool) {
567- script := fmt.Sprintf(`(function() {564+ wv.RunJavaScript(context.Background(), webutil.DarkModeScript(prefersDark, "__dumber_cef_prefers_dark"))
568- var prefersDark = %t;
569- window.__dumber_cef_prefers_dark = prefersDark;
570- var originalMatchMedia = window.matchMedia.bind(window);
571-
572- if (prefersDark) {
573- document.documentElement.classList.add('dark');
574- document.documentElement.classList.remove('light');
575- } else {
576- document.documentElement.classList.add('light');
577- document.documentElement.classList.remove('dark');
578- }
579-
580- function isColorSchemeQuery(query, scheme) {
581- if (typeof query !== 'string') return false;
582- var normalized = query.replace(/\s+/g, '').toLowerCase();
583- return normalized.indexOf('prefers-color-scheme:' + scheme) !== -1;
584- }
585-
586- function createFakeMediaQueryList(query, matches) {
587- var listeners = [];
588- var onchangeHandler = null;
589- return {
590- matches: matches,
591- media: query,
592- get onchange() { return onchangeHandler; },
593- set onchange(fn) { onchangeHandler = fn; },
594- addListener: function(cb) { if (typeof cb === 'function') listeners.push(cb); },
595- removeListener: function(cb) { var idx = listeners.indexOf(cb); if (idx !== -1) listeners.splice(idx, 1); },
596- addEventListener: function(type, cb) { if (type === 'change' && typeof cb === 'function') listeners.push(cb); },
597- removeEventListener: function(type, cb) {
598- if (type === 'change') { var idx = listeners.indexOf(cb); if (idx !== -1) listeners.splice(idx, 1); }
599- },
600- dispatchEvent: function(event) {
601- for (var i = 0; i < listeners.length; i++) { try { listeners[i](event); } catch (e) {} }
602- if (onchangeHandler) { try { onchangeHandler(event); } catch (e) {} }
603- return true;
604- }
605- };
606- }
607-
608- window.matchMedia = function(query) {
609- if (isColorSchemeQuery(query, 'dark')) return createFakeMediaQueryList(query, prefersDark);
610- if (isColorSchemeQuery(query, 'light')) return createFakeMediaQueryList(query, !prefersDark);
611- return originalMatchMedia(query);
612- };
613-})();`, prefersDark)
614-
615- wv.RunJavaScript(context.Background(), script)
616 }565 }
617 566
618 // injectMessageBridgeShim injects the window.dumber.postMessage JS client shim567 // injectMessageBridgeShim injects the window.dumber.postMessage JS client shim
@@ -622,14 +571,3 @@ func (ci *contentInjector) injectDarkModeScript(wv *WebView, prefersDark bool) {
622 func (ci *contentInjector) injectMessageBridgeShim(wv *WebView) {571 func (ci *contentInjector) injectMessageBridgeShim(wv *WebView) {
623 wv.RunJavaScript(context.Background(), MessageBridgeJS)572 wv.RunJavaScript(context.Background(), MessageBridgeJS)
624 }573 }
625-
626-// escapeForJSString escapes a string for use inside a JS single-quoted string literal.
627-func escapeForJSString(s string) string {
628- s = strings.ReplaceAll(s, "\\", "\\\\")
629- s = strings.ReplaceAll(s, "'", "\\'")
630- s = strings.ReplaceAll(s, "\n", "\\n")
631- s = strings.ReplaceAll(s, "\r", "\\r")
632- s = strings.ReplaceAll(s, "\u2028", "\\u2028")
633- s = strings.ReplaceAll(s, "\u2029", "\\u2029")
634- return s
635-}
M internal/infrastructure/cef/input_bridge.go +2 -3
@@ -9,6 +9,7 @@ import (
99 "github.com/bnema/puregotk/v4/gio"
1010 "github.com/bnema/puregotk/v4/gtk"
1111
12+ "github.com/bnema/dumber/internal/infrastructure/webutil"
1213 "github.com/bnema/dumber/internal/logging"
1314 )
1415
@@ -464,7 +465,7 @@ func (ib *inputBridge) pasteFromClipboard() {
464465 }
465466
466467 // Escape the text for embedding in a JS string literal.
467- escaped := escapeForJSString(text)
468+ escaped := webutil.EscapeForJSString(text)
468469 js := "document.execCommand('insertText',false,'" + escaped + "')"
469470 frame.ExecuteJavaScript(js, "", 0)
470471 log.Debug().Msg("cef: paste JS executed")
@@ -474,8 +475,6 @@ func (ib *inputBridge) pasteFromClipboard() {
474475 log.Debug().Msg("cef: ReadTextAsync dispatched")
475476 }
476477
477-// escapeForJSString is defined in content_injector.go — reused here.
478-
479478 // ---------------------------------------------------------------------------
480479 // Translation helpers
481480 // ---------------------------------------------------------------------------
@@ -9,6 +9,7 @@ import (
9 "github.com/bnema/puregotk/v4/gio"9 "github.com/bnema/puregotk/v4/gio"
10 "github.com/bnema/puregotk/v4/gtk"10 "github.com/bnema/puregotk/v4/gtk"
11 11
12+ "github.com/bnema/dumber/internal/infrastructure/webutil"
12 "github.com/bnema/dumber/internal/logging"13 "github.com/bnema/dumber/internal/logging"
13 )14 )
14 15
@@ -464,7 +465,7 @@ func (ib *inputBridge) pasteFromClipboard() {
464 }465 }
465 466
466 // Escape the text for embedding in a JS string literal.467 // Escape the text for embedding in a JS string literal.
467- escaped := escapeForJSString(text)468+ escaped := webutil.EscapeForJSString(text)
468 js := "document.execCommand('insertText',false,'" + escaped + "')"469 js := "document.execCommand('insertText',false,'" + escaped + "')"
469 frame.ExecuteJavaScript(js, "", 0)470 frame.ExecuteJavaScript(js, "", 0)
470 log.Debug().Msg("cef: paste JS executed")471 log.Debug().Msg("cef: paste JS executed")
@@ -474,8 +475,6 @@ func (ib *inputBridge) pasteFromClipboard() {
474 log.Debug().Msg("cef: ReadTextAsync dispatched")475 log.Debug().Msg("cef: ReadTextAsync dispatched")
475 }476 }
476 477
477-// escapeForJSString is defined in content_injector.go — reused here.
478-
479 // ---------------------------------------------------------------------------478 // ---------------------------------------------------------------------------
480 // Translation helpers479 // Translation helpers
481 // ---------------------------------------------------------------------------480 // ---------------------------------------------------------------------------
M internal/infrastructure/webkit/scheme_handler.go +4 -175
@@ -5,9 +5,7 @@ import (
55 "embed"
66 "encoding/json"
77 "fmt"
8- "html"
98 "io/fs"
10- "mime"
119 "net/http"
1210 "net/url"
1311 "path/filepath"
@@ -17,6 +15,7 @@ import (
1715 "github.com/bnema/dumber/internal/application/port"
1816 "github.com/bnema/dumber/internal/infrastructure/config"
1917 "github.com/bnema/dumber/internal/infrastructure/env"
18+ "github.com/bnema/dumber/internal/infrastructure/webutil"
2019 "github.com/bnema/dumber/internal/logging"
2120 "github.com/bnema/puregotk-webkit/soup"
2221 "github.com/bnema/puregotk-webkit/webkit"
@@ -155,131 +154,11 @@ func crashOriginalURI(requestURI string) string {
155154 }
156155
157156 func sanitizeCrashPageOriginalURI(originalURI string) string {
158- originalURI = strings.TrimSpace(originalURI)
159- if originalURI == "" {
160- return ""
161- }
162- parsed, err := url.Parse(originalURI)
163- if err != nil {
164- return ""
165- }
166- switch strings.ToLower(parsed.Scheme) {
167- case "http", "https":
168- if parsed.Host == "" {
169- return ""
170- }
171- return parsed.String()
172- case "dumb":
173- if parsed.Host == "" && parsed.Opaque == "" {
174- return ""
175- }
176- return parsed.String()
177- default:
178- return ""
179- }
157+ return webutil.SanitizeCrashPageOriginalURI(originalURI)
180158 }
181159
182160 func buildCrashPageHTML(originalURI string) string {
183- escapedURI := html.EscapeString(originalURI)
184- return fmt.Sprintf(`<!DOCTYPE html>
185-<html lang="en">
186-<head>
187- <meta charset="utf-8">
188- <meta name="viewport" content="width=device-width, initial-scale=1">
189- <title>Renderer crashed</title>
190- <style>
191- :root {
192- color-scheme: dark;
193- font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
194- }
195- body {
196- margin: 0;
197- min-height: 100vh;
198- display: flex;
199- align-items: center;
200- justify-content: center;
201- background: radial-gradient(circle at top, #253447, #101622 55%%);
202- color: #f2f6fa;
203- padding: 24px;
204- }
205- .card {
206- width: min(640px, 100%%);
207- background: rgba(10, 16, 26, 0.86);
208- border: 1px solid rgba(144, 173, 205, 0.35);
209- border-radius: 16px;
210- box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
211- padding: 28px;
212- }
213- h1 {
214- margin: 0 0 12px;
215- font-size: 1.8rem;
216- }
217- p {
218- margin: 0 0 16px;
219- line-height: 1.5;
220- color: #c8d6e6;
221- }
222- .url {
223- margin: 0 0 20px;
224- padding: 12px;
225- border-radius: 10px;
226- background: rgba(26, 38, 56, 0.85);
227- border: 1px solid rgba(139, 167, 194, 0.28);
228- font-family: "IBM Plex Mono", "Fira Code", monospace;
229- font-size: 0.92rem;
230- overflow-wrap: anywhere;
231- }
232- .actions {
233- display: flex;
234- gap: 12px;
235- flex-wrap: wrap;
236- }
237- button {
238- border: 0;
239- border-radius: 10px;
240- padding: 10px 16px;
241- cursor: pointer;
242- font-size: 0.95rem;
243- font-weight: 600;
244- }
245- .primary {
246- background: #4dd0e1;
247- color: #061018;
248- }
249- .secondary {
250- background: #233346;
251- color: #d6e5f5;
252- }
253- </style>
254-</head>
255-<body>
256- <div class="card">
257- <h1>Renderer process ended</h1>
258- <p>The current page was interrupted. You can reload it to continue browsing.</p>
259- <div class="url">%s</div>
260- <div class="actions">
261- <button class="primary" id="reload-btn" data-target="%s">Reload page</button>
262- <button class="secondary" id="stay-btn">Stay on this page</button>
263- </div>
264- </div>
265- <script>
266- const reloadButton = document.getElementById('reload-btn');
267- const targetUrl = (reloadButton.getAttribute('data-target') || '').trim();
268- reloadButton.addEventListener('click', function() {
269- if (targetUrl) {
270- window.location.href = targetUrl;
271- return;
272- }
273- window.location.reload();
274- });
275- // "Stay on this page" keeps the crash page visible without navigating away.
276- document.getElementById('stay-btn').addEventListener('click', function() {
277- this.disabled = true;
278- this.textContent = 'Staying on page';
279- });
280- </script>
281-</body>
282-</html>`, escapedURI, escapedURI)
161+ return webutil.BuildCrashPageHTML(originalURI)
283162 }
284163
285164 func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeResponse {
@@ -420,7 +299,7 @@ func (h *DumbSchemeHandler) handleAsset(u *url.URL) *SchemeResponse {
420299 return nil
421300 }
422301
423- contentType := h.getMimeType(relPath)
302+ contentType := webutil.GetMimeType(relPath)
424303 h.logger.Debug().
425304 Str("path", fullPath).
426305 Str("content_type", contentType).
@@ -468,56 +347,6 @@ func resolveAssetPath(u *url.URL) (string, bool) {
468347 }
469348 }
470349
471-// getMimeType determines the MIME type for a given file path.
472-func (h *DumbSchemeHandler) getMimeType(filename string) string {
473- if h == nil {
474- return "application/octet-stream"
475- }
476- ext := strings.ToLower(filepath.Ext(filename))
477-
478- // Try standard mime type first
479- mt := mime.TypeByExtension(ext)
480- if mt != "" {
481- return mt
482- }
483-
484- // Fallbacks for common web assets
485- switch ext {
486- case ".js", ".mjs":
487- return "application/javascript"
488- case ".css":
489- return "text/css"
490- case ".svg":
491- return "image/svg+xml"
492- case ".ico":
493- return "image/x-icon"
494- case ".woff":
495- return "font/woff"
496- case ".woff2":
497- return "font/woff2"
498- case ".ttf":
499- return "font/ttf"
500- case ".otf":
501- return "font/otf"
502- case ".png":
503- return "image/png"
504- case ".jpg", ".jpeg":
505- return "image/jpeg"
506- case ".gif":
507- return "image/gif"
508- case ".webp":
509- return "image/webp"
510- case ".json":
511- return "application/json"
512- case ".xml":
513- return "application/xml"
514- case ".html", ".htm":
515- return "text/html; charset=utf-8"
516- default:
517- return "text/plain"
518- }
519-}
520-
521350 // sendResponse sends the response back to WebKit.
522351 func (h *DumbSchemeHandler) sendResponse(req *webkit.URISchemeRequest, response *SchemeResponse) {
523352 if response == nil {
@@ -5,9 +5,7 @@ import (
5 "embed"5 "embed"
6 "encoding/json"6 "encoding/json"
7 "fmt"7 "fmt"
8- "html"
9 "io/fs"8 "io/fs"
10- "mime"
11 "net/http"9 "net/http"
12 "net/url"10 "net/url"
13 "path/filepath"11 "path/filepath"
@@ -17,6 +15,7 @@ import (
17 "github.com/bnema/dumber/internal/application/port"15 "github.com/bnema/dumber/internal/application/port"
18 "github.com/bnema/dumber/internal/infrastructure/config"16 "github.com/bnema/dumber/internal/infrastructure/config"
19 "github.com/bnema/dumber/internal/infrastructure/env"17 "github.com/bnema/dumber/internal/infrastructure/env"
18+ "github.com/bnema/dumber/internal/infrastructure/webutil"
20 "github.com/bnema/dumber/internal/logging"19 "github.com/bnema/dumber/internal/logging"
21 "github.com/bnema/puregotk-webkit/soup"20 "github.com/bnema/puregotk-webkit/soup"
22 "github.com/bnema/puregotk-webkit/webkit"21 "github.com/bnema/puregotk-webkit/webkit"
@@ -155,131 +154,11 @@ func crashOriginalURI(requestURI string) string {
155 }154 }
156 155
157 func sanitizeCrashPageOriginalURI(originalURI string) string {156 func sanitizeCrashPageOriginalURI(originalURI string) string {
158- originalURI = strings.TrimSpace(originalURI)157+ return webutil.SanitizeCrashPageOriginalURI(originalURI)
159- if originalURI == "" {
160- return ""
161- }
162- parsed, err := url.Parse(originalURI)
163- if err != nil {
164- return ""
165- }
166- switch strings.ToLower(parsed.Scheme) {
167- case "http", "https":
168- if parsed.Host == "" {
169- return ""
170- }
171- return parsed.String()
172- case "dumb":
173- if parsed.Host == "" && parsed.Opaque == "" {
174- return ""
175- }
176- return parsed.String()
177- default:
178- return ""
179- }
180 }158 }
181 159
182 func buildCrashPageHTML(originalURI string) string {160 func buildCrashPageHTML(originalURI string) string {
183- escapedURI := html.EscapeString(originalURI)161+ return webutil.BuildCrashPageHTML(originalURI)
184- return fmt.Sprintf(`<!DOCTYPE html>
185-<html lang="en">
186-<head>
187- <meta charset="utf-8">
188- <meta name="viewport" content="width=device-width, initial-scale=1">
189- <title>Renderer crashed</title>
190- <style>
191- :root {
192- color-scheme: dark;
193- font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
194- }
195- body {
196- margin: 0;
197- min-height: 100vh;
198- display: flex;
199- align-items: center;
200- justify-content: center;
201- background: radial-gradient(circle at top, #253447, #101622 55%%);
202- color: #f2f6fa;
203- padding: 24px;
204- }
205- .card {
206- width: min(640px, 100%%);
207- background: rgba(10, 16, 26, 0.86);
208- border: 1px solid rgba(144, 173, 205, 0.35);
209- border-radius: 16px;
210- box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
211- padding: 28px;
212- }
213- h1 {
214- margin: 0 0 12px;
215- font-size: 1.8rem;
216- }
217- p {
218- margin: 0 0 16px;
219- line-height: 1.5;
220- color: #c8d6e6;
221- }
222- .url {
223- margin: 0 0 20px;
224- padding: 12px;
225- border-radius: 10px;
226- background: rgba(26, 38, 56, 0.85);
227- border: 1px solid rgba(139, 167, 194, 0.28);
228- font-family: "IBM Plex Mono", "Fira Code", monospace;
229- font-size: 0.92rem;
230- overflow-wrap: anywhere;
231- }
232- .actions {
233- display: flex;
234- gap: 12px;
235- flex-wrap: wrap;
236- }
237- button {
238- border: 0;
239- border-radius: 10px;
240- padding: 10px 16px;
241- cursor: pointer;
242- font-size: 0.95rem;
243- font-weight: 600;
244- }
245- .primary {
246- background: #4dd0e1;
247- color: #061018;
248- }
249- .secondary {
250- background: #233346;
251- color: #d6e5f5;
252- }
253- </style>
254-</head>
255-<body>
256- <div class="card">
257- <h1>Renderer process ended</h1>
258- <p>The current page was interrupted. You can reload it to continue browsing.</p>
259- <div class="url">%s</div>
260- <div class="actions">
261- <button class="primary" id="reload-btn" data-target="%s">Reload page</button>
262- <button class="secondary" id="stay-btn">Stay on this page</button>
263- </div>
264- </div>
265- <script>
266- const reloadButton = document.getElementById('reload-btn');
267- const targetUrl = (reloadButton.getAttribute('data-target') || '').trim();
268- reloadButton.addEventListener('click', function() {
269- if (targetUrl) {
270- window.location.href = targetUrl;
271- return;
272- }
273- window.location.reload();
274- });
275- // "Stay on this page" keeps the crash page visible without navigating away.
276- document.getElementById('stay-btn').addEventListener('click', function() {
277- this.disabled = true;
278- this.textContent = 'Staying on page';
279- });
280- </script>
281-</body>
282-</html>`, escapedURI, escapedURI)
283 }162 }
284 163
285 func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeResponse {164 func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeResponse {
@@ -420,7 +299,7 @@ func (h *DumbSchemeHandler) handleAsset(u *url.URL) *SchemeResponse {
420 return nil299 return nil
421 }300 }
422 301
423- contentType := h.getMimeType(relPath)302+ contentType := webutil.GetMimeType(relPath)
424 h.logger.Debug().303 h.logger.Debug().
425 Str("path", fullPath).304 Str("path", fullPath).
426 Str("content_type", contentType).305 Str("content_type", contentType).
@@ -468,56 +347,6 @@ func resolveAssetPath(u *url.URL) (string, bool) {
468 }347 }
469 }348 }
470 349
471-// getMimeType determines the MIME type for a given file path.
472-func (h *DumbSchemeHandler) getMimeType(filename string) string {
473- if h == nil {
474- return "application/octet-stream"
475- }
476- ext := strings.ToLower(filepath.Ext(filename))
477-
478- // Try standard mime type first
479- mt := mime.TypeByExtension(ext)
480- if mt != "" {
481- return mt
482- }
483-
484- // Fallbacks for common web assets
485- switch ext {
486- case ".js", ".mjs":
487- return "application/javascript"
488- case ".css":
489- return "text/css"
490- case ".svg":
491- return "image/svg+xml"
492- case ".ico":
493- return "image/x-icon"
494- case ".woff":
495- return "font/woff"
496- case ".woff2":
497- return "font/woff2"
498- case ".ttf":
499- return "font/ttf"
500- case ".otf":
501- return "font/otf"
502- case ".png":
503- return "image/png"
504- case ".jpg", ".jpeg":
505- return "image/jpeg"
506- case ".gif":
507- return "image/gif"
508- case ".webp":
509- return "image/webp"
510- case ".json":
511- return "application/json"
512- case ".xml":
513- return "application/xml"
514- case ".html", ".htm":
515- return "text/html; charset=utf-8"
516- default:
517- return "text/plain"
518- }
519-}
520-
521 // sendResponse sends the response back to WebKit.350 // sendResponse sends the response back to WebKit.
522 func (h *DumbSchemeHandler) sendResponse(req *webkit.URISchemeRequest, response *SchemeResponse) {351 func (h *DumbSchemeHandler) sendResponse(req *webkit.URISchemeRequest, response *SchemeResponse) {
523 if response == nil {352 if response == nil {

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