Files
Egutierrez e4fdd0199d feat: reloj permanente, pestañas Procesos y Devices, líneas pixel-perfect
- Gráficos de línea pixel-perfect: ANTIALIAS_NONE + ancho 1px + coords
  ancladas al centro del pixel, para trazos nítidos sobre la rejilla.
- Reloj + fecha (formato europeo) en una franja superior común a todas las
  pestañas; la barra de pestañas y el contenido bajan para hacerle sitio.
- Nueva pestaña Procesos: nº de procesos (total + en ejecución), hilos,
  carga 1/5/15 min y tablas TOP CPU / RAM / I/O. Cada fila muestra el PID
  (clicable, abre htop -p) separado del nombre.
- Nueva pestaña Devices: almacenamiento (lsblk sin loops), interfaces de
  red físicas e IP, y dispositivos USB (lsusb).
- Docker con detalle: contador running/total y, por contenedor, nombre +
  imagen + estado abreviado (Up 33h / Up 2d) coloreado según salud.
- Fix: el header de Docker se dibujaba con baseline directa y solapaba la
  barra de pestañas; ahora usa el mismo offset que el resto de paneles.
- metric.sh: helpers nproc_count/running, nthreads, load_avg, top_cpu/ram/io,
  disk_list, usb_list, net_ifaces, docker_list, docker_count.
- Ventana 545 -> 575 px de alto. app.md a v0.2.0.
2026-06-06 13:20:08 +02:00

645 lines
25 KiB
Lua

