v0.8.0 0fa6785

scheme.go RawHistoryFolder

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}

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