v0.11.0 dc50343

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/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}

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