Files
slate32/sdcard/apps/settings/main.lua
T
evan d338dfe3e8 refactor: rename the app entry point to init() and require it
crosspoint-reader calls it init() and requires it; this called it setup() and treated
it as optional. Same concept, two spellings, so an app could not move between the two
firmwares for no reason worth defending. init() wins because it is also the stricter
contract: a misspelled entry point is now an error instead of an app that starts,
draws nothing, and explains nothing.

Requiring it exposed that error screens were unreadable. fail() painted the message
and the host relaunched the launcher over it on the very next frame, so every Lua
error was serial-only -- which would have made "Missing init()" useless to anyone
holding the device rather than a console.
2026-08-01 17:52:32 -04:00

292 lines
8.6 KiB
Lua

local ui = require("ui")
TICK_MS = 50
local INSET = 30
local screen, message
local mode = "menu"
local samples, pending, armed = {}, nil, false
local scanRequested, selectedNetwork, password = false, nil, ""
local keyboardPage = "lower"
local buildMenu, buildWifi, buildNetworks, buildKeyboard, startCalibration, cycleRotation
-- Two inset targets give a raw-per-pixel slope; extrapolate it to the screen edges.
-- Exposed as a global so test/settings_calibration.lua can exercise it.
function computeCalibration(s1, s2, w, h, inset)
local sx = (s2.x - s1.x) / (w - 2 * inset)
local sy = (s2.y - s1.y) / (h - 2 * inset)
return math.floor(s1.x - sx * inset), math.floor(s1.y - sy * inset),
math.floor(s2.x + sx * inset), math.floor(s2.y + sy * inset)
end
local function themed(items)
screen = ui.screen(ui.box(items)) -- colors come from the theme by inheritance
end
local function target(n)
if n == 1 then return INSET, INSET end
return 320 - INSET, 480 - INSET
end
local function drawTarget(n)
local x, y = target(n)
-- Drawn outside the component tree, so this is the case that reads the theme directly.
local theme = ui.theme
gui.clear(theme.bg)
gui.drawText("tap the cross", 10, 10, theme.fg, theme.bg)
gui.drawLine(x - 12, y, x + 12, y, theme.accent)
gui.drawLine(x, y - 12, x, y + 12, theme.accent)
end
local function finishCalibration()
local ok = sys.setCalibration(computeCalibration(samples[1], samples[2], 320, 480, INSET))
message = ok and "calibration saved" or "save failed"
mode = "menu"
gui.setRotation(sys.getRotation() / 90)
buildMenu()
end
function startCalibration()
message = nil
mode = "calibrate"
samples, pending, armed = {}, nil, false
gui.setRotation(0)
drawTarget(1)
end
function cycleRotation()
local ok = sys.setRotation((sys.getRotation() + 90) % 360)
message = ok and "rotation saved" or "save failed"
buildMenu()
end
local function cycleTheme()
local names = ui.themeNames()
local next_index = 1
for index, name in ipairs(names) do
if name == ui.themeName then next_index = index % #names + 1 end
end
local ok = sys.setTheme(names[next_index])
ui.reloadTheme()
message = ok and "theme saved" or "save failed"
buildMenu()
end
local zones = require("timezones")
-- The stored value is a POSIX rule, so a zone set by hand and missing from the list
-- shows its rule rather than pretending to be the first entry.
local function zoneLabel()
local current = sys.getTimezone()
for _, zone in ipairs(zones) do
if zone.tz == current then return zone.name end
end
return current
end
local function cycleTimezone()
local current = sys.getTimezone()
local next_index = 1
for index, zone in ipairs(zones) do
if zone.tz == current then next_index = index % #zones + 1 end
end
local ok = sys.setTimezone(zones[next_index].tz)
message = ok and "timezone saved" or "save failed"
buildMenu()
end
local function statusLabel(status)
if status.state == "connected" then return "wifi: " .. status.ssid end
if status.ssid ~= "" then return "wifi: " .. status.state end
return "wifi: not configured"
end
function buildMenu()
mode = "menu"
local items = {
pad = 12,
gap = 8,
ui.text("settings"),
ui.button{label = "calibrate touch", on_press = startCalibration},
ui.button{label = "rotation: " .. sys.getRotation() .. " deg", on_press = cycleRotation},
ui.button{label = statusLabel(wifi.status()), on_press = buildWifi},
ui.button{label = "theme: " .. ui.themeName, on_press = cycleTheme},
ui.button{label = "timezone: " .. zoneLabel(), on_press = cycleTimezone},
ui.button{label = "exit", on_press = sys.exit},
}
if message then items[#items + 1] = ui.text(message) end
themed(items)
end
local function requestScan()
mode = "scanning"
scanRequested = true
themed{pad = 12, gap = 8, ui.text("wifi"), ui.text("scanning...")}
end
local function forgetNetwork()
message = wifi.forget() and "wifi forgotten" or "save failed"
buildWifi()
end
function buildWifi()
mode = "wifi"
local status = wifi.status()
local items = {pad = 12, gap = 8, ui.text("wifi"), ui.text(statusLabel(status))}
if status.state == "connected" then
items[#items + 1] = ui.text("ip: " .. status.ip)
end
items[#items + 1] = ui.button{label = "scan networks", on_press = requestScan}
if status.ssid ~= "" then
items[#items + 1] = ui.button{label = "forget network", on_press = forgetNetwork}
end
items[#items + 1] = ui.button{label = "back", on_press = buildMenu}
themed(items)
end
local function connectSelected()
if not wifi.connect(selectedNetwork.ssid, password) then
message = "could not save wifi"
buildWifi()
return
end
mode = "connecting"
themed{pad = 12, gap = 8,
ui.text("wifi"),
ui.text("connecting to " .. selectedNetwork.ssid .. "..."),
ui.button{label = "back", on_press = buildWifi},
}
end
local function chooseNetwork(network)
selectedNetwork, password = network, ""
if network.secure then buildKeyboard() else connectSelected() end
end
function buildNetworks(networks)
mode = "networks"
local byName = {}
for _, network in ipairs(networks) do
local current = byName[network.ssid]
if network.ssid ~= "" and (not current or network.rssi > current.rssi) then
byName[network.ssid] = network
end
end
local list = {}
for _, network in pairs(byName) do list[#list + 1] = network end
table.sort(list, function(a, b) return a.rssi > b.rssi end)
local items = {pad = 12, gap = 6, ui.text("wifi networks")}
for i = 1, math.min(#list, 6) do
local network = list[i]
local lock = network.secure and " *" or ""
items[#items + 1] = ui.button{
label = network.ssid .. lock .. " " .. network.rssi,
on_press = function() chooseNetwork(network) end,
}
end
if #list == 0 then items[#items + 1] = ui.text("no networks found", {color = ui.theme.muted}) end
items[#items + 1] = ui.button{label = "rescan", on_press = requestScan}
items[#items + 1] = ui.button{label = "back", on_press = buildWifi}
themed(items)
end
local function keyButton(label, value, width)
return ui.button{label = label, w = width or 24, h = 28, pad = 4,
on_press = function()
if #password < 64 then password = password .. value end
buildKeyboard()
end,
}
end
local function keyRow(chars)
local row = {row = true, h = 28, gap = 4}
for char in chars:gmatch(".") do row[#row + 1] = keyButton(char, char) end
return ui.box(row)
end
function buildKeyboard()
mode = "password"
local shown = string.rep("*", #password)
local rows
if keyboardPage == "symbols" then
rows = {"!@#$%^&*()", "-_=+[]{}", "`~;:'\",.?", "/\\|<>"}
else
rows = {"1234567890", "qwertyuiop", "asdfghjkl", "zxcvbnm,.?"}
if keyboardPage == "upper" then
for i, row in ipairs(rows) do rows[i] = row:upper() end
end
end
local controls = {row = true, h = 28, gap = 4,
ui.button{label = keyboardPage, w = 48, h = 28, pad = 4,
on_press = function()
keyboardPage = keyboardPage == "lower" and "upper" or
(keyboardPage == "upper" and "symbols" or "lower")
buildKeyboard()
end},
ui.button{label = "backspace", w = 68, h = 28, pad = 4,
on_press = function() password = password:sub(1, -2); buildKeyboard() end},
keyButton("space", " ", 40),
ui.button{label = "connect", w = 60, h = 28, pad = 4, on_press = connectSelected},
ui.button{label = "cancel", w = 52, h = 28, pad = 4, on_press = buildWifi},
}
themed{pad = 8, gap = 5,
ui.text(selectedNetwork.ssid),
ui.text("password: " .. shown),
keyRow(rows[1]), keyRow(rows[2]), keyRow(rows[3]), keyRow(rows[4]),
ui.box(controls),
}
end
function init()
buildMenu()
end
function draw()
if mode ~= "calibrate" then screen:draw() end
end
function on_touch_down(x, y)
if mode ~= "calibrate" then screen:down(x, y) end
end
function on_touch_up(x, y)
if mode ~= "calibrate" then screen:up(x, y) end
end
function on_tick()
if scanRequested then
scanRequested = false
buildNetworks(wifi.scan())
return
end
if mode == "connecting" then
local status = wifi.status()
if status.state == "connected" then
message = "connected: " .. status.ip
buildWifi()
elseif status.state == "failed" or status.state == "not_found" then
message = "connection " .. status.state
buildWifi()
end
return
end
if mode ~= "calibrate" then return end
local rx, ry = input.getRawTouch()
if not armed then
if not rx then armed = true end
return
end
if rx then
pending = {x = rx, y = ry}
elseif pending then
samples[#samples + 1] = pending
pending = nil
if #samples == 1 then drawTarget(2) else finishCalibration() end
end
end