--[[ 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 = 545 local TAB_TOP = 4 local TAB_H = 24 local BTN_H = 26 local BTN_Y = 322 -- fila de botones de la pestaña Red -- 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" } local current_tab = 1 -- 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 -- 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 cairo_set_line_width(cr, 1.3); setcol(cr, col) for i = 1, n do if i == 1 then cairo_move_to(cr, px(i), py(i)) else cairo_line_to(cr, px(i), py(i)) end end cairo_stroke(cr) end end -- Barra de pestañas ---------------------------------------------------------- local function draw_tabs(cr) local tw = W / 3 for i = 1, 3 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 + 3, TAB_TOP, tw - 6, TAB_H, 5); cairo_fill(cr) local lbl = TABS[i] text(cr, x + tw / 2 - #lbl * 12 * 0.6 / 2, TAB_TOP + 17, lbl, active and COL.white or COL.dim, 12, active) end 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 = 46 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 = 38 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 -- 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 = 50 local running = str("${execi 3 docker ps -q 2>/dev/null | wc -l}") local total = str("${execi 10 docker ps -aq 2>/dev/null | wc -l}") text(cr, 12, y, "Contenedores", COL.text, 12, true) rtext(cr, W - 10, y, running .. " up / " .. total .. " total", COL.dim, 10) y = y + 22 local names = str("${execi 3 docker ps --format '{{.Names}}' 2>/dev/null | head -28}") if names == "" then text(cr, 16, y, "ninguno en marcha", COL.dim, 10) return end for line in names:gmatch("[^\n]+") do text(cr, 16, y, "● " .. line, COL.green, 10) y = y + 15 if y > H - 14 then break 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_tabs(cr) if current_tab == 1 then draw_sys(cr) elseif current_tab == 2 then draw_red(cr) else draw_docker(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 / 3)) + 1 if idx >= 1 and idx <= 3 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 end