From 5ad4c0d901e8061285ebd0a90effbcf1701480d4 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Tue, 2 Jun 2026 20:46:54 +0200 Subject: [PATCH] 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) --- .gitignore | 1 + README.md | 27 +++++ app.md | 83 +++++++++++++ conky.conf | 47 ++++++++ install.sh | 47 ++++++++ lua/launch.sh | 21 ++++ lua/widget.lua | 322 +++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 548 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.md create mode 100644 conky.conf create mode 100755 install.sh create mode 100755 lua/launch.sh create mode 100644 lua/widget.lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..397b4a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..edb27c7 --- /dev/null +++ b/README.md @@ -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. diff --git a/app.md b/app.md new file mode 100644 index 0000000..e0fe6df --- /dev/null +++ b/app.md @@ -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. diff --git a/conky.conf b/conky.conf new file mode 100644 index 0000000..2acc364 --- /dev/null +++ b/conky.conf @@ -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 = [[ ]] diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..52921d0 --- /dev/null +++ b/install.sh @@ -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" < 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)" diff --git a/lua/launch.sh b/lua/launch.sh new file mode 100755 index 0000000..12018ef --- /dev/null +++ b/lua/launch.sh @@ -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 +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 diff --git a/lua/widget.lua b/lua/widget.lua new file mode 100644 index 0000000..7ced9e1 --- /dev/null +++ b/lua/widget.lua @@ -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