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.
This commit is contained in:
Egutierrez
2026-06-06 13:20:08 +02:00
parent 653188201c
commit e4fdd0199d
4 changed files with 212 additions and 40 deletions
+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 -----------------------
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 "<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
@@ -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