detached bf08946

fix(scheme-handler): serve embedded webui assets with CORS support

commit
bf08946eef2a07647841d045975ad5e0d4d46e17
author
brice · · 2025-12-16 20:33 UTC
committer
brice · · 2025-12-16 20:27 UTC
parent
56a05c0
signature
unsigned
4 files · +187 -86
M assets/webui_embed.go +5 -13
@@ -1,17 +1,9 @@
11 package assets
22
3-import _ "embed"
3+import "embed"
44
5-// ColorSchemeScript handles dark mode detection in web pages.
6-// It patches window.matchMedia for prefers-color-scheme queries
7-// based on the __dumber_gtk_prefers_dark flag injected by Go.
5+// WebUIAssets contains the embedded webui build output (homepage and blocked pages).
6+// Used by the scheme handler to serve dumb://home and dumb://blocked.
87 //
9-//go:embed webui/color-scheme.js
10-var ColorSchemeScript string
11-
12-// Deprecated: These variables are kept for backwards compatibility during migration.
13-// They will be removed once all JS handlers are deprecated.
14-// The actual files may not exist - check before using.
15-var MainWorldScript string
16-var GUIScript string
17-var ComponentStyles string
8+//go:embed webui/*
9+var WebUIAssets embed.FS
@@ -1,17 +1,9 @@
1 package assets1 package assets
2 2
3-import _ "embed"3+import "embed"
4 4
5-// ColorSchemeScript handles dark mode detection in web pages.5+// WebUIAssets contains the embedded webui build output (homepage and blocked pages).
6-// It patches window.matchMedia for prefers-color-scheme queries6+// Used by the scheme handler to serve dumb://home and dumb://blocked.
7-// based on the __dumber_gtk_prefers_dark flag injected by Go.
8 //7 //
9-//go:embed webui/color-scheme.js8+//go:embed webui/*
10-var ColorSchemeScript string9+var WebUIAssets embed.FS
11-
12-// Deprecated: These variables are kept for backwards compatibility during migration.
13-// They will be removed once all JS handlers are deprecated.
14-// The actual files may not exist - check before using.
15-var MainWorldScript string
16-var GUIScript string
17-var ComponentStyles string
M cmd/dumber/main.go +7 -0
@@ -7,6 +7,7 @@ import (
77 "path/filepath"
88 "runtime"
99
10+ "github.com/bnema/dumber/assets"
1011 "github.com/bnema/dumber/internal/application/usecase"
1112 "github.com/bnema/dumber/internal/infrastructure/config"
1213 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
@@ -83,6 +84,12 @@ func main() {
8384 if err != nil {
8485 logger.Fatal().Err(err).Msg("failed to initialize WebKit context")
8586 }
87+
88+ // Register dumb:// scheme handler for serving embedded webui
89+ schemeHandler := webkit.NewDumbSchemeHandler(ctx)
90+ schemeHandler.SetAssets(assets.WebUIAssets)
91+ schemeHandler.RegisterWithContext(wkCtx)
92+
8693 settings := webkit.NewSettingsManager(ctx, cfg)
8794 injector := webkit.NewContentInjector(themeManager.PrefersDark())
8895 messageRouter := webkit.NewMessageRouter(ctx)
@@ -7,6 +7,7 @@ import (
7 "path/filepath"7 "path/filepath"
8 "runtime"8 "runtime"
9 9
10+ "github.com/bnema/dumber/assets"
10 "github.com/bnema/dumber/internal/application/usecase"11 "github.com/bnema/dumber/internal/application/usecase"
11 "github.com/bnema/dumber/internal/infrastructure/config"12 "github.com/bnema/dumber/internal/infrastructure/config"
12 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"13 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
@@ -83,6 +84,12 @@ func main() {
83 if err != nil {84 if err != nil {
84 logger.Fatal().Err(err).Msg("failed to initialize WebKit context")85 logger.Fatal().Err(err).Msg("failed to initialize WebKit context")
85 }86 }
87+
88+ // Register dumb:// scheme handler for serving embedded webui
89+ schemeHandler := webkit.NewDumbSchemeHandler(ctx)
90+ schemeHandler.SetAssets(assets.WebUIAssets)
91+ schemeHandler.RegisterWithContext(wkCtx)
92+
86 settings := webkit.NewSettingsManager(ctx, cfg)93 settings := webkit.NewSettingsManager(ctx, cfg)
87 injector := webkit.NewContentInjector(themeManager.PrefersDark())94 injector := webkit.NewContentInjector(themeManager.PrefersDark())
88 messageRouter := webkit.NewMessageRouter(ctx)95 messageRouter := webkit.NewMessageRouter(ctx)
M internal/infrastructure/webkit/scheme_handler.go +172 -70
@@ -2,17 +2,28 @@ package webkit
22
33 import (
44 "context"
5+ "embed"
6+ "io/fs"
7+ "mime"
58 "net/http"
9+ "net/url"
10+ "path/filepath"
611 "strings"
712 "sync"
813
914 "github.com/bnema/dumber/internal/logging"
1015 "github.com/bnema/puregotk-webkit/webkit"
1116 "github.com/jwijenbergh/puregotk/v4/gio"
12- "github.com/jwijenbergh/puregotk/v4/glib"
1317 "github.com/rs/zerolog"
1418 )
1519
20+// Scheme path constants
21+const (
22+ HomePath = "home"
23+ BlockedPath = "blocked"
24+ IndexHTML = "index.html"
25+)
26+
1627 // SchemeRequest represents a request to a custom URI scheme.
1728 type SchemeRequest struct {
1829 inner *webkit.URISchemeRequest
@@ -44,6 +55,8 @@ func (f PageHandlerFunc) Handle(req *SchemeRequest) *SchemeResponse {
4455 // DumbSchemeHandler handles dumb:// URI scheme requests.
4556 type DumbSchemeHandler struct {
4657 handlers map[string]PageHandler
58+ assets embed.FS
59+ assetDir string // subdirectory within embed.FS (e.g., "assets/webui")
4760 logger zerolog.Logger
4861 mu sync.RWMutex
4962 }
@@ -54,6 +67,7 @@ func NewDumbSchemeHandler(ctx context.Context) *DumbSchemeHandler {
5467
5568 h := &DumbSchemeHandler{
5669 handlers: make(map[string]PageHandler),
70+ assetDir: "webui",
5771 logger: log.With().Str("component", "scheme-handler").Logger(),
5872 }
5973
@@ -63,27 +77,17 @@ func NewDumbSchemeHandler(ctx context.Context) *DumbSchemeHandler {
6377 return h
6478 }
6579
80+// SetAssets sets the embedded filesystem containing webui assets.
81+func (h *DumbSchemeHandler) SetAssets(assets embed.FS) {
82+ h.mu.Lock()
83+ defer h.mu.Unlock()
84+ h.assets = assets
85+ h.logger.Debug().Msg("assets filesystem configured")
86+}
87+
6688 // registerDefaults sets up default page handlers.
6789 func (h *DumbSchemeHandler) registerDefaults() {
68- // New tab page
69- h.RegisterPage("/newtab", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse {
70- return &SchemeResponse{
71- Data: []byte(newTabHTML),
72- ContentType: "text/html",
73- StatusCode: http.StatusOK,
74- }
75- }))
76-
77- // Homepage (alias for newtab)
78- h.RegisterPage("/", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse {
79- return &SchemeResponse{
80- Data: []byte(newTabHTML),
81- ContentType: "text/html",
82- StatusCode: http.StatusOK,
83- }
84- }))
85-
86- // Error page
90+ // Error page (static fallback)
8791 h.RegisterPage("/error", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse {
8892 return &SchemeResponse{
8993 Data: []byte(errorPageHTML),
@@ -108,9 +112,10 @@ func (h *DumbSchemeHandler) HandleRequest(reqPtr uintptr) {
108112 return
109113 }
110114
115+ uri := req.GetUri()
111116 schemeReq := &SchemeRequest{
112117 inner: req,
113- URI: req.GetUri(),
118+ URI: uri,
114119 Path: req.GetPath(),
115120 Method: req.GetHttpMethod(),
116121 Scheme: req.GetScheme(),
@@ -122,11 +127,28 @@ func (h *DumbSchemeHandler) HandleRequest(reqPtr uintptr) {
122127 Str("method", schemeReq.Method).
123128 Msg("handling scheme request")
124129
125- // Look up handler
130+ // Parse the URI to extract host and path
131+ u, err := url.Parse(uri)
132+ if err != nil || u.Scheme != "dumb" {
133+ h.logger.Error().Err(err).Str("uri", uri).Msg("invalid URI")
134+ h.sendResponse(req, &SchemeResponse{
135+ Data: []byte("Invalid URI"),
136+ ContentType: "text/plain",
137+ StatusCode: http.StatusBadRequest,
138+ })
139+ return
140+ }
141+
142+ // Try to serve from embedded assets first
143+ if response := h.handleAsset(u); response != nil {
144+ h.sendResponse(req, response)
145+ return
146+ }
147+
148+ // Fall back to registered handlers
126149 h.mu.RLock()
127150 handler, ok := h.handlers[schemeReq.Path]
128151 if !ok {
129- // Try without leading slash
130152 handler, ok = h.handlers[strings.TrimPrefix(schemeReq.Path, "/")]
131153 }
132154 h.mu.RUnlock()
@@ -142,10 +164,119 @@ func (h *DumbSchemeHandler) HandleRequest(reqPtr uintptr) {
142164 }
143165 }
144166
145- // Send response
146167 h.sendResponse(req, response)
147168 }
148169
170+// handleAsset serves static assets from the embedded filesystem.
171+// Returns nil if no asset was found (allowing fallback to registered handlers).
172+func (h *DumbSchemeHandler) handleAsset(u *url.URL) *SchemeResponse {
173+ h.mu.RLock()
174+ hasAssets := h.assets != (embed.FS{})
175+ assetDir := h.assetDir
176+ h.mu.RUnlock()
177+
178+ if !hasAssets {
179+ return nil
180+ }
181+
182+ // Determine the target file based on host and path
183+ host := u.Host
184+ path := strings.TrimPrefix(u.Path, "/")
185+
186+ var relPath string
187+ switch {
188+ // dumb://home or dumb://home/ → index.html
189+ case host == HomePath && (path == "" || path == "/"):
190+ relPath = IndexHTML
191+ // dumb://home/<asset> → serve asset
192+ case host == HomePath && path != "":
193+ relPath = path
194+ // dumb://blocked or dumb://blocked/ → blocked.html
195+ case host == BlockedPath && (path == "" || path == "/"):
196+ relPath = "blocked.html"
197+ // dumb://blocked/<asset> → serve asset
198+ case host == BlockedPath && path != "":
199+ relPath = path
200+ // dumb:home (opaque form) → index.html
201+ case u.Opaque == HomePath:
202+ relPath = IndexHTML
203+ // dumb:blocked (opaque form) → blocked.html
204+ case u.Opaque == BlockedPath:
205+ relPath = "blocked.html"
206+ default:
207+ // Not a recognized asset path
208+ return nil
209+ }
210+
211+ // Read the asset from embedded FS
212+ fullPath := filepath.ToSlash(filepath.Join(assetDir, relPath))
213+ data, err := fs.ReadFile(h.assets, fullPath)
214+ if err != nil {
215+ h.logger.Debug().Str("path", fullPath).Err(err).Msg("asset not found")
216+ return nil
217+ }
218+
219+ contentType := h.getMimeType(relPath)
220+ h.logger.Debug().
221+ Str("path", fullPath).
222+ Str("content_type", contentType).
223+ Int("size", len(data)).
224+ Msg("serving asset")
225+
226+ return &SchemeResponse{
227+ Data: data,
228+ ContentType: contentType,
229+ StatusCode: http.StatusOK,
230+ }
231+}
232+
233+// getMimeType determines the MIME type for a given file path.
234+func (h *DumbSchemeHandler) getMimeType(filename string) string {
235+ ext := strings.ToLower(filepath.Ext(filename))
236+
237+ // Try standard mime type first
238+ mt := mime.TypeByExtension(ext)
239+ if mt != "" {
240+ return mt
241+ }
242+
243+ // Fallbacks for common web assets
244+ switch ext {
245+ case ".js", ".mjs":
246+ return "application/javascript"
247+ case ".css":
248+ return "text/css"
249+ case ".svg":
250+ return "image/svg+xml"
251+ case ".ico":
252+ return "image/x-icon"
253+ case ".woff":
254+ return "font/woff"
255+ case ".woff2":
256+ return "font/woff2"
257+ case ".ttf":
258+ return "font/ttf"
259+ case ".otf":
260+ return "font/otf"
261+ case ".png":
262+ return "image/png"
263+ case ".jpg", ".jpeg":
264+ return "image/jpeg"
265+ case ".gif":
266+ return "image/gif"
267+ case ".webp":
268+ return "image/webp"
269+ case ".json":
270+ return "application/json"
271+ case ".xml":
272+ return "application/xml"
273+ case ".html", ".htm":
274+ return "text/html; charset=utf-8"
275+ default:
276+ return "text/plain"
277+ }
278+}
279+
149280 // sendResponse sends the response back to WebKit.
150281 func (h *DumbSchemeHandler) sendResponse(req *webkit.URISchemeRequest, response *SchemeResponse) {
151282 if response == nil {
@@ -156,27 +287,28 @@ func (h *DumbSchemeHandler) sendResponse(req *webkit.URISchemeRequest, response
156287 }
157288 }
158289
159- // Create GBytes from response data
160- gbytes := glib.NewBytes(response.Data, uint(len(response.Data)))
161- if gbytes == nil {
162- h.logger.Error().Msg("failed to create GBytes for response")
163- return
290+ contentType := response.ContentType
291+ if contentType == "" {
292+ contentType = "text/html"
164293 }
165294
166- // Create MemoryInputStream from GBytes
167- stream := gio.NewMemoryInputStreamFromBytes(gbytes)
295+ // Create MemoryInputStream from data directly
296+ stream := gio.NewMemoryInputStreamFromData(response.Data, len(response.Data), nil)
168297 if stream == nil {
169298 h.logger.Error().Msg("failed to create MemoryInputStream for response")
170299 return
171300 }
172301
173- // Finish the request with the stream
174- contentType := response.ContentType
175- if contentType == "" {
176- contentType = "text/html"
302+ // Create response object for more control
303+ schemeResp := webkit.NewURISchemeResponse(&stream.InputStream, int64(len(response.Data)))
304+ if schemeResp == nil {
305+ h.logger.Error().Msg("failed to create URISchemeResponse")
306+ return
177307 }
308+ schemeResp.SetContentType(contentType)
309+ schemeResp.SetStatus(uint(response.StatusCode), nil)
178310
179- req.Finish(&stream.InputStream, int64(len(response.Data)), &contentType)
311+ req.FinishWithResponse(schemeResp)
180312 }
181313
182314 // RegisterWithContext registers the dumb:// scheme with a WebKitContext.
@@ -192,49 +324,19 @@ func (h *DumbSchemeHandler) RegisterWithContext(wkCtx *WebKitContext) {
192324
193325 wkCtx.Context().RegisterUriScheme("dumb", &callback, 0, nil)
194326
195- // Mark scheme as local and secure for proper security policies
327+ // Mark scheme as local, secure, and CORS-enabled for proper security policies
196328 secMgr := wkCtx.Context().GetSecurityManager()
197329 if secMgr != nil {
198330 secMgr.RegisterUriSchemeAsLocal("dumb")
199331 secMgr.RegisterUriSchemeAsSecure("dumb")
332+ secMgr.RegisterUriSchemeAsCorsEnabled("dumb")
333+ h.logger.Debug().Msg("dumb:// scheme registered as local, secure, and CORS-enabled")
200334 }
201335
202336 h.logger.Info().Msg("dumb:// scheme registered")
203337 }
204338
205-// Default page templates
206-
207-const newTabHTML = `<!DOCTYPE html>
208-<html>
209-<head>
210- <meta charset="utf-8">
211- <title>New Tab</title>
212- <style>
213- body {
214- font-family: system-ui, -apple-system, sans-serif;
215- background: #1a1a2e;
216- color: #eee;
217- display: flex;
218- align-items: center;
219- justify-content: center;
220- height: 100vh;
221- margin: 0;
222- }
223- .container {
224- text-align: center;
225- }
226- h1 {
227- font-weight: 300;
228- color: #888;
229- }
230- </style>
231-</head>
232-<body>
233- <div class="container">
234- <h1>dumber</h1>
235- </div>
236-</body>
237-</html>`
339+// Default page templates (fallback when assets not available)
238340
239341 const errorPageHTML = `<!DOCTYPE html>
240342 <html>
@@ -2,17 +2,28 @@ package webkit
2 2
3 import (3 import (
4 "context"4 "context"
5+ "embed"
6+ "io/fs"
7+ "mime"
5 "net/http"8 "net/http"
9+ "net/url"
10+ "path/filepath"
6 "strings"11 "strings"
7 "sync"12 "sync"
8 13
9 "github.com/bnema/dumber/internal/logging"14 "github.com/bnema/dumber/internal/logging"
10 "github.com/bnema/puregotk-webkit/webkit"15 "github.com/bnema/puregotk-webkit/webkit"
11 "github.com/jwijenbergh/puregotk/v4/gio"16 "github.com/jwijenbergh/puregotk/v4/gio"
12- "github.com/jwijenbergh/puregotk/v4/glib"
13 "github.com/rs/zerolog"17 "github.com/rs/zerolog"
14 )18 )
15 19
20+// Scheme path constants
21+const (
22+ HomePath = "home"
23+ BlockedPath = "blocked"
24+ IndexHTML = "index.html"
25+)
26+
16 // SchemeRequest represents a request to a custom URI scheme.27 // SchemeRequest represents a request to a custom URI scheme.
17 type SchemeRequest struct {28 type SchemeRequest struct {
18 inner *webkit.URISchemeRequest29 inner *webkit.URISchemeRequest
@@ -44,6 +55,8 @@ func (f PageHandlerFunc) Handle(req *SchemeRequest) *SchemeResponse {
44 // DumbSchemeHandler handles dumb:// URI scheme requests.55 // DumbSchemeHandler handles dumb:// URI scheme requests.
45 type DumbSchemeHandler struct {56 type DumbSchemeHandler struct {
46 handlers map[string]PageHandler57 handlers map[string]PageHandler
58+ assets embed.FS
59+ assetDir string // subdirectory within embed.FS (e.g., "assets/webui")
47 logger zerolog.Logger60 logger zerolog.Logger
48 mu sync.RWMutex61 mu sync.RWMutex
49 }62 }
@@ -54,6 +67,7 @@ func NewDumbSchemeHandler(ctx context.Context) *DumbSchemeHandler {
54 67
55 h := &DumbSchemeHandler{68 h := &DumbSchemeHandler{
56 handlers: make(map[string]PageHandler),69 handlers: make(map[string]PageHandler),
70+ assetDir: "webui",
57 logger: log.With().Str("component", "scheme-handler").Logger(),71 logger: log.With().Str("component", "scheme-handler").Logger(),
58 }72 }
59 73
@@ -63,27 +77,17 @@ func NewDumbSchemeHandler(ctx context.Context) *DumbSchemeHandler {
63 return h77 return h
64 }78 }
65 79
80+// SetAssets sets the embedded filesystem containing webui assets.
81+func (h *DumbSchemeHandler) SetAssets(assets embed.FS) {
82+ h.mu.Lock()
83+ defer h.mu.Unlock()
84+ h.assets = assets
85+ h.logger.Debug().Msg("assets filesystem configured")
86+}
87+
66 // registerDefaults sets up default page handlers.88 // registerDefaults sets up default page handlers.
67 func (h *DumbSchemeHandler) registerDefaults() {89 func (h *DumbSchemeHandler) registerDefaults() {
68- // New tab page90+ // Error page (static fallback)
69- h.RegisterPage("/newtab", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse {
70- return &SchemeResponse{
71- Data: []byte(newTabHTML),
72- ContentType: "text/html",
73- StatusCode: http.StatusOK,
74- }
75- }))
76-
77- // Homepage (alias for newtab)
78- h.RegisterPage("/", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse {
79- return &SchemeResponse{
80- Data: []byte(newTabHTML),
81- ContentType: "text/html",
82- StatusCode: http.StatusOK,
83- }
84- }))
85-
86- // Error page
87 h.RegisterPage("/error", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse {91 h.RegisterPage("/error", PageHandlerFunc(func(req *SchemeRequest) *SchemeResponse {
88 return &SchemeResponse{92 return &SchemeResponse{
89 Data: []byte(errorPageHTML),93 Data: []byte(errorPageHTML),
@@ -108,9 +112,10 @@ func (h *DumbSchemeHandler) HandleRequest(reqPtr uintptr) {
108 return112 return
109 }113 }
110 114
115+ uri := req.GetUri()
111 schemeReq := &SchemeRequest{116 schemeReq := &SchemeRequest{
112 inner: req,117 inner: req,
113- URI: req.GetUri(),118+ URI: uri,
114 Path: req.GetPath(),119 Path: req.GetPath(),
115 Method: req.GetHttpMethod(),120 Method: req.GetHttpMethod(),
116 Scheme: req.GetScheme(),121 Scheme: req.GetScheme(),
@@ -122,11 +127,28 @@ func (h *DumbSchemeHandler) HandleRequest(reqPtr uintptr) {
122 Str("method", schemeReq.Method).127 Str("method", schemeReq.Method).
123 Msg("handling scheme request")128 Msg("handling scheme request")
124 129
125- // Look up handler130+ // Parse the URI to extract host and path
131+ u, err := url.Parse(uri)
132+ if err != nil || u.Scheme != "dumb" {
133+ h.logger.Error().Err(err).Str("uri", uri).Msg("invalid URI")
134+ h.sendResponse(req, &SchemeResponse{
135+ Data: []byte("Invalid URI"),
136+ ContentType: "text/plain",
137+ StatusCode: http.StatusBadRequest,
138+ })
139+ return
140+ }
141+
142+ // Try to serve from embedded assets first
143+ if response := h.handleAsset(u); response != nil {
144+ h.sendResponse(req, response)
145+ return
146+ }
147+
148+ // Fall back to registered handlers
126 h.mu.RLock()149 h.mu.RLock()
127 handler, ok := h.handlers[schemeReq.Path]150 handler, ok := h.handlers[schemeReq.Path]
128 if !ok {151 if !ok {
129- // Try without leading slash
130 handler, ok = h.handlers[strings.TrimPrefix(schemeReq.Path, "/")]152 handler, ok = h.handlers[strings.TrimPrefix(schemeReq.Path, "/")]
131 }153 }
132 h.mu.RUnlock()154 h.mu.RUnlock()
@@ -142,10 +164,119 @@ func (h *DumbSchemeHandler) HandleRequest(reqPtr uintptr) {
142 }164 }
143 }165 }
144 166
145- // Send response
146 h.sendResponse(req, response)167 h.sendResponse(req, response)
147 }168 }
148 169
170+// handleAsset serves static assets from the embedded filesystem.
171+// Returns nil if no asset was found (allowing fallback to registered handlers).
172+func (h *DumbSchemeHandler) handleAsset(u *url.URL) *SchemeResponse {
173+ h.mu.RLock()
174+ hasAssets := h.assets != (embed.FS{})
175+ assetDir := h.assetDir
176+ h.mu.RUnlock()
177+
178+ if !hasAssets {
179+ return nil
180+ }
181+
182+ // Determine the target file based on host and path
183+ host := u.Host
184+ path := strings.TrimPrefix(u.Path, "/")
185+
186+ var relPath string
187+ switch {
188+ // dumb://home or dumb://home/ → index.html
189+ case host == HomePath && (path == "" || path == "/"):
190+ relPath = IndexHTML
191+ // dumb://home/<asset> → serve asset
192+ case host == HomePath && path != "":
193+ relPath = path
194+ // dumb://blocked or dumb://blocked/ → blocked.html
195+ case host == BlockedPath && (path == "" || path == "/"):
196+ relPath = "blocked.html"
197+ // dumb://blocked/<asset> → serve asset
198+ case host == BlockedPath && path != "":
199+ relPath = path
200+ // dumb:home (opaque form) → index.html
201+ case u.Opaque == HomePath:
202+ relPath = IndexHTML
203+ // dumb:blocked (opaque form) → blocked.html
204+ case u.Opaque == BlockedPath:
205+ relPath = "blocked.html"
206+ default:
207+ // Not a recognized asset path
208+ return nil
209+ }
210+
211+ // Read the asset from embedded FS
212+ fullPath := filepath.ToSlash(filepath.Join(assetDir, relPath))
213+ data, err := fs.ReadFile(h.assets, fullPath)
214+ if err != nil {
215+ h.logger.Debug().Str("path", fullPath).Err(err).Msg("asset not found")
216+ return nil
217+ }
218+
219+ contentType := h.getMimeType(relPath)
220+ h.logger.Debug().
221+ Str("path", fullPath).
222+ Str("content_type", contentType).
223+ Int("size", len(data)).
224+ Msg("serving asset")
225+
226+ return &SchemeResponse{
227+ Data: data,
228+ ContentType: contentType,
229+ StatusCode: http.StatusOK,
230+ }
231+}
232+
233+// getMimeType determines the MIME type for a given file path.
234+func (h *DumbSchemeHandler) getMimeType(filename string) string {
235+ ext := strings.ToLower(filepath.Ext(filename))
236+
237+ // Try standard mime type first
238+ mt := mime.TypeByExtension(ext)
239+ if mt != "" {
240+ return mt
241+ }
242+
243+ // Fallbacks for common web assets
244+ switch ext {
245+ case ".js", ".mjs":
246+ return "application/javascript"
247+ case ".css":
248+ return "text/css"
249+ case ".svg":
250+ return "image/svg+xml"
251+ case ".ico":
252+ return "image/x-icon"
253+ case ".woff":
254+ return "font/woff"
255+ case ".woff2":
256+ return "font/woff2"
257+ case ".ttf":
258+ return "font/ttf"
259+ case ".otf":
260+ return "font/otf"
261+ case ".png":
262+ return "image/png"
263+ case ".jpg", ".jpeg":
264+ return "image/jpeg"
265+ case ".gif":
266+ return "image/gif"
267+ case ".webp":
268+ return "image/webp"
269+ case ".json":
270+ return "application/json"
271+ case ".xml":
272+ return "application/xml"
273+ case ".html", ".htm":
274+ return "text/html; charset=utf-8"
275+ default:
276+ return "text/plain"
277+ }
278+}
279+
149 // sendResponse sends the response back to WebKit.280 // sendResponse sends the response back to WebKit.
150 func (h *DumbSchemeHandler) sendResponse(req *webkit.URISchemeRequest, response *SchemeResponse) {281 func (h *DumbSchemeHandler) sendResponse(req *webkit.URISchemeRequest, response *SchemeResponse) {
151 if response == nil {282 if response == nil {
@@ -156,27 +287,28 @@ func (h *DumbSchemeHandler) sendResponse(req *webkit.URISchemeRequest, response
156 }287 }
157 }288 }
158 289
159- // Create GBytes from response data290+ contentType := response.ContentType
160- gbytes := glib.NewBytes(response.Data, uint(len(response.Data)))291+ if contentType == "" {
161- if gbytes == nil {292+ contentType = "text/html"
162- h.logger.Error().Msg("failed to create GBytes for response")
163- return
164 }293 }
165 294
166- // Create MemoryInputStream from GBytes295+ // Create MemoryInputStream from data directly
167- stream := gio.NewMemoryInputStreamFromBytes(gbytes)296+ stream := gio.NewMemoryInputStreamFromData(response.Data, len(response.Data), nil)
168 if stream == nil {297 if stream == nil {
169 h.logger.Error().Msg("failed to create MemoryInputStream for response")298 h.logger.Error().Msg("failed to create MemoryInputStream for response")
170 return299 return
171 }300 }
172 301
173- // Finish the request with the stream302+ // Create response object for more control
174- contentType := response.ContentType303+ schemeResp := webkit.NewURISchemeResponse(&stream.InputStream, int64(len(response.Data)))
175- if contentType == "" {304+ if schemeResp == nil {
176- contentType = "text/html"305+ h.logger.Error().Msg("failed to create URISchemeResponse")
306+ return
177 }307 }
308+ schemeResp.SetContentType(contentType)
309+ schemeResp.SetStatus(uint(response.StatusCode), nil)
178 310
179- req.Finish(&stream.InputStream, int64(len(response.Data)), &contentType)311+ req.FinishWithResponse(schemeResp)
180 }312 }
181 313
182 // RegisterWithContext registers the dumb:// scheme with a WebKitContext.314 // RegisterWithContext registers the dumb:// scheme with a WebKitContext.
@@ -192,49 +324,19 @@ func (h *DumbSchemeHandler) RegisterWithContext(wkCtx *WebKitContext) {
192 324
193 wkCtx.Context().RegisterUriScheme("dumb", &callback, 0, nil)325 wkCtx.Context().RegisterUriScheme("dumb", &callback, 0, nil)
194 326
195- // Mark scheme as local and secure for proper security policies327+ // Mark scheme as local, secure, and CORS-enabled for proper security policies
196 secMgr := wkCtx.Context().GetSecurityManager()328 secMgr := wkCtx.Context().GetSecurityManager()
197 if secMgr != nil {329 if secMgr != nil {
198 secMgr.RegisterUriSchemeAsLocal("dumb")330 secMgr.RegisterUriSchemeAsLocal("dumb")
199 secMgr.RegisterUriSchemeAsSecure("dumb")331 secMgr.RegisterUriSchemeAsSecure("dumb")
332+ secMgr.RegisterUriSchemeAsCorsEnabled("dumb")
333+ h.logger.Debug().Msg("dumb:// scheme registered as local, secure, and CORS-enabled")
200 }334 }
201 335
202 h.logger.Info().Msg("dumb:// scheme registered")336 h.logger.Info().Msg("dumb:// scheme registered")
203 }337 }
204 338
205-// Default page templates339+// Default page templates (fallback when assets not available)
206-
207-const newTabHTML = `<!DOCTYPE html>
208-<html>
209-<head>
210- <meta charset="utf-8">
211- <title>New Tab</title>
212- <style>
213- body {
214- font-family: system-ui, -apple-system, sans-serif;
215- background: #1a1a2e;
216- color: #eee;
217- display: flex;
218- align-items: center;
219- justify-content: center;
220- height: 100vh;
221- margin: 0;
222- }
223- .container {
224- text-align: center;
225- }
226- h1 {
227- font-weight: 300;
228- color: #888;
229- }
230- </style>
231-</head>
232-<body>
233- <div class="container">
234- <h1>dumber</h1>
235- </div>
236-</body>
237-</html>`
238 340
239 const errorPageHTML = `<!DOCTYPE html>341 const errorPageHTML = `<!DOCTYPE html>
240 <html>342 <html>
M internal/ui/app.go +3 -3
@@ -204,8 +204,8 @@ func (a *App) onActivate(ctx context.Context) {
204204 a.navCoord.SetOmniboxProvider(a)
205205 log.Debug().Msg("omnibox config stored, provider set")
206206
207- // Create an initial tab using coordinator
208- if _, err := a.tabCoord.Create(ctx, "https://duckduckgo.com"); err != nil {
207+ // Create an initial tab using coordinator - opens dumb://home homepage
208+ if _, err := a.tabCoord.Create(ctx, "dumb://home"); err != nil {
209209 log.Error().Err(err).Msg("failed to create initial tab")
210210 }
211211
@@ -375,7 +375,7 @@ func (a *App) createWorkspaceView(ctx context.Context, tab *entity.Tab) {
375375 }
376376
377377 // Set the workspace
378- if err := wsView.SetWorkspace(tab.Workspace); err != nil {
378+ if err := wsView.SetWorkspace(ctx, tab.Workspace); err != nil {
379379 log.Error().Err(err).Msg("failed to set workspace in view")
380380 return
381381 }
@@ -204,8 +204,8 @@ func (a *App) onActivate(ctx context.Context) {
204 a.navCoord.SetOmniboxProvider(a)204 a.navCoord.SetOmniboxProvider(a)
205 log.Debug().Msg("omnibox config stored, provider set")205 log.Debug().Msg("omnibox config stored, provider set")
206 206
207- // Create an initial tab using coordinator207+ // Create an initial tab using coordinator - opens dumb://home homepage
208- if _, err := a.tabCoord.Create(ctx, "https://duckduckgo.com"); err != nil {208+ if _, err := a.tabCoord.Create(ctx, "dumb://home"); err != nil {
209 log.Error().Err(err).Msg("failed to create initial tab")209 log.Error().Err(err).Msg("failed to create initial tab")
210 }210 }
211 211
@@ -375,7 +375,7 @@ func (a *App) createWorkspaceView(ctx context.Context, tab *entity.Tab) {
375 }375 }
376 376
377 // Set the workspace377 // Set the workspace
378- if err := wsView.SetWorkspace(tab.Workspace); err != nil {378+ if err := wsView.SetWorkspace(ctx, tab.Workspace); err != nil {
379 log.Error().Err(err).Msg("failed to set workspace in view")379 log.Error().Err(err).Msg("failed to set workspace in view")
380 return380 return
381 }381 }

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