- commit
- affd3a6
- parent
- 8c20044
- author
- Eric Bower
- date
- 2024-07-19 00:27:17 -0400 EDT
feat: static assets folder Create a static folder that will be served as-is with the ability for users to bring-their-own static folder. If we detect `data_dir/static/` we will serve that instead of the embedded one we provide by default.
7 files changed,
+889,
-4
M
Makefile
+5,
-0
1@@ -26,3 +26,8 @@ bp: bp-setup
2 $(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-ssh:$(DOCKER_TAG)" --target release-ssh .
3 $(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-web:$(DOCKER_TAG)" --target release-web .
4 .PHONY: bp
5+
6+smol:
7+ curl https://pico.sh/smol.css -o ./static/smol.css
8+ cat patches/smol.diff | git apply
9+.PHONY: smol
+53,
-0
1@@ -0,0 +1,53 @@
2+diff --git a/static/smol.css b/static/smol.css
3+index e9b59ec..9e9d925 100644
4+--- a/static/smol.css
5++++ b/static/smol.css
6+@@ -15,48 +15,6 @@
7+ box-shadow: none;
8+ }
9+
10+-@media (prefers-color-scheme: light) {
11+- :root {
12+- --main-hue: 250;
13+- --white: #2e3f53;
14+- --white-light: #cfe0f4;
15+- --white-dark: #6c6a6a;
16+- --code: #52576f;
17+- --pre: #e1e7ee;
18+- --bg-color: #f4f4f4;
19+- --text-color: #24292f;
20+- --link-color: #005cc5;
21+- --visited: #6f42c1;
22+- --blockquote: #005cc5;
23+- --blockquote-bg: #cfe0f4;
24+- --hover: #c11e7a;
25+- --grey: #ccc;
26+- --grey-light: #6a708e;
27+- --shadow: #e8e8e8;
28+- }
29+-}
30+-
31+-@media (prefers-color-scheme: dark) {
32+- :root {
33+- --main-hue: 250;
34+- --white: #f2f2f2;
35+- --white-light: #f2f2f2;
36+- --white-dark: #e8e8e8;
37+- --code: #414558;
38+- --pre: #252525;
39+- --bg-color: #282a36;
40+- --text-color: #f2f2f2;
41+- --link-color: #8be9fd;
42+- --visited: #bd93f9;
43+- --blockquote: #bd93f9;
44+- --blockquote-bg: #353548;
45+- --hover: #ff80bf;
46+- --grey: #414558;
47+- --grey-light: #6a708e;
48+- --shadow: #252525;
49+- }
50+-}
51+-
52+ html {
53+ background-color: var(--bg-color);
54+ color: var(--text-color);
M
ssh.go
+1,
-1
1@@ -35,7 +35,7 @@ func GitSshServer(cfg *GitCfg) {
2 dbpath := filepath.Join(cfg.DataDir, "pr.db")
3 dbh, err := Open(dbpath, cfg.Logger)
4 if err != nil {
5- panic(fmt.Sprintf("cannot find database file, check folder and perms: %s", dbpath))
6+ panic(fmt.Sprintf("cannot find database file, check folder and perms: %s: %s", dbpath, err))
7 }
8
9 be := &Backend{
+726,
-0
1@@ -0,0 +1,726 @@
2+*,
3+::before,
4+::after {
5+ box-sizing: border-box;
6+}
7+
8+::-moz-focus-inner {
9+ border-style: none;
10+ padding: 0;
11+}
12+:-moz-focusring {
13+ outline: 1px dotted ButtonText;
14+}
15+:-moz-ui-invalid {
16+ box-shadow: none;
17+}
18+
19+html {
20+ background-color: var(--bg-color);
21+ color: var(--text-color);
22+ font-size: 18px;
23+ line-height: 1.5;
24+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
25+ Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
26+ sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
27+ -webkit-text-size-adjust: 100%;
28+ -moz-tab-size: 4;
29+ -o-tab-size: 4;
30+ tab-size: 4;
31+}
32+
33+body {
34+ margin: 0 auto;
35+}
36+
37+img {
38+ max-width: 100%;
39+ height: auto;
40+}
41+
42+b,
43+strong {
44+ font-weight: bold;
45+}
46+
47+code,
48+kbd,
49+samp,
50+pre {
51+ font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
52+ monospace;
53+}
54+
55+code,
56+kbd,
57+samp {
58+ border: 2px solid var(--code);
59+}
60+
61+pre > code {
62+ background-color: inherit;
63+ padding: 0;
64+ border: none;
65+}
66+
67+code {
68+ font-size: 90%;
69+ border-radius: 0.3rem;
70+ padding: 0.1rem 0.3rem;
71+}
72+
73+pre {
74+ font-size: 14px;
75+ border-radius: 5px;
76+ padding: 1rem;
77+ margin: 1rem 0;
78+ overflow-x: auto;
79+ background-color: var(--pre) !important;
80+}
81+
82+small {
83+ font-size: 0.8rem;
84+}
85+
86+summary {
87+ display: list-item;
88+ cursor: pointer;
89+}
90+
91+h1,
92+h2,
93+h3,
94+h4 {
95+ margin: 0;
96+ padding: 0.5rem 0 0 0;
97+ border: 0;
98+ font-style: normal;
99+ font-weight: inherit;
100+ font-size: inherit;
101+}
102+
103+path {
104+ fill: var(--text-color);
105+ stroke: var(--text-color);
106+}
107+
108+hr {
109+ color: inherit;
110+ border: 0;
111+ margin: 0;
112+ height: 2px;
113+ background: var(--grey);
114+ margin: 1rem auto;
115+ text-align: center;
116+}
117+
118+a {
119+ text-decoration: none;
120+ color: var(--link-color);
121+}
122+
123+a:hover,
124+a:visited:hover {
125+ text-decoration: underline;
126+ color: var(--hover);
127+}
128+
129+a:visited {
130+ color: var(--visited);
131+}
132+
133+a.link-grey {
134+ text-decoration: underline;
135+ color: var(--white);
136+}
137+
138+a.link-grey:visited {
139+ color: var(--white);
140+}
141+
142+section {
143+ margin-bottom: 1.4rem;
144+}
145+
146+section:last-child {
147+ margin-bottom: 0;
148+}
149+
150+header {
151+ margin: 1rem auto;
152+}
153+
154+p {
155+ margin: 0.5rem 0;
156+}
157+
158+article {
159+ overflow-wrap: break-word;
160+}
161+
162+blockquote {
163+ border-left: 5px solid var(--blockquote);
164+ background-color: var(--blockquote-bg);
165+ padding: 0.5rem 0.75rem;
166+ margin: 0.5rem 0;
167+}
168+
169+blockquote > p {
170+ margin: 0;
171+}
172+
173+blockquote code {
174+ border: 1px solid var(--blockquote);
175+}
176+
177+ul,
178+ol {
179+ padding: 0 0 0 1rem;
180+ list-style-position: outside;
181+}
182+
183+ul[style*="list-style-type: none;"] {
184+ padding: 0;
185+}
186+
187+li {
188+ margin: 0.5rem 0;
189+}
190+
191+li > pre {
192+ padding: 0;
193+}
194+
195+footer {
196+ text-align: center;
197+ margin-bottom: 4rem;
198+}
199+
200+dt {
201+ font-weight: bold;
202+}
203+
204+dd {
205+ margin-left: 0;
206+}
207+
208+dd:not(:last-child) {
209+ margin-bottom: 0.5rem;
210+}
211+
212+figure {
213+ margin: 0;
214+}
215+
216+.container {
217+ max-width: 50em;
218+ width: 100%;
219+}
220+
221+.container-sm {
222+ max-width: 40em;
223+ width: 100%;
224+}
225+
226+.container-center {
227+ width: 100%;
228+ height: 100%;
229+ display: flex;
230+ justify-content: center;
231+}
232+
233+.mono {
234+ font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
235+ monospace;
236+}
237+
238+.link-alt-adj,
239+.link-alt-adj:visited,
240+.link-alt-adj:visited:hover,
241+.link-alt-adj:hover {
242+ color: var(--link-color);
243+ text-decoration: none;
244+}
245+
246+.link-alt-adj:visited:hover,
247+.link-alt-adj:hover {
248+ text-decoration: underline;
249+}
250+
251+.link-alt-hover,
252+.link-alt-hover:visited,
253+.link-alt-hover:visited:hover,
254+.link-alt-hover:hover {
255+ color: var(--hover);
256+ text-decoration: none;
257+}
258+
259+.link-alt-hover:visited:hover,
260+.link-alt-hover:hover {
261+ text-decoration: underline;
262+}
263+
264+.link-alt,
265+.link-alt:visited,
266+.link-alt:visited:hover,
267+.link-alt:hover {
268+ color: var(--white);
269+ text-decoration: none;
270+}
271+
272+.link-alt:visited:hover,
273+.link-alt:hover {
274+ text-decoration: underline;
275+}
276+
277+.text-3xl {
278+ font-size: 2.5rem;
279+}
280+
281+.text-2xl {
282+ font-size: 1.9rem;
283+ line-height: 1.15;
284+}
285+
286+.text-xl {
287+ font-size: 1.55rem;
288+ line-height: 1.15;
289+}
290+
291+.text-lg {
292+ font-size: 1.35rem;
293+ line-height: 1.15;
294+}
295+
296+.text-md {
297+ font-size: 1.15rem;
298+ line-height: 1.15;
299+}
300+
301+.text-sm {
302+ font-size: 0.875rem;
303+}
304+
305+.text-xs {
306+ font-size: 0.775rem;
307+}
308+
309+.cursor-pointer {
310+ cursor: pointer;
311+}
312+
313+.w-full {
314+ width: 100%;
315+}
316+
317+.h-full {
318+ height: 100%;
319+}
320+
321+.border {
322+ border: 2px solid var(--grey-light);
323+}
324+
325+.text-left {
326+ text-align: left;
327+}
328+
329+.text-center {
330+ text-align: center;
331+}
332+
333+.text-underline {
334+ border-bottom: 3px solid var(--text-color);
335+ padding-bottom: 3px;
336+}
337+
338+.text-hdr {
339+ color: var(--hover);
340+}
341+
342+.text-underline-hdr {
343+ border-bottom: 3px solid var(--hover);
344+ padding-bottom: 3px;
345+}
346+
347+.font-bold {
348+ font-weight: bold;
349+}
350+
351+.font-italic {
352+ font-style: italic;
353+}
354+
355+.inline {
356+ display: inline;
357+}
358+
359+.inline-block {
360+ display: inline-block;
361+}
362+
363+.max-w-half {
364+ max-width: 50%;
365+}
366+
367+.h-screen {
368+ height: 100vh;
369+}
370+
371+.w-screen {
372+ width: 100vw;
373+}
374+
375+.flex {
376+ display: flex;
377+}
378+
379+.flex-col {
380+ flex-direction: column;
381+}
382+
383+.items-center {
384+ align-items: center;
385+}
386+
387+.m-0 {
388+ margin: 0;
389+}
390+
391+.mt {
392+ margin-top: 0.5rem;
393+}
394+
395+.mt-2 {
396+ margin-top: 1rem;
397+}
398+
399+.mt-4 {
400+ margin-top: 2rem;
401+}
402+
403+.mt-8 {
404+ margin-top: 4rem;
405+}
406+
407+.mb {
408+ margin-bottom: 0.5rem;
409+}
410+
411+.mb-2 {
412+ margin-bottom: 1rem;
413+}
414+
415+.mb-4 {
416+ margin-bottom: 2rem;
417+}
418+
419+.mb-8 {
420+ margin-bottom: 4rem;
421+}
422+
423+.mb-16 {
424+ margin-bottom: 8rem;
425+}
426+
427+.mr {
428+ margin-right: 0.5rem;
429+}
430+
431+.ml-sm {
432+ margin-left: 0.25rem;
433+}
434+
435+.ml {
436+ margin-left: 0.5rem;
437+}
438+
439+.pt-0 {
440+ padding-top: 0;
441+}
442+
443+.my {
444+ margin-top: 0.5rem;
445+ margin-bottom: 0.5rem;
446+}
447+
448+.my-2 {
449+ margin-top: 1rem;
450+ margin-bottom: 1rem;
451+}
452+
453+.my-4 {
454+ margin-top: 2rem;
455+ margin-bottom: 2rem;
456+}
457+
458+.my-8 {
459+ margin-top: 4rem;
460+ margin-bottom: 4rem;
461+}
462+
463+.mx {
464+ margin-left: 0.5rem;
465+ margin-right: 0.5rem;
466+}
467+
468+.mx-2 {
469+ margin-left: 1rem;
470+ margin-right: 1rem;
471+}
472+
473+.m-1 {
474+ margin: 0.5rem;
475+}
476+
477+.p-1 {
478+ padding: 0.5rem;
479+}
480+
481+.p-0 {
482+ padding: 0;
483+}
484+
485+.px-2 {
486+ padding-left: 1rem;
487+ padding-right: 1rem;
488+}
489+
490+.px-4 {
491+ padding-left: 2rem;
492+ padding-right: 2rem;
493+}
494+
495+.py {
496+ padding-top: 0.5rem;
497+ padding-bottom: 0.5rem;
498+}
499+
500+.py-2 {
501+ padding-top: 1rem;
502+ padding-bottom: 1rem;
503+}
504+
505+.py-4 {
506+ padding-top: 2rem;
507+ padding-bottom: 2rem;
508+}
509+
510+.py-8 {
511+ padding-top: 4rem;
512+ padding-bottom: 4rem;
513+}
514+
515+.justify-between {
516+ justify-content: space-between;
517+}
518+
519+.justify-center {
520+ justify-content: center;
521+}
522+
523+.gap {
524+ gap: 0.5rem;
525+}
526+
527+.gap-2 {
528+ gap: 1rem;
529+}
530+
531+.group {
532+ display: flex;
533+ flex-direction: column;
534+ gap: 0.5rem;
535+}
536+
537+.group-2 {
538+ display: flex;
539+ flex-direction: column;
540+ gap: 1rem;
541+}
542+
543+.group-h {
544+ display: flex;
545+ gap: 0.5rem;
546+ align-items: center;
547+}
548+
549+.flex-1 {
550+ flex: 1;
551+}
552+
553+.items-end {
554+ align-items: end;
555+}
556+
557+.items-start {
558+ align-items: start;
559+}
560+
561+.justify-end {
562+ justify-content: end;
563+}
564+
565+.font-grey-light {
566+ color: var(--grey-light);
567+}
568+
569+.hidden {
570+ display: none;
571+}
572+
573+.align-right {
574+ text-align: right;
575+}
576+
577+/* ==== MARKDOWN ==== */
578+
579+.md h1,
580+.md h2,
581+.md h3,
582+.md h4 {
583+ padding: 0;
584+ margin: 1.5rem 0 0.9rem 0;
585+ font-weight: bold;
586+}
587+
588+.md h1 a,
589+.md h2 a,
590+.md h3 a,
591+.md h4 a {
592+ color: var(--grey-light);
593+ text-decoration: none;
594+}
595+
596+.md h1 {
597+ font-size: 1.6rem;
598+ line-height: 1.15;
599+ border-bottom: 2px solid var(--grey);
600+ padding-bottom: 0.7rem;
601+}
602+
603+.md h2 {
604+ font-size: 1.3rem;
605+ line-height: 1.15;
606+ color: var(--white-dark);
607+}
608+
609+.md h3 {
610+ font-size: 1.2rem;
611+ color: var(--white-dark);
612+}
613+
614+.md h4 {
615+ font-size: 1rem;
616+ color: var(--white-dark);
617+}
618+
619+/* ==== HELPERS ==== */
620+
621+.logo-header {
622+ line-height: 1;
623+ display: inline-block;
624+ background-color: #FF79C6;
625+ background-image: linear-gradient(to right, #FF5555, #FF79C6, #F8F859);
626+ color: transparent;
627+ background-clip: text;
628+ border: 3px solid #FF79C6;
629+ padding: 8px 10px 10px 10px;
630+ border-radius: 10px;
631+ box-shadow: 0px 5px 0px 0px var(--shadow);
632+ background-size: 100%;
633+ -webkit-background-clip: text;
634+ -moz-background-clip: text;
635+ -webkit-text-fill-color: transparent;
636+ -moz-text-fill-color: transparent;
637+}
638+
639+.btn {
640+ border: 2px solid var(--link-color);
641+ color: var(--link-color);
642+ padding: 0.4rem 1rem;
643+ font-weight: bold;
644+ display: inline-block;
645+}
646+
647+.btn-link,
648+.btn-link:visited {
649+ border: 2px solid var(--link-color);
650+ color: var(--link-color);
651+ padding: 0.4rem 1rem;
652+ text-decoration: none;
653+ font-weight: bold;
654+ display: inline-block;
655+}
656+
657+.btn-link:visited:hover,
658+.btn-link:hover {
659+ border: 2px solid var(--hover);
660+}
661+
662+.btn-link-alt,
663+.btn-link-alt:visited {
664+ border: 2px solid var(--white);
665+ color: var(--white);
666+}
667+
668+.box {
669+ border: 2px solid var(--grey-light);
670+ padding: 0.5rem 0.75rem;
671+}
672+
673+.box-sm {
674+ border: 2px solid var(--grey-light);
675+ padding: 0.15rem 0.35rem;
676+}
677+
678+.box-alert {
679+ border: 2px solid var(--hover);
680+ padding: 0.5rem 0.75rem;
681+}
682+
683+.box-sm-alert {
684+ border: 2px solid var(--hover);
685+ padding: 0.15rem 0.35rem;
686+}
687+
688+.list-none {
689+ list-style-type: none;
690+}
691+
692+.list-disc {
693+ list-style-type: disc;
694+}
695+
696+.list-decimal {
697+ list-style-type: decimal;
698+}
699+
700+.pill {
701+ border: 1px solid var(--link-color);
702+ color: var(--link-color);
703+}
704+
705+.pill-alert {
706+ border: 1px solid var(--hover);
707+ color: var(--hover);
708+}
709+
710+.pill-info {
711+ border: 1px solid var(--visited);
712+ color: var(--visited);
713+}
714+
715+@media only screen and (max-width: 40em) {
716+ body {
717+ padding: 0 1rem;
718+ }
719+
720+ header {
721+ margin: 0;
722+ }
723+
724+ .flex-collapse {
725+ flex-direction: column;
726+ }
727+}
+18,
-0
1@@ -0,0 +1,18 @@
2+:root {
3+ --main-hue: 250;
4+ --white: #f2f2f2;
5+ --white-light: #f2f2f2;
6+ --white-dark: #e8e8e8;
7+ --code: #414558;
8+ --pre: #252525;
9+ --bg-color: #282a36;
10+ --text-color: #f2f2f2;
11+ --link-color: #8be9fd;
12+ --visited: #bd93f9;
13+ --blockquote: #bd93f9;
14+ --blockquote-bg: #353548;
15+ --hover: #ff80bf;
16+ --grey: #414558;
17+ --grey-light: #6a708e;
18+ --shadow: #252525;
19+}
+3,
-2
1@@ -9,8 +9,9 @@
2 <meta name="keywords" content="git, collaboration, patch, requests" />
3 {{template "meta" .}}
4
5- <link rel="stylesheet" href="https://pico.sh/smol.css" />
6- <link rel="stylesheet" href="https://pico.sh/syntax.css" />
7+ <link rel="stylesheet" href="/static/smol.css" />
8+ <link rel="stylesheet" href="/static/vars.css" />
9+ <link rel="stylesheet" href="/syntax.css" />
10 </head>
11 <body class="container">{{template "body" .}}</body>
12 </html>
M
web.go
+83,
-1
1@@ -6,8 +6,12 @@ import (
2 "embed"
3 "fmt"
4 "html/template"
5+ "io"
6+ "io/fs"
7 "log/slog"
8+ "mime"
9 "net/http"
10+ "os"
11 "path/filepath"
12 "slices"
13 "strconv"
14@@ -23,6 +27,9 @@ import (
15 //go:embed tmpl/*
16 var tmplFS embed.FS
17
18+//go:embed static/*
19+var embedStaticFS embed.FS
20+
21 type WebCtx struct {
22 Pr *PrCmd
23 Backend *Backend
24@@ -618,13 +625,81 @@ func chromaStyleHandler(w http.ResponseWriter, r *http.Request) {
25 }
26 }
27
28+func serveFile(userfs fs.FS, embedfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
29+ return func(w http.ResponseWriter, r *http.Request) {
30+ web, err := getWebCtx(r)
31+ if err != nil {
32+ w.WriteHeader(http.StatusUnprocessableEntity)
33+ return
34+ }
35+ logger := web.Logger
36+
37+ file := r.PathValue("file")
38+
39+ logger.Info("serving file", "file", file)
40+ // merging both embedded fs and whatever user provides
41+ var reader fs.File
42+ if userfs == nil {
43+ reader, err = embedfs.Open(file)
44+ } else {
45+ reader, err = userfs.Open(file)
46+ if err != nil {
47+ // serve embeded static folder
48+ reader, err = embedfs.Open(file)
49+ }
50+ }
51+
52+ if err != nil {
53+ logger.Error(err.Error())
54+ http.Error(w, "file not found", 404)
55+ return
56+ }
57+
58+ contents, err := io.ReadAll(reader)
59+ if err != nil {
60+ logger.Error(err.Error())
61+ http.Error(w, "file not found", 404)
62+ return
63+ }
64+ contentType := mime.TypeByExtension(filepath.Ext(file))
65+ if contentType == "" {
66+ contentType = http.DetectContentType(contents)
67+ }
68+ w.Header().Add("Content-Type", contentType)
69+
70+ _, err = w.Write(contents)
71+ if err != nil {
72+ logger.Error(err.Error())
73+ http.Error(w, "server error", 500)
74+ return
75+ }
76+ }
77+}
78+
79+func getUserDefinedFS(datadir, dirName string) fs.FS {
80+ dir := filepath.Join(datadir, dirName)
81+ _, err := os.Stat(dir)
82+ if err != nil {
83+ return nil
84+ }
85+ return os.DirFS(dir)
86+}
87+
88+func getEmbedFS(ffs embed.FS, dirName string) (fs.FS, error) {
89+ fsys, err := fs.Sub(ffs, dirName)
90+ if err != nil {
91+ return nil, err
92+ }
93+ return fsys, nil
94+}
95+
96 func StartWebServer(cfg *GitCfg) {
97 addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
98
99 dbpath := filepath.Join(cfg.DataDir, "pr.db")
100 dbh, err := Open(dbpath, cfg.Logger)
101 if err != nil {
102- panic(fmt.Sprintf("cannot find database file, check folder and perms: %s", dbpath))
103+ panic(fmt.Sprintf("cannot find database file, check folder and perms: %s: %s", dbpath, err))
104 }
105
106 be := &Backend{
107@@ -659,6 +734,13 @@ func StartWebServer(cfg *GitCfg) {
108 http.HandleFunc("GET /", ctxMdw(ctx, repoListHandler))
109 http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
110 http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
111+ embedFS, err := getEmbedFS(embedStaticFS, "static")
112+ if err != nil {
113+ panic(err)
114+ }
115+ userFS := getUserDefinedFS(cfg.DataDir, "static")
116+
117+ http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(userFS, embedFS)))
118
119 cfg.Logger.Info("starting web server", "addr", addr)
120 err = http.ListenAndServe(addr, nil)