detached 091d5df

add logo to callback page

commit
091d5df5efa08fdaf82831cee845684155d66b65
author
patriceckhart · · 2026-04-18 08:15 UTC
parent
d8a0cba
signature
unsigned
4 files · +58 -15
A internal/assets/assets.go +13 -0
@@ -0,0 +1,13 @@
1+// Package assets holds static resources embedded in the zot binary.
2+// Currently just the zot logo used by the tui welcome banner.
3+package assets
4+
5+import _ "embed"
6+
7+// LogoPNG is the pixel-art zot `z` logo as PNG bytes.
8+// Used by the interactive welcome banner; decoded once and rasterized
9+// to Unicode half-blocks so it renders on any terminal without needing
10+// inline image support.
11+//
12+//go:embed zot-logo.png
13+var LogoPNG []byte
@@ -0,0 +1,13 @@
1+// Package assets holds static resources embedded in the zot binary.
2+// Currently just the zot logo used by the tui welcome banner.
3+package assets
4+
5+import _ "embed"
6+
7+// LogoPNG is the pixel-art zot `z` logo as PNG bytes.
8+// Used by the interactive welcome banner; decoded once and rasterized
9+// to Unicode half-blocks so it renders on any terminal without needing
10+// inline image support.
11+//
12+//go:embed zot-logo.png
13+var LogoPNG []byte
A internal/assets/zot-logo.png +0 -0 binary file: no text diff

binary file: no text diff

