Create: Computing AgesBrass Docs
Built-in programs

devices

map of the connected devices

Runs on Mainframe+ Transistor Mainframe and newer · 937 lines
Type this at the prompt of a computer to copy it onto the medium:
Terminal
> examples devices
> edit devices

The program

devices
-- Everything this computer can drive, drawn as a map: the block on each of
-- its six faces, the devices on its data cable, its radios, the Wi-Fi
-- networks and the computers in reach. Click a device to see its readings
-- and the functions a program can call. Tab or the arrows change page,
-- Q quits.
local C = term.colors
local size = term.get_size()
local W = size.w
local H = size.h
local COLOR = os.tier() >= 4  -- older screens have a single phosphor colour
local FACES = 1
local CABLE = 2
local RADIO = 3
local DETAIL = 4

-- 8 x 8 icons: one hex colour per pixel (the colours of term.colors), "." empty
local I_PC = {"77777777", "7ffffff7", "7f5ffff7", "7ff5fff7", "7f5f55f7", "7ffffff7", "77777777", "..7777.."}
local I_CHEST = {"cccccccc", "c111111c", "c111111c", "ccc88ccc", "c118811c", "c111111c", "c111111c", "cccccccc"}
local I_COG = {"...11...", ".1.11.1.", "..1cc1..", "11c77c11", "11c77c11", "..1cc1..", ".1.11.1.", "...11..."}
local I_TANK = {"88888888", "8......8", "8......8", "8bbbbbb8", "8b3bbbb8", "8bbbb3b8", "8bbbbbb8", "88888888"}
local I_SCREEN = {"77777777", "7bbbbbb7", "7b33bbb7", "7b3bbbb7", "7bbbbbb7", "77777777", "...77...", ".777777."}
local I_ROUTER = {".a....a.", ".a....a.", ".a....a.", "aaaaaaaa", "a5a5a5aa", "aaaaaaaa", ".7....7.", "........"}
local I_RADIO = {"a..7..a.", ".a.7.a..", "...7....", "..777...", "...7....", "..7.7...", ".7...7..", "7777777."}
local I_WIFI = {"..2222..", ".2....2.", "2..22..2", "..2..2..", "........", "...22...", "...22...", "........"}
local I_LIGHT = {"..7777..", "..7ee7..", "..7777..", "..7117..", "..7777..", "..7557..", "..7777..", "...77..."}
local I_LOOP = {"77777777", "7eeeeee7", "7e7777e7", "7e7777e7", "7e7777e7", "7e7777e7", "7eeeeee7", "77777777"}
local I_LINK = {"...ee...", "..e66e..", "...ee...", "...cc...", "...cc...", ".777777.", "7e7777e7", "77777777"}
local I_SENSOR = {"..8888..", ".8....8.", "8.....58", "8....5.8", "8..55..8", "8..55..8", ".8....8.", "..8888.."}
local I_DUST = {"...ee...", "...ee...", "...ee...", "eeeeeeee", "eeeeeeee", "...ee...", "...ee...", "...ee..."}
local I_CABLE = {"...777..", "...797..", "...797..", "7777977.", "9999997.", "7777777.", "........", "........"}

