diff options
| author | a73x <[email protected]> | 2024-08-25 15:14:42 +0100 |
|---|---|---|
| committer | a73x <[email protected]> | 2024-08-25 15:14:42 +0100 |
| commit | ffdc0a5c34a0b78cec776049efde074147112ae5 (patch) | |
| tree | 8b0a644790c630b0d5ae0512073d2566b87314ca | |
| parent | 92a974b622f26645579e51c946a1266862384002 (diff) | |
feat: ssh access
| -rw-r--r-- | .build.yml | 1 | ||||
| -rw-r--r-- | cmd/home/main.go | 213 | ||||
| -rw-r--r-- | go.mod | 43 | ||||
| -rw-r--r-- | go.sum | 106 | ||||
| -rw-r--r-- | tui/post.go | 126 | ||||
| -rw-r--r-- | tui/ssh.go | 197 | ||||
| -rw-r--r-- | web/web.go | 178 |
7 files changed, 691 insertions, 173 deletions
@@ -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) } } @@ -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 ) @@ -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 +} |
