dashboard / git-pr / feat: static assets #3 rss

accepted · opened on 2024-07-19T14:53:23Z by erock
Help
# add changes to patch request
git format-patch main --stdout | ssh pr.pico.sh pr add 3
# add review to patch request
git format-patch main --stdout | ssh pr.pico.sh pr add --review 3
# remove patchset
ssh pr.pico.sh ps rm ps-x
# checkout all patches
ssh pr.pico.sh pr print 3 | git am -3
# print a diff between the last two patches in a patch request
ssh pr.pico.sh pr diff 3
# accept PR
ssh pr.pico.sh pr accept 3
# close PR
ssh pr.pico.sh pr close 3

Logs

erock created pr with ps-3 on 2024-07-19T14:53:23Z
erock added ps-4 on 2024-07-19T14:55:48Z
jolheiser reviewed pr with ps-5 on 2024-07-19T15:42:29Z
jolheiser changed status on 2024-07-19T15:42:29Z {"status":"reviewed"}
erock added ps-9 on 2024-07-19T17:43:47Z
jolheiser changed status on 2024-07-19T18:27:53Z {"status":"accepted"}

Patchsets

ps-3 by erock on 2024-07-19T14:53:23Z
Range Diff ↕
3: 7346122 < -: ------- chore: create patch for smol
4: d8792d5 < -: ------- feat: static folder
1: 8919af5 ! 2: 66cafc6 feat: static assets folder

