package main
import (
"bytes"
"cmp"
"embed"
"encoding/json"
"errors"
"fmt"
"html/template"
"io/fs"
"log"
"net/http"
"os"
"regexp"
"slices"
"strings"
"sync"
"time"
"github.com/a-h/templ"
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
"github.com/yuin/goldmark"
highlighting "github.com/yuin/goldmark-highlighting/v2"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
"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,
highlighting.NewHighlighting(
highlighting.WithFormatOptions(chromahtml.WithClasses(true)),
highlighting.WithWrapperRenderer(func(w util.BufWriter, ctx highlighting.CodeBlockContext, entering bool) {
if entering {
lang, _ := ctx.Language()
fmt.Fprintf(w, `
`, template.HTMLEscapeString(string(lang)))
} else {
w.WriteString(`
`)
}
}),
),
),
goldmark.WithRendererOptions(html.WithUnsafe()),
goldmark.WithParserOptions(
parser.WithASTTransformers(util.Prioritized(externalLinks{}, 100)),
),
)
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]
}
type externalLinks struct{}
func openInNewTab(n ast.Node) {
n.SetAttributeString("target", []byte("_blank"))
n.SetAttributeString("rel", []byte("noopener noreferrer"))
}
func (externalLinks) Transform(doc *ast.Document, reader text.Reader, pc parser.Context) {
src := reader.Source()
ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
switch link := n.(type) {
case *ast.Link:
if bytes.HasPrefix(link.Destination, []byte("http")) {
openInNewTab(link)
}
case *ast.AutoLink:
if link.AutoLinkType == ast.AutoLinkURL && bytes.HasPrefix(link.URL(src), []byte("http")) {
openInNewTab(link)
}
}
return ast.WalkContinue, nil
})
}
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, monospace`,
Normal: `Inter, system-ui, sans-serif`,
}
minecraft := FontPack{
Monospace: `Monocraft, JetBrains Mono, monospace`,
Normal: `Monocraft, Inter, 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)
}
}