Files
slate32/sdcard/lib/statusbar.lua
T
evan 021b94fca2 feat(lua): pass an argument to init() and let apps name themselves
sys.launch(path, arg) carries a string to the next app's init(arg), nil when there is
none. States share no memory, so one string is the whole handoff; anything structured
travels as a Lua literal the receiver loads. This is what a screen split across apps
needs to say "collect a password for this network".

sys.setAppName() retitles the status bar, defaulting to the directory name as before. A
setter rather than a declared constant, so one app can retitle per screen. The bar needs
no new invalidation path for it -- the name joins rotation and theme in the cache key --
and clips a name wide enough to reach the memory slot.
2026-08-02 12:02:09 -04:00

127 lines
4.5 KiB
Lua

-- The top bar, painted by the firmware once a second in every app. It draws in panel
-- coordinates (the firmware drops the app viewport around the call), so gui.getWidth()
-- here is the whole screen.
--
-- Invalidation is entirely local: draw() compares what it is about to paint against what
-- it last painted, so the firmware never has to tell the bar that anything changed.
local ui = require("ui")
local PAD = 6
-- The firmware reads both: `height` is the strip it keeps apps out of, `interval` is how
-- often it calls draw().
local M = {height = 22, interval = 1000}
local BAR_H = M.height
local SIGNAL_W = 11
local GAP = 8
-- What is currently on the panel. Every field repaints only when its value changes, so
-- the once-a-second tick costs one clock rectangle instead of a whole bar.
local shown = {}
local function clock()
return sys.isClockSynced() and os.date("%H:%M:%S") or "--:--:--"
end
local function signalBars()
local status = wifi.getStatus()
if status.state ~= "connected" then return 0 end
return status.rssi >= -60 and 3 or status.rssi >= -75 and 2 or 1
end
local function memPercent()
local free, total = sys.getMemory()
if not total or total <= 0 then return 0 end
return (total - free) * 100 // total
end
-- Bars rather than glyphs: there is no icon font, and signal strength is a scale.
local function drawSignal(x, y, bars, color, muted)
for i = 1, 3 do
local h = i * 3
gui.fillRect(x + (i - 1) * 4, y + 9 - h, 3, h, i <= bars and color or muted)
end
end
-- Fills the leading square of the bar, which is the rect the firmware treats as home.
-- Drawn as a button rather than left as a hidden hit region: a target nobody can see is
-- one nobody finds.
local function drawHome(theme)
local inset = 3
local side = BAR_H - 1 - inset * 2
gui.roundRect(inset, inset, side, side, 4, theme.bg, theme.face[1], theme.face[2], theme.muted)
local x, y = inset + side // 2 + 1, inset + side // 2
for offset = 0, 1 do -- two passes, because a one pixel chevron reads as a speck
gui.drawLine(x + offset, y - 4, x + offset - 4, y, theme.fg)
gui.drawLine(x + offset - 4, y, x + offset, y + 4, theme.fg)
end
end
-- Leaving fullscreen is the one change the bar cannot observe: no draw happens while an
-- app owns the panel, so the state it last painted still matches the state it would paint
-- now, while the pixels are gone. Apps toggle fullscreen through here for that reason.
function M.setFullscreen(on)
gui.setFullscreen(on)
if not on then shown = {} end
end
function M.draw(home)
local theme = ui.theme
gui.setTextSize(1) -- panel state: the app may have left it scaled up
local w = gui.getWidth()
local textY = math.floor((BAR_H - 1 - gui.getFontHeight()) / 2)
-- Slots sized for their widest value, so a shorter one never shifts its neighbours.
local clockW, memW = gui.getTextWidth("00:00:00"), gui.getTextWidth("100%")
local clockX = w - PAD - clockW
local signalX = clockX - GAP - SIGNAL_W
local memX = signalX - GAP - memW
-- Rotation moves every slot, the theme recolors them, and an app may rename itself at
-- any time. A new app gets a fresh Lua state, so an empty cache already means "repaint
-- everything".
local name = sys.getAppName()
local key = gui.getRotation() .. "|" .. ui.themeName .. "|" .. name
if key ~= shown.key then
shown = {key = key}
gui.fillRect(0, 0, w, BAR_H, theme.bg)
gui.fillRect(0, BAR_H - 1, w, 1, theme.muted) -- a rule, so the bar reads as chrome
local nameX = PAD
if home then
drawHome(theme)
nameX = BAR_H + PAD
end
-- Clipped rather than overrun: the name is the one field an app controls, and the
-- memory slot sits directly after it.
while #name > 1 and nameX + gui.getTextWidth(name) > memX - GAP do
name = name:sub(1, -2)
end
gui.drawText(name, nameX, textY, theme.fg, theme.bg)
end
local mem = memPercent()
if mem ~= shown.mem then
shown.mem = mem
local text = mem .. "%"
gui.fillRect(memX, 0, memW, BAR_H - 1, theme.bg)
gui.drawText(text, memX + memW - gui.getTextWidth(text), textY, theme.muted, theme.bg)
end
local bars = signalBars()
if bars ~= shown.bars then
shown.bars = bars
gui.fillRect(signalX, 0, SIGNAL_W, BAR_H - 1, theme.bg)
drawSignal(signalX, textY, bars, theme.fg, theme.muted)
end
local time = clock()
if time ~= shown.time then
shown.time = time
gui.fillRect(clockX, 0, clockW, BAR_H - 1, theme.bg)
gui.drawText(time, clockX, textY, theme.muted, theme.bg)
end
end
return M