repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

commit
a2877c7
parent
c71945d
author
Eric Bower
date
2024-04-08 20:37:28 +0000 UTC
chore: TUI only accessible via pico.sh

BREAKING CHANGE: TUI only accessible via pico.sh
50 files changed,  +248, -1304
M .env.example
+0, -41
 1@@ -13,7 +13,6 @@ MINIO_PROMETHEUS_AUTH_TYPE=public
 2 MINIO_PROMETHEUS_URL=
 3 MINIO_PROMETHEUS_JOB_ID=minio
 4 
 5-USE_IMGPROXY=1
 6 IMGPROXY_DOMAIN=imgproxy.dev.pico.sh
 7 IMGPROXY_URL=http://imgproxy:8080
 8 IMGPROXY_ALLOWED_SOURCES=s3://,local://
 9@@ -27,27 +26,6 @@ AWS_SECRET_ACCESS_KEY=$MINIO_ROOT_PASSWORD
10 IMGPROXY_PROMETHEUS_BIND=:8081
11 IMGPROXY_PROMETHEUS_NAMESPACE=imgproxy
12 
13-LISTS_CADDYFILE=./caddy/Caddyfile
14-LISTS_V4=
15-LISTS_V6=
16-LISTS_HTTP_V4=$LISTS_V4:80
17-LISTS_HTTP_V6=[$LISTS_V6]:80
18-LISTS_HTTPS_V4=$LISTS_V4:443
19-LISTS_HTTPS_V6=[$LISTS_V6]:443
20-LISTS_SSH_V4=$LISTS_V4:22
21-LISTS_SSH_V6=[$LISTS_V6]:22
22-LISTS_HOST=
23-LISTS_SSH_PORT=2222
24-LISTS_WEB_PORT=3000
25-LISTS_PROM_PORT=9222
26-LISTS_DOMAIN=lists.dev.pico.sh:3000
27-LISTS_EMAIL=hello@pico.sh
28-LISTS_SUBDOMAINS=1
29-LISTS_CUSTOMDOMAINS=1
30-LISTS_PROTOCOL=http
31-LISTS_ALLOW_REGISTER=1
32-LISTS_DEBUG=1
33-
34 PASTES_CADDYFILE=./caddy/Caddyfile
35 PASTES_V4=
36 PASTES_V6=
37@@ -62,11 +40,7 @@ PASTES_SSH_PORT=2222
38 PASTES_WEB_PORT=3000
39 PASTES_PROM_PORT=9222
40 PASTES_DOMAIN=pastes.dev.pico.sh:3001
41-PASTES_EMAIL=hello@pico.sh
42-PASTES_SUBDOMAINS=1
43-PASTES_CUSTOMDOMAINS=1
44 PASTES_PROTOCOL=http
45-PASTES_ALLOW_REGISTER=1
46 PASTES_DEBUG=1
47 
48 PROSE_CADDYFILE=./caddy/Caddyfile
49@@ -83,11 +57,7 @@ PROSE_SSH_PORT=2222
50 PROSE_WEB_PORT=3000
51 PROSE_PROM_PORT=9222
52 PROSE_DOMAIN=prose.dev.pico.sh:3002
53-PROSE_EMAIL=hello@pico.sh
54-PROSE_SUBDOMAINS=1
55-PROSE_CUSTOMDOMAINS=1
56 PROSE_PROTOCOL=http
57-PROSE_ALLOW_REGISTER=1
58 PROSE_DEBUG=1
59 
60 IMGS_CADDYFILE=./caddy/Caddyfile
61@@ -105,10 +75,7 @@ IMGS_WEB_PORT=3000
62 IMGS_PROM_PORT=9222
63 IMGS_DOMAIN=imgs.dev.pico.sh:3003
64 IMGS_EMAIL=hello@pico.sh
65-IMGS_SUBDOMAINS=1
66-IMGS_CUSTOMDOMAINS=1
67 IMGS_PROTOCOL=http
68-IMGS_ALLOW_REGISTER=1
69 IMGS_STORAGE_DIR=.storage
70 IMGS_DEBUG=1
71 
72@@ -127,11 +94,7 @@ FEEDS_SSH_PORT=2222
73 FEEDS_WEB_PORT=3000
74 FEEDS_PROM_PORT=9222
75 FEEDS_DOMAIN=feeds.dev.pico.sh:3004
76-FEEDS_EMAIL=hello@pico.sh
77-FEEDS_SUBDOMAINS=1
78-FEEDS_CUSTOMDOMAINS=1
79 FEEDS_PROTOCOL=http
80-FEEDS_ALLOW_REGISTER=1
81 FEEDS_DEBUG=1
82 
83 PGS_CADDYFILE=./caddy/Caddyfile
84@@ -148,11 +111,7 @@ PGS_SSH_PORT=2222
85 PGS_WEB_PORT=3000
86 PGS_PROM_PORT=9222
87 PGS_DOMAIN=pgs.dev.pico.sh:3005
88-PGS_EMAIL=hello@pico.sh
89-PGS_SUBDOMAINS=1
90-PGS_CUSTOMDOMAINS=1
91 PGS_PROTOCOL=http
92-PGS_ALLOW_REGISTER=1
93 PGS_STORAGE_DIR=.storage
94 PGS_DEBUG=1
95 
M cmd/scripts/clean-object-store/clean.go
+1, -2
 1@@ -10,7 +10,6 @@ import (
 2 	"github.com/picosh/pico/pgs"
 3 	"github.com/picosh/pico/shared"
 4 	"github.com/picosh/pico/shared/storage"
 5-	"github.com/picosh/pico/wish/cms/config"
 6 )
 7 
 8 func bail(err error) {
 9@@ -35,7 +34,7 @@ func main() {
10 	}
11 	logger := slog.Default()
12 
13-	picoCfg := config.NewConfigCms()
14+	picoCfg := shared.NewConfigSite()
15 	picoCfg.Logger = logger
16 	picoCfg.DbURL = os.Getenv("DATABASE_URL")
17 	picoCfg.MinioURL = os.Getenv("MINIO_URL")
M cmd/scripts/dates/dates.go
+1, -2
 1@@ -10,7 +10,6 @@ import (
 2 	"github.com/picosh/pico/db"
 3 	"github.com/picosh/pico/db/postgres"
 4 	"github.com/picosh/pico/shared"
 5-	"github.com/picosh/pico/wish/cms/config"
 6 )
 7 
 8 func findPosts(dbpool *sql.DB) ([]*db.Post, error) {
 9@@ -59,7 +58,7 @@ func updateDates(tx *sql.Tx, postID string, date *time.Time) error {
10 func main() {
11 	logger := slog.Default()
12 
13-	picoCfg := config.NewConfigCms()
14+	picoCfg := shared.NewConfigSite()
15 	picoCfg.Logger = logger
16 	picoCfg.DbURL = os.Getenv("DATABASE_URL")
17 	picoDb := postgres.NewDB(picoCfg.DbURL, picoCfg.Logger)
M cmd/scripts/file-size-sync/sync.go
+2, -2
 1@@ -6,7 +6,7 @@ import (
 2 	"os"
 3 
 4 	"github.com/picosh/pico/db/postgres"
 5-	"github.com/picosh/pico/wish/cms/config"
 6+	"github.com/picosh/pico/shared"
 7 )
 8 
 9 func bail(err error) {
10@@ -18,7 +18,7 @@ func bail(err error) {
11 func main() {
12 	logger := slog.Default()
13 
14-	picoCfg := config.NewConfigCms()
15+	picoCfg := shared.NewConfigSite()
16 	picoCfg.Logger = logger
17 	picoCfg.DbURL = os.Getenv("DATABASE_URL")
18 	picoDb := postgres.NewDB(picoCfg.DbURL, picoCfg.Logger)
M cmd/scripts/migrate/migrate.go
+4, -4
 1@@ -9,7 +9,7 @@ import (
 2 
 3 	"github.com/picosh/pico/db"
 4 	"github.com/picosh/pico/db/postgres"
 5-	"github.com/picosh/pico/wish/cms/config"
 6+	"github.com/picosh/pico/shared"
 7 )
 8 
 9 func findPosts(dbpool *sql.DB) ([]*db.Post, error) {
10@@ -101,17 +101,17 @@ type ConflictData struct {
11 func main() {
12 	logger := slog.Default()
13 
14-	listsCfg := config.NewConfigCms()
15+	listsCfg := shared.NewConfigSite()
16 	listsCfg.Logger = logger
17 	listsCfg.DbURL = os.Getenv("LISTS_DB_URL")
18 	listsDb := postgres.NewDB(listsCfg.DbURL, listsCfg.Logger)
19 
20-	proseCfg := config.NewConfigCms()
21+	proseCfg := shared.NewConfigSite()
22 	proseCfg.DbURL = os.Getenv("PROSE_DB_URL")
23 	proseCfg.Logger = logger
24 	proseDb := postgres.NewDB(proseCfg.DbURL, proseCfg.Logger)
25 
26-	picoCfg := config.NewConfigCms()
27+	picoCfg := shared.NewConfigSite()
28 	picoCfg.Logger = logger
29 	picoCfg.DbURL = os.Getenv("PICO_DB_URL")
30 	picoDb := postgres.NewDB(picoCfg.DbURL, picoCfg.Logger)
M cmd/scripts/shasum/shasum.go
+1, -2
 1@@ -6,12 +6,11 @@ import (
 2 
 3 	"github.com/picosh/pico/db/postgres"
 4 	"github.com/picosh/pico/shared"
 5-	"github.com/picosh/pico/wish/cms/config"
 6 )
 7 
 8 func main() {
 9 	logger := slog.Default()
10-	picoCfg := config.NewConfigCms()
11+	picoCfg := shared.NewConfigSite()
12 	picoCfg.Logger = logger
13 	picoCfg.DbURL = os.Getenv("DATABASE_URL")
14 	picoDb := postgres.NewDB(picoCfg.DbURL, picoCfg.Logger)
M cmd/scripts/tags/tags.go
+1, -2
 1@@ -8,7 +8,6 @@ import (
 2 	"github.com/picosh/pico/db"
 3 	"github.com/picosh/pico/db/postgres"
 4 	"github.com/picosh/pico/shared"
 5-	"github.com/picosh/pico/wish/cms/config"
 6 )
 7 
 8 func findPosts(dbpool *sql.DB) ([]*db.Post, error) {
 9@@ -52,7 +51,7 @@ func findPosts(dbpool *sql.DB) ([]*db.Post, error) {
10 func main() {
11 	logger := slog.Default()
12 
13-	picoCfg := config.NewConfigCms()
14+	picoCfg := shared.NewConfigSite()
15 	picoCfg.Logger = logger
16 	picoCfg.DbURL = os.Getenv("DATABASE_URL")
17 	picoDb := postgres.NewDB(picoCfg.DbURL, picoCfg.Logger)
M feeds/api.go
+0, -1
1@@ -77,7 +77,6 @@ func StartApiServer() {
2 		"Starting server on port",
3 		"port", cfg.Port,
4 		"domain", cfg.Domain,
5-		"email", cfg.Email,
6 	)
7 
8 	logger.Error(http.ListenAndServe(portStr, router).Error())
M feeds/config.go
+14, -32
 1@@ -2,52 +2,34 @@ package feeds
 2 
 3 import (
 4 	"github.com/picosh/pico/shared"
 5-	"github.com/picosh/pico/wish/cms/config"
 6 )
 7 
 8 func NewConfigSite() *shared.ConfigSite {
 9 	debug := shared.GetEnv("FEEDS_DEBUG", "0")
10 	domain := shared.GetEnv("FEEDS_DOMAIN", "feeds.sh")
11-	email := shared.GetEnv("FEEDS_EMAIL", "hello@feeds.sh")
12-	subdomains := shared.GetEnv("FEEDS_SUBDOMAINS", "0")
13-	customdomains := shared.GetEnv("FEEDS_CUSTOMDOMAINS", "0")
14 	port := shared.GetEnv("FEEDS_WEB_PORT", "3000")
15 	protocol := shared.GetEnv("FEEDS_PROTOCOL", "https")
16-	allowRegister := shared.GetEnv("FEEDS_ALLOW_REGISTER", "1")
17 	storageDir := shared.GetEnv("IMGS_STORAGE_DIR", ".storage")
18 	minioURL := shared.GetEnv("MINIO_URL", "")
19 	minioUser := shared.GetEnv("MINIO_ROOT_USER", "")
20 	minioPass := shared.GetEnv("MINIO_ROOT_PASSWORD", "")
21 	dbURL := shared.GetEnv("DATABASE_URL", "")
22 	sendgridKey := shared.GetEnv("SENDGRID_API_KEY", "")
23-	useImgProxy := shared.GetEnv("USE_IMGPROXY", "1")
24-
25-	intro := "To get started, enter a username.\n"
26-	intro += "To learn next steps go to our docs at https://pico.sh/feeds\n"
27 
28 	return &shared.ConfigSite{
29-		Debug:                debug == "1",
30-		SubdomainsEnabled:    subdomains == "1",
31-		CustomdomainsEnabled: customdomains == "1",
32-		UseImgProxy:          useImgProxy == "1",
33-		SendgridKey:          sendgridKey,
34-		ConfigCms: config.ConfigCms{
35-			Domain:        domain,
36-			Email:         email,
37-			Port:          port,
38-			Protocol:      protocol,
39-			DbURL:         dbURL,
40-			StorageDir:    storageDir,
41-			MinioURL:      minioURL,
42-			MinioUser:     minioUser,
43-			MinioPass:     minioPass,
44-			Description:   "An rss-to-email digest service for hackers",
45-			IntroText:     intro,
46-			Space:         "feeds",
47-			AllowedExt:    []string{".txt"},
48-			HiddenPosts:   []string{"_header.txt", "_readme.txt"},
49-			Logger:        shared.CreateLogger(debug == "1"),
50-			AllowRegister: allowRegister == "1",
51-		},
52+		Debug:       debug == "1",
53+		SendgridKey: sendgridKey,
54+		Domain:      domain,
55+		Port:        port,
56+		Protocol:    protocol,
57+		DbURL:       dbURL,
58+		StorageDir:  storageDir,
59+		MinioURL:    minioURL,
60+		MinioUser:   minioUser,
61+		MinioPass:   minioPass,
62+		Space:       "feeds",
63+		AllowedExt:  []string{".txt"},
64+		HiddenPosts: []string{"_header.txt", "_readme.txt"},
65+		Logger:      shared.CreateLogger(debug == "1"),
66 	}
67 }
M feeds/cron.go
+1, -1
1@@ -383,7 +383,7 @@ func (f *Fetcher) SendEmail(username, email string, subject string, msg *MsgBody
2 		return fmt.Errorf("(%s) does not have an email associated with their feed post", username)
3 	}
4 
5-	from := mail.NewEmail("team pico", f.cfg.Email)
6+	from := mail.NewEmail("team pico", shared.DefaultEmail)
7 	to := mail.NewEmail(username, email)
8 
9 	// f.cfg.Logger.Infof("message body (%s)", plainTextContent)
M feeds/html/marketing.page.tmpl
+4, -4
 1@@ -1,20 +1,20 @@
 2 {{template "base" .}}
 3 
 4-{{define "title"}}{{.Site.Domain}} -- an rss email notification service{{end}}
 5+{{define "title"}}{{.Site.Domain}} -- An rss email notification service{{end}}
 6 
 7 {{define "meta"}}
 8-<meta name="description" content="an rss email notification service" />
 9+<meta name="description" content="An rss email notification service" />
10 
11 <meta property="og:type" content="website">
12 <meta property="og:site_name" content="{{.Site.Domain}}">
13 <meta property="og:url" content="https://{{.Site.Domain}}">
14 <meta property="og:title" content="{{.Site.Domain}}">
15-<meta property="og:description" content="an rss email notification service">
16+<meta property="og:description" content="An rss email notification service">
17 
18 <meta name="twitter:card" content="summary" />
19 <meta property="twitter:url" content="https://{{.Site.Domain}}">
20 <meta property="twitter:title" content="{{.Site.Domain}}">
21-<meta property="twitter:description" content="an rss email notification service">
22+<meta property="twitter:description" content="An rss email notification service">
23 <meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
24 <meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
25 
M feeds/ssh.go
+1, -3
 1@@ -11,13 +11,11 @@ import (
 2 	"github.com/charmbracelet/promwish"
 3 	"github.com/charmbracelet/ssh"
 4 	"github.com/charmbracelet/wish"
 5-	bm "github.com/charmbracelet/wish/bubbletea"
 6 	"github.com/picosh/pico/db/postgres"
 7 	"github.com/picosh/pico/filehandlers"
 8 	"github.com/picosh/pico/shared"
 9 	"github.com/picosh/pico/shared/storage"
10 	wsh "github.com/picosh/pico/wish"
11-	"github.com/picosh/pico/wish/cms"
12 	"github.com/picosh/send/list"
13 	"github.com/picosh/send/pipe"
14 	"github.com/picosh/send/proxy"
15@@ -41,7 +39,7 @@ func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
16 			scp.Middleware(handler),
17 			wishrsync.Middleware(handler),
18 			auth.Middleware(handler),
19-			wsh.PtyMdw(bm.Middleware(cms.Middleware(&handler.Cfg.ConfigCms, handler.Cfg))),
20+			wsh.PtyMdw(wsh.DeprecatedNotice()),
21 			wsh.LogMiddleware(handler.GetLogger()),
22 		}
23 	}
M imgs/api.go
+0, -1
1@@ -335,7 +335,6 @@ func StartApiServer() {
2 		"Starting server on port",
3 		"port", cfg.Port,
4 		"domain", cfg.Domain,
5-		"email", cfg.Email,
6 	)
7 
8 	logger.Error(http.ListenAndServe(portStr, router).Error())
M imgs/config.go
+11, -30
 1@@ -2,50 +2,31 @@ package imgs
 2 
 3 import (
 4 	"github.com/picosh/pico/shared"
 5-	"github.com/picosh/pico/wish/cms/config"
 6 )
 7 
 8 func NewConfigSite() *shared.ConfigSite {
 9 	debug := shared.GetEnv("IMGS_DEBUG", "0")
10 	domain := shared.GetEnv("IMGS_DOMAIN", "prose.sh")
11-	email := shared.GetEnv("IMGS_EMAIL", "hello@prose.sh")
12-	subdomains := shared.GetEnv("IMGS_SUBDOMAINS", "0")
13-	customdomains := shared.GetEnv("IMGS_CUSTOMDOMAINS", "0")
14 	port := shared.GetEnv("IMGS_WEB_PORT", "3000")
15 	protocol := shared.GetEnv("IMGS_PROTOCOL", "https")
16-	allowRegister := shared.GetEnv("IMGS_ALLOW_REGISTER", "1")
17 	storageDir := shared.GetEnv("IMGS_STORAGE_DIR", ".storage")
18 	minioURL := shared.GetEnv("MINIO_URL", "")
19 	minioUser := shared.GetEnv("MINIO_ROOT_USER", "")
20 	minioPass := shared.GetEnv("MINIO_ROOT_PASSWORD", "")
21 	dbURL := shared.GetEnv("DATABASE_URL", "")
22-	useImgProxy := shared.GetEnv("USE_IMGPROXY", "1")
23-
24-	intro := "To get started, enter a username.\n"
25-	intro += "To learn next steps go to our docs at https://pico.sh/imgs\n"
26 
27 	cfg := shared.ConfigSite{
28-		Debug:                debug == "1",
29-		SubdomainsEnabled:    subdomains == "1",
30-		CustomdomainsEnabled: customdomains == "1",
31-		UseImgProxy:          useImgProxy == "1",
32-		ConfigCms: config.ConfigCms{
33-			Domain:        domain,
34-			Email:         email,
35-			Port:          port,
36-			Protocol:      protocol,
37-			DbURL:         dbURL,
38-			StorageDir:    storageDir,
39-			MinioURL:      minioURL,
40-			MinioUser:     minioUser,
41-			MinioPass:     minioPass,
42-			Description:   "An image hosting service for hackers.",
43-			IntroText:     intro,
44-			Space:         "imgs",
45-			AllowedExt:    []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico"},
46-			Logger:        shared.CreateLogger(debug == "1"),
47-			AllowRegister: allowRegister == "1",
48-		},
49+		Debug:      debug == "1",
50+		Domain:     domain,
51+		Port:       port,
52+		Protocol:   protocol,
53+		DbURL:      dbURL,
54+		StorageDir: storageDir,
55+		MinioURL:   minioURL,
56+		MinioUser:  minioUser,
57+		MinioPass:  minioPass,
58+		Space:      "imgs",
59+		Logger:     shared.CreateLogger(debug == "1"),
60 	}
61 
62 	return &cfg
M pastes/api.go
+0, -1
1@@ -392,7 +392,6 @@ func StartApiServer() {
2 		"Starting server on port",
3 		"port", cfg.Port,
4 		"domain", cfg.Domain,
5-		"email", cfg.Email,
6 	)
7 
8 	logger.Error(http.ListenAndServe(portStr, router).Error())
M pastes/config.go
+11, -29
 1@@ -2,48 +2,30 @@ package pastes
 2 
 3 import (
 4 	"github.com/picosh/pico/shared"
 5-	"github.com/picosh/pico/wish/cms/config"
 6 )
 7 
 8 func NewConfigSite() *shared.ConfigSite {
 9 	debug := shared.GetEnv("PASTES_DEBUG", "0")
10 	domain := shared.GetEnv("PASTES_DOMAIN", "pastes.sh")
11-	email := shared.GetEnv("PASTES_EMAIL", "hello@pastes.sh")
12-	subdomains := shared.GetEnv("PASTES_SUBDOMAINS", "0")
13-	customdomains := shared.GetEnv("PASTES_CUSTOMDOMAINS", "0")
14 	port := shared.GetEnv("PASTES_WEB_PORT", "3000")
15 	dbURL := shared.GetEnv("DATABASE_URL", "")
16 	protocol := shared.GetEnv("PASTES_PROTOCOL", "https")
17-	allowRegister := shared.GetEnv("PASTES_ALLOW_REGISTER", "1")
18 	storageDir := shared.GetEnv("IMGS_STORAGE_DIR", ".storage")
19 	minioURL := shared.GetEnv("MINIO_URL", "")
20 	minioUser := shared.GetEnv("MINIO_ROOT_USER", "")
21 	minioPass := shared.GetEnv("MINIO_ROOT_PASSWORD", "")
22-	useImgProxy := shared.GetEnv("USE_IMGPROXY", "1")
23-
24-	intro := "To get started, enter a username.\n"
25-	intro += "To learn next steps go to our docs at https://pico.sh/pastes\n"
26 
27 	return &shared.ConfigSite{
28-		Debug:                debug == "1",
29-		SubdomainsEnabled:    subdomains == "1",
30-		CustomdomainsEnabled: customdomains == "1",
31-		UseImgProxy:          useImgProxy == "1",
32-		ConfigCms: config.ConfigCms{
33-			Domain:        domain,
34-			Port:          port,
35-			Protocol:      protocol,
36-			Email:         email,
37-			DbURL:         dbURL,
38-			StorageDir:    storageDir,
39-			MinioURL:      minioURL,
40-			MinioUser:     minioUser,
41-			MinioPass:     minioPass,
42-			Description:   "A pastebin for hackers.",
43-			IntroText:     intro,
44-			Space:         "pastes",
45-			Logger:        shared.CreateLogger(debug == "1"),
46-			AllowRegister: allowRegister == "1",
47-		},
48+		Debug:      debug == "1",
49+		Domain:     domain,
50+		Port:       port,
51+		Protocol:   protocol,
52+		DbURL:      dbURL,
53+		StorageDir: storageDir,
54+		MinioURL:   minioURL,
55+		MinioUser:  minioUser,
56+		MinioPass:  minioPass,
57+		Space:      "pastes",
58+		Logger:     shared.CreateLogger(debug == "1"),
59 	}
60 }
M pastes/ssh.go
+1, -3
 1@@ -11,13 +11,11 @@ import (
 2 	"github.com/charmbracelet/promwish"
 3 	"github.com/charmbracelet/ssh"
 4 	"github.com/charmbracelet/wish"
 5-	bm "github.com/charmbracelet/wish/bubbletea"
 6 	"github.com/picosh/pico/db/postgres"
 7 	"github.com/picosh/pico/filehandlers"
 8 	"github.com/picosh/pico/shared"
 9 	"github.com/picosh/pico/shared/storage"
10 	wsh "github.com/picosh/pico/wish"
11-	"github.com/picosh/pico/wish/cms"
12 	"github.com/picosh/send/list"
13 	"github.com/picosh/send/pipe"
14 	"github.com/picosh/send/proxy"
15@@ -41,7 +39,7 @@ func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
16 			scp.Middleware(handler),
17 			wishrsync.Middleware(handler),
18 			auth.Middleware(handler),
19-			wsh.PtyMdw(bm.Middleware(cms.Middleware(&handler.Cfg.ConfigCms, handler.Cfg))),
20+			wsh.PtyMdw(wsh.DeprecatedNotice()),
21 			wsh.LogMiddleware(handler.GetLogger()),
22 		}
23 	}
M pgs/api.go
+1, -2
 1@@ -45,7 +45,7 @@ func checkHandler(w http.ResponseWriter, r *http.Request) {
 2 
 3 	if cfg.IsCustomdomains() {
 4 		hostDomain := r.URL.Query().Get("domain")
 5-		appDomain := strings.Split(cfg.ConfigCms.Domain, ":")[0]
 6+		appDomain := strings.Split(cfg.Domain, ":")[0]
 7 
 8 		if !strings.Contains(hostDomain, appDomain) {
 9 			subdomain := shared.GetCustomDomain(hostDomain, cfg.Space)
10@@ -514,7 +514,6 @@ func StartApiServer() {
11 		"Starting server on port",
12 		"port", cfg.Port,
13 		"domain", cfg.Domain,
14-		"email", cfg.Email,
15 	)
16 	err = http.ListenAndServe(portStr, router)
17 	logger.Error(
M pgs/cli.go
+1, -1
1@@ -14,7 +14,7 @@ import (
2 	"github.com/picosh/pico/db"
3 	"github.com/picosh/pico/shared"
4 	"github.com/picosh/pico/shared/storage"
5-	"github.com/picosh/pico/wish/cms/ui/common"
6+	"github.com/picosh/pico/tui/common"
7 )
8 
9 func styleRows(styles common.Styles) func(row, col int) lipgloss.Style {
M pgs/config.go
+14, -32
 1@@ -2,7 +2,6 @@ package pgs
 2 
 3 import (
 4 	"github.com/picosh/pico/shared"
 5-	"github.com/picosh/pico/wish/cms/config"
 6 )
 7 
 8 var maxSize = uint64(25 * shared.MB)
 9@@ -11,50 +10,33 @@ var maxAssetSize = int64(5 * shared.MB)
10 func NewConfigSite() *shared.ConfigSite {
11 	debug := shared.GetEnv("PGS_DEBUG", "0")
12 	domain := shared.GetEnv("PGS_DOMAIN", "pgs.sh")
13-	email := shared.GetEnv("PGS_EMAIL", "hello@pico.sh")
14-	subdomains := shared.GetEnv("PGS_SUBDOMAINS", "0")
15-	customdomains := shared.GetEnv("PGS_CUSTOMDOMAINS", "0")
16 	port := shared.GetEnv("PGS_WEB_PORT", "3000")
17 	protocol := shared.GetEnv("PGS_PROTOCOL", "https")
18-	allowRegister := shared.GetEnv("PGS_ALLOW_REGISTER", "1")
19 	storageDir := shared.GetEnv("PGS_STORAGE_DIR", ".storage")
20 	minioURL := shared.GetEnv("MINIO_URL", "")
21 	minioUser := shared.GetEnv("MINIO_ROOT_USER", "")
22 	minioPass := shared.GetEnv("MINIO_ROOT_PASSWORD", "")
23 	dbURL := shared.GetEnv("DATABASE_URL", "")
24-	useImgProxy := shared.GetEnv("USE_IMGPROXY", "1")
25 	secret := shared.GetEnv("PICO_SECRET", "")
26 	if secret == "" {
27 		panic("must provide PICO_SECRET environment variable")
28 	}
29 
30-	intro := "To create an account, enter a username.\n"
31-	intro += "After that, go to https://pico.sh/getting-started#next-steps"
32-
33 	cfg := shared.ConfigSite{
34-		Debug:                debug == "1",
35-		SubdomainsEnabled:    subdomains == "1",
36-		CustomdomainsEnabled: customdomains == "1",
37-		UseImgProxy:          useImgProxy == "1",
38-		Secret:               secret,
39-		ConfigCms: config.ConfigCms{
40-			Domain:        domain,
41-			Email:         email,
42-			Port:          port,
43-			Protocol:      protocol,
44-			DbURL:         dbURL,
45-			StorageDir:    storageDir,
46-			MinioURL:      minioURL,
47-			MinioUser:     minioUser,
48-			MinioPass:     minioPass,
49-			Description:   "A zero-install static site hosting service for hackers",
50-			IntroText:     intro,
51-			Space:         "pgs",
52-			MaxSize:       maxSize,
53-			MaxAssetSize:  maxAssetSize,
54-			Logger:        shared.CreateLogger(debug == "1"),
55-			AllowRegister: allowRegister == "1",
56-		},
57+		Debug:        debug == "1",
58+		Secret:       secret,
59+		Domain:       domain,
60+		Port:         port,
61+		Protocol:     protocol,
62+		DbURL:        dbURL,
63+		StorageDir:   storageDir,
64+		MinioURL:     minioURL,
65+		MinioUser:    minioUser,
66+		MinioPass:    minioPass,
67+		Space:        "pgs",
68+		MaxSize:      maxSize,
69+		MaxAssetSize: maxAssetSize,
70+		Logger:       shared.CreateLogger(debug == "1"),
71 	}
72 
73 	return &cfg
M pgs/html/base.layout.tmpl
+1, -1
1@@ -8,7 +8,7 @@
2 
3     <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
4 
5-    <meta name="keywords" content="static, site, hosting" />
6+    <meta name="keywords" content="static, site, hosting, hackers, pico" />
7 
8     <link rel="stylesheet" href="https://pico.sh/smol.css" />
9     <link rel="stylesheet" href="main.css" />
M pgs/html/marketing.page.tmpl
+4, -4
 1@@ -1,20 +1,20 @@
 2 {{template "base" .}}
 3 
 4-{{define "title"}}{{.Site.Domain}} -- {{.Site.Description}}{{end}}
 5+{{define "title"}}{{.Site.Domain}} -- A zero-install static site hosting service for hackers{{end}}
 6 
 7 {{define "meta"}}
 8-<meta name="description" content="{{.Site.Description}}" />
 9+<meta name="description" content="A zero-install static site hosting service for hackers" />
10 
11 <meta property="og:type" content="website">
12 <meta property="og:site_name" content="{{.Site.Domain}}">
13 <meta property="og:url" content="https://{{.Site.Domain}}">
14 <meta property="og:title" content="{{.Site.Domain}}">
15-<meta property="og:description" content="{{.Site.Description}}">
16+<meta property="og:description" content="A zero-install static site hosting service for hackers">
17 
18 <meta name="twitter:card" content="summary" />
19 <meta property="twitter:url" content="https://{{.Site.Domain}}">
20 <meta property="twitter:title" content="{{.Site.Domain}}">
21-<meta property="twitter:description" content="{{.Site.Description}}">
22+<meta property="twitter:description" content="A zero-install static site hosting service for hackers">
23 <meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
24 <meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
25 
M pgs/ssh.go
+2, -1
 1@@ -16,6 +16,7 @@ import (
 2 	uploadassets "github.com/picosh/pico/filehandlers/assets"
 3 	"github.com/picosh/pico/shared"
 4 	"github.com/picosh/pico/shared/storage"
 5+	"github.com/picosh/pico/tui"
 6 	wsh "github.com/picosh/pico/wish"
 7 	"github.com/picosh/ptun"
 8 	"github.com/picosh/send/list"
 9@@ -40,7 +41,7 @@ func createRouter(cfg *shared.ConfigSite, handler *uploadassets.UploadAssetHandl
10 			scp.Middleware(handler),
11 			wishrsync.Middleware(handler),
12 			auth.Middleware(handler),
13-			wsh.PtyMdw(bm.Middleware(CmsMiddleware(&cfg.ConfigCms, cfg))),
14+			wsh.PtyMdw(bm.Middleware(tui.CmsMiddleware(cfg))),
15 			WishMiddleware(handler),
16 			wsh.LogMiddleware(handler.GetLogger()),
17 		}
M pgs/wish.go
+1, -1
1@@ -12,7 +12,7 @@ import (
2 	"github.com/picosh/pico/db"
3 	uploadassets "github.com/picosh/pico/filehandlers/assets"
4 	"github.com/picosh/pico/shared"
5-	"github.com/picosh/pico/wish/cms/ui/common"
6+	"github.com/picosh/pico/tui/common"
7 	"github.com/picosh/send/send/utils"
8 )
9 
M prose/api.go
+1, -2
 1@@ -905,7 +905,7 @@ func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
 2 
 3 func StartApiServer() {
 4 	cfg := NewConfigSite()
 5-	dbpool := postgres.NewDB(cfg.ConfigCms.DbURL, cfg.Logger)
 6+	dbpool := postgres.NewDB(cfg.DbURL, cfg.Logger)
 7 	defer dbpool.Close()
 8 	logger := cfg.Logger
 9 
10@@ -946,7 +946,6 @@ func StartApiServer() {
11 		"Starting server on port",
12 		"port", cfg.Port,
13 		"domain", cfg.Domain,
14-		"email", cfg.Email,
15 	)
16 
17 	logger.Error(http.ListenAndServe(portStr, router).Error())
M prose/config.go
+23, -41
 1@@ -2,24 +2,18 @@ package prose
 2 
 3 import (
 4 	"github.com/picosh/pico/shared"
 5-	"github.com/picosh/pico/wish/cms/config"
 6 )
 7 
 8 func NewConfigSite() *shared.ConfigSite {
 9 	debug := shared.GetEnv("PROSE_DEBUG", "0")
10 	domain := shared.GetEnv("PROSE_DOMAIN", "prose.sh")
11-	email := shared.GetEnv("PROSE_EMAIL", "hello@prose.sh")
12-	subdomains := shared.GetEnv("PROSE_SUBDOMAINS", "0")
13-	customdomains := shared.GetEnv("PROSE_CUSTOMDOMAINS", "0")
14 	port := shared.GetEnv("PROSE_WEB_PORT", "3000")
15 	protocol := shared.GetEnv("PROSE_PROTOCOL", "https")
16-	allowRegister := shared.GetEnv("PROSE_ALLOW_REGISTER", "1")
17 	storageDir := shared.GetEnv("IMGS_STORAGE_DIR", ".storage")
18 	minioURL := shared.GetEnv("MINIO_URL", "")
19 	minioUser := shared.GetEnv("MINIO_ROOT_USER", "")
20 	minioPass := shared.GetEnv("MINIO_ROOT_PASSWORD", "")
21 	dbURL := shared.GetEnv("DATABASE_URL", "")
22-	useImgProxy := shared.GetEnv("USE_IMGPROXY", "1")
23 	maxSize := uint64(500 * shared.MB)
24 	maxImgSize := int64(10 * shared.MB)
25 	secret := shared.GetEnv("PICO_SECRET", "")
26@@ -27,42 +21,30 @@ func NewConfigSite() *shared.ConfigSite {
27 		panic("must provide PICO_SECRET environment variable")
28 	}
29 
30-	intro := "To get started, enter a username.\n"
31-	intro += "To learn next steps go to our docs at https://pico.sh/prose\n"
32-
33 	return &shared.ConfigSite{
34-		Debug:                debug == "1",
35-		SubdomainsEnabled:    subdomains == "1",
36-		CustomdomainsEnabled: customdomains == "1",
37-		UseImgProxy:          useImgProxy == "1",
38-		Secret:               secret,
39-		ConfigCms: config.ConfigCms{
40-			Domain:      domain,
41-			Email:       email,
42-			Port:        port,
43-			Protocol:    protocol,
44-			DbURL:       dbURL,
45-			StorageDir:  storageDir,
46-			MinioURL:    minioURL,
47-			MinioUser:   minioUser,
48-			MinioPass:   minioPass,
49-			Description: "A blog platform for hackers.",
50-			IntroText:   intro,
51-			Space:       "prose",
52-			AllowedExt: []string{
53-				".md",
54-				".jpg",
55-				".jpeg",
56-				".png",
57-				".gif",
58-				".webp",
59-				".svg",
60-			},
61-			HiddenPosts:   []string{"_readme.md", "_styles.css", "_footer.md", "_404.md"},
62-			Logger:        shared.CreateLogger(debug == "1"),
63-			AllowRegister: allowRegister == "1",
64-			MaxSize:       maxSize,
65-			MaxAssetSize:  maxImgSize,
66+		Debug:      debug == "1",
67+		Secret:     secret,
68+		Domain:     domain,
69+		Port:       port,
70+		Protocol:   protocol,
71+		DbURL:      dbURL,
72+		StorageDir: storageDir,
73+		MinioURL:   minioURL,
74+		MinioUser:  minioUser,
75+		MinioPass:  minioPass,
76+		Space:      "prose",
77+		AllowedExt: []string{
78+			".md",
79+			".jpg",
80+			".jpeg",
81+			".png",
82+			".gif",
83+			".webp",
84+			".svg",
85 		},
86+		HiddenPosts:  []string{"_readme.md", "_styles.css", "_footer.md", "_404.md"},
87+		Logger:       shared.CreateLogger(debug == "1"),
88+		MaxSize:      maxSize,
89+		MaxAssetSize: maxImgSize,
90 	}
91 }
M prose/html/base.layout.tmpl
+1, -1
1@@ -6,7 +6,7 @@
2         <meta name="viewport" content="width=device-width, initial-scale=1" />
3         <title>{{template "title" .}}</title>
4 
5-        <meta name="keywords" content="blog, blogging, write, writing" />
6+        <meta name="keywords" content="blog, blogging, write, writing, hackers, developers, terminal" />
7 
8         <link rel="stylesheet" href="/smol.css" />
9         <link rel="stylesheet" href="/main.css" />
M prose/html/read.page.tmpl
+21, -2
 1@@ -1,10 +1,29 @@
 2 {{template "base" .}}
 3 
 4-{{define "title"}}discover prose -- {{.Site.Domain}}{{end}}
 5+{{define "title"}}prose.sh -- A blog platform for hackers{{end}}
 6 
 7 {{define "meta"}}
 8-<meta name="description" content="discover interesting posts" />
 9+<meta name="description" content="A blog platform for hackers" />
10 <link rel="alternate" href="/rss" type="application/rss+xml" title="RSS feed for prose.sh" />
11+<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
12+
13+<meta property="og:type" content="website">
14+<meta property="og:site_name" content="{{.Site.Domain}}">
15+<meta property="og:url" content="https://{{.Site.Domain}}">
16+<meta property="og:title" content="{{.Site.Domain}}">
17+<meta property="og:description" content="A blog platform for hackers">
18+
19+<meta name="twitter:card" content="summary" />
20+<meta property="twitter:url" content="https://{{.Site.Domain}}">
21+<meta property="twitter:title" content="{{.Site.Domain}}">
22+<meta property="twitter:description" content="A blog platform for hackers">
23+<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
24+<meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
25+
26+<meta property="og:image:width" content="300" />
27+<meta property="og:image:height" content="300" />
28+<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
29+<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
30 {{end}}
31 
32 {{define "attrs"}}{{end}}
M prose/ssh.go
+1, -3
 1@@ -11,14 +11,12 @@ import (
 2 	"github.com/charmbracelet/promwish"
 3 	"github.com/charmbracelet/ssh"
 4 	"github.com/charmbracelet/wish"
 5-	bm "github.com/charmbracelet/wish/bubbletea"
 6 	"github.com/picosh/pico/db/postgres"
 7 	"github.com/picosh/pico/filehandlers"
 8 	uploadimgs "github.com/picosh/pico/filehandlers/imgs"
 9 	"github.com/picosh/pico/shared"
10 	"github.com/picosh/pico/shared/storage"
11 	wsh "github.com/picosh/pico/wish"
12-	"github.com/picosh/pico/wish/cms"
13 	"github.com/picosh/send/list"
14 	"github.com/picosh/send/pipe"
15 	"github.com/picosh/send/proxy"
16@@ -42,7 +40,7 @@ func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
17 			scp.Middleware(handler),
18 			wishrsync.Middleware(handler),
19 			auth.Middleware(handler),
20-			wsh.PtyMdw(bm.Middleware(cms.Middleware(&handler.Cfg.ConfigCms, handler.Cfg))),
21+			wsh.PtyMdw(wsh.DeprecatedNotice()),
22 			wsh.LogMiddleware(handler.GetLogger()),
23 		}
24 	}
M shared/api.go
+1, -1
1@@ -53,7 +53,7 @@ func CheckHandler(w http.ResponseWriter, r *http.Request) {
2 
3 	if cfg.IsCustomdomains() {
4 		hostDomain := r.URL.Query().Get("domain")
5-		appDomain := strings.Split(cfg.ConfigCms.Domain, ":")[0]
6+		appDomain := strings.Split(cfg.Domain, ":")[0]
7 
8 		if !strings.Contains(hostDomain, appDomain) {
9 			subdomain := GetCustomDomain(hostDomain, cfg.Space)
M shared/config.go
+29, -20
 1@@ -9,15 +9,13 @@ import (
 2 	"os"
 3 	"path"
 4 	"strings"
 5-
 6-	"github.com/picosh/pico/wish/cms/config"
 7 )
 8 
 9+var DefaultEmail = "hello@pico.sh"
10+
11 type SitePageData struct {
12-	Domain      template.URL
13-	HomeURL     template.URL
14-	Email       string
15-	Description string
16+	Domain  template.URL
17+	HomeURL template.URL
18 }
19 
20 type PageData struct {
21@@ -25,14 +23,27 @@ type PageData struct {
22 }
23 
24 type ConfigSite struct {
25-	config.ConfigCms
26-	config.ConfigURL
27-	Debug                bool
28-	SubdomainsEnabled    bool
29-	CustomdomainsEnabled bool
30-	SendgridKey          string
31-	UseImgProxy          bool
32-	Secret               string
33+	Debug        bool
34+	SendgridKey  string
35+	Secret       string
36+	Domain       string
37+	Port         string
38+	Protocol     string
39+	DbURL        string
40+	StorageDir   string
41+	MinioURL     string
42+	MinioUser    string
43+	MinioPass    string
44+	Space        string
45+	AllowedExt   []string
46+	HiddenPosts  []string
47+	MaxSize      uint64
48+	MaxAssetSize int64
49+	Logger       *slog.Logger
50+}
51+
52+func NewConfigSite() *ConfigSite {
53+	return &ConfigSite{}
54 }
55 
56 type CreateURL struct {
57@@ -69,19 +80,17 @@ func CreateURLFromRequest(cfg *ConfigSite, r *http.Request) *CreateURL {
58 
59 func (c *ConfigSite) GetSiteData() *SitePageData {
60 	return &SitePageData{
61-		Domain:      template.URL(c.Domain),
62-		HomeURL:     template.URL(c.HomeURL()),
63-		Email:       c.Email,
64-		Description: c.Description,
65+		Domain:  template.URL(c.Domain),
66+		HomeURL: template.URL(c.HomeURL()),
67 	}
68 }
69 
70 func (c *ConfigSite) IsSubdomains() bool {
71-	return c.SubdomainsEnabled
72+	return true
73 }
74 
75 func (c *ConfigSite) IsCustomdomains() bool {
76-	return c.CustomdomainsEnabled
77+	return true
78 }
79 
80 func (c *ConfigSite) HomeURL() string {
M shared/router.go
+1, -1
1@@ -111,7 +111,7 @@ func findRouteConfig(r *http.Request, routes []Route, subdomainRoutes []Route, c
2 
3 	if cfg.IsCustomdomains() || cfg.IsSubdomains() {
4 		hostDomain := strings.ToLower(strings.Split(r.Host, ":")[0])
5-		appDomain := strings.ToLower(strings.Split(cfg.ConfigCms.Domain, ":")[0])
6+		appDomain := strings.ToLower(strings.Split(cfg.Domain, ":")[0])
7 
8 		if hostDomain != appDomain {
9 			if strings.Contains(hostDomain, appDomain) {
R wish/cms/ui/account/create.go => tui/account/create.go
+7, -12
 1@@ -9,8 +9,7 @@ import (
 2 	input "github.com/charmbracelet/bubbles/textinput"
 3 	tea "github.com/charmbracelet/bubbletea"
 4 	"github.com/picosh/pico/db"
 5-	"github.com/picosh/pico/wish/cms/config"
 6-	"github.com/picosh/pico/wish/cms/ui/common"
 7+	"github.com/picosh/pico/tui/common"
 8 )
 9 
10 type state int
11@@ -49,7 +48,6 @@ type CreateModel struct {
12 	Done bool // true when it's time to exit this view
13 	Quit bool // true when the user wants to quit the whole program
14 
15-	cfg       *config.ConfigCms
16 	dbpool    db.DB
17 	publicKey string
18 	styles    common.Styles
19@@ -94,7 +92,7 @@ func (m *CreateModel) indexBackward() {
20 }
21 
22 // NewModel returns a new username model in its initial state.
23-func NewCreateModel(styles common.Styles, cfg *config.ConfigCms, dbpool db.DB, publicKey string) CreateModel {
24+func NewCreateModel(styles common.Styles, dbpool db.DB, publicKey string) CreateModel {
25 	im := input.New()
26 	im.Cursor.Style = styles.Cursor
27 	im.Placeholder = "enter username"
28@@ -103,7 +101,6 @@ func NewCreateModel(styles common.Styles, cfg *config.ConfigCms, dbpool db.DB, p
29 	im.Focus()
30 
31 	return CreateModel{
32-		cfg:       cfg,
33 		Done:      false,
34 		Quit:      false,
35 		dbpool:    dbpool,
36@@ -119,9 +116,9 @@ func NewCreateModel(styles common.Styles, cfg *config.ConfigCms, dbpool db.DB, p
37 }
38 
39 // Init is the Bubble Tea initialization function.
40-func Init(styles common.Styles, cfg *config.ConfigCms, dbpool db.DB, publicKey string) func() (CreateModel, tea.Cmd) {
41+func Init(styles common.Styles, dbpool db.DB, publicKey string) func() (CreateModel, tea.Cmd) {
42 	return func() (CreateModel, tea.Cmd) {
43-		m := NewCreateModel(styles, cfg, dbpool, publicKey)
44+		m := NewCreateModel(styles, dbpool, publicKey)
45 		return m, InitialCmd()
46 	}
47 }
48@@ -237,11 +234,9 @@ func Update(msg tea.Msg, m CreateModel) (CreateModel, tea.Cmd) {
49 
50 // View renders current view from the model.
51 func View(m CreateModel) string {
52-	if !m.cfg.AllowRegister {
53-		return "Registration is closed for this service.  Press 'esc' to exit."
54-	}
55-
56-	s := fmt.Sprintf("%s\n\n%s\n", "hacker labs", m.cfg.IntroText)
57+	intro := "To create an account, enter a username.\n"
58+	intro += "After that, go to https://pico.sh/getting-started#next-steps"
59+	s := fmt.Sprintf("%s\n\n%s\n", "hacker labs", intro)
60 	s += fmt.Sprintf("Public Key: %s\n\n", m.publicKey)
61 	s += m.input.View() + "\n\n"
62 
R pgs/cms.go => tui/cms.go
+19, -32
  1@@ -1,4 +1,4 @@
  2-package pgs
  3+package tui
  4 
  5 import (
  6 	"errors"
  7@@ -15,13 +15,11 @@ import (
  8 	"github.com/picosh/pico/db"
  9 	"github.com/picosh/pico/db/postgres"
 10 	"github.com/picosh/pico/shared"
 11-	"github.com/picosh/pico/shared/storage"
 12-	"github.com/picosh/pico/wish/cms/config"
 13-	"github.com/picosh/pico/wish/cms/ui/account"
 14-	"github.com/picosh/pico/wish/cms/ui/common"
 15-	"github.com/picosh/pico/wish/cms/ui/info"
 16-	"github.com/picosh/pico/wish/cms/ui/keys"
 17-	"github.com/picosh/pico/wish/cms/ui/tokens"
 18+	"github.com/picosh/pico/tui/account"
 19+	"github.com/picosh/pico/tui/common"
 20+	"github.com/picosh/pico/tui/info"
 21+	"github.com/picosh/pico/tui/keys"
 22+	"github.com/picosh/pico/tui/tokens"
 23 )
 24 
 25 type status int
 26@@ -72,7 +70,7 @@ func NewSpinner(styles common.Styles) spinner.Model {
 27 
 28 type GotDBMsg db.DB
 29 
 30-func CmsMiddleware(cfg *config.ConfigCms, urls config.ConfigURL) bm.Handler {
 31+func CmsMiddleware(cfg *shared.ConfigSite) bm.Handler {
 32 	return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
 33 		logger := cfg.Logger
 34 
 35@@ -91,13 +89,6 @@ func CmsMiddleware(cfg *config.ConfigCms, urls config.ConfigURL) bm.Handler {
 36 
 37 		dbpool := postgres.NewDB(cfg.DbURL, cfg.Logger)
 38 
 39-		var st storage.StorageServe
 40-		if cfg.MinioURL == "" {
 41-			st, err = storage.NewStorageFS(cfg.StorageDir)
 42-		} else {
 43-			st, err = storage.NewStorageMinio(cfg.MinioURL, cfg.MinioUser, cfg.MinioPass)
 44-		}
 45-
 46 		if err != nil {
 47 			logger.Error(err.Error())
 48 		}
 49@@ -108,10 +99,8 @@ func CmsMiddleware(cfg *config.ConfigCms, urls config.ConfigURL) bm.Handler {
 50 
 51 		m := model{
 52 			cfg:        cfg,
 53-			urls:       urls,
 54 			publicKey:  key,
 55 			dbpool:     dbpool,
 56-			st:         st,
 57 			sshUser:    sshUser,
 58 			status:     statusInit,
 59 			menuChoice: unsetChoice,
 60@@ -139,11 +128,9 @@ func CmsMiddleware(cfg *config.ConfigCms, urls config.ConfigURL) bm.Handler {
 61 
 62 // Just a generic tea.Model to demo terminal information of ssh.
 63 type model struct {
 64-	cfg             *config.ConfigCms
 65-	urls            config.ConfigURL
 66+	cfg             *shared.ConfigSite
 67 	publicKey       string
 68 	dbpool          db.DB
 69-	st              storage.StorageServe
 70 	user            *db.User
 71 	plusFeatureFlag *db.FeatureFlag
 72 	err             error
 73@@ -247,18 +234,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 74 		m.status = statusReady
 75 		m.info.User = msg
 76 		m.user = msg
 77-		m.info = info.NewModel(m.styles, m.cfg, m.urls, m.user, m.plusFeatureFlag)
 78-		m.keys = keys.NewModel(m.styles, m.cfg, m.dbpool, m.user)
 79-		m.tokens = tokens.NewModel(m.styles, m.cfg, m.dbpool, m.user)
 80-		m.createAccount = account.NewCreateModel(m.styles, m.cfg, m.dbpool, m.publicKey)
 81+		m.info = info.NewModel(m.styles, m.user, m.plusFeatureFlag)
 82+		m.keys = keys.NewModel(m.styles, m.cfg.Logger, m.dbpool, m.user)
 83+		m.tokens = tokens.NewModel(m.styles, m.dbpool, m.user)
 84+		m.createAccount = account.NewCreateModel(m.styles, m.dbpool, m.publicKey)
 85 	}
 86 
 87 	switch m.status {
 88 	case statusInit:
 89-		m.info = info.NewModel(m.styles, m.cfg, m.urls, m.user, m.plusFeatureFlag)
 90-		m.keys = keys.NewModel(m.styles, m.cfg, m.dbpool, m.user)
 91-		m.tokens = tokens.NewModel(m.styles, m.cfg, m.dbpool, m.user)
 92-		m.createAccount = account.NewCreateModel(m.styles, m.cfg, m.dbpool, m.publicKey)
 93+		m.info = info.NewModel(m.styles, m.user, m.plusFeatureFlag)
 94+		m.keys = keys.NewModel(m.styles, m.cfg.Logger, m.dbpool, m.user)
 95+		m.tokens = tokens.NewModel(m.styles, m.dbpool, m.user)
 96+		m.createAccount = account.NewCreateModel(m.styles, m.dbpool, m.publicKey)
 97 		if m.user == nil {
 98 			m.status = statusNoAccount
 99 		} else {
100@@ -288,7 +275,7 @@ func updateChildren(msg tea.Msg, m model) (model, tea.Cmd) {
101 		cmd = newCmd
102 
103 		if m.keys.Exit {
104-			m.keys = keys.NewModel(m.styles, m.cfg, m.dbpool, m.user)
105+			m.keys = keys.NewModel(m.styles, m.cfg.Logger, m.dbpool, m.user)
106 			m.status = statusReady
107 		} else if m.keys.Quit {
108 			m.status = statusQuitting
109@@ -304,7 +291,7 @@ func updateChildren(msg tea.Msg, m model) (model, tea.Cmd) {
110 		cmd = newCmd
111 
112 		if m.tokens.Exit {
113-			m.tokens = tokens.NewModel(m.styles, m.cfg, m.dbpool, m.user)
114+			m.tokens = tokens.NewModel(m.styles, m.dbpool, m.user)
115 			m.status = statusReady
116 		} else if m.tokens.Quit {
117 			m.status = statusQuitting
118@@ -313,7 +300,7 @@ func updateChildren(msg tea.Msg, m model) (model, tea.Cmd) {
119 	case statusNoAccount:
120 		m.createAccount, cmd = account.Update(msg, m.createAccount)
121 		if m.createAccount.Done {
122-			m.createAccount = account.NewCreateModel(m.styles, m.cfg, m.dbpool, m.publicKey) // reset the state
123+			m.createAccount = account.NewCreateModel(m.styles, m.dbpool, m.publicKey) // reset the state
124 			m.status = statusReady
125 		} else if m.createAccount.Quit {
126 			m.status = statusQuitting
R wish/cms/ui/common/styles.go => tui/common/styles.go
+8, -2
 1@@ -109,7 +109,8 @@ type Styles struct {
 2 	CliPadding,
 3 	CliBorder,
 4 	CliHeader,
 5-	App lipgloss.Style
 6+	App,
 7+	RoundedBorder lipgloss.Style
 8 	Renderer *lipgloss.Renderer
 9 }
10 
11@@ -152,7 +153,7 @@ func DefaultStyles(renderer *lipgloss.Renderer) Styles {
12 	s.SelectedMenuItem = renderer.NewStyle().Foreground(Fuschia)
13 	s.Logo = renderer.NewStyle().
14 		Foreground(Cream).
15-		Background(lipgloss.Color("#5A56E0")).
16+		Background(Indigo).
17 		Padding(0, 1)
18 	s.BlurredButtonStyle = renderer.NewStyle().
19 		Foreground(Cream).
20@@ -172,6 +173,11 @@ func DefaultStyles(renderer *lipgloss.Renderer) Styles {
21 	s.CliPadding = renderer.NewStyle().Padding(0, 1)
22 	s.CliHeader = s.CliPadding.Copy().Foreground(Indigo).Bold(true)
23 	s.CliBorder = renderer.NewStyle().Foreground(lipgloss.Color("238"))
24+	s.RoundedBorder = renderer.
25+		NewStyle().
26+		Padding(0, 1).
27+		BorderForeground(Indigo).
28+		Border(lipgloss.RoundedBorder(), true, true)
29 
30 	return s
31 }
R wish/cms/ui/common/views.go => tui/common/views.go
+0, -0
R wish/cms/ui/createkey/create.go => tui/createkey/create.go
+2, -3
 1@@ -8,8 +8,7 @@ import (
 2 	input "github.com/charmbracelet/bubbles/textinput"
 3 	tea "github.com/charmbracelet/bubbletea"
 4 	"github.com/picosh/pico/db"
 5-	"github.com/picosh/pico/wish/cms/config"
 6-	"github.com/picosh/pico/wish/cms/ui/common"
 7+	"github.com/picosh/pico/tui/common"
 8 	"golang.org/x/crypto/ssh"
 9 )
10 
11@@ -87,7 +86,7 @@ func (m *Model) indexBackward() {
12 }
13 
14 // NewModel returns a new username model in its initial state.
15-func NewModel(styles common.Styles, cfg *config.ConfigCms, dbpool db.DB, user *db.User) Model {
16+func NewModel(styles common.Styles, dbpool db.DB, user *db.User) Model {
17 	im := input.New()
18 	im.Cursor.Style = styles.Cursor
19 	im.Placeholder = "ssh-ed25519 AAAA..."
R wish/cms/ui/createtoken/create.go => tui/createtoken/create.go
+2, -3
 1@@ -8,8 +8,7 @@ import (
 2 	input "github.com/charmbracelet/bubbles/textinput"
 3 	tea "github.com/charmbracelet/bubbletea"
 4 	"github.com/picosh/pico/db"
 5-	"github.com/picosh/pico/wish/cms/config"
 6-	"github.com/picosh/pico/wish/cms/ui/common"
 7+	"github.com/picosh/pico/tui/common"
 8 )
 9 
10 type state int
11@@ -89,7 +88,7 @@ func (m *Model) indexBackward() {
12 }
13 
14 // NewModel returns a new username model in its initial state.
15-func NewModel(styles common.Styles, cfg *config.ConfigCms, dbpool db.DB, user *db.User) Model {
16+func NewModel(styles common.Styles, dbpool db.DB, user *db.User) Model {
17 	im := input.New()
18 	im.Cursor.Style = styles.Cursor
19 	im.Placeholder = "A name used for your reference"
R wish/cms/ui/info/info.go => tui/info/info.go
+2, -7
 1@@ -3,8 +3,7 @@ package info
 2 import (
 3 	tea "github.com/charmbracelet/bubbletea"
 4 	"github.com/picosh/pico/db"
 5-	"github.com/picosh/pico/wish/cms/config"
 6-	"github.com/picosh/pico/wish/cms/ui/common"
 7+	"github.com/picosh/pico/tui/common"
 8 )
 9 
10 type errMsg struct {
11@@ -18,8 +17,6 @@ func (e errMsg) Error() string {
12 
13 // Model stores the state of the info user interface.
14 type Model struct {
15-	cfg             *config.ConfigCms
16-	urls            config.ConfigURL
17 	Quit            bool // signals it's time to exit the whole application
18 	Err             error
19 	User            *db.User
20@@ -28,13 +25,11 @@ type Model struct {
21 }
22 
23 // NewModel returns a new Model in its initial state.
24-func NewModel(styles common.Styles, cfg *config.ConfigCms, urls config.ConfigURL, user *db.User, ff *db.FeatureFlag) Model {
25+func NewModel(styles common.Styles, user *db.User, ff *db.FeatureFlag) Model {
26 	return Model{
27 		Quit:            false,
28 		User:            user,
29 		styles:          styles,
30-		cfg:             cfg,
31-		urls:            urls,
32 		PlusFeatureFlag: ff,
33 	}
34 }
R wish/cms/ui/keys/keys.go => tui/keys/keys.go
+11, -12
 1@@ -1,15 +1,14 @@
 2 package keys
 3 
 4 import (
 5-	"fmt"
 6+	"log/slog"
 7 
 8 	pager "github.com/charmbracelet/bubbles/paginator"
 9 	"github.com/charmbracelet/bubbles/spinner"
10 	tea "github.com/charmbracelet/bubbletea"
11 	"github.com/picosh/pico/db"
12-	"github.com/picosh/pico/wish/cms/config"
13-	"github.com/picosh/pico/wish/cms/ui/common"
14-	"github.com/picosh/pico/wish/cms/ui/createkey"
15+	"github.com/picosh/pico/tui/common"
16+	"github.com/picosh/pico/tui/createkey"
17 )
18 
19 const keysPerPage = 4
20@@ -47,7 +46,7 @@ type (
21 
22 // Model is the Tea state model for this user interface.
23 type Model struct {
24-	cfg            *config.ConfigCms
25+	logger         *slog.Logger
26 	dbpool         db.DB
27 	user           *db.User
28 	styles         common.Styles
29@@ -82,14 +81,14 @@ func (m *Model) UpdatePaging(msg tea.Msg) {
30 }
31 
32 // NewModel creates a new model with defaults.
33-func NewModel(styles common.Styles, cfg *config.ConfigCms, dbpool db.DB, user *db.User) Model {
34+func NewModel(styles common.Styles, logger *slog.Logger, dbpool db.DB, user *db.User) Model {
35 	p := pager.New()
36 	p.PerPage = keysPerPage
37 	p.Type = pager.Dots
38 	p.InactiveDot = styles.InactivePagination.Render("•")
39 
40 	return Model{
41-		cfg:            cfg,
42+		logger:         logger,
43 		dbpool:         dbpool,
44 		user:           user,
45 		styles:         styles,
46@@ -236,7 +235,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
47 
48 	switch m.state {
49 	case stateNormal:
50-		m.createKey = createkey.NewModel(m.styles, m.cfg, m.dbpool, m.user)
51+		m.createKey = createkey.NewModel(m.styles, m.dbpool, m.user)
52 	case stateDeletingKey:
53 		// If an item is being confirmed for delete, any key (other than the key
54 		// used for confirmation above) cancels the deletion
55@@ -269,7 +268,7 @@ func updateChildren(msg tea.Msg, m Model) (Model, tea.Cmd) {
56 		m.createKey = createKeyModel
57 		cmd = newCmd
58 		if m.createKey.Done {
59-			m.createKey = createkey.NewModel(m.styles, m.cfg, m.dbpool, m.user) // reset the state
60+			m.createKey = createkey.NewModel(m.styles, m.dbpool, m.user) // reset the state
61 			m.state = stateNormal
62 		} else if m.createKey.Quit {
63 			m.state = stateQuitting
64@@ -293,11 +292,11 @@ func (m Model) View() string {
65 	case stateLoading:
66 		s = m.spinner.View() + " Loading...\n\n"
67 	case stateQuitting:
68-		s = fmt.Sprintf("Thanks for using %s!\n", m.cfg.Domain)
69+		s = "Thanks for using pico.sh!\n"
70 	case stateCreateKey:
71 		s = m.createKey.View()
72 	default:
73-		s = fmt.Sprintf("Here are the keys linked to your %s account.\n\n", m.cfg.Domain)
74+		s = "Here are the keys linked to your pico.sh account.\n\n"
75 
76 		// Keys
77 		s += keysView(m)
78@@ -409,7 +408,7 @@ func unlinkKey(m Model) tea.Cmd {
79 func deleteAccount(m Model) tea.Cmd {
80 	return func() tea.Msg {
81 		id := m.keys[m.getSelectedIndex()].UserID
82-		m.cfg.Logger.Info("user requested account deletion", "user", m.user.Name, "id", id)
83+		m.logger.Info("user requested account deletion", "user", m.user.Name, "id", id)
84 		err := m.dbpool.RemoveUsers([]string{id})
85 		if err != nil {
86 			return errMsg{err}
R wish/cms/ui/keys/keyview.go => tui/keys/keyview.go
+1, -1
1@@ -5,7 +5,7 @@ import (
2 	"strings"
3 
4 	"github.com/picosh/pico/db"
5-	"github.com/picosh/pico/wish/cms/ui/common"
6+	"github.com/picosh/pico/tui/common"
7 	"golang.org/x/crypto/ssh"
8 )
9 
R wish/cms/ui/tokens/tokens.go => tui/tokens/tokens.go
+6, -11
 1@@ -1,15 +1,12 @@
 2 package tokens
 3 
 4 import (
 5-	"fmt"
 6-
 7 	pager "github.com/charmbracelet/bubbles/paginator"
 8 	"github.com/charmbracelet/bubbles/spinner"
 9 	tea "github.com/charmbracelet/bubbletea"
10 	"github.com/picosh/pico/db"
11-	"github.com/picosh/pico/wish/cms/config"
12-	"github.com/picosh/pico/wish/cms/ui/common"
13-	"github.com/picosh/pico/wish/cms/ui/createtoken"
14+	"github.com/picosh/pico/tui/common"
15+	"github.com/picosh/pico/tui/createtoken"
16 )
17 
18 const keysPerPage = 4
19@@ -45,7 +42,6 @@ type (
20 
21 // Model is the Tea state model for this user interface.
22 type Model struct {
23-	cfg            *config.ConfigCms
24 	dbpool         db.DB
25 	user           *db.User
26 	styles         common.Styles
27@@ -80,14 +76,13 @@ func (m *Model) UpdatePaging(msg tea.Msg) {
28 }
29 
30 // NewModel creates a new model with defaults.
31-func NewModel(styles common.Styles, cfg *config.ConfigCms, dbpool db.DB, user *db.User) Model {
32+func NewModel(styles common.Styles, dbpool db.DB, user *db.User) Model {
33 	p := pager.New()
34 	p.PerPage = keysPerPage
35 	p.Type = pager.Dots
36 	p.InactiveDot = styles.InactivePagination.Render("•")
37 
38 	return Model{
39-		cfg:            cfg,
40 		dbpool:         dbpool,
41 		user:           user,
42 		styles:         styles,
43@@ -207,7 +202,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
44 
45 	switch m.state {
46 	case stateNormal:
47-		m.createKey = createtoken.NewModel(m.styles, m.cfg, m.dbpool, m.user)
48+		m.createKey = createtoken.NewModel(m.styles, m.dbpool, m.user)
49 	case stateDeletingKey:
50 		// If an item is being confirmed for delete, any key (other than the key
51 		// used for confirmation above) cancels the deletion
52@@ -240,7 +235,7 @@ func updateChildren(msg tea.Msg, m Model) (Model, tea.Cmd) {
53 		m.createKey = createKeyModel
54 		cmd = newCmd
55 		if m.createKey.Done {
56-			m.createKey = createtoken.NewModel(m.styles, m.cfg, m.dbpool, m.user) // reset the state
57+			m.createKey = createtoken.NewModel(m.styles, m.dbpool, m.user) // reset the state
58 			m.state = stateNormal
59 		} else if m.createKey.Quit {
60 			m.state = stateQuitting
61@@ -264,7 +259,7 @@ func (m Model) View() string {
62 	case stateLoading:
63 		s = m.spinner.View() + " Loading...\n\n"
64 	case stateQuitting:
65-		s = fmt.Sprintf("Thanks for using %s!\n", m.cfg.Domain)
66+		s = "Thanks for using pico.sh!\n"
67 	case stateCreateKey:
68 		s = m.createKey.View()
69 	default:
R wish/cms/ui/tokens/tokenview.go => tui/tokens/tokenview.go
+1, -1
1@@ -4,7 +4,7 @@ import (
2 	"fmt"
3 
4 	"github.com/picosh/pico/db"
5-	"github.com/picosh/pico/wish/cms/ui/common"
6+	"github.com/picosh/pico/tui/common"
7 )
8 
9 type styledKey struct {
D wish/cms/cms.go
+0, -431
  1@@ -1,431 +0,0 @@
  2-package cms
  3-
  4-import (
  5-	"errors"
  6-	"fmt"
  7-	"math"
  8-
  9-	"github.com/charmbracelet/bubbles/spinner"
 10-	tea "github.com/charmbracelet/bubbletea"
 11-	"github.com/charmbracelet/lipgloss"
 12-	"github.com/charmbracelet/ssh"
 13-	bm "github.com/charmbracelet/wish/bubbletea"
 14-	"github.com/muesli/reflow/indent"
 15-	"github.com/muesli/reflow/wordwrap"
 16-	"github.com/muesli/reflow/wrap"
 17-	"github.com/picosh/pico/db"
 18-	"github.com/picosh/pico/db/postgres"
 19-	"github.com/picosh/pico/shared"
 20-	"github.com/picosh/pico/shared/storage"
 21-	"github.com/picosh/pico/wish/cms/config"
 22-	"github.com/picosh/pico/wish/cms/ui/account"
 23-	"github.com/picosh/pico/wish/cms/ui/common"
 24-	"github.com/picosh/pico/wish/cms/ui/info"
 25-	"github.com/picosh/pico/wish/cms/ui/keys"
 26-	"github.com/picosh/pico/wish/cms/ui/posts"
 27-	"github.com/picosh/pico/wish/cms/ui/tokens"
 28-)
 29-
 30-type status int
 31-
 32-const (
 33-	statusInit status = iota
 34-	statusReady
 35-	statusNoAccount
 36-	statusBrowsingPosts
 37-	statusBrowsingKeys
 38-	statusBrowsingTokens
 39-	statusQuitting
 40-)
 41-
 42-func (s status) String() string {
 43-	return [...]string{
 44-		"initializing",
 45-		"ready",
 46-		"browsing posts",
 47-		"browsing keys",
 48-		"quitting",
 49-		"error",
 50-	}[s]
 51-}
 52-
 53-// menuChoice represents a chosen menu item.
 54-type menuChoice int
 55-
 56-// menu choices.
 57-const (
 58-	postsChoice menuChoice = iota
 59-	keysChoice
 60-	tokensChoice
 61-	exitChoice
 62-	unsetChoice // set when no choice has been made
 63-)
 64-
 65-// menu text corresponding to menu choices. these are presented to the user.
 66-var menuChoices = map[menuChoice]string{
 67-	keysChoice:   "Manage keys",
 68-	tokensChoice: "Manage tokens",
 69-	postsChoice:  "Manage posts",
 70-	exitChoice:   "Exit",
 71-}
 72-
 73-func NewSpinner(styles common.Styles) spinner.Model {
 74-	s := spinner.New()
 75-	s.Spinner = spinner.Dot
 76-	s.Style = styles.Spinner
 77-	return s
 78-}
 79-
 80-type GotDBMsg db.DB
 81-
 82-func Middleware(cfg *config.ConfigCms, urls config.ConfigURL) bm.Handler {
 83-	return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
 84-		logger := cfg.Logger
 85-
 86-		_, _, active := s.Pty()
 87-		if !active {
 88-			logger.Info("no active terminal, skipping")
 89-			return nil, nil
 90-		}
 91-		key, err := shared.KeyText(s)
 92-		if err != nil {
 93-			logger.Error(err.Error())
 94-		}
 95-
 96-		sshUser := s.User()
 97-
 98-		dbpool := postgres.NewDB(cfg.DbURL, cfg.Logger)
 99-
100-		var st storage.StorageServe
101-		if cfg.MinioURL == "" {
102-			st, err = storage.NewStorageFS(cfg.StorageDir)
103-		} else {
104-			st, err = storage.NewStorageMinio(cfg.MinioURL, cfg.MinioUser, cfg.MinioPass)
105-		}
106-
107-		if err != nil {
108-			logger.Error(err.Error())
109-		}
110-
111-		renderer := lipgloss.NewRenderer(s)
112-		renderer.SetOutput(common.OutputFromSession(s))
113-		styles := common.DefaultStyles(renderer)
114-
115-		m := model{
116-			cfg:        cfg,
117-			urls:       urls,
118-			publicKey:  key,
119-			dbpool:     dbpool,
120-			st:         st,
121-			sshUser:    sshUser,
122-			status:     statusInit,
123-			menuChoice: unsetChoice,
124-			styles:     styles,
125-			spinner:    common.NewSpinner(styles),
126-			terminalSize: tea.WindowSizeMsg{
127-				Width:  80,
128-				Height: 24,
129-			},
130-		}
131-
132-		user, err := m.findUser()
133-		if err != nil {
134-			_, _ = fmt.Fprintln(s.Stderr(), err)
135-			return nil, nil
136-		}
137-		m.user = user
138-
139-		ff, _ := m.findPlusFeatureFlag()
140-		m.plusFeatureFlag = ff
141-
142-		return m, []tea.ProgramOption{tea.WithAltScreen()}
143-	}
144-}
145-
146-// Just a generic tea.Model to demo terminal information of ssh.
147-type model struct {
148-	cfg             *config.ConfigCms
149-	urls            config.ConfigURL
150-	publicKey       string
151-	dbpool          db.DB
152-	st              storage.StorageServe
153-	user            *db.User
154-	plusFeatureFlag *db.FeatureFlag
155-	err             error
156-	sshUser         string
157-	status          status
158-	menuIndex       int
159-	menuChoice      menuChoice
160-	terminalSize    tea.WindowSizeMsg
161-	styles          common.Styles
162-	info            info.Model
163-	spinner         spinner.Model
164-	posts           posts.Model
165-	keys            keys.Model
166-	tokens          tokens.Model
167-	createAccount   account.CreateModel
168-}
169-
170-func (m model) Init() tea.Cmd {
171-	return m.spinner.Tick
172-}
173-
174-func (m model) findUser() (*db.User, error) {
175-	logger := m.cfg.Logger
176-	var user *db.User
177-
178-	if m.sshUser == "new" {
179-		logger.Info("user requesting to register account")
180-		return nil, nil
181-	}
182-
183-	user, err := m.dbpool.FindUserForKey(m.sshUser, m.publicKey)
184-
185-	if err != nil {
186-		logger.Error(err.Error())
187-		// we only want to throw an error for specific cases
188-		if errors.Is(err, &db.ErrMultiplePublicKeys{}) {
189-			return nil, err
190-		}
191-		return nil, nil
192-	}
193-
194-	return user, nil
195-}
196-
197-func (m model) findPlusFeatureFlag() (*db.FeatureFlag, error) {
198-	if m.user == nil {
199-		return nil, nil
200-	}
201-
202-	ff, err := m.dbpool.FindFeatureForUser(m.user.ID, "pgs")
203-	if err != nil {
204-		return nil, err
205-	}
206-
207-	return ff, nil
208-}
209-
210-func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
211-	var (
212-		cmds []tea.Cmd
213-		cmd  tea.Cmd
214-	)
215-
216-	switch msg := msg.(type) {
217-	case tea.WindowSizeMsg:
218-		m.terminalSize = msg
219-	case tea.KeyMsg:
220-		switch msg.Type {
221-		case tea.KeyCtrlC:
222-			m.dbpool.Close()
223-			return m, tea.Quit
224-		}
225-
226-		if m.status == statusReady { // Process keys for the menu
227-			switch msg.String() {
228-			// Quit
229-			case "q", "esc":
230-				m.status = statusQuitting
231-				m.dbpool.Close()
232-				return m, tea.Quit
233-
234-			// Prev menu item
235-			case "up", "k":
236-				m.menuIndex--
237-				if m.menuIndex < 0 {
238-					m.menuIndex = len(menuChoices) - 1
239-				}
240-
241-			// Select menu item
242-			case "enter":
243-				m.menuChoice = menuChoice(m.menuIndex)
244-
245-			// Next menu item
246-			case "down", "j":
247-				m.menuIndex++
248-				if m.menuIndex >= len(menuChoices) {
249-					m.menuIndex = 0
250-				}
251-			}
252-		}
253-
254-	case account.CreateAccountMsg:
255-		m.status = statusReady
256-		m.info.User = msg
257-		m.user = msg
258-		m.info = info.NewModel(m.styles, m.cfg, m.urls, m.user, m.plusFeatureFlag)
259-		m.keys = keys.NewModel(m.styles, m.cfg, m.dbpool, m.user)
260-		m.tokens = tokens.NewModel(m.styles, m.cfg, m.dbpool, m.user)
261-		m.createAccount = account.NewCreateModel(m.styles, m.cfg, m.dbpool, m.publicKey)
262-
263-		perPage := math.Floor(float64(m.terminalSize.Height) / 10.0)
264-		m.posts = posts.NewModel(m.styles, m.cfg, m.urls, m.dbpool, m.user, m.st, int(perPage))
265-	}
266-
267-	switch m.status {
268-	case statusInit:
269-		m.info = info.NewModel(m.styles, m.cfg, m.urls, m.user, m.plusFeatureFlag)
270-		m.keys = keys.NewModel(m.styles, m.cfg, m.dbpool, m.user)
271-		m.tokens = tokens.NewModel(m.styles, m.cfg, m.dbpool, m.user)
272-		m.createAccount = account.NewCreateModel(m.styles, m.cfg, m.dbpool, m.publicKey)
273-		if m.user == nil {
274-			m.status = statusNoAccount
275-		} else {
276-			m.status = statusReady
277-		}
278-
279-		perPage := math.Floor(float64(m.terminalSize.Height) / 10.0)
280-		m.posts = posts.NewModel(m.styles, m.cfg, m.urls, m.dbpool, m.user, m.st, int(perPage))
281-	}
282-
283-	m, cmd = updateChildren(msg, m)
284-	if cmd != nil {
285-		cmds = append(cmds, cmd)
286-	}
287-
288-	return m, tea.Batch(cmds...)
289-}
290-
291-func updateChildren(msg tea.Msg, m model) (model, tea.Cmd) {
292-	var cmd tea.Cmd
293-
294-	switch m.status {
295-	case statusBrowsingPosts:
296-		newModel, newCmd := m.posts.Update(msg)
297-		postsModel, ok := newModel.(posts.Model)
298-		if !ok {
299-			panic("could not perform assertion on posts model")
300-		}
301-		m.posts = postsModel
302-		cmd = newCmd
303-
304-		if m.posts.Exit {
305-			perPage := math.Floor(float64(m.terminalSize.Height) / 10.0)
306-			m.posts = posts.NewModel(m.styles, m.cfg, m.urls, m.dbpool, m.user, m.st, int(perPage))
307-
308-			m.posts = posts.NewModel(m.styles, m.cfg, m.urls, m.dbpool, m.user, m.st, int(perPage))
309-			m.status = statusReady
310-		} else if m.posts.Quit {
311-			m.status = statusQuitting
312-			return m, tea.Quit
313-		}
314-	case statusBrowsingKeys:
315-		newModel, newCmd := m.keys.Update(msg)
316-		keysModel, ok := newModel.(keys.Model)
317-		if !ok {
318-			panic("could not perform assertion on posts model")
319-		}
320-		m.keys = keysModel
321-		cmd = newCmd
322-
323-		if m.keys.Exit {
324-			m.keys = keys.NewModel(m.styles, m.cfg, m.dbpool, m.user)
325-			m.status = statusReady
326-		} else if m.keys.Quit {
327-			m.status = statusQuitting
328-			return m, tea.Quit
329-		}
330-	case statusBrowsingTokens:
331-		newModel, newCmd := m.tokens.Update(msg)
332-		tokensModel, ok := newModel.(tokens.Model)
333-		if !ok {
334-			panic("could not perform assertion on posts model")
335-		}
336-		m.tokens = tokensModel
337-		cmd = newCmd
338-
339-		if m.tokens.Exit {
340-			m.tokens = tokens.NewModel(m.styles, m.cfg, m.dbpool, m.user)
341-			m.status = statusReady
342-		} else if m.tokens.Quit {
343-			m.status = statusQuitting
344-			return m, tea.Quit
345-		}
346-	case statusNoAccount:
347-		m.createAccount, cmd = account.Update(msg, m.createAccount)
348-		if m.createAccount.Done {
349-			m.createAccount = account.NewCreateModel(m.styles, m.cfg, m.dbpool, m.publicKey) // reset the state
350-			m.status = statusReady
351-		} else if m.createAccount.Quit {
352-			m.status = statusQuitting
353-			return m, tea.Quit
354-		}
355-	}
356-
357-	// Handle the menu
358-	switch m.menuChoice {
359-	case postsChoice:
360-		m.status = statusBrowsingPosts
361-		m.menuChoice = unsetChoice
362-		cmd = posts.LoadPosts(m.posts)
363-	case keysChoice:
364-		m.status = statusBrowsingKeys
365-		m.menuChoice = unsetChoice
366-		cmd = keys.LoadKeys(m.keys)
367-	case tokensChoice:
368-		m.status = statusBrowsingTokens
369-		m.menuChoice = unsetChoice
370-		cmd = tokens.LoadKeys(m.tokens)
371-	case exitChoice:
372-		m.status = statusQuitting
373-		m.dbpool.Close()
374-		cmd = tea.Quit
375-	}
376-
377-	return m, cmd
378-}
379-
380-func (m model) menuView() string {
381-	var s string
382-	for i := 0; i < len(menuChoices); i++ {
383-		e := "  "
384-		menuItem := menuChoices[menuChoice(i)]
385-		if i == m.menuIndex {
386-			e = m.styles.SelectionMarker.String() +
387-				m.styles.SelectedMenuItem.Render(menuItem)
388-		} else {
389-			e += menuItem
390-		}
391-		if i < len(menuChoices)-1 {
392-			e += "\n"
393-		}
394-		s += e
395-	}
396-
397-	return s
398-}
399-
400-func footerView(m model) string {
401-	if m.err != nil {
402-		return m.errorView(m.err)
403-	}
404-	return "\n\n" + common.HelpView(m.styles, "j/k, ↑/↓: choose", "enter: select")
405-}
406-
407-func (m model) errorView(err error) string {
408-	head := m.styles.Error.Render("Error: ")
409-	body := m.styles.Subtle.Render(err.Error())
410-	msg := m.styles.Wrap.Render(head + body)
411-	return "\n\n" + indent.String(msg, 2)
412-}
413-
414-func (m model) View() string {
415-	w := m.terminalSize.Width - m.styles.App.GetHorizontalFrameSize()
416-	s := m.styles.Logo.SetString(m.cfg.Domain).String() + "\n\n"
417-	switch m.status {
418-	case statusNoAccount:
419-		s += account.View(m.createAccount)
420-	case statusReady:
421-		s += m.info.View()
422-		s += "\n\n" + m.menuView()
423-		s += footerView(m)
424-	case statusBrowsingPosts:
425-		s += m.posts.View()
426-	case statusBrowsingKeys:
427-		s += m.keys.View()
428-	case statusBrowsingTokens:
429-		s += m.tokens.View()
430-	}
431-	return m.styles.App.Render(wrap.String(wordwrap.String(s, w), w))
432-}
D wish/cms/config/config.go
+0, -33
 1@@ -1,33 +0,0 @@
 2-package config
 3-
 4-import "log/slog"
 5-
 6-type ConfigURL interface {
 7-	BlogURL(username string) string
 8-	PostURL(username string, filename string) string
 9-}
10-
11-type ConfigCms struct {
12-	Domain        string
13-	Port          string
14-	Email         string
15-	Protocol      string
16-	DbURL         string
17-	StorageDir    string
18-	MinioURL      string
19-	MinioUser     string
20-	MinioPass     string
21-	Description   string
22-	IntroText     string
23-	Space         string
24-	AllowedExt    []string
25-	HiddenPosts   []string
26-	Logger        *slog.Logger
27-	AllowRegister bool
28-	MaxSize       uint64
29-	MaxAssetSize  int64
30-}
31-
32-func NewConfigCms() *ConfigCms {
33-	return &ConfigCms{}
34-}
D wish/cms/ui/posts/post_view.go
+0, -97
 1@@ -1,97 +0,0 @@
 2-package posts
 3-
 4-import (
 5-	"fmt"
 6-
 7-	"github.com/picosh/pico/db"
 8-	"github.com/picosh/pico/wish/cms/config"
 9-	"github.com/picosh/pico/wish/cms/ui/common"
10-)
11-
12-type styledKey struct {
13-	styles         common.Styles
14-	date           string
15-	gutter         string
16-	postLabel      string
17-	dateLabel      string
18-	dateVal        string
19-	title          string
20-	urlLabel       string
21-	url            string
22-	views          int
23-	viewsLabel     string
24-	model          Model
25-	expiresAtLabel string
26-	expiresAt      string
27-}
28-
29-func (m Model) newStyledKey(styles common.Styles, post *db.Post, urls config.ConfigURL) styledKey {
30-	publishAt := post.PublishAt
31-
32-	expiresAt := styles.LabelDim.Render("never")
33-	if post.ExpiresAt != nil {
34-		expiresAt = styles.LabelDim.Render(post.ExpiresAt.Format("02 Jan, 2006"))
35-	}
36-
37-	// Default state
38-	return styledKey{
39-		styles:         styles,
40-		gutter:         " ",
41-		postLabel:      "post:",
42-		date:           publishAt.String(),
43-		dateLabel:      "publish_at:",
44-		dateVal:        styles.LabelDim.Render(publishAt.Format("02 Jan, 2006")),
45-		title:          post.Title,
46-		urlLabel:       "url:",
47-		url:            urls.PostURL(post.Username, post.Slug),
48-		viewsLabel:     "views:",
49-		views:          post.Views,
50-		model:          m,
51-		expiresAtLabel: "expires_at:",
52-		expiresAt:      expiresAt,
53-	}
54-}
55-
56-// Selected state.
57-func (k *styledKey) selected() {
58-	k.gutter = common.VerticalLine(k.styles.Renderer, common.StateSelected)
59-	k.postLabel = k.styles.Label.Render("post:")
60-	k.dateLabel = k.styles.Label.Render("publish_at:")
61-	k.viewsLabel = k.styles.Label.Render("views:")
62-	k.urlLabel = k.styles.Label.Render("url:")
63-	k.expiresAtLabel = k.styles.Label.Render("expires_at:")
64-}
65-
66-// Deleting state.
67-func (k *styledKey) deleting() {
68-	k.gutter = common.VerticalLine(k.styles.Renderer, common.StateDeleting)
69-	k.postLabel = k.styles.Delete.Render("post:")
70-	k.dateLabel = k.styles.Delete.Render("publish_at:")
71-	k.urlLabel = k.styles.Delete.Render("url:")
72-	k.viewsLabel = k.styles.Delete.Render("views:")
73-	k.title = k.styles.DeleteDim.Render(k.title)
74-	k.expiresAtLabel = k.styles.Delete.Render("expires_at:")
75-}
76-
77-func (k styledKey) render(state postState) string {
78-	switch state {
79-	case postSelected:
80-		k.selected()
81-	case postDeleting:
82-		k.deleting()
83-	}
84-
85-	mainBody := fmt.Sprintf(
86-		"%s %s %s\n%s %s %s\n%s %s %d\n%s %s %s\n",
87-		k.gutter, k.postLabel, k.title,
88-		k.gutter, k.dateLabel, k.dateVal,
89-		k.gutter, k.viewsLabel, k.views,
90-		k.gutter, k.urlLabel, k.url,
91-	)
92-
93-	if k.model.cfg.Space == "pastes" {
94-		mainBody += fmt.Sprintf("%s %s %s\n", k.gutter, k.expiresAtLabel, k.expiresAt)
95-	}
96-
97-	return mainBody + "\n"
98-}
D wish/cms/ui/posts/posts.go
+0, -369
  1@@ -1,369 +0,0 @@
  2-package posts
  3-
  4-import (
  5-	"errors"
  6-	"log/slog"
  7-
  8-	pager "github.com/charmbracelet/bubbles/paginator"
  9-	"github.com/charmbracelet/bubbles/spinner"
 10-	tea "github.com/charmbracelet/bubbletea"
 11-
 12-	"github.com/picosh/pico/db"
 13-	"github.com/picosh/pico/shared/storage"
 14-	"github.com/picosh/pico/wish/cms/config"
 15-	"github.com/picosh/pico/wish/cms/ui/common"
 16-)
 17-
 18-const keysPerPage = 1
 19-
 20-type state int
 21-
 22-const (
 23-	stateLoading state = iota
 24-	stateNormal
 25-	stateDeletingPost
 26-	stateQuitting
 27-)
 28-
 29-type postState int
 30-
 31-const (
 32-	postNormal postState = iota
 33-	postSelected
 34-	postDeleting
 35-)
 36-
 37-type PostLoader struct {
 38-	Posts []*db.Post
 39-}
 40-
 41-type (
 42-	postsLoadedMsg PostLoader
 43-	removePostMsg  int
 44-	errMsg         struct {
 45-		err error
 46-	}
 47-)
 48-
 49-// Model is the Tea state model for this user interface.
 50-type Model struct {
 51-	cfg     *config.ConfigCms
 52-	urls    config.ConfigURL
 53-	dbpool  db.DB
 54-	st      storage.StorageServe
 55-	user    *db.User
 56-	posts   []*db.Post
 57-	styles  common.Styles
 58-	pager   pager.Model
 59-	state   state
 60-	err     error
 61-	index   int // index of selected key in relation to the current page
 62-	Exit    bool
 63-	Quit    bool
 64-	spinner spinner.Model
 65-	logger  *slog.Logger
 66-}
 67-
 68-// getSelectedIndex returns the index of the cursor in relation to the total
 69-// number of items.
 70-func (m *Model) getSelectedIndex() int {
 71-	return m.index + m.pager.Page*m.pager.PerPage
 72-}
 73-
 74-// UpdatePaging runs an update against the underlying pagination model as well
 75-// as performing some related tasks on this model.
 76-func (m *Model) UpdatePaging(msg tea.Msg) {
 77-	// Handle paging
 78-	m.pager.SetTotalPages(len(m.posts))
 79-	m.pager, _ = m.pager.Update(msg)
 80-
 81-	// If selected item is out of bounds, put it in bounds
 82-	numItems := m.pager.ItemsOnPage(len(m.posts))
 83-	m.index = min(m.index, numItems-1)
 84-}
 85-
 86-// NewModel creates a new model with defaults.
 87-func NewModel(styles common.Styles, cfg *config.ConfigCms, urls config.ConfigURL, dbpool db.DB, user *db.User, stor storage.StorageServe, perPage int) Model {
 88-	logger := cfg.Logger
 89-
 90-	p := pager.New()
 91-	p.PerPage = keysPerPage
 92-	p.Type = pager.Dots
 93-	p.InactiveDot = styles.InactivePagination.Render("•")
 94-
 95-	if perPage > 0 {
 96-		p.PerPage = perPage
 97-	}
 98-
 99-	return Model{
100-		cfg:     cfg,
101-		dbpool:  dbpool,
102-		st:      stor,
103-		user:    user,
104-		styles:  styles,
105-		pager:   p,
106-		state:   stateLoading,
107-		err:     nil,
108-		posts:   []*db.Post{},
109-		index:   0,
110-		spinner: common.NewSpinner(styles),
111-		Exit:    false,
112-		Quit:    false,
113-		logger:  logger,
114-		urls:    urls,
115-	}
116-}
117-
118-// Init is the Tea initialization function.
119-func (m Model) Init() tea.Cmd {
120-	return tea.Batch(
121-		m.spinner.Tick,
122-	)
123-}
124-
125-// Update is the tea update function which handles incoming messages.
126-func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
127-	switch msg := msg.(type) {
128-	case tea.KeyMsg:
129-		switch msg.String() {
130-		case "ctrl+c", "q", "esc":
131-			m.Exit = true
132-			return m, nil
133-
134-		// Select individual items
135-		case "up", "k":
136-			// Move up
137-			m.index--
138-			if m.index < 0 && m.pager.Page > 0 {
139-				m.index = m.pager.PerPage - 1
140-				m.pager.PrevPage()
141-			}
142-			m.index = max(0, m.index)
143-		case "down", "j":
144-			// Move down
145-			itemsOnPage := m.pager.ItemsOnPage(len(m.posts))
146-			m.index++
147-			if m.index > itemsOnPage-1 && m.pager.Page < m.pager.TotalPages-1 {
148-				m.index = 0
149-				m.pager.NextPage()
150-			}
151-			m.index = min(itemsOnPage-1, m.index)
152-
153-		// Delete
154-		case "x":
155-			if len(m.posts) > 0 {
156-				m.state = stateDeletingPost
157-				m.UpdatePaging(msg)
158-			}
159-
160-			return m, nil
161-
162-		// Confirm Delete
163-		case "y":
164-			switch m.state {
165-			case stateDeletingPost:
166-				m.state = stateNormal
167-				return m, removePost(m)
168-			}
169-		}
170-
171-	case errMsg:
172-		m.err = msg.err
173-		return m, nil
174-
175-	case postsLoadedMsg:
176-		m.state = stateNormal
177-		m.index = 0
178-		m.posts = msg.Posts
179-
180-	case removePostMsg:
181-		if m.state == stateQuitting {
182-			return m, tea.Quit
183-		}
184-		i := m.getSelectedIndex()
185-
186-		// Remove key from array
187-		m.posts = append(m.posts[:i], m.posts[i+1:]...)
188-
189-		// Update pagination
190-		m.pager.SetTotalPages(len(m.posts))
191-		m.pager.Page = min(m.pager.Page, m.pager.TotalPages-1)
192-
193-		// Update cursor
194-		m.index = min(m.index, m.pager.ItemsOnPage(len(m.posts)-1))
195-
196-		return m, nil
197-
198-	case spinner.TickMsg:
199-		var cmd tea.Cmd
200-		if m.state < stateNormal {
201-			m.spinner, cmd = m.spinner.Update(msg)
202-		}
203-		return m, cmd
204-	}
205-
206-	m.UpdatePaging(msg)
207-
208-	// If an item is being confirmed for delete, any key (other than the key
209-	// used for confirmation above) cancels the deletion
210-	k, ok := msg.(tea.KeyMsg)
211-	if ok && k.String() != "x" {
212-		m.state = stateNormal
213-	}
214-
215-	return m, nil
216-}
217-
218-// View renders the current UI into a string.
219-func (m Model) View() string {
220-	if m.err != nil {
221-		return m.err.Error()
222-	}
223-
224-	var s string
225-
226-	switch m.state {
227-	case stateLoading:
228-		s = m.spinner.View() + " Loading...\n\n"
229-	case stateQuitting:
230-		s = "Thanks for using lists!\n"
231-	default:
232-		s = "Here are the posts linked to your account.\n\n"
233-
234-		s += postsView(m)
235-		if m.pager.TotalPages > 1 {
236-			s += m.pager.View()
237-		}
238-
239-		// Footer
240-		switch m.state {
241-		case stateDeletingPost:
242-			s += m.promptView("Delete this post?")
243-		default:
244-			s += "\n\n" + helpView(m)
245-		}
246-	}
247-
248-	return s
249-}
250-
251-func postsView(m Model) string {
252-	var (
253-		s          string
254-		state      postState
255-		start, end = m.pager.GetSliceBounds(len(m.posts))
256-		slice      = m.posts[start:end]
257-	)
258-
259-	destructiveState := m.state == stateDeletingPost
260-
261-	if len(m.posts) == 0 {
262-		s += "You don't have any posts yet."
263-		return s
264-	}
265-
266-	// Render key info
267-	for i, post := range slice {
268-		if destructiveState && m.index == i {
269-			state = postDeleting
270-		} else if m.index == i {
271-			state = postSelected
272-		} else {
273-			state = postNormal
274-		}
275-		s += m.newStyledKey(m.styles, post, m.urls).render(state)
276-	}
277-
278-	// If there aren't enough keys to fill the view, fill the missing parts
279-	// with whitespace
280-	if len(slice) < m.pager.PerPage {
281-		for i := len(slice); i < m.pager.PerPage; i++ {
282-			s += "\n\n\n"
283-		}
284-	}
285-
286-	return s
287-}
288-
289-func helpView(m Model) string {
290-	var items []string
291-	if len(m.posts) > 1 {
292-		items = append(items, "j/k, ↑/↓: choose")
293-	}
294-	if m.pager.TotalPages > 1 {
295-		items = append(items, "h/l, ←/→: page")
296-	}
297-	if len(m.posts) > 0 {
298-		items = append(items, "x: delete")
299-	}
300-	items = append(items, "esc: exit")
301-	return common.HelpView(m.styles, items...)
302-}
303-
304-func (m Model) promptView(prompt string) string {
305-	st := m.styles.Delete.Copy().MarginTop(2).MarginRight(1)
306-	return st.Render(prompt) +
307-		m.styles.DeleteDim.Render("(y/N)")
308-}
309-
310-func LoadPosts(m Model) tea.Cmd {
311-	if m.user == nil {
312-		m.logger.Info("user not found!")
313-		err := errors.New("user not found")
314-		return func() tea.Msg {
315-			return errMsg{err}
316-		}
317-	}
318-
319-	return tea.Batch(
320-		m.fetchPosts(m.user.ID),
321-		m.spinner.Tick,
322-	)
323-}
324-
325-func (m Model) fetchPosts(userID string) tea.Cmd {
326-	return func() tea.Msg {
327-		posts, _ := m.dbpool.FindAllPostsForUser(userID, m.cfg.Space)
328-		loader := PostLoader{
329-			Posts: posts,
330-		}
331-		return postsLoadedMsg(loader)
332-	}
333-}
334-
335-func removePost(m Model) tea.Cmd {
336-	return func() tea.Msg {
337-		bucket, err := m.st.UpsertBucket(m.user.ID)
338-		if err != nil {
339-			return errMsg{err}
340-		}
341-
342-		err = m.st.DeleteObject(bucket, m.posts[m.getSelectedIndex()].Filename)
343-		if err != nil {
344-			return errMsg{err}
345-		}
346-
347-		err = m.dbpool.RemovePosts([]string{m.posts[m.getSelectedIndex()].ID})
348-		if err != nil {
349-			return errMsg{err}
350-		}
351-
352-		return removePostMsg(m.index)
353-	}
354-}
355-
356-// Utils
357-
358-func min(a, b int) int {
359-	if a < b {
360-		return a
361-	}
362-	return b
363-}
364-
365-func max(a, b int) int {
366-	if a > b {
367-		return a
368-	}
369-	return b
370-}
M wish/logger.go
+8, -16
 1@@ -16,28 +16,20 @@ func LogMiddleware(logger *slog.Logger) wish.Middleware {
 2 
 3 			logger.Info(
 4 				"connect",
 5-				"user",
 6-				s.User(),
 7-				"ip",
 8-				s.RemoteAddr().String(),
 9-				"pty",
10-				ok,
11-				"term",
12-				pty.Term,
13-				"windowWidth",
14-				pty.Window.Width,
15-				"windowHeight",
16-				pty.Window.Height,
17+				"user", s.User(),
18+				"ip", s.RemoteAddr().String(),
19+				"pty", ok,
20+				"term", pty.Term,
21+				"windowWidth", pty.Window.Width,
22+				"windowHeight", pty.Window.Height,
23 			)
24 
25 			sh(s)
26 
27 			logger.Info(
28 				"disconnect",
29-				"ip",
30-				s.RemoteAddr().String(),
31-				"duration",
32-				time.Since(ct),
33+				"ip", s.RemoteAddr().String(),
34+				"duration", time.Since(ct),
35 			)
36 		}
37 	}
D wish/main.go
+0, -1
1@@ -1 +0,0 @@
2-package wish
M wish/mdw.go
+26, -0
 1@@ -1,10 +1,36 @@
 2 package wish
 3 
 4 import (
 5+	"fmt"
 6+
 7+	"github.com/charmbracelet/lipgloss"
 8 	"github.com/charmbracelet/ssh"
 9 	"github.com/charmbracelet/wish"
10+	"github.com/picosh/pico/tui/common"
11 )
12 
13+func SessionMessage(sesh ssh.Session, msg string) {
14+	_, _ = sesh.Write([]byte(msg + "\r\n"))
15+}
16+
17+func DeprecatedNotice() wish.Middleware {
18+	return func(next ssh.Handler) ssh.Handler {
19+		return func(sesh ssh.Session) {
20+			renderer := lipgloss.NewRenderer(sesh)
21+			renderer.SetOutput(common.OutputFromSession(sesh))
22+			styles := common.DefaultStyles(renderer)
23+
24+			msg := fmt.Sprintf(
25+				"%s\n\nRun %s to access pico's TUI",
26+				styles.Logo.Render("DEPRECATED"),
27+				styles.Code.Render("ssh pico.sh"),
28+			)
29+			SessionMessage(sesh, styles.RoundedBorder.Render(msg))
30+			next(sesh)
31+		}
32+	}
33+}
34+
35 func PtyMdw(mdw wish.Middleware) wish.Middleware {
36 	return func(next ssh.Handler) ssh.Handler {
37 		return func(sesh ssh.Session) {