GitHub Followers
Your GitHub follower count on the panel.
| System | AWTRIX NG Scripts |
|---|---|
| Firmware | AWTRIX NG |
| Topic | Social |
| Built by | Golevka2001 |
| File | o0Phbtd83FWj.ax · 5.2 KB |
| Icons | 1 |
| Published | 30 Aug 2026 · updated 30 Aug 2026 |
Your GitHub follower count on the panel.
Press the select button for an instant refresh.
Setup: works with just a username — it queries api.github.com/users/<name>, no key needed. Optionally add a personal access token (github.com → Settings → Developer settings → Personal access tokens) to query the authenticated /user endpoint instead.
Heads-up: without a token GitHub allows 60 requests/hour, so don't set the refresh interval too low. A token is stored in plain text on the clock — use one with minimal scope that you can revoke.
Default icon ID: 71442.
o0Phbtd83FWj.ax
# @name GitHub Followers
# @desc Follower count from the GitHub users API
# @author Golevka2001
# @version 1.0
# @config token text "Token" help="Optional. Leave empty to use Username instead"
# @config user text "Username" help="github.com/<username>, ignored when Token is set"
# @config ic text "Icon ID" default="71442" help="LaMetric icon ID"
# @config every number "Refresh" default=60 min=1 unit=min
import string
class GithubFollowers
var url, hdr, units, label, tx, tc, ic, err
# refresh countdown + failure backoff, see due()/failed()/ok()/now()
var ticks, span, retry, busy
def init()
# saving a setting restarts the app, so the URL and headers built here never
# go stale. Token takes precedence over username.
var t = store.get("token")
var u = store.get("user")
self.url = ""
# GitHub answers 403 without a User-Agent
self.hdr = {'User-Agent': "awtrix-github-followers",
'Accept': "application/vnd.github+json",
'X-GitHub-Api-Version': "2022-11-28"}
if t != nil && t != ""
self.url = "https://api.github.com/user"
self.hdr['Authorization'] = "Bearer " + str(t)
elif u != nil && u != ""
self.url = "https://api.github.com/users/" + str(u)
end
self.units = ["", "k", "M", "B", "T"]
self.label = self.url == "" ? "SET?" : "..."
self.tx = 9
self.tc = 0xFFFFFF
self.ic = ""
self.ticks = 0
self.span = 60
self.retry = 30
self.busy = false
self.err = false
end
# first fetch + fill display state before the first frame
def setup()
self.loop()
end
# select forces a refresh; left and right just rotate
def on_button(btn)
if btn == "select"
self.now()
end
end
def on_body(body, status)
shared.set("f", 0)
# non-200 or nil body: nothing usable this attempt
if status != 200 || body == nil
self.failed()
return
end
# ` *` tolerates the pretty-printed space after the colon; the colon keeps
# the needle off the earlier "followers_url" field
var m = re.search("\"followers\": *(\\d+)", body)
if m == nil
self.failed()
return
end
var n = num(m[1])
if n == nil
self.failed()
return
end
self.ok()
# format here, not in draw() -- draw() must not allocate
var d = 1
var i = 0
# promote at 999.5 so a count rounding up to 1000 becomes 1.00 of the next unit
while n >= d * 999.5 && i < 4
d *= 1000
i += 1
end
if i == 0
self.label = str(int(n))
else
var v = n * 1.0 / d # * 1.0 forces real division
# cut on the rounded value so 9999 reads "10.0k", never "10.00k"
if v < 9.995
self.label = string.format("%.2f", v) + self.units[i]
elif v < 99.95
self.label = string.format("%.1f", v) + self.units[i]
else
self.label = str(round(v)) + self.units[i]
end
end
end
def loop()
# url checked first: due() raises busy as a side effect
if self.url != "" && self.due(store.get("every"))
shared.set("f", 1)
http.get(self.url, / b, st -> self.on_body(b, st),
{'headers': self.hdr, 'find': "\"followers\":", 'keep': 24})
end
# while there is no reading yet, the placeholder reflects the last attempt
if self.label == "..."
self.label = self.err ? "?" : "..."
end
self.tx = 9 + int((width() - 9 - text_ink_width(self.label)) / 2)
self.ic = store.get("ic")
self.tc = settings.get("textColor")
if self.tc == nil self.tc = 0xFFFFFF end
if self.label == "?" self.tc = 0xCC7722 end
end
# Fires at most once per interval, never while a request is out or another
# app's TLS handshake is running (shared "f" flag, official convention).
# Raises busy, 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
for k : shared.keys()
var n = size(k)
if n > 2 && k[n - 2 .. n - 1] == ".f" && 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; raises
# err so a fetch with no data yet can show "?" instead of "..."
def failed()
self.busy = false
self.ticks = self.retry
self.retry = min(self.retry * 2, self.span)
self.err = true
end
def ok()
self.busy = false
self.retry = 30
self.err = false
end
# manual refresh: clears the countdown but leaves busy alone, so a press
# while a request is out can't fire a second one
def now()
self.ticks = 0
self.retry = 30
end
def draw()
clear()
if !icon(self.ic, 0, 0)
rect_fill(0, 0, 8, 8, 0x222222)
end
text(self.tx, 6, self.label, self.tc)
end
end
return GithubFollowers()
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:
These icons belong on the device, in /ICONS. Each one is at
most 32×8
pixels — shown here magnified, at their real proportions.
8×8 px
More flows for AWTRIX NG Scripts or Social
Something wrong with this flow? Report it.