summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.build.yml1
-rw-r--r--cmd/home/main.go213
-rw-r--r--go.mod43
-rw-r--r--go.sum106
-rw-r--r--tui/post.go126
-rw-r--r--tui/ssh.go197
-rw-r--r--web/web.go178
7 files changed, 691 insertions, 173 deletions
diff --git a/.build.yml b/.build.yml
index ba96bcd..cb3ddca 100644
--- a/.build.yml
+++ b/.build.yml
@@ -14,6 +14,5 @@ tasks:
sudo usermod -aG docker $(whoami)
cat ~/.docker_token | docker login -u a73xsh --password-stdin
- setup: |
- ls
cd home
make public
diff --git a/cmd/home/main.go b/cmd/home/main.go
index 5a4573f..9585266 100644
--- a/cmd/home/main.go
+++ b/cmd/home/main.go
@@ -1,181 +1,54 @@
package main
import (
- "bytes"
- "fmt"
- "io"
- "io/fs"
- "log"
- "net/http"
- "path/filepath"
- "strings"
- "text/template"
+ "context"
+ "errors"
+ "os"
+ "os/signal"
+ "syscall"
"time"
- "git.sr.ht/~a73x/home"
- "github.com/yuin/goldmark"
- "github.com/yuin/goldmark/parser"
- "go.abhg.dev/goldmark/frontmatter"
- "go.uber.org/zap"
-)
-
-type CommonData struct {
- Posts []PostData
-}
-
-type PostData struct {
- Title string
- Path string
-}
-
-func GeneratePosts(mux *http.ServeMux) ([]PostData, error) {
- converter := goldmark.New(
- goldmark.WithExtensions(
- &frontmatter.Extender{},
- ))
-
- t, err := template.ParseFS(home.Content, "templates/layouts/*.html")
- if err != nil {
- return nil, fmt.Errorf("failed to parse layouts: %v", err)
- }
-
- final := []PostData{}
- posts, err := fs.Glob(home.Content, "posts/*.md")
- if err != nil {
- return nil, fmt.Errorf("failed to glob posts: %v", err)
- }
-
- for _, post := range posts {
- postName := filepath.Base(post)
- postName = strings.Split(postName, ".")[0] + ".html"
-
- // parse markdown
- input, err := fs.ReadFile(home.Content, post)
- if err != nil {
- return nil, fmt.Errorf("failed to read post: %v", err)
- }
-
- var b bytes.Buffer
- ctx := parser.NewContext()
- if err := converter.Convert(input, &b, parser.WithContext(ctx)); err != nil {
- return nil, err
- }
-
- d := frontmatter.Get(ctx)
- var meta map[string]string
- if err := d.Decode(&meta); err != nil {
- return nil, err
- }
-
- foo, err := t.ParseFS(home.Content, "templates/post.html")
- if err != nil {
- return nil, fmt.Errorf("failed to parse post template: %v", err)
- }
-
- content, err := io.ReadAll(&b)
- if err != nil {
- return nil, fmt.Errorf("failed to read post template: %v", err)
- }
-
- type IPostData struct {
- Title string
- Post string
- }
- mux.HandleFunc("/posts/"+postName, func(w http.ResponseWriter, r *http.Request) {
- if err := foo.ExecuteTemplate(w, "post.html", IPostData{
- Title: meta["title"],
- Post: string(content),
- }); err != nil {
- fmt.Println(err)
- }
- })
-
- final = append(final, PostData{
- Title: meta["title"],
- Path: "posts/" + postName,
- })
- }
+ "git.sr.ht/~a73x/home/tui"
+ "git.sr.ht/~a73x/home/web"
- return final, nil
-}
-
-func loadTemplates(mux *http.ServeMux, data any) error {
- tmplFiles, err := fs.ReadDir(home.Content, "templates")
- if err != nil {
- return fmt.Errorf("failed to parse template layouts")
- }
-
- for _, tmpl := range tmplFiles {
- if tmpl.IsDir() {
- continue
- }
+ "github.com/charmbracelet/log"
+ "github.com/charmbracelet/ssh"
+)
- pt, err := template.ParseFS(home.Content, "templates/"+tmpl.Name(), "templates/layouts/*.html")
- if err != nil {
- return fmt.Errorf("failed to parse template "+tmpl.Name(), err)
- }
+const (
+ port = "2222"
+)
- if tmpl.Name() == "index.html" {
- mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- pt.ExecuteTemplate(w, tmpl.Name(), data)
- })
- } else if tmpl.Name() == "page.html" {
- continue
- } else {
- mux.HandleFunc("/"+tmpl.Name(), func(w http.ResponseWriter, r *http.Request) {
- pt, err := template.ParseFS(home.Content, "templates/"+tmpl.Name(), "templates/layouts/*.html")
- if err != nil {
- fmt.Printf("failed to parse template "+tmpl.Name(), err)
- }
- pt.ExecuteTemplate(w, tmpl.Name(), data)
- })
- }
- }
- return nil
-}
func main() {
- logger, _ := zap.NewProduction()
- loggingMiddleware := func(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- start := time.Now()
- next.ServeHTTP(w, r)
- logger.Info("request received",
- zap.String("url", r.URL.Path),
- zap.String("method", r.Method),
- zap.Duration("duration", time.Since(start)),
- zap.String("user-agent", r.UserAgent()),
- )
- })
- }
-
- mux := http.NewServeMux()
- postData, err := GeneratePosts(mux)
- data := CommonData{
- Posts: postData,
- }
-
- if err != nil {
- log.Fatal("failed to generate posts", err)
- }
-
- if err := loadTemplates(mux, data); err != nil {
- log.Fatal("failed to parse templates", err)
- }
-
- staticFs, err := fs.Sub(home.Content, "public/static")
- if err != nil {
- log.Fatal(err)
- }
-
- mux.Handle("GET /static", http.FileServer(http.FS(staticFs)))
-
- server := http.Server{
- Addr: ":8080",
- Handler: loggingMiddleware(mux),
- }
-
- err = server.ListenAndServe()
- if err != nil {
- log.Fatal(err)
+ host := os.Getenv("DOMAIN")
+ if host == "" {
+ host = "localhost"
+ }
+ server, err := web.New()
+
+ s, err := tui.New(host, port)
+
+ done := make(chan os.Signal, 1)
+ signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+ log.Info("Starting SSH server", "host", host, "port", port)
+ go func() {
+ if err := server.ListenAndServe(); err != nil {
+ log.Error("fail running web server", "error", err)
+ }
+ done <- nil
+ }()
+ go func() {
+ if err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
+ log.Error("Could not start server", "error", err)
+ done <- nil
+ }
+ }()
+
+ <-done
+ log.Info("Stopping SSH server")
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer func() { cancel() }()
+ if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
+ log.Error("Could not stop server", "error", err)
}
}
diff --git a/go.mod b/go.mod
index c283353..d1731cd 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,13 @@ module git.sr.ht/~a73x/home
go 1.22.5
require (
+ github.com/charmbracelet/bubbles v0.19.0
+ github.com/charmbracelet/bubbletea v0.27.0
+ github.com/charmbracelet/glamour v0.8.0
+ github.com/charmbracelet/lipgloss v0.12.1
+ github.com/charmbracelet/log v0.4.0
+ github.com/charmbracelet/ssh v0.0.0-20240725163421-eb71b85b27aa
+ github.com/charmbracelet/wish v1.4.2
github.com/yuin/goldmark v1.7.4
go.abhg.dev/goldmark/frontmatter v0.2.0
go.uber.org/zap v1.27.0
@@ -10,6 +17,42 @@ require (
require (
github.com/BurntSushi/toml v1.2.1 // indirect
+ github.com/alecthomas/chroma/v2 v2.14.0 // indirect
+ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/aymerick/douceur v0.2.0 // indirect
+ github.com/charmbracelet/keygen v0.5.1 // indirect
+ github.com/charmbracelet/x/ansi v0.1.4 // indirect
+ github.com/charmbracelet/x/conpty v0.1.0 // indirect
+ github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 // indirect
+ github.com/charmbracelet/x/input v0.1.3 // indirect
+ github.com/charmbracelet/x/term v0.1.1 // indirect
+ github.com/charmbracelet/x/termios v0.1.0 // indirect
+ github.com/charmbracelet/x/windows v0.1.2 // indirect
+ github.com/creack/pty v1.1.21 // indirect
+ github.com/dlclark/regexp2 v1.11.0 // indirect
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
+ github.com/go-logfmt/logfmt v0.6.0 // indirect
+ github.com/gorilla/css v1.0.1 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-localereader v0.0.1 // indirect
+ github.com/mattn/go-runewidth v0.0.16 // indirect
+ github.com/microcosm-cc/bluemonday v1.0.27 // indirect
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
+ github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/muesli/reflow v0.3.0 // indirect
+ github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+ github.com/yuin/goldmark-emoji v1.0.3 // indirect
go.uber.org/multierr v1.10.0 // indirect
+ golang.org/x/crypto v0.26.0 // indirect
+ golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
+ golang.org/x/net v0.27.0 // indirect
+ golang.org/x/sync v0.8.0 // indirect
+ golang.org/x/sys v0.24.0 // indirect
+ golang.org/x/term v0.23.0 // indirect
+ golang.org/x/text v0.17.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index 9d9984a..441b2d6 100644
--- a/go.sum
+++ b/go.sum
@@ -1,13 +1,99 @@
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
+github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
+github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
+github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
+github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
+github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
+github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
+github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
+github.com/charmbracelet/bubbles v0.19.0 h1:gKZkKXPP6GlDk6EcfujDK19PCQqRjaJZQ7QRERx1UF0=
+github.com/charmbracelet/bubbles v0.19.0/go.mod h1:WILteEqZ+krG5c3ntGEMeG99nCupcuIk7V0/zOP0tOA=
+github.com/charmbracelet/bubbletea v0.27.0 h1:Mznj+vvYuYagD9Pn2mY7fuelGvP0HAXtZYGgRBCbHvU=
+github.com/charmbracelet/bubbletea v0.27.0/go.mod h1:5MdP9XH6MbQkgGhnlxUqCNmBXf9I74KRQ8HIidRxV1Y=
+github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs=
+github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw=
+github.com/charmbracelet/keygen v0.5.1 h1:zBkkYPtmKDVTw+cwUyY6ZwGDhRxXkEp0Oxs9sqMLqxI=
+github.com/charmbracelet/keygen v0.5.1/go.mod h1:zznJVmK/GWB6dAtjluqn2qsttiCBhA5MZSiwb80fcHw=
+github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs=
+github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8=
+github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
+github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
+github.com/charmbracelet/ssh v0.0.0-20240725163421-eb71b85b27aa h1:6rePgmsJguB6Z7Y55stsEVDlWFJoUpQvOX4mdnBjgx4=
+github.com/charmbracelet/ssh v0.0.0-20240725163421-eb71b85b27aa/go.mod h1:LmMZag2g7ILMmWtDmU7dIlctUopwmb73KpPzj0ip1uk=
+github.com/charmbracelet/wish v1.4.2 h1:H2BKXewugK9Al75wST9h0hpQ95h981RZ5rtimDpygPQ=
+github.com/charmbracelet/wish v1.4.2/go.mod h1:3Bzq7qMU2LTvdaM61KrCnhrzGP92D/Ru7CasrpyZmzY=
+github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM=
+github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
+github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
+github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
+github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
+github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
+github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
+github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
+github.com/charmbracelet/x/input v0.1.3 h1:oy4TMhyGQsYs/WWJwu1ELUMFnjiUAXwtDf048fHbCkg=
+github.com/charmbracelet/x/input v0.1.3/go.mod h1:1gaCOyw1KI9e2j00j/BBZ4ErzRZqa05w0Ghn83yIhKU=
+github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
+github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw=
+github.com/charmbracelet/x/termios v0.1.0 h1:y4rjAHeFksBAfGbkRDmVinMg7x7DELIGAFbdNvxg97k=
+github.com/charmbracelet/x/termios v0.1.0/go.mod h1:H/EVv/KRnrYjz+fCYa9bsKdqF3S8ouDK0AZEbG7r+/U=
+github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg=
+github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
+github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
+github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
+github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
+github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
+github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
+github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
+github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
+github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
+github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
-github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
+github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4=
+github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw=
go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px76YjkOzhB4YlU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -16,6 +102,22 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
+golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
+golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
+golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
+golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
+golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
+golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
+golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/tui/post.go b/tui/post.go
new file mode 100644
index 0000000..7762b56
--- /dev/null
+++ b/tui/post.go
@@ -0,0 +1,126 @@
+package tui
+
+// An example program demonstrating the pager component from the Bubbles
+// component library.
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/charmbracelet/bubbles/viewport"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+)
+
+// You generally won't need this unless you're processing stuff with
+// complicated ANSI escape sequences. Turn it on if you notice flickering.
+//
+// Also keep in mind that high performance rendering only works for programs
+// that use the full size of the terminal. We're enabling that below with
+// tea.EnterAltScreen().
+const useHighPerformanceRenderer = false
+
+var (
+ titleStyle = func() lipgloss.Style {
+ b := lipgloss.RoundedBorder()
+ b.Right = "├"
+ return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
+ }()
+
+ infoStyle = func() lipgloss.Style {
+ b := lipgloss.RoundedBorder()
+ b.Left = "┤"
+ return titleStyle.BorderStyle(b)
+ }()
+)
+
+type PostModel struct {
+ content string
+ ready bool
+ viewport viewport.Model
+}
+
+func (m PostModel) Init() tea.Cmd {
+ return nil
+}
+
+func (m PostModel) Update(msg tea.Msg) (PostModel, tea.Cmd) {
+ var (
+ cmd tea.Cmd
+ cmds []tea.Cmd
+ )
+
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" {
+ return m, tea.Quit
+ }
+
+ case tea.WindowSizeMsg:
+ headerHeight := lipgloss.Height(m.headerView())
+ footerHeight := lipgloss.Height(m.footerView())
+ verticalMarginHeight := headerHeight + footerHeight
+
+ if !m.ready {
+ // Since this program is using the full size of the viewport we
+ // need to wait until we've received the window dimensions before
+ // we can initialize the viewport. The initial dimensions come in
+ // quickly, though asynchronously, which is why we wait for them
+ // here.
+ m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight)
+ m.viewport.YPosition = headerHeight
+ m.viewport.HighPerformanceRendering = useHighPerformanceRenderer
+ m.viewport.SetContent(m.content)
+ m.ready = true
+
+ // This is only necessary for high performance rendering, which in
+ // most cases you won't need.
+ //
+ // Render the viewport one line below the header.
+ m.viewport.YPosition = headerHeight + 1
+ } else {
+ m.viewport.Width = msg.Width
+ m.viewport.Height = msg.Height - verticalMarginHeight
+ }
+
+ if useHighPerformanceRenderer {
+ // Render (or re-render) the whole viewport. Necessary both to
+ // initialize the viewport and when the window is resized.
+ //
+ // This is needed for high-performance rendering only.
+ cmds = append(cmds, viewport.Sync(m.viewport))
+ }
+ }
+
+ // Handle keyboard and mouse events in the viewport
+ m.viewport, cmd = m.viewport.Update(msg)
+ cmds = append(cmds, cmd)
+
+ return m, tea.Batch(cmds...)
+}
+
+func (m PostModel) View() string {
+ if !m.ready {
+ return "\n Initializing..."
+ }
+ return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView())
+}
+
+func (m PostModel) headerView() string {
+ title := titleStyle.Render("Mr. Pager")
+ line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title)))
+ return lipgloss.JoinHorizontal(lipgloss.Center, title, line)
+}
+
+func (m PostModel) footerView() string {
+ info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100))
+ line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info)))
+ return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
+}
+
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
diff --git a/tui/ssh.go b/tui/ssh.go
new file mode 100644
index 0000000..2309b3f
--- /dev/null
+++ b/tui/ssh.go
@@ -0,0 +1,197 @@
+package tui
+
+// An example Bubble Tea server. This will put an ssh session into alt screen
+// and continually print up to date terminal information.
+
+import (
+ "fmt"
+ "io/fs"
+ "net"
+
+ "git.sr.ht/~a73x/home"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/glamour"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/log"
+ "github.com/charmbracelet/ssh"
+ "github.com/charmbracelet/wish"
+ "github.com/charmbracelet/wish/activeterm"
+ "github.com/charmbracelet/wish/bubbletea"
+ "github.com/charmbracelet/wish/logging"
+)
+
+func New(host, port string) (*ssh.Server, error) {
+ s, err := wish.NewServer(
+ wish.WithAddress(net.JoinHostPort(host, port)),
+ wish.WithHostKeyPath(".ssh/id_ed25519"),
+ wish.WithMiddleware(
+ bubbletea.Middleware(teaHandler),
+ activeterm.Middleware(), // Bubble Tea apps usually require a PTY.
+ logging.Middleware(),
+ ),
+ )
+ if err != nil {
+ log.Error("Could not start server", "error", err)
+ }
+
+ return s, err
+}
+
+// You can wire any Bubble Tea model up to the middleware with a function that
+// handles the incoming ssh.Session. Here we just grab the terminal info and
+// pass it to the new model. You can also return tea.ProgramOptions (such as
+// tea.WithAltScreen) on a session by session basis.
+func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
+ // This should never fail, as we are using the activeterm middleware.
+ pty, _, _ := s.Pty()
+
+ // When running a Bubble Tea app over SSH, you shouldn't use the default
+ // lipgloss.NewStyle function.
+ // That function will use the color profile from the os.Stdin, which is the
+ // server, not the client.
+ // We provide a MakeRenderer function in the bubbletea middleware package,
+ // so you can easily get the correct renderer for the current session, and
+ // use it to create the styles.
+ // The recommended way to use these styles is to then pass them down to
+ // your Bubble Tea model.
+ renderer := bubbletea.MakeRenderer(s)
+ txtStyle := renderer.NewStyle().Foreground(lipgloss.Color("10"))
+ quitStyle := renderer.NewStyle().Foreground(lipgloss.Color("8"))
+
+ bg := "light"
+ if renderer.HasDarkBackground() {
+ bg = "dark"
+ }
+
+ posts, err := home.Content.ReadDir("posts")
+ if err != nil {
+ // fk knows
+ }
+
+ postFiles := make([]string, 0, len(posts))
+ for _, post := range posts {
+ postFiles = append(postFiles, "posts/"+post.Name())
+ }
+
+ m := model{
+ term: pty.Term,
+ profile: renderer.ColorProfile().Name(),
+ width: pty.Window.Width,
+ height: pty.Window.Height,
+ bg: bg,
+ txtStyle: txtStyle,
+ quitStyle: quitStyle,
+ posts: postFiles,
+ }
+ return m, []tea.ProgramOption{tea.WithAltScreen()}
+}
+
+// Just a generic tea.Model to demo terminal information of ssh.
+type model struct {
+ term string
+ profile string
+ width int
+ height int
+ bg string
+ txtStyle lipgloss.Style
+ quitStyle lipgloss.Style
+ posts []string
+ cursor int
+ viewer *PostModel
+ err error
+}
+
+func (m model) Init() tea.Cmd {
+ return nil
+}
+
+func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
+
+ if m.viewer != nil {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch msg.String() {
+ case "q", "ctrl+c":
+ m.viewer = nil
+ return m, nil
+ case "esc":
+ return m, nil
+ }
+ }
+ viewer, cmd := m.viewer.Update(msg)
+ m.viewer = &viewer
+ return m, cmd
+ }
+
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ m.height = msg.Height
+ m.width = msg.Width
+ case tea.KeyMsg:
+ switch msg.String() {
+ case "q", "ctrl+c":
+ return m, tea.Quit
+ case "up", "k":
+ if m.cursor > 0 {
+ m.cursor--
+ }
+ case "down", "j":
+ if m.cursor < len(m.posts)-1 {
+ m.cursor++
+ }
+ case "enter":
+ content, err := fs.ReadFile(home.Content, m.posts[m.cursor])
+ if err != nil {
+ m.err = err
+ return m, nil
+ }
+
+ out, err := glamour.Render(string(content), "dark")
+ if err != nil {
+ m.err = err
+ return m, nil
+ }
+
+ m.viewer = &PostModel{
+ content: out,
+ }
+ fakeWindowMsg := tea.WindowSizeMsg{
+ Width: m.width,
+ Height: m.height - 1,
+ }
+
+ viewer, cmd := m.viewer.Update(fakeWindowMsg)
+ m.viewer = &viewer
+ return m, cmd
+ }
+ }
+
+ return m, tea.Batch(cmds...)
+}
+
+func (m model) View() string {
+ if m.err != nil {
+ return m.err.Error()
+ }
+
+ if m.viewer != nil {
+ return m.viewer.View() + "\n arrow keys to scroll and q to go back"
+ }
+
+ s := "a73x\n\n"
+ for i, choice := range m.posts {
+ // Is the cursor pointing at this choice?
+ cursor := " " // no cursor
+ if m.cursor == i {
+ cursor = ">" // cursor!
+ }
+
+ // Render the row
+ s += fmt.Sprintf("%s %s\n", cursor, choice)
+ }
+ s += "\narrow keys to select post and enter to select"
+ s += "\npress q to quit"
+
+ return s
+}
diff --git a/web/web.go b/web/web.go
new file mode 100644
index 0000000..aa78f51
--- /dev/null
+++ b/web/web.go
@@ -0,0 +1,178 @@
+package web
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "io/fs"
+ "net/http"
+ "path/filepath"
+ "strings"
+ "text/template"
+ "time"
+
+ "git.sr.ht/~a73x/home"
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/parser"
+ "go.abhg.dev/goldmark/frontmatter"
+ "go.uber.org/zap"
+)
+
+type CommonData struct {
+ Posts []PostData
+}
+
+type PostData struct {
+ Title string
+ Path string
+}
+
+func GeneratePosts(mux *http.ServeMux) ([]PostData, error) {
+ converter := goldmark.New(
+ goldmark.WithExtensions(
+ &frontmatter.Extender{},
+ ))
+
+ t, err := template.ParseFS(home.Content, "templates/layouts/*.html")
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse layouts: %v", err)
+ }
+
+ final := []PostData{}
+ posts, err := fs.Glob(home.Content, "posts/*.md")
+ if err != nil {
+ return nil, fmt.Errorf("failed to glob posts: %v", err)
+ }
+
+ for _, post := range posts {
+ postName := filepath.Base(post)
+ postName = strings.Split(postName, ".")[0] + ".html"
+
+ // parse markdown
+ input, err := fs.ReadFile(home.Content, post)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read post: %v", err)
+ }
+
+ var b bytes.Buffer
+ ctx := parser.NewContext()
+ if err := converter.Convert(input, &b, parser.WithContext(ctx)); err != nil {
+ return nil, err
+ }
+
+ d := frontmatter.Get(ctx)
+ var meta map[string]string
+ if err := d.Decode(&meta); err != nil {
+ return nil, err
+ }
+
+ foo, err := t.ParseFS(home.Content, "templates/post.html")
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse post template: %v", err)
+ }
+
+ content, err := io.ReadAll(&b)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read post template: %v", err)
+ }
+
+ type IPostData struct {
+ Title string
+ Post string
+ }
+
+ mux.HandleFunc("/posts/"+postName, func(w http.ResponseWriter, r *http.Request) {
+ if err := foo.ExecuteTemplate(w, "post.html", IPostData{
+ Title: meta["title"],
+ Post: string(content),
+ }); err != nil {
+ fmt.Println(err)
+ }
+ })
+
+ final = append(final, PostData{
+ Title: meta["title"],
+ Path: "posts/" + postName,
+ })
+ }
+
+ return final, nil
+}
+
+func loadTemplates(mux *http.ServeMux, data any) error {
+ tmplFiles, err := fs.ReadDir(home.Content, "templates")
+ if err != nil {
+ return fmt.Errorf("failed to parse template layouts")
+ }
+
+ for _, tmpl := range tmplFiles {
+ if tmpl.IsDir() {
+ continue
+ }
+
+ pt, err := template.ParseFS(home.Content, "templates/"+tmpl.Name(), "templates/layouts/*.html")
+ if err != nil {
+ return fmt.Errorf("failed to parse template "+tmpl.Name(), err)
+ }
+
+ if tmpl.Name() == "index.html" {
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ pt.ExecuteTemplate(w, tmpl.Name(), data)
+ })
+ } else if tmpl.Name() == "page.html" {
+ continue
+ } else {
+ mux.HandleFunc("/"+tmpl.Name(), func(w http.ResponseWriter, r *http.Request) {
+ pt, err := template.ParseFS(home.Content, "templates/"+tmpl.Name(), "templates/layouts/*.html")
+ if err != nil {
+ fmt.Printf("failed to parse template "+tmpl.Name(), err)
+ }
+ pt.ExecuteTemplate(w, tmpl.Name(), data)
+ })
+ }
+ }
+ return nil
+}
+func New() (*http.Server, error) {
+ logger, _ := zap.NewProduction()
+ loggingMiddleware := func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ start := time.Now()
+ next.ServeHTTP(w, r)
+ logger.Info("request received",
+ zap.String("url", r.URL.Path),
+ zap.String("method", r.Method),
+ zap.Duration("duration", time.Since(start)),
+ zap.String("user-agent", r.UserAgent()),
+ )
+ })
+ }
+
+ mux := http.NewServeMux()
+ postData, err := GeneratePosts(mux)
+ if err != nil {
+ return nil, fmt.Errorf("failed to generate posts: %v", err)
+ }
+
+ data := CommonData{
+ Posts: postData,
+ }
+
+ if err := loadTemplates(mux, data); err != nil {
+ return nil, fmt.Errorf("failed to parse templates: %v", err)
+ }
+
+ staticFs, err := fs.Sub(home.Content, "public/static")
+ if err != nil {
+ return nil, fmt.Errorf("failed to setup static handler: %v", err)
+ }
+
+ mux.Handle("GET /static", http.FileServer(http.FS(staticFs)))
+
+ server := http.Server{
+ Addr: ":8080",
+ Handler: loggingMiddleware(mux),
+ }
+
+ return &server, nil
+}