-- Colour and icon of each type of device (peripheral.type)
local KIND = {
  traffic_light = {color = C.green, icon = I_LIGHT},
  inductive_loop = {color = C.pink, icon = I_LOOP},
  monitor = {color = C.cyan, icon = I_SCREEN},
  wifi_router = {color = C.magenta, icon = I_ROUTER},
  radio_modem = {color = C.purple, icon = I_RADIO},
  redstone_link = {color = C.red, icon = I_LINK},
  kinetic = {color = C.orange, icon = I_COG},
  inventory = {color = C.yellow, icon = I_CHEST},
  tank = {color = C.light_blue, icon = I_TANK},
}
local SENSOR = {color = C.lime, icon = I_SENSOR}  -- the sensors of Create Aeronautics
-- Names short enough for a card (13 letters)
local NAMES = {
  traffic_light = "Traffic light", inductive_loop = "Loop detector", monitor = "Monitor",
  wifi_router = "Wi-Fi router", radio_modem = "Radio modem", redstone_link = "Redstone Link",
  kinetic = "Kinetic block", inventory = "Inventory", tank = "Tank", altitude_sensor = "Altimeter",
  gimbal_sensor = "Gyroscope", velocity_sensor = "Speed sensor", optical_sensor = "Optic sensor",
  navigation_table = "Nav table", swivel_bearing = "Bearing", torsion_spring = "Spring",
  hot_air_burner = "Burner",
}
local SIDE_NAME = {top = "Top", bottom = "Bottom", left = "Left", right = "Right", front = "Front", back = "Back"}
local ASPECTS = {off = "off", red = "red", red_amber = "red + amber", amber = "amber", green = "green", flashing = "flashing"}
local TABS = {"Faces", "Cable", "Wireless"}
local T = {
  title = "Devices", back = "< Back", this = "Computer #", cable = "Cable", radio = "Wireless",
  empty = "empty", noLabel = "no label", more = "details >", wifi = "Wi-Fi: ", blocks = " blocks",
  device = "device", devices = "devices", computer = "computer", computers = "computers",
  antenna = "antenna", antennas = "antennas", item = "item", items = "items", slot = "slot", slots = "slots",
  router = "router", routers = "routers", rpm = " RPM", overstressed = "OVERSTRESSED", range = "range ",
  noSsid = "no SSID", receiver = "receiver", transmitter = "transmitter", onIt = " on it", nothing = "nothing on it",
  yours = "yours", alsoInv = "+ inventory", alsoTank = "+ tank", alsoKinetic = "+ Create", tank = "tank ",
  name = "Name", readings = "Readings", contents = "Contents", functions = "Functions", noReading = "No readings.",
  needMini = {"Available from", "the Mini-", "computer"}, needPc = {"Available from", "the Personal", "Computer"},
  noAntenna = {"No antenna:", "place a modem", "or a router"},
  cableDevice = "device on the cable", cableDevices = "devices on the cable",
  cableComputer = "computer on the cable", cableComputers = "computers on the cable",
  myAntenna = "antenna on this computer", myAntennas = "antennas on this computer",
  network = "Wi-Fi network in reach", networks = "Wi-Fi networks in reach",
  inReach = "computer in reach", inReachs = "computers in reach",
  noCable = "Data cables need a Minicomputer or newer.",
  noRadio = "Radio and Wi-Fi need a Personal Computer or newer.",
  noneCable = "None yet: connect blocks to the computer with data cable.", none = "None.",
  noneAntenna = "None: place a radio modem or a Wi-Fi router against the computer or on its cable.",
  noneNetwork = "None: a Wi-Fi router gets a network name with set_ssid; a modem in range of a router joins its network.",
  gone = "Disconnected: the device is not there anymore.",
  hintFaces = "Click: details  Tab: next page  Q: quit",
  hintList = "Click: details  Up/Down: scroll  Q: quit",
  hintDetail = "Click or Backspace: back  Up/Down: scroll",
}

-- The faces page is a 3 x 3 grid of cards, the computer in the middle. Depth goes
-- on the diagonal, like a cube seen from the front: back up right, front down left.
local GX = 1  -- columns between two cards
if W >= 60 then
  GX = 2