@@ static/smol.css
+*,
+::before,
+::after {
+  box-sizing: border-box;
+}
+
+::-moz-focus-inner {
+  border-style: none;
+  padding: 0;
+}
+:-moz-focusring {
+  outline: 1px dotted ButtonText;
+}
+:-moz-ui-invalid {
+  box-shadow: none;
+}
+
+@media (prefers-color-scheme: light) {
+  :root {
+    --main-hue: 250;
+    --white: #2e3f53;
+    --white-light: #cfe0f4;
+    --white-dark: #6c6a6a;
+    --code: #52576f;
+    --pre: #e1e7ee;
+    --bg-color: #f4f4f4;
+    --text-color: #24292f;
+    --link-color: #005cc5;
+    --visited: #6f42c1;
+    --blockquote: #005cc5;
+    --blockquote-bg: #cfe0f4;
+    --hover: #c11e7a;
+    --grey: #ccc;
+    --grey-light: #6a708e;
+    --shadow: #e8e8e8;
+  }
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --main-hue: 250;
+    --white: #f2f2f2;
+    --white-light: #f2f2f2;
+    --white-dark: #e8e8e8;
+    --code: #414558;
+    --pre: #252525;
+    --bg-color: #282a36;
+    --text-color: #f2f2f2;
+    --link-color: #8be9fd;
+    --visited: #bd93f9;
+    --blockquote: #bd93f9;
+    --blockquote-bg: #353548;
+    --hover: #ff80bf;
+    --grey: #414558;
+    --grey-light: #6a708e;
+    --shadow: #252525;
+  }
+}
+
+html {
+  background-color: var(--bg-color);
+  color: var(--text-color);
+  font-size: 18px;
+  line-height: 1.5;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
+    Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
+    sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+  -webkit-text-size-adjust: 100%;
+  -moz-tab-size: 4;
+  -o-tab-size: 4;
+  tab-size: 4;
+}
+
+body {
+  margin: 0 auto;
+}
+
+img {
+  max-width: 100%;
+  height: auto;
+}
+
+b,
+strong {
+  font-weight: bold;
+}
+
+code,
+kbd,
+samp,
+pre {
+  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
+    monospace;
+}
+
+code,
+kbd,
+samp {
+  border: 2px solid var(--code);
+}
+
+pre > code {
+  background-color: inherit;
+  padding: 0;
+  border: none;
+}
+
+code {
+  font-size: 90%;
+  border-radius: 0.3rem;
+  padding: 0.1rem 0.3rem;
+}
+
+pre {
+  font-size: 14px;
+  border-radius: 5px;
+  padding: 1rem;
+  margin: 1rem 0;
+  overflow-x: auto;
+  background-color: var(--pre) !important;
+}
+
+small {
+  font-size: 0.8rem;
+}
+
+summary {
+  display: list-item;
+  cursor: pointer;
+}
+
+h1,
+h2,
+h3,
+h4 {
+  margin: 0;
+  padding: 0.5rem 0 0 0;
+  border: 0;
+  font-style: normal;
+  font-weight: inherit;
+  font-size: inherit;
+}
+
+path {
+  fill: var(--text-color);
+  stroke: var(--text-color);
+}
+
+hr {
+  color: inherit;
+  border: 0;
+  margin: 0;
+  height: 2px;
+  background: var(--grey);
+  margin: 1rem auto;
+  text-align: center;
+}
+
+a {
+  text-decoration: none;
+  color: var(--link-color);
+}
+
+a:hover,
+a:visited:hover {
+  text-decoration: underline;
+  color: var(--hover);
+}
+
+a:visited {
+  color: var(--visited);
+}
+
+a.link-grey {
+  text-decoration: underline;
+  color: var(--white);
+}
+
+a.link-grey:visited {
+  color: var(--white);
+}
+
+section {
+  margin-bottom: 1.4rem;
+}
+
+section:last-child {
+  margin-bottom: 0;
+}
+
+header {
+  margin: 1rem auto;
+}
+
+p {
+  margin: 0.5rem 0;
+}
+
+article {
+  overflow-wrap: break-word;
+}
+
+blockquote {
+  border-left: 5px solid var(--blockquote);
+  background-color: var(--blockquote-bg);
+  padding: 0.5rem 0.75rem;
+  margin: 0.5rem 0;
+}
+
+blockquote > p {
+  margin: 0;
+}
+
+blockquote code {
+  border: 1px solid var(--blockquote);
+}
+
+ul,
+ol {
+  padding: 0 0 0 1rem;
+  list-style-position: outside;
+}
+
+ul[style*="list-style-type: none;"] {
+  padding: 0;
+}
+
+li {
+  margin: 0.5rem 0;
+}
+
+li > pre {
+  padding: 0;
+}
+
+footer {
+  text-align: center;
+  margin-bottom: 4rem;
+}
+
+dt {
+  font-weight: bold;
+}
+
+dd {
+  margin-left: 0;
+}
+
+dd:not(:last-child) {
+  margin-bottom: 0.5rem;
+}
+
+figure {
+  margin: 0;
+}
+
+.container {
+  max-width: 50em;
+  width: 100%;
+}
+
+.container-sm {
+  max-width: 40em;
+  width: 100%;
+}
+
+.container-center {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+}
+
+.mono {
+  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
+    monospace;
+}
+
+.link-alt-adj,
+.link-alt-adj:visited,
+.link-alt-adj:visited:hover,
+.link-alt-adj:hover {
+  color: var(--link-color);
+  text-decoration: none;
+}
+
+.link-alt-adj:visited:hover,
+.link-alt-adj:hover {
+  text-decoration: underline;
+}
+
+.link-alt-hover,
+.link-alt-hover:visited,
+.link-alt-hover:visited:hover,
+.link-alt-hover:hover {
+  color: var(--hover);
+  text-decoration: none;
+}
+
+.link-alt-hover:visited:hover,
+.link-alt-hover:hover {
+  text-decoration: underline;
+}
+
+.link-alt,
+.link-alt:visited,
+.link-alt:visited:hover,
+.link-alt:hover {
+  color: var(--white);
+  text-decoration: none;
+}
+
+.link-alt:visited:hover,
+.link-alt:hover {
+  text-decoration: underline;
+}
+
+.text-3xl {
+  font-size: 2.5rem;
+}
+
+.text-2xl {
+  font-size: 1.9rem;
+  line-height: 1.15;
+}
+
+.text-xl {
+  font-size: 1.55rem;
+  line-height: 1.15;
+}
+
+.text-lg {
+  font-size: 1.35rem;
+  line-height: 1.15;
+}
+
+.text-md {
+  font-size: 1.15rem;
+  line-height: 1.15;
+}
+
+.text-sm {
+  font-size: 0.875rem;
+}
+
+.text-xs {
+  font-size: 0.775rem;
+}
+
+.cursor-pointer {
+  cursor: pointer;
+}
+
+.w-full {
+  width: 100%;
+}
+
+.h-full {
+  height: 100%;
+}
+
+.border {
+  border: 2px solid var(--grey-light);
+}
+
+.text-left {
+  text-align: left;
+}
+
+.text-center {
+  text-align: center;
+}
+
+.text-underline {
+  border-bottom: 3px solid var(--text-color);
+  padding-bottom: 3px;
+}
+
+.text-hdr {
+  color: var(--hover);
+}
+
+.text-underline-hdr {
+  border-bottom: 3px solid var(--hover);
+  padding-bottom: 3px;
+}
+
+.font-bold {
+  font-weight: bold;
+}
+
+.font-italic {
+  font-style: italic;
+}
+
+.inline {
+  display: inline;
+}
+
+.inline-block {
+  display: inline-block;
+}
+
+.max-w-half {
+  max-width: 50%;
+}
+
+.h-screen {
+  height: 100vh;
+}
+
+.w-screen {
+  width: 100vw;
+}
+
+.flex {
+  display: flex;
+}
+
+.flex-col {
+  flex-direction: column;
+}
+
+.items-center {
+  align-items: center;
+}
+
+.m-0 {
+  margin: 0;
+}
+
+.mt {
+  margin-top: 0.5rem;
+}
+
+.mt-2 {
+  margin-top: 1rem;
+}
+
+.mt-4 {
+  margin-top: 2rem;
+}
+
+.mt-8 {
+  margin-top: 4rem;
+}
+
+.mb {
+  margin-bottom: 0.5rem;
+}
+
+.mb-2 {
+  margin-bottom: 1rem;
+}
+
+.mb-4 {
+  margin-bottom: 2rem;
+}
+
+.mb-8 {
+  margin-bottom: 4rem;
+}
+
+.mb-16 {
+  margin-bottom: 8rem;
+}
+
+.mr {
+  margin-right: 0.5rem;
+}
+
+.ml-sm {
+  margin-left: 0.25rem;
+}
+
+.ml {
+  margin-left: 0.5rem;
+}
+
+.pt-0 {
+  padding-top: 0;
+}
+
+.my {
+  margin-top: 0.5rem;
+  margin-bottom: 0.5rem;
+}
+
+.my-2 {
+  margin-top: 1rem;
+  margin-bottom: 1rem;
+}
+
+.my-4 {
+  margin-top: 2rem;
+  margin-bottom: 2rem;
+}
+
+.my-8 {
+  margin-top: 4rem;
+  margin-bottom: 4rem;
+}
+
+.mx {
+  margin-left: 0.5rem;
+  margin-right: 0.5rem;
+}
+
+.mx-2 {
+  margin-left: 1rem;
+  margin-right: 1rem;
+}
+
+.m-1 {
+  margin: 0.5rem;
+}
+
+.p-1 {
+  padding: 0.5rem;
+}
+
+.p-0 {
+  padding: 0;
+}
+
+.px-2 {
+  padding-left: 1rem;
+  padding-right: 1rem;
+}
+
+.px-4 {
+  padding-left: 2rem;
+  padding-right: 2rem;
+}
+
+.py {
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+}
+
+.py-2 {
+  padding-top: 1rem;
+  padding-bottom: 1rem;
+}
+
+.py-4 {
+  padding-top: 2rem;
+  padding-bottom: 2rem;
+}
+
+.py-8 {
+  padding-top: 4rem;
+  padding-bottom: 4rem;
+}
+
+.justify-between {
+  justify-content: space-between;
+}
+
+.justify-center {
+  justify-content: center;
+}
+
+.gap {
+  gap: 0.5rem;
+}
+
+.gap-2 {
+  gap: 1rem;
+}
+
+.group {
+  display: flex;
+  flex-direction: column;
+  gap: 0.5rem;
+}
+
+.group-2 {
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+}
+
+.group-h {
+  display: flex;
+  gap: 0.5rem;
+  align-items: center;
+}
+
+.flex-1 {
+  flex: 1;
+}
+
+.items-end {
+  align-items: end;
+}
+
+.items-start {
+  align-items: start;
+}
+
+.justify-end {
+  justify-content: end;
+}
+
+.font-grey-light {
+  color: var(--grey-light);
+}
+
+.hidden {
+  display: none;
+}
+
+.align-right {
+  text-align: right;
+}
+
+/* ==== MARKDOWN ==== */
+
+.md h1,
+.md h2,
+.md h3,
+.md h4 {
+  padding: 0;
+  margin: 1.5rem 0 0.9rem 0;
+  font-weight: bold;
+}
+
+.md h1 a,
+.md h2 a,
+.md h3 a,
+.md h4 a {
+  color: var(--grey-light);
+  text-decoration: none;
+}
+
+.md h1 {
+  font-size: 1.6rem;
+  line-height: 1.15;
+  border-bottom: 2px solid var(--grey);
+  padding-bottom: 0.7rem;
+}
+
+.md h2 {
+  font-size: 1.3rem;
+  line-height: 1.15;
+  color: var(--white-dark);
+}
+
+.md h3 {
+  font-size: 1.2rem;
+  color: var(--white-dark);
+}
+
+.md h4 {
+  font-size: 1rem;
+  color: var(--white-dark);
+}
+
+/* ==== HELPERS ==== */
+
+.logo-header {
+  line-height: 1;
+  display: inline-block;
+  background-color: #FF79C6;
+  background-image: linear-gradient(to right, #FF5555, #FF79C6, #F8F859);
+  color: transparent;
+  background-clip: text;
+  border: 3px solid #FF79C6;
+  padding: 8px 10px 10px 10px;
+  border-radius: 10px;
+  box-shadow: 0px 5px 0px 0px var(--shadow);
+  background-size: 100%;
+  -webkit-background-clip: text;
+  -moz-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  -moz-text-fill-color: transparent;
+}
+
+.btn {
+  border: 2px solid var(--link-color);
+  color: var(--link-color);
+  padding: 0.4rem 1rem;
+  font-weight: bold;
+  display: inline-block;
+}
+
+.btn-link,
+.btn-link:visited {
+  border: 2px solid var(--link-color);
+  color: var(--link-color);
+  padding: 0.4rem 1rem;
+  text-decoration: none;
+  font-weight: bold;
+  display: inline-block;
+}
+
+.btn-link:visited:hover,
+.btn-link:hover {
+  border: 2px solid var(--hover);
+}
+
+.btn-link-alt,
+.btn-link-alt:visited {
+  border: 2px solid var(--white);
+  color: var(--white);
+}
+
+.box {
+  border: 2px solid var(--grey-light);
+  padding: 0.5rem 0.75rem;
+}
+
+.box-sm {
+  border: 2px solid var(--grey-light);
+  padding: 0.15rem 0.35rem;
+}
+
+.box-alert {
+  border: 2px solid var(--hover);
+  padding: 0.5rem 0.75rem;
+}
+
+.box-sm-alert {
+  border: 2px solid var(--hover);
+  padding: 0.15rem 0.35rem;
+}
+
+.list-none {
+  list-style-type: none;
+}
+
+.list-disc {
+  list-style-type: disc;
+}
+
+.list-decimal {
+  list-style-type: decimal;
+}
+
+.pill {
+  border: 1px solid var(--link-color);
+  color: var(--link-color);
+}
+
+.pill-alert {
+  border: 1px solid var(--hover);
+  color: var(--hover);
+}
+
+.pill-info {
+  border: 1px solid var(--visited);
+  color: var(--visited);
+}
+
+@media only screen and (max-width: 40em) {
+  body {
+    padding: 0 1rem;
+  }
+
+  header {
+    margin: 0;
+  }
+
+  .flex-collapse {
+    flex-direction: column;
+  }
+}
2: 3b99dc0 ! 1: 5e76ed3 fix(cli): access control for removing patchsets
ps-4 by erock on 2024-07-19T14:55:48Z
Range Diff ↕
2: 5e76ed3 < -: ------- fix(cli): access control for removing patchsets
1: 66cafc6 = 1: cc56ea1 feat: static assets folder
-: ------- > 2: ef749a4 review: typo and future enhancement comment
ps-5 by jolheiser on 2024-07-19T15:42:29Z
Range Diff ↕
1: cc56ea1 = 1: 0467f9e feat: static assets folder
2: ef749a4 = 2: da1730f review: typo and future enhancement comment
-: ------- > 3: c038404 refactor: per-file override for static folder
ps-9 by erock on 2024-07-19T17:43:47Z

Patchset ps-3

feat: static assets

Eric Bower
2024-07-19T04:27:17Z
Makefile
+4 -0
web.go
+57 -0

chore: inline smol.css

Eric Bower
2024-07-19T04:28:04Z
static/smol.css
+768 -0

chore: create patch for smol

Eric Bower
2024-07-19T04:29:31Z
Makefile
+1 -0

feat: static folder

Eric Bower
2024-07-19T14:52:39Z
ssh.go
+1 -1
web.go
+25 -15
Back to top

feat: static assets

Makefile link
+4 -0
 1diff --git a/Makefile b/Makefile
 2index cb3a367..bc76fd9 100644
 3--- a/Makefile
 4+++ b/Makefile
 5@@ -26,3 +26,7 @@ bp: bp-setup
 6 	$(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-ssh:$(DOCKER_TAG)" --target release-ssh .
 7 	$(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-web:$(DOCKER_TAG)" --target release-web .
 8 .PHONY: bp
 9+
10+smol:
11+	curl https://pico.sh/smol.css -o ./static/smol.css
12+.PHONY: smol
web.go link
+57 -0
 1diff --git a/web.go b/web.go
 2index 4e782c8..12db697 100644
 3--- a/web.go
 4+++ b/web.go
 5@@ -6,8 +6,11 @@ import (
 6 	"embed"
 7 	"fmt"
 8 	"html/template"
 9+	"io"
10+	"io/fs"
11 	"log/slog"
12 	"net/http"
13+	"os"
14 	"path/filepath"
15 	"slices"
16 	"strconv"
17@@ -23,6 +26,9 @@ import (
18 //go:embed tmpl/*
19 var tmplFS embed.FS
20 
21+//go:embed static/*
22+var staticFS embed.FS
23+
24 type WebCtx struct {
25 	Pr        *PrCmd
26 	Backend   *Backend
27@@ -618,6 +624,49 @@ func chromaStyleHandler(w http.ResponseWriter, r *http.Request) {
28 	}
29 }
30 
31+func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
32+	return func(w http.ResponseWriter, r *http.Request) {
33+		web, err := getWebCtx(r)
34+		if err != nil {
35+			w.WriteHeader(http.StatusUnprocessableEntity)
36+			return
37+		}
38+		logger := web.Logger
39+
40+		file := r.PathValue("file")
41+		reader, err := staticfs.Open(file)
42+		if err != nil {
43+			logger.Error(err.Error())
44+			http.Error(w, "file not found", 404)
45+			return
46+		}
47+		contents, err := io.ReadAll(reader)
48+		if err != nil {
49+			logger.Error(err.Error())
50+			http.Error(w, "file not found", 404)
51+			return
52+		}
53+		contentType := http.DetectContentType(contents)
54+		w.Header().Add("Content-Type", contentType)
55+
56+		_, err = w.Write(contents)
57+		if err != nil {
58+			logger.Error(err.Error())
59+			http.Error(w, "server error", 500)
60+			return
61+		}
62+	}
63+}
64+
65+type StaticFs struct {
66+	Dir string
67+}
68+
69+func (fs *StaticFs) Open(name string) (os.File, error) {
70+	fp, err := os.Open(filepath.Join(fs.Dir, name))
71+	return *fp, err
72+}
73+
74 func StartWebServer(cfg *GitCfg) {
75 	addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
76 
77@@ -659,6 +708,14 @@ func StartWebServer(cfg *GitCfg) {
78 	http.HandleFunc("GET /", ctxMdw(ctx, repoListHandler))
79 	http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
80 	http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
81+	dir := filepath.Join(cfg.DataDir, "static")
82+	var filesys fs.FS = staticFS
83+	_, err = os.Stat(dir)
84+	if err == nil {
85+		cfg.Logger.Info("detected static folder, using instead of the default one")
86+		filesys = os.DirFS(dir)
87+	}
88+	http.HandleFunc("GET /static/{file}", serveFile(filesys))
89 
90 	cfg.Logger.Info("starting web server", "addr", addr)
91 	err = http.ListenAndServe(addr, nil)

chore: inline smol.css

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

chore: create patch for smol

Makefile link
+1 -0
 1diff --git a/Makefile b/Makefile
 2index bc76fd9..e521f82 100644
 3--- a/Makefile
 4+++ b/Makefile
 5@@ -29,4 +29,5 @@ bp: bp-setup
 6 
 7 smol:
 8 	curl https://pico.sh/smol.css -o ./static/smol.css
 9+	cat patches/smol.diff | git apply
10 .PHONY: smol
patches/smol.diff link
+53 -0
 1diff --git a/patches/smol.diff b/patches/smol.diff
 2new file mode 100644
 3index 0000000..ae2df55
 4--- /dev/null
 5+++ b/patches/smol.diff
 6@@ -0,0 +1,53 @@
 7+diff --git a/static/smol.css b/static/smol.css
 8+index e9b59ec..9e9d925 100644
 9+--- a/static/smol.css
10++++ b/static/smol.css
11+@@ -15,48 +15,6 @@
12+   box-shadow: none;
13+ }
14+ 
15+-@media (prefers-color-scheme: light) {
16+-  :root {
17+-    --main-hue: 250;
18+-    --white: #2e3f53;
19+-    --white-light: #cfe0f4;
20+-    --white-dark: #6c6a6a;
21+-    --code: #52576f;
22+-    --pre: #e1e7ee;
23+-    --bg-color: #f4f4f4;
24+-    --text-color: #24292f;
25+-    --link-color: #005cc5;
26+-    --visited: #6f42c1;
27+-    --blockquote: #005cc5;
28+-    --blockquote-bg: #cfe0f4;
29+-    --hover: #c11e7a;
30+-    --grey: #ccc;
31+-    --grey-light: #6a708e;
32+-    --shadow: #e8e8e8;
33+-  }
34+-}
35+-
36+-@media (prefers-color-scheme: dark) {
37+-  :root {
38+-    --main-hue: 250;
39+-    --white: #f2f2f2;
40+-    --white-light: #f2f2f2;
41+-    --white-dark: #e8e8e8;
42+-    --code: #414558;
43+-    --pre: #252525;
44+-    --bg-color: #282a36;
45+-    --text-color: #f2f2f2;
46+-    --link-color: #8be9fd;
47+-    --visited: #bd93f9;
48+-    --blockquote: #bd93f9;
49+-    --blockquote-bg: #353548;
50+-    --hover: #ff80bf;
51+-    --grey: #414558;
52+-    --grey-light: #6a708e;
53+-    --shadow: #252525;
54+-  }
55+-}
56+-
57+ html {
58+   background-color: var(--bg-color);
59+   color: var(--text-color);
static/smol.css link
+0 -42
 1diff --git a/static/smol.css b/static/smol.css
 2index e9b59ec..9e9d925 100644
 3--- a/static/smol.css
 4+++ b/static/smol.css
 5@@ -15,48 +15,6 @@
 6   box-shadow: none;
 7 }
 8 
 9-@media (prefers-color-scheme: light) {
10-  :root {
11-    --main-hue: 250;
12-    --white: #2e3f53;
13-    --white-light: #cfe0f4;
14-    --white-dark: #6c6a6a;
15-    --code: #52576f;
16-    --pre: #e1e7ee;
17-    --bg-color: #f4f4f4;
18-    --text-color: #24292f;
19-    --link-color: #005cc5;
20-    --visited: #6f42c1;
21-    --blockquote: #005cc5;
22-    --blockquote-bg: #cfe0f4;
23-    --hover: #c11e7a;
24-    --grey: #ccc;
25-    --grey-light: #6a708e;
26-    --shadow: #e8e8e8;
27-  }
28-}
29-
30-@media (prefers-color-scheme: dark) {
31-  :root {
32-    --main-hue: 250;
33-    --white: #f2f2f2;
34-    --white-light: #f2f2f2;
35-    --white-dark: #e8e8e8;
36-    --code: #414558;
37-    --pre: #252525;
38-    --bg-color: #282a36;
39-    --text-color: #f2f2f2;
40-    --link-color: #8be9fd;
41-    --visited: #bd93f9;
42-    --blockquote: #bd93f9;
43-    --blockquote-bg: #353548;
44-    --hover: #ff80bf;
45-    --grey: #414558;
46-    --grey-light: #6a708e;
47-    --shadow: #252525;
48-  }
49-}
50-
51 html {
52   background-color: var(--bg-color);
53   color: var(--text-color);

feat: static folder

ssh.go link
+1 -1
 1diff --git a/ssh.go b/ssh.go
 2index 1fb34d0..3e5e407 100644
 3--- a/ssh.go
 4+++ b/ssh.go
 5@@ -35,7 +35,7 @@ func GitSshServer(cfg *GitCfg) {
 6 	dbpath := filepath.Join(cfg.DataDir, "pr.db")
 7 	dbh, err := Open(dbpath, cfg.Logger)
 8 	if err != nil {
 9-		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s", dbpath))
10+		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s: %s", dbpath, err))
11 	}
12 
13 	be := &Backend{
static/vars.css link
+18 -0
 1diff --git a/static/vars.css b/static/vars.css
 2new file mode 100644
 3index 0000000..6300534
 4--- /dev/null
 5+++ b/static/vars.css
 6@@ -0,0 +1,18 @@
 7+:root {
 8+  --main-hue: 250;
 9+  --white: #f2f2f2;
10+  --white-light: #f2f2f2;
11+  --white-dark: #e8e8e8;
12+  --code: #414558;
13+  --pre: #252525;
14+  --bg-color: #282a36;
15+  --text-color: #f2f2f2;
16+  --link-color: #8be9fd;
17+  --visited: #bd93f9;
18+  --blockquote: #bd93f9;
19+  --blockquote-bg: #353548;
20+  --hover: #ff80bf;
21+  --grey: #414558;
22+  --grey-light: #6a708e;
23+  --shadow: #252525;
24+}
tmpl/base.html link
+3 -2
 1diff --git a/tmpl/base.html b/tmpl/base.html
 2index a656cd5..b5f5fb3 100644
 3--- a/tmpl/base.html
 4+++ b/tmpl/base.html
 5@@ -9,8 +9,9 @@
 6     <meta name="keywords" content="git, collaboration, patch, requests" />
 7     {{template "meta" .}}
 8 
 9-    <link rel="stylesheet" href="https://pico.sh/smol.css" />
10-    <link rel="stylesheet" href="https://pico.sh/syntax.css" />
11+    <link rel="stylesheet" href="/static/smol.css" />
12+    <link rel="stylesheet" href="/static/vars.css" />
13+    <link rel="stylesheet" href="/syntax.css" />
14   </head>
15   <body class="container">{{template "body" .}}</body>
16 </html>
web.go link
+25 -15
 1diff --git a/web.go b/web.go
 2index 12db697..99bfec6 100644
 3--- a/web.go
 4+++ b/web.go
 5@@ -9,6 +9,7 @@ import (
 6 	"io"
 7 	"io/fs"
 8 	"log/slog"
 9+	"mime"
10 	"net/http"
11 	"os"
12 	"path/filepath"
13@@ -634,6 +635,7 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
14 		logger := web.Logger
15 
16 		file := r.PathValue("file")
17+		logger.Info("serving file", "file", file, "fs", staticfs)
18 		reader, err := staticfs.Open(file)
19 		if err != nil {
20 			logger.Error(err.Error())
21@@ -646,7 +648,10 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
22 			http.Error(w, "file not found", 404)
23 			return
24 		}
25-		contentType := http.DetectContentType(contents)
26+		contentType := mime.TypeByExtension(filepath.Ext(file))
27+		if contentType == "" {
28+			contentType = http.DetectContentType(contents)
29+		}
30 		w.Header().Add("Content-Type", contentType)
31 
32 		_, err = w.Write(contents)
33@@ -658,13 +663,21 @@ func serveFile(staticfs fs.FS) func(w http.ResponseWriter, r *http.Request) {
34 	}
35 }
36 
37-type StaticFs struct {
38-	Dir string
39-}
40+func getFileSystem(logger *slog.Logger, ffs embed.FS, datadir string, dirName string) (fs.FS, error) {
41+	dir := filepath.Join(datadir, dirName)
42+	_, err := os.Stat(dir)
43+	if err == nil {
44+		logger.Info("found folder in data_dir", "dir", dir)
45+		return os.DirFS(dir), nil
46+	}
47 
48-func (fs *StaticFs) Open(name string) (os.File, error) {
49-	fp, err := os.Open(filepath.Join(fs.Dir, name))
50-	return *fp, err
51+	logger.Info("using embeded folder", "dir", dir)
52+	fsys, err := fs.Sub(ffs, dirName)
53+	if err != nil {
54+		return nil, err
55+	}
56+
57+	return fsys, nil
58 }
59 
60 func StartWebServer(cfg *GitCfg) {
61@@ -673,7 +686,7 @@ func StartWebServer(cfg *GitCfg) {
62 	dbpath := filepath.Join(cfg.DataDir, "pr.db")
63 	dbh, err := Open(dbpath, cfg.Logger)
64 	if err != nil {
65-		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s", dbpath))
66+		panic(fmt.Sprintf("cannot find database file, check folder and perms: %s: %s", dbpath, err))
67 	}
68 
69 	be := &Backend{
70@@ -708,14 +721,11 @@ func StartWebServer(cfg *GitCfg) {
71 	http.HandleFunc("GET /", ctxMdw(ctx, repoListHandler))
72 	http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
73 	http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
74-	dir := filepath.Join(cfg.DataDir, "static")
75-	var filesys fs.FS = staticFS
76-	_, err = os.Stat(dir)
77-	if err == nil {
78-		cfg.Logger.Info("detected static folder, using instead of the default one")
79-		filesys = os.DirFS(dir)
80+	filesys, err := getFileSystem(cfg.Logger, staticFS, cfg.DataDir, "static")
81+	if err != nil {
82+		panic(err)
83 	}
84-	http.HandleFunc("GET /static/{file}", serveFile(filesys))
85+	http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(filesys)))
86 
87 	cfg.Logger.Info("starting web server", "addr", addr)
88 	err = http.ListenAndServe(addr, nil)