diff --git a/app.md b/app.md index e0fe6df..14644ca 100644 --- a/app.md +++ b/app.md @@ -2,9 +2,9 @@ name: conky_widget lang: lua domain: infra -version: 0.1.0 -description: "Widget de escritorio Conky con pestañas clickeables (Red / Sistema / Docker) y botones que lanzan herramientas de análisis de red." -tags: [conky, widget, desktop, monitoring, network, lua, launcher-buttons] +version: 0.2.0 +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, processes, devices, lua, launcher-buttons] uses_functions: [] uses_types: [] 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 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ñ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** | CPU total + barras por core, RAM, Swap, uso de disco `/`, temperatura, load average y uptime. | -| **Docker** | Número de contenedores en marcha / totales y lista de nombres activos (lectura sin sudo). | +| **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. | +| **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** | 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 `) 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 diff --git a/conky.conf b/conky.conf index aa8b3bd..6a02edd 100644 --- a/conky.conf +++ b/conky.conf @@ -16,7 +16,7 @@ conky.config = { gap_y = 50, -- separa del panel superior de XFCE minimum_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) ---------- own_window = true, diff --git a/lua/widget.lua b/lua/widget.lua index bea7400..1693279 100644 --- a/lua/widget.lua +++ b/lua/widget.lua @@ -23,11 +23,14 @@ 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 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 = 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) ------------------ local NIF = "enp5s0" @@ -36,9 +39,15 @@ local NIF = "enp5s0" local MET = os.getenv("HOME") .. "/.config/conky/conky_widget/metric.sh" -- Pestañas (Sistema por defecto) --------------------------------------------- -local TABS = { "Sistema", "Red", "Docker" } +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 = { @@ -253,28 +262,45 @@ local function graph(cr, x, y, w, h, series) cairo_close_path(cr) setcol(cr, col, 0.25); cairo_fill(cr) 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 - 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 cairo_stroke(cr) + cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT) end end -- Barra de pestañas ---------------------------------------------------------- local function draw_tabs(cr) - local tw = W / 3 - for i = 1, 3 do + 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 + 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] - text(cr, x + tw / 2 - #lbl * 12 * 0.6 / 2, TAB_TOP + 17, lbl, - active and COL.white or COL.dim, 12, active) + 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 @@ -290,7 +316,7 @@ end -- Panel: Red ----------------------------------------------------------------- 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, "Down", COL.down, 12) @@ -344,7 +370,7 @@ end local function draw_sys(cr) local x = 10 local gw = W - 20 - local y = 38 + local y = CONTENT_TOP local function row(label, value, lc, gh) text(cr, x, y + 11, label, lc, 11, true) @@ -421,25 +447,118 @@ 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 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 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) + 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 names:gmatch("[^\n]+") do - text(cr, 16, y, "● " .. line, COL.green, 10) - y = y + 15 + 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 " " 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 @@ -462,13 +581,13 @@ function conky_draw() 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) - else - draw_docker(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) @@ -496,8 +615,8 @@ function conky_mouse(event) -- 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 + local idx = math.floor(x / (W / NTABS)) + 1 + if idx >= 1 and idx <= NTABS then current_tab = idx end return end @@ -512,4 +631,14 @@ function conky_mouse(event) 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 diff --git a/metric.sh b/metric.sh index c43607b..14fbd2d 100755 --- a/metric.sh +++ b/metric.sh @@ -12,5 +12,42 @@ case "$1" in cpu_temp) for h in /sys/class/hwmon/hwmon*; do [ "$(cat "$h/name" 2>/dev/null)" = coretemp ] && { cat "$h/temp1_input"; break; } 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 ;; esac