GitHub Heatmap
Show off your GitHub contributions right on your AWTRIX.
| System | AWTRIX NG Scripts |
|---|---|
| Firmware | AWTRIX NG |
| Topic | Social |
| Built by | Golevka2001 |
| File | fxaGdh4z8w5m.ax · 5.0 KB |
| Icons | — |
| Published | 29 Aug 2026 |
| Downloads | 11 |
A live heatmap of your GitHub contributions, updating on your AWTRIX — today's pixel turns green while you code.
Heads-up: this app relies on an external worker, a tiny Cloudflare Worker you deploy once with wrangler (needs a free GitHub token and a Cloudflare account).
Setup is a couple of minutes, step by step at https://github.com/Golevka2001/awtrixng-github-heatmap
fxaGdh4z8w5m.ax
# @name GitHub Heatmap
# @desc GitHub contribution heatmap via external worker
# @author Golevka2001
# @version 1.3
# @config server text "Server URL" help="Worker endpoint URL"
# @config user text "Username" help="GitHub username"
# @config client_id text "(optional) CF Access Client ID"
# @config client_secret text "(optional) CF Access Client Secret"
# @config rainbow bool "Rainbow Months" default=1
# @config split bool "Split by Month" default=0
# @config avatar bool "Show Avatar" default=0
# @config contrast number "Avatar Contrast" default=1 min=1 max=3 step=0.1 help="Boost avatar contrast"
# @config every number "Refresh" default=60 min=1 unit=min
class GitHubHeatmap
var url, usr, client_id, client_secret, rainbow, split, avatar, contrast, pixels
# Refresh state: ticks counts down to the next fetch, span is the configured
# interval in seconds, retry is the current backoff delay, busy means a
# request is still out.
var ticks, span, retry, busy
def init()
self.url = ""
self.usr = ""
var srv = store.get("server")
var u = store.get("user")
if srv != nil && srv != "" && u != nil && u != ""
self.url = srv
self.usr = str(u)
end
var cid = store.get("client_id")
self.client_id = cid == nil ? "" : str(cid)
var secret = store.get("client_secret")
self.client_secret = secret == nil ? "" : str(secret)
var rb = store.get("rainbow")
self.rainbow = rb == nil ? true : rb
var sp = store.get("split")
self.split = sp == nil ? false : sp
var av = store.get("avatar")
self.avatar = av == nil ? false : av
var ct = store.get("contrast")
self.contrast = ct == nil ? 1 : ct
self.pixels = []
self.ticks = 0
self.span = 60
self.retry = 30
self.busy = false
end
def setup()
self.loop()
end
def on_button(btn)
if btn == "select"
self.now()
end
end
def on_body(body, status)
shared.set("tls", 0)
if body == nil
self.failed()
return
end
# Extract all numbers from the JSON array [n,n,n,...]. re.matchall is
# cheaper than json.load on a 96 KB heap, and the full response is small
# enough that find/keep is not needed.
var m = re.matchall("[0-9]+", body)
if m == nil || size(m) == 0
self.failed()
return
end
var n = size(m)
var px = []
for i : 0 .. n - 1
px.push(int(m[i]))
end
if size(px) != 256
self.failed()
return
end
self.ok()
self.pixels = px
end
def loop()
if self.url != "" && self.due(store.get("every"))
shared.set("tls", 1)
http.get(self.url, / b, st -> self.on_body(b, st),
{'headers': {'CF-Access-Client-Id': self.client_id,
'CF-Access-Client-Secret': self.client_secret,
'X-User': self.usr,
'X-Rainbow': self.rainbow ? "1" : "0",
'X-Split': self.split ? "1" : "0",
'X-Avatar': self.avatar ? "1" : "0",
'X-Contrast': str(self.contrast)}})
end
end
# True at most once per interval, and never while a request is still out.
# Also waits out other apps' TLS handshakes -- concurrent handshakes are
# the main OOM risk on a 96 KB heap. busy is raised here, so only call it
# when a request will actually be issued.
def due(every)
self.span = max(60, num(every, 1) * 60)
if self.ticks > 0
self.ticks = self.ticks - 1
return false
end
if self.busy
return false
end
# skip this cycle while another app is mid-handshake
for k : shared.keys()
if size(k) > 4 && k[size(k) - 4 .. size(k) - 1] == ".tls" && shared.get(k) == 1
var a = shared.age(k)
if a != nil && a < 20000
return false
end
end
end
self.ticks = self.span
self.busy = true
return true
end
# Failure backoff: retry at 30s, doubling, capped at the interval.
def failed()
self.busy = false
self.ticks = self.retry
self.retry = min(self.retry * 2, self.span)
end
def ok()
self.busy = false
self.retry = 30
end
# Manual refresh: clears ticks, leaves busy alone so a press while a
# request is out cannot fire a second one.
def now()
self.ticks = 0
self.retry = 30
end
def draw()
clear()
if self.pixels == nil || size(self.pixels) == 0
var w = text_ink_width("...")
text(int((width() - w) / 2), 6, "...", 0x666666)
return
end
# 32x8 panel, row-major: pixel index i → column i/8, row i%8
for i : 0 .. size(self.pixels) - 1
var c = self.pixels[i]
if c != 0
pixel(int(i / 8), i - int(i / 8) * 8, c)
end
end
end
end
return GitHubHeatmap()
Install it on your AWTRIX NG
Your browser writes the script straight to the device on your network. It joins the app rotation right away.
Some browsers refuse to talk to a device on your network from a public page. Download the flow and paste it into the Scripts tab of your device, or install it from a terminal:
More flows for AWTRIX NG Scripts or Social
Something wrong with this flow? Report it.