feat: scaffold conky_widget desktop monitor

Conky (X11 + Lua + Cairo) desktop widget with three clickable tabs
(Red / Sistema / Docker) rendered entirely with Cairo:

- Red: live down/up speed for enp5s0, 60s history graph, active
  connections count, totals, and launcher buttons for Wireshark,
  ntopng and nethogs (with notify-send fallback when missing).
- Sistema: total CPU + per-core bars, RAM, swap, root disk usage,
  temperature, load average and uptime.
- Docker: running/total container count and active container names
  (read without sudo).

Includes install.sh (symlink into ~/.config/conky + XFCE autostart),
launch.sh (tool launcher with missing-binary fallback) and app.md
with e2e_checks. Positioned top-right of the primary monitor
(xinerama_head 0), configurable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Egutierrez
2026-06-02 20:46:54 +02:00
commit 5ad4c0d901
7 changed files with 548 additions and 0 deletions
+322
View File
@@ -0,0 +1,322 @@
--[[
conky_widget — render Cairo + pestañas clickeables.
Estructura:
- Estado: current_tab (1=Red, 2=Sistema, 3=Docker).
- conky_draw (lua_draw_hook_post): 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.
Geometria fija (la ventana mide 300x360 segun conky.conf):
- Barra de pestañas: y 4..28, tres pestañas de ancho W/3.
- Botones de la pestaña Red: fila inferior en BTN_Y.
]]
require 'cairo'
pcall(require, 'cairo_xlib') -- conky >= 1.12 separa el modulo cairo_xlib
-- Geometria compartida entre dibujo y eventos de raton -----------------------
local W = 300
local H = 360
local TAB_TOP = 4
local TAB_H = 24
local BTN_H = 28
local BTN_Y = H - 40 -- 320
-- Interfaz de red a monitorizar (detectada: enp5s0 es la fisica activa) ------
local NIF = "enp5s0"
-- Pestañas -------------------------------------------------------------------
local TABS = { "Red", "Sistema", "Docker" }
local current_tab = 1
-- Botones de la pestaña Red. cmd es un comando shell; bin se comprueba antes
-- de lanzar y, si falta, launch.sh avisa con notify-send.
local BTNS = {
{ label = "Wireshark", bin = "wireshark", pkg = "wireshark",
cmd = "wireshark /var/log/pktcap/cap.pcapng 2>/dev/null || wireshark" },
{ label = "ntopng", bin = "ntopng", pkg = "ntopng",
cmd = "xdg-open http://localhost:3000" },
{ label = "nethogs", bin = "nethogs", pkg = "nethogs",
cmd = "xfce4-terminal --title='nethogs ' -e 'sudo nethogs " .. NIF .. "'" },
}
local LAUNCH = os.getenv("HOME") .. "/.config/conky/conky_widget/lua/launch.sh"
-- Historico de velocidad de red para el grafico en vivo ----------------------
local HIST = 60
local down_hist, up_hist = {}, {}
for i = 1, HIST do down_hist[i] = 0; up_hist[i] = 0 end
local function push(t, v)
table.remove(t, 1)
t[#t + 1] = v
end
-- Paleta ---------------------------------------------------------------------
local COL = {
bg = { 0.08, 0.09, 0.11 },
panel = { 0.14, 0.15, 0.18 },
tab_active = { 0.18, 0.55, 0.85 },
tab_inactive = { 0.18, 0.19, 0.23 },
text = { 0.86, 0.87, 0.90 },
white = { 1.00, 1.00, 1.00 },
dim = { 0.55, 0.57, 0.62 },
green = { 0.30, 0.80, 0.45 },
orange = { 0.95, 0.60, 0.20 },
red = { 0.90, 0.30, 0.35 },
down = { 0.30, 0.70, 0.95 },
up = { 0.95, 0.55, 0.30 },
}
-- 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
-- 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
-- Centrado aproximado para fuente monoespaciada (avance ~0.6*tamaño) ----------
local function ctext(cr, cx, y, s, c, size, bold)
local tw = #s * (size or 12) * 0.6
text(cr, cx - tw / 2, 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
-- 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)
ctext(cr, x + tw / 2, TAB_TOP + 17, TABS[i],
active and COL.white or COL.dim, 12, active)
end
end
-- Grafico de red en vivo -----------------------------------------------------
local function draw_netgraph(cr, x, y, w, h)
setcol(cr, COL.panel); rrect(cr, x, y, w, h, 6); cairo_fill(cr)
local maxv = 1
for i = 1, HIST do
if down_hist[i] > maxv then maxv = down_hist[i] end
if up_hist[i] > maxv then maxv = up_hist[i] end
end
local function plot(hist, c)
cairo_set_line_width(cr, 1.5); setcol(cr, c)
for i = 1, HIST do
local px = x + (i - 1) / (HIST - 1) * w
local py = y + h - (hist[i] / maxv) * (h - 6) - 3
if i == 1 then cairo_move_to(cr, px, py) else cairo_line_to(cr, px, py) end
end
cairo_stroke(cr)
end
plot(down_hist, COL.down)
plot(up_hist, COL.up)
text(cr, x + 5, y + 12, string.format("max %.0f KiB/s", maxv), COL.dim, 8)
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)
ctext(cr, bx + bw / 2, BTN_Y + 18, BTNS[i].label, COL.text, 10)
end
end
-- Panel: Red -----------------------------------------------------------------
local function draw_red(cr)
local y = 50
text(cr, 14, y, "Interfaz " .. NIF, COL.dim, 11); y = y + 22
text(cr, 14, y, "Down", COL.down, 12)
text(cr, 90, y, str("${downspeed " .. NIF .. "}"), COL.text, 13, true); y = y + 20
text(cr, 14, y, "Up", COL.up, 12)
text(cr, 90, y, str("${upspeed " .. NIF .. "}"), COL.text, 13, true); y = y + 24
draw_netgraph(cr, 14, y, W - 28, 78); y = y + 90
local conns = str("${execi 2 ss -tun state established 2>/dev/null | tail -n +2 | wc -l}")
text(cr, 14, y, "Conexiones activas: " .. conns, COL.text, 11); y = y + 18
text(cr, 14, y, "Total ↓ " .. str("${totaldown " .. NIF .. "}") ..
"" .. str("${totalup " .. NIF .. "}"), COL.dim, 10)
draw_buttons(cr)
end
-- Panel: Sistema -------------------------------------------------------------
local function draw_sys(cr)
local y = 50
local cpu = num("${cpu cpu0}")
text(cr, 14, y, "CPU", COL.text, 12, true)
text(cr, W - 52, y, string.format("%d%%", cpu), COL.text, 12)
y = y + 6
bar(cr, 14, y, W - 28, 8, cpu / 100, cpu > 80 and COL.red or COL.green)
y = y + 18
local nproc = math.floor(num("${exec nproc}"))
if nproc < 1 then nproc = 1 elseif nproc > 16 then nproc = 16 end
local cw = (W - 28) / nproc
for i = 1, nproc do
local cu = num("${cpu cpu" .. i .. "}")
bar(cr, 14 + (i - 1) * cw, y, cw - 2, 6, cu / 100,
cu > 80 and COL.red or COL.green)
end
y = y + 20
local memp = num("${memperc}")
text(cr, 14, y, "RAM", COL.text, 12, true)
text(cr, W - 150, y, str("${mem}") .. " / " .. str("${memmax}"), COL.dim, 9)
y = y + 6
bar(cr, 14, y, W - 28, 8, memp / 100, memp > 85 and COL.red or COL.orange)
y = y + 18
local swapp = num("${swapperc}")
text(cr, 14, y, "Swap", COL.text, 12, true)
text(cr, W - 150, y, str("${swap}") .. " / " .. str("${swapmax}"), COL.dim, 9)
y = y + 6
bar(cr, 14, y, W - 28, 8, swapp / 100, COL.orange)
y = y + 18
local diskp = num("${fs_used_perc /}")
text(cr, 14, y, "Disco /", COL.text, 12, true)
text(cr, W - 150, y, str("${fs_used /}") .. " / " .. str("${fs_size /}"), COL.dim, 9)
y = y + 6
bar(cr, 14, y, W - 28, 8, diskp / 100, diskp > 90 and COL.red or COL.green)
y = y + 24
local traw = tonumber(str("${execi 5 cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null}"))
local tstr = traw and string.format("%.0f°C", traw / 1000) or "n/a"
text(cr, 14, y, "Temp " .. tstr, COL.dim, 10)
text(cr, W - 150, y, "Load " .. str("${loadavg 1}"), COL.dim, 10); y = y + 16
text(cr, 14, y, "Uptime " .. str("${uptime_short}"), COL.dim, 10)
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, 14, y, "Contenedores", COL.text, 12, true)
text(cr, W - 110, y, running .. " up / " .. total .. " total", COL.dim, 10)
y = y + 22
local names = str("${execi 3 docker ps --format '{{.Names}}' 2>/dev/null | head -16}")
if names == "" then
text(cr, 18, y, "ninguno en marcha", COL.dim, 10)
return
end
for line in names:gmatch("[^\n]+") do
text(cr, 18, y, "" .. line, COL.green, 10)
y = y + 15
if y > H - 16 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 el historico de red vivo en todas las pestañas.
push(down_hist, num("${downspeedf " .. NIF .. "}"))
push(up_hist, num("${upspeedf " .. NIF .. "}"))
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_red(cr)
elseif current_tab == 2 then
draw_sys(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)
local cmd = string.format("%s %s %s %s &",
shell_quote(LAUNCH), shell_quote(b.bin), shell_quote(b.pkg), shell_quote(b.cmd))
os.execute(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
if current_tab == 1 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