start with bubble example
This commit is contained in:
parent
cc78733a45
commit
969a057851
4 changed files with 138 additions and 114 deletions
152
main.go
152
main.go
|
|
@ -1,116 +1,86 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gemini_site/internal/microblog"
|
||||
"gemini_site/internal/pocketbase"
|
||||
|
||||
gemini "github.com/kulak/gemini"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type MainHandler struct {
|
||||
blog microblog.Handler
|
||||
type model struct {
|
||||
cursor int
|
||||
choices []string
|
||||
selected map[int]struct{}
|
||||
}
|
||||
|
||||
func (h MainHandler) ServeGemini(w gemini.ResponseWriter, req *gemini.Request) {
|
||||
slog.Info("gemini request",
|
||||
"path", req.URL.Path,
|
||||
"user", strings.Join(userName(req), " "))
|
||||
func initialModel() model {
|
||||
return model{
|
||||
choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},
|
||||
|
||||
// Check if this is a blog request
|
||||
if strings.HasPrefix(req.URL.Path, "/microblog") {
|
||||
h.blog.HandleBlogRequest(w, req)
|
||||
return
|
||||
// A map which indicates which choices are selected. We're using
|
||||
// the map like a mathematical set. The keys refer to the indexes
|
||||
// of the `choices` slice, above.
|
||||
selected: make(map[int]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
switch req.URL.Path {
|
||||
case "/":
|
||||
gemini.ServeFileName("pages/home.gmi", "text/gemini")(w, req)
|
||||
// err := w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
|
||||
// requireNoError(err)
|
||||
// _, err = w.WriteBody([]byte("Hello, world!"))
|
||||
// requireNoError(err)
|
||||
case "/user":
|
||||
if req.Certificate() == nil {
|
||||
w.WriteStatusMsg(gemini.StatusCertRequired, "Authentication Required")
|
||||
return
|
||||
func (m model) Init() tea.Cmd {
|
||||
return tea.SetWindowTitle("Grocery List")
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "up", "k":
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.cursor < len(m.choices)-1 {
|
||||
m.cursor++
|
||||
}
|
||||
case "enter", " ":
|
||||
_, ok := m.selected[m.cursor]
|
||||
if ok {
|
||||
delete(m.selected, m.cursor)
|
||||
} else {
|
||||
m.selected[m.cursor] = struct{}{}
|
||||
}
|
||||
}
|
||||
w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
|
||||
w.WriteBody([]byte(req.Certificate().Subject.CommonName))
|
||||
case "/die":
|
||||
requireNoError(errors.New("must die"))
|
||||
case "/file":
|
||||
gemini.ServeFileName("cmd/example/hello.gmi", "text/gemini")(w, req)
|
||||
case "/post":
|
||||
if req.URL.Scheme != gemini.SchemaTitan {
|
||||
w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
|
||||
w.WriteBody([]byte("Use titan scheme to upload data"))
|
||||
return
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
s := "What should we buy at the market?\n\n"
|
||||
|
||||
for i, choice := range m.choices {
|
||||
cursor := " "
|
||||
if m.cursor == i {
|
||||
cursor = ">"
|
||||
}
|
||||
payload, err := req.ReadTitanPayload()
|
||||
requireNoError(err)
|
||||
w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
|
||||
w.WriteBody([]byte("Titan Upload Parameters\r\n"))
|
||||
w.WriteBody([]byte(fmt.Sprintf("Upload MIME Type: %s\r\n", req.Titan.Mime)))
|
||||
w.WriteBody([]byte(fmt.Sprintf("Token: %s\r\n", req.Titan.Token)))
|
||||
w.WriteBody([]byte(fmt.Sprintf("Size: %v\r\n", req.Titan.Size)))
|
||||
w.WriteBody([]byte("Payload:\r\n"))
|
||||
w.WriteBody(payload)
|
||||
|
||||
default:
|
||||
w.WriteStatusMsg(gemini.StatusNotFound, req.URL.Path)
|
||||
checked := " "
|
||||
if _, ok := m.selected[i]; ok {
|
||||
checked = "x"
|
||||
}
|
||||
|
||||
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
|
||||
}
|
||||
|
||||
}
|
||||
s += "\nPress q to quit.\n"
|
||||
|
||||
func requireNoError(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func dateToStr(t time.Time) string {
|
||||
return strconv.FormatInt(t.Unix(), 36)
|
||||
}
|
||||
|
||||
func userName(r *gemini.Request) []string {
|
||||
cert := r.Certificate()
|
||||
if cert == nil {
|
||||
return []string{""}
|
||||
}
|
||||
return []string{cert.Subject.CommonName, cert.SerialNumber.String(), dateToStr(cert.NotBefore), dateToStr(cert.NotAfter)}
|
||||
return s
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Set up structured JSON logging
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
slog.SetDefault(logger)
|
||||
|
||||
slog.Info("Starting gemini server")
|
||||
var host, cert, key string
|
||||
flag.StringVar(&host, "host", ":1965", "listen on host and port. Example: hostname:1965")
|
||||
flag.StringVar(&cert, "cert", "server.crt.pem", "certificate file")
|
||||
flag.StringVar(&key, "key", "server.key.pem", "private key associated with certificate file")
|
||||
flag.Parse()
|
||||
|
||||
pbClient := pocketbase.NewPocketBaseClient()
|
||||
handler := MainHandler{
|
||||
blog: microblog.NewHandler(pbClient),
|
||||
}
|
||||
|
||||
err := gemini.ListenAndServe(host, cert, key, gemini.TrapPanic(handler.ServeGemini))
|
||||
if err != nil {
|
||||
slog.Error("server failed to start", "error", err)
|
||||
p := tea.NewProgram(initialModel())
|
||||
if _, err := p.Run(); err != nil {
|
||||
fmt.Printf("Alas, there's been an error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue