Create: Computing AgesDoc Brass
Programmes intégrés

devices

carte des appareils reliés

Tourne sur Transistors+ Ordinateur à transistors et suivants · 938 lignes
Tapez ceci à l'invite d'un ordinateur pour le copier sur le support :
Terminal
> examples devices
> edit devices

Le programme

devices
-- Tout ce que cet ordinateur peut piloter, dessiné comme une carte : le bloc
-- de chacune de ses six faces, les appareils de son câble de données, ses
-- radios, les réseaux Wi-Fi et les ordinateurs joignables. Cliquez un
-- appareil pour voir ses mesures et les fonctions qu'un programme peut
-- appeler. Tab ou les flèches changent de page, Q quitte.
local C = term.colors
local size = term.get_size()
local W = size.w
local H = size.h
local COLOR = os.tier() >= 4  -- les écrans plus anciens n'ont qu'une couleur
local FACES = 1
local CABLE = 2
local RADIO = 3
local DETAIL = 4

-- Icônes de 8 x 8 : une couleur hexa par pixel (celles de term.colors), "." vide
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.", "........", "........"}

-- Couleur et icône de chaque type d'appareil (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}  -- les capteurs de Create Aeronautics
-- Des noms assez courts pour une carte (13 lettres)
local NAMES = {
  traffic_light = "Feu tricolore", inductive_loop = "Boucle détec.", monitor = "Moniteur",
  wifi_router = "Routeur Wi-Fi", radio_modem = "Modem radio", redstone_link = "Redstone Link",
  kinetic = "Bloc Create", inventory = "Inventaire", tank = "Réservoir", altitude_sensor = "Altimètre",
  gimbal_sensor = "Gyroscope", velocity_sensor = "Vélocimètre", optical_sensor = "Capteur opt.",
  navigation_table = "Table de nav.", swivel_bearing = "Roulement", torsion_spring = "Ressort",
  hot_air_burner = "Brûleur",
}
local SIDE_NAME = {top = "Dessus", bottom = "Dessous", left = "Gauche", right = "Droite", front = "Devant", back = "Derrière"}
local ASPECTS = {off = "éteint", red = "rouge", red_amber = "rouge + orange", amber = "orange", green = "vert", flashing = "clignotant"}
local TABS = {"Faces", "Câble", "Sans fil"}
local T = {
  title = "Périphériques", back = "< Retour", this = "Ordinateur #", cable = "Câble", radio = "Sans fil",
  empty = "vide", noLabel = "sans nom", more = "détails >", wifi = "Wi-Fi : ", blocks = " blocs",
  device = "appareil", devices = "appareils", computer = "ordinateur", computers = "ordinateurs",
  antenna = "antenne", antennas = "antennes", item = "objet", items = "objets", slot = "case", slots = "cases",
  router = "routeur", routers = "routeurs", rpm = " tr/min", overstressed = "SURCHARGÉ", range = "portée ",
  noSsid = "sans SSID", receiver = "récepteur", transmitter = "émetteur", onIt = " dessus", nothing = "rien dessus",
  yours = "le vôtre", alsoInv = "+ inventaire", alsoTank = "+ réservoir", alsoKinetic = "+ Create", tank = "cuve ",
  name = "Nom", readings = "Mesures", contents = "Contenu", functions = "Fonctions", noReading = "Aucune mesure.",
  needMini = {"Disponible dès", "le mini-", "ordinateur"}, needPc = {"Disponible dès", "le micro-", "ordinateur"},
  noAntenna = {"Aucune antenne", "posez un modem", "ou un routeur"},
  cableDevice = "appareil sur le câble", cableDevices = "appareils sur le câble",
  cableComputer = "ordinateur sur le câble", cableComputers = "ordinateurs sur le câble",
  myAntenna = "antenne sur cet ordinateur", myAntennas = "antennes sur cet ordinateur",
  network = "réseau Wi-Fi joignable", networks = "réseaux Wi-Fi joignables",
  inReach = "ordinateur joignable", inReachs = "ordinateurs joignables",
  noCable = "Les câbles de données demandent un mini-ordinateur ou mieux.",
  noRadio = "La radio et le Wi-Fi demandent un micro-ordinateur ou mieux.",
  noneCable = "Aucun pour l'instant : reliez des blocs à l'ordinateur avec du câble de données.", none = "Aucun.",
  noneAntenna = "Aucune : posez un modem radio ou un routeur Wi-Fi contre l'ordinateur ou sur son câble.",
  noneNetwork = "Aucun : un routeur Wi-Fi reçoit un nom de réseau avec set_ssid ; un modem à portée d'un routeur rejoint son réseau.",
  gone = "Déconnecté : l'appareil n'est plus là.",
  hintFaces = "Clic : détails  Tab : page  Q : quitter",
  hintList = "Clic : détails  Haut/Bas : défiler  Q : quitter",
  hintDetail = "Clic ou Retour arr. : revenir  Haut/Bas : défiler",
}

