1package api
2
3import (
4 "embed"
5 "log"
6 "mime"
7 neturl "net/url"
8 "path/filepath"
9 "strings"
10
11 "github.com/bnema/dumber/internal/app/constants"
12 "github.com/bnema/dumber/internal/config"
13 "github.com/bnema/dumber/internal/services"
14)
15
16// SchemeHandler handles custom dumb:// scheme resolution
17type SchemeHandler struct {
18 assets embed.FS
19 parserService *services.ParserService
20 browserService *services.BrowserService
21}
22
23// NewSchemeHandler creates a new scheme handler
24func NewSchemeHandler(
25 assets embed.FS,
26 parserService *services.ParserService,
27 browserService *services.BrowserService,
28) *SchemeHandler {
29 return &SchemeHandler{
30 assets: assets,
31 parserService: parserService,
32 browserService: browserService,
33 }
34}
35
36// Handle processes dumb:// scheme requests
37func (s *SchemeHandler) Handle(uri string, cfg *config.Config) (string, []byte, bool) {
38 log.Printf("[scheme] request: %s", uri)
39
40 // Known forms:
41 // - dumb://homepage or dumb:homepage → index.html
42 // - dumb://app/index.html, dumb://app/<path> → serve from gui/<path>
43 // - dumb://<anything> without path → index.html
44 u, err := neturl.Parse(uri)
45 if err != nil || u.Scheme != "dumb" {
46 return "", nil, false
47 }
48
49 // Static assets
50 return s.handleAsset(u)
51}
52
53// handleAsset serves static assets from embedded filesystem
54func (s *SchemeHandler) handleAsset(u *neturl.URL) (string, []byte, bool) {
55 // Resolve target path inside embed FS
56 var rel string
57 if u.Opaque == constants.HomepagePath || (u.Host == constants.HomepagePath && (u.Path == "" || u.Path == "/")) || (u.Host == "" && (u.Path == "" || u.Path == "/")) {
58 rel = constants.IndexHTML
59 } else {
60 host := u.Host
61 p := strings.TrimPrefix(u.Path, "/")
62 switch {
63 case host == "app" && p == "":
64 rel = constants.IndexHTML
65 case host == "app":
66 rel = p
67 case host == constants.HomepagePath && p != "":
68 // dumb://homepage/<asset>
69 rel = p
70 case p != "":
71 rel = p
72 default:
73 rel = constants.IndexHTML
74 }
75 }
76
77 // Special-case homepage favicon: map .ico request to embedded PNG file
78 if (u.Host == constants.HomepagePath || u.Opaque == constants.HomepagePath) && strings.EqualFold(rel, "favicon.ico") {
79 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")))
81 if rerr == nil {
82 return "image/png", data, true
83 }
84 }
85
86 log.Printf("[scheme] asset: rel=%s (host=%s path=%s)", rel, u.Host, u.Path)
87
88 // Try to read the requested asset
89 data, rerr := s.assets.ReadFile(filepath.ToSlash(filepath.Join("assets", "gui", rel)))
90 if rerr != nil {
91 log.Printf("[scheme] not found: %s", rel)
92 return "", nil, false
93 }
94
95 // Determine mime type
96 mt := s.getMimeType(rel)
97
98 // Add cache control for development (prevents future caching issues)
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)
101 return mt, data, true
102}
103
104// getMimeType determines the MIME type for a given file path
105func (s *SchemeHandler) getMimeType(filename string) string {
106 ext := strings.ToLower(filepath.Ext(filename))
107
108 // Try standard mime type first
109 mt := mime.TypeByExtension(ext)
110 if mt != "" {
111 return mt
112 }
113
114 // Fallbacks for common web assets
115 switch ext {
116 case ".js":
117 return "application/javascript"
118 case ".mjs":
119 return "application/javascript"
120 case ".css":
121 return "text/css"
122 case ".svg":
123 return "image/svg+xml"
124 case ".ico":
125 return "image/x-icon"
126 case ".woff":
127 return "font/woff"
128 case ".woff2":
129 return "font/woff2"
130 case ".ttf":
131 return "font/ttf"
132 case ".otf":
133 return "font/otf"
134 case ".eot":
135 return "application/vnd.ms-fontobject"
136 case ".png":
137 return "image/png"
138 case ".jpg", ".jpeg":
139 return "image/jpeg"
140 case ".gif":
141 return "image/gif"
142 case ".webp":
143 return "image/webp"
144 case ".json":
145 return "application/json"
146 case ".xml":
147 return "application/xml"
148 case ".html", ".htm":
149 return "text/html; charset=utf-8"
150 default:
151 // Default to text/plain for unknown extensions
152 return "text/plain"
153 }
154}