end
local CW = (W - 2 * GX) // 3  -- size of a card in characters
local CH = math.min(5, (H - 2) // 3)
local GY = math.min(2, (H - 2 - 3 * CH) // 2)
local OX = (W - 3 * CW - 2 * GX) // 2 + 1
local OY = 2 + (H - 2 - 3 * CH - 2 * GY) // 2
local GRID = {
  {col = 2, row = 1, side = "top"},
  {col = 3, row = 1, side = "back"},
  {col = 1, row = 2, side = "left"},
  {col = 3, row = 2, side = "right"},
  {col = 1, row = 3, side = "front"},
  {col = 2, row = 3, side = "bottom"},
}
local A = (W - 5) * 2 // 5  -- columns of the lists: name, reading, place
local B = (W - 5) // 4
local KEY = math.min(18, W // 3)
-- Functions that only read, called on the detail page; those needing arguments fail and are skipped
local READERS = {"get", "count", "detected", "size", "speed", "stress", "capacity", "overstressed",
  "range", "get_ssid", "is_receiver", "frequency", "height", "pressure", "angle_x", "angle_z",
  "velocity", "hit", "distance", "block", "angle", "target_angle", "limit", "running"}

local page = FACES
local back = FACES  -- the page a detail was opened from
local target = nil  -- name of the device shown in detail
local scroll = 0
local faces = {}
local cable = {}
local antennas = {}
local wired = {}
local remote = {}
local networks = {}
local found = 0
local rows = {}
local hits = {}  -- clickable areas of the screen
local tabs = {}
local shown = {}  -- what each part of the screen shows, to redraw only changes
local counts = {}
local running = true
local scanned = -1

-- Text cut or padded with spaces to n characters
local function fit(s, n)
  if #s > n then
    return string.sub(s, 1, n)
  end
  return s .. string.rep(" ", n - #s)
end

local function right(s, n)
  if #s > n then
    return string.sub(s, 1, n)
  end
  return string.rep(" ", n - #s) .. s
end

local function put(x, y, s, fg)
  term.set_cursor(x, y)
  term.set_fg(fg)
  term.write(s)
end

local function num(v)
  if v == math.floor(v) then
    return tostring(v)
  end
  return string.format("%.1f", v)
end

local function plural(n, one, many)
  if n == 1 then
    return num(n) .. " " .. one
  end
  return num(n) .. " " .. many
end

-- "minecraft:water" -> "water"
local function short(id)
  local at = string.find(id, ":", 1, true)
  if at then
    return string.sub(id, at + 1)
  end
  return id
end

-- "inventory@12,64,-3" -> "12,64,-3"
local function coords(name)
  local at = string.find(name, "@", 1, true)
  if at then
    return string.sub(name, at + 1)
  end
  return name
end

-- Calls a function of a device; nil when it fails (it needs arguments, the block is gone...)
local function try(p, method)
  local f = p[method]
  if f == nil then
    return nil
  end
  local r = pcall(f)
  if r.ok then
    return r.value
  end
  return nil
end

local function show(v)
  if type(v) == "number" then
    return num(v)
  elseif type(v) == "table" then
    if v.w then
      return num(v.w) .. " x " .. num(v.h)
    elseif v.a then
      return short(v.a) .. " + " .. short(v.b)
    end
    return "{...}"
  end
  return tostring(v)
end

-- Cuts a text into lines of at most n characters, between words
local function wrap(text, n)
  local lines = {}
  local line = ""
  for _, word in ipairs(string.split(text, " ")) do
    if line == "" then
      line = word
    elseif #line + 1 + #word <= n then
      line = line .. " " .. word
    else
      table.insert(lines, line)
      line = word
    end
  end
  if line ~= "" then
    table.insert(lines, line)
  end
  return lines
end

-- Redstone power received and emitted on a face, in a few words
local function redstone(power, output)
  if power > 0 and output > 0 then return "in " .. power .. ", out " .. output end
  if power > 0 then return "receives " .. power end
  if output > 0 then return "emits " .. output end
  return ""
end

local function who(c)
  local s = "#" .. num(c.id)
  if c.label then
    s = s .. " " .. c.label
  end
  return s
end

-- One device, ready to draw: its name, colour, icon and two short readings
local function describe(name, p)
  local t = p.type()
  local k = KIND[t] or SENSOR
  local d = {name = name, type = t, label = NAMES[t] or t, color = k.color, icon = k.icon, s1 = "", s2 = ""}
  if t == "traffic_light" then
    d.s1 = ASPECTS[try(p, "get") or "off"] or ""
  elseif t == "inductive_loop" then
    local n = try(p, "count") or 0
    if n > 0 then
      d.s1 = num(n) .. T.onIt
    else
      d.s1 = T.nothing
    end
  elseif t == "monitor" then
    local s = try(p, "size")
    if s then
      d.s1 = num(s.w) .. " x " .. num(s.h)
    end
  elseif t == "wifi_router" then
    local ssid = try(p, "get_ssid") or ""
    if ssid == "" then
      d.s1 = T.noSsid
    else
      d.s1 = "SSID " .. ssid
    end
    d.s2 = T.range .. num(try(p, "range") or 0)
  elseif t == "radio_modem" then
    d.s1 = T.range .. num(try(p, "range") or 0)
  elseif t == "redstone_link" then
    if try(p, "is_receiver") then
      d.s1 = T.receiver
    else
      d.s1 = T.transmitter
    end
    local f = try(p, "frequency")
    if f then
      d.s2 = short(f.a) .. "+" .. short(f.b)
    end
  elseif t == "kinetic" then
    d.s1 = num(math.abs(try(p, "speed") or 0)) .. T.rpm
    if try(p, "overstressed") then
      d.s2 = T.overstressed
    else
      d.s2 = num(try(p, "stress") or 0) .. " / " .. num(try(p, "capacity") or 0) .. " su"
    end
  elseif t == "inventory" then
    d.s1 = plural(try(p, "count") or 0, T.item, T.items)
    d.s2 = plural(try(p, "size") or 0, T.slot, T.slots)
  elseif t == "tank" then
    local list = try(p, "tanks")
    if list and list[1] then
      if list[1].fluid then
        d.s1 = short(list[1].fluid)
      else
        d.s1 = T.empty
      end
      d.s2 = num(list[1].amount) .. " / " .. num(list[1].capacity) .. " mB"
    end
  else
    -- a sensor: its first two readings
    for key, value in pairs(try(p, "read") or {}) do
      if d.s1 == "" then
        d.s1 = key .. " " .. show(value)
      elseif d.s2 == "" then
        d.s2 = key .. " " .. show(value)
      end
    end
  end
  -- a block can be of several types: a millstone turns and holds items
  if t ~= "inventory" and p.push then
    d.s2 = T.alsoInv
  elseif t ~= "tank" and p.tanks then
    d.s2 = T.alsoTank
  elseif t ~= "kinetic" and p.overstressed then
    d.s2 = T.alsoKinetic
  end
  return d
end

local function isAntenna(d)
  return d.type == "radio_modem" or d.type == "wifi_router"
end

-- Looks at everything in reach. Faces are named as seen facing the screen.
local function scan()
  faces = {}
  antennas = {}
  found = 0
  for _, side in ipairs(rs.sides()) do
    local f = {side = side, power = rs.get(side), output = rs.get_output(side)}
    local p = peripheral.wrap(side)
    if p then
      f.device = describe(side, p)
      f.device.where = SIDE_NAME[side]
      found = found + 1
      if isAntenna(f.device) then
        table.insert(antennas, f.device)
      end
    end
    faces[side] = f
  end
  -- devices on the cable are named type@x,y,z
  cable = {}
  for _, name in ipairs(peripheral.list()) do
    if string.find(name, "@", 1, true) then
      local p = peripheral.wrap(name)
      if p then
        local d = describe(name, p)
        d.where = coords(name)
        table.insert(cable, d)
        if isAntenna(d) then
          table.insert(antennas, d)
        end
      end
    end
  end
  found = found + #cable
  wired = {}
  remote = {}
  networks = {}
  if net then  -- nil before the minicomputer
    for _, c in ipairs(net.computers()) do
      if c.via == "cable" then
        table.insert(wired, c)
      else
        table.insert(remote, c)
      end
    end
    networks = net.networks()
  end
end

local function addHead(text, color)
  table.insert(rows, {kind = "head", text = text, color = color})
end

local function addItem(icon, color, a, b, c, link)
  table.insert(rows, {kind = "item", icon = icon, color = color, a = a, b = b, c = c, link = link})
end

local function addPair(key, value)
  table.insert(rows, {kind = "pair", a = key, b = value})
end

local function addText(text, color)
  for _, line in ipairs(wrap(text, W - 4)) do
    table.insert(rows, {kind = "text", text = line, color = color})
  end
end

local function addBlank()
  table.insert(rows, {kind = "text", text = "", color = C.white})
end

local function buildCable()
  rows = {}
  if net == nil then
    addText(T.noCable, C.light_gray)
    return
  end
  addHead(plural(#cable, T.cableDevice, T.cableDevices), C.light_gray)
  for _, d in ipairs(cable) do
    addItem(d.icon, d.color, d.label, d.s1, d.where, d.name)
  end
  if #cable == 0 then
    addText(T.noneCable, C.light_gray)
  end
  addBlank()
  addHead(plural(#wired, T.cableComputer, T.cableComputers), C.white)
  for _, c in ipairs(wired) do
    addItem(I_PC, C.white, who(c), num(c.distance or 0) .. T.blocks, "", nil)
  end
  if #wired == 0 then
    addText(T.none, C.light_gray)
  end
end

local function buildRadio()
  rows = {}
  if net == nil or os.tier() < 4 then
    addText(T.noRadio, C.light_gray)
    return
  end
  addHead(plural(#antennas, T.myAntenna, T.myAntennas), C.purple)
  for _, d in ipairs(antennas) do
    addItem(d.icon, d.color, d.label, d.s1, d.where, d.name)
  end
  if #antennas == 0 then
    addText(T.noneAntenna, C.light_gray)
  end
  addBlank()
  addHead(plural(#networks, T.network, T.networks), C.magenta)
  for _, n in ipairs(networks) do
    local place = num(n.distance) .. T.blocks
    if n.own then
      place = T.yours
    end
    addItem(I_WIFI, C.magenta, n.ssid, plural(n.routers, T.router, T.routers), place, nil)
  end
  if #networks == 0 then
    addText(T.noneNetwork, C.light_gray)
  end
  addBlank()
  addHead(plural(#remote, T.inReach, T.inReachs), C.white)
  for _, c in ipairs(remote) do
    local via = c.via
    if via == "wifi" then
      via = "Wi-Fi"
    end
    addItem(I_PC, C.white, who(c), via, num(c.distance or 0) .. T.blocks, nil)
  end
  if #remote == 0 then
    addText(T.none, C.light_gray)
  end
end

local function byCount(a, b)
  return counts[a] > counts[b]
end

-- The detail page: every reading of the device, what it holds, its functions
local function buildDetail()
  rows = {}
  local p = peripheral.wrap(target)
  if p == nil then
    addHead(target, C.gray)
    addText(T.gone, C.light_gray)
    return
  end
  local d = describe(target, p)
  addHead(d.label, d.color)
  addPair(T.name, target)
  if SIDE_NAME[target] then
    local text = redstone(rs.get(target), rs.get_output(target))
    if text == "" then
      text = "0"
    end
    addPair("Redstone", text)
  end
  addText('p = peripheral.wrap("' .. target .. '")', C.lime)
  addBlank()
  addHead(T.readings, C.gray)
  local before = #rows
  for _, m in ipairs(READERS) do
    if p[m] then
      local r = pcall(p[m])
      if r.ok and r.value ~= nil then
        addPair(m .. "()", show(r.value))
      end
    end
  end
  if p.tanks then
    for i, tank in ipairs(try(p, "tanks") or {}) do
      local fluid = T.empty
      if tank.fluid then
        fluid = short(tank.fluid)
      end
      addPair(T.tank .. i, fluid .. "  " .. num(tank.amount) .. " / " .. num(tank.capacity) .. " mB")
    end
  end
  if #rows == before then
    addText(T.noReading, C.light_gray)
  end
  if p.push then
    -- the same items of several slots are counted together, the most first
    counts = {}
    local order = {}
    for _, stack in pairs(try(p, "list") or {}) do
      if counts[stack.display] == nil then
        counts[stack.display] = 0
        table.insert(order, stack.display)
      end
      counts[stack.display] = counts[stack.display] + stack.count
    end
    table.sort(order, byCount)
    addBlank()
    addHead(T.contents, C.gray)
    for _, item in ipairs(order) do
      addPair(item, num(counts[item]))
    end
    if #order == 0 then
      addText(T.empty, C.light_gray)
    end
  end
  local names = {}
  for key, value in pairs(p) do
    table.insert(names, key .. "()")
  end
  table.sort(names)
  addBlank()
  addHead(T.functions, C.gray)
  addText(table.concat(names, "  "), C.white)
end

-- Pixel of a character cell: a cell is 6 x 9 pixels
local function px(x)
  return (x - 1) * 6 + 1
end

local function py(y)
  return (y - 1) * 9 + 1
end

local function cellX(col)
  return OX + (col - 1) * (CW + GX)
end

local function cellY(row)
  return OY + (row - 1) * (CH + GY)
end

-- True when that part of the screen must change; drawing only changes avoids flicker
local function changed(key, sig)
  if shown[key] == sig then
    return false
  end
  shown[key] = sig
  return true
end

-- A card: in colour, a coloured title band and border. One-colour screens light
-- every pixel drawn with the same phosphor: a filled band would hide its text.
local function card(c)
  local x = cellX(c.col)
  local y = cellY(c.row)
  table.insert(hits, {x1 = x, y1 = y, x2 = x + CW - 1, y2 = y + CH - 1, link = c.link, page = c.page})
  if not changed("card" .. c.col .. c.row, tostring(c.color) .. c.head .. table.concat(c.lines, "|")) then
    return
  end
  local left = px(x)
  local top = py(y)
  gfx.rect(left, top, CW * 6, CH * 9, "none", true)
  if COLOR then
    gfx.rect(left, top, CW * 6, 9, c.color, true)
    gfx.rect(left, top, CW * 6, CH * 9, c.color)
  else
    gfx.rect(left, top, CW * 6, CH * 9, "white")
    gfx.line(left, top + 9, left + CW * 6 - 1, top + 9, "white")
  end
  put(x + 1, y, fit(c.head, CW - 2), c.ink or C.black)
  for i = 1, CH - 1 do
    put(x + 1, y + i, fit(c.lines[i] or "", CW - 2), c.colors[i] or C.white)
  end
  if c.icon then
    gfx.image(left + 2, top + 9, c.icon)  -- under the first column of the first line
  end
end

-- The line from the computer to a face, in the colour of what is there
local function connect(g)
  local f = faces[g.side]
  local color = "none"
  if f.device then
    color = f.device.color
  elseif f.power > 0 or f.output > 0 then
    color = C.red
  elseif COLOR then
    color = C.gray
  end
  if not changed("line" .. g.side, tostring(color)) then
    return
  end
  local pl = px(cellX(2))
  local pt = py(cellY(2))
  local pr = pl + CW * 6 - 1
  local pb = pt + CH * 9 - 1
  local fl = px(cellX(g.col))
  local ft = py(cellY(g.row))
  local fr = fl + CW * 6 - 1
  local fb = ft + CH * 9 - 1
  local mx = (pl + pr) // 2
  local my = (pt + pb) // 2
  if g.side == "left" then
    gfx.line(fr + 1, my, pl - 2, my, color, 2)
  elseif g.side == "right" then
    gfx.line(pr + 1, my, fl - 2, my, color, 2)
  elseif GY == 0 then
    return  -- no room between the rows
  elseif g.side == "top" then
    gfx.line(mx, fb + 1, mx, pt - 2, color, 2)
  elseif g.side == "bottom" then
    gfx.line(mx, pb + 1, mx, ft - 2, color, 2)
  elseif g.side == "back" then
    gfx.line(pr + 1, pt - 2, fl - 2, fb + 1, color, 2)
  else
    gfx.line(pl - 2, pb + 1, fr + 1, ft - 2, color, 2)
  end
end

local function faceCard(g)
  local f = faces[g.side]
  local d = f.device
  local signal = redstone(f.power, f.output)
  local head = SIDE_NAME[g.side]
  if string.lower(head) ~= g.side and #head + 1 + #g.side <= CW - 2 then
    head = fit(head, CW - 3 - #g.side) .. " " .. g.side  -- the name for programs
  end
  local c = {col = g.col, row = g.row, head = head, color = C.gray, ink = C.light_gray}
  local a = T.empty
  local b = ""
  local e = ""
  local first = C.gray
  if d then
    c.color = d.color
    c.ink = C.black
    c.icon = d.icon
    c.link = g.side
    a = " " .. d.label
    b = d.s1
    e = d.s2
    first = d.color
  elseif signal ~= "" then
    c.color = C.red
    c.ink = C.black
    c.icon = I_DUST
    a = " Redstone"
    b = signal
    signal = ""
    first = C.red
  end
  c.colors = {first, C.white, C.white, C.red}
  if CH >= 5 then
    c.lines = {a, b, e, signal}
  elseif signal == "" then
    c.lines = {a, b, e}
  elseif CH == 4 then
    c.lines = {a, b, signal}
    c.colors[3] = C.red
  else
    c.lines = {a, signal}
    c.colors[2] = C.red
  end
  card(c)
end

local function computerCard()
  local lines = {" " .. (os.label() or T.noLabel), plural(found, T.device, T.devices), ""}
  if net then
    lines[3] = plural(#wired + #remote, T.computer, T.computers)
  end
  card({col = 2, row = 2, head = T.this .. num(os.id()), color = C.white, icon = I_PC, lines = lines,
    colors = {C.white, C.white, C.white}})
end

local function cableCard()
  local c = {col = 1, row = 1, head = T.cable, color = C.light_gray, icon = I_CABLE, page = CABLE,
    colors = {C.white, C.white, C.white, C.light_gray}}
  if net == nil then
    c.lines = T.needMini
    c.icon = nil
    c.colors = {C.light_gray, C.light_gray, C.light_gray}
  else
    c.lines = {" " .. plural(#cable, T.device, T.devices), plural(#wired, T.computer, T.computers), "", T.more}
  end
  card(c)
end

local function radioCard()
  local c = {col = 3, row = 3, head = T.radio, color = C.purple, page = RADIO,
    colors = {C.light_gray, C.light_gray, C.light_gray}}
  if net == nil or os.tier() < 4 then
    c.lines = T.needPc
  elseif #antennas == 0 then
    c.lines = T.noAntenna
  else
    c.icon = I_WIFI
    c.lines = {" " .. plural(#antennas, T.antenna, T.antennas), T.wifi .. #networks,
      plural(#remote, T.computer, T.computers), T.more}
    c.colors = {C.white, C.white, C.white, C.light_gray}
  end
  card(c)
end

local function drawFaces()
  hits = {}
  for _, g in ipairs(GRID) do
    connect(g)
  end
  computerCard()
  for _, g in ipairs(GRID) do
    faceCard(g)
  end
  cableCard()
  radioCard()
end

-- One line of a list; icons are drawn under the two spaces left for them
local function drawRow(y, r)
  local sig = ""
  if r then
    sig = r.kind .. "|" .. (r.text or "") .. "|" .. (r.a or "") .. "|" .. (r.b or "") .. "|" .. (r.c or "") .. "|" .. tostring(r.color)
  end
  if not changed("row" .. y, sig) then
    return
  end
  gfx.rect(1, py(y), W * 6, 9, "none", true)
  if r == nil then
    put(1, y, fit("", W), C.white)
  elseif r.kind == "head" and COLOR then
    gfx.rect(1, py(y), W * 6, 9, r.color, true)
    put(1, y, fit(" " .. r.text, W), C.black)
  elseif r.kind == "head" then
    put(1, y, fit("-- " .. r.text .. " --", W), C.white)
  elseif r.kind == "item" then
    put(1, y, "    " .. fit(r.a, A) .. " ", r.color)
    put(6 + A, y, fit(r.b, B) .. " ", C.white)
    put(7 + A + B, y, right(r.c, W - 6 - A - B), C.light_gray)
    gfx.image(px(2) + 2, py(y), r.icon)
  elseif r.kind == "pair" then
    put(1, y, fit("  " .. r.a, KEY + 2), C.light_gray)
    put(KEY + 3, y, fit(r.b, W - KEY - 2), C.white)
  else
    put(1, y, fit("  " .. r.text, W), r.color)
  end
end

local function drawRows()
  local view = H - 2
  scroll = math.max(0, math.min(scroll, #rows - view))
  hits = {}
  for i = 1, view do
    local r = rows[scroll + i]
    drawRow(i + 1, r)
    if r and r.link then
      table.insert(hits, {x1 = 1, y1 = i + 1, x2 = W, y2 = i + 1, link = r.link})
    end
  end
end

local function drawTabs()
  local active = page
  if page == DETAIL then
    active = back
  end
  if not changed("tabs", page .. "/" .. active) then
    return
  end
  if COLOR then
    gfx.rect(1, 1, W * 6, 9, "blue", true)
  end
  local x = W + 1
  for i = 1, 3 do
    x = x - #TABS[i] - 2
  end
  local title = " " .. T.title
  if page == DETAIL then
    title = " " .. T.back
  end
  put(1, 1, fit(title, x - 1), C.white)
  for i = 1, 3 do
    local label = " " .. TABS[i] .. " "
    local ink = C.white
    if i == active then
      ink = C.black
      if COLOR then
        gfx.rect(px(x), 1, #label * 6, 9, "white", true)
      else
        label = "[" .. TABS[i] .. "]"
      end
    end
    put(x, 1, label, ink)
    tabs[i] = {x1 = x, x2 = x + #label - 1}
    x = x + #label
  end
end

local function draw()
  drawTabs()
  if page == FACES then
    drawFaces()
  else
    drawRows()
  end
  local hint = T.hintFaces
  if page == DETAIL then
    hint = T.hintDetail
  elseif page ~= FACES then
    hint = T.hintList
  end
  if changed("footer", hint) then
    put(1, H, fit(" " .. hint, W), C.light_gray)
  end
end

local function refresh()
  if page == DETAIL then
    buildDetail()
  else
    scan()
    if page == CABLE then
      buildCable()
    elseif page == RADIO then
      buildRadio()
    end
  end
  scanned = os.clock()
  draw()
end

local function setPage(p)
  page = p
  scroll = 0
  shown = {}
  term.set_bg(C.black)
  term.clear()
  gfx.clear()
  refresh()
end

local function open(name)
  if page ~= DETAIL then
    back = page
  end
  target = name
  setPage(DETAIL)
end

-- e.x, e.y: the character clicked, in the terminal or on a monitor
local function onClick(e)
  if e.y == 1 then
    for i = 1, 3 do
      if e.x >= tabs[i].x1 and e.x <= tabs[i].x2 then
        setPage(i)
        return
      end
    end
    if page == DETAIL then
      setPage(back)
    end
    return
  end
  for _, h in ipairs(hits) do
    if e.x >= h.x1 and e.x <= h.x2 and e.y >= h.y1 and e.y <= h.y2 then
      if h.link then
        open(h.link)
        return
      elseif h.page then
        setPage(h.page)
        return
      end
    end
  end
  if page == DETAIL then
    setPage(back)
  end
end

local function onKey(k)
  if page == DETAIL and (k == "backspace" or k == "left" or k == "right" or k == "tab") then
    setPage(back)
  elseif k == "tab" or k == "right" then
    setPage(page % 3 + 1)
  elseif k == "left" then
    setPage((page + 1) % 3 + 1)
  elseif page ~= FACES and k == "up" then
    scroll = scroll - 1
    drawRows()
  elseif page ~= FACES and k == "down" then
    scroll = scroll + 1
    drawRows()
  end
end

setPage(FACES)
local timer = os.start_timer(2)
while running do
  local e = os.pull_event()
  if e.name == "timer" and e.id == timer then
    refresh()  -- readings change: every 2 s, every second in detail
    if page == DETAIL then
      timer = os.start_timer(1)
    else
      timer = os.start_timer(2)
    end
  elseif e.name == "redstone" and os.clock() - scanned >= 0.5 then
    refresh()
  elseif e.name == "click" then
    onClick(e)
  elseif e.name == "key" then
    onKey(e.key)
  elseif e.name == "char" and string.lower(e.char) == "q" then
    running = false
  elseif e.name == "char" and tonumber(e.char) and tonumber(e.char) >= 1 and tonumber(e.char) <= 3 then
    setPage(tonumber(e.char))
  end
end
term.set_bg(C.black)
term.set_fg(C.white)
term.clear()
gfx.clear()
term.set_cursor(1, 1)