package main import ( "bytes" "cmp" "embed" "encoding/json" "errors" "fmt" "html" "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" gmhtml "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(gmhtml.WithUnsafe()), goldmark.WithParserOptions( parser.WithASTTransformers(util.Prioritized(externalLinks{}, 100)), ), ) var tagRe = regexp.MustCompile(`<[^>]*>`) func ligatures(html string) string { var out strings.Builder depth, last := 0, 0 apply := func(s string) string { if depth == 0 { return arrows.Replace(s) } return s } for _, loc := range tagRe.FindAllStringIndex(html, -1) { out.WriteString(apply(html[last:loc[0]])) tag := html[loc[0]:loc[1]] switch { case strings.HasPrefix(tag, " 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 := ligatures(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 newReplacer(pairs map[string]string) *strings.Replacer { keys := make([]string, 0, len(pairs)) for k := range pairs { keys = append(keys, k) } slices.SortFunc(keys, func(a, b string) int { return cmp.Compare(len(b), len(a)) }) args := make([]string, 0, len(keys)*2) for _, k := range keys { args = append(args, html.EscapeString(k), pairs[k]) // rendered text has < and > escaped } return strings.NewReplacer(args...) } var arrows = newReplacer(map[string]string{ // arrows "->": "→", "<-": "←", "<->": "↔", "-->": "⟶", "<--": "⟵", "<-->": "⟷", "->>": "↠", "<<-": "↞", "|->": "↦", "~>": "↝", "<~": "↜", // double arrows "=>": "⇒", "<=>": "⇔", "==>": "⟹", "<==": "⟸", "<==>": "⟺", // comparisons ">=": "≥", "<=": "≤", "!=": "≠", "+-": "±", }) 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]")}, {"Raylib", Color("text-[#FFFFFF]")}, }, 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: `Inter, Lower Pixel Regular, Monocraft, system-ui, sans-serif`, Normal: `LowerPixelRegular, 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) } }