M internal/auth/callback.go +40 -2
@@ -8,6 +8,8 @@ import (
88 "strings"
99 "sync"
1010 "time"
11+
12+ "github.com/patriceckhart/zot/internal/assets"
1113 )
1214
1315 // CallbackResult is what an OAuth callback server returns once the
@@ -47,6 +49,7 @@ func NewCallbackServer(p OAuthProvider, expectedState string) (*CallbackServer,
4749 }
4850 mux := http.NewServeMux()
4951 mux.HandleFunc(p.RedirectPath, cs.handle)
52+ mux.HandleFunc("/logo.png", serveLogo)
5053 cs.srv = &http.Server{
5154 Handler: mux,
5255 ReadTimeout: 15 * time.Second,
@@ -119,8 +122,9 @@ func (cs *CallbackServer) deliver(r CallbackResult) {
119122
120123 // ---- static HTML for the callback tab ----
121124
122-// All zot-served pages share a single monochrome style: black text on
123-// white, thin horizontal rules, monospace type. Matches the tui aesthetic.
125+// All zot-served pages share a single style: white background, black
126+// text, the cyan pixel-art `z` logo at the top, thin black rules,
127+// monospace type.
124128 const monoStyle = `<style>
125129 :root { color-scheme: light; }
126130 * { box-sizing: border-box; }
@@ -133,6 +137,14 @@ const monoStyle = `<style>
133137 padding: 3rem 1.5rem;
134138 line-height: 1.55;
135139 }
140+ .logo {
141+ display: block;
142+ width: 120px;
143+ height: auto;
144+ image-rendering: pixelated;
145+ image-rendering: crisp-edges;
146+ margin: 0 0 1.5rem;
147+ }
136148 h1 {
137149 font-size: 1rem;
138150 font-weight: 600;
@@ -145,11 +157,36 @@ const monoStyle = `<style>
145157 .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; word-break: break-all; }
146158 .msg { padding: 0.75rem 0; }
147159 a { color: inherit; }
160+ input[type=password], input[type=text] {
161+ width: 100%; padding: 0.5rem 0.6rem;
162+ border: 1px solid #000000; background: #ffffff; color: #000000;
163+ font-family: inherit; font-size: 0.95rem;
164+ }
165+ button {
166+ padding: 0.5rem 1.25rem;
167+ background: #000000; color: #ffffff;
168+ border: 1px solid #000000; font-family: inherit; font-size: 0.95rem;
169+ cursor: pointer;
170+ }
171+ button:hover { background: #ffffff; color: #000000; }
148172 </style>`
149173
174+// logoTag is the <img> element used at the top of every zot-served
175+// page. The image bytes are served from /logo.png by the same server.
176+const logoTag = `<img class="logo" src="/logo.png" alt="zot" />`
177+
178+// serveLogo writes the embedded PNG with appropriate caching headers.
179+// Shared between the oauth callback server and the api-key login server.
180+func serveLogo(w http.ResponseWriter, r *http.Request) {
181+ w.Header().Set("content-type", "image/png")
182+ w.Header().Set("cache-control", "public, max-age=86400")
183+ _, _ = w.Write(assets.LogoPNG)
184+}
185+
150186 func oauthSuccessHTML(provider string) string {
151187 p := strings.ToLower(provider)
152188 return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · logged in</title>` + monoStyle + `</head><body>
189+` + logoTag + `
153190 <h1><span class="mark">✓</span> logged in to ` + p + `</h1>
154191 <hr class="rule">
155192 <p class="msg">zot received the callback. you can close this tab.</p>
@@ -158,6 +195,7 @@ func oauthSuccessHTML(provider string) string {
158195
159196 func oauthErrorHTML(msg string) string {
160197 return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · error</title>` + monoStyle + `</head><body>
198+` + logoTag + `
161199 <h1><span class="mark">✗</span> login failed</h1>
162200 <hr class="rule">
163201 <p class="msg mono">` + htmlEscape(msg) + `</p>
@@ -8,6 +8,8 @@ import (
8 "strings"8 "strings"
9 "sync"9 "sync"
10 "time"10 "time"
11+
12+ "github.com/patriceckhart/zot/internal/assets"
11 )13 )
12 14
13 // CallbackResult is what an OAuth callback server returns once the15 // CallbackResult is what an OAuth callback server returns once the
@@ -47,6 +49,7 @@ func NewCallbackServer(p OAuthProvider, expectedState string) (*CallbackServer,
47 }49 }
48 mux := http.NewServeMux()50 mux := http.NewServeMux()
49 mux.HandleFunc(p.RedirectPath, cs.handle)51 mux.HandleFunc(p.RedirectPath, cs.handle)
52+ mux.HandleFunc("/logo.png", serveLogo)
50 cs.srv = &http.Server{53 cs.srv = &http.Server{
51 Handler: mux,54 Handler: mux,
52 ReadTimeout: 15 * time.Second,55 ReadTimeout: 15 * time.Second,
@@ -119,8 +122,9 @@ func (cs *CallbackServer) deliver(r CallbackResult) {
119 122
120 // ---- static HTML for the callback tab ----123 // ---- static HTML for the callback tab ----
121 124
122-// All zot-served pages share a single monochrome style: black text on125+// All zot-served pages share a single style: white background, black
123-// white, thin horizontal rules, monospace type. Matches the tui aesthetic.126+// text, the cyan pixel-art `z` logo at the top, thin black rules,
127+// monospace type.
124 const monoStyle = `<style>128 const monoStyle = `<style>
125 :root { color-scheme: light; }129 :root { color-scheme: light; }
126 * { box-sizing: border-box; }130 * { box-sizing: border-box; }
@@ -133,6 +137,14 @@ const monoStyle = `<style>
133 padding: 3rem 1.5rem;137 padding: 3rem 1.5rem;
134 line-height: 1.55;138 line-height: 1.55;
135 }139 }
140+ .logo {
141+ display: block;
142+ width: 120px;
143+ height: auto;
144+ image-rendering: pixelated;
145+ image-rendering: crisp-edges;
146+ margin: 0 0 1.5rem;
147+ }
136 h1 {148 h1 {
137 font-size: 1rem;149 font-size: 1rem;
138 font-weight: 600;150 font-weight: 600;
@@ -145,11 +157,36 @@ const monoStyle = `<style>
145 .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; word-break: break-all; }157 .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; word-break: break-all; }
146 .msg { padding: 0.75rem 0; }158 .msg { padding: 0.75rem 0; }
147 a { color: inherit; }159 a { color: inherit; }
160+ input[type=password], input[type=text] {
161+ width: 100%; padding: 0.5rem 0.6rem;
162+ border: 1px solid #000000; background: #ffffff; color: #000000;
163+ font-family: inherit; font-size: 0.95rem;
164+ }
165+ button {
166+ padding: 0.5rem 1.25rem;
167+ background: #000000; color: #ffffff;
168+ border: 1px solid #000000; font-family: inherit; font-size: 0.95rem;
169+ cursor: pointer;
170+ }
171+ button:hover { background: #ffffff; color: #000000; }
148 </style>`172 </style>`
149 173
174+// logoTag is the <img> element used at the top of every zot-served
175+// page. The image bytes are served from /logo.png by the same server.
176+const logoTag = `<img class="logo" src="/logo.png" alt="zot" />`
177+
178+// serveLogo writes the embedded PNG with appropriate caching headers.
179+// Shared between the oauth callback server and the api-key login server.
180+func serveLogo(w http.ResponseWriter, r *http.Request) {
181+ w.Header().Set("content-type", "image/png")
182+ w.Header().Set("cache-control", "public, max-age=86400")
183+ _, _ = w.Write(assets.LogoPNG)
184+}
185+
150 func oauthSuccessHTML(provider string) string {186 func oauthSuccessHTML(provider string) string {
151 p := strings.ToLower(provider)187 p := strings.ToLower(provider)
152 return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · logged in</title>` + monoStyle + `</head><body>188 return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · logged in</title>` + monoStyle + `</head><body>
189+` + logoTag + `
153 <h1><span class="mark">✓</span> logged in to ` + p + `</h1>190 <h1><span class="mark">✓</span> logged in to ` + p + `</h1>
154 <hr class="rule">191 <hr class="rule">
155 <p class="msg">zot received the callback. you can close this tab.</p>192 <p class="msg">zot received the callback. you can close this tab.</p>
@@ -158,6 +195,7 @@ func oauthSuccessHTML(provider string) string {
158 195
159 func oauthErrorHTML(msg string) string {196 func oauthErrorHTML(msg string) string {
160 return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · error</title>` + monoStyle + `</head><body>197 return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · error</title>` + monoStyle + `</head><body>
198+` + logoTag + `
161 <h1><span class="mark">✗</span> login failed</h1>199 <h1><span class="mark">✗</span> login failed</h1>
162 <hr class="rule">200 <hr class="rule">
163 <p class="msg mono">` + htmlEscape(msg) + `</p>201 <p class="msg mono">` + htmlEscape(msg) + `</p>
M internal/auth/server.go +5 -13
@@ -62,6 +62,7 @@ func NewServer() (*Server, error) {
6262 mux.HandleFunc("/callback", s.handleCallback)
6363 mux.HandleFunc("/success", s.handleSuccess)
6464 mux.HandleFunc("/error", s.handleError)
65+ mux.HandleFunc("/logo.png", serveLogo)
6566 s.srv = &http.Server{
6667 Handler: mux,
6768 ReadTimeout: 15 * time.Second,
@@ -219,6 +220,7 @@ func Redirect(w http.ResponseWriter, r *http.Request, u *url.URL) {
219220 // rules, no rounded boxes, no color.
220221
221222 var tpl = template.Must(template.New("index").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot login</title>` + monoStyle + `</head><body>
223+` + logoTag + `
222224 <h1>zot login</h1>
223225 <hr class="rule">
224226 <p>paste an api key for anthropic or openai. zot probes the provider once, then saves the key to <span class="mono">~/Library/Application Support/zot/auth.json</span>.</p>
@@ -234,20 +236,8 @@ func init() {
234236 template.Must(tpl.New("apikey").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot login</title>` + monoStyle + `<style>
235237 form { display: flex; flex-direction: column; gap: 0.75rem; }
236238 label { font-size: 0.875rem; }
237- input[type=password] {
238- width: 100%; padding: 0.5rem 0.6rem;
239- border: 1px solid #000000; background: #ffffff; color: #000000;
240- font-family: inherit; font-size: 0.95rem;
241- }
242- button {
243- align-self: flex-start;
244- padding: 0.5rem 1.25rem;
245- background: #000000; color: #ffffff;
246- border: 1px solid #000000; font-family: inherit; font-size: 0.95rem;
247- cursor: pointer;
248- }
249- button:hover { background: #ffffff; color: #000000; }
250239 </style></head><body>
240+` + logoTag + `
251241 <h1>zot login · {{.Provider}} api key</h1>
252242 <hr class="rule">
253243 <p>paste your {{.Provider}} api key. zot will probe the provider with it once, then save it if the key is accepted.</p>
@@ -260,6 +250,7 @@ func init() {
260250 </body></html>`))
261251
262252 template.Must(tpl.New("success").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · logged in</title>` + monoStyle + `</head><body>
253+` + logoTag + `
263254 <h1><span class="mark">✓</span> logged in to {{.Provider}}</h1>
264255 <hr class="rule">
265256 <p class="msg">method: {{.Method}}</p>
@@ -267,6 +258,7 @@ func init() {
267258 </body></html>`))
268259
269260 template.Must(tpl.New("error").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · error</title>` + monoStyle + `</head><body>
261+` + logoTag + `
270262 <h1><span class="mark">✗</span> login failed</h1>
271263 <hr class="rule">
272264 <p class="msg mono">{{.Message}}</p>
@@ -62,6 +62,7 @@ func NewServer() (*Server, error) {
62 mux.HandleFunc("/callback", s.handleCallback)62 mux.HandleFunc("/callback", s.handleCallback)
63 mux.HandleFunc("/success", s.handleSuccess)63 mux.HandleFunc("/success", s.handleSuccess)
64 mux.HandleFunc("/error", s.handleError)64 mux.HandleFunc("/error", s.handleError)
65+ mux.HandleFunc("/logo.png", serveLogo)
65 s.srv = &http.Server{66 s.srv = &http.Server{
66 Handler: mux,67 Handler: mux,
67 ReadTimeout: 15 * time.Second,68 ReadTimeout: 15 * time.Second,
@@ -219,6 +220,7 @@ func Redirect(w http.ResponseWriter, r *http.Request, u *url.URL) {
219 // rules, no rounded boxes, no color.220 // rules, no rounded boxes, no color.
220 221
221 var tpl = template.Must(template.New("index").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot login</title>` + monoStyle + `</head><body>222 var tpl = template.Must(template.New("index").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot login</title>` + monoStyle + `</head><body>
223+` + logoTag + `
222 <h1>zot login</h1>224 <h1>zot login</h1>
223 <hr class="rule">225 <hr class="rule">
224 <p>paste an api key for anthropic or openai. zot probes the provider once, then saves the key to <span class="mono">~/Library/Application Support/zot/auth.json</span>.</p>226 <p>paste an api key for anthropic or openai. zot probes the provider once, then saves the key to <span class="mono">~/Library/Application Support/zot/auth.json</span>.</p>
@@ -234,20 +236,8 @@ func init() {
234 template.Must(tpl.New("apikey").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot login</title>` + monoStyle + `<style>236 template.Must(tpl.New("apikey").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot login</title>` + monoStyle + `<style>
235 form { display: flex; flex-direction: column; gap: 0.75rem; }237 form { display: flex; flex-direction: column; gap: 0.75rem; }
236 label { font-size: 0.875rem; }238 label { font-size: 0.875rem; }
237- input[type=password] {
238- width: 100%; padding: 0.5rem 0.6rem;
239- border: 1px solid #000000; background: #ffffff; color: #000000;
240- font-family: inherit; font-size: 0.95rem;
241- }
242- button {
243- align-self: flex-start;
244- padding: 0.5rem 1.25rem;
245- background: #000000; color: #ffffff;
246- border: 1px solid #000000; font-family: inherit; font-size: 0.95rem;
247- cursor: pointer;
248- }
249- button:hover { background: #ffffff; color: #000000; }
250 </style></head><body>239 </style></head><body>
240+` + logoTag + `
251 <h1>zot login · {{.Provider}} api key</h1>241 <h1>zot login · {{.Provider}} api key</h1>
252 <hr class="rule">242 <hr class="rule">
253 <p>paste your {{.Provider}} api key. zot will probe the provider with it once, then save it if the key is accepted.</p>243 <p>paste your {{.Provider}} api key. zot will probe the provider with it once, then save it if the key is accepted.</p>
@@ -260,6 +250,7 @@ func init() {
260 </body></html>`))250 </body></html>`))
261 251
262 template.Must(tpl.New("success").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · logged in</title>` + monoStyle + `</head><body>252 template.Must(tpl.New("success").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · logged in</title>` + monoStyle + `</head><body>
253+` + logoTag + `
263 <h1><span class="mark">✓</span> logged in to {{.Provider}}</h1>254 <h1><span class="mark">✓</span> logged in to {{.Provider}}</h1>
264 <hr class="rule">255 <hr class="rule">
265 <p class="msg">method: {{.Method}}</p>256 <p class="msg">method: {{.Method}}</p>
@@ -267,6 +258,7 @@ func init() {
267 </body></html>`))258 </body></html>`))
268 259
269 template.Must(tpl.New("error").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · error</title>` + monoStyle + `</head><body>260 template.Must(tpl.New("error").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · error</title>` + monoStyle + `</head><body>
261+` + logoTag + `
270 <h1><span class="mark">✗</span> login failed</h1>262 <h1><span class="mark">✗</span> login failed</h1>
271 <hr class="rule">263 <hr class="rule">
272 <p class="msg mono">{{.Message}}</p>264 <p class="msg mono">{{.Message}}</p>

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 1

Tags 476

↑↓ moveenter openesc close