package main import ( "bytes" "cmp" "embed" "encoding/json" "errors" "fmt" "io/fs" "log" "net/http" "os" "regexp" "slices" "strings" "sync" "time" "github.com/a-h/templ" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/renderer/html" "gopkg.in/yaml.v3" ) //go:embed public blogs var embedded embed.FS func Color(color string) string { var result string if strings.HasPrefix(color, "#") { result = fmt.Sprintf("text-[%v]", color) } else if strings.HasPrefix(color, "--") { result = fmt.Sprintf("text-(%v)", color) } else { result = color } return result } func Assert(ok bool) { if !ok { panic("assertion failure") } } type Theme struct { primary string secondary string accent string extra string text string alt string background string } type FontPack struct { Monospace string Normal string } type Style struct { theme Theme fontPack FontPack } var ( mu sync.RWMutex themes = map[string]Theme{} fontPacks = map[string]FontPack{} ) func SetFontPack(name string, fontPack FontPack) { mu.Lock() defer mu.Unlock() fontPacks[name] = fontPack } func GetFontPack(name string) (FontPack, bool) { mu.RLock() defer mu.RUnlock() fontPack, ok := fontPacks[name] return fontPack, ok } func SetTheme(name string, theme Theme) { mu.Lock() defer mu.Unlock() themes[name] = theme } func GetTheme(name string) (Theme, bool) { mu.RLock() defer mu.RUnlock() theme, ok := themes[name] return theme, ok } func currentStyle(r *http.Request) Style { theme, ok := GetTheme("default") Assert(ok) fontPack, ok := GetFontPack("default") Assert(ok) style := Style{ theme: theme, fontPack: fontPack, } if cookie, err := r.Cookie("theme"); err == nil { if theme, ok := GetTheme(cookie.Value); ok { style.theme = theme } } if cookie, err := r.Cookie("font-pack"); err == nil { if fontPack, ok := GetFontPack(cookie.Value); ok { style.fontPack = fontPack } } return style } func (theme Theme) Vars() map[string]string { return map[string]string{ "primary": theme.primary, "secondary": theme.secondary, "accent": theme.accent, "extra": theme.extra, "text": theme.text, "alt": theme.alt, "background": theme.background, } } func (fontPack FontPack) Vars() map[string]string { return map[string]string{ "font-mono": fontPack.Monospace, "font-normal": fontPack.Normal, } } func (style Style) Vars() map[string]string { vars := style.theme.Vars() for k, v := range style.fontPack.Vars() { vars[k] = v } return vars } func styleToCss(style Style) templ.SafeCSS { var b strings.Builder for k, v := range style.Vars() { fmt.Fprintf(&b, "--%s:%s;", k, v) } return templ.SafeCSS(b.String()) } type Blog struct { Slug string Title string Desc string Date time.Time Body string // raw markdown for rendering HTML string // rendered body } var md = goldmark.New( goldmark.WithExtensions(extension.GFM, extension.Footnote), goldmark.WithRendererOptions(html.WithUnsafe()), ) type frontmatter struct { Title string `yaml:"title"` Desc string `yaml:"description"` Date time.Time `yaml:"date"` } func findBlog(blogs []Blog, slug string) (blog, prev, next *Blog) { i := slices.IndexFunc(blogs, func(b Blog) bool { return b.Slug == slug }) if i < 0 { return nil, nil, nil } if i+1 < len(blogs) { prev = &blogs[i+1] // older } if i > 0 { next = &blogs[i-1] // newer } return &blogs[i], prev, next } var datePrefix = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2})-(.+)\.md$`) func parseBlog(name, raw string) (Blog, error) { raw = strings.ReplaceAll(raw, "\r\n", "\n") body := raw var frontmatter frontmatter if rest, ok := strings.CutPrefix(raw, "---\n"); ok { if head, after, found := strings.Cut(rest, "\n---\n"); found { if err := yaml.Unmarshal([]byte(head), &frontmatter); err != nil { return Blog{}, err } body = after } } var buf bytes.Buffer if err := md.Convert([]byte(body), &buf); err != nil { return Blog{}, err } HTML := buf.String() slug := strings.TrimSuffix(name, ".md") date := frontmatter.Date // frontmatter wins if match := datePrefix.FindStringSubmatch(name); match != nil { slug = match[2] if date.IsZero() { date, _ = time.Parse(time.DateOnly, match[1]) } } if date.IsZero() { return Blog{}, errors.New("no date in frontmatter or filename") } title := frontmatter.Title if title == "" { title = slug } return Blog{ Slug: slug, Title: title, Date: date, Body: body, Desc: frontmatter.Desc, HTML: HTML, }, nil } func LoadBlogs(fsys fs.FS) ([]Blog, error) { entries, err := fs.ReadDir(fsys, ".") if err != nil { return nil, err } var blogs []Blog for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") || strings.HasPrefix(e.Name(), "_") { continue } raw, err := fs.ReadFile(fsys, e.Name()) if err != nil { return nil, err } blog, err := parseBlog(e.Name(), string(raw)) if err != nil { return nil, fmt.Errorf("%s: %w", e.Name(), err) } blogs = append(blogs, blog) } slices.SortFunc(blogs, func(a, b Blog) int { // newest first return cmp.Or(b.Date.Compare(a.Date), strings.Compare(a.Slug, b.Slug)) }) return blogs, nil } func loadBlogs() ([]Blog, error) { blogFS, err := fs.Sub(embedded, "blogs") if err != nil { return nil, err } return LoadBlogs(blogFS) } var ( devMode = os.Getenv("DEV") != "" cachedBlogs []Blog ) func getBlogs() []Blog { if devMode || cachedBlogs == nil { var err error cachedBlogs, err = loadBlogs() if err != nil { log.Println("reloading blogs failed:", err) } } return cachedBlogs } func latestBlog(blogs []Blog) *Blog { if len(blogs) == 0 { return nil } return &blogs[0] } var projects = []Project{ { Slug: "krypton", Title: "Krypton", Category: "Programming Language Compiler", Desc: "", // Icon: IconCpu("text-[#F5A670] self-center"), Icon: IconKrypton(), Tags: []ProjectTag{ {"C", Color("text-[#5C6CC1]")}, {"LLVM", Color("text-[#FFFFFF]")}, // {"Java", Color("#F5A670")}, }, Links: []ProjectLink{ {IconReleases("text-[#FF8019]"), "Releases", "https://"}, {IconDocs("text-[#00ADD7]"), "Documentation", "/projects/krypton"}, }, Link: "", }, { Slug: "cpu-vs-ai", Title: "CPU vs AI", Category: "Video Game", Desc: "A tower defense game where an AI chip on your motherboard " + "generates bugs! Place CPUs to destroy the bugs! " + "Don't let them corrupt your RAM!", Icon: IconCpu("text-[#F16265]"), Tags: []ProjectTag{ {"Zig", Color("text-[#D8C44B]")}, }, Links: []ProjectLink{ {IconGit("text-[#F5A670]"), "Git", "https://git.kyren.codes/kyren/cpu-vs-ai.git"}, {IconGithub("text-(--secondary)"), "GitHub", "https://github.com/the-memory-managers/cpu-vs-ai"}, {IconVideo("text-[#F16265]"), "Showcase", "https://www.youtube.com/watch?v=_clLDGNlef0"}, }, Link: "https://github.com/the-memory-managers/cpu-vs-ai", }, { Slug: "eko", Title: "Eko", Category: "Social Media Platform", Desc: "A terminal-native alternative to Discord for Terminal nerds!", Icon: IconEko("text-[#D27D9B]"), Tags: []ProjectTag{ {"Golang", Color("text-[#00ADD7]")}, {"Sqlite", Color("text-[#BDC3C7]")}, {"Bubbletea", Color("text-[#C7727F]")}, {"Prometheus", Color("text-[#ED532D]")}, {"Grafana", Color("text-[#F9B010]")}, }, Links: []ProjectLink{ {IconGit("text-[#F5A670]"), "Git", "https://git.kyren.codes/kyren/eko.git"}, {IconGithub("text-(--secondary)"), "GitHub", "https://github.com/kyren223/eko"}, }, Link: "https://github.com/kyren223/eko", }, { Slug: "trident", Title: "Trident", Category: "IntelliJ Plugin", Desc: "A blazingly fast way to navigate to your most used files. Minimize keystrokes and maximize productivity!", Icon: IconTrident("text-[#739ebb]"), Tags: []ProjectTag{ // {"Kotlin", Color("text-[#897ccb]")}, {"Kotlin", Color("text-[#a34eeb]")}, {"JetBrains Plugin SDK", Color("text-[#FF8019]")}, }, Links: []ProjectLink{ {IconGit("text-[#F5A670]"), "Git", "https://git.kyren.codes/kyren/trident.git"}, {IconGithub("text-(--secondary)"), "GitHub", "https://github.com/kyren223/trident"}, {IconJetbrains("text-[#FF8019]"), "JetBrains Marketplace", "https://plugins.jetbrains.com/plugin/23818-trident"}, }, Link: "https://github.com/kyren223/trident", }, { Slug: "dotfiles", Title: "Dotfiles", Category: "Configuration", Desc: "My Linux configuration, includes Neovim, tmux and other programs.", Icon: IconDotfiles("text-[#5874FF]"), Tags: []ProjectTag{ {"Neovim", Color("text-[#46d46c]")}, {"Scripts", Color("text-[#D8C44B]")}, {"Git", Color("text-[#F5A670]")}, }, Links: []ProjectLink{ {IconGit("text-[#F5A670]"), "Git", "https://git.kyren.codes/kyren/dotfiles.git/"}, {IconGithub("text-(--secondary)"), "GitHub", "https://github.com/kyren223/dotfiles"}, }, Link: "https://git.kyren.codes/kyren/dotfiles.git/", }, } var flagship FlagshipInfo = FlagshipInfo{ Version: "Sprout.0 2026-08-13", Downloads: []Download{ // {"linux-amd64", "https://foobar", "23 MB", true}, // {"linux-arm64", "https://foobar", "52 MB"}, // {"macos-arm64", "https://fooshit", "47 MB"}, // {"windows-amd64", "https://fooshit", "65.7 MB", true}, {"linux-amd64", "https://foobar", "? MB", false}, {"windows-amd64", "https://fooshit", "? MB", false}, }, // Stats: []Stat{{"lines of code", "18.7k"}}, Snippet: ` io :: import "core/io"; main :: proc() { io.printf("Hello, %s", "world"); } `, } func styleHandler(cookie string, lookup func(string) (map[string]string, bool)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") vars, ok := lookup(name) if !ok { http.NotFound(w, r) return } http.SetCookie(w, &http.Cookie{ Name: cookie, Value: name, Path: "/", MaxAge: 31536000, SameSite: http.SameSiteLaxMode, }) w.Header().Set("Cache-Control", "no-store") json.NewEncoder(w).Encode(vars) } } func notFoundHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) NotFoundPage(currentStyle(r), r.URL.Path).Render(r.Context(), w) } type MyUptime struct { mu sync.RWMutex Uptime string LastUpdate time.Time } var myUptime MyUptime // GetMyUptime returns the current status string (handles offline state) func GetMyUptime() string { myUptime.mu.RLock() defer myUptime.mu.RUnlock() // If no ping received in 3 minutes, treat as offline if time.Since(myUptime.LastUpdate) > 1*time.Minute { return "Asleep" } uptime := myUptime.Uptime uptime, _ = strings.CutPrefix(uptime, "up ") uptime = "Awake for " + uptime return uptime } func handleUptimePing(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "uptime": GetMyUptime(), }) } else if r.Method == http.MethodPost { if devMode == false { secretToken := os.Getenv("MY_UPTIME_TOKEN") if r.Header.Get("Authorization") != "Bearer "+secretToken { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } } var req struct { Uptime string `json:"uptime"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } myUptime.mu.Lock() myUptime.Uptime = req.Uptime myUptime.LastUpdate = time.Now() myUptime.mu.Unlock() w.WriteHeader(http.StatusOK) } else { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } } func main() { carbonightCat := Theme{ // primary: "#94e2d5", // secondary: "#b4befe", primary: "#54D7A9", secondary: "#BB91F0", accent: "#74c7ec", extra: "#a6e3a1", text: "#cdd6f4", alt: "#585b70", background: "#1e1e2e", } darkness := Theme{ primary: "#000000", secondary: "#000000", accent: "#000000", extra: "#000000", text: "#000000", alt: "#000000", background: "#000000", } dark := Theme{ primary: "#54d7a9", secondary: "#9b7eca", accent: "#7ed4fb", extra: "#8fcc75", text: "#ffffff", alt: "#999999", background: "#000000", } light := Theme{ primary: "#009399", secondary: "#440b8e", accent: "#177282", extra: "#40a02b", text: "#000000", alt: "#4d4d4d", background: "#ffffff", } banana := Theme{ primary: "#ffdd44", secondary: "#9b7eca", accent: "#7ed4fb", extra: "#000000", text: "#ffffff", alt: "#999999", background: "#000000", } jetbrains := FontPack{ Monospace: "JetBrains Mono", Normal: `system-ui, sans-serif`, } minecraft := FontPack{ Monospace: `"Monocraft", "JetBrains Mono", monospace`, Normal: `"Monocraft", system-ui, sans-serif`, } SetTheme("carbonight-cat", carbonightCat) SetTheme("banana", banana) SetTheme("black", dark) SetTheme("dark", darkness) SetTheme("light", light) SetTheme("white", light) SetTheme("sun", light) SetTheme("sunlight", light) SetTheme("flashbang", light) SetTheme("default", carbonightCat) SetFontPack("jetbrains", jetbrains) SetFontPack("minecraft", minecraft) SetFontPack("default", jetbrains) mux := http.NewServeMux() mux.Handle("GET /public/", http.FileServerFS(embedded)) mux.HandleFunc("GET /api/my-uptime", handleUptimePing) mux.HandleFunc("POST /api/my-uptime", handleUptimePing) mux.HandleFunc("/", notFoundHandler) // matches everything mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { HomePage(currentStyle(r), latestBlog(getBlogs())).Render(r.Context(), w) }) mux.HandleFunc("GET /info", func(w http.ResponseWriter, r *http.Request) { InfoPage(currentStyle(r)).Render(r.Context(), w) }) mux.HandleFunc("GET /projects", func(w http.ResponseWriter, r *http.Request) { ProjectsPage(currentStyle(r), projects, 2).Render(r.Context(), w) }) mux.HandleFunc("GET /blogs", func(w http.ResponseWriter, r *http.Request) { BlogsPage(currentStyle(r), getBlogs()).Render(r.Context(), w) }) mux.HandleFunc("GET /blogs/{slug}", func(w http.ResponseWriter, r *http.Request) { blog, prev, next := findBlog(getBlogs(), r.PathValue("slug")) if blog == nil { notFoundHandler(w, r) return } BlogPage(currentStyle(r), *blog, prev, next).Render(r.Context(), w) }) mux.HandleFunc("GET /theme/{name}", styleHandler("theme", func(n string) (map[string]string, bool) { theme, ok := GetTheme(n) return theme.Vars(), ok })) mux.HandleFunc("GET /font-pack/{name}", styleHandler("font-pack", func(n string) (map[string]string, bool) { fontPack, ok := GetFontPack(n) return fontPack.Vars(), ok })) fmt.Println("Server running at http://localhost:8080") if err := http.ListenAndServe(":8080", mux); err != nil { panic(err) } }