Files
slate32/sdcard/.lua/lib/statusbar.lua
T
evan 1adf121dc6 feat(nav)!: own routing in Lua now that the runtime only carries arguments
sys.startApp(path, args) tears the runtime down and starts over from a
file, and keeps nothing else: no history, no title, no app identity. All
of that can ride in the arguments, so it does, and sdcard/.lua/lib/nav.lua
is the single writer of that table.

nav owns the back stack, the bar's title and /.lua/data/<AppId>, which
means the launcher, what "back" reaches and how deep a route nests are
editable on the card rather than in a reflash. Apps call nav.launch,
nav.replace and nav.back; sys.startApp has one caller.

Boot passes no arguments at all, which is how main.lua tells a cold start
from a navigation and opens its own launcher, replacing the home field the
runtime used to read. Arguments cross as JSON, so history is a list of
tables rather than a packed string, and an app passing something JSON
cannot carry sees the error at its own nav call.
2026-08-05 17:04:49 -04:00

180 lines
5.2 KiB
Lua

-- The top strip, built into the same tree as the running app by /.lua/main.lua.
--
-- It keeps no cache of what it painted: each field is its own node, so ui.setText()
-- repaints a clock that ticked and leaves the rest of the bar alone. The title is the
-- exception -- a longer one needs a wider box -- so a change to it rebuilds the screen.
local ui = require "ui"
local nav = require "nav"
local FONT = screen.FONT_SMALL
-- The app name reads as a heading, matching a card title; the clock and memory
-- stay small so the right-hand stack still fits two rows.
local TITLE_FONT = screen.FONT_UI
local BAR_H = 44
local PAD, GAP = 6, 8
local WIFI_W, WIFI_HEIGHTS = 11, { 3, 5, 8 }
local M = { height = BAR_H }
local ids = {}
local title, shownBars, shownOffline
local hidden = false
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, true
end
return status.rssi >= -60 and 3 or status.rssi >= -75 and 2 or 1, false
end
local function memory()
local free, total = sys.getMemory()
local used = math.max(0, (total or 0) - (free or 0))
return (used + 512) // 1024 .. "kB"
end
local function paintWifi(_, x, y)
local theme = ui.theme
local bars, offline = signalBars()
local empty = offline and theme.disabled or theme.muted
for i, h in ipairs(WIFI_HEIGHTS) do
screen.fillRect(x + (i - 1) * 4, y + 8 - h, 3, h, i <= bars and theme.color or empty)
end
if offline then
screen.drawLine(x, y, x + WIFI_W - 1, y + 8, theme.muted)
end
end
-- Painted rather than assembled from a button and a glyph: the chevron is four lines at
-- this size, and a custom node keeps the press feedback to one repaint of one box.
local function paintBack(id, x, y, w, h)
local theme = ui.theme
local pressed = tree.isPressed(id)
local face = pressed and theme.pressedFace or theme.face
local color = pressed and theme.pressedColor or theme.color
screen.roundRect(x, y, w, h, 4, theme.background, face)
local cx, cy = x + w // 2 + 1, y + h // 2
for offset = 0, 1 do -- two passes, because a one pixel chevron reads as a speck
screen.drawLine(cx + offset, cy - 4, cx + offset - 4, cy, color)
screen.drawLine(cx + offset - 4, cy, cx + offset, cy + 4, color)
end
end
local function backNode()
local side = BAR_H - 6
return ui.custom {
w = side,
h = side,
paint = paintBack,
press_style = false, -- paintBack draws its own, so nothing else should repaint
on_enter = function(id)
tree.setPressed(id, true)
ui.invalidate(id)
end,
on_exit = function(id)
tree.setPressed(id, false)
ui.invalidate(id)
end,
on_click = function()
nav.back()
end,
}
end
---@return NodeId
function M.node()
local theme = ui.theme
title = nav.getTitle()
local clockW = screen.getTextWidth(FONT, "00:00:00")
local memW = screen.getTextWidth(FONT, "0000kB")
local fontH = screen.getFontHeight(FONT)
ids = {}
ids.clock = ui.text(clock(), { w = clockW, h = fontH, font = FONT, color = theme.muted })
ids.memory = ui.text(memory(), { w = memW, h = fontH, font = FONT, color = theme.muted })
ids.wifi = ui.custom { w = WIFI_W, h = 9, paint = paintWifi }
shownBars, shownOffline = signalBars() -- what the first paint is about to show
-- Every slot is sized here rather than left to fill: a fill in a row takes the whole
-- width, and the three slots have to share it.
local statusW = math.max(clockW, memW + GAP + WIFI_W)
local backW = nav.canGoBack() and BAR_H - 6 or 0
local titleW = screen.getWidth() - 2 * PAD - 2 * GAP - backW - statusW
local status = ui.box {
w = statusW,
gap = 4,
align = "end",
ids.clock,
ui.box { row = true, w = memW + GAP + WIFI_W, gap = GAP, align = "center", ids.memory, ids.wifi },
}
-- A rule under the strip, so the bar reads as chrome rather than as the top of the app.
return ui.box {
w = "fill",
h = BAR_H,
background = theme.background,
ui.box {
w = "fill",
h = BAR_H - 1,
row = true,
pad = PAD,
gap = GAP,
align = "center",
backW > 0 and backNode() or ui.spacer { w = 0, h = 1 },
ui.box {
row = true,
w = titleW,
justify = "center",
align = "center",
ui.label(title, { font = TITLE_FONT, fit = titleW }),
},
status,
},
ui.box { w = "fill", h = 1, background = theme.muted },
}
end
---Repaints only the fields that changed. A new title needs a new box, so that one is a
---rebuild of the screen rather than a repaint of the bar.
function M.tick()
if hidden then
return
end
if nav.getTitle() ~= title then
ui.rebuild()
return
end
ui.setText(ids.clock, clock())
ui.setText(ids.memory, memory())
local bars, offline = signalBars()
if bars ~= shownBars or offline ~= shownOffline then
shownBars, shownOffline = bars, offline
ui.invalidate(ids.wifi)
end
end
---@return boolean
function M.isVisible()
return not hidden
end
---Hands the whole panel to the app, or takes the strip back.
---@param on boolean
function M.setFullscreen(on)
if hidden == on then
return
end
hidden = on
ui.rebuild()
end
return M