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
+1
View File
@@ -0,0 +1 @@
*.log
+27
View File
@@ -0,0 +1,27 @@
# conky_widget
Widget de escritorio Conky con tres pestañas clickeables para monitorizar el PC
en vivo y lanzar herramientas de análisis de red.
![tabs](Red · Sistema · Docker)
## Arranque rápido
```bash
./install.sh
conky -c ~/.config/conky/conky_widget/conky.conf &
```
## Estructura
```
conky_widget/
conky.conf # ventana, posición, carga de Lua
lua/
widget.lua # render Cairo de las pestañas + hook de clics
launch.sh # lanza apps con fallback a notify-send
install.sh # symlink en ~/.config/conky + autostart XFCE
app.md # frontmatter de registro
```
Ver `app.md` para detalle de pestañas, configuración y requisitos.
+83
View File
@@ -0,0 +1,83 @@
---
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]
uses_functions: []
uses_types: []
framework: "conky"
entry_point: "conky.conf"
dir_path: "apps/conky_widget"
repo_url: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/conky_widget"
e2e_checks:
- id: lint_install
cmd: "bash -n install.sh"
timeout_s: 10
- id: lint_launch
cmd: "bash -n lua/launch.sh"
timeout_s: 10
- id: lua_parse
cmd: "luac -p lua/widget.lua 2>/dev/null || lua -e \"assert(loadfile('lua/widget.lua'))\""
timeout_s: 10
severity: warning
---
# conky_widget
Visualizador de escritorio basado en Conky (1.19, X11 + Lua + Cairo) que muestra
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.
## 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). |
## Cómo funciona
- `conky.conf` posiciona la ventana (`own_window_type = normal` para recibir
clics) y delega todo el dibujo a `lua/widget.lua`.
- `lua/widget.lua` dibuja con Cairo en el hook `conky_draw` y gestiona los clics
en el hook `conky_mouse`: la barra superior cambia de pestaña y los botones de
la pestaña Red lanzan la herramienta correspondiente.
- `lua/launch.sh` comprueba si el binario existe antes de lanzarlo; si falta,
muestra un `notify-send` indicando el paquete a instalar.
## Instalación
```bash
cd apps/conky_widget
./install.sh # symlink en ~/.config/conky + autostart XFCE
conky -c ~/.config/conky/conky_widget/conky.conf & # arrancar ahora
```
Parar: `pkill -f conky_widget/conky.conf`
## Configuración
- **Monitor**: editar `xinerama_head` en `conky.conf` (`0` = HDMI-0 primario,
`1` = DP-1).
- **Interfaz de red**: cambiar `NIF` al inicio de `lua/widget.lua`.
- **Botones**: editar la tabla `BTNS` en `lua/widget.lua` (etiqueta, binario a
comprobar, paquete apt y comando shell).
## Requisitos
- `conky` compilado con Lua bindings + Cairo (verificable con `conky --version`).
- Sesión X11 (en Wayland los clics y `own_window` no funcionan).
- Opcional para los botones: `wireshark`, `ntopng`, `nethogs`. Sin ellos el
widget funciona igual; los botones avisan de cómo instalarlos.
## Notas
- El botón Wireshark abre `/var/log/pktcap/cap.pcapng` (buffer rotativo de 10
min) si existe; en caso contrario abre Wireshark en vivo. Ese buffer lo genera
un servicio `dumpcap` aparte (pendiente de montar).
- El historial del gráfico de red se mantiene en memoria mientras Conky corre;
al reiniciar el widget se empieza de cero.
+47
View File
@@ -0,0 +1,47 @@
--[[
conky_widget — visualizador de escritorio con pestañas clickeables.
Tres pestañas (Red / Sistema / Docker) dibujadas con Cairo en lua/widget.lua.
Posicionado en la esquina superior derecha del monitor primario (HDMI-0 = head 0).
Para usarlo en el otro monitor (DP-1), cambiar xinerama_head a 1.
Render y eventos de ratón viven en lua/widget.lua.
]]
conky.config = {
-- Posicionamiento --------------------------------------------------------
alignment = 'top_right',
xinerama_head = 0, -- 0 = HDMI-0 (primario, derecha). 1 = DP-1.
gap_x = 24, -- separacion desde el borde derecho
gap_y = 24, -- separacion desde el borde superior
minimum_width = 300,
maximum_width = 300,
minimum_height = 360,
-- Ventana (tipo 'normal' = recibe clicks; 'desktop' los ignora) ----------
own_window = true,
own_window_type = 'normal',
own_window_argb_visual = true,
own_window_transparent = true,
own_window_hints = 'undecorated,below,sticky,skip_taskbar,skip_pager',
own_window_class = 'ConkyWidget',
own_window_title = 'conky_widget',
-- Render -----------------------------------------------------------------
double_buffer = true,
update_interval = 1.0,
background = false,
use_xft = true,
font = 'DejaVu Sans Mono:size=9',
draw_shades = false,
default_color = 'cccccc',
-- Lua: todo el dibujo y los clicks ---------------------------------------
lua_load = '~/.config/conky/conky_widget/lua/widget.lua',
lua_draw_hook_post = 'conky_draw',
lua_mouse_hook = 'conky_mouse',
}
-- El contenido se dibuja integramente con Cairo en el hook conky_draw.
-- Un espacio mantiene el bloque de texto valido sin pintar nada visible.
conky.text = [[ ]]
Executable
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Instala conky_widget: enlaza la config en ~/.config/conky, registra el
# autostart de XFCE y deja el widget listo para arrancar.
#
# Idempotente: se puede ejecutar varias veces sin efectos duplicados.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONKY_DIR="$HOME/.config/conky"
LINK="$CONKY_DIR/conky_widget"
AUTOSTART_DIR="$HOME/.config/autostart"
DESKTOP="$AUTOSTART_DIR/conky_widget.desktop"
CONF="$LINK/conky.conf"
echo "==> conky_widget install"
# 1. Comprobar dependencia
if ! command -v conky >/dev/null 2>&1; then
echo "ERROR: conky no esta instalado. Instalar: sudo apt install conky-all" >&2
exit 1
fi
# 2. Enlazar la app dentro de ~/.config/conky (ruta estable para la config)
mkdir -p "$CONKY_DIR"
ln -sfn "$SCRIPT_DIR" "$LINK"
echo " symlink: $LINK -> $SCRIPT_DIR"
# 3. Permisos de ejecucion para los scripts
chmod +x "$SCRIPT_DIR/lua/launch.sh" "$SCRIPT_DIR/install.sh" 2>/dev/null || true
# 4. Autostart de XFCE (espera 5s a que el compositor este listo)
mkdir -p "$AUTOSTART_DIR"
cat > "$DESKTOP" <<EOF
[Desktop Entry]
Type=Application
Name=Conky Widget
Comment=Visualizador de escritorio con pestañas (Red / Sistema / Docker)
Exec=sh -c "sleep 5; exec conky -c '$CONF'"
Terminal=false
X-GNOME-Autostart-enabled=true
EOF
echo " autostart: $DESKTOP"
echo "==> Listo."
echo " Arrancar ahora: conky -c '$CONF' &"
echo " Parar: pkill -f conky_widget/conky.conf"
echo " Monitor: editar xinerama_head en conky.conf (0=HDMI-0, 1=DP-1)"
Executable
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Lanza una herramienta si su binario existe; si falta, avisa con notify-send
# indicando el paquete a instalar. Llamado desde widget.lua al clicar un boton.
#
# Uso: launch.sh <binario_a_comprobar> <paquete-apt> <comando-shell>
set -u
bin="${1:-}"
pkg="${2:-}"
cmd="${3:-}"
if [ -z "$bin" ] || [ -z "$cmd" ]; then
exit 1
fi
if command -v "$bin" >/dev/null 2>&1; then
setsid sh -c "$cmd" >/dev/null 2>&1 &
else
notify-send "conky_widget — falta herramienta" \
"No esta '$bin'. Instalar: sudo apt install $pkg" 2>/dev/null || true
fi
+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