--[[
conky_widget — render Cairo + pestañas clickeables.
Estructura:
- Estado: current_tab (1=Sistema, 2=Red, 3=Docker; Sistema por defecto).
- conky_draw (lua_draw_hook_post): mantiene los historicos y dibuja la barra
de pestañas y el panel activo.
- conky_mouse (lua_mouse_hook): cambia de pestaña y lanza apps al clicar botones.
Los datos del sistema se obtienen llamando a conky_parse("${...}") desde Lua,
de modo que se pueden colocar con libertad mediante Cairo.
La pestaña Sistema reproduce el panel del widget previo: graficas de area
(CPU, RAM, CPU temp, GPU, GPU temp, VRAM, disk I/O) que se vuelven rojas al
superar su umbral, mas las barras de uso de los cuatro discos. Los valores de
temperatura/GPU se obtienen de metric.sh (nvidia-smi + coretemp hwmon).
Geometria fija (la ventana mide W x H segun conky.conf).
]]
require 'cairo'
pcall(require, 'cairo_xlib') -- conky >= 1.12 separa el modulo cairo_xlib
-- Geometria compartida entre dibujo y eventos de raton -----------------------
local W = 290
local H = 575
local HDR_TOP = 4 -- franja del reloj (comun a todas las pestañas)
local HDR_H = 30
local TAB_TOP = HDR_TOP + HDR_H -- la barra de pestañas baja bajo el reloj
local TAB_H = 24
local CONTENT_TOP = TAB_TOP + TAB_H + 4 -- inicio del area de cada panel
local BTN_H = 26
local BTN_Y = CONTENT_TOP + 276 -- fila de botones de la pestaña Red (offset original del panel)
-- Interfaz de red a monitorizar (enp5s0 es la fisica activa) ------------------
local NIF = "enp5s0"
-- Helper de metricas (temps + GPU), portado del widget previo -----------------
local MET = os.getenv("HOME") .. "/.config/conky/conky_widget/metric.sh"
-- Pestañas (Sistema por defecto) ---------------------------------------------
local TABS = { "Sistema", "Red", "Docker", "Procs", "Devs" }
local NTABS = #TABS
local current_tab = 1
-- Filas clicables de la pestaña Procesos (se rellenan en cada frame de dibujo).
-- Al clicar una fila se abre el comando PROC_CLICK con el PID de esa fila.
local g_proc_rows = {}
local PROC_CLICK = "kitty -e htop -p %s" -- %s = PID
-- Botones de la pestaña Red. bin se comprueba antes de lanzar; si falta,
-- launch.sh avisa con notify-send.
local BTNS = {
{ label = "Wireshark", bin = "wireshark", pkg = "wireshark",
cmd = "f=$(ls -t /var/log/pktcap/*.pcapng 2>/dev/null | head -1); " ..
"if [ -n \"$f\" ]; then wireshark -r \"$f\"; else wireshark; fi" },
{ label = "ntopng", bin = "ntopng", pkg = "ntopng",
cmd = "xdg-open http://localhost:3000" },
{ label = "nethogs", bin = "nethogs", pkg = "nethogs",
cmd = "x-terminal-emulator -e bash -c 'sudo nethogs " .. NIF ..
"; read -p \"Enter para cerrar...\" _'" },
}
local LAUNCH = os.getenv("HOME") .. "/.config/conky/conky_widget/lua/launch.sh"
-- Historicos para los graficos en vivo ---------------------------------------
local GH = 58
local KEYS = { "cpu", "ram", "cputemp", "gputil", "gputemp", "vram", "diskio", "down", "up" }
local hist = {}
for _, k in ipairs(KEYS) do
hist[k] = {}
for i = 1, GH do hist[k][i] = 0 end
end
local function push(t, v)
table.remove(t, 1)
t[#t + 1] = v
end
-- Paleta (Nord, como el widget previo) ---------------------------------------
local function hex(s)
return {
tonumber(s:sub(1, 2), 16) / 255,
tonumber(s:sub(3, 4), 16) / 255,
tonumber(s:sub(5, 6), 16) / 255,
}
end
local COL = {
bg = hex("0e0f12"),
panel = hex("1b1d23"),
tab_active = hex("5e81ac"),
tab_inactive = hex("2e3440"),
text = hex("d8dee9"),
snow = hex("eceff4"),
white = { 1, 1, 1 },
dim = hex("7b8394"),
teal = hex("8fbcbb"),
green = hex("a3be8c"),
cyan = hex("88c0d0"),
blue = hex("81a1c1"),
frost = hex("5e81ac"),
yellow = hex("ebcb8b"),
orange = hex("d08770"),
purple = hex("b48ead"),
red = hex("bf616a"),
down = hex("88c0d0"),
up = hex("d08770"),
}
-- Helpers de lectura de datos ------------------------------------------------
local function str(expr)
local s = conky_parse(expr)
if s == nil then return "" end
return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end
local function num(expr)
return tonumber(str(expr)) or 0
end
-- Longitud visible en caracteres (code points UTF-8), no bytes. Necesario para
-- alinear texto que contiene flechas (↓ ↑) en fuente monoespaciada.
local function vlen(s)
local _, n = s:gsub("[^\128-\191]", "")
return n
end
-- Convierte un texto de tasa de conky ("1.2MiB", "300KiB", "5B") a KiB/s ------
local function rate_kib(s)
local n, unit = s:match("([%d%.]+)%s*([KMGTPi]*)B?")
n = tonumber(n) or 0
unit = unit or ""
if unit:find("M") then return n * 1024
elseif unit:find("G") then return n * 1024 * 1024
elseif unit:find("K") then return n
else return n / 1024 end
end
-- Paquetes por intervalo y protocolo: delta de los contadores de /proc/net/snmp.
-- g_proto guarda los paquetes del ultimo intervalo (≈ por segundo con
-- update_interval=1). Se actualiza una vez por frame desde conky_draw.
local g_proto = { tcp_in = 0, tcp_out = 0, udp_in = 0, udp_out = 0, icmp_in = 0, icmp_out = 0 }
local snmp_prev = nil
local function update_proto()
local f = io.open("/proc/net/snmp", "r")
if not f then return end
local hdr, cur = {}, {}
for line in f:lines() do
local proto, rest = line:match("^(%w+):%s+(.+)$")
if proto then
if hdr[proto] == nil then
local cols, i = {}, 0
for tok in rest:gmatch("%S+") do i = i + 1; cols[tok] = i end
hdr[proto] = cols
else
local vals = {}
for tok in rest:gmatch("%S+") do vals[#vals + 1] = tok end
cur[proto] = vals
end
end
end
f:close()
local function get(proto, col)
local cols = hdr[proto]; local vals = cur[proto]
if not cols or not vals then return 0 end
local idx = cols[col]; if not idx then return 0 end
return tonumber(vals[idx]) or 0
end
local c = {
tcp_in = get("Tcp", "InSegs"), tcp_out = get("Tcp", "OutSegs"),
udp_in = get("Udp", "InDatagrams"), udp_out = get("Udp", "OutDatagrams"),
icmp_in = get("Icmp", "InMsgs"), icmp_out = get("Icmp", "OutMsgs"),
}
if snmp_prev then
for k, v in pairs(c) do
local d = v - (snmp_prev[k] or v)
g_proto[k] = (d < 0) and 0 or d
end
end
snmp_prev = c
end
-- Top hosts remotos por numero de conexiones TCP/UDP establecidas (ss) --------
local function top_remotes()
local out = str("${execi 3 ss -tun state established 2>/dev/null}")
local counts = {}
for line in out:gmatch("[^\n]+") do
if not line:match("^Netid") and not line:match("^State") then
local ip = line:match("([%d%.]+):%d+%s*$") or line:match("%[([%x:]+)%]:%d+%s*$")
-- Excluir loopback: no son hosts "remotos"
if ip and not ip:match("^127%.") and ip ~= "::1" then
counts[ip] = (counts[ip] or 0) + 1
end
end
end
local arr = {}
for ip, c in pairs(counts) do arr[#arr + 1] = { ip = ip, c = c } end
table.sort(arr, function(a, b) return a.c > b.c end)
return arr
end
-- Helpers de dibujo ----------------------------------------------------------
local function setcol(cr, c, a)
cairo_set_source_rgba(cr, c[1], c[2], c[3], a or 1.0)
end
local function rrect(cr, x, y, w, h, r)
cairo_new_sub_path(cr)
cairo_arc(cr, x + w - r, y + r, r, -math.pi / 2, 0)
cairo_arc(cr, x + w - r, y + h - r, r, 0, math.pi / 2)
cairo_arc(cr, x + r, y + h - r, r, math.pi / 2, math.pi)
cairo_arc(cr, x + r, y + r, r, math.pi, 1.5 * math.pi)
cairo_close_path(cr)
end
local function text(cr, x, y, s, c, size, bold)
cairo_select_font_face(cr, "DejaVu Sans Mono", CAIRO_FONT_SLANT_NORMAL,
bold and CAIRO_FONT_WEIGHT_BOLD or CAIRO_FONT_WEIGHT_NORMAL)
cairo_set_font_size(cr, size or 12)
setcol(cr, c or COL.text)
cairo_move_to(cr, x, y)
cairo_show_text(cr, s)
end
-- Texto alineado a la derecha (avance mono ~0.6*tamaño) ----------------------
local function rtext(cr, xr, y, s, c, size, bold)
text(cr, xr - #s * (size or 12) * 0.6, y, s, c, size, bold)
end
local function bar(cr, x, y, w, h, frac, c)
if frac < 0 then frac = 0 elseif frac > 1 then frac = 1 end
setcol(cr, COL.tab_inactive); rrect(cr, x, y, w, h, 3); cairo_fill(cr)
setcol(cr, c); rrect(cr, x, y, math.max(2, w * frac), h, 3); cairo_fill(cr)
end
-- Grafico sobre un panel redondeado. Cada serie con s.fill=true se dibuja como
-- area (relleno translucido) ademas de la linea; si no, solo linea.
local function graph(cr, x, y, w, h, series)
setcol(cr, COL.panel); rrect(cr, x, y, w, h, 4); cairo_fill(cr)
-- Maximo comun a todas las series (fijo si se indica)
local maxv = 1
for _, s in ipairs(series) do
if s.max then
if s.max > maxv then maxv = s.max end
else
for i = 1, #s.data do if s.data[i] > maxv then maxv = s.data[i] end end
end
end
for _, s in ipairs(series) do
local n = #s.data
local function px(i) return x + (i - 1) / (n - 1) * w end
local function py(i) return y + h - (s.data[i] / maxv) * (h - 4) - 2 end
-- Color de alerta cuando el valor actual supera el umbral
local col = s.c
if s.warn and s.data[n] >= s.warn then col = COL.red end
if s.fill then
cairo_move_to(cr, px(1), y + h)
for i = 1, n do cairo_line_to(cr, px(i), py(i)) end
cairo_line_to(cr, px(n), y + h)
cairo_close_path(cr)
setcol(cr, col, 0.25); cairo_fill(cr)
end
-- Lineas pixel-perfect: sin antialias y con las coordenadas ancladas al
-- centro del pixel (floor + 0.5) para que el trazo de 1px caiga nitido
-- sobre la rejilla en lugar de difuminarse entre dos columnas.
cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE)
cairo_set_line_width(cr, 1.0); setcol(cr, col)
for i = 1, n do
local sx = math.floor(px(i)) + 0.5
local sy = math.floor(py(i)) + 0.5
if i == 1 then cairo_move_to(cr, sx, sy) else cairo_line_to(cr, sx, sy) end
end
cairo_stroke(cr)
cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT)
end
end
-- Barra de pestañas ----------------------------------------------------------
local function draw_tabs(cr)
local tw = W / NTABS
for i = 1, NTABS do
local x = (i - 1) * tw
local active = (i == current_tab)
setcol(cr, active and COL.tab_active or COL.tab_inactive)
rrect(cr, x + 2, TAB_TOP, tw - 4, TAB_H, 4); cairo_fill(cr)
local lbl = TABS[i]
text(cr, x + tw / 2 - #lbl * 10 * 0.6 / 2, TAB_TOP + 16, lbl,
active and COL.white or COL.dim, 10, active)
end
end
-- Reloj + fecha: franja superior comun a todas las pestañas (formato europeo)
local function draw_clock(cr)
local hora = str("${time %H:%M:%S}")
local fecha = str("${time %d/%m/%Y}")
local dia = str("${time %A}")
text(cr, 12, HDR_TOP + 22, hora, COL.snow, 22, true)
rtext(cr, W - 10, HDR_TOP + 13, dia, COL.cyan, 10)
rtext(cr, W - 10, HDR_TOP + 26, fecha, COL.dim, 11)
end
-- Botones de la pestaña Red --------------------------------------------------
local function draw_buttons(cr)
local bw = (W - 24) / 3
for i = 1, 3 do
local bx = 8 + (i - 1) * (bw + 4)
setcol(cr, COL.tab_inactive); rrect(cr, bx, BTN_Y, bw, BTN_H, 6); cairo_fill(cr)
setcol(cr, COL.tab_active, 0.9); cairo_set_line_width(cr, 1)
rrect(cr, bx, BTN_Y, bw, BTN_H, 6); cairo_stroke(cr)
local lbl = BTNS[i].label
text(cr, bx + bw / 2 - #lbl * 10 * 0.6 / 2, BTN_Y + 17, lbl, COL.text, 10)
end
end
-- Panel: Red -----------------------------------------------------------------
local function draw_red(cr)
local y = CONTENT_TOP
text(cr, 12, y + 10, "Interfaz " .. NIF, COL.dim, 11); y = y + 20
text(cr, 12, y + 10, "Down", COL.down, 12)
text(cr, 90, y + 10, str("${downspeed " .. NIF .. "}"), COL.text, 13, true); y = y + 18
text(cr, 12, y + 10, "Up", COL.up, 12)
text(cr, 90, y + 10, str("${upspeed " .. NIF .. "}"), COL.text, 13, true); y = y + 20
graph(cr, 12, y, W - 24, 56, {
{ data = hist.down, c = COL.down },
{ data = hist.up, c = COL.up },
})
y = y + 62
-- Paquetes por segundo y protocolo (delta de /proc/net/snmp)
text(cr, 12, y + 10, "PAQUETES/s", COL.snow, 10, true)
rtext(cr, W - 10, y + 10, "in / out", COL.dim, 9); y = y + 15
local function prow(label, c, ipkt, opkt)
text(cr, 14, y + 10, label, c, 10)
rtext(cr, W - 10, y + 10, ipkt .. " / " .. opkt, COL.text, 10); y = y + 13
end
prow("TCP", COL.cyan, g_proto.tcp_in, g_proto.tcp_out)
prow("UDP", COL.green, g_proto.udp_in, g_proto.udp_out)
prow("ICMP", COL.yellow, g_proto.icmp_in, g_proto.icmp_out)
y = y + 4
-- Top hosts remotos por numero de conexiones establecidas
text(cr, 12, y + 10, "TOP REMOTOS", COL.snow, 10, true)
rtext(cr, W - 10, y + 10, "conex.", COL.dim, 9); y = y + 15
local arr = top_remotes()
if #arr == 0 then
text(cr, 14, y + 10, "sin conexiones establecidas", COL.dim, 9); y = y + 13
else
for i = 1, math.min(4, #arr) do
text(cr, 14, y + 10, arr[i].ip, COL.text, 10)
rtext(cr, W - 10, y + 10, tostring(arr[i].c), COL.green, 10); y = y + 13
end
end
y = y + 4
local conns = str("${execi 2 ss -tun state established 2>/dev/null | tail -n +2 | wc -l}")
text(cr, 12, y + 10, "Conexiones activas: " .. conns, COL.text, 10); y = y + 13
text(cr, 12, y + 9, "Total ↓ " .. str("${totaldown " .. NIF .. "}") ..
"" .. str("${totalup " .. NIF .. "}"), COL.dim, 9)
draw_buttons(cr)
text(cr, 12, BTN_Y + BTN_H + 18,
"Wireshark abre el buffer de captura mas reciente.", COL.dim, 9)
end
-- Panel: Sistema (portado del widget previo) ---------------------------------
local function draw_sys(cr)
local x = 10
local gw = W - 20
local y = CONTENT_TOP
local function row(label, value, lc, gh)
text(cr, x, y + 11, label, lc, 11, true)
if value and value ~= "" then rtext(cr, W - 10, y + 11, value, COL.snow, 10) end
y = y + 15
return gh
end
local cputemp = str("${execi 3 " .. MET .. " cpu_temp}")
local gputemp = str("${execi 2 " .. MET .. " gpu_temp}")
-- CPU
local gh = row("CPU " .. math.floor(num("${cpu cpu0}")) .. "%",
str("${freq_g}") .. "GHz " .. cputemp .. "°C", COL.teal, 26)
graph(cr, x, y, gw, gh, { { data = hist.cpu, c = COL.green, max = 100, fill = true, warn = 85 } }); y = y + gh + 4
-- RAM
gh = row("RAM " .. math.floor(num("${memperc}")) .. "%",
str("${mem}") .. " / " .. str("${memmax}"), COL.green, 26)
graph(cr, x, y, gw, gh, { { data = hist.ram, c = COL.green, max = 100, fill = true, warn = 85 } }); y = y + gh + 4
-- CPU TEMP
gh = row("CPU TEMP " .. cputemp .. "°C", "", COL.yellow, 20)
graph(cr, x, y, gw, gh, { { data = hist.cputemp, c = COL.yellow, max = 100, fill = true, warn = 80 } }); y = y + gh + 4
-- GPU
gh = row("GPU " .. str("${execi 2 " .. MET .. " gpu_util}") .. "%",
gputemp .. "°C", COL.cyan, 26)
graph(cr, x, y, gw, gh, { { data = hist.gputil, c = COL.cyan, max = 100, fill = true, warn = 90 } }); y = y + gh + 4
-- GPU TEMP
gh = row("GPU TEMP " .. gputemp .. "°C", "", COL.yellow, 20)
graph(cr, x, y, gw, gh, { { data = hist.gputemp, c = COL.yellow, max = 100, fill = true, warn = 80 } }); y = y + gh + 4
-- VRAM
gh = row("VRAM " .. str("${execi 2 " .. MET .. " gpu_memp}") .. "%",
str("${execi 2 " .. MET .. " gpu_memi}"), COL.purple, 26)
graph(cr, x, y, gw, gh, { { data = hist.vram, c = COL.purple, max = 100, fill = true, warn = 92 } }); y = y + gh + 4
-- DISK I/O
gh = row("DISK I/O", str("${diskio}"), COL.orange, 26)
graph(cr, x, y, gw, gh, { { data = hist.diskio, c = COL.purple, fill = true } }); y = y + gh + 6
-- NET (red: download + upload) como área. Cabecera con las flechas
-- coloreadas igual que su línea (↓ cian = down, ↑ naranja = up) para
-- distinguir las dos series de un vistazo.
text(cr, x, y + 11, "NET", COL.blue, 11, true)
local dn_s = "" .. str("${downspeed " .. NIF .. "}")
local up_s = "" .. str("${upspeed " .. NIF .. "}")
local up_x = W - 10 - vlen(up_s) * 10 * 0.6
local dn_x = up_x - 10 - vlen(dn_s) * 10 * 0.6
text(cr, dn_x, y + 11, dn_s, COL.down, 10)
text(cr, up_x, y + 11, up_s, COL.up, 10)
y = y + 15
gh = 26
graph(cr, x, y, gw, gh, {
{ data = hist.down, c = COL.down, fill = true },
{ data = hist.up, c = COL.up, fill = true },
}); y = y + gh + 6
-- Uso de discos
text(cr, x, y + 10, "USO DE DISCOS", COL.snow, 10, true); y = y + 16
local disks = { "/", "/mnt/1tb", "/mnt/2tb", "/mnt/16tb" }
for _, m in ipairs(disks) do
local p = num("${fs_used_perc " .. m .. "}")
text(cr, x, y + 9, m, COL.green, 10)
rtext(cr, W - 10, y + 9,
str("${fs_used " .. m .. "}") .. "/" .. str("${fs_size " .. m .. "}") ..
" " .. math.floor(p) .. "%", COL.dim, 9)
y = y + 12
bar(cr, x, y, gw, 6, p / 100, p > 90 and COL.red or COL.green); y = y + 11
end
end
-- Panel: Docker --------------------------------------------------------------
local function draw_docker(cr)
local y = CONTENT_TOP
local count = str("${execi 3 " .. MET .. " docker_count}") -- "running/total"
text(cr, 12, y + 14, "Contenedores", COL.snow, 12, true)
rtext(cr, W - 10, y + 14, count .. " up", COL.green, 10)
y = y + 28
local list = str("${execi 3 " .. MET .. " docker_list}")
if list == "" then
text(cr, 16, y + 10, "ninguno en marcha", COL.dim, 10)
return
end
for line in list:gmatch("[^\n]+") do
local name, status, image = line:match("^(.-)|(.-)|(.+)$")
if name then
local up = status:match("^Up") ~= nil
local bad = status:match("UNHEALTHY") ~= nil
text(cr, 14, y + 9, "" .. name:sub(1, 16), up and COL.green or COL.dim, 10)
rtext(cr, W - 10, y + 9, status:sub(1, 14),
bad and COL.red or (up and COL.cyan or COL.dim), 9)
y = y + 12
text(cr, 24, y + 9, image:sub(1, 34), COL.dim, 9)
y = y + 14
end
if y > H - 14 then break end
end
end
-- Panel: Procesos ------------------------------------------------------------
local function draw_procesos(cr)
local y = CONTENT_TOP
g_proc_rows = {} -- se rellena con las filas clicables de TOP
local total = str("${execi 2 " .. MET .. " nproc_count}")
local running = str("${execi 2 " .. MET .. " nproc_running}")
local threads = str("${execi 3 " .. MET .. " nthreads}")
local load = str("${execi 2 " .. MET .. " load_avg}")
text(cr, 12, y + 11, "Procesos", COL.snow, 12, true)
rtext(cr, W - 10, y + 11, total .. " (" .. running .. " run)", COL.green, 11); y = y + 17
text(cr, 12, y + 11, "Hilos", COL.dim, 10)
rtext(cr, W - 10, y + 11, threads, COL.text, 10); y = y + 14
text(cr, 12, y + 11, "Carga 1/5/15m", COL.dim, 10)
rtext(cr, W - 10, y + 11, load, COL.text, 10); y = y + 18
-- Tabla "<pid> <valor> <nombre>" por linea. El PID se pinta como enlace
-- (azul) y la fila completa queda registrada en g_proc_rows para abrir
-- PROC_CLICK al clicar.
local function toplist(titulo, key, unidad, col)
text(cr, 12, y + 10, titulo, COL.snow, 10, true)
rtext(cr, W - 10, y + 10, unidad, COL.dim, 9); y = y + 14
local out = str("${execi 3 " .. MET .. " " .. key .. "}")
if out == "" then
text(cr, 16, y + 9, "sin datos", COL.dim, 9); y = y + 13
else
for line in out:gmatch("[^\n]+") do
local pid, val, name = line:match("^%s*(%d+)%s+([%d%.]+)%s+(.+)$")
if pid and val and name then
local ry = y
text(cr, 16, y + 9, pid, COL.blue, 10) -- PID = enlace
text(cr, 72, y + 9, name:sub(1, 13), COL.text, 10)
rtext(cr, W - 10, y + 9, val, col, 10)
g_proc_rows[#g_proc_rows + 1] = { y0 = ry, y1 = ry + 13, pid = pid }
y = y + 13
end
if y > H - 12 then break end
end
end
y = y + 4
end
toplist("TOP CPU", "top_cpu", "%cpu", COL.teal)
toplist("TOP RAM", "top_ram", "%mem", COL.green)
toplist("TOP I/O", "top_io", "KB/s", COL.orange)
end
-- Panel: Devices -------------------------------------------------------------
local function draw_devices(cr)
local y = CONTENT_TOP
text(cr, 12, y + 10, "ALMACENAMIENTO", COL.snow, 10, true); y = y + 15
local disks = str("${execi 10 " .. MET .. " disk_list}")
for line in disks:gmatch("[^\n]+") do
local name, size, model = line:match("^(%S+)%s+(%S+)%s*(.*)$")
if name then
text(cr, 16, y + 9, name .. " " .. (size or ""), COL.cyan, 10)
if model and model ~= "" then rtext(cr, W - 10, y + 9, model:sub(1, 16), COL.dim, 9) end
y = y + 13
end
end
y = y + 6
text(cr, 12, y + 10, "RED", COL.snow, 10, true); y = y + 15
local nets = str("${execi 5 " .. MET .. " net_ifaces}")
for line in nets:gmatch("[^\n]+") do
local ifc, addr = line:match("^(%S+)%s+(%S+)$")
if ifc then
text(cr, 16, y + 9, ifc, COL.green, 10)
rtext(cr, W - 10, y + 9, addr or "", COL.text, 9); y = y + 13
end
end
y = y + 6
text(cr, 12, y + 10, "USB", COL.snow, 10, true); y = y + 15
local usb = str("${execi 10 " .. MET .. " usb_list}")
if usb == "" then
text(cr, 16, y + 9, "sin dispositivos USB", COL.dim, 9); y = y + 13
else
for line in usb:gmatch("[^\n]+") do
text(cr, 16, y + 9, "" .. line:sub(1, 40), COL.text, 9); y = y + 12
if y > H - 12 then break end
end
end
end
-- Hook de dibujo principal ---------------------------------------------------
function conky_draw()
if conky_window == nil then return end
local cs = cairo_xlib_surface_create(conky_window.display,
conky_window.drawable, conky_window.visual,
conky_window.width, conky_window.height)
local cr = cairo_create(cs)
-- Mantener todos los historicos vivos en cualquier pestaña.
push(hist.cpu, num("${cpu cpu0}"))
push(hist.ram, num("${memperc}"))
push(hist.cputemp, num("${execi 3 " .. MET .. " cpu_temp}"))
push(hist.gputil, num("${execi 2 " .. MET .. " gpu_util}"))
push(hist.gputemp, num("${execi 2 " .. MET .. " gpu_temp}"))
push(hist.vram, num("${execi 2 " .. MET .. " gpu_memp}"))
push(hist.diskio, rate_kib(str("${diskio}")))
push(hist.down, num("${downspeedf " .. NIF .. "}"))
push(hist.up, num("${upspeedf " .. NIF .. "}"))
update_proto()
setcol(cr, COL.bg, 0.86); rrect(cr, 0, 0, W, H, 10); cairo_fill(cr)
draw_clock(cr)
draw_tabs(cr)
if current_tab == 1 then draw_sys(cr)
elseif current_tab == 2 then draw_red(cr)
elseif current_tab == 3 then draw_docker(cr)
elseif current_tab == 4 then draw_procesos(cr)
else draw_devices(cr)
end
cairo_destroy(cr)
cairo_surface_destroy(cs)
end
-- Lanza una app con fallback a notify-send si falta el binario ---------------
local function shell_quote(s)
return "'" .. s:gsub("'", "'\\''") .. "'"
end
local function launch(b)
os.execute(string.format("%s %s %s %s &",
shell_quote(LAUNCH), shell_quote(b.bin), shell_quote(b.pkg), shell_quote(b.cmd)))
end
-- Hook de raton: cambia de pestaña y lanza apps ------------------------------
function conky_mouse(event)
if type(event) ~= "table" then return end
if event.type ~= "button_down" then return end
if event.button ~= nil and event.button ~= 1 then return end
local x = event.x or 0
local y = event.y or 0
-- Barra de pestañas
if y >= TAB_TOP and y <= TAB_TOP + TAB_H then
local idx = math.floor(x / (W / NTABS)) + 1
if idx >= 1 and idx <= NTABS then current_tab = idx end
return
end
-- Botones de la pestaña Red (ahora es la pestaña 2)
if current_tab == 2 and y >= BTN_Y and y <= BTN_Y + BTN_H then
local bw = (W - 24) / 3
for i = 1, 3 do
local bx = 8 + (i - 1) * (bw + 4)
if x >= bx and x <= bx + bw then
launch(BTNS[i])
return
end
end
end
-- Pestaña Procesos: clic en una fila TOP abre el proceso (PROC_CLICK con su PID)
if current_tab == 4 then
for _, r in ipairs(g_proc_rows) do
if y >= r.y0 and y <= r.y1 then
os.execute(string.format(PROC_CLICK .. " >/dev/null 2>&1 &", r.pid))
return
end
end
end
end