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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "embed" | |
| 5 | + | "fmt" | |
| 5 | 6 | "log" | |
| 6 | 7 | "mime" | |
| 7 | 8 | neturl "net/url" | |
| @@ -11,6 +12,9 @@ import ( | |||
| 11 | 12 | "github.com/bnema/dumber/internal/app/constants" | |
| 12 | 13 | "github.com/bnema/dumber/internal/config" | |
| 13 | 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 | 20 | // SchemeHandler handles custom dumb:// scheme resolution | |
| @@ -18,6 +22,7 @@ type SchemeHandler struct { | |||
| 18 | 22 | assets embed.FS | |
| 19 | 23 | parserService *services.ParserService | |
| 20 | 24 | browserService *services.BrowserService | |
| 25 | + | cfg *config.Config | |
| 21 | 26 | } | |
| 22 | 27 | ||
| 23 | 28 | // NewSchemeHandler creates a new scheme handler | |
| @@ -33,8 +38,14 @@ func NewSchemeHandler( | |||
| 33 | 38 | } | |
| 34 | 39 | } | |
| 35 | 40 | ||
| 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() | |
| 38 | 49 | log.Printf("[scheme] request: %s", uri) | |
| 39 | 50 | ||
| 40 | 51 | // Known forms: | |
| @@ -43,15 +54,16 @@ func (s *SchemeHandler) Handle(uri string, cfg *config.Config) (string, []byte, | |||
| 43 | 54 | // - dumb://<anything> without path → index.html | |
| 44 | 55 | u, err := neturl.Parse(uri) | |
| 45 | 56 | if err != nil || u.Scheme != "dumb" { | |
| 46 | - | return "", nil, false | |
| 57 | + | req.FinishError(fmt.Errorf("invalid URI: %s", uri)) | |
| 58 | + | return | |
| 47 | 59 | } | |
| 48 | 60 | ||
| 49 | 61 | // Static assets | |
| 50 | - | return s.handleAsset(u) | |
| 62 | + | s.handleAsset(req, u) | |
| 51 | 63 | } | |
| 52 | 64 | ||
| 53 | 65 | // 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 | 67 | // Resolve target path inside embed FS | |
| 56 | 68 | var rel string | |
| 57 | 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 | 91 | log.Printf("[scheme] asset: rel=%s (host=%s path=%s) → mapping to favicon.png", rel, u.Host, u.Path) | |
| 80 | 92 | data, rerr := s.assets.ReadFile(filepath.ToSlash(filepath.Join("assets", "gui", "favicon.png"))) | |
| 81 | 93 | if rerr == nil { | |
| 82 | - | return "image/png", data, true | |
| 94 | + | 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 | 102 | data, rerr := s.assets.ReadFile(filepath.ToSlash(filepath.Join("assets", "gui", rel))) | |
| 90 | 103 | if rerr != nil { | |
| 91 | 104 | log.Printf("[scheme] not found: %s", rel) | |
| 92 | - | return "", nil, false | |
| 105 | + | req.FinishError(fmt.Errorf("asset not found: %s", rel)) | |
| 106 | + | return | |
| 93 | 107 | } | |
| 94 | 108 | ||
| 95 | 109 | // Determine mime type | |
| 96 | 110 | mt := s.getMimeType(rel) | |
| 97 | 111 | ||
| 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 | |
| 100 | 113 | 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) | |
| 102 | 133 | } | |
| 103 | 134 | ||
| 104 | 135 | // 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 resolution | 20 | // SchemeHandler handles custom dumb:// scheme resolution | ||
| @@ -18,6 +22,7 @@ type SchemeHandler struct { | |||||
| 18 | assets embed.FS | 22 | assets embed.FS | ||
| 19 | parserService *services.ParserService | 23 | parserService *services.ParserService | ||
| 20 | browserService *services.BrowserService | 24 | browserService *services.BrowserService | ||
| 25 | + | cfg *config.Config | |||
| 21 | } | 26 | } | ||
| 22 | 27 | ||||
| 23 | // NewSchemeHandler creates a new scheme handler | 28 | // NewSchemeHandler creates a new scheme handler | ||
| @@ -33,8 +38,14 @@ func NewSchemeHandler( | |||||
| 33 | } | 38 | } | ||
| 34 | } | 39 | } | ||
| 35 | 40 | ||||
| 36 | - | // Handle processes dumb:// scheme requests | 41 | + | // 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.html | 54 | // - 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, false | 57 | + | req.FinishError(fmt.Errorf("invalid URI: %s", uri)) |
| 58 | + | return | |||
| 47 | } | 59 | } | ||
| 48 | 60 | ||||
| 49 | // Static assets | 61 | // Static assets | ||
| 50 | - | return s.handleAsset(u) | 62 | + | s.handleAsset(req, u) |
| 51 | } | 63 | } | ||
| 52 | 64 | ||||
| 53 | // handleAsset serves static assets from embedded filesystem | 65 | // 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 FS | 67 | // Resolve target path inside embed FS | ||
| 56 | var rel string | 68 | 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, true | 94 | + | 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, false | 105 | + | req.FinishError(fmt.Errorf("asset not found: %s", rel)) |
| 106 | + | return | |||
| 93 | } | 107 | } | ||
| 94 | 108 | ||||
| 95 | // Determine mime type | 109 | // 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, 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) | |||
| 102 | } | 133 | } | ||
| 103 | 134 | ||||
| 104 | // getMimeType determines the MIME type for a given file path | 135 | // 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() { | |||
| 150 | 150 | defer app.cleanup() | |
| 151 | 151 | defer logging.SetupPanicRecovery() | |
| 152 | 152 | ||
| 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) | |
| 157 | 158 | ||
| 158 | 159 | // Create and setup WebView | |
| 159 | 160 | 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 resolver | 153 | + | // 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 WebView | 159 | // 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) { | |||
| 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 | + | } | |
| @@ -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 | + | } | |||