Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- PocketOS installer
- -- Run this on an advanced pocket computer to install PocketOS.
- -- It writes all OS files, then reboots into PocketOS.
- -- Existing music, messages, scores are kept. config.lua keeps your
- -- values but gains any new keys this version added.
- local files = {
- ["startup.lua"] = [==[
- -- PocketOS bootstrap
- -- Requires an ADVANCED (gold) pocket computer for colour + touch.
- -- Dedicated roles: the configured tracker hub or bank server boots
- -- into that program instead of PocketOS.
- do
- local ok, cfg = pcall(dofile, "/pocketos/config.lua")
- if ok and type(cfg) == "table" then
- if tonumber(cfg.tracker) == os.getComputerID()
- and fs.exists("tracker.lua") then
- shell.run("tracker.lua")
- return
- end
- if tonumber(cfg.bank) == os.getComputerID()
- and fs.exists("bank.lua") then
- shell.run("bank.lua")
- return
- end
- end
- end
- if not term.isColor or not term.isColor() then
- print("PocketOS needs an *advanced* (gold) pocket computer.")
- return
- end
- local ok, err = pcall(function()
- shell.run("/pocketos/main.lua")
- end)
- if not ok and err and not tostring(err):match("Terminated") then
- term.setBackgroundColor(colors.black)
- term.setTextColor(colors.white)
- term.clear()
- term.setCursorPos(1, 1)
- print("PocketOS crashed:")
- print(tostring(err))
- print("")
- print("Press any key to reboot.")
- os.pullEvent("key")
- os.reboot()
- end
- ]==],
- ["tracker.lua"] = [==[
- -- PocketOS tracker hub.
- -- Shows a live table of every PocketOS device reporting its location.
- -- The computer whose ID matches 'tracker' in /pocketos/config.lua boots
- -- into this automatically. Needs a wireless modem attached.
- local devices = {}
- local hasModem = false
- for _, side in ipairs(peripheral.getNames()) do
- if peripheral.getType(side) == "modem" then
- if pcall(rednet.open, side) then hasModem = true end
- end
- end
- if hasModem then
- rednet.host("pocketos.track", "tracker" .. os.getComputerID())
- end
- -- admin safety: only these IDs may pull the fleet location list
- local admins = { 11, 18 }
- do
- local ok, cfg = pcall(dofile, "/pocketos/config.lua")
- if ok and type(cfg) == "table" and type(cfg.admins) == "table" then
- admins = cfg.admins
- end
- end
- local function isAdmin(id)
- for _, a in ipairs(admins) do
- if tonumber(a) == tonumber(id) then return true end
- end
- return false
- end
- local color = term.isColor and term.isColor()
- local function tint(c)
- if color then term.setTextColor(c) end
- end
- local function draw()
- term.setBackgroundColor(colors.black)
- term.setTextColor(colors.white)
- term.clear()
- local w, h = term.getSize()
- term.setCursorPos(1, 1)
- tint(colors.yellow)
- write("PocketOS Tracker - computer #" .. os.getComputerID())
- if not hasModem then
- term.setCursorPos(1, 2)
- tint(colors.red)
- write("No modem attached!")
- end
- term.setCursorPos(1, 3)
- tint(colors.lightGray)
- write(("%-4s %-12s %6s %4s %6s %s"):format("ID", "Name", "X", "Y", "Z", "Seen"))
- local ids = {}
- for id in pairs(devices) do ids[#ids + 1] = id end
- table.sort(ids)
- tint(colors.white)
- local y = 4
- for _, id in ipairs(ids) do
- if y > h - 1 then break end
- local d = devices[id]
- local age = math.floor(os.clock() - d.seen)
- term.setCursorPos(1, y)
- write(("%-4d %-12s %6d %4d %6d %ds"):format(
- id, (d.label or "?"):sub(1, 12), d.x, d.y, d.z, age))
- y = y + 1
- end
- if #ids == 0 then
- term.setCursorPos(1, 5)
- tint(colors.gray)
- write("Waiting for reports...")
- term.setCursorPos(1, 7)
- write("On each pocket, set tracker = " .. os.getComputerID())
- term.setCursorPos(1, 8)
- write("in /pocketos/config.lua")
- end
- term.setCursorPos(1, h)
- tint(colors.gray)
- write("Hold Ctrl+T to exit")
- end
- local timer = os.startTimer(1)
- draw()
- while true do
- local e, a, b, c = os.pullEvent()
- if e == "rednet_message" then
- local id, msg, proto = a, b, c
- if proto == "pocketos.track" and type(msg) == "table" then
- if msg.type == "query" then
- -- a developer's Fleet screen asking for all positions
- if isAdmin(id) then
- local list = {}
- for did, d in pairs(devices) do
- list[#list + 1] = {
- id = did, label = d.label, x = d.x, y = d.y, z = d.z,
- age = math.floor(os.clock() - d.seen),
- }
- end
- rednet.send(id, { ok = true, devices = list }, "pocketos.track")
- else
- rednet.send(id, { ok = false, err = "Not an admin" }, "pocketos.track")
- end
- elseif type(msg.x) == "number" and type(msg.y) == "number"
- and type(msg.z) == "number" then
- devices[id] = {
- label = type(msg.label) == "string" and msg.label or nil,
- x = math.floor(msg.x),
- y = math.floor(msg.y),
- z = math.floor(msg.z),
- seen = os.clock(),
- }
- draw()
- end
- end
- elseif e == "timer" and a == timer then
- draw()
- timer = os.startTimer(1)
- end
- end
- ]==],
- ["gpshost.lua"] = [==[
- -- PocketOS helper: turn this computer into a GPS host.
- --
- -- Minecraft has no built-in GPS. A "GPS cluster" is 4+ computers with
- -- wireless modems, spread out (high up for range, a few blocks apart,
- -- and NOT all at the same height), each running as a gps host. Pocket
- -- computers in radio range can then work out where they are.
- --
- -- Run this on each host computer and type in that computer's own
- -- block coordinates (press F3 in-game to read them off).
- local hasModem = false
- for _, side in ipairs(peripheral.getNames()) do
- if peripheral.getType(side) == "modem"
- and peripheral.call(side, "isWireless") then
- hasModem = true
- end
- end
- if not hasModem then
- print("Attach a wireless modem to this computer first!")
- return
- end
- print("This computer's block coordinates (press F3 to see them):")
- write("x: ")
- local x = tonumber(read())
- write("y: ")
- local y = tonumber(read())
- write("z: ")
- local z = tonumber(read())
- if not (x and y and z) then
- print("Those need to be numbers - run gpshost again.")
- return
- end
- local f = fs.open("startup.lua", "w")
- f.write("-- GPS host (created by gpshost)\n")
- f.write(("shell.run(\"gps\", \"host\", %s, %s, %s)\n"):format(x, y, z))
- f.close()
- print(("GPS host at %s, %s, %s saved."):format(x, y, z))
- print("This computer now serves GPS on every boot.")
- sleep(1.5)
- os.reboot()
- ]==],
- ["doorlock.lua"] = [==[
- -- doorlock - a touch PIN lock for an ADVANCED computer (not pocket).
- --
- -- First run: pick a PIN on the keypad; the program takes over startup
- -- so the computer boots locked from then on. Ctrl+T is blocked, and
- -- attempts/lockout survive reboots.
- --
- -- 3 wrong PINs = locked out. The unlock code is rednet-messaged to the
- -- pocket computer OWNER_ID (it lands in the Messages app) - the door
- -- computer needs a wireless modem for that. With no modem it falls
- -- back to a 60 second cooldown.
- --
- -- When unlocked: big OPEN DOOR button (your redstone goes in
- -- openDoor() below), plus Lock and Shell buttons in the corners.
- -------------------------------------------------- CONFIG - edit me --
- local OWNER_ID = 11 -- pocket computer that receives unlock codes
- local DOOR_SIDE = "bottom" -- redstone side used by the default openDoor
- local DOOR_TIME = 3 -- seconds the door stays powered
- local AUTO_LOCK = 60 -- relock after this many idle seconds (0 = never)
- local door = peripheral.find("redstoneIntegrator")
- -- The OPEN DOOR button runs this. Put YOUR redstone logic here.
- local function openDoor()
- door.setOutput(DOOR_SIDE, true)
- sleep(DOOR_TIME)
- door.setOutput(DOOR_SIDE, false)
- end
- -----------------------------------------------------------------------
- local DATA = "/.doorlock"
- if not term.isColor or not term.isColor() then
- print("doorlock needs an advanced (gold) computer.")
- return
- end
- math.randomseed(os.epoch and os.epoch("utc") or os.time() * 1000)
- local hasModem = false
- for _, side in ipairs(peripheral.getNames()) do
- if peripheral.getType(side) == "modem" then
- if pcall(rednet.open, side) then hasModem = true end
- end
- end
- local w, h = term.getSize()
- local function loadState()
- if fs.exists(DATA) then
- local f = fs.open(DATA, "r")
- local v = textutils.unserialize(f.readAll() or "")
- f.close()
- if type(v) == "table" then return v end
- end
- return {}
- end
- local st = loadState()
- local function save()
- local f = fs.open(DATA, "w")
- f.write(textutils.serialize(st))
- f.close()
- end
- local function hash(s)
- local n = 5381
- for i = 1, #s do
- n = (n * 33 + s:byte(i)) % 4294967296
- end
- return tostring(n)
- end
- local function fill(x, y, wd, ht, bg)
- term.setBackgroundColor(bg)
- for i = 0, ht - 1 do
- term.setCursorPos(x, y + i)
- term.write(string.rep(" ", wd))
- end
- end
- local function center(y, s, fg)
- term.setCursorPos(math.floor((w - #s) / 2) + 1, y)
- term.setTextColor(fg)
- term.setBackgroundColor(colors.black)
- term.write(s)
- end
- local function sendCode(code)
- if not hasModem then return false end
- rednet.send(OWNER_ID, {
- text = "3 wrong PIN tries on #" .. os.getComputerID()
- .. ". Unlock code: " .. code,
- label = os.getComputerLabel() or "Door Lock",
- }, "pocketos")
- return true
- end
- -- GUI number pad. Returns the digits entered once OK/Enter is pressed.
- -- opts: hint (bottom line), onResend (adds a resend-code button).
- local function keypad(title, msg, opts)
- opts = opts or {}
- local entry = ""
- local info = msg
- local infoColor = colors.red
- local layout = {
- { "1", "2", "3" },
- { "4", "5", "6" },
- { "7", "8", "9" },
- { "C", "0", "OK" },
- }
- while true do
- term.setBackgroundColor(colors.black)
- term.clear()
- center(2, title, colors.white)
- if #entry > 0 then
- center(4, string.rep("\7 ", #entry), colors.blue)
- else
- center(4, "_ _ _", colors.gray)
- end
- local regions = {}
- local x0 = math.floor(w / 2) - 8
- for r = 1, 4 do
- for cn = 1, 3 do
- local label = layout[r][cn]
- local bx = x0 + (cn - 1) * 6
- local by = 4 + r * 2
- fill(bx, by, 5, 1, label == "OK" and colors.green or colors.gray)
- term.setCursorPos(bx + math.floor((5 - #label) / 2), by)
- term.setTextColor(colors.white)
- term.write(label)
- regions[#regions + 1] = { x1 = bx, x2 = bx + 4, y = by, label = label }
- end
- end
- if info then center(14, info, infoColor) end
- if opts.onResend then
- center(16, "[ Resend code ]", colors.blue)
- end
- center(h, opts.hint or "Typing digits works too", colors.gray)
- local function press(label)
- if label == "OK" then
- if #entry >= 3 then return entry end
- info, infoColor = "At least 3 digits", colors.red
- elseif label == "C" then
- entry = ""
- elseif #entry < 8 then
- entry = entry .. label
- end
- end
- local e, a, b, c = os.pullEventRaw() -- raw: Ctrl+T is ignored
- if e == "mouse_click" then
- local handled = false
- for _, r in ipairs(regions) do
- if c == r.y and b >= r.x1 and b <= r.x2 then
- local done = press(r.label)
- if done then return done end
- handled = true
- break
- end
- end
- if not handled and opts.onResend and c == 16 then
- opts.onResend()
- info, infoColor = "Code sent again!", colors.green
- end
- elseif e == "char" and a:match("^%d$") then
- if #entry < 8 then entry = entry .. a end
- elseif e == "key" then
- if a == keys.enter then
- if #entry >= 3 then return entry end
- info, infoColor = "At least 3 digits", colors.red
- elseif a == keys.backspace then
- entry = entry:sub(1, -2)
- end
- end
- end
- end
- local function cooldown(secs)
- for left = secs, 1, -1 do
- term.setBackgroundColor(colors.black)
- term.clear()
- center(8, "Too many attempts", colors.red)
- center(10, "No modem to send a code.", colors.gray)
- center(12, "Try again in " .. left .. "s", colors.white)
- local t = os.startTimer(1)
- repeat
- local e, id = os.pullEventRaw()
- until e == "timer" and id == t
- end
- end
- local function unlockedScreen()
- while true do
- term.setBackgroundColor(colors.black)
- term.clear()
- center(2, (os.getComputerLabel() or "Door") .. " - unlocked", colors.white)
- local bw, bh = 25, 5
- local bx = math.floor((w - bw) / 2) + 1
- local by = math.floor((h - bh) / 2) + 1
- fill(bx, by, bw, bh, colors.green)
- term.setCursorPos(bx + math.floor((bw - 9) / 2), by + 2)
- term.setTextColor(colors.white)
- term.write("OPEN DOOR")
- fill(2, h - 1, 6, 1, colors.gray)
- term.setCursorPos(3, h - 1)
- term.setTextColor(colors.white)
- term.write("Lock")
- fill(w - 8, h - 1, 7, 1, colors.gray)
- term.setCursorPos(w - 7, h - 1)
- term.write("Shell")
- local timer = AUTO_LOCK > 0 and os.startTimer(AUTO_LOCK) or nil
- local e, a, b, c = os.pullEventRaw()
- if e == "timer" and a == timer then
- return "lock" -- idle too long
- elseif e == "mouse_click" then
- if b >= bx and b <= bx + bw - 1 and c >= by and c <= by + bh - 1 then
- fill(bx, by, bw, bh, colors.lime)
- term.setCursorPos(bx + math.floor((bw - 10) / 2), by + 2)
- term.setTextColor(colors.black)
- term.write("DOOR OPEN!")
- pcall(openDoor)
- elseif c == h - 1 and b >= 2 and b <= 7 then
- return "lock"
- elseif c == h - 1 and b >= w - 8 then
- return "shell"
- end
- end
- end
- end
- -- first-run setup
- if not st.pin then
- local m
- while true do
- local a = keypad("Set up Door Lock", m, { hint = "Choose a PIN (3-8 digits)" })
- local b = keypad("Set up Door Lock", nil, { hint = "Repeat the PIN" })
- if a == b then
- st.pin = hash(a)
- st.attempts = 0
- save()
- break
- end
- m = "PINs didn't match - retry"
- end
- local f = fs.open("startup.lua", "w")
- f.write("-- Door lock (created by doorlock)\n")
- f.write("shell.run(\"doorlock.lua\")\n")
- f.close()
- end
- local msg
- while true do
- if st.lockout then
- local entered = keypad("LOCKED OUT", msg, {
- hint = "Code sent to pocket #" .. OWNER_ID,
- onResend = hasModem and function() sendCode(st.lockout) end or nil,
- })
- if entered == st.lockout then
- st.lockout = nil
- st.attempts = 0
- save()
- msg = nil
- if unlockedScreen() == "shell" then break end
- else
- msg = "Wrong code"
- end
- else
- local entered = keypad("\7 Locked", msg)
- if hash(entered) == st.pin then
- st.attempts = 0
- save()
- msg = nil
- if unlockedScreen() == "shell" then break end
- else
- st.attempts = (st.attempts or 0) + 1
- save()
- if st.attempts >= 3 then
- if hasModem then
- st.lockout = tostring(math.random(100000, 999999))
- save()
- sendCode(st.lockout)
- msg = nil
- else
- cooldown(60)
- st.attempts = 0
- save()
- msg = nil
- end
- else
- msg = "Wrong PIN - " .. (3 - st.attempts) .. " left"
- end
- end
- end
- end
- term.setBackgroundColor(colors.black)
- term.setTextColor(colors.white)
- term.clear()
- term.setCursorPos(1, 1)
- print("Door lock exited. Run 'reboot' to lock again.")
- ]==],
- ["bank.lua"] = [==[
- -- PocketOS bank server: stores everyone's money.
- --
- -- Setup: pick a computer with a wireless modem, set `bank = <its ID>`
- -- in /pocketos/config.lua (on every device), and reboot it - it boots
- -- into the bank automatically, just like the tracker hub. Accounts are
- -- keyed by pocket computer ID and saved in /.bank.
- --
- -- Deposits come from ATMs (see atm.lua); payments come from the Bank
- -- app on pockets. The bank messages people their receipts.
- -------------------------------------------------- CONFIG - edit me --
- -- Only these computer IDs may submit ATM deposits, e.g. { 12, 15 }.
- -- nil = accept deposits from any computer (fine among friends).
- local ATMS = nil
- -- Balance for brand-new accounts.
- local START = 0
- -----------------------------------------------------------------------
- local DATA = "/.bank"
- local accounts = {}
- if fs.exists(DATA) then
- local f = fs.open(DATA, "r")
- accounts = textutils.unserialize(f.readAll() or "") or {}
- f.close()
- end
- local function save()
- local f = fs.open(DATA, "w")
- f.write(textutils.serialize(accounts))
- f.close()
- end
- local MDATA = "/.market"
- local market = { nextId = 1, items = {} }
- if fs.exists(MDATA) then
- local f = fs.open(MDATA, "r")
- local v = textutils.unserialize(f.readAll() or "")
- f.close()
- if type(v) == "table" and type(v.items) == "table" then market = v end
- end
- local function saveMarket()
- local f = fs.open(MDATA, "w")
- f.write(textutils.serialize(market))
- f.close()
- end
- local hasModem = false
- for _, side in ipairs(peripheral.getNames()) do
- if peripheral.getType(side) == "modem" then
- if pcall(rednet.open, side) then hasModem = true end
- end
- end
- if not hasModem then
- print("Attach a wireless modem first!")
- return
- end
- rednet.host("pocketos.bank", "bank" .. os.getComputerID())
- -- admin safety: only these IDs may read the transaction ledger
- local admins = { 11, 18 }
- do
- local ok, cfg = pcall(dofile, "/pocketos/config.lua")
- if ok and type(cfg) == "table" and type(cfg.admins) == "table" then
- admins = cfg.admins
- end
- end
- local function isAdmin(id)
- for _, a in ipairs(admins) do
- if tonumber(a) == tonumber(id) then return true end
- end
- return false
- end
- local log = {}
- local function addLog(s)
- local ok, t = pcall(os.date, "%H:%M ")
- table.insert(log, 1, (ok and t or "") .. s)
- while #log > 12 do table.remove(log) end
- end
- -- persistent transaction history for the developer Ledger view
- local TDATA = "/.bank_txns"
- local txns = {}
- if fs.exists(TDATA) then
- local f = fs.open(TDATA, "r")
- txns = textutils.unserialize(f.readAll() or "") or {}
- f.close()
- end
- local function record(kind, from, to, amount)
- local ok, t = pcall(os.date, "%H:%M")
- table.insert(txns, 1, {
- kind = kind, from = from, to = to, amount = amount,
- time = ok and t or "",
- })
- while #txns > 200 do table.remove(txns) end
- local f = fs.open(TDATA, "w")
- f.write(textutils.serialize(txns))
- f.close()
- end
- local function get(id)
- local key = tostring(id)
- local acc = accounts[key]
- if not acc then
- acc = { bal = START }
- accounts[key] = acc
- end
- return acc
- end
- local function notify(id, text)
- rednet.send(tonumber(id), { text = text, label = "Bank" }, "pocketos")
- end
- local color = term.isColor and term.isColor()
- local function tint(c)
- if color then term.setTextColor(c) end
- end
- local function draw()
- term.setBackgroundColor(colors.black)
- term.setTextColor(colors.white)
- term.clear()
- local w, h = term.getSize()
- term.setCursorPos(1, 1)
- tint(colors.yellow)
- write("PocketOS Bank - computer #" .. os.getComputerID())
- local n, total = 0, 0
- for _, acc in pairs(accounts) do
- n = n + 1
- total = total + (acc.bal or 0)
- end
- term.setCursorPos(1, 2)
- tint(colors.lightGray)
- write(n .. " accounts, $" .. total .. " in circulation, "
- .. #market.items .. " listings")
- term.setCursorPos(1, 4)
- write("Recent:")
- tint(colors.white)
- for i, s in ipairs(log) do
- if 4 + i > h - 1 then break end
- term.setCursorPos(1, 4 + i)
- write(s:sub(1, w))
- end
- term.setCursorPos(1, h)
- tint(colors.gray)
- write("Hold Ctrl+T to stop")
- end
- local function allowedATM(id)
- if not ATMS then return true end
- for _, a in ipairs(ATMS) do
- if a == id then return true end
- end
- return false
- end
- addLog("Bank started")
- draw()
- while true do
- local id, msg = rednet.receive("pocketos.bank")
- if type(msg) == "table" and type(msg.type) == "string" then
- local reply
- if msg.type == "balance" then
- local acc = get(id)
- if type(msg.label) == "string" and #msg.label > 0 then
- acc.label = msg.label:sub(1, 12)
- save()
- end
- reply = { ok = true, balance = acc.bal, label = acc.label }
- elseif msg.type == "info" then
- local account = tonumber(msg.account)
- if account then
- local acc = get(account)
- reply = { ok = true, balance = acc.bal, label = acc.label }
- else
- reply = { ok = false, err = "Bad account" }
- end
- elseif msg.type == "deposit" then
- local account = tonumber(msg.account)
- local amount = tonumber(msg.amount)
- amount = amount and math.floor(amount)
- if not allowedATM(id) then
- reply = { ok = false, err = "ATM not authorised" }
- elseif not account or not amount or amount <= 0
- or amount > 1000000000 then
- reply = { ok = false, err = "Bad deposit" }
- else
- local acc = get(account)
- acc.bal = (acc.bal or 0) + amount
- save()
- addLog("#" .. account .. " deposited $" .. amount)
- record("deposit", nil, account, amount)
- notify(account, "Deposited $" .. amount
- .. ". Balance: $" .. acc.bal)
- reply = { ok = true, balance = acc.bal, label = acc.label }
- end
- elseif msg.type == "pay" then
- local to = tonumber(msg.to)
- local amount = tonumber(msg.amount)
- amount = amount and math.floor(amount)
- local from = get(id)
- if type(msg.label) == "string" and #msg.label > 0 then
- from.label = msg.label:sub(1, 12)
- end
- if not to or not amount or amount <= 0 then
- reply = { ok = false, err = "Bad payment" }
- elseif to == id then
- reply = { ok = false, err = "That's you!" }
- elseif (from.bal or 0) < amount then
- reply = { ok = false, err = "Not enough money" }
- else
- local dest = get(to)
- from.bal = from.bal - amount
- dest.bal = (dest.bal or 0) + amount
- save()
- addLog("#" .. id .. " paid #" .. to .. " $" .. amount)
- record("pay", id, to, amount)
- notify(to, "You got $" .. amount .. " from "
- .. (from.label and (from.label .. " ") or "")
- .. "#" .. id .. ". Balance: $" .. dest.bal)
- reply = { ok = true, balance = from.bal }
- end
- elseif msg.type == "txns" then
- if not isAdmin(id) then
- reply = { ok = false, err = "Not an admin" }
- else
- local n = math.min(#txns, tonumber(msg.limit) or 40)
- local out = {}
- for i = 1, n do out[i] = txns[i] end
- reply = { ok = true, txns = out }
- end
- elseif msg.type == "mklist" then
- reply = { ok = true, items = market.items }
- elseif msg.type == "mkpost" then
- local name = type(msg.name) == "string" and msg.name:sub(1, 20) or ""
- local price = tonumber(msg.price)
- price = price and math.floor(price)
- local desc = type(msg.desc) == "string" and msg.desc:sub(1, 60) or ""
- local mine = 0
- for _, it in ipairs(market.items) do
- if it.seller == id then mine = mine + 1 end
- end
- if name == "" or not price or price <= 0 or price > 1000000000 then
- reply = { ok = false, err = "Bad listing" }
- elseif mine >= 8 then
- reply = { ok = false, err = "Max 8 listings" }
- elseif #market.items >= 60 then
- reply = { ok = false, err = "Market is full" }
- else
- local acc = get(id)
- if type(msg.label) == "string" and #msg.label > 0 then
- acc.label = msg.label:sub(1, 12)
- save()
- end
- market.items[#market.items + 1] = {
- id = market.nextId,
- seller = id,
- label = acc.label,
- name = name,
- price = price,
- desc = desc,
- }
- market.nextId = market.nextId + 1
- saveMarket()
- addLog("#" .. id .. " listed " .. name .. " $" .. price)
- reply = { ok = true, id = market.nextId - 1 }
- end
- elseif msg.type == "mkremove" then
- local lid = tonumber(msg.id)
- local removed = false
- for k, it in ipairs(market.items) do
- if it.id == lid and it.seller == id then
- table.remove(market.items, k)
- removed = true
- break
- end
- end
- if removed then
- saveMarket()
- addLog("#" .. id .. " removed a listing")
- reply = { ok = true }
- else
- reply = { ok = false, err = "Not your listing" }
- end
- end
- if reply then
- rednet.send(id, reply, "pocketos.bank")
- draw()
- end
- end
- end
- ]==],
- ["atm.lua"] = [==[
- -- PocketOS ATM - touch-screen deposits into the PocketOS bank.
- --
- -- Hardware: an ADVANCED computer with a wireless modem, a Create
- -- display target ("create_target") whose display lists the deposit
- -- chest's contents, and redstone (or an Advanced Peripherals redstone
- -- integrator) that moves the deposited items away when pulsed.
- --
- -- Flow: player taps START, types their pocket computer ID, puts iron/
- -- gold/diamonds in the chest, taps DEPOSIT ITEMS, confirms - the items
- -- get taken and the value lands in their bank account (they also get a
- -- receipt in Messages). Runs once and takes over startup as a kiosk.
- -------------------------------------------------- CONFIG - edit me --
- local RATES = { iron = 1, gold = 5, diamond = 25 } -- $ per item
- local ACCEPT_SIDE = "bottom" -- redstone side that accepts the items
- local IDLE_TIME = 45 -- seconds before returning to the start
- -- Runs when a deposit is accepted: move the items out of the chest.
- local redint = peripheral.find("redstoneIntegrator")
- local function acceptItems()
- local out = redint or redstone
- out.setOutput(ACCEPT_SIDE, true)
- sleep(1)
- out.setOutput(ACCEPT_SIDE, false)
- end
- -----------------------------------------------------------------------
- local NICE = { iron = "Iron", gold = "Gold", diamond = "Diamond" }
- if not term.isColor or not term.isColor() then
- print("The ATM needs an advanced (gold) computer.")
- return
- end
- local target = peripheral.find("create_target")
- local hasModem = false
- for _, side in ipairs(peripheral.getNames()) do
- if peripheral.getType(side) == "modem" then
- if pcall(rednet.open, side) then hasModem = true end
- end
- end
- local w, h = term.getSize()
- -- take over startup so the ATM boots as a kiosk
- do
- local cur = ""
- if fs.exists("startup.lua") then
- local f = fs.open("startup.lua", "r")
- cur = f.readAll() or ""
- f.close()
- end
- if not cur:find("atm.lua", 1, true) then
- local f = fs.open("startup.lua", "w")
- f.write("-- ATM kiosk (created by atm.lua)\n")
- f.write("shell.run(\"atm.lua\")\n")
- f.close()
- end
- end
- local function fill(x, y, wd, ht, bg)
- term.setBackgroundColor(bg)
- for i = 0, ht - 1 do
- term.setCursorPos(x, y + i)
- term.write(string.rep(" ", wd))
- end
- end
- local function center(y, s, fg)
- term.setCursorPos(math.floor((w - #s) / 2) + 1, y)
- term.setTextColor(fg)
- term.setBackgroundColor(colors.black)
- term.write(s)
- end
- local function button(x, y, label, bg)
- fill(x, y, #label + 2, 1, bg)
- term.setCursorPos(x + 1, y)
- term.setTextColor(colors.white)
- term.write(label)
- return { x1 = x, y1 = y, x2 = x + #label + 1, y2 = y }
- end
- local function hit(r, x, y)
- return r and x >= r.x1 and x <= r.x2 and y >= r.y1 and y <= r.y2
- end
- local function screenMsg(title, lines, buttons)
- term.setBackgroundColor(colors.black)
- term.clear()
- center(4, title, colors.white)
- for i, s in ipairs(lines) do
- center(6 + i, s, colors.lightGray)
- end
- local regions = {}
- local total = #buttons - 1
- for _, b in ipairs(buttons) do total = total + #b[1] + 2 end
- local bx = math.floor((w - total) / 2) + 1
- for i, b in ipairs(buttons) do
- regions[i] = button(bx, 9 + #lines, b[1], b[2])
- bx = bx + #b[1] + 3
- end
- while true do
- local _, _, mx, my = os.pullEvent("mouse_click")
- for i, r in ipairs(regions) do
- if hit(r, mx, my) then return i end
- end
- end
- end
- ------------------------------------------------------------- bank --
- local bankId
- local function findBank()
- if bankId then return bankId end
- local ok, cfg = pcall(dofile, "/pocketos/config.lua")
- if ok and type(cfg) == "table" and tonumber(cfg.bank) then
- bankId = tonumber(cfg.bank)
- return bankId
- end
- bankId = rednet.lookup("pocketos.bank")
- return bankId
- end
- local function request(msg)
- if not hasModem then return nil, "No modem on the ATM" end
- local bid = findBank()
- if not bid then return nil, "No bank found" end
- rednet.send(bid, msg, "pocketos.bank")
- local id, resp = rednet.receive("pocketos.bank", 3)
- if id == bid and type(resp) == "table" then return resp end
- return nil, "Bank not answering"
- end
- -- read the Create display target listing the deposit chest
- local function scanDeposit()
- local found, total = {}, 0
- if not target then return found, 0 end
- for i = 1, 8 do
- local ok, line = pcall(target.getLine, i)
- if ok and type(line) == "string" and line ~= "" then
- local amount = tonumber(line:match("(%d+)"))
- if amount then
- local low = line:lower()
- local kind
- if low:find("iron", 1, true) then
- kind = "iron"
- elseif low:find("gold", 1, true) then
- kind = "gold"
- elseif low:find("diamond", 1, true) then
- kind = "diamond"
- end
- if kind and RATES[kind] then
- local value = amount * RATES[kind]
- found[#found + 1] = { n = amount, kind = kind, value = value }
- total = total + value
- end
- end
- end
- end
- return found, total
- end
- --------------------------------------------------------------- ui --
- local function keypad(title)
- local entry = ""
- local layout = {
- { "1", "2", "3" },
- { "4", "5", "6" },
- { "7", "8", "9" },
- { "C", "0", "OK" },
- }
- while true do
- term.setBackgroundColor(colors.black)
- term.clear()
- center(2, title, colors.white)
- center(4, entry == "" and "_" or entry, colors.yellow)
- local regions = {}
- local x0 = math.floor(w / 2) - 8
- for r = 1, 4 do
- for cn = 1, 3 do
- local label = layout[r][cn]
- local bx = x0 + (cn - 1) * 6
- local by = 4 + r * 2
- fill(bx, by, 5, 1, label == "OK" and colors.green or colors.gray)
- term.setCursorPos(bx + math.floor((5 - #label) / 2), by)
- term.setTextColor(colors.white)
- term.write(label)
- regions[#regions + 1] = { x1 = bx, x2 = bx + 4, y = by, label = label }
- end
- end
- center(14, "[ Cancel ]", colors.red)
- center(h, "Your ID is shown in Settings on your pocket", colors.gray)
- local e, a, b, c = os.pullEvent()
- if e == "mouse_click" then
- if c == 14 then return nil end
- for _, r in ipairs(regions) do
- if c == r.y and b >= r.x1 and b <= r.x2 then
- if r.label == "OK" then
- if #entry > 0 then return tonumber(entry) end
- elseif r.label == "C" then
- entry = ""
- elseif #entry < 7 then
- entry = entry .. r.label
- end
- end
- end
- elseif e == "char" and a:match("^%d$") and #entry < 7 then
- entry = entry .. a
- elseif e == "key" then
- if a == keys.enter and #entry > 0 then return tonumber(entry) end
- if a == keys.backspace then entry = entry:sub(1, -2) end
- end
- end
- end
- local function idleScreen()
- term.setBackgroundColor(colors.black)
- term.clear()
- center(3, "PocketOS ATM", colors.yellow)
- center(5, string.format("Iron $%d Gold $%d Diamond $%d",
- RATES.iron, RATES.gold, RATES.diamond), colors.lightGray)
- local bw, bh = 15, 3
- local bx = math.floor((w - bw) / 2) + 1
- fill(bx, 8, bw, bh, colors.green)
- term.setCursorPos(bx + math.floor((bw - 5) / 2), 9)
- term.setTextColor(colors.white)
- term.write("START")
- if not target then
- center(13, "! No Create target attached", colors.red)
- end
- if not hasModem then
- center(14, "! No wireless modem", colors.red)
- end
- center(h, "ATM #" .. os.getComputerID(), colors.gray)
- while true do
- local e = os.pullEvent("mouse_click")
- if e then return end
- end
- end
- local function depositFlow(account)
- local items, total = scanDeposit()
- if not target then
- screenMsg("ATM Error", { "No Create display", "target attached." },
- { { "OK", colors.blue } })
- return nil
- end
- if total <= 0 then
- screenMsg("Nothing to Deposit", {
- "Put iron, gold or",
- "diamonds into the",
- "deposit chest first.",
- }, { { "OK", colors.blue } })
- return nil
- end
- term.setBackgroundColor(colors.black)
- term.clear()
- center(2, "Deposit for #" .. account, colors.white)
- local y = 4
- for _, it in ipairs(items) do
- center(y, it.n .. " " .. NICE[it.kind] .. " = $" .. it.value, colors.lightGray)
- y = y + 1
- end
- center(y + 1, "Total: $" .. total, colors.yellow)
- local accept = button(math.floor(w / 2) - 11, y + 3, "Accept", colors.green)
- local cancel = button(math.floor(w / 2) + 3, y + 3, "Cancel", colors.red)
- while true do
- local _, _, mx, my = os.pullEvent("mouse_click")
- if hit(accept, mx, my) then break end
- if hit(cancel, mx, my) then return nil end
- end
- center(y + 5, "Processing...", colors.gray)
- pcall(acceptItems)
- local resp, err
- for _ = 1, 3 do
- resp, err = request({ type = "deposit", account = account, amount = total })
- if resp then break end
- sleep(1)
- end
- if resp and resp.ok then
- screenMsg("Deposited!", {
- "$" .. total .. " added.",
- "New balance: $" .. (resp.balance or "?"),
- }, { { "OK", colors.green } })
- return resp.balance
- end
- screenMsg("Bank Offline!", {
- "Items were taken but",
- "not credited. Tell an",
- "admin: #" .. account .. " +$" .. total,
- }, { { "OK", colors.red } })
- return nil
- end
- local function accountScreen(account, info)
- local balance = info.balance or 0
- local label = info.label
- while true do
- term.setBackgroundColor(colors.black)
- term.clear()
- center(2, "#" .. account .. (label and (" " .. label) or ""), colors.white)
- center(4, "Balance: $" .. balance, colors.yellow)
- local bw, bh = 25, 5
- local bx = math.floor((w - bw) / 2) + 1
- local by = 7
- fill(bx, by, bw, bh, colors.green)
- term.setCursorPos(bx + math.floor((bw - 13) / 2), by + 2)
- term.setTextColor(colors.white)
- term.write("DEPOSIT ITEMS")
- fill(w - 7, h - 1, 6, 1, colors.gray)
- term.setCursorPos(w - 6, h - 1)
- term.setTextColor(colors.white)
- term.write("Done")
- center(h, "Insert items into the chest first", colors.gray)
- local t = os.startTimer(IDLE_TIME)
- local e, a, b, c = os.pullEvent()
- if e == "timer" and a == t then
- return
- elseif e == "mouse_click" then
- if b >= bx and b <= bx + bw - 1 and c >= by and c <= by + bh - 1 then
- local nb = depositFlow(account)
- if nb then balance = nb end
- elseif c == h - 1 and b >= w - 7 then
- return
- end
- end
- end
- end
- --------------------------------------------------------------- run --
- while true do
- idleScreen()
- local account = keypad("Enter your pocket ID")
- if account then
- term.setBackgroundColor(colors.black)
- term.clear()
- center(9, "Checking...", colors.gray)
- local info, err = request({ type = "info", account = account })
- if info and info.ok then
- accountScreen(account, info)
- else
- screenMsg("No Bank", { tostring((info and info.err) or err or "Offline") },
- { { "OK", colors.red } })
- end
- end
- end
- ]==],
- ["update.lua"] = [==[
- -- PocketOS updater (command line).
- -- For the bank and tracker computers, which boot into their role
- -- program instead of PocketOS and so have no Settings > Software
- -- Update screen. Hold Ctrl+T to reach the shell, then run:
- -- update <pastebin-code-or-url>
- -- With no argument it uses the 'update' link from config.lua.
- -- Reinstalls PocketOS in place; config.lua (bank/tracker IDs) is kept.
- local args = { ... }
- local src = args[1]
- if not src then
- local ok, cfg = pcall(dofile, "/pocketos/config.lua")
- if ok and type(cfg) == "table" and type(cfg.update) == "string" then
- src = cfg.update
- end
- end
- if not src then
- write("Installer pastebin code or URL: ")
- src = read()
- end
- src = (src or ""):gsub("%s", "")
- if src == "" then
- print("Nothing to fetch.")
- return
- end
- if not http then
- print("HTTP is disabled in the server config.")
- return
- end
- local url = src:match("^https?://") and src or ("https://pastebin.com/raw/" .. src)
- print("Downloading installer...")
- local res = http.get(url)
- if not res then
- print("Download failed - check the code/URL.")
- return
- end
- local body = res.readAll() or ""
- res.close()
- if not body:find("PocketOS installer", 1, true) then
- print("That doesn't look like a PocketOS installer.")
- return
- end
- local fn, err = load(body, "installer", "t", _ENV)
- if not fn then
- print("Installer is broken: " .. tostring(err))
- return
- end
- print("Installing (config is kept)...")
- fn() -- writes files and reboots
- ]==],
- ["pocketos/config.lua"] = [==[
- -- PocketOS configuration.
- -- Safe to edit (run: edit /pocketos/config.lua). Installs and updates
- -- never overwrite this file once it exists.
- return {
- -- Location tracking: set this to a computer ID (a number, e.g. 5)
- -- and this device will send its GPS position to that computer every
- -- few seconds. The computer with that ID automatically boots as the
- -- tracker hub instead of PocketOS. Set to nil to disable.
- -- Needs wireless modems and a GPS host cluster in range.
- tracker = 7,
- -- Seconds between location reports.
- trackEvery = 15,
- -- Where Software Update looks for new versions: a pastebin code in
- -- quotes (e.g. "AbC123Xy") or a full URL to an installer. Settings >
- -- Software Update comes prefilled with this, so updating is one tap.
- update = "kwgdK8Ps",
- -- Which Groq model the AI app talks to.
- aiModel = "llama-3.3-70b-versatile",
- -- Computer ID of the bank server (see bank.lua). That computer
- -- auto-boots as the bank; pockets and ATMs will contact it
- -- directly. nil = find the bank automatically by rednet lookup.
- bank = 20,
- -- ADMIN SAFETY. Only these computer IDs may use developer powers -
- -- remote shell, ban/unban, and the Fleet/Ledger views. Every other
- -- device is ignored even if it somehow sends those commands. Keep
- -- this list short and trusted.
- admins = { 11, 18 },
- -- Online music: URL of the PocketOS music server (see musicserver.py).
- -- e.g. "http://mybox.example.com:8080". When set, the Music app shows
- -- a "Search online music" option that streams full-quality audio.
- -- nil = disabled. Note: CC: Tweaked blocks private/LAN addresses by
- -- default, so host it publicly or allow the address in the CC config.
- musicServer = "https://pocketos-web-1.onrender.com",
- }
- ]==],
- ["pocketos/main.lua"] = [==[
- -- PocketOS ------------------------------------------------------------
- -- A tiny Apple-flavoured OS for CC: Tweaked advanced pocket computers.
- -- Booted by /startup.lua. Apps live in /pocketos/apps.
- local ROOT = "/pocketos"
- local ui = dofile(ROOT .. "/lib/ui.lua")
- math.randomseed((os.epoch and os.epoch("utc") or os.time() * 1000) + os.getComputerID())
- ---------------------------------------------------------- shared state --
- local ctx = {
- version = "3.4",
- protocol = "pocketos",
- dataDir = "/.pocketos",
- messages = {},
- modem = false,
- }
- function ctx.load(name, default)
- local path = ctx.dataDir .. "/" .. name
- if fs.exists(path) then
- local f = fs.open(path, "r")
- local v = textutils.unserialize(f.readAll() or "")
- f.close()
- if v ~= nil then return v end
- end
- return default
- end
- function ctx.save(name, value)
- if not fs.exists(ctx.dataDir) then fs.makeDir(ctx.dataDir) end
- local f = fs.open(ctx.dataDir .. "/" .. name, "w")
- f.write(textutils.serialize(value))
- f.close()
- end
- function ctx.addMessage(m)
- table.insert(ctx.messages, m)
- while #ctx.messages > 100 do table.remove(ctx.messages, 1) end
- ctx.save("messages", ctx.messages)
- end
- function ctx.unreadCount()
- local n = 0
- for _, m in ipairs(ctx.messages) do
- if m.dir == "in" and not m.read then n = n + 1 end
- end
- return n
- end
- -- Save songs from a file_transfer event (files drag-and-dropped onto the
- -- Minecraft window, CC: Tweaked 1.101+). Returns how many were saved.
- function ctx.importFiles(transfer)
- local saved, skipped = 0, 0
- if not transfer or not transfer.getFiles then return 0, 0 end
- if not fs.exists("/music") then fs.makeDir("/music") end
- for _, file in ipairs(transfer.getFiles()) do
- local name = (file.getName() or "song"):gsub("[^%w%._%- ]", "")
- local lower = name:lower()
- if lower:match("%.nbs$") or lower:match("%.mid$") or lower:match("%.midi$") then
- local ext = name:match("(%.[^%.]+)$")
- local path = "/music/" .. name
- local n = 1
- while fs.exists(path) do
- n = n + 1
- path = "/music/" .. name:gsub("%.[^%.]+$", "") .. n .. ext
- end
- local data = file.readAll() or ""
- local f = fs.open(path, "wb")
- f.write(data)
- f.close()
- saved = saved + 1
- else
- skipped = skipped + 1
- end
- pcall(file.close)
- end
- return saved, skipped
- end
- ctx.messages = ctx.load("messages", {})
- do -- user-editable config; survives OS updates
- local ok, cfg = pcall(dofile, ROOT .. "/config.lua")
- ctx.config = (ok and type(cfg) == "table") and cfg or {}
- end
- -- shell-provided require, for rom modules like cc.audio.dfpwm
- ctx.require = require
- ctx.shell = shell
- -- developer mode (unlocked by tapping Computer ID in Settings)
- ctx.dev = ctx.load("devmode", false) and true or false
- -- ban state, pushed by a developer; { reason, by, at } or nil
- ctx.banned = ctx.load("ban", nil)
- if type(ctx.banned) ~= "table" then ctx.banned = nil end
- -- clock preferences (set in Settings > Date & Time)
- ui.tzOffset = tonumber(ctx.load("tz_offset", 0)) or 0
- ui.time24 = ctx.load("time_24h", true) ~= false
- -- Admin safety: only these IDs may use developer powers. Defaults to a
- -- fixed pair if config.lua doesn't list any (keeps old installs safe).
- ctx.admins = (ctx.config and type(ctx.config.admins) == "table")
- and ctx.config.admins or { 11, 18 }
- function ctx.isAdmin(id)
- for _, a in ipairs(ctx.admins) do
- if tonumber(a) == tonumber(id) then return true end
- end
- return false
- end
- for _, side in ipairs(peripheral.getNames()) do
- if peripheral.getType(side) == "modem" then
- if pcall(rednet.open, side) then ctx.modem = true end
- end
- end
- ---------------------------------------------------------- home screen --
- local apps = {
- { id = "messages", name = "Messages", color = colors.green, glyph = "\7\7\7" },
- { folder = true, name = "Games" },
- { id = "music", name = "Music", color = colors.red, glyph = "\14" },
- { id = "web", name = "Web", color = colors.blue, glyph = "G" },
- { id = "ai", name = "AI", color = colors.magenta, glyph = "\1" },
- { id = "bank", name = "Bank", color = colors.black, glyph = "$" },
- { id = "market", name = "Market", color = colors.purple, glyph = "%" },
- { id = "maps", name = "Maps", color = colors.lime, glyph = "\24" },
- { id = "gps", name = "GPS", color = colors.orange, glyph = "\4" },
- { id = "settings", name = "Settings", color = colors.gray, glyph = "\15" },
- }
- local devApp = { id = "dev", name = "Dev", color = colors.cyan, glyph = "\187" }
- local function homeApps()
- -- Dev app appears only on admin devices with developer mode on.
- if not (ctx.dev and ctx.isAdmin(os.getComputerID())) then return apps end
- local list = {}
- for _, a in ipairs(apps) do list[#list + 1] = a end
- table.insert(list, #list, devApp) -- just before Settings
- return list
- end
- local games = {
- { id = "snake", name = "Snake", color = colors.lime, glyph = "~ \7", dir = "games" },
- { id = "blocks", name = "Blocks", color = colors.purple, glyph = "##", dir = "games" },
- { id = "match", name = "Match", color = colors.pink, glyph = "\3", dir = "games" },
- { id = "mines", name = "Mines", color = colors.black, glyph = "*", dir = "games" },
- { id = "tetris", name = "Tetris", color = colors.cyan, glyph = "T", dir = "games" },
- { id = "solitaire", name = "Cards", color = colors.green, glyph = "\6\3", dir = "games" },
- { id = "flappy", name = "Flappy", color = colors.yellow, glyph = "\16", dir = "games" },
- { id = "sudoku", name = "Sudoku", color = colors.lightBlue, glyph = "9", dir = "games" },
- }
- local function drawTile(app, x, y, iw, ih)
- local bg = app.folder and colors.lightGray or app.color
- ui.fill(x, y, iw, ih, bg)
- -- knock out the four corners so the icon reads as rounded
- ui.text(x, y, " ", colors.white, colors.white)
- ui.text(x + iw - 1, y, " ", colors.white, colors.white)
- ui.text(x, y + ih - 1, " ", colors.white, colors.white)
- ui.text(x + iw - 1, y + ih - 1, " ", colors.white, colors.white)
- if app.folder then
- -- mini game icons inside the folder
- ui.fill(x + 2, y + 1, 1, 1, colors.lime)
- ui.fill(x + 4, y + 1, 1, 1, colors.cyan)
- ui.fill(x + 2, y + 2, 1, 1, colors.pink)
- ui.fill(x + 4, y + 2, 1, 1, colors.orange)
- else
- ui.text(x + math.floor((iw - #app.glyph) / 2), y + 1, app.glyph,
- colors.white, bg)
- end
- ui.text(x + math.floor((iw - #app.name) / 2), y + ih, app.name,
- colors.gray, colors.white)
- end
- local function drawGrid(list, regions, rowPitch)
- local iw, ih = 7, 3
- local gap = math.floor((ui.w - 3 * iw) / 4)
- local unread = ctx.unreadCount()
- for i, app in ipairs(list) do
- local col = (i - 1) % 3
- local row = math.floor((i - 1) / 3)
- local x = gap + col * (iw + gap) + 1
- local y = 3 + row * rowPitch
- drawTile(app, x, y, iw, ih)
- if app.id == "messages" and unread > 0 then
- ui.text(x + iw - 1, y, unread > 9 and "+" or tostring(unread),
- colors.white, colors.red)
- end
- regions[#regions + 1] = { x1 = x, y1 = y, x2 = x + iw - 1, y2 = y + ih, app = app }
- end
- end
- local regions = {}
- local function drawHome()
- ui.clear(colors.white)
- local name = os.getComputerLabel() or ("Pocket #" .. os.getComputerID())
- ui.text(2, 1, name:sub(1, 12), colors.gray, colors.white)
- local clk = ui.clock()
- -- signal dot (green with modem, red without) then the clock
- ui.text(ui.w - #clk - 2, 1, "\7", ctx.modem and colors.green or colors.red, colors.white)
- ui.text(ui.w - #clk, 1, clk, colors.black, colors.white)
- regions = {}
- drawGrid(homeApps(), regions, 4)
- -- iPhone-style home indicator
- ui.fill(math.floor(ui.w / 2) - 3, ui.h - 1, 8, 1, colors.lightGray)
- end
- local function runApp(app)
- local ok, err = pcall(function()
- local sub = app.dir and (app.dir .. "/") or ""
- local mod = dofile(ROOT .. "/apps/" .. sub .. app.id .. ".lua")
- mod.run(ui, ctx)
- end)
- if not ok then
- local msg = tostring(err)
- if msg:match("Terminated") then error(err, 0) end
- ui.clear(colors.white)
- ui.center(3, app.name .. " crashed", colors.red, colors.white)
- local y = 5
- for _, line in ipairs(ui.wrap(msg, ui.w - 4)) do
- if y > ui.h - 3 then break end
- ui.text(3, y, line, colors.gray, colors.white)
- y = y + 1
- end
- ui.center(ui.h - 1, "Tap to go home", colors.blue, colors.white)
- os.pullEvent("mouse_click")
- end
- end
- local function gamesScreen()
- while true do
- ui.clear(colors.white)
- ui.topBar("Games")
- local gregions = {}
- drawGrid(games, gregions, 4)
- ui.fill(math.floor(ui.w / 2) - 3, ui.h - 1, 8, 1, colors.lightGray)
- local e, a, b, c = os.pullEvent()
- if e == "mouse_click" then
- if ui.isBack(b, c) then return end
- for _, r in ipairs(gregions) do
- if b >= r.x1 and b <= r.x2 and c >= r.y1 and c <= r.y2 then
- runApp(r.app)
- break
- end
- end
- elseif e == "term_resize" then
- ui.w, ui.h = term.getSize()
- end
- end
- end
- -- Full-screen lock shown while this device is banned. Blocks until an
- -- unban arrives (queued as "pocketos_unban"); swallows other input,
- -- including Ctrl+T (the boot loop re-locks if a coroutine is killed).
- local function banScreen()
- while ctx.banned do
- ui.fill(1, 1, ui.w, ui.h, colors.red)
- ui.center(3, "\4 BANNED \4", colors.white, colors.red)
- ui.center(5, "This device is locked.", colors.white, colors.red)
- ui.center(7, "Reason:", colors.white, colors.red)
- local y = 8
- for _, l in ipairs(ui.wrap(ctx.banned.reason or "No reason given", ui.w - 2)) do
- if y > ui.h - 4 then break end
- ui.center(y, l, colors.white, colors.red)
- y = y + 1
- end
- if ctx.banned.by then
- ui.center(ui.h - 2, "Banned by #" .. tostring(ctx.banned.by), colors.white, colors.red)
- end
- ui.center(ui.h, "Contact an admin", colors.white, colors.red)
- local e = os.pullEventRaw()
- if e == "pocketos_unban" then return end
- if e == "term_resize" then ui.w, ui.h = term.getSize() end
- end
- end
- local function home()
- while true do
- if ctx.banned then banScreen() end
- drawHome()
- local t = os.startTimer(15) -- keep the clock fresh
- while true do
- local e, a, b, c = os.pullEvent()
- if e == "pocketos_ban" then
- break -- outer loop shows the ban screen
- elseif e == "mouse_click" then
- local tapped
- for _, r in ipairs(regions) do
- if b >= r.x1 and b <= r.x2 and c >= r.y1 and c <= r.y2 then
- tapped = r.app
- end
- end
- if tapped then
- if tapped.folder then
- gamesScreen()
- else
- runApp(tapped)
- end
- break
- end
- elseif e == "timer" and a == t then
- break
- elseif e == "file_transfer" then
- local saved = ctx.importFiles(a)
- local msg = saved > 0
- and ("Imported " .. saved .. " song" .. (saved == 1 and "" or "s"))
- or "Only .nbs/.mid works"
- ui.text(math.floor((ui.w - #msg - 2) / 2) + 1, ui.h - 3,
- " " .. msg .. " ", colors.white,
- saved > 0 and colors.green or colors.gray)
- sleep(1.2)
- break
- elseif e == "pocketos_message" or e == "term_resize" then
- if e == "term_resize" then ui.w, ui.h = term.getSize() end
- break
- end
- end
- end
- end
- ------------------------------------------------------ message listener --
- local function listener()
- while true do
- local id, msg = rednet.receive(ctx.protocol)
- if type(msg) == "table" and type(msg.text) == "string" then
- ctx.addMessage({
- peer = msg.bc and "all" or id,
- from = id,
- dir = "in",
- text = msg.text:sub(1, 200),
- label = type(msg.label) == "string" and msg.label:sub(1, 12) or nil,
- time = os.epoch and os.epoch("utc") or 0,
- read = false,
- })
- os.queueEvent("pocketos_message")
- end
- end
- end
- ----------------------------------------------------- location service --
- -- The one place gps.locate is called: keeps ctx.fix fresh for the GPS
- -- app (two coroutines locating at once trample the shared GPS channel)
- -- and reports our position to the tracker computer from config.lua.
- -- Apps can queue a "pocketos_locate" event to request a fix right away.
- local function locator()
- if not ctx.modem then
- while true do sleep(3600) end
- end
- local target = tonumber(ctx.config and ctx.config.tracker)
- if target == os.getComputerID() then target = nil end
- local every = tonumber(ctx.config and ctx.config.trackEvery) or 15
- local lastSent = -math.huge
- while true do
- local x, y, z = gps.locate(2)
- if x then
- ctx.fix = {
- x = math.floor(x + 0.5),
- y = math.floor(y + 0.5),
- z = math.floor(z + 0.5),
- at = os.clock(),
- }
- os.queueEvent("pocketos_fix")
- if target and os.clock() - lastSent >= every then
- rednet.send(target, {
- id = os.getComputerID(),
- label = os.getComputerLabel(),
- x = ctx.fix.x,
- y = ctx.fix.y,
- z = ctx.fix.z,
- }, "pocketos.track")
- lastSent = os.clock()
- end
- end
- local t = os.startTimer(5)
- while true do
- local e, id = os.pullEvent()
- if e == "timer" and id == t then
- break
- elseif e == "pocketos_locate" then
- os.cancelTimer(t)
- break
- end
- end
- end
- end
- -------------------------------------------------- remote shell server --
- -- A virtual terminal that records everything written to it, so a
- -- command's output can be captured and sent back to a developer.
- local function captureTerm()
- local lines = { "" }
- local cx, cy = 1, 1
- local t = {}
- local function ensure(y)
- for i = #lines + 1, y do lines[i] = "" end
- end
- function t.write(s)
- s = tostring(s)
- ensure(cy)
- local line = lines[cy]
- if #line < cx - 1 then line = line .. string.rep(" ", cx - 1 - #line) end
- lines[cy] = line:sub(1, cx - 1) .. s .. line:sub(cx + #s)
- cx = cx + #s
- end
- function t.blit(s) t.write(s) end
- function t.getCursorPos() return cx, cy end
- function t.setCursorPos(x, y) cx, cy = math.floor(x), math.floor(y); ensure(cy) end
- function t.getSize() return 51, 19 end
- function t.getTextColor() return colors.white end
- function t.getBackgroundColor() return colors.black end
- function t.isColor() return true end
- function t.scroll(n)
- for _ = 1, n do table.remove(lines, 1); lines[#lines + 1] = "" end
- end
- function t.clear() lines = { "" }; cx, cy = 1, 1 end
- function t.clearLine() lines[cy] = "" end
- function t.setCursorBlink() end
- function t.setTextColor() end
- function t.setBackgroundColor() end
- function t.getPaletteColor() return 1, 1, 1 end
- -- British spellings CC also expects
- t.setTextColour = t.setTextColor
- t.setBackgroundColour = t.setBackgroundColor
- t.getTextColour = t.getTextColor
- t.getBackgroundColour = t.getBackgroundColor
- t.isColour = t.isColor
- t.getPaletteColour = t.getPaletteColor
- local function text()
- local out = {}
- for _, l in ipairs(lines) do out[#out + 1] = (l:gsub("%s+$", "")) end
- while #out > 0 and out[#out] == "" do out[#out] = nil end
- return table.concat(out, "\n")
- end
- return t, text
- end
- local function runCaptured(cmd)
- local cap, getText = captureTerm()
- local prev = term.redirect(cap)
- local finished = false
- pcall(parallel.waitForAny,
- function()
- pcall(ctx.shell and ctx.shell.run or shell.run, cmd)
- finished = true
- end,
- function() sleep(8) end) -- guard against interactive/hanging programs
- term.redirect(prev)
- local out = getText()
- if not finished then
- out = out .. (out == "" and "" or "\n") .. "[timed out after 8s]"
- end
- if out == "" then out = "(no output)" end
- return out
- end
- -- Answers developer requests from any device running the Dev app.
- -- Every PocketOS device is remotely reachable, dev mode or not.
- local function devServer()
- while true do
- local id, msg = rednet.receive("pocketos.dev")
- -- privileged commands are honoured only from admin IDs
- if type(msg) == "table" and ctx.isAdmin(id) then
- if msg.type == "ping" then
- rednet.send(id, {
- ok = true, label = os.getComputerLabel(), id = os.getComputerID(),
- version = ctx.version,
- }, "pocketos.dev")
- elseif msg.type == "exec" and type(msg.cmd) == "string" then
- local out = runCaptured(msg.cmd:sub(1, 200))
- rednet.send(id, { ok = true, output = out:sub(1, 4000) }, "pocketos.dev")
- elseif msg.type == "ban" then
- ctx.banned = {
- reason = tostring(msg.reason or "No reason given"):sub(1, 200),
- by = id,
- at = os.epoch and os.epoch("utc") or 0,
- }
- ctx.save("ban", ctx.banned)
- rednet.send(id, { ok = true, banned = true }, "pocketos.dev")
- sleep(0.3) -- let the reply leave before we reboot
- os.reboot() -- guarantees the lock even mid-app
- elseif msg.type == "unban" then
- ctx.banned = nil
- local p = ctx.dataDir .. "/ban"
- if fs.exists(p) then fs.delete(p) end
- os.queueEvent("pocketos_unban")
- rednet.send(id, { ok = true, banned = false }, "pocketos.dev")
- elseif msg.type == "banstate" then
- rednet.send(id, {
- ok = true, banned = ctx.banned ~= nil,
- reason = ctx.banned and ctx.banned.reason,
- }, "pocketos.dev")
- end
- end
- end
- end
- ------------------------------------------------------------------ boot --
- local function splash()
- ui.clear(colors.white)
- ui.center(math.floor(ui.h / 2) - 1, "PocketOS", colors.black, colors.white)
- local x0 = math.floor((ui.w - 5) / 2) + 1
- local y0 = math.floor(ui.h / 2) + 1
- ui.text(x0, y0, "\7 \7 \7", colors.lightGray, colors.white)
- for i = 0, 2 do
- sleep(0.25)
- ui.text(x0 + i * 2, y0, "\7", colors.black, colors.white)
- end
- sleep(0.2)
- end
- ui.init()
- splash()
- while true do
- local ok, err = pcall(parallel.waitForAny, home, listener, locator, devServer)
- if ctx.banned then
- -- A banned device can't escape to the shell: if Ctrl+T killed a
- -- background coroutine, loop straight back into the lock screen.
- sleep(0.1)
- else
- ui.restore()
- if not ok and not tostring(err):match("Terminated") then
- error(err, 0)
- end
- break
- end
- end
- ]==],
- ["pocketos/lib/ui.lua"] = [==[
- -- PocketOS shared UI helpers + Apple-flavoured colour palette.
- local ui = {}
- ui.w, ui.h = term.getSize()
- -- Clock preferences, set from saved settings at boot (see main.lua).
- ui.tzOffset = 0 -- hours offset from UTC
- ui.time24 = true -- 24-hour vs 12-hour display
- local PALETTE = {
- [colors.white] = 0xF5F5F7, -- iOS background
- [colors.lightGray] = 0xDDDDE2,
- [colors.gray] = 0x8E8E93,
- [colors.black] = 0x1D1D1F,
- [colors.blue] = 0x0A84FF,
- [colors.green] = 0x34C759,
- [colors.lime] = 0x84E36B,
- [colors.red] = 0xFF3B30,
- [colors.orange] = 0xFF9500,
- [colors.yellow] = 0xFFCC00,
- [colors.purple] = 0xAF52DE,
- [colors.cyan] = 0x5AC8FA,
- [colors.pink] = 0xFF7AA2,
- [colors.magenta] = 0x5E5CE6, -- iOS indigo (AI app)
- [colors.lightBlue] = 0x74A7FF,
- }
- function ui.init()
- ui.w, ui.h = term.getSize()
- if term.setPaletteColour then
- for c, hex in pairs(PALETTE) do
- term.setPaletteColour(c, hex)
- end
- end
- term.setCursorBlink(false)
- end
- function ui.restore()
- if term.setPaletteColour and term.nativePaletteColour then
- for c in pairs(PALETTE) do
- term.setPaletteColour(c, term.nativePaletteColour(c))
- end
- end
- term.setBackgroundColor(colors.black)
- term.setTextColor(colors.white)
- term.clear()
- term.setCursorPos(1, 1)
- end
- function ui.fill(x, y, w, h, bg)
- term.setBackgroundColor(bg)
- local line = string.rep(" ", w)
- for i = 0, h - 1 do
- term.setCursorPos(x, y + i)
- term.write(line)
- end
- end
- function ui.clear(bg)
- ui.fill(1, 1, ui.w, ui.h, bg or colors.white)
- end
- function ui.text(x, y, s, fg, bg)
- if fg then term.setTextColor(fg) end
- if bg then term.setBackgroundColor(bg) end
- term.setCursorPos(x, y)
- term.write(s)
- end
- function ui.center(y, s, fg, bg)
- ui.text(math.floor((ui.w - #s) / 2) + 1, y, s, fg, bg)
- end
- function ui.clock()
- local ok, t = pcall(os.time, "utc")
- if not ok or type(t) ~= "number" then t = os.time() end
- t = (t + (ui.tzOffset or 0)) % 24
- return textutils.formatTime(t, ui.time24 ~= false)
- end
- -- Top bar: "<" back button on the left, centred title, clock (or custom
- -- text) on the right. Back hit test via ui.isBack(x, y).
- function ui.topBar(title, right, rightColor)
- ui.fill(1, 1, ui.w, 1, colors.white)
- ui.text(1, 1, " < ", colors.blue, colors.white)
- ui.center(1, title, colors.black, colors.white)
- right = right or ui.clock()
- ui.text(ui.w - #right, 1, right, rightColor or colors.gray, colors.white)
- end
- function ui.isBack(x, y)
- return y == 1 and x <= 3
- end
- function ui.button(x, y, label, bg, fg)
- ui.text(x, y, " " .. label .. " ", fg or colors.white, bg or colors.blue)
- return { x1 = x, y1 = y, x2 = x + #label + 1, y2 = y }
- end
- function ui.hit(r, x, y)
- return r and x >= r.x1 and x <= r.x2 and y >= r.y1 and y <= r.y2
- end
- -- A filled panel with a soft drop shadow and rounded corners, for tiles,
- -- cards and dialogs. `under` is the colour behind the panel that shows
- -- through the rounded corners and the shadow gaps (default page white).
- function ui.card(x, y, w, h, bg, under)
- under = under or colors.white
- if w >= 3 and h >= 2 then
- ui.fill(x + 1, y + h, w, 1, colors.lightGray) -- shadow below
- ui.fill(x + w, y + 1, 1, h, colors.lightGray) -- shadow right
- end
- ui.fill(x, y, w, h, bg)
- -- knock out the four corners so the panel reads as rounded
- ui.text(x, y, " ", under, under)
- ui.text(x + w - 1, y, " ", under, under)
- ui.text(x, y + h - 1, " ", under, under)
- ui.text(x + w - 1, y + h - 1, " ", under, under)
- end
- -- Touch-friendly single-line text input. Returns the typed string on
- -- Enter, or nil if the user taps the back arrow (top-left) to cancel -
- -- so every screen using it keeps a working back button. Supports
- -- Backspace and Ctrl+V paste. opts: default, width, fg, bg, mask.
- function ui.input(x, y, opts)
- opts = opts or {}
- local s = tostring(opts.default or "")
- local width = opts.width or (ui.w - x)
- local bg = opts.bg or colors.lightGray
- local fg = opts.fg or colors.black
- while true do
- local shown = opts.mask and string.rep("*", #s) or s
- if #shown > width - 1 then shown = shown:sub(#shown - (width - 2)) end
- term.setBackgroundColor(bg)
- term.setTextColor(fg)
- term.setCursorPos(x, y)
- term.write(shown .. string.rep(" ", width - #shown))
- term.setCursorPos(x + #shown, y)
- term.setCursorBlink(true)
- local e, a, b, c = os.pullEvent()
- if e == "char" then
- s = s .. a
- elseif e == "paste" then
- s = s .. a
- elseif e == "key" then
- if a == keys.enter or a == keys.numPadEnter then
- term.setCursorBlink(false)
- return s
- elseif a == keys.backspace then
- s = s:sub(1, -2)
- end
- elseif e == "mouse_click" and ui.isBack(b, c) then
- term.setCursorBlink(false)
- return nil
- end
- end
- end
- function ui.wrap(s, width)
- local lines, line = {}, ""
- for word in tostring(s):gmatch("%S+") do
- if #line == 0 then
- line = word
- elseif #line + 1 + #word <= width then
- line = line .. " " .. word
- else
- lines[#lines + 1] = line
- line = word
- end
- while #line > width do
- lines[#lines + 1] = line:sub(1, width)
- line = line:sub(width + 1)
- end
- end
- if #line > 0 then lines[#lines + 1] = line end
- if #lines == 0 then lines = { "" } end
- return lines
- end
- -- Modal card with buttons. buttons = { {label, color}, ... }
- -- Returns the index of the tapped button (Enter picks button 1).
- function ui.dialog(title, lines, buttons)
- local w = 22
- local h = #lines + 5
- local x = math.floor((ui.w - w) / 2) + 1
- local y = math.floor((ui.h - h) / 2) + 1
- -- soft drop shadow
- ui.fill(x + 1, y + h, w, 1, colors.gray)
- ui.fill(x + w, y + 1, 1, h, colors.gray)
- -- white body with a coloured title bar
- ui.fill(x, y, w, h, colors.white)
- ui.fill(x, y, w, 1, colors.blue)
- ui.text(x, y, " ", colors.white, colors.white) -- round top corners
- ui.text(x + w - 1, y, " ", colors.white, colors.white)
- ui.text(x + math.floor((w - #title) / 2), y, title, colors.white, colors.blue)
- for i, s in ipairs(lines) do
- ui.text(x + math.floor((w - #s) / 2), y + 1 + i, s, colors.gray, colors.white)
- end
- local regions = {}
- local total = #buttons - 1
- for _, b in ipairs(buttons) do total = total + #b[1] + 2 end
- local bx = x + math.floor((w - total) / 2)
- local by = y + h - 2
- for i, b in ipairs(buttons) do
- regions[i] = ui.button(bx, by, b[1], b[2])
- bx = bx + #b[1] + 3
- end
- while true do
- local e, p1, p2, p3 = os.pullEvent()
- if e == "mouse_click" then
- for i, r in ipairs(regions) do
- if ui.hit(r, p2, p3) then return i end
- end
- elseif e == "key" and p1 == keys.enter then
- return 1
- end
- end
- end
- return ui
- ]==],
- ["pocketos/lib/nbs.lua"] = [==[
- -- PocketOS lib: NBS (Note Block Studio) parsing + speaker playback.
- -- Supports classic .nbs and OpenNBS versions 1-5.
- local nbs = {}
- local INSTRUMENTS = {
- [0] = "harp", "bass", "basedrum", "snare", "hat", "guitar", "flute",
- "bell", "chime", "xylophone", "iron_xylophone", "cow_bell",
- "didgeridoo", "bit", "banjo", "pling",
- }
- function nbs.parse(path)
- local f = fs.open(path, "rb")
- if not f then return nil, "Can't open file" end
- local function u8()
- return f.read() or 0
- end
- local function u16()
- return u8() + u8() * 256
- end
- local function u32()
- return u16() + u16() * 65536
- end
- local function str()
- local n = u32()
- if n <= 0 or n > 100000 then return "" end
- return f.read(n) or ""
- end
- local length = u16()
- local version = 0
- if length == 0 then -- OpenNBS marker
- version = u8()
- u8() -- vanilla instrument count
- if version >= 3 then length = u16() end
- end
- u16() -- layer count
- local name = str()
- local author = str()
- local origAuthor = str()
- str() -- description
- local tempo = u16() -- (ticks per second) * 100
- u8() -- auto save
- u8() -- auto save duration
- u8() -- time signature
- u32() u32() u32() u32() u32() -- editor stats
- str() -- imported file name
- if version >= 4 then
- u8() -- loop on
- u8() -- max loop count
- u16() -- loop start tick
- end
- local groups = {}
- local tick = -1
- local maxTick = 0
- while true do
- local jump = u16()
- if jump == 0 then break end
- tick = tick + jump
- local notes = {}
- while true do
- local lj = u16()
- if lj == 0 then break end
- local inst = u8()
- local key = u8()
- local vel = 100
- if version >= 4 then
- vel = u8()
- u8() -- panning
- u16() -- fine pitch
- end
- if #notes < 8 then -- speakers max out at 8 notes per tick
- notes[#notes + 1] = { inst = inst, key = key, vel = vel }
- end
- end
- groups[#groups + 1] = { tick = tick, notes = notes }
- maxTick = tick
- end
- f.close()
- if tempo == 0 then tempo = 1000 end
- if name == "" then
- name = fs.getName(path):gsub("%.nbs$", "")
- end
- if author == "" then author = origAuthor end
- return {
- name = name,
- author = author,
- tempo = tempo / 100, -- NBS ticks per second
- length = math.max(length, maxTick + 1),
- groups = groups,
- }
- end
- -- Event-driven player. Feed every event to p.onEvent(e, id); it returns
- -- true when it consumed a timer event. Uses os.clock() so timing
- -- self-corrects instead of drifting.
- function nbs.newPlayer(song, speaker)
- local spt = 1 / song.tempo -- seconds per NBS tick
- local p = {
- index = 1,
- timer = nil,
- playing = false,
- pauseAt = 0,
- done = false,
- t0 = 0,
- }
- function p.duration()
- return song.length * spt
- end
- function p.elapsed()
- local t
- if p.playing then t = os.clock() - p.t0 else t = p.pauseAt end
- return math.max(0, math.min(t, p.duration()))
- end
- function p.progress()
- local d = p.duration()
- if d <= 0 then return 0 end
- return p.elapsed() / d
- end
- local function schedule()
- local g = song.groups[p.index]
- if not g then
- local left = p.duration() - (os.clock() - p.t0)
- if left > 0.05 then
- p.timer = os.startTimer(left)
- else
- p.playing = false
- p.done = true
- p.pauseAt = p.duration()
- p.timer = nil
- end
- return
- end
- local dt = g.tick * spt - (os.clock() - p.t0)
- p.timer = os.startTimer(math.max(0, dt))
- end
- function p.begin()
- p.done = false
- p.playing = true
- p.t0 = os.clock() - (p.pauseAt or 0)
- schedule()
- end
- function p.stop()
- if p.playing then p.pauseAt = os.clock() - p.t0 end
- p.playing = false
- if p.timer then os.cancelTimer(p.timer) end
- p.timer = nil
- end
- function p.restart()
- p.stop()
- p.index = 1
- p.pauseAt = 0
- p.done = false
- p.begin()
- end
- function p.onEvent(e, id)
- if e ~= "timer" or id ~= p.timer or not p.playing then return false end
- local now = os.clock() - p.t0
- local g = song.groups[p.index]
- if not g and now >= p.duration() - 0.01 then
- p.playing = false
- p.done = true
- p.pauseAt = p.duration()
- p.timer = nil
- return true
- end
- while g and g.tick * spt <= now + 0.01 do
- if speaker then
- for _, n in ipairs(g.notes) do
- -- MIDI songs carry instName/pitch directly; NBS
- -- notes get out-of-range keys folded by octaves
- local inst = n.instName or INSTRUMENTS[n.inst] or "harp"
- local pitch = n.pitch
- if not pitch then
- pitch = (n.key or 45) - 33
- while pitch < 0 do pitch = pitch + 12 end
- while pitch > 24 do pitch = pitch - 12 end
- end
- local vol = math.min(3, ((n.vel or 100) / 100) * 2)
- pcall(speaker.playNote, inst, vol, pitch)
- end
- end
- p.index = p.index + 1
- g = song.groups[p.index]
- end
- schedule()
- return true
- end
- return p
- end
- return nbs
- ]==],
- ["pocketos/lib/midi.lua"] = [==[
- -- PocketOS lib: MIDI (.mid) parsing for note block playback.
- -- Converts a Standard MIDI File into the same song table the NBS
- -- player uses. Notes are spread across bass/harp/bell instruments so
- -- about six octaves play correctly instead of note blocks' usual two;
- -- channel 10 percussion maps to basedrum/snare/hat.
- local midi = {}
- function midi.parse(path)
- local f = fs.open(path, "rb")
- if not f then return nil, "Can't open file" end
- local data = f.readAll() or ""
- f.close()
- if #data < 14 then return nil, "Not a MIDI file" end
- if #data > 400000 then return nil, "MIDI file too large" end
- local pos = 1
- local function u8()
- local b = data:byte(pos)
- pos = pos + 1
- return b or 0
- end
- local function u16() return u8() * 256 + u8() end
- local function u32() return u16() * 65536 + u16() end
- local function chunk(n)
- local s = data:sub(pos, pos + n - 1)
- pos = pos + n
- return s
- end
- local function vlq()
- local v = 0
- for _ = 1, 4 do
- local b = u8()
- v = v * 128 + (b % 128)
- if b < 128 then break end
- end
- return v
- end
- if chunk(4) ~= "MThd" then return nil, "Not a MIDI file" end
- local hlen = u32()
- chunk(2) -- format
- local ntracks = u16()
- local division = u16()
- if hlen > 6 then pos = pos + hlen - 6 end
- if division >= 0x8000 then return nil, "SMPTE MIDI unsupported" end
- if division == 0 then division = 96 end
- local events = {}
- local title
- for _ = 1, ntracks do
- if pos + 8 > #data then break end
- local id = chunk(4)
- local len = u32()
- local endPos = pos + len
- if id ~= "MTrk" then
- pos = endPos
- else
- local tick = 0
- local status = 0
- while pos < endPos do
- tick = tick + vlq()
- local b = u8()
- if b < 0x80 then
- if status < 0x80 then break end -- corrupt track
- pos = pos - 1
- b = status -- running status
- else
- status = b
- end
- local hi = math.floor(b / 16)
- if b == 0xFF then
- status = 0
- local mt = u8()
- local mlen = vlq()
- if mt == 0x51 and mlen == 3 then
- events[#events + 1] = {
- tick = tick,
- tempo = u8() * 65536 + u8() * 256 + u8(),
- }
- elseif mt == 0x03 and not title and mlen > 0 and mlen < 40 then
- title = chunk(mlen):gsub("[^%w%p ]", "")
- else
- pos = pos + mlen
- end
- elseif b == 0xF0 or b == 0xF7 then
- status = 0
- pos = pos + vlq()
- elseif hi == 9 then -- note on
- local note, vel = u8(), u8()
- if vel > 0 then
- events[#events + 1] = {
- tick = tick, ch = b % 16, note = note, vel = vel,
- }
- end
- elseif hi == 8 or hi == 10 or hi == 11 or hi == 14 then
- u8() u8()
- elseif hi == 12 or hi == 13 then
- u8()
- else
- break -- unexpected byte; skip rest of track
- end
- end
- pos = endPos
- end
- end
- if #events == 0 then return nil, "No notes found" end
- table.sort(events, function(a, b)
- if a.tick ~= b.tick then return a.tick < b.tick end
- return (a.tempo ~= nil) and (b.tempo == nil)
- end)
- -- walk events, converting MIDI ticks to seconds (honouring tempo
- -- changes), then to speaker game-ticks (20 per second)
- local byTick = {}
- local uspq = 500000 -- microseconds per quarter note (120 bpm)
- local lastTick, seconds = 0, 0
- local maxGt = 0
- for _, ev in ipairs(events) do
- seconds = seconds + (ev.tick - lastTick) * uspq / (division * 1000000)
- lastTick = ev.tick
- if ev.tempo then
- uspq = ev.tempo
- else
- local inst, pitch
- if ev.ch == 9 then -- percussion channel
- if ev.note == 35 or ev.note == 36 then
- inst, pitch = "basedrum", 6
- elseif ev.note == 38 or ev.note == 40 then
- inst, pitch = "snare", 12
- else
- inst, pitch = "hat", 12
- end
- else
- -- bass covers MIDI 30-54, harp 54-78, bell 78-102;
- -- fold anything outside by octaves
- local n = ev.note
- while n < 30 do n = n + 12 end
- while n > 102 do n = n - 12 end
- if n <= 54 then
- inst, pitch = "bass", n - 30
- elseif n <= 78 then
- inst, pitch = "harp", n - 54
- else
- inst, pitch = "bell", n - 78
- end
- end
- local gt = math.floor(seconds * 20 + 0.5)
- local g = byTick[gt]
- if not g then
- g = {}
- byTick[gt] = g
- end
- if #g < 8 then -- speakers play at most 8 notes per tick
- g[#g + 1] = {
- instName = inst,
- pitch = pitch,
- vel = math.floor(ev.vel * 100 / 127),
- }
- if gt > maxGt then maxGt = gt end
- end
- end
- end
- local ticks = {}
- for gt in pairs(byTick) do ticks[#ticks + 1] = gt end
- table.sort(ticks)
- local groups = {}
- for _, gt in ipairs(ticks) do
- groups[#groups + 1] = { tick = gt, notes = byTick[gt] }
- end
- local name = title
- if not name or name == "" then
- name = fs.getName(path):gsub("%.[^%.]+$", "")
- end
- return {
- name = name,
- author = "",
- tempo = 20, -- game-ticks per second
- length = maxGt + 10,
- groups = groups,
- }
- end
- return midi
- ]==],
- ["pocketos/apps/messages.lua"] = [==[
- -- PocketOS app: Messages - iMessage-style chat over rednet.
- -- Needs a wireless modem (use a Wireless Pocket Computer).
- -- Conversations are per computer ID; "all" broadcasts to everyone.
- local M = {}
- local ui, ctx
- local function fmtTime(t)
- if not t or t == 0 then return "" end
- local ok, s = pcall(os.date, "%H:%M", math.floor(t / 1000))
- if ok and type(s) == "string" then return s end
- return ""
- end
- local function peerName(peer, label)
- if peer == "all" then return "Everyone" end
- if label then return label .. " #" .. tostring(peer) end
- return "#" .. tostring(peer)
- end
- local function conversations()
- local map, list = {}, {}
- for i, m in ipairs(ctx.messages) do
- local key = tostring(m.peer)
- local cv = map[key]
- if not cv then
- cv = { peer = m.peer, unread = 0 }
- map[key] = cv
- list[#list + 1] = cv
- end
- cv.last = m
- cv.idx = i
- if m.label then cv.label = m.label end
- if m.dir == "in" and not m.read then cv.unread = cv.unread + 1 end
- end
- table.sort(list, function(a, b) return a.idx > b.idx end)
- return list
- end
- local function markRead(peer)
- local dirty = false
- for _, m in ipairs(ctx.messages) do
- if tostring(m.peer) == tostring(peer) and m.dir == "in" and not m.read then
- m.read = true
- dirty = true
- end
- end
- if dirty then ctx.save("messages", ctx.messages) end
- end
- local function sendTo(peer, text)
- local payload = { text = text, label = os.getComputerLabel(), bc = (peer == "all") }
- if peer == "all" then
- rednet.broadcast(payload, ctx.protocol)
- else
- rednet.send(tonumber(peer), payload, ctx.protocol)
- end
- ctx.addMessage({
- peer = peer, dir = "out", text = text, read = true,
- time = os.epoch and os.epoch("utc") or 0,
- })
- end
- local function titleFor(peer)
- if peer == "all" then return "Everyone" end
- local label
- for _, m in ipairs(ctx.messages) do
- if tostring(m.peer) == tostring(peer) and m.label then label = m.label end
- end
- return peerName(peer, label)
- end
- local function chatLines(peer)
- local lines = {}
- for _, m in ipairs(ctx.messages) do
- if tostring(m.peer) == tostring(peer) then
- if peer == "all" and m.dir == "in" then
- lines[#lines + 1] = {
- text = m.label or ("#" .. tostring(m.from or "?")),
- x = 2, fg = colors.gray, bg = colors.white,
- }
- end
- local wrapped = ui.wrap(m.text, 18)
- local width = 0
- for _, s in ipairs(wrapped) do width = math.max(width, #s) end
- for _, s in ipairs(wrapped) do
- local pad = " " .. s .. string.rep(" ", width - #s) .. " "
- if m.dir == "out" then
- lines[#lines + 1] = { text = pad, x = ui.w - #pad, fg = colors.white, bg = colors.blue }
- else
- lines[#lines + 1] = { text = pad, x = 2, fg = colors.black, bg = colors.lightGray }
- end
- end
- lines[#lines + 1] = { spacer = true }
- end
- end
- if #lines > 0 and lines[#lines].spacer then lines[#lines] = nil end
- return lines
- end
- local function drawChat(peer, scroll)
- ui.clear(colors.white)
- ui.topBar(titleFor(peer):sub(1, 14))
- local lines = chatLines(peer)
- local top, bottom = 2, ui.h - 2
- local yy = bottom
- local i = #lines - scroll
- while i >= 1 and yy >= top do
- local L = lines[i]
- if not L.spacer then
- ui.text(L.x, yy, L.text, L.fg, L.bg)
- end
- yy = yy - 1
- i = i - 1
- end
- if #lines == 0 then
- ui.center(9, "Say hello!", colors.gray, colors.white)
- end
- ui.fill(1, ui.h, ui.w, 1, colors.lightGray)
- ui.text(2, ui.h, "Message...", colors.gray, colors.lightGray)
- return #lines
- end
- local function chat(peer)
- local scroll = 0
- while true do
- markRead(peer)
- local total = drawChat(peer, scroll)
- local e, b, mx, my = os.pullEvent()
- if e == "mouse_click" then
- if ui.isBack(mx, my) then return end
- if my == ui.h then
- ui.fill(1, ui.h, ui.w, 1, colors.white)
- local text = ui.input(2, ui.h, { width = ui.w - 2, bg = colors.white })
- if text and text ~= "" then
- if ctx.modem then
- sendTo(peer, text)
- scroll = 0
- else
- ui.dialog("No Modem",
- { "A wireless modem", "is needed to send." },
- { { "OK", colors.blue } })
- end
- end
- end
- elseif e == "mouse_scroll" then
- if b > 0 then
- scroll = math.max(0, scroll - 3)
- else
- scroll = math.min(math.max(0, total - 1), scroll + 3)
- end
- end
- -- pocketos_message and other events just trigger a redraw
- end
- end
- local function compose()
- ui.clear(colors.white)
- ui.topBar("New Message")
- ui.text(2, 4, "Send to computer ID,", colors.gray, colors.white)
- ui.text(2, 5, "or 'all' to broadcast:", colors.gray, colors.white)
- ui.fill(2, 7, ui.w - 2, 1, colors.lightGray)
- local s = ui.input(3, 7, { width = ui.w - 4 })
- if s == nil then return nil end
- s = s:gsub("%s+", "")
- if s == "" then return nil end
- if s:lower() == "all" then return "all" end
- local n = tonumber(s)
- if n and n >= 0 then return math.floor(n) end
- ui.center(10, "That's not a valid ID", colors.red, colors.white)
- sleep(1.2)
- return nil
- end
- function M.run(u, c)
- ui, ctx = u, c
- local scroll = 0
- while true do
- local convs = conversations()
- ui.clear(colors.white)
- ui.topBar("Messages", "+", colors.blue)
- local y = 3
- local rows = {}
- if not ctx.modem then
- ui.center(y, "No wireless modem!", colors.red, colors.white)
- y = y + 2
- end
- for i = scroll + 1, #convs do
- local cv = convs[i]
- if y + 1 > ui.h - 2 then break end
- if cv.unread > 0 then
- ui.text(1, y, "\7", colors.blue, colors.white)
- end
- ui.text(3, y, peerName(cv.peer, cv.label):sub(1, 15), colors.black, colors.white)
- local t = fmtTime(cv.last.time)
- if #t > 0 then ui.text(ui.w - #t, y, t, colors.gray, colors.white) end
- local prev = (cv.last.dir == "out" and "You: " or "") .. cv.last.text
- ui.text(3, y + 1, prev:sub(1, ui.w - 3), colors.gray, colors.white)
- rows[#rows + 1] = { y1 = y, y2 = y + 1, peer = cv.peer }
- y = y + 3
- end
- if #convs == 0 then
- ui.center(9, "No messages yet", colors.gray, colors.white)
- ui.center(11, "Tap + to say hi", colors.gray, colors.white)
- end
- ui.center(ui.h, "My ID: " .. os.getComputerID(), colors.gray, colors.white)
- local e, b, mx, my = os.pullEvent()
- if e == "mouse_click" then
- if ui.isBack(mx, my) then return end
- if my == 1 and mx >= ui.w - 3 then
- local peer = compose()
- if peer ~= nil then chat(peer) end
- else
- for _, r in ipairs(rows) do
- if my >= r.y1 and my <= r.y2 then
- chat(r.peer)
- break
- end
- end
- end
- elseif e == "mouse_scroll" then
- scroll = math.max(0, math.min(scroll + b, math.max(0, #convs - 1)))
- end
- -- pocketos_message just triggers a redraw of the list
- end
- end
- return M
- ]==],
- ["pocketos/apps/music.lua"] = [==[
- -- PocketOS app: Music - plays Note Block Studio (.nbs) and MIDI (.mid)
- -- songs, plus REAL audio streamed from .dfpwm links (only the link is
- -- saved - nothing is downloaded to the pocket). Files live in /music;
- -- import by drag-and-drop or direct URL. MIDI notes are spread across
- -- bass/harp/bell for ~6 octaves of range.
- -- Sound needs a speaker (noisy pocket computer).
- local M = {}
- local ui, ctx, nbs, midi, speaker
- local DIR = "/music"
- local function isMidi(name)
- name = name:lower()
- return name:match("%.mid$") ~= nil or name:match("%.midi$") ~= nil
- end
- local function isSong(name)
- return name:lower():match("%.nbs$") ~= nil or isMidi(name)
- end
- local function stripExt(name)
- return (name:gsub("%.[^%.]+$", ""))
- end
- local function parseSong(fname)
- if isMidi(fname) then
- return midi.parse(DIR .. "/" .. fname)
- end
- return nbs.parse(DIR .. "/" .. fname)
- end
- local function fmtT(s)
- s = math.max(0, math.floor(s + 0.5))
- return string.format("%d:%02d", math.floor(s / 60), s % 60)
- end
- local function songList()
- local out = {}
- for _, f in ipairs(fs.list(DIR)) do
- if isSong(f) then out[#out + 1] = f end
- end
- table.sort(out)
- return out
- end
- local function import()
- ui.clear(colors.white)
- ui.topBar("Import")
- ui.text(2, 3, "Link to .nbs/.mid to", colors.gray, colors.white)
- ui.text(2, 4, "download, or .dfpwm", colors.gray, colors.white)
- ui.text(2, 5, "audio to stream:", colors.gray, colors.white)
- ui.text(2, 13, "Tip: dragging a song", colors.lightGray, colors.white)
- ui.text(2, 14, "file onto the window", colors.lightGray, colors.white)
- ui.text(2, 15, "is even easier!", colors.lightGray, colors.white)
- ui.fill(2, 7, ui.w - 2, 1, colors.lightGray)
- local url = ui.input(3, 7, { width = ui.w - 4 })
- if url == nil then return end
- url = url:gsub("^%s+", ""):gsub("%s+$", "")
- if url == "" then return end
- local clean = url:lower():gsub("[?#].*$", "")
- if not (clean:match("%.nbs$") or clean:match("%.mid$") or clean:match("%.midi$")) then
- -- not a downloadable song: save the link as an audio stream
- ui.fill(1, 8, ui.w, ui.h - 7, colors.white)
- ui.text(2, 9, "Saving as a stream.", colors.gray, colors.white)
- ui.text(2, 10, "Name it:", colors.gray, colors.white)
- ui.fill(2, 12, ui.w - 2, 1, colors.lightGray)
- local name = ui.input(3, 12, { width = ui.w - 4 })
- if name == nil then return end
- name = name:gsub("^%s+", ""):gsub("%s+$", ""):sub(1, 20)
- if name == "" then name = "Stream" end
- local streams = ctx.load("streams", {})
- streams[#streams + 1] = { name = name, url = url }
- ctx.save("streams", streams)
- ui.center(14, "Stream saved!", colors.green, colors.white)
- sleep(1)
- return
- end
- if not http then
- ui.center(9, "HTTP is disabled :(", colors.red, colors.white)
- sleep(1.5)
- return
- end
- ui.center(9, "Downloading...", colors.gray, colors.white)
- local ok, res = pcall(http.get, url, nil, true)
- if not ok or not res then
- ui.center(11, "Download failed", colors.red, colors.white)
- sleep(1.5)
- return
- end
- local data = res.readAll() or ""
- res.close()
- if #data < 50 then
- ui.center(11, "That file looks empty", colors.red, colors.white)
- sleep(1.5)
- return
- end
- local base = url:gsub("[?#].*$", ""):match("([^/]+)$") or "song.nbs"
- base = base:gsub("%%20", " "):gsub("[^%w%._%- ]", "")
- if base == "" then base = "song.nbs" end
- if not isSong(base) then base = base .. ".nbs" end
- local ext = base:match("(%.[^%.]+)$")
- local path = DIR .. "/" .. base
- local n = 1
- while fs.exists(path) do
- n = n + 1
- path = DIR .. "/" .. stripExt(base) .. n .. ext
- end
- local f = fs.open(path, "wb")
- f.write(data)
- f.close()
- ui.center(11, "Saved " .. fs.getName(path):sub(1, 18), colors.green, colors.white)
- sleep(1.2)
- end
- local function nowPlaying(fname)
- local song, err = parseSong(fname)
- if not song then
- ui.dialog("Can't Play", { tostring(err or "Bad .nbs file") }, { { "OK", colors.blue } })
- return
- end
- if not speaker then
- ui.dialog("No Speaker", { "Craft this pocket", "with a speaker!" }, { { "OK", colors.blue } })
- return
- end
- local pl = nbs.newPlayer(song, speaker)
- local btn
- local rst = { x1 = 3, y1 = 16, x2 = 6, y2 = 16 }
- local function drawControls()
- ui.fill(1, 16, ui.w, 1, colors.white)
- local label = pl.done and "Replay" or (pl.playing and "Pause" or "Play")
- btn = ui.button(math.floor((ui.w - #label - 2) / 2) + 1, 16, label, colors.blue)
- ui.button(3, 16, "<<", colors.gray)
- end
- local function drawProgress()
- local wb = ui.w - 4
- local filled = math.min(wb, math.floor(pl.progress() * wb + 0.5))
- ui.fill(3, 12, wb, 1, colors.lightGray)
- if filled > 0 then ui.fill(3, 12, filled, 1, colors.blue) end
- ui.text(3, 13, fmtT(pl.elapsed()), colors.gray, colors.white)
- local d = fmtT(pl.duration())
- ui.text(ui.w - 1 - #d, 13, d, colors.gray, colors.white)
- end
- ui.clear(colors.white)
- ui.topBar("Now Playing")
- local ax = math.floor((ui.w - 10) / 2) + 1
- ui.fill(ax, 3, 10, 4, colors.red)
- ui.text(ax + 4, 5, "\14", colors.white, colors.red)
- ui.center(8, song.name:sub(1, 24), colors.black, colors.white)
- local by = (song.author and song.author ~= "") and song.author or "Unknown artist"
- ui.center(9, by:sub(1, 24), colors.gray, colors.white)
- pl.begin()
- drawControls()
- drawProgress()
- local ut = os.startTimer(0.5)
- local wasDone = false
- while true do
- local e, a, b, c = os.pullEvent()
- if pl.onEvent(e, a) then
- if pl.done and not wasDone then
- wasDone = true
- drawControls()
- drawProgress()
- end
- elseif e == "timer" and a == ut then
- drawProgress()
- ut = os.startTimer(0.5)
- elseif e == "mouse_click" then
- if ui.isBack(b, c) then
- pl.stop()
- return
- end
- if ui.hit(btn, b, c) then
- if pl.done then
- wasDone = false
- pl.restart()
- elseif pl.playing then
- pl.stop()
- else
- pl.begin()
- end
- drawControls()
- elseif ui.hit(rst, b, c) then
- wasDone = false
- pl.restart()
- drawControls()
- drawProgress()
- end
- end
- end
- end
- local function getDfpwm()
- if not ctx.require then return nil end
- local ok, mod = pcall(ctx.require, "cc.audio.dfpwm")
- if ok and type(mod) == "table" and mod.make_decoder then return mod end
- return nil
- end
- -- Streams real audio from a saved .dfpwm link. Nothing is written to
- -- disk; the audio is fetched fresh each play.
- local function streamScreen(streams, idx)
- local st = streams[idx]
- if not speaker then
- ui.dialog("No Speaker", { "Craft this pocket", "with a speaker!" }, { { "OK", colors.blue } })
- return
- end
- if not http then
- ui.dialog("No HTTP", { "HTTP is disabled in", "the server config." }, { { "OK", colors.blue } })
- return
- end
- local dfpwm = getDfpwm()
- if not dfpwm then
- ui.dialog("Too Old", { "This CC version has", "no DFPWM audio." }, { { "OK", colors.blue } })
- return
- end
- local function drawHeader()
- ui.clear(colors.white)
- ui.topBar("Streaming", "Del", colors.red)
- local ax = math.floor((ui.w - 10) / 2) + 1
- ui.fill(ax, 3, 10, 4, colors.green)
- ui.text(ax + 4, 5, "\14", colors.white, colors.green)
- ui.center(8, st.name:sub(1, 24), colors.black, colors.white)
- ui.center(9, st.url:gsub("^https?://", ""):sub(1, 24), colors.lightGray, colors.white)
- end
- local function status(msg, color)
- ui.fill(1, 11, ui.w, 1, colors.white)
- if msg then ui.center(11, msg, color, colors.white) end
- end
- while true do -- retry loop
- drawHeader()
- status("Connecting...", colors.gray)
- local okGet, res = pcall(http.get, st.url, nil, true)
- if not (okGet and res) then
- local pick = ui.dialog("No Connection", { "Couldn't reach", "that link." },
- { { "Retry", colors.blue }, { "Back", colors.gray } })
- if pick ~= 1 then return end
- else
- status("Buffering...", colors.gray)
- local data = res.readAll() or ""
- res.close()
- -- Links that aren't real DFPWM decode into slow noise about
- -- 8x the song's true length - catch the common mistakes.
- local head = data:sub(1, 16)
- local problem
- if head:match("^%s*<") then
- problem = { "That's a web page!", "Paste the RAW file", "link instead." }
- elseif head:sub(1, 4) == "RIFF" then
- problem = { "That's a WAV file.", "Convert to .dfpwm at", "music.madefor.cc" }
- elseif head:sub(1, 3) == "ID3"
- or (head:byte(1) == 255 and (head:byte(2) or 0) >= 224) then
- problem = { "That's an MP3 file.", "Convert to .dfpwm at", "music.madefor.cc" }
- elseif head:sub(1, 4) == "OggS" or head:sub(1, 4) == "fLaC" then
- problem = { "Not a .dfpwm file.", "Convert to .dfpwm at", "music.madefor.cc" }
- elseif #data < 100 then
- problem = { "That file is empty." }
- end
- if problem then
- ui.dialog("Bad Audio", problem, { { "OK", colors.blue } })
- return
- end
- local totalSec = #data * 8 / 48000
- local decoder = dfpwm.make_decoder()
- local at = 1
- local playing = true
- local buf = nil
- local samples = 0
- local shown = -1
- local btn
- local function fmt(s)
- s = math.max(0, math.floor(s + 0.5))
- return string.format("%d:%02d", math.floor(s / 60), s % 60)
- end
- local function drawControls()
- ui.fill(1, 16, ui.w, 1, colors.white)
- local label = playing and "Pause" or "Play"
- btn = ui.button(math.floor((ui.w - #label - 2) / 2) + 1, 16, label, colors.blue)
- end
- local function drawProgress(force)
- local s = math.floor(samples / 48000)
- if s == shown and not force then return end
- shown = s
- local wb = ui.w - 4
- local filled = math.min(wb, math.floor(s / math.max(1, totalSec) * wb + 0.5))
- ui.fill(3, 12, wb, 1, colors.lightGray)
- if filled > 0 then ui.fill(3, 12, filled, 1, colors.green) end
- ui.fill(1, 13, ui.w, 1, colors.white)
- ui.text(3, 13, fmt(s), colors.gray, colors.white)
- local d = fmt(totalSec)
- ui.text(ui.w - 1 - #d, 13, d, colors.gray, colors.white)
- end
- local function redrawAll()
- drawHeader()
- status(not playing and "Paused" or nil, colors.gray)
- drawControls()
- drawProgress(true)
- end
- status(nil)
- drawControls()
- drawProgress(true)
- while true do
- local idle = true
- if playing and not buf and at <= #data then
- local chunk = data:sub(at, at + 8191)
- at = at + 8192
- buf = decoder(chunk)
- end
- if playing and buf and speaker.playAudio(buf) then
- samples = samples + #buf
- buf = nil
- drawProgress()
- idle = false -- keep filling until the speaker is full
- elseif playing and not buf and at > #data then
- -- all data queued; let the tail play out, then finish
- local t = os.startTimer(3)
- while true do
- local e, id = os.pullEvent()
- if (e == "timer" and id == t) or e == "speaker_audio_empty" then
- break
- end
- end
- local pick = ui.dialog("Stream Ended", { st.name:sub(1, 18) },
- { { "Replay", colors.blue }, { "Back", colors.gray } })
- if pick ~= 1 then return end
- decoder = dfpwm.make_decoder()
- at = 1
- samples = 0
- playing = true
- redrawAll()
- idle = false
- end
- if idle then
- local e, a, b, c = os.pullEvent()
- if e == "mouse_click" then
- if ui.isBack(b, c) then
- speaker.stop()
- return
- elseif c == 1 and b >= ui.w - 4 then
- speaker.stop()
- local pick = ui.dialog("Delete?", { st.name },
- { { "Delete", colors.red }, { "Keep", colors.gray } })
- if pick == 1 then
- table.remove(streams, idx)
- ctx.save("streams", streams)
- return
- end
- redrawAll()
- elseif ui.hit(btn, b, c) then
- playing = not playing
- if not playing then speaker.stop() end
- drawControls()
- status(not playing and "Paused" or nil, colors.gray)
- end
- end
- end
- end
- end
- end
- end
- local function jsonDecode(s)
- local d = textutils.unserialiseJSON or textutils.unserializeJSON
- if not d then return nil end
- local ok, v = pcall(d, s)
- return ok and v or nil
- end
- local function serverURL()
- local s = ctx.config and ctx.config.musicServer
- if type(s) == "string" and s ~= "" then return (s:gsub("/+$", "")) end
- return nil
- end
- -- Streams full-quality audio (real songs) from the music server. The
- -- server transcodes to DFPWM on the fly, so it arrives as a live stream
- -- we decode chunk by chunk - exactly what CC speakers want.
- local function streamOnline(title, author, url, duration)
- if not speaker then
- ui.dialog("No Speaker", { "Craft this pocket", "with a speaker!" }, { { "OK", colors.blue } })
- return
- end
- local dfpwm = getDfpwm()
- if not dfpwm then
- ui.dialog("Too Old", { "This CC version has", "no DFPWM audio." }, { { "OK", colors.blue } })
- return
- end
- -- saves the song's stream link to the library so it can be replayed
- -- without searching again (the audio itself is far too big to store)
- local function saveSong()
- local streams = ctx.load("streams", {})
- for _, s in ipairs(streams) do
- if s.url == url then return false end -- already saved
- end
- streams[#streams + 1] = {
- name = tostring(title):sub(1, 20), url = url,
- online = true, duration = duration,
- }
- ctx.save("streams", streams)
- return true
- end
- ui.clear(colors.white)
- ui.topBar("Streaming", "Save", colors.blue)
- local ax = math.floor((ui.w - 10) / 2) + 1
- ui.fill(ax, 3, 10, 4, colors.green)
- ui.text(ax + 4, 5, "\14", colors.white, colors.green)
- ui.center(8, tostring(title):sub(1, 24), colors.black, colors.white)
- if author and author ~= "" then
- ui.center(9, tostring(author):sub(1, 24), colors.gray, colors.white)
- end
- ui.center(12, "Connecting...", colors.gray, colors.white)
- local handle, err = http.get(url, nil, true)
- if not handle then
- ui.dialog("Can't Stream", ui.wrap(tostring(err or "no response"), 20),
- { { "OK", colors.blue } })
- return
- end
- local decoder = dfpwm.make_decoder()
- local playing, quit, finished = true, false, false
- local samples = 0
- local function fmt(s)
- s = math.max(0, math.floor(s + 0.5))
- return string.format("%d:%02d", math.floor(s / 60), s % 60)
- end
- local btn
- local function drawControls()
- ui.fill(1, 16, ui.w, 1, colors.white)
- local label = playing and "Pause" or "Play"
- btn = ui.button(math.floor((ui.w - #label - 2) / 2) + 1, 16, label, colors.blue)
- end
- local function drawProgress()
- local secs = samples / 48000
- ui.fill(1, 12, ui.w, 1, colors.white)
- ui.fill(1, 13, ui.w, 1, colors.white)
- if duration and duration > 0 then
- local wb = ui.w - 4
- local filled = math.min(wb, math.floor(secs / duration * wb + 0.5))
- ui.fill(3, 12, wb, 1, colors.lightGray)
- if filled > 0 then ui.fill(3, 12, filled, 1, colors.green) end
- ui.text(3, 13, fmt(secs), colors.gray, colors.white)
- local d = fmt(duration)
- ui.text(ui.w - 1 - #d, 13, d, colors.gray, colors.white)
- else
- ui.center(12, fmt(secs), colors.gray, colors.white)
- end
- end
- drawControls()
- drawProgress()
- local function player()
- while not quit do
- if playing then
- local chunk = handle.read(16 * 1024)
- if not chunk then finished = true; return end
- local buffer = decoder(chunk)
- samples = samples + #buffer
- while not speaker.playAudio(buffer) do
- os.pullEvent("speaker_audio_empty")
- if quit or not playing then break end
- end
- else
- os.sleep(0.1)
- end
- end
- end
- local function control()
- local t = os.startTimer(0.5)
- while not quit do
- local e, a, b, c = os.pullEvent()
- if e == "timer" and a == t then
- drawProgress()
- if finished then quit = true end
- t = os.startTimer(0.5)
- elseif e == "mouse_click" then
- if ui.isBack(b, c) then
- quit = true
- elseif c == 1 and b >= ui.w - 4 then
- -- "Save" in the top bar
- local ok2 = saveSong()
- ui.fill(1, 14, ui.w, 1, colors.white)
- ui.center(14, ok2 and "Saved to library" or "Already saved",
- ok2 and colors.green or colors.gray, colors.white)
- elseif ui.hit(btn, b, c) then
- playing = not playing
- if not playing then speaker.stop() end
- drawControls()
- end
- end
- end
- end
- parallel.waitForAny(player, control)
- pcall(handle.close)
- speaker.stop()
- if finished and samples == 0 then
- ui.dialog("No Audio",
- { "The server sent no", "audio for this track.", "Try another song." },
- { { "OK", colors.blue } })
- elseif finished then
- local pick = ui.dialog("Finished", { tostring(title):sub(1, 18) },
- { { "Save", colors.green }, { "Done", colors.blue } })
- if pick == 1 then saveSong() end
- end
- end
- -- Search the music server and stream a result.
- local function searchOnline()
- local base = serverURL()
- if not base then return end
- ui.clear(colors.white)
- ui.topBar("Search")
- ui.text(2, 3, "Search for a song:", colors.gray, colors.white)
- ui.fill(2, 5, ui.w - 2, 1, colors.lightGray)
- local q = ui.input(3, 5, { width = ui.w - 4 })
- if q == nil then return end
- q = q:gsub("^%s+", ""):gsub("%s+$", "")
- if q == "" then return end
- if not http then
- ui.dialog("No HTTP", { "HTTP is disabled." }, { { "OK", colors.blue } })
- return
- end
- ui.center(8, "Searching...", colors.gray, colors.white)
- ui.center(9, "(may wake the server)", colors.lightGray, colors.white)
- local res, err = http.get(base .. "/search?q=" .. textutils.urlEncode(q))
- if not res then
- ui.dialog("Can't Reach Server",
- ui.wrap(tostring(err or "no response"), 20),
- { { "OK", colors.blue } })
- return
- end
- local body = res.readAll() or ""
- res.close()
- local data = jsonDecode(body)
- if not data then
- ui.dialog("Bad Response",
- { "Server replied but", "not with JSON.", "ffmpeg/yt-dlp ok?" },
- { { "OK", colors.blue } })
- return
- end
- local results = data.results
- if type(results) ~= "table" or #results == 0 then
- ui.center(11, "No results found", colors.gray, colors.white)
- sleep(1.2)
- return
- end
- local scroll = 0
- while true do
- ui.clear(colors.white)
- ui.topBar("Results")
- local rows = {}
- local y = 3
- for i = scroll + 1, #results do
- if y + 1 > ui.h - 1 then break end
- local r = results[i]
- ui.text(2, y, tostring(r.title):sub(1, ui.w - 2), colors.black, colors.white)
- local sub = tostring(r.author or "")
- if r.duration and r.duration > 0 then
- local d = string.format("%d:%02d", math.floor(r.duration / 60), r.duration % 60)
- sub = (sub ~= "" and (sub .. " - ") or "") .. d
- end
- ui.text(2, y + 1, sub:sub(1, ui.w - 2), colors.gray, colors.white)
- rows[#rows + 1] = { y1 = y, y2 = y + 1, r = r }
- y = y + 3
- end
- ui.center(ui.h, "Tap a song to play", colors.lightGray, colors.white)
- local e, a, b, c = os.pullEvent()
- if e == "mouse_click" then
- if ui.isBack(b, c) then return end
- for _, row in ipairs(rows) do
- if c >= row.y1 and c <= row.y2 then
- streamOnline(row.r.title, row.r.author,
- base .. "/stream?id=" .. textutils.urlEncode(tostring(row.r.id)),
- tonumber(row.r.duration))
- break
- end
- end
- elseif e == "mouse_scroll" then
- scroll = math.max(0, math.min(scroll + a, math.max(0, #results - 1)))
- end
- end
- end
- function M.run(u, c)
- ui, ctx = u, c
- nbs = dofile("/pocketos/lib/nbs.lua")
- midi = dofile("/pocketos/lib/midi.lua")
- speaker = peripheral.find("speaker")
- if not fs.exists(DIR) then fs.makeDir(DIR) end
- local scroll = 0
- while true do
- local files = songList()
- local streams = ctx.load("streams", {})
- local entries = {}
- for _, f in ipairs(files) do
- entries[#entries + 1] = { file = f }
- end
- for i, s in ipairs(streams) do
- entries[#entries + 1] = { stream = i, name = s.name }
- end
- ui.clear(colors.white)
- ui.topBar("Music", "+", colors.blue)
- local rows = {}
- local y = 3
- local searchRow
- if serverURL() then
- ui.text(2, y, "\7", colors.green, colors.white)
- ui.text(4, y, "Search online music", colors.blue, colors.white)
- searchRow = y
- y = y + 2
- end
- for i = scroll + 1, #entries do
- if y > ui.h - 2 then break end
- local en = entries[i]
- if en.file then
- ui.text(2, y, "\14", isMidi(en.file) and colors.blue or colors.red, colors.white)
- ui.text(4, y, stripExt(en.file):sub(1, ui.w - 4), colors.black, colors.white)
- else
- ui.text(2, y, "\16", colors.green, colors.white)
- ui.text(4, y, (en.name or "Stream"):sub(1, ui.w - 4), colors.black, colors.white)
- end
- rows[#rows + 1] = { y = y, entry = en }
- y = y + 2
- end
- if #entries == 0 then
- ui.center(8, "No songs yet", colors.gray, colors.white)
- ui.center(10, "Drag .nbs or .mid files", colors.gray, colors.white)
- ui.center(11, "onto the game window,", colors.gray, colors.white)
- ui.center(12, "or tap + for a URL", colors.gray, colors.white)
- end
- if speaker then
- ui.center(ui.h, "Drop songs to import", colors.lightGray, colors.white)
- else
- ui.center(ui.h, "No speaker!", colors.red, colors.white)
- end
- local e, a, b, c2 = os.pullEvent()
- if e == "mouse_click" then
- if ui.isBack(b, c2) then return end
- if c2 == 1 and b >= ui.w - 3 then
- import()
- elseif searchRow and c2 == searchRow then
- searchOnline()
- else
- for _, r in ipairs(rows) do
- if c2 == r.y then
- if r.entry.file then
- nowPlaying(r.entry.file)
- else
- local s = streams[r.entry.stream]
- if s and s.online then
- streamOnline(s.name, "", s.url, s.duration)
- else
- streamScreen(streams, r.entry.stream)
- end
- end
- break
- end
- end
- end
- elseif e == "mouse_scroll" then
- scroll = math.max(0, math.min(scroll + a, math.max(0, #entries - 1)))
- elseif e == "file_transfer" then
- local saved = ctx.importFiles(a)
- local msg = saved > 0
- and ("Imported " .. saved .. " song" .. (saved == 1 and "" or "s"))
- or "Only .nbs/.mid works"
- ui.text(math.floor((ui.w - #msg - 2) / 2) + 1, ui.h - 2,
- " " .. msg .. " ", colors.white,
- saved > 0 and colors.green or colors.gray)
- sleep(1.2)
- end
- end
- end
- return M
- ]==],
- ["pocketos/apps/web.lua"] = [==[
- -- PocketOS app: Web - search the real internet.
- -- Uses Wikipedia's public API over CC's http, since it returns clean
- -- JSON a pocket computer can actually render (Google blocks bots).
- local M = {}
- local ui
- local API = "https://en.wikipedia.org/w/api.php"
- local function jsonGet(url)
- if not http then return nil, "HTTP is disabled" end
- local ok, res = pcall(http.get, url)
- if not ok or not res then return nil, "No connection" end
- local body = res.readAll()
- res.close()
- local decode = textutils.unserialiseJSON or textutils.unserializeJSON
- local data = decode and decode(body)
- if not data then return nil, "Bad response" end
- return data
- end
- local function clean(s)
- s = tostring(s or "")
- s = s:gsub("<[^>]->", "")
- s = s:gsub(""", "\""):gsub("'", "'"):gsub("&", "&")
- s = s:gsub("<", "<"):gsub(">", ">"):gsub(" ", " ")
- return s
- end
- local function readArticle(title)
- ui.clear(colors.white)
- ui.topBar(title:sub(1, 14))
- ui.center(10, "Loading...", colors.gray, colors.white)
- local data, err = jsonGet(API
- .. "?action=query&prop=extracts&explaintext=1&exchars=6000"
- .. "&redirects=1&format=json&formatversion=2&titles="
- .. textutils.urlEncode(title))
- local text
- if data then
- local pages = data.query and data.query.pages
- local pg = pages and pages[1]
- text = pg and pg.extract
- end
- if not text or text == "" then
- ui.center(12, err or "Nothing found", colors.red, colors.white)
- sleep(1.2)
- return
- end
- local lines = {}
- for para in (text .. "\n"):gmatch("(.-)\n") do
- local h = para:match("^=+%s*(.-)%s*=+$")
- if h then
- lines[#lines + 1] = ""
- lines[#lines + 1] = "\17 " .. h -- heading marker
- elseif para == "" then
- lines[#lines + 1] = ""
- else
- for _, l in ipairs(ui.wrap(para, ui.w - 2)) do
- lines[#lines + 1] = l
- end
- end
- end
- local view = ui.h - 3
- local scroll = 0
- while true do
- ui.clear(colors.white)
- ui.topBar(title:sub(1, 14))
- local y = 3
- for i = scroll + 1, #lines do
- if y > ui.h - 1 then break end
- local l = lines[i]
- local h = l:match("^\17 (.*)")
- if h then
- ui.text(2, y, h:sub(1, ui.w - 2), colors.blue, colors.white)
- elseif l ~= "" then
- ui.text(2, y, l, colors.black, colors.white)
- end
- y = y + 1
- end
- ui.center(ui.h, "scroll for more", colors.lightGray, colors.white)
- local e, a, b, c = os.pullEvent()
- if e == "mouse_click" and ui.isBack(b, c) then return end
- if e == "mouse_scroll" then
- scroll = math.max(0, math.min(scroll + a * 3, math.max(0, #lines - view)))
- elseif e == "key" then
- if a == keys.down then
- scroll = math.min(scroll + 3, math.max(0, #lines - view))
- elseif a == keys.up then
- scroll = math.max(0, scroll - 3)
- end
- end
- end
- end
- local function results(q)
- ui.clear(colors.white)
- ui.topBar("Results")
- ui.center(10, "Searching...", colors.gray, colors.white)
- local data, err = jsonGet(API
- .. "?action=query&list=search&srlimit=8&format=json&formatversion=2"
- .. "&srsearch=" .. textutils.urlEncode(q))
- local hits = data and data.query and data.query.search
- if not hits or #hits == 0 then
- ui.fill(1, 10, ui.w, 1, colors.white)
- ui.center(10, err or "No results", colors.red, colors.white)
- sleep(1.2)
- return
- end
- local scroll = 0
- while true do
- ui.clear(colors.white)
- ui.topBar("Results")
- local rows = {}
- local y = 3
- for i = scroll + 1, #hits do
- if y + 1 > ui.h - 1 then break end
- local it = hits[i]
- ui.text(2, y, clean(it.title):sub(1, ui.w - 2), colors.blue, colors.white)
- ui.text(2, y + 1, clean(it.snippet):sub(1, ui.w - 2), colors.gray, colors.white)
- rows[#rows + 1] = { y1 = y, y2 = y + 1, title = it.title }
- y = y + 3
- end
- local e, a, b, c = os.pullEvent()
- if e == "mouse_click" then
- if ui.isBack(b, c) then return end
- for _, r in ipairs(rows) do
- if c >= r.y1 and c <= r.y2 then
- readArticle(r.title)
- break
- end
- end
- elseif e == "mouse_scroll" then
- scroll = math.max(0, math.min(scroll + a, math.max(0, #hits - 1)))
- end
- end
- end
- function M.run(u)
- ui = u
- while true do
- ui.clear(colors.white)
- ui.topBar("Web")
- local logo = {
- { "W", colors.blue }, { "e", colors.red }, { "b", colors.yellow },
- }
- local lx = math.floor((ui.w - 3) / 2) + 1
- for i, l in ipairs(logo) do
- ui.text(lx + i - 1, 5, l[1], l[2], colors.white)
- end
- ui.center(8, "Search the internet", colors.gray, colors.white)
- ui.fill(3, 10, ui.w - 4, 1, colors.lightGray)
- ui.text(4, 10, "Search...", colors.gray, colors.lightGray)
- ui.center(ui.h - 1, "Powered by Wikipedia", colors.lightGray, colors.white)
- local e, a, b, c = os.pullEvent()
- if e == "mouse_click" then
- if ui.isBack(b, c) then return end
- if c == 10 and b >= 3 and b <= ui.w - 2 then
- ui.fill(3, 10, ui.w - 4, 1, colors.lightGray)
- local q = ui.input(4, 10, { width = ui.w - 6 })
- if q and q:gsub("%s", "") ~= "" then
- results(q)
- end
- end
- end
- end
- end
- return M
- ]==],
- ["pocketos/apps/ai.lua"] = [==[
- -- PocketOS app: AI - chat with an AI via the Groq API.
- -- Get a free API key at console.groq.com, then paste it in on the
- -- device (Ctrl+V pastes). The key is stored only on this computer,
- -- in /.pocketos, and survives OS updates.
- local M = {}
- local ui, ctx
- local URL = "https://api.groq.com/openai/v1/chat/completions"
- local SYSTEM = "You are a helpful assistant running on a tiny pocket "
- .. "computer in Minecraft (ComputerCraft). Answer in short plain "
- .. "text; the screen is only 26 characters wide."
- local function decode(s)
- local d = textutils.unserialiseJSON or textutils.unserializeJSON
- if not d then return nil end
- local ok, v = pcall(d, s)
- return ok and v or nil
- end
- local function encode(t)
- local e = textutils.serialiseJSON or textutils.serializeJSON
- return e(t)
- end
- local function model()
- local m = ctx.config and ctx.config.aiModel
- if type(m) == "string" and m ~= "" then return m end
- return "llama-3.3-70b-versatile"
- end
- local function masked(key)
- if #key <= 8 then return "****" end
- return key:sub(1, 4) .. "..." .. key:sub(-4)
- end
- local function keyScreen()
- ui.clear(colors.white)
- ui.topBar("API Key")
- local key = ctx.load("groq_key", "")
- ui.text(2, 3, "Groq API key:", colors.gray, colors.white)
- ui.text(2, 4, key == "" and "Not set" or masked(key),
- key == "" and colors.red or colors.black, colors.white)
- ui.text(2, 6, "Get a free key at", colors.gray, colors.white)
- ui.text(2, 7, "console.groq.com", colors.blue, colors.white)
- ui.text(2, 9, "Paste it below with", colors.gray, colors.white)
- ui.text(2, 10, "Ctrl+V, then Enter:", colors.gray, colors.white)
- ui.fill(2, 12, ui.w - 2, 1, colors.lightGray)
- ui.text(2, 14, "Enter = keep current,", colors.lightGray, colors.white)
- ui.text(2, 15, "type 'clear' to remove", colors.lightGray, colors.white)
- local s = ui.input(3, 12, { width = ui.w - 4 })
- if s == nil then return end
- s = s:gsub("%s", "")
- if s:lower() == "clear" then
- ctx.save("groq_key", "")
- ui.center(17, "Key removed", colors.gray, colors.white)
- sleep(0.8)
- elseif s ~= "" then
- ctx.save("groq_key", s)
- ui.center(17, "Key saved!", colors.green, colors.white)
- sleep(0.8)
- end
- end
- local function ask(msgs)
- local key = ctx.load("groq_key", "")
- local body = encode({
- model = model(),
- messages = msgs,
- max_tokens = 400,
- temperature = 0.7,
- })
- local ok, res, err, errRes = pcall(http.post, URL, body, {
- ["Content-Type"] = "application/json",
- ["Authorization"] = "Bearer " .. key,
- })
- if not ok then return nil, "Request failed" end
- if not res then
- local detail
- if errRes then
- local eb = errRes.readAll()
- pcall(errRes.close)
- local data = decode(eb)
- detail = data and data.error and data.error.message
- end
- return nil, detail or err or "No connection"
- end
- local data = decode(res.readAll())
- res.close()
- local content = data and data.choices and data.choices[1]
- and data.choices[1].message and data.choices[1].message.content
- if type(content) ~= "string" or content == "" then
- local emsg = data and data.error and data.error.message
- return nil, emsg or "Empty response"
- end
- return content
- end
- local function chatLines(chat)
- local lines = {}
- for _, m in ipairs(chat) do
- local wrapped = ui.wrap(m.text, 19)
- local width = 0
- for _, s in ipairs(wrapped) do width = math.max(width, #s) end
- for _, s in ipairs(wrapped) do
- local pad = " " .. s .. string.rep(" ", width - #s) .. " "
- if m.role == "user" then
- lines[#lines + 1] = { text = pad, x = ui.w - #pad, fg = colors.white, bg = colors.blue }
- elseif m.role == "error" then
- lines[#lines + 1] = { text = pad, x = 2, fg = colors.red, bg = colors.white }
- else
- lines[#lines + 1] = { text = pad, x = 2, fg = colors.black, bg = colors.lightGray }
- end
- end
- lines[#lines + 1] = { spacer = true }
- end
- if #lines > 0 then lines[#lines] = nil end
- return lines
- end
- local function drawChat(chat, scroll, busy)
- ui.clear(colors.white)
- ui.topBar("AI", "\15 +", colors.blue)
- local lines = chatLines(chat)
- local top, bottom = 2, ui.h - 2
- local yy = bottom
- local i = #lines - scroll
- while i >= 1 and yy >= top do
- local L = lines[i]
- if not L.spacer then
- ui.text(L.x, yy, L.text, L.fg, L.bg)
- end
- yy = yy - 1
- i = i - 1
- end
- if #chat == 0 then
- ui.center(8, "Ask me anything!", colors.gray, colors.white)
- ui.center(10, "\15 = API key", colors.lightGray, colors.white)
- ui.center(11, "+ = new chat", colors.lightGray, colors.white)
- end
- ui.fill(1, ui.h, ui.w, 1, colors.lightGray)
- if busy then
- ui.text(2, ui.h, "Thinking...", colors.gray, colors.lightGray)
- else
- ui.text(2, ui.h, "Ask something...", colors.gray, colors.lightGray)
- end
- return #lines
- end
- function M.run(u, c)
- ui, ctx = u, c
- local chat = ctx.load("ai_chat", {})
- local scroll = 0
- if ctx.load("groq_key", "") == "" then keyScreen() end
- while true do
- local total = drawChat(chat, scroll, false)
- local e, b, mx, my = os.pullEvent()
- if e == "mouse_click" then
- if ui.isBack(mx, my) then return end
- if my == 1 and mx >= ui.w - 3 then
- if mx >= ui.w - 1 then
- chat = {}
- ctx.save("ai_chat", chat)
- scroll = 0
- else
- keyScreen()
- end
- elseif my == ui.h then
- if not http then
- ui.dialog("No HTTP",
- { "HTTP is disabled in", "the server config." },
- { { "OK", colors.blue } })
- elseif ctx.load("groq_key", "") == "" then
- keyScreen()
- else
- ui.fill(1, ui.h, ui.w, 1, colors.white)
- local q = ui.input(2, ui.h, { width = ui.w - 2, bg = colors.white })
- if q and q:gsub("%s", "") ~= "" then
- chat[#chat + 1] = { role = "user", text = q }
- scroll = 0
- drawChat(chat, scroll, true)
- local msgs = { { role = "system", content = SYSTEM } }
- for i = math.max(1, #chat - 9), #chat do
- local m = chat[i]
- if m.role ~= "error" then
- msgs[#msgs + 1] = { role = m.role, content = m.text }
- end
- end
- local reply, err = ask(msgs)
- if reply then
- reply = reply:gsub("^%s+", ""):gsub("%s+$", ""):sub(1, 1200)
- chat[#chat + 1] = { role = "assistant", text = reply }
- else
- chat[#chat + 1] = { role = "error", text = "! " .. tostring(err):sub(1, 150) }
- end
- while #chat > 40 do table.remove(chat, 1) end
- ctx.save("ai_chat", chat)
- end
- end
- end
- elseif e == "mouse_scroll" then
- if b > 0 then
- scroll = math.max(0, scroll - 3)
- else
- scroll = math.min(math.max(0, total - 1), scroll + 3)
- end
- end
- end
- end
- return M
- ]==],
- ["pocketos/apps/bank.lua"] = [==[
- -- PocketOS app: Bank - your wallet.
- -- Shows your balance from the bank server and lets you pay other
- -- pockets by ID. Deposits happen at ATMs (see atm.lua); receipts
- -- arrive in Messages. Your account is this pocket's computer ID.
- local M = {}
- local ui, ctx
- local bankId
- local function findBank()
- if ctx.config and tonumber(ctx.config.bank) then
- return tonumber(ctx.config.bank)
- end
- return rednet.lookup("pocketos.bank")
- end
- local function request(msg)
- if not ctx.modem then return nil, "No wireless modem" end
- if not bankId then bankId = findBank() end
- if not bankId then return nil, "No bank in range" end
- msg.label = os.getComputerLabel()
- rednet.send(bankId, msg, "pocketos.bank")
- local deadline = os.clock() + 3
- while true do
- local left = deadline - os.clock()
- if left <= 0 then return nil, "Bank not answering" end
- local id, resp = rednet.receive("pocketos.bank", left)
- if id == bankId and type(resp) == "table" then
- return resp
- elseif id == nil then
- return nil, "Bank not answering"
- end
- end
- end
- local function payFlow()
- ui.clear(colors.white)
- ui.topBar("Pay")
- ui.text(2, 3, "Pay to pocket ID:", colors.gray, colors.white)
- ui.fill(2, 5, ui.w - 2, 1, colors.lightGray)
- local to = ui.input(3, 5, { width = ui.w - 4 })
- if to == nil then return end
- to = tonumber(to)
- if not to then return end
- ui.text(2, 7, "Amount ($):", colors.gray, colors.white)
- ui.fill(2, 9, ui.w - 2, 1, colors.lightGray)
- local amount = ui.input(3, 9, { width = ui.w - 4 })
- if amount == nil then return end
- amount = tonumber(amount)
- if not amount or amount <= 0 then return end
- amount = math.floor(amount)
- local pick = ui.dialog("Confirm",
- { "Pay $" .. amount, "to #" .. to .. "?" },
- { { "Pay", colors.green }, { "Cancel", colors.gray } })
- if pick ~= 1 then return end
- ui.center(12, "Sending...", colors.gray, colors.white)
- local resp, err = request({ type = "pay", to = to, amount = amount })
- if resp and resp.ok then
- ui.dialog("Sent!", { "Paid $" .. amount, "to #" .. to },
- { { "OK", colors.blue } })
- else
- ui.dialog("Failed",
- { tostring((resp and resp.err) or err or "Unknown error"):sub(1, 18) },
- { { "OK", colors.blue } })
- end
- end
- function M.run(u, c)
- ui, ctx = u, c
- local balance, err
- local function refresh()
- ui.clear(colors.white)
- ui.topBar("Bank")
- ui.center(9, "Talking to bank...", colors.gray, colors.white)
- local resp
- resp, err = request({ type = "balance" })
- if resp and resp.ok then
- balance = resp.balance
- err = nil
- elseif resp then
- err = resp.err or "Bank error"
- end
- end
- refresh()
- while true do
- local payBtn, retryBtn
- ui.clear(colors.white)
- ui.topBar("Bank")
- if err then
- ui.center(6, "Can't reach the bank", colors.red, colors.white)
- ui.center(8, tostring(err):sub(1, 24), colors.gray, colors.white)
- ui.center(11, "Needs a wireless modem", colors.lightGray, colors.white)
- ui.center(12, "+ a bank server in range", colors.lightGray, colors.white)
- retryBtn = ui.button(math.floor((ui.w - 7) / 2) + 1, 15, "Retry", colors.blue)
- else
- -- balance card, credit-card style
- ui.card(3, 4, ui.w - 6, 5, colors.blue)
- ui.text(5, 5, "Your balance", colors.lightGray, colors.blue)
- ui.text(5, 7, "$ " .. tostring(balance or 0), colors.white, colors.blue)
- payBtn = ui.button(math.floor((ui.w - 13) / 2) + 1, 11, "Pay someone", colors.green)
- ui.center(14, "Deposit iron, gold or", colors.lightGray, colors.white)
- ui.center(15, "diamonds at any ATM", colors.lightGray, colors.white)
- ui.center(17, "Tap balance to refresh", colors.lightGray, colors.white)
- ui.center(ui.h, "My ID: " .. os.getComputerID(), colors.gray, colors.white)
- end
- local e, a, b, c2 = os.pullEvent()
- if e == "mouse_click" then
- if ui.isBack(b, c2) then return end
- if err then
- if ui.hit(retryBtn, b, c2) then
- bankId = nil
- refresh()
- end
- elseif ui.hit(payBtn, b, c2) then
- payFlow()
- refresh()
- elseif c2 >= 4 and c2 <= 8 then
- refresh()
- end
- elseif e == "pocketos_message" then
- -- a bank receipt probably just arrived; update the number
- refresh()
- end
- end
- end
- return M
- ]==],
- ["pocketos/apps/market.lua"] = [==[
- -- PocketOS app: Market - buy and sell with other players.
- -- Listings live on the bank server. No shipping, this is Minecraft:
- -- message the seller through Messages to arrange the handover, and
- -- pay them from your bank balance when you're ready - paying is always
- -- a manual, confirmed step.
- local M = {}
- local ui, ctx
- local bankId
- local function findBank()
- if ctx.config and tonumber(ctx.config.bank) then
- return tonumber(ctx.config.bank)
- end
- return rednet.lookup("pocketos.bank")
- end
- local function request(msg)
- if not ctx.modem then return nil, "No wireless modem" end
- if not bankId then bankId = findBank() end
- if not bankId then return nil, "No bank in range" end
- msg.label = os.getComputerLabel()
- rednet.send(bankId, msg, "pocketos.bank")
- local deadline = os.clock() + 3
- while true do
- local left = deadline - os.clock()
- if left <= 0 then return nil, "Bank not answering" end
- local id, resp = rednet.receive("pocketos.bank", left)
- if id == bankId and type(resp) == "table" then
- return resp
- elseif id == nil then
- return nil, "Bank not answering"
- end
- end
- end
- -- drops a note into the other player's Messages app
- local function sendNote(to, text)
- rednet.send(tonumber(to),
- { text = text, label = os.getComputerLabel() }, "pocketos")
- end
- local function createFlow()
- ui.clear(colors.white)
- ui.topBar("New Listing")
- ui.text(2, 3, "Item name:", colors.gray, colors.white)
- ui.fill(2, 4, ui.w - 2, 1, colors.lightGray)
- local name = ui.input(3, 4, { width = ui.w - 4 })
- if name == nil then return end
- name = name:gsub("^%s+", ""):gsub("%s+$", ""):sub(1, 20)
- if name == "" then return end
- ui.text(2, 6, "Price ($):", colors.gray, colors.white)
- ui.fill(2, 7, ui.w - 2, 1, colors.lightGray)
- local price = ui.input(3, 7, { width = ui.w - 4 })
- if price == nil then return end
- price = tonumber(price)
- if not price or price <= 0 then
- ui.center(12, "Price must be a number", colors.red, colors.white)
- sleep(1.2)
- return
- end
- price = math.floor(price)
- ui.text(2, 9, "Details (optional):", colors.gray, colors.white)
- ui.fill(2, 10, ui.w - 2, 1, colors.lightGray)
- local desc = ui.input(3, 10, { width = ui.w - 4 })
- if desc == nil then return end
- desc = desc:sub(1, 60)
- local pick = ui.dialog("Post It?", { name:sub(1, 18), "$" .. price },
- { { "Post", colors.green }, { "Cancel", colors.gray } })
- if pick ~= 1 then return end
- local resp, err = request({ type = "mkpost", name = name, price = price, desc = desc })
- if resp and resp.ok then
- ui.center(13, "Listed!", colors.green, colors.white)
- else
- ui.center(13, tostring((resp and resp.err) or err or "Failed"):sub(1, 24),
- colors.red, colors.white)
- end
- sleep(1)
- end
- local function detail(it)
- local mine = it.seller == os.getComputerID()
- while true do
- ui.clear(colors.white)
- ui.topBar(tostring(it.name):sub(1, 14))
- ui.center(3, "$ " .. it.price, colors.green, colors.white)
- ui.center(5, ("Sold by " .. (it.label and (it.label .. " ") or "")
- .. "#" .. it.seller):sub(1, 24), colors.gray, colors.white)
- local y = 7
- if it.desc and it.desc ~= "" then
- for _, l in ipairs(ui.wrap(it.desc, ui.w - 4)) do
- if y > 12 then break end
- ui.text(3, y, l, colors.black, colors.white)
- y = y + 1
- end
- end
- local payBtn, msgBtn, rmBtn
- if mine then
- rmBtn = ui.button(math.floor((ui.w - 16) / 2) + 1, 15, "Remove listing", colors.red)
- ui.center(18, "This one's yours", colors.lightGray, colors.white)
- else
- payBtn = ui.button(3, 15, "Pay $" .. it.price, colors.green)
- msgBtn = ui.button(ui.w - 6, 15, "Msg", colors.blue)
- ui.center(18, "Msg them to arrange it", colors.lightGray, colors.white)
- end
- local e, a, b, c = os.pullEvent("mouse_click")
- if ui.isBack(b, c) then return end
- if mine and ui.hit(rmBtn, b, c) then
- local pick = ui.dialog("Remove?", { tostring(it.name):sub(1, 18) },
- { { "Remove", colors.red }, { "Keep", colors.gray } })
- if pick == 1 then
- request({ type = "mkremove", id = it.id })
- return
- end
- elseif not mine and ui.hit(payBtn, b, c) then
- local pick = ui.dialog("Confirm",
- { "Pay $" .. it.price .. " to", "#" .. it.seller .. " for this?" },
- { { "Pay", colors.green }, { "Cancel", colors.gray } })
- if pick == 1 then
- local resp, err = request({ type = "pay", to = it.seller, amount = it.price })
- if resp and resp.ok then
- sendNote(it.seller, "[Market] Paid $" .. it.price .. " for: " .. it.name)
- ui.dialog("Paid!", { "Now message them to", "arrange the handover." },
- { { "OK", colors.blue } })
- else
- ui.dialog("Failed",
- { tostring((resp and resp.err) or err or "?"):sub(1, 18) },
- { { "OK", colors.blue } })
- end
- end
- elseif not mine and ui.hit(msgBtn, b, c) then
- sendNote(it.seller, "[Market] Interested in: " .. it.name .. " ($" .. it.price .. ")")
- ui.dialog("Sent!", { "Check Messages for", "their reply." },
- { { "OK", colors.blue } })
- end
- end
- end
- function M.run(u, c)
- ui, ctx = u, c
- local items, err
- local scroll = 0
- local function fetch()
- ui.clear(colors.white)
- ui.topBar("Market")
- ui.center(9, "Loading...", colors.gray, colors.white)
- local resp
- resp, err = request({ type = "mklist" })
- if resp and resp.ok and type(resp.items) == "table" then
- items = resp.items
- err = nil
- else
- items = nil
- err = (resp and resp.err) or err or "Bank offline"
- end
- scroll = 0
- end
- fetch()
- while true do
- local retryBtn
- ui.clear(colors.white)
- ui.topBar("Market", "+", colors.blue)
- local rows = {}
- if err then
- ui.center(6, "Can't reach the market", colors.red, colors.white)
- ui.center(8, tostring(err):sub(1, 24), colors.gray, colors.white)
- retryBtn = ui.button(math.floor((ui.w - 7) / 2) + 1, 12, "Retry", colors.blue)
- elseif #items == 0 then
- ui.center(8, "Nothing for sale yet", colors.gray, colors.white)
- ui.center(10, "Tap + to list an item", colors.gray, colors.white)
- else
- local y = 3
- for i = scroll + 1, #items do
- if y + 1 > ui.h - 1 then break end
- local it = items[i]
- ui.text(2, y, tostring(it.name):sub(1, 17), colors.black, colors.white)
- local p = "$" .. it.price
- ui.text(ui.w - #p, y, p, colors.green, colors.white)
- local who
- if it.seller == os.getComputerID() then
- who = "your listing"
- else
- who = "by " .. (it.label and (it.label .. " ") or "") .. "#" .. it.seller
- end
- ui.text(2, y + 1, who:sub(1, ui.w - 2), colors.gray, colors.white)
- rows[#rows + 1] = { y1 = y, y2 = y + 1, it = it }
- y = y + 3
- end
- end
- local e, a, b, c2 = os.pullEvent()
- if e == "mouse_click" then
- if ui.isBack(b, c2) then return end
- if c2 == 1 and b >= ui.w - 3 then
- createFlow()
- fetch()
- elseif err and ui.hit(retryBtn, b, c2) then
- bankId = nil
- fetch()
- else
- for _, r in ipairs(rows) do
- if c2 >= r.y1 and c2 <= r.y2 then
- detail(r.it)
- fetch()
- break
- end
- end
- end
- elseif e == "mouse_scroll" and items then
- scroll = math.max(0, math.min(scroll + a, math.max(0, #items - 1)))
- end
- end
- end
- return M
- ]==],
- ["pocketos/apps/maps.lua"] = [==[
- -- PocketOS app: Maps - save waypoints and navigate back to them.
- -- A compass needle points at the selected place. Walk a few blocks and
- -- the needle rotates to your direction of travel (up = straight ahead,
- -- N marker = true north); standing still it points with north up.
- -- Waypoints are saved in /.pocketos and survive OS updates.
- local M = {}
- local ui, ctx
- local atan2 = math.atan2 or function(y, x) return math.atan(y, x) end
- local function flatDist(a, b)
- local dx, dz = b.x - a.x, b.z - a.z
- return math.sqrt(dx * dx + dz * dz)
- end
- local function fmtDist(d)
- if d >= 1000 then
- return string.format("%.1fkm", d / 1000)
- end
- return math.floor(d + 0.5) .. "m"
- end
- local function loadList()
- return ctx.load("waypoints", {})
- end
- local function saveList(list)
- ctx.save("waypoints", list)
- end
- local function inputAt(y, label, default)
- ui.text(2, y, label, colors.gray, colors.white)
- ui.fill(8, y, ui.w - 8, 1, colors.lightGray)
- return ui.input(9, y, { default = default or "", width = ui.w - 9 })
- end
- local function addPlace(list)
- ui.clear(colors.white)
- ui.topBar("Add Place")
- local fix = ctx.fix
- if fix then
- ui.text(2, 3, "Prefilled with where", colors.lightGray, colors.white)
- ui.text(2, 4, "you are right now.", colors.lightGray, colors.white)
- end
- local name = inputAt(6, "Name:", "")
- if name == nil then return end
- name = name:gsub("^%s+", ""):gsub("%s+$", ""):sub(1, 12)
- if name == "" then return end
- local xs = inputAt(8, "X:", fix and tostring(fix.x) or "")
- if xs == nil then return end
- local ys = inputAt(10, "Y:", fix and tostring(fix.y) or "")
- if ys == nil then return end
- local zs = inputAt(12, "Z:", fix and tostring(fix.z) or "")
- if zs == nil then return end
- local x, y, z = tonumber(xs), tonumber(ys), tonumber(zs)
- if not (x and y and z) then
- ui.center(15, "Coords must be numbers", colors.red, colors.white)
- sleep(1.4)
- return
- end
- list[#list + 1] = {
- name = name,
- x = math.floor(x),
- y = math.floor(y),
- z = math.floor(z),
- }
- saveList(list)
- ui.center(15, "Saved " .. name .. "!", colors.green, colors.white)
- sleep(0.8)
- end
- local function drawCompass(rel, heading)
- local cx, cy = math.floor(ui.w / 2) + 1, 11
- local rx, ry = 8, 5
- -- ring
- for i = 0, 23 do
- local a = i * math.pi / 12
- local px = cx + math.floor(math.sin(a) * rx + 0.5)
- local py = cy - math.floor(math.cos(a) * ry + 0.5)
- ui.text(px, py, "\7", colors.lightGray, colors.white)
- end
- -- north marker (rotates when the needle is heading-relative)
- local an = -(heading or 0)
- ui.text(cx + math.floor(math.sin(an) * rx + 0.5),
- cy - math.floor(math.cos(an) * ry + 0.5), "N", colors.gray, colors.white)
- -- needle
- for _, f in ipairs({ 0.45, 0.75 }) do
- local px = cx + math.floor(math.sin(rel) * rx * f + 0.5)
- local py = cy - math.floor(math.cos(rel) * ry * f + 0.5)
- ui.text(px, py, "\7", colors.blue, colors.white)
- end
- local tx = cx + math.floor(math.sin(rel) * rx + 0.5)
- local ty = cy - math.floor(math.cos(rel) * ry + 0.5)
- ui.text(tx, ty, "\4", colors.red, colors.white)
- ui.text(cx, cy, "\7", colors.black, colors.white)
- end
- local function navigate(list, idx)
- local wp = list[idx]
- local heading = nil
- local prev = ctx.fix
- os.queueEvent("pocketos_locate")
- local timer = os.startTimer(0.5)
- while true do
- local fix = ctx.fix
- if fix and not prev then
- prev = fix
- elseif fix and prev then
- local mdx, mdz = fix.x - prev.x, fix.z - prev.z
- if mdx * mdx + mdz * mdz >= 4 then
- heading = atan2(mdx, -mdz)
- prev = fix
- end
- end
- ui.clear(colors.white)
- ui.topBar(wp.name:sub(1, 12), "Del", colors.red)
- ui.center(2, wp.x .. ", " .. wp.y .. ", " .. wp.z, colors.lightGray, colors.white)
- if not fix then
- ui.center(9, "No GPS signal", colors.red, colors.white)
- ui.center(11, "Waiting for a fix...", colors.gray, colors.white)
- else
- local dx, dz = wp.x - fix.x, wp.z - fix.z
- local d = math.sqrt(dx * dx + dz * dz)
- if d < 3 then
- ui.center(9, "\4 You're here! \4", colors.green, colors.white)
- ui.center(11, "(within 3 blocks)", colors.gray, colors.white)
- local dy = wp.y - fix.y
- if math.abs(dy) >= 2 then
- ui.center(13, (dy > 0 and "\24 " or "\25 ") .. math.abs(dy) .. " blocks",
- colors.gray, colors.white)
- end
- else
- local bearing = atan2(dx, -dz)
- drawCompass(bearing - (heading or 0), heading)
- ui.center(17, "Distance " .. fmtDist(d), colors.black, colors.white)
- local dy = wp.y - fix.y
- if math.abs(dy) >= 2 then
- ui.center(18, (dy > 0 and "\24 up " or "\25 down ") .. math.abs(dy),
- colors.gray, colors.white)
- end
- if heading then
- ui.center(19, "Arrow = your direction", colors.lightGray, colors.white)
- else
- ui.center(19, "North up - walk to aim", colors.lightGray, colors.white)
- end
- end
- end
- local e, a, b, c = os.pullEvent()
- if e == "timer" and a == timer then
- os.queueEvent("pocketos_locate")
- timer = os.startTimer(0.5)
- elseif e == "mouse_click" then
- if ui.isBack(b, c) then return end
- if c == 1 and b >= ui.w - 4 then
- local pick = ui.dialog("Delete?", { wp.name },
- { { "Delete", colors.red }, { "Keep", colors.gray } })
- if pick == 1 then
- table.remove(list, idx)
- saveList(list)
- return
- end
- end
- end
- -- "pocketos_fix" events just fall through and redraw
- end
- end
- function M.run(u, c)
- ui, ctx = u, c
- local list = loadList()
- local scroll = 0
- os.queueEvent("pocketos_locate")
- local timer = os.startTimer(2)
- while true do
- ui.clear(colors.white)
- ui.topBar("Maps", "+", colors.blue)
- local fix = ctx.fix
- local rows = {}
- local y = 3
- for i = scroll + 1, #list do
- if y > ui.h - 2 then break end
- local wp = list[i]
- ui.text(2, y, "\4", colors.blue, colors.white)
- ui.text(4, y, wp.name:sub(1, 12), colors.black, colors.white)
- local d = fix and fmtDist(flatDist(fix, wp)) or "-"
- ui.text(ui.w - #d, y, d, colors.gray, colors.white)
- rows[#rows + 1] = { y = y, i = i }
- y = y + 2
- end
- if #list == 0 then
- ui.center(8, "No places saved yet", colors.gray, colors.white)
- ui.center(10, "Tap + to add one", colors.gray, colors.white)
- end
- if fix then
- ui.center(ui.h, "You: " .. fix.x .. " " .. fix.y .. " " .. fix.z,
- colors.lightGray, colors.white)
- else
- ui.center(ui.h, "No GPS signal", colors.red, colors.white)
- end
- local e, a, b, c2 = os.pullEvent()
- if e == "timer" and a == timer then
- os.queueEvent("pocketos_locate")
- timer = os.startTimer(3)
- elseif e == "mouse_click" then
- if ui.isBack(b, c2) then return end
- if c2 == 1 and b >= ui.w - 3 then
- addPlace(list)
- else
- for _, r in ipairs(rows) do
- if c2 == r.y then
- navigate(list, r.i)
- break
- end
- end
- end
- elseif e == "mouse_scroll" then
- scroll = math.max(0, math.min(scroll + a, math.max(0, #list - 1)))
- end
- end
- end
- return M
- ]==],
- ["pocketos/apps/gps.lua"] = [==[
- -- PocketOS app: GPS - shows this device's coordinates.
- -- Positions come from the OS's background locator, which needs a
- -- wireless modem and a GPS host cluster in range (see gpshost.lua).
- local M = {}
- function M.run(ui, ctx)
- os.queueEvent("pocketos_locate") -- ask for a fresh fix right away
- local tick = 0
- local timer = os.startTimer(1)
- while true do
- local fix = ctx.fix
- ui.clear(colors.white)
- ui.topBar("GPS")
- if not ctx.modem then
- ui.center(7, "No wireless modem", colors.red, colors.white)
- ui.center(9, "GPS needs a wireless", colors.gray, colors.white)
- ui.center(10, "pocket computer", colors.gray, colors.white)
- elseif fix then
- ui.text(6, 5, "X", colors.gray, colors.white)
- ui.text(10, 5, tostring(fix.x), colors.black, colors.white)
- ui.text(6, 7, "Y", colors.gray, colors.white)
- ui.text(10, 7, tostring(fix.y), colors.black, colors.white)
- ui.text(6, 9, "Z", colors.gray, colors.white)
- ui.text(10, 9, tostring(fix.z), colors.black, colors.white)
- local age = math.floor(os.clock() - fix.at)
- if age <= 7 then
- ui.center(12, "\7 Live", colors.green, colors.white)
- else
- ui.center(12, "Updated " .. age .. "s ago", colors.gray, colors.white)
- end
- else
- ui.center(4, "No GPS signal", colors.red, colors.white)
- ui.center(6, "You need 4 computers", colors.gray, colors.white)
- ui.center(7, "with wireless modems", colors.gray, colors.white)
- ui.center(8, "running as gps hosts", colors.gray, colors.white)
- ui.center(9, "within radio range.", colors.gray, colors.white)
- ui.center(11, "Run 'gpshost' on each", colors.lightGray, colors.white)
- ui.center(12, "to set them up.", colors.lightGray, colors.white)
- end
- local trk = ctx.config and tonumber(ctx.config.tracker)
- if trk then
- if fix then
- ui.center(15, "Reporting to #" .. trk, colors.blue, colors.white)
- else
- ui.center(15, "Report to #" .. trk .. ": no fix", colors.orange, colors.white)
- end
- else
- ui.center(15, "Reporting: off", colors.lightGray, colors.white)
- end
- ui.center(17, "Tap to refresh", colors.lightGray, colors.white)
- local e, a, b, c = os.pullEvent()
- if e == "timer" and a == timer then
- tick = tick + 1
- if tick % 2 == 0 then os.queueEvent("pocketos_locate") end
- timer = os.startTimer(1)
- elseif e == "mouse_click" then
- if ui.isBack(b, c) then return end
- os.queueEvent("pocketos_locate")
- end
- -- "pocketos_fix" events just fall through and trigger a redraw
- end
- end
- return M
- ]==],
- ["pocketos/apps/dev.lua"] = [==[
- -- PocketOS app: Dev - developer tools (visible only in developer mode).
- -- * Remote Shell - run shell commands on other PocketOS devices that
- -- also have developer mode on.
- -- * Fleet - live locations of every device, from the tracker hub.
- -- * Ledger - recent bank transactions.
- local M = {}
- local ui, ctx
- -- generic request/response over a rednet protocol to a known id
- local function ask(to, proto, msg, timeout)
- if not ctx.modem then return nil, "No wireless modem" end
- if not to then return nil, "No target" end
- rednet.send(to, msg, proto)
- local deadline = os.clock() + (timeout or 3)
- while true do
- local left = deadline - os.clock()
- if left <= 0 then return nil, "No response" end
- local id, resp = rednet.receive(proto, left)
- if id == to and type(resp) == "table" then return resp end
- if id == nil then return nil, "No response" end
- end
- end
- local function bankId()
- if ctx.config and tonumber(ctx.config.bank) then return tonumber(ctx.config.bank) end
- return rednet.lookup("pocketos.bank")
- end
- ------------------------------------------------------ remote shell --
- local function askID(title, prompt)
- ui.clear(colors.white)
- ui.topBar(title)
- ui.text(2, 4, prompt, colors.gray, colors.white)
- ui.fill(2, 6, ui.w - 2, 1, colors.lightGray)
- local s = ui.input(3, 6, { default = tostring(ctx.load("dev_last", "") or ""), width = ui.w - 4 })
- if s == nil then return nil end
- return tonumber((s:gsub("%s", "")))
- end
- local function remoteShell()
- local target = askID("Remote Shell", "Target computer ID:")
- if not target then return end
- ctx.save("dev_last", target)
- ui.clear(colors.white)
- ui.topBar("Shell #" .. target)
- ui.center(9, "Connecting...", colors.gray, colors.white)
- local resp, err = ask(target, "pocketos.dev", { type = "ping" })
- if not resp then
- ui.dialog("Unreachable",
- { tostring(err), "Is it on and in range?" }, { { "OK", colors.blue } })
- return
- end
- local history = { "Connected to " .. (resp.label or ("#" .. target))
- .. " (PocketOS " .. tostring(resp.version or "?") .. ")",
- "Type a command below. 'exit' quits." }
- local scroll = 0
- local function draw()
- ui.clear(colors.white)
- ui.topBar("Shell #" .. target)
- -- flatten history into wrapped lines
- local lines = {}
- for _, entry in ipairs(history) do
- local color = colors.black
- local text = entry
- if type(entry) == "table" then color = entry.c; text = entry[1] end
- for _, l in ipairs(ui.wrap(text, ui.w - 1)) do
- lines[#lines + 1] = { l, color }
- end
- end
- local view = ui.h - 3
- local maxScroll = math.max(0, #lines - view)
- if scroll > maxScroll then scroll = maxScroll end
- local first = #lines - view - scroll + 1
- local y = 3
- for i = math.max(1, first), #lines do
- if y > ui.h - 1 then break end
- if i >= first then
- ui.text(2, y, lines[i][1], lines[i][2], colors.white)
- y = y + 1
- end
- end
- ui.fill(1, ui.h, ui.w, 1, colors.lightGray)
- ui.text(2, ui.h, "$", colors.green, colors.lightGray)
- end
- while true do
- draw()
- local cmd = ui.input(4, ui.h, { width = ui.w - 4 })
- if cmd == nil then
- return -- back arrow tapped
- elseif cmd:gsub("%s", "") == "" then
- -- ignore empty
- elseif cmd == "exit" then
- return
- else
- history[#history + 1] = { "$ " .. cmd, colors.blue }
- scroll = 0
- draw()
- ui.text(2, ui.h - 1, "running...", colors.gray, colors.white)
- local r, e2 = ask(target, "pocketos.dev", { type = "exec", cmd = cmd }, 12)
- if r and r.ok then
- for line in (r.output .. "\n"):gmatch("(.-)\n") do
- history[#history + 1] = { line, colors.gray }
- end
- else
- history[#history + 1] = { "! " .. tostring(e2 or "failed"), colors.red }
- end
- while #history > 200 do table.remove(history, 1) end
- end
- end
- end
- ------------------------------------------------------------- fleet --
- local function fleet()
- local tid = ctx.config and tonumber(ctx.config.tracker)
- if not tid and ctx.modem then tid = rednet.lookup("pocketos.track") end
- while true do
- ui.clear(colors.white)
- ui.topBar("Fleet")
- if not tid then
- ui.center(8, "No tracker set", colors.red, colors.white)
- ui.center(10, "Set tracker in", colors.gray, colors.white)
- ui.center(11, "config.lua first", colors.gray, colors.white)
- local e, _, mx, my = os.pullEvent("mouse_click")
- if ui.isBack(mx, my) then return end
- else
- ui.center(9, "Loading...", colors.gray, colors.white)
- local resp, err = ask(tid, "pocketos.track", { type = "query" })
- ui.clear(colors.white)
- ui.topBar("Fleet", "\21", colors.blue)
- if not resp or not resp.devices then
- ui.center(8, "Tracker offline", colors.red, colors.white)
- ui.center(10, tostring(err or ""):sub(1, 24), colors.gray, colors.white)
- else
- local list = resp.devices
- table.sort(list, function(a, b) return a.id < b.id end)
- ui.text(2, 2, "ID Name X/Y/Z", colors.gray, colors.white)
- local y = 3
- for _, d in ipairs(list) do
- if y > ui.h - 1 then break end
- ui.text(2, y, "#" .. d.id, colors.black, colors.white)
- ui.text(6, y, (d.label or "?"):sub(1, 8), colors.black, colors.white)
- local live = d.age <= 30
- ui.text(15, y, (d.x .. "," .. d.y .. "," .. d.z):sub(1, ui.w - 15),
- live and colors.blue or colors.gray, colors.white)
- y = y + 1
- end
- if #list == 0 then
- ui.center(9, "No devices reporting", colors.gray, colors.white)
- end
- end
- ui.center(ui.h, "Tap refresh, back to exit", colors.lightGray, colors.white)
- local e, _, mx, my = os.pullEvent("mouse_click")
- if ui.isBack(mx, my) then return end
- -- any other tap refreshes
- end
- end
- end
- ------------------------------------------------------------ ledger --
- local function ledger()
- local scroll = 0
- while true do
- ui.clear(colors.white)
- ui.topBar("Ledger")
- ui.center(9, "Loading...", colors.gray, colors.white)
- local resp, err = ask(bankId(), "pocketos.bank", { type = "txns", limit = 60 })
- ui.clear(colors.white)
- ui.topBar("Ledger", "\21", colors.blue)
- local rows
- if not resp or not resp.txns then
- ui.center(8, "Bank offline", colors.red, colors.white)
- ui.center(10, tostring(err or ""):sub(1, 24), colors.gray, colors.white)
- else
- rows = resp.txns
- local y = 2
- for i = scroll + 1, #rows do
- if y > ui.h - 1 then break end
- local t = rows[i]
- local line
- if t.kind == "deposit" then
- line = "ATM -> #" .. t.to
- else
- line = "#" .. t.from .. " -> #" .. t.to
- end
- ui.text(2, y, (t.time or "") .. " " .. line, colors.black, colors.white)
- local amt = "$" .. t.amount
- ui.text(ui.w - #amt, y,
- amt, t.kind == "deposit" and colors.green or colors.blue, colors.white)
- y = y + 1
- end
- if #rows == 0 then
- ui.center(9, "No transactions yet", colors.gray, colors.white)
- end
- end
- ui.center(ui.h, "Scroll; back to exit", colors.lightGray, colors.white)
- local e, a, mx, my = os.pullEvent()
- if e == "mouse_click" then
- if ui.isBack(mx, my) then return end
- if my == 1 and mx >= ui.w - 2 then scroll = 0 end -- refresh
- elseif e == "mouse_scroll" and rows then
- scroll = math.max(0, math.min(scroll + a, math.max(0, #rows - 1)))
- end
- end
- end
- --------------------------------------------------------------- menu --
- ------------------------------------------------------------- bans --
- local function bans()
- local target = askID("Ban User", "Ban/unban which ID?")
- if not target then return end
- ctx.save("dev_last", target)
- while true do
- ui.clear(colors.white)
- ui.topBar("Ban #" .. target)
- ui.center(3, "Manage device #" .. target, colors.gray, colors.white)
- local banBtn = ui.button(math.floor((ui.w - 17) / 2) + 1, 6, "Ban with reason", colors.red)
- local unBtn = ui.button(math.floor((ui.w - 7) / 2) + 1, 9, "Unban", colors.green)
- ui.center(12, "Banning locks their OS", colors.lightGray, colors.white)
- ui.center(13, "until you unban them.", colors.lightGray, colors.white)
- ui.center(ui.h, "Target must be online", colors.lightGray, colors.white)
- local e, a, mx, my = os.pullEvent("mouse_click")
- if ui.isBack(mx, my) then return end
- if ui.hit(banBtn, mx, my) then
- ui.clear(colors.white)
- ui.topBar("Ban #" .. target)
- ui.text(2, 3, "Reason for the ban:", colors.gray, colors.white)
- ui.fill(2, 5, ui.w - 2, 1, colors.lightGray)
- local reason = ui.input(3, 5, { width = ui.w - 4 })
- if reason ~= nil and reason:gsub("%s", "") ~= "" then
- local pick = ui.dialog("Confirm Ban",
- { "Lock device #" .. target .. "?" },
- { { "Ban", colors.red }, { "Cancel", colors.gray } })
- if pick == 1 then
- local resp, err = ask(target, "pocketos.dev",
- { type = "ban", reason = reason }, 3)
- if resp and resp.ok then
- ui.dialog("Banned", { "#" .. target .. " is locked." },
- { { "OK", colors.blue } })
- else
- ui.dialog("No Response",
- { "Device offline?", "Ban only applies", "while it's online." },
- { { "OK", colors.blue } })
- end
- return
- end
- end
- elseif ui.hit(unBtn, mx, my) then
- local resp, err = ask(target, "pocketos.dev", { type = "unban" })
- if resp and resp.ok then
- ui.dialog("Unbanned", { "#" .. target .. " is free." },
- { { "OK", colors.blue } })
- else
- ui.dialog("No Response",
- { tostring(err or "Offline"):sub(1, 18) }, { { "OK", colors.blue } })
- end
- return
- end
- end
- end
- --------------------------------------------------------------- menu --
- function M.run(u, c)
- ui, ctx = u, c
- local tools = {
- { name = "Remote Shell", desc = "Run commands remotely", run = remoteShell, color = colors.cyan },
- { name = "Fleet", desc = "Device locations", run = fleet, color = colors.orange },
- { name = "Ledger", desc = "Bank transactions", run = ledger, color = colors.green },
- { name = "Ban User", desc = "Lock a device out", run = bans, color = colors.red },
- }
- while true do
- ui.clear(colors.white)
- ui.topBar("Developer")
- local rows = {}
- local y = 3
- for _, t in ipairs(tools) do
- ui.card(2, y, ui.w - 3, 3, t.color)
- ui.text(4, y + 1, t.name, colors.white, t.color)
- ui.text(4, y + 2, t.desc, colors.white, t.color)
- rows[#rows + 1] = { y1 = y, y2 = y + 2, run = t.run }
- y = y + 4
- end
- ui.center(ui.h, "Dev mode: #" .. os.getComputerID(), colors.lightGray, colors.white)
- local e, a, mx, my = os.pullEvent("mouse_click")
- if ui.isBack(mx, my) then return end
- for _, r in ipairs(rows) do
- if my >= r.y1 and my <= r.y2 then
- r.run()
- break
- end
- end
- end
- end
- return M
- ]==],
- ["pocketos/apps/settings.lua"] = [==[
- -- PocketOS app: Settings + Software Update.
- local M = {}
- local function doUpdate(ui, ctx)
- ui.clear(colors.white)
- ui.topBar("Update")
- ui.text(2, 3, "Installed: PocketOS " .. ctx.version, colors.gray, colors.white)
- ui.text(2, 5, "Pastebin code or URL", colors.gray, colors.white)
- ui.text(2, 6, "of the new installer:", colors.gray, colors.white)
- ui.fill(2, 8, ui.w - 2, 1, colors.lightGray)
- -- prefill: last code actually used wins, else the config default
- local def = ctx.load("update_src", "")
- if def == "" then
- local cu = ctx.config and ctx.config.update
- if type(cu) == "string" then def = cu end
- end
- local s = ui.input(3, 8, { default = def, width = ui.w - 4 })
- if s == nil then return end
- s = s:gsub("%s", "")
- if s == "" then return end
- if not http then
- ui.center(11, "HTTP is disabled :(", colors.red, colors.white)
- sleep(1.5)
- return
- end
- local url = s:match("^https?://") and s or ("https://pastebin.com/raw/" .. s)
- ui.center(11, "Downloading...", colors.gray, colors.white)
- local ok, res = pcall(http.get, url)
- if not ok or not res then
- ui.center(13, "Download failed", colors.red, colors.white)
- sleep(1.5)
- return
- end
- local body = res.readAll() or ""
- res.close()
- if not body:find("PocketOS installer", 1, true) then
- ui.center(13, "Not a PocketOS installer", colors.red, colors.white)
- sleep(1.8)
- return
- end
- ctx.save("update_src", s)
- local pick = ui.dialog("Install?",
- { "Music, messages and", "scores are kept.", "Reboots when done." },
- { { "Install", colors.blue }, { "Cancel", colors.gray } })
- if pick ~= 1 then return end
- term.setBackgroundColor(colors.black)
- term.setTextColor(colors.white)
- term.clear()
- term.setCursorPos(1, 1)
- local fn, err = load(body, "installer", "t", _ENV)
- if not fn then
- ui.clear(colors.white)
- ui.center(9, "Installer is broken:", colors.red, colors.white)
- ui.center(11, tostring(err):sub(1, 24), colors.gray, colors.white)
- sleep(2)
- return
- end
- local ok2, err2 = pcall(fn) -- writes files and reboots
- if not ok2 then
- print("Update failed: " .. tostring(err2))
- print("Re-run the update, or reinstall.")
- sleep(3)
- end
- end
- local function factoryReset(ui, ctx)
- local pick = ui.dialog("Factory Reset?",
- { "Erases ALL data:", "messages, scores,", "songs, keys, settings." },
- { { "Erase", colors.red }, { "Cancel", colors.gray } })
- if pick ~= 1 then return end
- local sure = ui.dialog("Are you sure?",
- { "This cannot be undone." },
- { { "Erase All", colors.red }, { "Cancel", colors.gray } })
- if sure ~= 1 then return end
- if fs.exists(ctx.dataDir) then fs.delete(ctx.dataDir) end
- if fs.exists("/music") then fs.delete("/music") end
- pcall(os.setComputerLabel, nil)
- ui.clear(colors.white)
- ui.center(9, "Erased.", colors.black, colors.white)
- ui.center(11, "Rebooting...", colors.gray, colors.white)
- sleep(1.2)
- os.reboot()
- end
- local function timeSettings(ui, ctx)
- while true do
- ui.clear(colors.white)
- ui.topBar("Date & Time")
- ui.center(3, ui.clock(), colors.black, colors.white)
- ui.center(4, "live preview", colors.lightGray, colors.white)
- ui.text(2, 7, "Format", colors.black, colors.white)
- local b12 = ui.button(ui.w - 13, 7, "12h",
- ui.time24 and colors.lightGray or colors.blue,
- ui.time24 and colors.gray or colors.white)
- local b24 = ui.button(ui.w - 6, 7, "24h",
- ui.time24 and colors.blue or colors.lightGray,
- ui.time24 and colors.white or colors.gray)
- ui.text(2, 10, "Time zone", colors.black, colors.white)
- local tz = "UTC" .. (ui.tzOffset >= 0 and "+" or "") .. tostring(ui.tzOffset)
- local minus = ui.button(3, 12, "-", colors.blue)
- ui.center(12, tz, colors.black, colors.white)
- local plus = ui.button(ui.w - 3, 12, "+", colors.blue)
- ui.center(ui.h, "Clock is UTC + offset", colors.lightGray, colors.white)
- local e, b, mx, my = os.pullEvent("mouse_click")
- if ui.isBack(mx, my) then return end
- if ui.hit(b12, mx, my) then
- ui.time24 = false
- ctx.save("time_24h", false)
- elseif ui.hit(b24, mx, my) then
- ui.time24 = true
- ctx.save("time_24h", true)
- elseif ui.hit(minus, mx, my) then
- ui.tzOffset = math.max(-12, ui.tzOffset - 1)
- ctx.save("tz_offset", ui.tzOffset)
- elseif ui.hit(plus, mx, my) then
- ui.tzOffset = math.min(14, ui.tzOffset + 1)
- ctx.save("tz_offset", ui.tzOffset)
- end
- end
- end
- function M.run(ui, ctx)
- local idTaps = 0
- while true do
- ui.clear(colors.white)
- ui.topBar("Settings")
- local trk = ctx.config and tonumber(ctx.config.tracker)
- local tz = "UTC" .. (ui.tzOffset >= 0 and "+" or "") .. tostring(ui.tzOffset)
- local items = {
- { y = 3, label = "Name", value = os.getComputerLabel() or "Not set", act = "name", tap = true },
- { y = 5, label = "Computer ID", value = "#" .. os.getComputerID(), act = "devcount" },
- { y = 7, label = "Date & Time", value = (ui.time24 and "24h " or "12h ") .. tz, act = "time", tap = true },
- { y = 9, label = "Software Update", value = "", act = "update", tap = true },
- { y = 11, label = "Wireless", value = ctx.modem and "On" or "Off" },
- { y = 12, label = "Tracker", value = trk and ("#" .. trk) or "Off" },
- { y = 13, label = "Version", value = "PocketOS " .. ctx.version },
- }
- if ctx.dev then
- items[#items + 1] = { y = 15, label = "Developer Mode", value = "On", act = "dev", tap = true }
- end
- for _, it in ipairs(items) do
- ui.text(2, it.y, it.label, colors.black, colors.white)
- local v = tostring(it.value):sub(1, 12)
- local off = it.tap and 2 or 0
- if #v > 0 then
- ui.text(ui.w - #v - off, it.y,
- it.act == "dev" and "\7 " .. v or v,
- it.act == "dev" and colors.green or colors.gray, colors.white)
- end
- if it.tap then ui.text(ui.w - 1, it.y, ">", colors.gray, colors.white) end
- end
- local reboot = ui.button(3, 17, "Reboot", colors.blue)
- local off = ui.button(14, 17, "Shut Down", colors.red)
- local reset = ui.button(math.floor((ui.w - 15) / 2) + 1, 19, "Factory Reset", colors.red)
- local e, b, mx, my = os.pullEvent("mouse_click")
- if ui.isBack(mx, my) then return end
- local clicked
- for _, it in ipairs(items) do
- if it.y == my then clicked = it end
- end
- if clicked and clicked.act == "devcount" then
- -- tap the Computer ID row 7 times to toggle developer mode
- idTaps = idTaps + 1
- local admin = ctx.isAdmin and ctx.isAdmin(os.getComputerID())
- if not ctx.dev and idTaps >= 7 then
- idTaps = 0
- if admin then
- ctx.dev = true
- ctx.save("devmode", true)
- ui.dialog("Developer Mode",
- { "You are now a", "developer!" }, { { "Nice", colors.green } })
- else
- ui.dialog("Restricted",
- { "Developer access is", "for admins only." }, { { "OK", colors.blue } })
- end
- elseif not ctx.dev and idTaps >= 3 then
- ui.text(2, 6, (7 - idTaps) .. " taps to dev mode",
- colors.lightGray, colors.white)
- sleep(0.6)
- end
- elseif clicked and clicked.act == "name" then
- ui.fill(1, 3, ui.w, 1, colors.lightGray)
- local newName = ui.input(2, 3, { default = os.getComputerLabel() or "", width = ui.w - 2 })
- if newName and newName ~= "" then
- os.setComputerLabel(newName)
- end
- elseif clicked and clicked.act == "time" then
- timeSettings(ui, ctx)
- elseif clicked and clicked.act == "update" then
- doUpdate(ui, ctx)
- elseif clicked and clicked.act == "dev" then
- local pick = ui.dialog("Developer Mode",
- { "Turn OFF developer", "mode? Hides the Dev", "app and remote access." },
- { { "Turn Off", colors.red }, { "Keep On", colors.gray } })
- if pick == 1 then
- ctx.dev = false
- ctx.save("devmode", false)
- end
- elseif ui.hit(reboot, mx, my) then
- os.reboot()
- elseif ui.hit(off, mx, my) then
- os.shutdown()
- elseif ui.hit(reset, mx, my) then
- factoryReset(ui, ctx)
- end
- end
- end
- return M
- ]==],
- ["pocketos/apps/games/snake.lua"] = [==[
- -- PocketOS app: Snake
- -- Arrow keys / WASD, or tap the screen on the side you want to turn.
- local M = {}
- local function keyDir(k)
- if k == keys.up or k == keys.w then return { x = 0, y = -1 } end
- if k == keys.down or k == keys.s then return { x = 0, y = 1 } end
- if k == keys.left or k == keys.a then return { x = -1, y = 0 } end
- if k == keys.right or k == keys.d then return { x = 1, y = 0 } end
- end
- local function game(ui, best)
- local x1, y1 = 2, 3
- local x2, y2 = ui.w - 1, ui.h - 1
- ui.clear(colors.white)
- ui.topBar("Snake", string.format("%4d", 0))
- ui.fill(1, 2, ui.w, 1, colors.lightGray)
- ui.fill(1, ui.h, ui.w, 1, colors.lightGray)
- ui.fill(1, 2, 1, ui.h - 1, colors.lightGray)
- ui.fill(ui.w, 2, 1, ui.h - 1, colors.lightGray)
- local function cell(x, y, c)
- term.setCursorPos(x, y)
- term.setBackgroundColor(c)
- term.write(" ")
- end
- local occ = {}
- for y = y1, y2 do occ[y] = {} end
- local cx = math.floor((x1 + x2) / 2)
- local cy = math.floor((y1 + y2) / 2)
- local snake = { { x = cx, y = cy }, { x = cx - 1, y = cy }, { x = cx - 2, y = cy } }
- for i, s in ipairs(snake) do
- occ[s.y][s.x] = true
- cell(s.x, s.y, i == 1 and colors.green or colors.lime)
- end
- local dir = { x = 1, y = 0 }
- local nextDir = dir
- local score = 0
- local interval = 0.18
- local food = {}
- local function placeFood()
- while true do
- local fx = math.random(x1, x2)
- local fy = math.random(y1, y2)
- if not occ[fy][fx] then
- food.x, food.y = fx, fy
- cell(fx, fy, colors.red)
- return
- end
- end
- end
- placeFood()
- local hintY = y2 - 1
- ui.center(hintY, "Tap or use arrow keys", colors.gray, colors.white)
- -- wait for the first input before moving
- while true do
- local e, a, b, c = os.pullEvent()
- if e == "key" then
- local d = keyDir(a)
- if d then
- if not (d.x == -dir.x and d.y == -dir.y) then nextDir = d end
- break
- end
- elseif e == "mouse_click" then
- if ui.isBack(b, c) then return "home", score end
- if c >= y1 then break end
- end
- end
- ui.fill(x1, hintY, x2 - x1 + 1, 1, colors.white)
- if food.y == hintY then cell(food.x, food.y, colors.red) end
- for i, s in ipairs(snake) do
- if s.y == hintY then cell(s.x, s.y, i == 1 and colors.green or colors.lime) end
- end
- local timer = os.startTimer(interval)
- while true do
- local e, a, b, c = os.pullEvent()
- if e == "timer" and a == timer then
- if not (nextDir.x == -dir.x and nextDir.y == -dir.y) then dir = nextDir end
- local head = snake[1]
- local hx, hy = head.x + dir.x, head.y + dir.y
- if hx < x1 or hx > x2 or hy < y1 or hy > y2 or occ[hy][hx] then
- cell(head.x, head.y, colors.red)
- local pick = ui.dialog("Game Over",
- { "Score: " .. score, "Best: " .. math.max(best, score) },
- { { "Again", colors.blue }, { "Home", colors.gray } })
- return pick == 1 and "again" or "home", score
- end
- table.insert(snake, 1, { x = hx, y = hy })
- occ[hy][hx] = true
- cell(head.x, head.y, colors.lime)
- cell(hx, hy, colors.green)
- if hx == food.x and hy == food.y then
- score = score + 1
- interval = math.max(0.08, 0.18 - score * 0.004)
- ui.text(ui.w - 4, 1, string.format("%4d", score), colors.gray, colors.white)
- if #snake >= (x2 - x1 + 1) * (y2 - y1 + 1) - 1 then
- local pick = ui.dialog("You Win!",
- { "Score: " .. score },
- { { "Again", colors.blue }, { "Home", colors.gray } })
- return pick == 1 and "again" or "home", score
- end
- placeFood()
- else
- local tail = table.remove(snake)
- occ[tail.y][tail.x] = nil
- cell(tail.x, tail.y, colors.white)
- end
- timer = os.startTimer(interval)
- elseif e == "key" then
- local d = keyDir(a)
- if d and not (d.x == -dir.x and d.y == -dir.y) then nextDir = d end
- elseif e == "mouse_click" then
- if ui.isBack(b, c) then return "home", score end
- if c >= y1 then
- local head = snake[1]
- local dx, dy = b - head.x, c - head.y
- local d
- if math.abs(dx) > math.abs(dy) then
- d = { x = dx > 0 and 1 or -1, y = 0 }
- else
- d = { x = 0, y = dy > 0 and 1 or -1 }
- end
- if d.x == -dir.x and d.y == -dir.y then
- -- reversing isn't allowed; use the other axis instead
- if d.x ~= 0 then
- d = { x = 0, y = dy >= 0 and 1 or -1 }
- else
- d = { x = dx >= 0 and 1 or -1, y = 0 }
- end
- end
- if not (d.x == -dir.x and d.y == -dir.y) then nextDir = d end
- end
- end
- end
- end
- function M.run(ui, ctx)
- local best = ctx.load("snake_best", 0)
- while true do
- local action, score = game(ui, best)
- if score and score > best then
- best = score
- ctx.save("snake_best", best)
- end
- if action ~= "again" then return end
- end
- end
- return M
- ]==],
- ["pocketos/apps/games/blocks.lua"] = [==[
- -- PocketOS app: Blocks - a Block Blast style puzzle.
- -- Tap a piece in the tray, then tap (or drag onto) the 8x8 board.
- -- Full rows and columns clear. Game ends when nothing fits.
- local M = {}
- local N = 8
- local SHAPES = {
- { "#" },
- { "##" }, { "#", "#" },
- { "###" }, { "#", "#", "#" },
- { "####" }, { "#", "#", "#", "#" },
- { "#####" }, { "#", "#", "#", "#", "#" },
- { "##", "##" },
- { "###", "###", "###" },
- { "#.", "##" }, { ".#", "##" }, { "##", "#." }, { "##", ".#" },
- { "#..", "#..", "###" }, { "..#", "..#", "###" },
- { "###", "#.." }, { "###", "..#" },
- { "###", ".#." }, { ".#.", "###" },
- { "#.", "##", "#." }, { ".#", "##", ".#" },
- }
- local PALETTE = {
- colors.blue, colors.red, colors.green, colors.orange,
- colors.purple, colors.cyan, colors.pink,
- }
- local PIECES = {}
- for _, rows in ipairs(SHAPES) do
- local cells, w = {}, 0
- for y, row in ipairs(rows) do
- w = math.max(w, #row)
- for x = 1, #row do
- if row:sub(x, x) == "#" then
- cells[#cells + 1] = { x = x - 1, y = y - 1 }
- end
- end
- end
- PIECES[#PIECES + 1] = { cells = cells, w = w, h = #rows }
- end
- local toBlit = colors.toBlit or function(c)
- local i, v = 0, c
- while v > 1 do
- v = math.floor(v / 2)
- i = i + 1
- end
- return ("0123456789abcdef"):sub(i + 1, i + 1)
- end
- local function newPiece()
- local p = PIECES[math.random(#PIECES)]
- return { cells = p.cells, w = p.w, h = p.h, color = PALETTE[math.random(#PALETTE)] }
- end
- local function canPlace(grid, p, ax, ay)
- for _, c in ipairs(p.cells) do
- local x, y = ax + c.x, ay + c.y
- if x < 1 or x > N or y < 1 or y > N or grid[y][x] then return false end
- end
- return true
- end
- local function anyMove(grid, tray)
- for _, p in pairs(tray) do
- for ay = 1, N - p.h + 1 do
- for ax = 1, N - p.w + 1 do
- if canPlace(grid, p, ax, ay) then return true end
- end
- end
- end
- return false
- end
- local function clearFull(grid)
- local rows, cols = {}, {}
- for y = 1, N do
- local full = true
- for x = 1, N do
- if not grid[y][x] then full = false; break end
- end
- if full then rows[#rows + 1] = y end
- end
- for x = 1, N do
- local full = true
- for y = 1, N do
- if not grid[y][x] then full = false; break end
- end
- if full then cols[#cols + 1] = x end
- end
- for _, y in ipairs(rows) do
- for x = 1, N do grid[y][x] = nil end
- end
- for _, x in ipairs(cols) do
- for y = 1, N do grid[y][x] = nil end
- end
- return #rows + #cols
- end
- local function game(ui, best)
- local boardX = math.floor((ui.w - N * 2) / 2) + 1
- local boardY = 4
- local trayY = 14
- local grid = {}
- for y = 1, N do grid[y] = {} end
- local tray = { newPiece(), newPiece(), newPiece() }
- local score = 0
- local selected, ghost
- local function drawScore()
- ui.fill(1, 2, ui.w, 1, colors.white)
- ui.text(2, 2, "Score " .. score, colors.black, colors.white)
- local b = "Best " .. math.max(best, score)
- ui.text(ui.w - #b, 2, b, colors.gray, colors.white)
- end
- local function drawBoard()
- for y = 1, N do
- local txt, fg, bg = "", "", ""
- for x = 1, N do
- local c = grid[y][x]
- if not c and ghost and ghost.set[y * 16 + x] then c = colors.gray end
- local h = toBlit(c or colors.lightGray)
- txt = txt .. " "
- fg = fg .. h .. h
- bg = bg .. h .. h
- end
- term.setCursorPos(boardX, boardY + y - 1)
- term.blit(txt, fg, bg)
- end
- end
- local function slotX(i) return 2 + (i - 1) * 8 end
- local function drawTray()
- ui.fill(1, trayY - 1, ui.w, 7, colors.white)
- for i = 1, 3 do
- local p = tray[i]
- if p then
- local sx = slotX(i)
- if selected == i then
- ui.fill(sx - 1, trayY - 1, 9, 7, colors.lightGray)
- end
- local ox = sx + math.floor((7 - p.w) / 2)
- local oy = trayY + math.floor((5 - p.h) / 2)
- for _, c in ipairs(p.cells) do
- ui.fill(ox + c.x, oy + c.y, 1, 1, p.color)
- end
- end
- end
- end
- local function onBoard(mx, my)
- return mx >= boardX and mx <= boardX + N * 2 - 1
- and my >= boardY and my <= boardY + N - 1
- end
- local function slotAt(mx, my)
- if my < trayY - 1 or my > trayY + 5 then return nil end
- for i = 1, 3 do
- local sx = slotX(i)
- if mx >= sx - 1 and mx <= sx + 7 then return i end
- end
- end
- local function makeGhost(mx, my)
- local p = tray[selected]
- local bx = math.floor((mx - boardX) / 2) + 1
- local by = my - boardY + 1
- local ax = math.max(1, math.min(bx - math.floor((p.w - 1) / 2), N - p.w + 1))
- local ay = math.max(1, math.min(by - math.floor((p.h - 1) / 2), N - p.h + 1))
- local g = { ax = ax, ay = ay, valid = canPlace(grid, p, ax, ay), set = {} }
- if g.valid then
- for _, c in ipairs(p.cells) do
- g.set[(ay + c.y) * 16 + (ax + c.x)] = true
- end
- end
- return g
- end
- ui.clear(colors.white)
- ui.topBar("Blocks")
- drawScore()
- drawBoard()
- drawTray()
- ui.center(ui.h, "Tap a piece, then board", colors.gray, colors.white)
- while true do
- local e, b, mx, my = os.pullEvent()
- if e == "mouse_click" then
- if ui.isBack(mx, my) then return "home", score end
- local s = slotAt(mx, my)
- if s and tray[s] then
- selected = (selected ~= s) and s or nil
- ghost = nil
- drawTray()
- drawBoard()
- elseif selected and onBoard(mx, my) then
- ghost = makeGhost(mx, my)
- drawBoard()
- end
- elseif e == "mouse_drag" and selected then
- ghost = onBoard(mx, my) and makeGhost(mx, my) or nil
- drawBoard()
- elseif e == "mouse_up" and ghost then
- if ghost.valid then
- local p = tray[selected]
- for _, c in ipairs(p.cells) do
- grid[ghost.ay + c.y][ghost.ax + c.x] = p.color
- end
- score = score + #p.cells
- local lines = clearFull(grid)
- if lines > 0 then score = score + lines * lines * 10 end
- tray[selected] = nil
- selected, ghost = nil, nil
- if not (tray[1] or tray[2] or tray[3]) then
- tray = { newPiece(), newPiece(), newPiece() }
- end
- drawScore()
- drawBoard()
- drawTray()
- if not anyMove(grid, tray) then
- local pick = ui.dialog("Game Over",
- { "Score: " .. score, "Best: " .. math.max(best, score) },
- { { "Again", colors.blue }, { "Home", colors.gray } })
- return pick == 1 and "again" or "home", score
- end
- else
- ghost = nil
- drawBoard()
- end
- end
- end
- end
- function M.run(ui, ctx)
- local best = ctx.load("blocks_best", 0)
- while true do
- local action, score = game(ui, best)
- if score and score > best then
- best = score
- ctx.save("blocks_best", best)
- end
- if action ~= "again" then return end
- end
- end
- return M
- ]==],
- ["pocketos/apps/games/match.lua"] = [==[
- -- PocketOS app: Match - a Royal Match style match-3.
- -- Tap a gem, then an adjacent one (or drag) to swap. Line up 3+ of a
- -- kind to clear them; cascades multiply the score. 25 moves per game.
- local M = {}
- local N = 8
- local MOVES = 25
- local KINDS = {
- { c = colors.red, g = "\3" },
- { c = colors.yellow, g = "\15" },
- { c = colors.blue, g = "\4" },
- { c = colors.green, g = "\5" },
- { c = colors.purple, g = "\6" },
- }
- local function game(ui, ctx, best)
- local x0, y0 = 6, 4
- local grid = {}
- local score = 0
- local moves = MOVES
- local sel = nil
- local function randKind(x, y)
- while true do
- local k = math.random(#KINDS)
- local a = grid[y] and grid[y][x - 1]
- local b = grid[y] and grid[y][x - 2]
- local c = grid[y - 1] and grid[y - 1][x]
- local d = grid[y - 2] and grid[y - 2][x]
- if not ((a == k and b == k) or (c == k and d == k)) then
- return k
- end
- end
- end
- for y = 1, N do
- grid[y] = {}
- for x = 1, N do
- grid[y][x] = randKind(x, y)
- end
- end
- local function drawCell(x, y, hl)
- local k = grid[y][x]
- term.setCursorPos(x0 + (x - 1) * 2, y0 + y - 1)
- if not k then
- term.setBackgroundColor(colors.white)
- term.write(" ")
- else
- local kind = KINDS[k]
- if hl then
- term.setBackgroundColor(colors.white)
- term.setTextColor(kind.c)
- else
- term.setBackgroundColor(kind.c)
- term.setTextColor(colors.white)
- end
- term.write(kind.g .. " ")
- end
- end
- local function drawBoard()
- for y = 1, N do
- for x = 1, N do
- drawCell(x, y, sel and sel.x == x and sel.y == y)
- end
- end
- end
- local function drawHud()
- ui.fill(1, 2, ui.w, 1, colors.white)
- ui.text(2, 2, "Score " .. score, colors.black, colors.white)
- local b = "Best " .. math.max(best, score)
- ui.text(ui.w - #b, 2, b, colors.gray, colors.white)
- ui.fill(1, 14, ui.w, 1, colors.white)
- ui.center(14, "Moves left: " .. moves,
- moves <= 5 and colors.red or colors.gray, colors.white)
- end
- local function findMatches()
- local mark, count = {}, 0
- for y = 1, N do
- local x = 1
- while x <= N do
- local k = grid[y][x]
- local len = 1
- while k and x + len <= N and grid[y][x + len] == k do
- len = len + 1
- end
- if k and len >= 3 then
- for i = 0, len - 1 do
- local key = y * 16 + x + i
- if not mark[key] then
- mark[key] = true
- count = count + 1
- end
- end
- end
- x = x + len
- end
- end
- for x = 1, N do
- local y = 1
- while y <= N do
- local k = grid[y][x]
- local len = 1
- while k and y + len <= N and grid[y + len][x] == k do
- len = len + 1
- end
- if k and len >= 3 then
- for i = 0, len - 1 do
- local key = (y + i) * 16 + x
- if not mark[key] then
- mark[key] = true
- count = count + 1
- end
- end
- end
- y = y + len
- end
- end
- return mark, count
- end
- local function resolve()
- local chain = 0
- while true do
- local mark, count = findMatches()
- if count == 0 then break end
- chain = chain + 1
- score = score + count * 10 * chain
- for key in pairs(mark) do
- local x = key % 16
- local y = math.floor(key / 16)
- ui.fill(x0 + (x - 1) * 2, y0 + y - 1, 2, 1, colors.white)
- end
- sleep(0.15)
- for key in pairs(mark) do
- local x = key % 16
- local y = math.floor(key / 16)
- grid[y][x] = nil
- end
- for x = 1, N do
- local write = N
- for y = N, 1, -1 do
- if grid[y][x] then
- local k = grid[y][x]
- if write ~= y then grid[y][x] = nil end
- grid[write][x] = k
- write = write - 1
- end
- end
- for y = write, 1, -1 do
- grid[y][x] = math.random(#KINDS)
- end
- end
- drawBoard()
- drawHud()
- sleep(0.1)
- end
- end
- local function hasMove()
- local function try(x1, y1, x2, y2)
- grid[y1][x1], grid[y2][x2] = grid[y2][x2], grid[y1][x1]
- local _, n = findMatches()
- grid[y1][x1], grid[y2][x2] = grid[y2][x2], grid[y1][x1]
- return n > 0
- end
- for y = 1, N do
- for x = 1, N do
- if x < N and try(x, y, x + 1, y) then return true end
- if y < N and try(x, y, x, y + 1) then return true end
- end
- end
- return false
- end
- local function ensureMoves()
- while not hasMove() do
- for y = 1, N do
- for x = 1, N do grid[y][x] = nil end
- end
- for y = 1, N do
- for x = 1, N do grid[y][x] = randKind(x, y) end
- end
- drawBoard()
- ui.center(14, "Shuffled!", colors.blue, colors.white)
- sleep(0.6)
- drawHud()
- end
- end
- local function trySwap(a, b)
- if math.abs(a.x - b.x) + math.abs(a.y - b.y) ~= 1 then return end
- grid[a.y][a.x], grid[b.y][b.x] = grid[b.y][b.x], grid[a.y][a.x]
- local _, n = findMatches()
- if n == 0 then
- -- not a match: show it, then swap back (no move used)
- drawCell(a.x, a.y)
- drawCell(b.x, b.y)
- sleep(0.15)
- grid[a.y][a.x], grid[b.y][b.x] = grid[b.y][b.x], grid[a.y][a.x]
- drawCell(a.x, a.y)
- drawCell(b.x, b.y)
- return
- end
- moves = moves - 1
- drawBoard()
- drawHud()
- resolve()
- drawBoard()
- drawHud()
- ensureMoves()
- end
- local function cellAt(mx, my)
- if my < y0 or my > y0 + N - 1 then return nil end
- local x = math.floor((mx - x0) / 2) + 1
- if x < 1 or x > N then return nil end
- return { x = x, y = my - y0 + 1 }
- end
- ui.clear(colors.white)
- ui.topBar("Match")
- drawHud()
- drawBoard()
- ui.center(16, "Tap two gems to swap", colors.lightGray, colors.white)
- ensureMoves()
- while true do
- local e, b, mx, my = os.pullEvent()
- if e == "mouse_click" then
- if ui.isBack(mx, my) then return "home", score end
- local c = cellAt(mx, my)
- if c then
- if sel and sel.x == c.x and sel.y == c.y then
- sel = nil
- drawCell(c.x, c.y)
- elseif sel and math.abs(sel.x - c.x) + math.abs(sel.y - c.y) == 1 then
- local old = sel
- sel = nil
- drawCell(old.x, old.y)
- trySwap(old, c)
- else
- local old = sel
- sel = c
- if old then drawCell(old.x, old.y) end
- drawCell(c.x, c.y, true)
- end
- end
- elseif e == "mouse_drag" and sel then
- local c = cellAt(mx, my)
- if c and math.abs(sel.x - c.x) + math.abs(sel.y - c.y) == 1 then
- local old = sel
- sel = nil
- drawCell(old.x, old.y)
- trySwap(old, c)
- end
- end
- if moves <= 0 then
- local pick = ui.dialog("Out of Moves!",
- { "Score: " .. score, "Best: " .. math.max(best, score) },
- { { "Again", colors.blue }, { "Home", colors.gray } })
- return (pick == 1 and "again" or "home"), score
- end
- end
- end
- function M.run(ui, ctx)
- local best = ctx.load("match_best", 0)
- while true do
- local action, score = game(ui, ctx, best)
- if score and score > best then
- best = score
- ctx.save("match_best", best)
- end
- if action ~= "again" then return end
- end
- end
- return M
- ]==],
- ["pocketos/apps/games/mines.lua"] = [==[
- -- PocketOS app: Mines - classic minesweeper.
- -- Tap to dig. Toggle Flag mode (top right) or right-click to flag.
- -- First tap is always safe.
- local M = {}
- local W, H, MINES = 12, 14, 24
- local NUMCOL = {
- colors.blue, colors.green, colors.red, colors.purple,
- colors.orange, colors.cyan, colors.black, colors.gray,
- }
- local function game(ui, ctx, best)
- local x0, y0 = 2, 4
- local mine, rev, flag = {}, {}, {}
- for y = 1, H do
- mine[y], rev[y], flag[y] = {}, {}, {}
- end
- local placed = false
- local flags = 0
- local opened = 0
- local flagMode = false
- local secs = 0
- local clock = os.startTimer(1)
- local function neighbors(x, y, fn)
- for dy = -1, 1 do
- for dx = -1, 1 do
- local nx, ny = x + dx, y + dy
- if (dx ~= 0 or dy ~= 0) and nx >= 1 and nx <= W and ny >= 1 and ny <= H then
- fn(nx, ny)
- end
- end
- end
- end
- local function countAround(x, y)
- local n = 0
- neighbors(x, y, function(nx, ny)
- if mine[ny][nx] then n = n + 1 end
- end)
- return n
- end
- local function drawCell(x, y)
- term.setCursorPos(x0 + (x - 1) * 2, y0 + y - 1)
- if rev[y][x] then
- if mine[y][x] then
- term.setBackgroundColor(colors.red)
- term.setTextColor(colors.black)
- term.write("* ")
- else
- local n = countAround(x, y)
- term.setBackgroundColor(colors.white)
- if n == 0 then
- term.write(" ")
- else
- term.setTextColor(NUMCOL[n] or colors.black)
- term.write(n .. " ")
- end
- end
- else
- term.setBackgroundColor((x + y) % 2 == 0 and colors.lightGray or colors.gray)
- if flag[y][x] then
- term.setTextColor(colors.red)
- term.write("F ")
- else
- term.write(" ")
- end
- end
- end
- local function drawTop()
- ui.fill(1, 2, ui.w, 1, colors.white)
- ui.text(2, 2, "\7 " .. (MINES - flags), colors.black, colors.white)
- local t = string.format("%d:%02d", math.floor(secs / 60), secs % 60)
- ui.center(2, t, colors.gray, colors.white)
- local lbl = flagMode and "Flag:ON " or "Flag:OFF"
- ui.text(ui.w - #lbl - 1, 2, " " .. lbl,
- flagMode and colors.white or colors.gray,
- flagMode and colors.red or colors.lightGray)
- end
- local function drawAll()
- ui.clear(colors.white)
- ui.topBar("Mines")
- drawTop()
- for y = 1, H do
- for x = 1, W do drawCell(x, y) end
- end
- ui.center(ui.h - 1, "Right-click also flags", colors.lightGray, colors.white)
- end
- local function placeMines(sx, sy)
- local safe = { [sy * 100 + sx] = true }
- neighbors(sx, sy, function(nx, ny) safe[ny * 100 + nx] = true end)
- local left = MINES
- while left > 0 do
- local x, y = math.random(W), math.random(H)
- if not mine[y][x] and not safe[y * 100 + x] then
- mine[y][x] = true
- left = left - 1
- end
- end
- placed = true
- end
- local function reveal(x, y)
- local stack = { { x, y } }
- while #stack > 0 do
- local c = table.remove(stack)
- local cx, cy = c[1], c[2]
- if not rev[cy][cx] and not flag[cy][cx] then
- rev[cy][cx] = true
- opened = opened + 1
- drawCell(cx, cy)
- if not mine[cy][cx] and countAround(cx, cy) == 0 then
- neighbors(cx, cy, function(nx, ny)
- if not rev[ny][nx] then stack[#stack + 1] = { nx, ny } end
- end)
- end
- end
- end
- end
- local function boardAt(mx, my)
- if my < y0 or my > y0 + H - 1 then return nil end
- local x = math.floor((mx - x0) / 2) + 1
- if x < 1 or x > W then return nil end
- return x, my - y0 + 1
- end
- drawAll()
- while true do
- local e, b, mx, my = os.pullEvent()
- if e == "timer" and b == clock then
- if placed then
- secs = math.min(secs + 1, 5999)
- drawTop()
- end
- clock = os.startTimer(1)
- elseif e == "mouse_click" then
- if ui.isBack(mx, my) then return "home" end
- if my == 2 and mx >= ui.w - 9 then
- flagMode = not flagMode
- drawTop()
- else
- local x, y = boardAt(mx, my)
- if x then
- if flagMode or b == 2 then
- if not rev[y][x] then
- flag[y][x] = not flag[y][x]
- flags = flags + (flag[y][x] and 1 or -1)
- drawCell(x, y)
- drawTop()
- end
- elseif not flag[y][x] and not rev[y][x] then
- if not placed then placeMines(x, y) end
- if mine[y][x] then
- for yy = 1, H do
- for xx = 1, W do
- if mine[yy][xx] then
- rev[yy][xx] = true
- drawCell(xx, yy)
- end
- end
- end
- local pick = ui.dialog("Boom!",
- { "You hit a mine." },
- { { "Again", colors.blue }, { "Home", colors.gray } })
- return pick == 1 and "again" or "home"
- end
- reveal(x, y)
- if opened >= W * H - MINES then
- local lines = { "Cleared in " .. secs .. "s!" }
- if best and best > 0 then
- lines[2] = "Best: " .. best .. "s"
- end
- local pick = ui.dialog("You Win!", lines,
- { { "Again", colors.blue }, { "Home", colors.gray } })
- return (pick == 1 and "again" or "home"), secs
- end
- end
- end
- end
- end
- end
- end
- function M.run(ui, ctx)
- local best = ctx.load("mines_best", 0)
- while true do
- local action, secs = game(ui, ctx, best)
- if secs and (best == 0 or secs < best) then
- best = secs
- ctx.save("mines_best", best)
- end
- if action ~= "again" then return end
- end
- end
- return M
- ]==],
- ["pocketos/apps/games/tetris.lua"] = [==[
- -- PocketOS app: Tetris.
- -- Keys: arrows/WASD move + rotate, Space hard drops.
- -- Touch: tap left/right third of the board to move, middle to rotate,
- -- bottom rows to hard drop.
- local M = {}
- local BW, BH = 10, 18
- local PIECES = {
- { c = colors.cyan, m = { { 0, 0, 0, 0 }, { 1, 1, 1, 1 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } } },
- { c = colors.yellow, m = { { 1, 1 }, { 1, 1 } } },
- { c = colors.purple, m = { { 0, 1, 0 }, { 1, 1, 1 }, { 0, 0, 0 } } },
- { c = colors.green, m = { { 0, 1, 1 }, { 1, 1, 0 }, { 0, 0, 0 } } },
- { c = colors.red, m = { { 1, 1, 0 }, { 0, 1, 1 }, { 0, 0, 0 } } },
- { c = colors.blue, m = { { 1, 0, 0 }, { 1, 1, 1 }, { 0, 0, 0 } } },
- { c = colors.orange, m = { { 0, 0, 1 }, { 1, 1, 1 }, { 0, 0, 0 } } },
- }
- local toBlit = colors.toBlit or function(c)
- local i, v = 0, c
- while v > 1 do
- v = math.floor(v / 2)
- i = i + 1
- end
- return ("0123456789abcdef"):sub(i + 1, i + 1)
- end
- local function rot(m)
- local n = #m
- local r = {}
- for y = 1, n do
- r[y] = {}
- for x = 1, n do
- r[y][x] = m[n - x + 1][y]
- end
- end
- return r
- end
- local function game(ui, ctx, best)
- local x0, y0 = 2, 2
- local board = {}
- for y = 1, BH do board[y] = {} end
- local score, lines, level = 0, 0, 1
- local cur, nxt
- local function newPiece()
- local p = PIECES[math.random(#PIECES)]
- return { m = p.m, c = p.c, x = math.floor((BW - #p.m) / 2) + 1, y = 0 }
- end
- local function fits(m, px, py)
- for j = 1, #m do
- for i = 1, #m do
- if m[j][i] == 1 then
- local bx, by = px + i - 1, py + j - 1
- if bx < 1 or bx > BW or by > BH then return false end
- if by >= 1 and board[by][bx] then return false end
- end
- end
- end
- return true
- end
- local function dropY(p)
- local y = p.y
- while fits(p.m, p.x, y + 1) do y = y + 1 end
- return y
- end
- local function drawBoard()
- local gy = cur and dropY(cur)
- for y = 1, BH do
- local txt, fg, bg = {}, {}, {}
- for x = 1, BW do
- local c = board[y][x]
- if not c and cur and gy then
- local i, j = x - cur.x + 1, y - gy + 1
- if i >= 1 and i <= #cur.m and j >= 1 and j <= #cur.m
- and cur.m[j][i] == 1 then
- c = colors.lightGray -- ghost landing preview
- end
- end
- if cur then
- local i, j = x - cur.x + 1, y - cur.y + 1
- if i >= 1 and i <= #cur.m and j >= 1 and j <= #cur.m
- and cur.m[j][i] == 1 then
- c = cur.c
- end
- end
- local h = toBlit(c or colors.white)
- txt[#txt + 1] = " "
- fg[#fg + 1] = h .. h
- bg[#bg + 1] = h .. h
- end
- term.setCursorPos(x0, y0 + y - 1)
- term.blit(table.concat(txt), table.concat(fg), table.concat(bg))
- end
- end
- local function drawPanel()
- ui.fill(23, 2, ui.w - 22, BH, colors.white)
- ui.text(23, 2, "Next", colors.gray, colors.white)
- if nxt then
- for j = 1, #nxt.m do
- for i = 1, #nxt.m do
- if nxt.m[j][i] == 1 then
- ui.fill(22 + i, 3 + j, 1, 1, nxt.c)
- end
- end
- end
- end
- ui.text(23, 9, "Pts", colors.gray, colors.white)
- local st = score > 9999 and (math.floor(score / 1000) .. "k") or tostring(score)
- ui.text(23, 10, st, colors.black, colors.white)
- ui.text(23, 12, "Lns", colors.gray, colors.white)
- ui.text(23, 13, tostring(lines), colors.black, colors.white)
- ui.text(23, 15, "Lvl", colors.gray, colors.white)
- ui.text(23, 16, tostring(level), colors.black, colors.white)
- end
- local function lock()
- for j = 1, #cur.m do
- for i = 1, #cur.m do
- if cur.m[j][i] == 1 then
- local bx, by = cur.x + i - 1, cur.y + j - 1
- if by < 1 then return false end
- board[by][bx] = cur.c
- end
- end
- end
- local cleared = 0
- local y = BH
- while y >= 1 do
- local full = true
- for x = 1, BW do
- if not board[y][x] then full = false; break end
- end
- if full then
- table.remove(board, y)
- table.insert(board, 1, {})
- cleared = cleared + 1
- else
- y = y - 1
- end
- end
- if cleared > 0 then
- lines = lines + cleared
- score = score + ({ 100, 300, 500, 800 })[cleared] * level
- level = math.floor(lines / 10) + 1
- end
- return true
- end
- local interval = 0.5
- local function settle()
- if not lock() then return false end
- cur = nxt
- nxt = newPiece()
- interval = math.max(0.1, 0.5 - (level - 1) * 0.05)
- drawPanel()
- return fits(cur.m, cur.x, cur.y)
- end
- local function gameOver()
- local pick = ui.dialog("Game Over",
- { "Score: " .. score, "Best: " .. math.max(best, score) },
- { { "Again", colors.blue }, { "Home", colors.gray } })
- return pick == 1 and "again" or "home"
- end
- local function tryRotate()
- local m2 = rot(cur.m)
- for _, k in ipairs({ 0, -1, 1, -2, 2 }) do
- if fits(m2, cur.x + k, cur.y) then
- cur.m = m2
- cur.x = cur.x + k
- drawBoard()
- return
- end
- end
- end
- ui.clear(colors.white)
- ui.topBar("Tetris")
- ui.fill(1, 2, 1, BH, colors.lightGray)
- ui.fill(22, 2, 1, BH, colors.lightGray)
- ui.center(ui.h, "\27\26 move \24 spin \25 drop", colors.gray, colors.white)
- cur = newPiece()
- nxt = newPiece()
- drawBoard()
- drawPanel()
- local grav = os.startTimer(interval)
- local function hardDrop()
- local y = dropY(cur)
- score = score + (y - cur.y) * 2
- cur.y = y
- if not settle() then return false end
- drawBoard()
- grav = os.startTimer(interval)
- return true
- end
- while true do
- local e, a, b, c = os.pullEvent()
- if e == "timer" and a == grav then
- if fits(cur.m, cur.x, cur.y + 1) then
- cur.y = cur.y + 1
- elseif not settle() then
- return gameOver(), score
- end
- drawBoard()
- grav = os.startTimer(interval)
- elseif e == "key" then
- if a == keys.left or a == keys.a then
- if fits(cur.m, cur.x - 1, cur.y) then
- cur.x = cur.x - 1
- drawBoard()
- end
- elseif a == keys.right or a == keys.d then
- if fits(cur.m, cur.x + 1, cur.y) then
- cur.x = cur.x + 1
- drawBoard()
- end
- elseif a == keys.up or a == keys.w then
- tryRotate()
- elseif a == keys.down or a == keys.s then
- if fits(cur.m, cur.x, cur.y + 1) then
- cur.y = cur.y + 1
- score = score + 1
- drawBoard()
- end
- elseif a == keys.space then
- if not hardDrop() then return gameOver(), score end
- end
- elseif e == "mouse_click" then
- if ui.isBack(b, c) then return "home", score end
- if b >= x0 and b <= x0 + BW * 2 - 1 and c >= 2 and c <= BH + 1 then
- local bx = math.floor((b - x0) / 2) + 1
- if c >= 16 then
- if not hardDrop() then return gameOver(), score end
- elseif bx <= 3 then
- if fits(cur.m, cur.x - 1, cur.y) then
- cur.x = cur.x - 1
- drawBoard()
- end
- elseif bx >= 8 then
- if fits(cur.m, cur.x + 1, cur.y) then
- cur.x = cur.x + 1
- drawBoard()
- end
- else
- tryRotate()
- end
- end
- end
- end
- end
- function M.run(ui, ctx)
- local best = ctx.load("tetris_best", 0)
- while true do
- local action, score = game(ui, ctx, best)
- if score and score > best then
- best = score
- ctx.save("tetris_best", best)
- end
- if action ~= "again" then return end
- end
- end
- return M
- ]==],
- ["pocketos/apps/games/solitaire.lua"] = [==[
- -- PocketOS game: Cards - Klondike solitaire.
- -- Tap the deck to draw a card. Tap a face-up card to pick it up (cards
- -- lower in a pile grab the whole stack), then tap the destination pile
- -- or a foundation. Tapping a selected top card again auto-sends it to
- -- a foundation when it fits. Face-down cards show as a grey counter.
- local M = {}
- local SUITC = { "\3", "\4", "\5", "\6" } -- hearts diamonds clubs spades
- local RANKC = { "A", "2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K" }
- local function isRed(s) return s <= 2 end
- local function game(ui)
- local deck = {}
- for s = 1, 4 do
- for r = 1, 13 do
- deck[#deck + 1] = { r = r, s = s, up = false }
- end
- end
- for i = #deck, 2, -1 do
- local j = math.random(i)
- deck[i], deck[j] = deck[j], deck[i]
- end
- local tab = {}
- for i = 1, 7 do
- tab[i] = {}
- for k = 1, i do
- local card = table.remove(deck)
- card.up = (k == i)
- tab[i][k] = card
- end
- end
- local stock, waste = deck, {}
- local found = { {}, {}, {}, {} }
- local sel = nil -- { t = "waste" } or { t = "tab", p = pile, i = index }
- local function pileX(i) return 3 * i end
- local function downCount(pile)
- local n = 0
- for _, c in ipairs(pile) do
- if c.up then break end
- n = n + 1
- end
- return n
- end
- local function drawCard(x, y, c, hl)
- term.setCursorPos(x, y)
- if not c then
- term.setBackgroundColor(colors.white)
- term.setTextColor(colors.lightGray)
- term.write("--")
- elseif not c.up then
- term.setBackgroundColor(colors.lightGray)
- term.write(" ")
- else
- term.setBackgroundColor(hl and colors.blue or colors.white)
- term.setTextColor(hl and colors.white
- or (isRed(c.s) and colors.red or colors.black))
- term.write(RANKC[c.r] .. SUITC[c.s])
- end
- end
- local function drawAll()
- ui.clear(colors.white)
- ui.topBar("Cards", "+", colors.blue)
- term.setCursorPos(3, 3)
- if #stock > 0 then
- term.setBackgroundColor(colors.green)
- term.write(" ")
- else
- term.setBackgroundColor(colors.white)
- term.setTextColor(colors.gray)
- term.write("O ")
- end
- drawCard(6, 3, waste[#waste], sel and sel.t == "waste")
- for f = 1, 4 do
- drawCard(9 + 3 * f, 3, found[f][#found[f]])
- end
- for i = 1, 7 do
- local pile = tab[i]
- local downs = downCount(pile)
- local y = 5
- if #pile == 0 then
- drawCard(pileX(i), y, nil)
- else
- if downs > 0 then
- term.setCursorPos(pileX(i), y)
- term.setBackgroundColor(colors.lightGray)
- term.setTextColor(colors.gray)
- term.write(downs .. " ")
- y = y + 1
- end
- for k = downs + 1, #pile do
- drawCard(pileX(i), y, pile[k],
- sel and sel.t == "tab" and sel.p == i and k >= sel.i)
- y = y + 1
- end
- end
- end
- ui.center(ui.h, "Drag cards or tap-tap", colors.lightGray, colors.white)
- end
- local function canTab(card, pile)
- if #pile == 0 then return card.r == 13 end
- local top = pile[#pile]
- return top.up and top.r == card.r + 1 and isRed(top.s) ~= isRed(card.s)
- end
- local function canFound(card, f)
- local pile = found[f]
- if #pile == 0 then return card.r == 1 end
- local top = pile[#pile]
- return top.s == card.s and card.r == top.r + 1
- end
- local function selSource()
- if not sel then return nil end
- if sel.t == "waste" then return waste, #waste end
- return tab[sel.p], sel.i
- end
- local function afterMove()
- if sel and sel.t == "tab" then
- local p = tab[sel.p]
- if #p > 0 and not p[#p].up then p[#p].up = true end
- end
- sel = nil
- end
- local function toFoundation(f)
- local src, si = selSource()
- if not src or si ~= #src then return false end
- local card = src[si]
- if not card or not canFound(card, f) then return false end
- found[f][#found[f] + 1] = table.remove(src)
- afterMove()
- return true
- end
- local function autoFoundation()
- for f = 1, 4 do
- if toFoundation(f) then return true end
- end
- return false
- end
- local function toTableau(i)
- local src, si = selSource()
- local card = src and src[si]
- if not card then return false end
- if sel.t == "tab" and sel.p == i then return false end
- if not canTab(card, tab[i]) then return false end
- for k = si, #src do
- tab[i][#tab[i] + 1] = src[k]
- end
- for _ = si, #src do
- table.remove(src)
- end
- afterMove()
- return true
- end
- local function won()
- return #found[1] + #found[2] + #found[3] + #found[4] == 52
- end
- -- what's under a screen position (generous hit bands)
- local function zoneAt(mx, my)
- if my >= 2 and my <= 4 then
- if mx >= 3 and mx <= 5 then return "stock" end
- if mx >= 6 and mx <= 8 then return "waste" end
- if mx >= 12 and mx <= 23 then
- local rel = mx - 12
- if rel % 3 <= 1 then return "found", math.floor(rel / 3) + 1 end
- end
- elseif my >= 5 then
- local i = math.floor(mx / 3)
- if i >= 1 and i <= 7 then return "tab", i end
- end
- end
- local function selectAt(i, my)
- local pile = tab[i]
- local downs = downCount(pile)
- if #pile <= downs then
- sel = nil
- return
- end
- local firstUp = 5 + (downs > 0 and 1 or 0)
- local idx = downs + (my - firstUp + 1)
- if idx < downs + 1 then idx = downs + 1 end
- if idx > #pile then idx = #pile end
- sel = { t = "tab", p = i, i = idx }
- end
- drawAll()
- while true do
- local e, _, mx, my = os.pullEvent()
- local changed = false
- if e == "mouse_click" then
- if ui.isBack(mx, my) then return "home" end
- if my == 1 and mx >= ui.w - 2 then return "again" end -- new deal
- local zone, zi = zoneAt(mx, my)
- if zone == "stock" then
- sel = nil
- if #stock > 0 then
- local c = table.remove(stock)
- c.up = true
- waste[#waste + 1] = c
- else
- for _ = 1, #waste do
- local c = table.remove(waste)
- c.up = false
- stock[#stock + 1] = c
- end
- end
- changed = true
- elseif zone == "waste" then
- if sel and sel.t == "waste" then
- if not autoFoundation() then sel = nil end
- elseif #waste > 0 then
- sel = { t = "waste" }
- end
- changed = true
- elseif zone == "found" then
- if sel then toFoundation(zi) end
- changed = true
- elseif zone == "tab" then
- if sel then
- if sel.t == "tab" and sel.p == zi then
- local downs = downCount(tab[zi])
- local firstUp = 5 + (downs > 0 and 1 or 0)
- local idx = downs + (my - firstUp + 1)
- if idx == sel.i and sel.i == #tab[zi] then
- autoFoundation()
- else
- selectAt(zi, my)
- end
- elseif not toTableau(zi) then
- -- couldn't move there: pick that card up instead
- selectAt(zi, my)
- end
- else
- selectAt(zi, my)
- end
- changed = true
- end
- elseif e == "mouse_up" and sel then
- -- drag-and-drop: release over a different pile moves there
- local zone, zi = zoneAt(mx, my)
- if zone == "tab" and not (sel.t == "tab" and sel.p == zi) then
- if toTableau(zi) then changed = true end
- elseif zone == "found" then
- if toFoundation(zi) then changed = true end
- end
- end
- if changed then
- drawAll()
- if won() then
- local pick = ui.dialog("You Win!", { "All 52 cards home!" },
- { { "Again", colors.blue }, { "Home", colors.gray } })
- return pick == 1 and "again" or "home"
- end
- end
- end
- end
- function M.run(ui, ctx)
- while true do
- if game(ui) ~= "again" then return end
- end
- end
- return M
- ]==],
- ["pocketos/apps/games/flappy.lua"] = [==[
- -- PocketOS game: Flappy - tap or press Space to flap through the pipes.
- local M = {}
- local toBlit = colors.toBlit or function(c)
- local i, v = 0, c
- while v > 1 do
- v = math.floor(v / 2)
- i = i + 1
- end
- return ("0123456789abcdef"):sub(i + 1, i + 1)
- end
- local function game(ui, best)
- local top, bottom = 2, ui.h - 2
- local gapH = 6
- local bx = 7
- local by, vy = 9, 0
- local pipes = {}
- local score = 0
- local dist = 0
- local dead = false
- ui.clear(colors.white)
- ui.topBar("Flappy", string.format("%4d", 0))
- ui.fill(1, ui.h - 1, ui.w, 1, colors.gray)
- ui.center(ui.h, "Tap or Space to flap", colors.gray, colors.white)
- local WB = toBlit(colors.white)
- local GB = toBlit(colors.green)
- local YB = toBlit(colors.yellow)
- local KB = toBlit(colors.black)
- local RB = toBlit(colors.red)
- local function drawFrame()
- local birdRow = math.max(top, math.min(bottom, math.floor(by + 0.5)))
- for y = top, bottom do
- local txt, fg, bg = {}, {}, {}
- for x = 1, ui.w do
- local ch, f, b = " ", WB, WB
- for _, p in ipairs(pipes) do
- if (x == p.x or x == p.x + 1)
- and (y < p.gap or y >= p.gap + gapH) then
- f, b = GB, GB
- end
- end
- if x == bx and y == birdRow then
- ch = ">"
- f = KB
- b = dead and RB or YB
- end
- txt[#txt + 1], fg[#fg + 1], bg[#bg + 1] = ch, f, b
- end
- term.setCursorPos(1, y)
- term.blit(table.concat(txt), table.concat(fg), table.concat(bg))
- end
- end
- drawFrame()
- -- wait for the first flap
- while true do
- local e, a, b, c = os.pullEvent()
- if e == "key" and (a == keys.space or a == keys.up or a == keys.w) then
- vy = -1.15
- break
- elseif e == "mouse_click" then
- if ui.isBack(b, c) then return "home", score end
- vy = -1.15
- break
- end
- end
- local timer = os.startTimer(0.12)
- while true do
- local e, a, b, c = os.pullEvent()
- if e == "timer" and a == timer then
- dist = dist + 1
- if dist % 11 == 0 then
- pipes[#pipes + 1] = {
- x = ui.w,
- gap = math.random(top + 1, bottom - gapH),
- }
- end
- for i = #pipes, 1, -1 do
- local p = pipes[i]
- p.x = p.x - 1
- if not p.passed and p.x + 1 < bx then
- p.passed = true
- score = score + 1
- ui.text(ui.w - 4, 1, string.format("%4d", score),
- colors.gray, colors.white)
- end
- if p.x + 1 < 1 then table.remove(pipes, i) end
- end
- vy = vy + 0.35
- by = by + vy
- if by < top then
- by = top
- vy = 0
- end
- local birdRow = math.floor(by + 0.5)
- local hit = birdRow > bottom
- if not hit then
- for _, p in ipairs(pipes) do
- if (bx == p.x or bx == p.x + 1)
- and (birdRow < p.gap or birdRow >= p.gap + gapH) then
- hit = true
- break
- end
- end
- end
- if hit then
- dead = true
- by = math.min(by, bottom)
- drawFrame()
- sleep(0.4)
- local pick = ui.dialog("Game Over",
- { "Score: " .. score, "Best: " .. math.max(best, score) },
- { { "Again", colors.blue }, { "Home", colors.gray } })
- return (pick == 1 and "again" or "home"), score
- end
- drawFrame()
- timer = os.startTimer(0.12)
- elseif e == "key" and (a == keys.space or a == keys.up or a == keys.w) then
- vy = -1.15
- elseif e == "mouse_click" then
- if ui.isBack(b, c) then return "home", score end
- vy = -1.15
- end
- end
- end
- function M.run(ui, ctx)
- local best = ctx.load("flappy_best", 0)
- while true do
- local action, score = game(ui, best)
- if score and score > best then
- best = score
- ctx.save("flappy_best", best)
- end
- if action ~= "again" then return end
- end
- end
- return M
- ]==],
- ["pocketos/apps/games/sudoku.lua"] = [==[
- -- PocketOS game: Sudoku. A fresh puzzle every game.
- -- Tap a cell (or move with arrows), then tap the number pad or type
- -- 1-9. X, 0 or Backspace erases. Wrong numbers show up red.
- local M = {}
- local function newPuzzle()
- local g = {}
- for y = 1, 9 do g[y] = {} end
- local function ok(y, x, v)
- for i = 1, 9 do
- if g[y][i] == v or g[i][x] == v then return false end
- end
- local by = math.floor((y - 1) / 3) * 3
- local bx = math.floor((x - 1) / 3) * 3
- for yy = by + 1, by + 3 do
- for xx = bx + 1, bx + 3 do
- if g[yy][xx] == v then return false end
- end
- end
- return true
- end
- local function fill(pos)
- if pos > 81 then return true end
- local y = math.floor((pos - 1) / 9) + 1
- local x = (pos - 1) % 9 + 1
- local vals = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }
- for i = 9, 2, -1 do
- local j = math.random(i)
- vals[i], vals[j] = vals[j], vals[i]
- end
- for _, v in ipairs(vals) do
- if ok(y, x, v) then
- g[y][x] = v
- if fill(pos + 1) then return true end
- g[y][x] = nil
- end
- end
- return false
- end
- fill(1)
- local given = {}
- for y = 1, 9 do
- given[y] = {}
- for x = 1, 9 do given[y][x] = true end
- end
- local removed = 0
- while removed < 45 do
- local y, x = math.random(9), math.random(9)
- if given[y][x] then
- given[y][x] = false
- removed = removed + 1
- end
- end
- local val = {}
- for y = 1, 9 do
- val[y] = {}
- for x = 1, 9 do
- if given[y][x] then val[y][x] = g[y][x] end
- end
- end
- return val, given
- end
- local function game(ui, ctx, best)
- local val, given = newPuzzle()
- local x0, y0 = 5, 4
- local sx, sy = 5, 5
- local secs = 0
- local clock = os.startTimer(1)
- local function conflict(y, x)
- local v = val[y][x]
- if not v then return false end
- for i = 1, 9 do
- if i ~= x and val[y][i] == v then return true end
- if i ~= y and val[i][x] == v then return true end
- end
- local by = math.floor((y - 1) / 3) * 3
- local bx = math.floor((x - 1) / 3) * 3
- for yy = by + 1, by + 3 do
- for xx = bx + 1, bx + 3 do
- if (yy ~= y or xx ~= x) and val[yy][xx] == v then return true end
- end
- end
- return false
- end
- local function drawCell(x, y)
- local box = (math.floor((x - 1) / 3) + math.floor((y - 1) / 3)) % 2
- local bg = (x == sx and y == sy) and colors.yellow
- or (box == 0 and colors.white or colors.lightGray)
- local v = val[y][x]
- local fg
- if v and conflict(y, x) then
- fg = colors.red
- elseif given[y][x] then
- fg = colors.black
- else
- fg = colors.blue
- end
- ui.text(x0 + (x - 1) * 2, y0 + y - 1, (v and tostring(v) or " ") .. " ", fg, bg)
- end
- local function drawBoard()
- for y = 1, 9 do
- for x = 1, 9 do drawCell(x, y) end
- end
- end
- local function blanks()
- local n = 0
- for y = 1, 9 do
- for x = 1, 9 do
- if not val[y][x] then n = n + 1 end
- end
- end
- return n
- end
- local function drawHud()
- ui.fill(1, 2, ui.w, 1, colors.white)
- ui.text(2, 2, "Left: " .. blanks(), colors.black, colors.white)
- local t = string.format("%d:%02d", math.floor(secs / 60), secs % 60)
- ui.text(ui.w - #t - 1, 2, t, colors.gray, colors.white)
- end
- local function drawPad()
- for i = 1, 10 do
- local label = i <= 9 and tostring(i) or "X"
- ui.text(4 + (i - 1) * 2, 15, label .. " ",
- i <= 9 and colors.black or colors.red, colors.lightGray)
- end
- end
- local function checkWin()
- for y = 1, 9 do
- for x = 1, 9 do
- if not val[y][x] or conflict(y, x) then return false end
- end
- end
- return true
- end
- local function select(nx, ny)
- local ox, oy = sx, sy
- sx, sy = nx, ny
- drawCell(ox, oy)
- drawCell(sx, sy)
- end
- ui.clear(colors.white)
- ui.topBar("Sudoku")
- drawHud()
- drawBoard()
- drawPad()
- ui.center(17, "Tap cell, then number", colors.lightGray, colors.white)
- while true do
- local e, a, b, c = os.pullEvent()
- local placed = nil
- if e == "timer" and a == clock then
- secs = math.min(secs + 1, 5999)
- drawHud()
- clock = os.startTimer(1)
- elseif e == "mouse_click" then
- if ui.isBack(b, c) then return "home" end
- if c >= y0 and c <= y0 + 8 then
- local x = math.floor((b - x0) / 2) + 1
- if x >= 1 and x <= 9 then select(x, c - y0 + 1) end
- elseif c == 15 then
- local i = math.floor((b - 4) / 2) + 1
- if i >= 1 and i <= 9 then
- placed = i
- elseif i == 10 then
- placed = 0
- end
- end
- elseif e == "char" then
- if a:match("^[1-9]$") then
- placed = tonumber(a)
- elseif a == "0" or a:lower() == "x" then
- placed = 0
- end
- elseif e == "key" then
- if a == keys.backspace or a == keys.delete then
- placed = 0
- elseif a == keys.up then
- select(sx, math.max(1, sy - 1))
- elseif a == keys.down then
- select(sx, math.min(9, sy + 1))
- elseif a == keys.left then
- select(math.max(1, sx - 1), sy)
- elseif a == keys.right then
- select(math.min(9, sx + 1), sy)
- end
- end
- if placed and not given[sy][sx] then
- val[sy][sx] = placed ~= 0 and placed or nil
- drawBoard() -- conflicts elsewhere may change too
- drawHud()
- if placed ~= 0 and checkWin() then
- local lines = { "Solved in " .. secs .. "s" }
- if best and best > 0 then lines[2] = "Best: " .. best .. "s" end
- local pick = ui.dialog("You Win!", lines,
- { { "Again", colors.blue }, { "Home", colors.gray } })
- return (pick == 1 and "again" or "home"), secs
- end
- end
- end
- end
- function M.run(ui, ctx)
- local best = ctx.load("sudoku_best", 0)
- while true do
- local action, secs = game(ui, ctx, best)
- if secs and (best == 0 or secs < best) then
- best = secs
- ctx.save("sudoku_best", best)
- end
- if action ~= "again" then return end
- end
- end
- return M
- ]==],
- }
- local CONFIG = "pocketos/config.lua"
- if not (term.isColor and term.isColor()) then
- print("Warning: PocketOS needs an *advanced*")
- print("(gold) pocket computer to run.")
- print("Installing anyway...")
- end
- -- serialize a config value back to Lua source
- local function ser(v)
- local t = type(v)
- if t == "string" then return string.format("%q", v) end
- if t == "number" or t == "boolean" then return tostring(v) end
- if t == "table" then
- local parts = {}
- for _, x in ipairs(v) do parts[#parts + 1] = ser(x) end
- return "{ " .. table.concat(parts, ", ") .. " }"
- end
- return "nil"
- end
- -- The default config shipped with this build (with the maintainer's baked
- -- values). Used to ADD keys a device's config.lua is missing, so new
- -- options appear on update without clobbering the user's existing values.
- local function shippedConfig()
- local fn = load(files[CONFIG], "config", "t", {})
- if not fn then return nil end
- local ok, t = pcall(fn)
- if ok and type(t) == "table" then return t end
- return nil
- end
- local function mergeConfig()
- local rf = fs.open(CONFIG, "r")
- local text = rf.readAll() or ""
- rf.close()
- local shipped = shippedConfig()
- if not shipped then print(" kept " .. CONFIG); return end
- local additions = ""
- for k, v in pairs(shipped) do
- if type(k) == "string" and not text:find("\n%s*" .. k .. "%s*=") then
- additions = additions .. "\n " .. k .. " = " .. ser(v) .. ",\n"
- end
- end
- if additions == "" then print(" kept " .. CONFIG); return end
- local last
- for p in text:gmatch("()}") do last = p end
- if not last then print(" kept " .. CONFIG .. " (no merge point)"); return end
- text = text:sub(1, last - 1) .. additions .. text:sub(last)
- local wf = fs.open(CONFIG, "w")
- wf.write(text)
- wf.close()
- print(" merged new keys into " .. CONFIG)
- end
- print("Installing PocketOS...")
- for path, content in pairs(files) do
- if path == CONFIG and fs.exists(CONFIG) then
- mergeConfig() -- keep values, add new keys
- else
- local dir = fs.getDir(path)
- if dir ~= "" and not fs.exists(dir) then
- fs.makeDir(dir)
- end
- local f = fs.open(path, "w")
- f.write(content)
- f.close()
- print(" " .. path)
- end
- end
- -- clean up files from older layouts and removed apps
- for _, old in ipairs({
- "/pocketos/apps/dash.lua",
- "/pocketos/apps/snake.lua",
- "/pocketos/apps/blocks.lua",
- "/pocketos/apps/match.lua",
- "/pocketos/apps/mines.lua",
- "/pocketos/apps/tetris.lua",
- }) do
- if fs.exists(old) then fs.delete(old) end
- end
- print("Done! Rebooting into PocketOS...")
- sleep(1.5)
- os.reboot()
Advertisement
Add Comment
Please, Sign In to add comment