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/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
78 // Special-case homepage favicon: map .ico request to embedded SVG file
79 if (u.Host == constants.HomepagePath || u.Opaque == constants.HomepagePath) && strings.EqualFold(rel, "favicon.ico") {
80 log.Printf("[scheme] asset: rel=%s (host=%s path=%s) → mapping to favicon.svg", rel, u.Host, u.Path)
81 data, rerr := s.assets.ReadFile(filepath.ToSlash(filepath.Join("assets", "gui", "favicon.svg")))
82 if rerr == nil {
83 return "image/svg+xml", data, true
84 }
85 }
86
87 log.Printf("[scheme] asset: rel=%s (host=%s path=%s)", rel, u.Host, u.Path)
88
89 // Try to read the requested asset
90 data, rerr := s.assets.ReadFile(filepath.ToSlash(filepath.Join("assets", "gui", rel)))
91 if rerr != nil {
92 log.Printf("[scheme] not found: %s", rel)
93 return "", nil, false
94 }
95
96 // Determine mime type
97 mt := s.getMimeType(rel)
98
99 // Add cache control for development (prevents future caching issues)
100 // Note: This doesn't affect the current cache, but prevents new caching
101 log.Printf("[scheme] serving %s with mime-type: %s", rel, mt)
102 return mt, data, true
103}
104
105// getMimeType determines the MIME type for a given file path
106func (s *SchemeHandler) getMimeType(filename string) string {
107 ext := strings.ToLower(filepath.Ext(filename))
108
109 // Try standard mime type first
110 mt := mime.TypeByExtension(ext)
111 if mt != "" {
112 return mt
113 }
114
115 // Fallbacks for common web assets
116 switch ext {
117 case ".js":
118 return "application/javascript"
119 case ".mjs":
120 return "application/javascript"
121 case ".css":
122 return "text/css"
123 case ".svg":
124 return "image/svg+xml"
125 case ".ico":
126 return "image/x-icon"
127 case ".woff":
128 return "font/woff"
129 case ".woff2":
130 return "font/woff2"
131 case ".ttf":
132 return "font/ttf"
133 case ".otf":
134 return "font/otf"
135 case ".eot":
136 return "application/vnd.ms-fontobject"
137 case ".png":
138 return "image/png"
139 case ".jpg", ".jpeg":
140 return "image/jpeg"
141 case ".gif":
142 return "image/gif"
143 case ".webp":
144 return "image/webp"
145 case ".json":
146 return "application/json"
147 case ".xml":
148 return "application/xml"
149 case ".html", ".htm":
150 return "text/html; charset=utf-8"
151 default:
152 // Default to text/plain for unknown extensions
153 return "text/plain"
154 }
155}