Compare commits

...

2 Commits

Author SHA1 Message Date
egutierrez 6224120f7d merge: pixel-perfect + reloj + pestañas Procesos/Devices (v0.2.0) 2026-06-06 13:20:08 +02:00
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
4 changed files with 212 additions and 40 deletions
+12 -6
View File
@@ -2,9 +2,9 @@
name: conky_widget name: conky_widget
lang: lua lang: lua
domain: infra domain: infra
version: 0.1.0 version: 0.2.0
description: "Widget de escritorio Conky con pestañas clickeables (Red / Sistema / Docker) y botones que lanzan herramientas de análisis de red." description: "Widget de escritorio Conky con 5 pestañas clickeables (Sistema / Red / Docker / Procesos / Devices), reloj permanente, gráficos de línea pixel-perfect y botones que lanzan herramientas de análisis de red."
tags: [conky, widget, desktop, monitoring, network, lua, launcher-buttons] tags: [conky, widget, desktop, monitoring, network, processes, devices, lua, launcher-buttons]
uses_functions: [] uses_functions: []
uses_types: [] uses_types: []
framework: "conky" framework: "conky"
@@ -31,13 +31,19 @@ el estado del PC en vivo en la esquina superior derecha del monitor primario.
El contenido está organizado en tres pestañas clickeables y, en la pestaña de El contenido está organizado en tres pestañas clickeables y, en la pestaña de
red, incluye botones que abren herramientas de análisis de paquetes. red, incluye botones que abren herramientas de análisis de paquetes.
Una franja superior con **reloj + fecha** (formato europeo) es común a todas las
pestañas. Los gráficos de línea se dibujan sin antialiasing y con las coordenadas
ancladas al pixel (`pixel-perfect`).
## Pestañas ## Pestañas
| Pestaña | Contenido | | Pestaña | Contenido |
|---|---| |---|---|
| **Red** | Velocidad de bajada/subida de `enp5s0`, gráfico histórico en vivo (60 s), conexiones TCP/UDP establecidas, total descargado/subido, y botones [Wireshark] [ntopng] [nethogs]. | | **Sistema** | Gráficos de área en vivo: CPU, RAM, CPU temp, GPU, GPU temp, VRAM, disco I/O y red, más barras de uso de los cuatro discos. Las series se vuelven rojas al superar su umbral. |
| **Sistema** | CPU total + barras por core, RAM, Swap, uso de disco `/`, temperatura, load average y uptime. | | **Red** | Velocidad de bajada/subida de `enp5s0`, gráfico histórico en vivo, paquetes/s por protocolo, top hosts remotos, conexiones establecidas, total descargado/subido, y botones [Wireshark] [ntopng] [nethogs]. |
| **Docker** | Número de contenedores en marcha / totales y lista de nombres activos (lectura sin sudo). | | **Docker** | Contador `running/total` y, por contenedor, nombre + imagen + estado abreviado (`Up 33h`, `Up 2d`), coloreado según salud. |
| **Procesos** | Nº de procesos (total + en ejecución), hilos, carga 1/5/15 min, y tablas TOP CPU / TOP RAM / TOP I/O. Cada fila muestra el PID (clicable: abre `htop -p <pid>`) y el nombre. |
| **Devices** | Dispositivos de almacenamiento (`lsblk`, sin loops), interfaces de red físicas con su IP, y dispositivos USB (`lsusb`). |
## Cómo funciona ## Cómo funciona
+1 -1
View File
@@ -16,7 +16,7 @@ conky.config = {
gap_y = 50, -- separa del panel superior de XFCE gap_y = 50, -- separa del panel superior de XFCE
minimum_width = 290, minimum_width = 290,
maximum_width = 290, maximum_width = 290,
minimum_height = 545, minimum_height = 575,
-- Ventana ('dock' = recibe clicks Y el WM no lo mueve con Alt+drag; 'normal' lo dejaba mover; 'desktop' no recibe clicks) ---------- -- Ventana ('dock' = recibe clicks Y el WM no lo mueve con Alt+drag; 'normal' lo dejaba mover; 'desktop' no recibe clicks) ----------
own_window = true, own_window = true,
+162 -33
View File
@@ -23,11 +23,14 @@ pcall(require, 'cairo_xlib') -- conky >= 1.12 separa el modulo cairo_xlib
-- Geometria compartida entre dibujo y eventos de raton ----------------------- -- Geometria compartida entre dibujo y eventos de raton -----------------------
local W = 290 local W = 290
local H = 545 local H = 575
local TAB_TOP = 4 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 TAB_H = 24
local CONTENT_TOP = TAB_TOP + TAB_H + 4 -- inicio del area de cada panel
local BTN_H = 26 local BTN_H = 26
local BTN_Y = 322 -- fila de botones de la pestaña Red 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) ------------------ -- Interfaz de red a monitorizar (enp5s0 es la fisica activa) ------------------
local NIF = "enp5s0" local NIF = "enp5s0"
@@ -36,9 +39,15 @@ local NIF = "enp5s0"
local MET = os.getenv("HOME") .. "/.config/conky/conky_widget/metric.sh" local MET = os.getenv("HOME") .. "/.config/conky/conky_widget/metric.sh"
-- Pestañas (Sistema por defecto) --------------------------------------------- -- Pestañas (Sistema por defecto) ---------------------------------------------
local TABS = { "Sistema", "Red", "Docker" } local TABS = { "Sistema", "Red", "Docker", "Procs", "Devs" }
local NTABS = #TABS
local current_tab = 1 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, -- Botones de la pestaña Red. bin se comprueba antes de lanzar; si falta,
-- launch.sh avisa con notify-send. -- launch.sh avisa con notify-send.
local BTNS = { local BTNS = {
@@ -253,28 +262,45 @@ local function graph(cr, x, y, w, h, series)
cairo_close_path(cr) cairo_close_path(cr)
setcol(cr, col, 0.25); cairo_fill(cr) setcol(cr, col, 0.25); cairo_fill(cr)
end end
cairo_set_line_width(cr, 1.3); setcol(cr, col) -- 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 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 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 end
cairo_stroke(cr) cairo_stroke(cr)
cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT)
end end
end end
-- Barra de pestañas ---------------------------------------------------------- -- Barra de pestañas ----------------------------------------------------------
local function draw_tabs(cr) local function draw_tabs(cr)
local tw = W / 3 local tw = W / NTABS
for i = 1, 3 do for i = 1, NTABS do
local x = (i - 1) * tw local x = (i - 1) * tw
local active = (i == current_tab) local active = (i == current_tab)
setcol(cr, active and COL.tab_active or COL.tab_inactive) 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) rrect(cr, x + 2, TAB_TOP, tw - 4, TAB_H, 4); cairo_fill(cr)
local lbl = TABS[i] local lbl = TABS[i]
text(cr, x + tw / 2 - #lbl * 12 * 0.6 / 2, TAB_TOP + 17, lbl, text(cr, x + tw / 2 - #lbl * 10 * 0.6 / 2, TAB_TOP + 16, lbl,
active and COL.white or COL.dim, 12, active) active and COL.white or COL.dim, 10, active)
end end
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 -------------------------------------------------- -- Botones de la pestaña Red --------------------------------------------------
local function draw_buttons(cr) local function draw_buttons(cr)
local bw = (W - 24) / 3 local bw = (W - 24) / 3
@@ -290,7 +316,7 @@ end
-- Panel: Red ----------------------------------------------------------------- -- Panel: Red -----------------------------------------------------------------
local function draw_red(cr) local function draw_red(cr)
local y = 46 local y = CONTENT_TOP
text(cr, 12, y + 10, "Interfaz " .. NIF, COL.dim, 11); y = y + 20 text(cr, 12, y + 10, "Interfaz " .. NIF, COL.dim, 11); y = y + 20
text(cr, 12, y + 10, "Down", COL.down, 12) text(cr, 12, y + 10, "Down", COL.down, 12)
@@ -344,7 +370,7 @@ end
local function draw_sys(cr) local function draw_sys(cr)
local x = 10 local x = 10
local gw = W - 20 local gw = W - 20
local y = 38 local y = CONTENT_TOP
local function row(label, value, lc, gh) local function row(label, value, lc, gh)
text(cr, x, y + 11, label, lc, 11, true) text(cr, x, y + 11, label, lc, 11, true)
@@ -421,25 +447,118 @@ end
-- Panel: Docker -------------------------------------------------------------- -- Panel: Docker --------------------------------------------------------------
local function draw_docker(cr) local function draw_docker(cr)
local y = 50 local y = CONTENT_TOP
local running = str("${execi 3 docker ps -q 2>/dev/null | wc -l}") local count = str("${execi 3 " .. MET .. " docker_count}") -- "running/total"
local total = str("${execi 10 docker ps -aq 2>/dev/null | wc -l}") text(cr, 12, y + 14, "Contenedores", COL.snow, 12, true)
text(cr, 12, y, "Contenedores", COL.text, 12, true) rtext(cr, W - 10, y + 14, count .. " up", COL.green, 10)
rtext(cr, W - 10, y, running .. " up / " .. total .. " total", COL.dim, 10) y = y + 28
y = y + 22
local names = str("${execi 3 docker ps --format '{{.Names}}' 2>/dev/null | head -28}") local list = str("${execi 3 " .. MET .. " docker_list}")
if names == "" then if list == "" then
text(cr, 16, y, "ninguno en marcha", COL.dim, 10) text(cr, 16, y + 10, "ninguno en marcha", COL.dim, 10)
return return
end end
for line in names:gmatch("[^\n]+") do for line in list:gmatch("[^\n]+") do
text(cr, 16, y, "" .. line, COL.green, 10) local name, status, image = line:match("^(.-)|(.-)|(.+)$")
y = y + 15 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 if y > H - 14 then break end
end 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 --------------------------------------------------- -- Hook de dibujo principal ---------------------------------------------------
function conky_draw() function conky_draw()
if conky_window == nil then return end if conky_window == nil then return end
@@ -462,13 +581,13 @@ function conky_draw()
setcol(cr, COL.bg, 0.86); rrect(cr, 0, 0, W, H, 10); cairo_fill(cr) setcol(cr, COL.bg, 0.86); rrect(cr, 0, 0, W, H, 10); cairo_fill(cr)
draw_clock(cr)
draw_tabs(cr) draw_tabs(cr)
if current_tab == 1 then if current_tab == 1 then draw_sys(cr)
draw_sys(cr) elseif current_tab == 2 then draw_red(cr)
elseif current_tab == 2 then elseif current_tab == 3 then draw_docker(cr)
draw_red(cr) elseif current_tab == 4 then draw_procesos(cr)
else else draw_devices(cr)
draw_docker(cr)
end end
cairo_destroy(cr) cairo_destroy(cr)
@@ -496,8 +615,8 @@ function conky_mouse(event)
-- Barra de pestañas -- Barra de pestañas
if y >= TAB_TOP and y <= TAB_TOP + TAB_H then if y >= TAB_TOP and y <= TAB_TOP + TAB_H then
local idx = math.floor(x / (W / 3)) + 1 local idx = math.floor(x / (W / NTABS)) + 1
if idx >= 1 and idx <= 3 then current_tab = idx end if idx >= 1 and idx <= NTABS then current_tab = idx end
return return
end end
@@ -512,4 +631,14 @@ function conky_mouse(event)
end end
end 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 end
+37
View File
@@ -12,5 +12,42 @@ case "$1" in
cpu_temp) for h in /sys/class/hwmon/hwmon*; do cpu_temp) for h in /sys/class/hwmon/hwmon*; do
[ "$(cat "$h/name" 2>/dev/null)" = coretemp ] && { cat "$h/temp1_input"; break; } [ "$(cat "$h/name" 2>/dev/null)" = coretemp ] && { cat "$h/temp1_input"; break; }
done | awk '{printf "%d", $1/1000}' ;; done | awk '{printf "%d", $1/1000}' ;;
# --- Pestaña Procesos ---
nproc_count) ps -e --no-headers 2>/dev/null | wc -l ;;
nproc_running) awk '/^procs_running/{print $2}' /proc/stat 2>/dev/null ;;
nthreads) ps -eLf --no-headers 2>/dev/null | wc -l ;;
load_avg) awk '{printf "%s %s %s", $1, $2, $3}' /proc/loadavg 2>/dev/null ;;
# "pid %valor nombre" por linea, mayor primero
top_cpu) ps -eo pid=,pcpu=,comm= --sort=-pcpu 2>/dev/null | head -n 6 ;;
top_ram) ps -eo pid=,pmem=,comm= --sort=-pmem 2>/dev/null | head -n 6 ;;
# I/O por proceso: requiere pidstat (paquete sysstat); si falta, vacio.
# Las columnas kB_rd/s y kB_wr/s se cuentan desde el final (Command = ultimo
# campo, iodelay = NF-1, kB_ccwr/s = NF-2, kB_wr/s = NF-3, kB_rd/s = NF-4),
# robusto frente a que la primera columna sea epoch o UID.
top_io) command -v pidstat >/dev/null 2>&1 \
&& pidstat -d 1 1 2>/dev/null \
| awk 'NF>=6 && $NF!="Command" && $0 !~ /Average|Linux|^[[:space:]]*$/ \
{ io=$(NF-4)+$(NF-3); if (io>0) printf "%s %.0f %s\n", $(NF-5), io, $NF }' \
| sort -k2 -rn | head -n 5 \
|| echo "" ;;
# --- Pestaña Devices ---
# -e 7,11 excluye loops (snaps) y cdrom; solo discos reales
disk_list) lsblk -dno NAME,SIZE,MODEL -e 7,11 2>/dev/null | head -n 8 ;;
usb_list) lsusb 2>/dev/null \
| sed -E 's/^Bus [0-9]+ Device [0-9]+: ID [0-9a-fA-F]+:[0-9a-fA-F]+ //' \
| grep -viE 'root hub' | head -n 10 ;;
# Interfaces fisicas/reales (excluye docker, bridges, veth, virbr)
net_ifaces) ip -br -4 addr 2>/dev/null \
| awk '$1 !~ /^(docker|br-|veth|virbr)/ && ($2=="UP" || $1=="lo") {printf "%s %s\n", $1, $3}' \
| head -n 6 ;;
# --- Pestaña Docker ---
# "nombre|estado|imagen" por contenedor. Estado y imagen abreviados.
docker_list) docker ps --format '{{.Names}}|{{.Status}}|{{.Image}}' 2>/dev/null \
| sed -E 's/Up ([0-9]+) seconds?/Up \1s/; s/Up ([0-9]+) minutes?/Up \1m/; s/Up ([0-9]+) hours?/Up \1h/; s/Up ([0-9]+) days?/Up \1d/; s/Up ([0-9]+) weeks?/Up \1w/; s/Up ([0-9]+) months?/Up \1mo/; s/Up Less than a second/Up 0s/; s/ \(healthy\)//; s/ \(unhealthy\)/ BAD/' \
| head -n 14 ;;
docker_count) printf "%s/%s" "$(docker ps -q 2>/dev/null | wc -l)" "$(docker ps -aq 2>/dev/null | wc -l)" ;;
*) echo 0 ;; *) echo 0 ;;
esac esac