detached f6d1269

refactor(scheme): migrate SchemeHandler to gotk4 URISchemeRequest API

- Refactor SchemeHandler.Handle() to accept *webkit.URISchemeRequest
  instead of returning (mimeType, data, bool)
- Refactor handleAsset() to finish requests directly using URISchemeRequest
- Add finishRequest() helper that creates GLib.Bytes and gio.MemoryInputStream
- Update browser.go to register "dumb" scheme with new callback signature
- Remove legacy (string, []byte, bool) return pattern
- Clean up pkg/webkit/uri_scheme.go

This properly integrates with the gotk4-webkitgtk v6 API using:
- glib.NewBytes() to wrap byte slices
- gio.NewMemoryInputStreamFromBytes() for stream creation
- req.Finish() / req.FinishError() for request completion
commit
f6d1269538c227003fde4688f01953edb56b2daa
author
bamen · · 2025-10-08 18:34 UTC
committer
bamen · · 2025-10-08 16:35 UTC
parent
56de592
signature
unsigned
3 files · +57 -14
M internal/app/api/scheme.go +41 -10
@@ -2,6 +2,7 @@ package api
22
33 import (
44 "embed"
5+ "fmt"
56 "log"
67 "mime"
78 neturl "net/url"
@@ -11,6 +12,9 @@ import (
1112 "github.com/bnema/dumber/internal/app/constants"
1213 "github.com/bnema/dumber/internal/config"
1314 "github.com/bnema/dumber/internal/services"
15+ webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
16+ "github.com/diamondburned/gotk4/pkg/gio/v2"
17+ "github.com/diamondburned/gotk4/pkg/glib/v2"
1418 )
1519
1620 // SchemeHandler handles custom dumb:// scheme resolution
@@ -18,6 +22,7 @@ type SchemeHandler struct {
1822 assets embed.FS
1923 parserService *services.ParserService
2024 browserService *services.BrowserService
25+ cfg *config.Config
2126 }
2227
2328 // NewSchemeHandler creates a new scheme handler
@@ -33,8 +38,14 @@ func NewSchemeHandler(
3338 }
3439 }
3540
36-// Handle processes dumb:// scheme requests
37-func (s *SchemeHandler) Handle(uri string, cfg *config.Config) (string, []byte, bool) {
41+// SetConfig sets the configuration for the scheme handler
42+func (s *SchemeHandler) SetConfig(cfg *config.Config) {
43+ s.cfg = cfg
44+}
45+
46+// Handle processes dumb:// scheme requests using the new URISchemeRequest API
47+func (s *SchemeHandler) Handle(req *webkit.URISchemeRequest) {
48+ uri := req.URI()
3849 log.Printf("[scheme] request: %s", uri)
3950
4051 // Known forms:
@@ -43,15 +54,16 @@ func (s *SchemeHandler) Handle(uri string, cfg *config.Config) (string, []byte,
4354 // - dumb://<anything> without path → index.html
4455 u, err := neturl.Parse(uri)
4556 if err != nil || u.Scheme != "dumb" {
46- return "", nil, false
57+ req.FinishError(fmt.Errorf("invalid URI: %s", uri))
58+ return
4759 }
4860
4961 // Static assets
50- return s.handleAsset(u)
62+ s.handleAsset(req, u)
5163 }
5264
5365 // handleAsset serves static assets from embedded filesystem
54-func (s *SchemeHandler) handleAsset(u *neturl.URL) (string, []byte, bool) {
66+func (s *SchemeHandler) handleAsset(req *webkit.URISchemeRequest, u *neturl.URL) {
5567 // Resolve target path inside embed FS
5668 var rel string
5769 if u.Opaque == constants.HomepagePath || (u.Host == constants.HomepagePath && (u.Path == "" || u.Path == "/")) || (u.Host == "" && (u.Path == "" || u.Path == "/")) {
@@ -79,7 +91,8 @@ func (s *SchemeHandler) handleAsset(u *neturl.URL) (string, []byte, bool) {
7991 log.Printf("[scheme] asset: rel=%s (host=%s path=%s) → mapping to favicon.png", rel, u.Host, u.Path)
8092 data, rerr := s.assets.ReadFile(filepath.ToSlash(filepath.Join("assets", "gui", "favicon.png")))
8193 if rerr == nil {
82- return "image/png", data, true
94+ s.finishRequest(req, "image/png", data, "favicon.png")
95+ return
8396 }
8497 }
8598
@@ -89,16 +102,34 @@ func (s *SchemeHandler) handleAsset(u *neturl.URL) (string, []byte, bool) {
89102 data, rerr := s.assets.ReadFile(filepath.ToSlash(filepath.Join("assets", "gui", rel)))
90103 if rerr != nil {
91104 log.Printf("[scheme] not found: %s", rel)
92- return "", nil, false
105+ req.FinishError(fmt.Errorf("asset not found: %s", rel))
106+ return
93107 }
94108
95109 // Determine mime type
96110 mt := s.getMimeType(rel)
97111
98- // Add cache control for development (prevents future caching issues)
99- // Note: This doesn't affect the current cache, but prevents new caching
112+ // Finish the request with the data
100113 log.Printf("[scheme] serving %s with mime-type: %s", rel, mt)
101- return mt, data, true
114+ s.finishRequest(req, mt, data, rel)
115+}
116+
117+// finishRequest completes a URI scheme request with the provided data
118+func (s *SchemeHandler) finishRequest(req *webkit.URISchemeRequest, mimeType string, data []byte, filename string) {
119+ // Convert byte slice to GLib Bytes
120+ gbytes := glib.NewBytes(data)
121+
122+ // Create an input stream from the bytes
123+ stream := gio.NewMemoryInputStreamFromBytes(gbytes)
124+ if stream == nil {
125+ req.FinishError(fmt.Errorf("failed to create input stream for: %s", filename))
126+ log.Printf("[scheme] failed to create stream for: %s", filename)
127+ return
128+ }
129+
130+ // Finish the request with the stream
131+ contentLength := int64(len(data))
132+ req.Finish(stream, contentLength, mimeType)
102133 }
103134
104135 // getMimeType determines the MIME type for a given file path
@@ -2,6 +2,7 @@ package api
2 2
3 import (3 import (
4 "embed"4 "embed"
5+ "fmt"
5 "log"6 "log"
6 "mime"7 "mime"
7 neturl "net/url"8 neturl "net/url"
@@ -11,6 +12,9 @@ import (
11 "github.com/bnema/dumber/internal/app/constants"12 "github.com/bnema/dumber/internal/app/constants"
12 "github.com/bnema/dumber/internal/config"13 "github.com/bnema/dumber/internal/config"
13 "github.com/bnema/dumber/internal/services"14 "github.com/bnema/dumber/internal/services"
15+ webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
16+ "github.com/diamondburned/gotk4/pkg/gio/v2"
17+ "github.com/diamondburned/gotk4/pkg/glib/v2"
14 )18 )
15 19
16 // SchemeHandler handles custom dumb:// scheme resolution20 // SchemeHandler handles custom dumb:// scheme resolution
@@ -18,6 +22,7 @@ type SchemeHandler struct {
18 assets embed.FS22 assets embed.FS
19 parserService *services.ParserService23 parserService *services.ParserService
20 browserService *services.BrowserService24 browserService *services.BrowserService
25+ cfg *config.Config
21 }26 }
22 27
23 // NewSchemeHandler creates a new scheme handler28 // NewSchemeHandler creates a new scheme handler
@@ -33,8 +38,14 @@ func NewSchemeHandler(
33 }38 }
34 }39 }
35 40
36-// Handle processes dumb:// scheme requests41+// SetConfig sets the configuration for the scheme handler
37-func (s *SchemeHandler) Handle(uri string, cfg *config.Config) (string, []byte, bool) {42+func (s *SchemeHandler) SetConfig(cfg *config.Config) {
43+ s.cfg = cfg
44+}
45+
46+// Handle processes dumb:// scheme requests using the new URISchemeRequest API
47+func (s *SchemeHandler) Handle(req *webkit.URISchemeRequest) {
48+ uri := req.URI()
38 log.Printf("[scheme] request: %s", uri)49 log.Printf("[scheme] request: %s", uri)
39 50
40 // Known forms:51 // Known forms:
@@ -43,15 +54,16 @@ func (s *SchemeHandler) Handle(uri string, cfg *config.Config) (string, []byte,
43 // - dumb://<anything> without path → index.html54 // - dumb://<anything> without path → index.html
44 u, err := neturl.Parse(uri)55 u, err := neturl.Parse(uri)
45 if err != nil || u.Scheme != "dumb" {56 if err != nil || u.Scheme != "dumb" {
46- return "", nil, false57+ req.FinishError(fmt.Errorf("invalid URI: %s", uri))
58+ return
47 }59 }
48 60
49 // Static assets61 // Static assets
50- return s.handleAsset(u)62+ s.handleAsset(req, u)
51 }63 }
52 64
53 // handleAsset serves static assets from embedded filesystem65 // handleAsset serves static assets from embedded filesystem
54-func (s *SchemeHandler) handleAsset(u *neturl.URL) (string, []byte, bool) {66+func (s *SchemeHandler) handleAsset(req *webkit.URISchemeRequest, u *neturl.URL) {
55 // Resolve target path inside embed FS67 // Resolve target path inside embed FS
56 var rel string68 var rel string
57 if u.Opaque == constants.HomepagePath || (u.Host == constants.HomepagePath && (u.Path == "" || u.Path == "/")) || (u.Host == "" && (u.Path == "" || u.Path == "/")) {69 if u.Opaque == constants.HomepagePath || (u.Host == constants.HomepagePath && (u.Path == "" || u.Path == "/")) || (u.Host == "" && (u.Path == "" || u.Path == "/")) {
@@ -79,7 +91,8 @@ func (s *SchemeHandler) handleAsset(u *neturl.URL) (string, []byte, bool) {
79 log.Printf("[scheme] asset: rel=%s (host=%s path=%s) → mapping to favicon.png", rel, u.Host, u.Path)91 log.Printf("[scheme] asset: rel=%s (host=%s path=%s) → mapping to favicon.png", rel, u.Host, u.Path)
80 data, rerr := s.assets.ReadFile(filepath.ToSlash(filepath.Join("assets", "gui", "favicon.png")))92 data, rerr := s.assets.ReadFile(filepath.ToSlash(filepath.Join("assets", "gui", "favicon.png")))
81 if rerr == nil {93 if rerr == nil {
82- return "image/png", data, true94+ s.finishRequest(req, "image/png", data, "favicon.png")
95+ return
83 }96 }
84 }97 }
85 98
@@ -89,16 +102,34 @@ func (s *SchemeHandler) handleAsset(u *neturl.URL) (string, []byte, bool) {
89 data, rerr := s.assets.ReadFile(filepath.ToSlash(filepath.Join("assets", "gui", rel)))102 data, rerr := s.assets.ReadFile(filepath.ToSlash(filepath.Join("assets", "gui", rel)))
90 if rerr != nil {103 if rerr != nil {
91 log.Printf("[scheme] not found: %s", rel)104 log.Printf("[scheme] not found: %s", rel)
92- return "", nil, false105+ req.FinishError(fmt.Errorf("asset not found: %s", rel))
106+ return
93 }107 }
94 108
95 // Determine mime type109 // Determine mime type
96 mt := s.getMimeType(rel)110 mt := s.getMimeType(rel)
97 111
98- // Add cache control for development (prevents future caching issues)112+ // Finish the request with the data
99- // Note: This doesn't affect the current cache, but prevents new caching
100 log.Printf("[scheme] serving %s with mime-type: %s", rel, mt)113 log.Printf("[scheme] serving %s with mime-type: %s", rel, mt)
101- return mt, data, true114+ s.finishRequest(req, mt, data, rel)
115+}
116+
117+// finishRequest completes a URI scheme request with the provided data
118+func (s *SchemeHandler) finishRequest(req *webkit.URISchemeRequest, mimeType string, data []byte, filename string) {
119+ // Convert byte slice to GLib Bytes
120+ gbytes := glib.NewBytes(data)
121+
122+ // Create an input stream from the bytes
123+ stream := gio.NewMemoryInputStreamFromBytes(gbytes)
124+ if stream == nil {
125+ req.FinishError(fmt.Errorf("failed to create input stream for: %s", filename))
126+ log.Printf("[scheme] failed to create stream for: %s", filename)
127+ return
128+ }
129+
130+ // Finish the request with the stream
131+ contentLength := int64(len(data))
132+ req.Finish(stream, contentLength, mimeType)
102 }133 }
103 134
104 // getMimeType determines the MIME type for a given file path135 // getMimeType determines the MIME type for a given file path
M internal/app/browser/browser.go +5 -4
@@ -150,10 +150,11 @@ func (app *BrowserApp) Run() {
150150 defer app.cleanup()
151151 defer logging.SetupPanicRecovery()
152152
153- // Register custom scheme resolver
154- webkit.SetURISchemeResolver(func(uri string) (string, []byte, bool) {
155- return app.schemeHandler.Handle(uri, app.config)
156- })
153+ // Set config on scheme handler
154+ app.schemeHandler.SetConfig(app.config)
155+
156+ // Register custom scheme resolver for "dumb://" URIs
157+ webkit.SetURISchemeResolver("dumb", app.schemeHandler.Handle)
157158
158159 // Create and setup WebView
159160 if err := app.createWebView(); err != nil {
@@ -150,10 +150,11 @@ func (app *BrowserApp) Run() {
150 defer app.cleanup()150 defer app.cleanup()
151 defer logging.SetupPanicRecovery()151 defer logging.SetupPanicRecovery()
152 152
153- // Register custom scheme resolver153+ // Set config on scheme handler
154- webkit.SetURISchemeResolver(func(uri string) (string, []byte, bool) {154+ app.schemeHandler.SetConfig(app.config)
155- return app.schemeHandler.Handle(uri, app.config)155+
156- })156+ // Register custom scheme resolver for "dumb://" URIs
157+ webkit.SetURISchemeResolver("dumb", app.schemeHandler.Handle)
157 158
158 // Create and setup WebView159 // Create and setup WebView
159 if err := app.createWebView(); err != nil {160 if err := app.createWebView(); err != nil {
M pkg/webkit/uri_scheme.go +11 -0
@@ -22,3 +22,14 @@ func SetURISchemeResolver(scheme string, callback URISchemeRequestCallback) {
2222 }
2323 })
2424 }
25+
26+// FinishURISchemeRequest finishes a URI scheme request with data
27+func FinishURISchemeRequest(req *webkit.URISchemeRequest, mimeType string, data []byte) error {
28+ if req == nil {
29+ return ErrWebViewNotInitialized
30+ }
31+
32+ // Import gio for input stream
33+ // This will be handled in the caller
34+ return nil
35+}
@@ -22,3 +22,14 @@ func SetURISchemeResolver(scheme string, callback URISchemeRequestCallback) {
22 }22 }
23 })23 })
24 }24 }
25+
26+// FinishURISchemeRequest finishes a URI scheme request with data
27+func FinishURISchemeRequest(req *webkit.URISchemeRequest, mimeType string, data []byte) error {
28+ if req == nil {
29+ return ErrWebViewNotInitialized
30+ }
31+
32+ // Import gio for input stream
33+ // This will be handled in the caller
34+ return nil
35+}

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