package main import ( "bytes" "cmp" "embed" "encoding/json" "errors" "fmt" "html" "html/template" "io/fs" "log" "maps" "net/http" "os" "regexp" "slices" "sort" "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, `
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
})
}
type ShellResult struct {
Output templ.Component // nil renders nothing
Clear bool
Vars map[string]string
Cookies []*http.Cookie
}
type shellCommand func(r *http.Request, args []string) ShellResult
var shellCommands map[string]shellCommand
func init() {
shellCommands = map[string]shellCommand{
"fastfetch": func(*http.Request, []string) ShellResult { return ShellResult{Output: Fastfetch(fetchRows)} },
"clear": func(*http.Request, []string) ShellResult { return ShellResult{Clear: true} },
"help": func(*http.Request, []string) ShellResult { return ShellResult{Output: ShellHelp(shellNames())} },
"export": func(r *http.Request, args []string) ShellResult {
if len(args) == 0 {
return ShellResult{Output: ShellText("usage: export KEY=value")}
}
res := ShellResult{Vars: map[string]string{}}
var lines []string
for _, arg := range args {
key, value, ok := strings.Cut(arg, "=")
if !ok {
lines = append(lines, "export: expected KEY=value, got "+arg)
continue
}
value = strings.Trim(value, `"'`)
vars, cookie, err := setEnvVar(key, value)
if err != nil {
lines = append(lines, "export: "+err.Error())
continue
}
maps.Copy(res.Vars, vars)
res.Cookies = append(res.Cookies, cookie)
lines = append(lines, key+"="+value)
}
res.Output = ShellLines(lines)
return res
},
}
}
func setEnvVar(key, value string) (map[string]string, *http.Cookie, error) {
switch key {
case "THEME":
theme, ok := GetTheme(value)
if !ok {
return nil, nil, fmt.Errorf("unknown theme: %s", value)
}
return theme.Vars(), envCookie("theme", value), nil
case "FONT_PACK":
pack, ok := GetFontPack(value)
if !ok {
return nil, nil, fmt.Errorf("unknown font pack: %s", value)
}
return pack.Vars(), envCookie("font_pack", value), nil
default:
return nil, nil, fmt.Errorf("unknown variable: %s", key)
}
}
func envCookie(name, value string) *http.Cookie {
return &http.Cookie{Name: name, Value: value, Path: "/", MaxAge: 31536000, SameSite: http.SameSiteLaxMode}
}
func shellNames() []string {
names := make([]string, 0, len(shellCommands))
for name := range shellCommands {
names = append(names, name)
}
sort.Strings(names)
return names
}
func runShell(r *http.Request, line string) ShellResult {
fields := strings.Fields(line)
if len(fields) == 0 {
return ShellResult{}
}
run, ok := shellCommands[fields[0]]
if !ok {
return ShellResult{Output: ShellText(fields[0] + ": command not found")}
}
return run(r, fields[1:])
}
var projects = []Project{
{
Slug: "krypton",
Title: "Krypton",
Category: "Programming Language Compiler",
Desc: "Krypton is a simple, high-performance, general-purpose language with C-like control designed for good programmers.",
// 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) > 3*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 /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 /shell", func(w http.ResponseWriter, r *http.Request) {
ShellPage(currentStyle(r)).Render(r.Context(), w)
})
mux.HandleFunc("POST /shell/run", func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<10)
line := strings.TrimSpace(r.FormValue("cmd"))
if len(line) > 200 {
line = line[:200]
}
res := runShell(r, line)
for _, c := range res.Cookies {
http.SetCookie(w, c)
}
var buf bytes.Buffer
if res.Output != nil {
if err := ShellEntry(line, res.Output).Render(r.Context(), &buf); err != nil {
http.Error(w, "render failed", http.StatusInternalServerError)
return
}
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
json.NewEncoder(w).Encode(map[string]any{"clear": res.Clear, "html": buf.String(), "vars": res.Vars})
})
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)
}
}