-- La page des faces est une grille de 3 x 3 cartes, l'ordinateur au milieu. La
-- profondeur est en diagonale, comme un cube vu de face : l'arrière en haut à
-- droite, l'avant en bas à gauche.
local GX = 1  -- colonnes entre deux cartes
if W >= 60 then
  GX = 2
end
local CW = (W - 2 * GX) // 3  -- taille d'une carte en caractères
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  -- colonnes des listes : nom, mesure, emplacement
local B = (W - 5) // 4
local KEY = math.min(18, W // 3)
-- Fonctions qui ne font que lire, appelées sur la page de détail ; celles qui veulent des arguments échouent et sont sautées
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  -- la page d'où vient le détail
local target = nil  -- nom de l'appareil montré en détail
local scroll = 0
local faces = {}
local cable = {}
local antennas = {}
local wired = {}
local remote = {}
local networks = {}
local found = 0
local rows = {}
local hits = {}  -- zones cliquables de l'écran
local tabs = {}
local shown = {}  -- ce que montre chaque partie de l'écran, pour ne redessiner que les changements
local counts = {}
local running = true
local scanned = -1

-- Texte coupé ou complété d'espaces jusqu'à n caractères
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

-- Appelle une fonction d'un appareil ; nil si elle échoue (elle veut des arguments, le bloc est parti...)
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

-- Découpe un texte en lignes d'au plus n caractères, entre les mots
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

-- La redstone reçue et émise sur une face, en quelques mots
local function redstone(power, output)
  if power > 0 and output > 0 then return "reçu " .. power .. " émis " .. output end
  if power > 0 then return "reçoit " .. power end
  if output > 0 then return "émet " .. 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

-- Un appareil, prêt à dessiner : son nom, sa couleur, son icône et deux mesures courtes
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
    -- un capteur : ses deux premières mesures
    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
  -- un bloc peut avoir plusieurs types : une meule tourne et contient des objets
  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

-- Regarde tout ce qui est joignable. Les faces sont vues face à l'écran.
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
  -- les appareils du câble s'appellent 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 avant le mini-ordinateur
    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

-- La page de détail : toutes les mesures de l'appareil, son contenu, ses fonctions
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
    -- les mêmes objets de plusieurs cases sont comptés ensemble, les plus nombreux d'abord
    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 d'une case de caractère : une case fait 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

-- Vrai si cette partie de l'écran doit changer ; ne dessiner que les changements évite le scintillement
local function changed(key, sig)
  if shown[key] == sig then
    return false
  end
  shown[key] = sig
  return true
end

-- Une carte : en couleur, un bandeau de titre et un bord colorés. Les écrans d'une
-- seule couleur allument tout pixel dessiné : un bandeau plein cacherait son texte.
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)  -- sous la première colonne de la première ligne
  end
end

-- Le trait de l'ordinateur vers une face, de la couleur de ce qui s'y trouve
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  -- pas de place entre les lignes
  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  -- le nom pour les programmes
  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

-- Une ligne d'une liste ; les icônes sont dessinées sous les deux espaces laissés pour elles
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 : le caractère cliqué, dans le terminal ou sur un moniteur
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()  -- les mesures changent : toutes les 2 s, chaque seconde en détail
    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)