Hackfetch
- 16 Devlogs
- 99 Total hours
custom fetch tool built for hack clubbers. live hackatime stats, 7 hack-club logos, and a leaderboard so you can see who's crushing it today.
custom fetch tool built for hack clubbers. live hackatime stats, 7 hack-club logos, and a leaderboard so you can see who's crushing it today.
fresh out of the oven: hackfetch v2.1.1 (aka the reviewer bug fix pass)
shell detection used to just read $SHELL which is your login shell. if you log in as bash but run fish, hackfetch was calling you a bash user. it now checks $FISH_VERSION / $ZSH_VERSION / $BASH_VERSION and falls back to asking the OS who your parent process actually is
editor detection was two bugs in a trench coat. it wasnt stripping args so zeditor --wait showed up as “zeditor –wait” instead of “Zed”, and if you had no $EDITOR set it defaulted to nano even if you had literally never opened nano. now it strips flags, maps launcher names to human ones (zeditor -> Zed, code -> VS Code, cursor -> Cursor, hx -> Helix etc), and says “-” instead of nano if nothing is set.
top project label was ambiguous. it was pulling weekly stats but the row just said “top project” so people thought it meant all time. now weekly rows read top proj 7d, top lang 7d, top ed 7d, top cat 7d.
setup dialog was scaring people. it always printed “file exists but has no api_key” whenever it reached the setup flow, even if the api_key line was actually there but just failing to parse. loadConfig is now more forgiving (inline comments, quoted values, case insensitive keys) and setup tells you WHY its stuck instead of gaslighting you about your own config. also short circuits if you run hackfetch -setup when youre already set up.
people said they wanted a gay flag color was already there under the name pride but that wasnt obvious. gay is now an alias so both work.
brew upgrade hackfetch
yay -S hackfetch-bin
curl -fsSL https://raw.githubusercontent.com/xerneas3318/hackfetch/main/install.sh | sh
again thank you to everyone who took the time to actually run it and tell me what was broken! 
so my last two devlogs teased two features. shipping both today.
-leaderboard
last devlog i said “compete with your friends on hackatime hours is coming soon.” i thought i was gonna have to build a whole nest server for it. turns out /api/v1/leaderboard/daily and /api/v1/leaderboard/weekly already exist and are public on hackatime. just a single call!! 
so hackfetch -leaderboard daily (or weekly) now shows the top 10:
✦ daily leaderboard Last 24 hours
🥇 1. soaphia14 11h 12m
🥈 2. AnastKara 10h 32m
🥉 3. RoSerMar 10h 6m
4. euledone 10h
5. TheShovel 10h
6. Dylhouse 9h 1m
7. Dexrn 8h 59m
8. HexFud 8h 31m
9. zeddash 8h 29m
10. Trtotomotoro 8h 26m
every username is an OSC 8 clickable link to that person’s hackatime profile. modern terminals (iterm2, kitty, wezterm, alacritty, gnome-terminal) render it as an actual hyperlink you can click. terminals without OSC 8 just show the plain text so it doesnt look broken.
if you’re on the board it highlights your row in orange and adds (you) next to your name.
the other thing. someone else competing in stardance messaged me the other day about integrating hackatime into their own project and needed help figuring out the api (YAY). helping them made me realize hackfetch is doing a bunch of work (auth, caching, streak reconstruction, per-day bucketing) that other tools would love to piggyback on. so now:
$ hackfetch -json
{
"version": "2",
"fetched_at": "2026-07-25T17:00: 20.900829+05:30",
"system": {
"user": "xerneas",
"host": "Macbook-Pro",
"os": "macOS 26.5.2",
...
},
"hackatime": {
"slack": "@U0B8FHHRQPJ",
"streak": 12,
"today_seconds": 15820,
"week_seconds": 237501,
"top_project": { "name": "hackfetch", "
"top_language": { "name": "Go", "seconds": 97418 },
"week_chart": [36967, 31651, 33841, 284
"machines": 3
}
}
week_chart is 7 numbers = daily seconds oldest to newest same as the sparkline. top_language skips the unknown/other buckets. everything lines up with what the normal fetch would show, just parseable. 
landed in v2.1.0.
brew update && brew upgrade hackfetch
yay -Syu hackfetch-bin
then try:
hackfetch -leaderboard weekly
hackfetch -json | jq
For a general install use:
curl -fsSL https://raw.githubusercontent.com/xerneas3318/hackfetch/main/install.sh | sh
went to add a simple 7-day sparkline. by the end of the night i had rewritten how hackfetch talks to hackatime and my streak field is finally accurate for the first time in weeks.
the plan was get the status from the last 7 days. draw ▁▂▃▄▅▆▇█ bars 
except the endpoint i needed straight up 404s. /users/current/summaries doesnt exist on the current hackatime deployment.
Which means every single time i ran hackfetch it was quietly making a 30-day range call that always failed, then falling back to a placeholder that said “if today has activity, streak = 1”. thats it. no matter how long ive actually been on a streak, the fetch would just show 1 day. 🤦
my streak has said 1 day for the last 2 weeks even though ive been coding literally every day. never noticed because 1 day looks kinda plausible (i added 1 day as a fallback so that it didn’t show an error a couple weeks ago and forgot
)
the fix:
went digging in hackatimes rails routes on github. turns out theres a whole /api/v1/ namespace i was never using. and one of the endpoints is /api/v1/users/:id/heartbeats/spans. one call, ~150kb of json, every coding session ive ever logged with start_time, end_time, duration. 2000+ spans.
which is exactly what i need for both features. bucket the timestamps by local calendar day, count unique days for streak, sum durations per day for the sparkline. one request covers both. and while i was already in the api layer i noticed fetchUser was making a whole separate call for my slack handle, which is silly because that data is already in the weekly stats response we cache. so i piggybacked it. weve gone from 4 api calls per fetch to 3.
7-day chart ▄▄▇▅▄▅▃ 65h 28m
oldest on the left, newest on the right. that tall bar in the middle is a big lock-in session from earlier in the week. the two shorter ones on the right are today (still coding) and yesterday (took a break).
opt in for -status --sparkline if you want it in your tmux bar too:
2h 9m today · streak 9 · ▄▄▇▅▄▅▃ 65h 28m
I’m planning on building a whole leaderboard server on a free hack club nest box so people can compete on their hackatime hours. while i was poking around the routes file for the streak fix i noticed something.
/api/v1/leaderboard/daily and /api/v1/leaderboard/weekly already exist. public. no auth needed. no server needed. no cron sync. hackatime literally already has it directly.
so hackfetch -leaderboard is coming soon (private friend groups and personal competitions can come later)
after the ship the devlogs are going to be huge.
stay tuned :P
so heres what my day looked like: nvim was calling hackfetch -status on every redraw for my lualine. tmux was calling it every 60 seconds for the status bar. i had 3 tmux sessions open. and every time i actually opened a new terminal, hackfetch fired again on shell start.
quick napkin math: nvim redraws are basically continuous when im typing, tmux was hitting the api 3x/minute across sessions, plus every terminal open = another fetch. we’re talking hundreds of calls to hackatime in a normal work session. from ONE user.
me 
hackatime is a free hack club service and i was starting to feel bad lol. 
the fix: cache the response
~/.cache/hackfetch/last.json stores the last snapshot with a timestamp. every fetch checks that file first. if its fresh enough, skip the network entirely and just render from disk.
TTL is different depending on the caller:
hackfetch (one-shot): 30 seconds. long enough that a normal re-run inside a devlog attempt is instant, short enough that you dont get badly stale numbers.
hackfetch -status: 60 seconds
tmux polls roughly this often anyway so its basically free now.
hackfetch -watch: ignores the cache, always fresh (thats the whole point of watch mode).
opt out with -no-cache or HACKFETCH_CACHE_TTL=0 if you want. also HACKFETCH_CACHE_TTL=300 to be even more polite.
So yea tmux is now basically free. 
but the real win writing this made me realize the cache does more than save milliseconds:
every fetch was a full TLS handshake, wifi radio spin up, etc. now most fetches touch the disk once and nothing else. macbook fan appreciates it.
offline. if your wifi drops mid coding session you keep working from the cache instead of showing a spinner forever. tmux bar just keeps saying what it said 40 seconds ago.
battery again but for the hackatime server. hackfetch stops asking the api 5-10x more than they need to. multiply by everyone who has it in their tmux config and thats a real thing.
atomic write via .tmp + rename so nothing ever reads a half-written file. cache is user-only (0600), because it does contain your top project name.
while i was fixing the polite-to-the-api stuff i added -doctor, which walks a checklist and tells you whats broken:
hackfetch doctor
✓ wakatime config exists /Users/xerneas/.wakatime.cfg
✓ api_key present loaded from config
✓ wakatime-cli found /Users/xerneas/.wakatime/wakatime-cli
✓ hackatime api reachable today: 28m
✓ terminal supports truecolor $COLORTERM=truecolor
✓ browser opener available /usr/bin/open
green if its working, orange if its not, and non-zero exit if anything failed so you can script it. this was going to be a small self-diagnosis feature and it caught a real bug in the api client on the very first run i did while writing it lol (a string field where hackatime returns an int, so one of the fields was silently missing from every fetch since v1.0). fixed that too.
both landed in v1.10.0. update as usual:
brew update
brew upgrade hackfetch
or arch:
yay -Syu hackfetch-bin
then run hackfetch -doctor first, and if anything is broken it’ll tell you exactly what.
new thing today: hackfetch will just show up wherever you already look. tmux status bar. nvim statusline. any tool that can shell out to a command and put text on a screen.
hackfetch -status
prints one line:
2h 11m today · hackfetch (45m) · streak 12
just the numbers, for tools that already have their own rendering. hackfetch is a data source now, not just a display.
in tmux
two lines in ~/.tmux.conf:
set -g status-interval 60
thats it. every tmux session, every window, refreshing once a minute. i dropped it in a pink Catppuccin capsule to the left of my existing date capsule and now i cant stop glancing at it lol.
in neovim (lualine)
same story, five lines of lua:
sections = {
lualine_z = {
function()
return vim.fn.system("hackfetch -status"):gsub("\n", "")
end
}
}
}
shells out on every redraw. works with airline too, or any custom statusline that lets you plug in a function.
the fun part
making hackfetch call safe for “something tmux runs every 60s forever” meant fixing three assumptions the normal fetch was making. this was way harder than it should have been ngl.
setup cant block. if you dont have ~/.wakatime.cfg, normal hackfetch throws you into an interactive setup wizard (ask me how i know). in status mode that would freeze your tmux bar until you notice and Ctrl+C it. so status mode is fully non interactive now.
no config → print nothing → tmux shows nothing → no deadlock 🙏
api errors cant leak into your bar. hackatime offline? old behavior would dump HTTP 500 timeout into your status area which looks INSANE. now status mode exits silently instead so your bar stays clean
every call has to be cheap. hackfetch’s parallel fetch alr seconds, and the shared HTTP client keeps connections alive across the 4 API calls it fires, so tmux polling every 60s is easy on the API. a proper local file cache is on the list for later but honestly not
needed for now (i’m lazy)
landed in v1.9.0. if you already have it:
brew update
brew upgrade hackfetch
or arch family:
yay -Syu hackfetch-bin
(for more installs for ur specific os check out the github pls)
then drop the tmux or lualine snippet above into your dotfiles and its just there in every terminal. no plugin. no config. no daemon. just a flag.
New thing today: hackfetch now emits 24-bit color when your terminal supports it, and spreads gradients smoothly across each row instead of cycling through the palette one character at a time.
Old behavior: for a per-char scheme like rainbow, each character got exactly one palette entry, wrapping around every N characters. On a 30-column row with a 3-color palette, that looked like ten cycles of red, green, blue, red, green, blue… basically RGB static.
New behavior: the palette spreads across the row once, and characters between palette entries get interpolated in-between colors. Same 3-color palette on a 30-column row now shows red, olive, green, teal, blue, magenta, back to red.
Terminals speak two color modes:
\x1b[38;5;Nm): pick from a fixed palette of 256 preset RGB values.\x1b[38;2;R;G;Bm): specify any of ~16 million RGB values directly.The old code always used 256-color. That meant every character had to snap to one of the palette entries. No in-betweens possible.
The new code checks $COLORTERM (set to truecolor by iTerm2, kitty, WezTerm, Alacritty, GNOME Terminal, and most modern terminals) and emits 24-bit escapes when supported. When it’s not, it falls back to the old cycled-palette behavior automatically.
You can force it either way with HACKFETCH_TRUECOLOR=1 or =0.
The built-in rainbow scheme has 23 palette stops, so the difference on that one is small. You just see a smoother color distribution and less banding.
The change is dramatic on:
~/.config/hackfetch/colors.json for the wildest gradient)stardance palette (11 stops, per-char), where sparkles now flow through the spectrum in order instead of landing on random-looking colorspride, trans, bi, pan, sunset, ocean, forest) since they now emit exact RGB values instead of nearest-256-colorBonus: the SVG, PNG, and JPG export paths use the smooth interpolation too, so exported cards look better regardless of your terminal.
Landed in v1.8.0.
brew update
brew upgrade hackfetch
Or on the Arch family:
yay -Syu hackfetch-bin
Try hackfetch rocket forest and just watch.
Two small things today: hackfetch is now on the AUR, and there’s a new logo.
A reviewer on my last ship said they had a hard time getting hackfetch running on CachyOS (an Arch-based Linux distro). Instead of trying to reproduce their exact issue, I figured the cleanest fix was to publish hackfetch through the package manager Arch users already trust.
So hackfetch is now up as hackfetch-bin on the AUR: aur.archlinux.org/packages/hackfetch-bin
If you’re on Arch, CachyOS, Manjaro, EndeavourOS, Garuda, or any other Arch-based distro:
yay -S hackfetch-bin
Or with paru:
paru -S hackfetch-bin
The package pulls the pre-built binary straight from the GitHub release, so there’s no waiting on a source build. Works on both x86_64 and aarch64.
Add it up and hackfetch is now installable one command from:
This thing is getting kind of hard to NOT install.
Hack Club runs on pizza. Now hackfetch does too.
hackfetch pizza
It’s a triangular slice viewed from the tip, HACK CLUB baked into the middle, and a rolled crust at the wide edge.
The tricky part was the crust. I went through three drafts trying to make it curve at the bottom without breaking the connection to the slice’s diagonal sides (turns out you can’t cleanly connect diagonal characters to horizontal ones in a monospace grid). Landed on a two-row rolled crust where the diagonals keep going and the interior fills with a double horizontal band.
Pairs surprisingly well with sunset:
hackfetch pizza sunset
Turns your terminal into a golden-hour pizza box.
Both landed in v1.7.3. If you’re on Arch-family:
yay -S hackfetch-bin
Elsewhere:
brew update
brew upgrade hackfetch
Now try hackfetch pizza and tell me if the crust looks right.
Last log I added a loading spinner so you’d have something to look at while hackfetch was hitting the Hackatime API. This week I made it so you barely have time to see the spinner at all (undoing all my work lol)
hackfetch was making four separate calls to Hackatime for every fetch:
/users/current/heartbeats/most_recent (for your slack handle)
/users/current/summaries (for your streak)
/users/current/statusbar/today (for today's coding time)
/users/current/stats/last_7_days (for weekly stats)
Doing them one after another meant the total wait was the sum of all four. Turns out that’s about 1.7 seconds on a normal connection. Not the end of the world, but very noticeable when you just want to open a terminal.
Now hackfetch fires all four calls in parallel using goroutines, waits for the slowest one to come back, and then builds the fetch from the responses. Total wait drops from “sum of all four” to “just the slowest one.”
60% faster. Every single fetch. Every single time.
While I was in there, I also switched to a single shared HTTP client with a bigger connection pool. The default Go setup caps you at 2 open connections per host, which is fine when you’re calling one endpoint at a time but hurts when you’re firing four at once. Now the parallel calls can share connections instead of stacking up on each other.
Nothing about the fetch itself changed. Same info, same layout, same colors. Just faster.
Shipped in v1.7.2. If you have hackfetch installed:
brew update
brew upgrade hackfetch
time hackfetch
Watch the spinner barely have time to appear.
Thanks so much to everyone who took the time to comment, rate, and drop feedback on my latest ship. All of it was genuinely so helpful and I’ve been spending the last couple days fixing things up a bit. I totally forgot to devlog so I’m actually losing a couple hours since it’s been over ten hours since my last one :sob
One reviewer pointed out that hackfetch takes a couple of seconds when it’s pulling your Hackatime stats for the first time, and there was no signal that anything was happening. Now there is.
⠋ fetching Hackatime...
The spinner cycles through 10 braille frames and the whole line clears out the instant the fetch finishes. Same fetch, way less awkward pause.
Two small details that make it feel right:
hackfetch runs it around the API calls automatically. No config, no opt-in. It’s just there.
Another reviewer said the 1.5k-line file was too much. They were completely right. hackfetch had grown into one giant main.go that held the logos, the color schemes, the Hackatime API client, the terminal render, the SVG exporter, the PNG rasterizer, everything.
Refactored today into files that each do one thing:
Same binary, same behavior, zero user-visible change. But next time I want to add a new logo or a new color scheme, I know exactly which file to open. Also way friendlier for anyone who wants to hack on it.
Both landed in v1.7.1. If you already have it:
brew update
brew upgrade hackfetch
Or the curl installer will pull v1.7.1 the next time you run it.
Try it.
The goal was simple: your terminal fetch should be more than a terminal thing. If it looks cool on your screen, you should be able to post it in a Slack channel or drop it in a devlog without ever having to take a screenshot.
hackfetch -export card.svg
Run that and instead of printing to your terminal, hackfetch writes an SVG file that looks exactly like the fetch on your screen: logo on the left, stats on the right, rounded corners, monospace font, every color preserved.
Getting the general SVG output was the easy part. Draw a rectangle, put text inside it, done.
The hard part was preserving the colors.
Terminals speak ANSI 256-color codes. SVGs speak hex color strings. They don’t line up one-to-one, so a naive export just drops every color and produces a black-and-white version of the fetch. Not very shareable.
The fix: a lookup table from ANSI 256-color codes to hex values, plus a rendering path that emits every styled character as its own SVG element with the correct fill color.
That’s actually a lot of tspans. Schemes like per-char, where every single glyph has its own gradient stop, can produce hundreds of colored elements per fetch. But it works. The SVG renders smoothly in any browser and looks identical to what shows up in your terminal.
Some things you can do with the exported card:
hackfetch -export card.svg trans
Open it.
Post it.
Ship it.
New milestone today: hackfetch can now stay open and update itself while you code.
The goal was simple: run hackfetch once, leave it in the corner of your terminal, and watch the numbers move in real time as you work.
hackfetch -watch
Your today-hours tick up. Your top project changes when you switch repos. Your streak rolls over at midnight. All without running the command a second time.
The static render was already in place from the regular one-shot fetch. The new part was redrawing without scrolling.
That sounds simple. It is not.
In a terminal, every time you print something, it appends to the bottom of the screen. By default, refreshing the fetch would just print a second copy of itself underneath the first, then a third, then a fourth. Within a minute you’d have a wall of identical fetches scrolling away into the past.
The trick is ANSI escape codes. There’s a sequence that means “move the cursor up N lines” and one that means “clear the rest of the screen from here down.” Together they let you erase the previous fetch and redraw a new one in the same spot. Same screen, fresh data, no scroll.
Hackatime aggregates heartbeats in 1-minute buckets internally, so polling faster than that doesn’t tell you anything new 30 seconds is fast enough to feel “live” without hammering the API for no reason
Ctrl+C to quit. Resizing the terminal doesn’t break it. Pair it with a gradient color scheme and the whole thing turns into a tiny dashboard for your coding session:
hackfetch -watch sunset
Hackfetch crossed another milestone today: you no longer need Go installed to use it. Or Homebrew. Or anything, really.
The goal was simple: one command, any Linux, any Mac, no prerequisites.
curl -fsSL https://raw.githubusercontent.com/xerneas3318/hackfetch/main/install.sh | sh
Run that on Ubuntu, Fedora, Alpine, Arch, openSUSE, whatever you have, and a few seconds later hackfetch is on your PATH.
Getting the basic installer working was the easy part. Detect your OS, detect your CPU architecture, grab the matching binary from GitHub Releases, drop it in /usr/local/bin. Standard installer stuff.
The harder problem was the script itself failing on some Linuxes.
I’d written the first version in Bash, with set -euo pipefail at the top and $'\033[1m' style escape sequences sprinkled through. Worked great on my Mac. Worked great on Ubuntu. Then I checked which shell Alpine Linux ships by default. Not Bash. The whole script would die on line one for anyone on a minimal container.
So I rewrote it in POSIX sh, which is the one shell every Unix system actually has. No Bash-isms, no fancy quoting, no pipefail. Smaller, dumber, way more portable.
The next problem: the script needs curl and tar to do its job. If you don’t have them, normally you’d just be stuck.
Instead, the installer now detects your system’s package manager and installs the missing prereqs for you:
They all do the same thing. They all use different words for it. The installer knows all seven.
If you’ve been around Hack Club for more than five minutes, you know Orpheus. The little dinosaur shows up everywhere: stickers,
websites, T-shirts, hackathon merch.
Until today, he wasn’t in hackfetch. That’s been bugging me since v1.0.
The goal was simple: give Orpheus a spot in the logo lineup, the same way flag, bot, and rocket already have one.
hackfetch orpheus
Drawing him was the harder part.
Every logo in hackfetch has to fit inside a fixed width of 33 characters, otherwise the system info column next to it gets pushed off the screen. 33 characters is not a lot of room to draw a recognizable dinosaur, especially one with personality.
I tried him head-on first. It didn’t work. Round dinosaur faces in ASCII end up looking like blobs because you don’t have curves, only corners.
So I redrew him in profile. The result:
Nine lines tall. No wasted rows.
He plays especially well with the gradient color schemes:
hackfetch orpheus -color forest
hackfetch orpheus -color sunset
Forest makes him look like he’s wandering through tall grass. Sunset gives him a “main character in the closing shot” vibe.
The mascot finally has a seat at the terminal.
Also if any better artists want to have a crack at it, I’m super open to that, and I can credit you in my repo as a collaborator! THNX!
Hackfetch crossed a pretty big milestone today: it actually fetches things.
The goal is simple: build a Neofetch-style dashboard for Hack Club.
Run a single command and get a snapshot of your machine alongside your Hack Club activity.
hackfetch
and instantly see information like:
Getting the system information was the easy part. Go makes it straightforward to grab things like the current user, hostname, operating system, and environment variables.
The harder problem was rendering everything cleanly beside an ASCII logo.
Each logo has different dimensions and line lengths, so I ended up building a small layout engine that calculates the width of every logo row and pads it dynamically before printing the corresponding information. Without that, the text column slowly drifts into the logo and the entire fetch screen looks broken.
Hooking into Hackatime
The more interesting work was integrating with the Hackatime API.Hackfetch reads your API key from:
~/.wakatime.cfg
authenticates with Hackatime, and pulls fresh coding statistics every time you run the command.Right now it can display:
Today’s coding time
Weekly coding time
Current streak
Top project
Most-used language
No browser.
No dashboard.
Just a terminal command.
In honor of pride month, I have added a pride color scheme to my custom hackfetch. It is currently installable via brew. On macs, simply run:
brew install xerneas3318/tap/hackfetch
Currently working on better support for more operating systems. Follow and like to stay updated!
Introducing hackfetch a tiny terminal fetch tool for hackatime. Run hackfetch and get your hours today, weekly total, streak, top project, and top language right in your terminal.
5 logos. 8 color schemes. mix and match:
hackfetch stardance rainbow
How it works: hackfetch reads your hackatime key from ~/.wakatime.cfg, fetches your Hackatime summaries and heartbeats, then renders your stats directly in the terminal.