start with bubble example

This commit is contained in:
Travis Shears 2025-09-30 09:52:13 +02:00
parent cc78733a45
commit 969a057851
4 changed files with 138 additions and 114 deletions

152
main.go
View file

@ -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)
}
}