JARVIS/config/ventanas.sh
sito 6a81cef3be JARVIS elige modelo, backend Wayland, y roadmap para la comunidad
- Seleccion de modelo: JARVIS cambia su cerebro en caliente (elegir_modelo).
- Ventanas en Wayland: backend para Sway e Hyprland (ventanas-wayland.sh).
- ROADMAP.md y CONTRIBUTING.md: tareas acotadas y como empezar, para animar
  commits (mas gestores, mas acciones, probar otros modelos, el RAG).
- docs/rag.md: tabla explicita de que se publica y que no del RAG; nota de
  que un clon recien hecho ya tiene RAG util (man pages + glosario) sin datos
  privados.
- README enlaza el roadmap y menciona la eleccion de modelo.
2026-08-16 16:58:41 +02:00

320 lines
15 KiB
Bash
Executable file

#!/usr/bin/env bash
# Mover ventanas y repartirlas por las pantallas. El motor de las acciones del
# grupo "ventanas": ellas solo llaman aqui con un verbo y un argumento.
#
# Por que un script del host y no un modulo dentro del flatpak: las acciones se
# ejecutan como comandos de shell, y toda la logica en Python obligaria a
# recompilar el flatpak por cada ajuste. Aqui se edita y ya esta, igual que los
# envoltorios de voz.
#
# Motor X11 (xdotool). En Wayland, mover ventanas AJENAS depende del compositor
# —cada uno tiene su via (swaymsg, hyprctl...) o directamente lo prohibe por su
# modelo de seguridad (GNOME/KDE), y los tiling ni tienen "mitad izquierda"—, asi
# que no hay una via universal. Aqui se detecta el entorno y se avisa claro; el
# resto de JARVIS funciona igual, solo estas ordenes de ventanas quedan fuera en
# Wayland. Soporte por compositor: pendiente (ver el roadmap del README).
#
# ventanas.sh pantallas lista las pantallas
# ventanas.sh listar lista las ventanas movibles
# ventanas.sh mover <ventana> <pantalla> la manda a esa pantalla
# ventanas.sh zona <ventana> <zona> mitad izquierda, cuarto, centro...
# ventanas.sh escritorio <ventana> <n> la manda a ese escritorio virtual
# ventanas.sh maximiza <ventana> / restaura / minimiza
# ventanas.sh alias <pantalla> <nombre> le pone nombre a una salida
#
# <ventana> puede ser: parte del titulo o de la clase ("terminal", "firefox"),
# o "esta"/"activa" para la que tiene el foco.
# <pantalla> puede ser: un alias ("tele"), una salida ("HDMI-1"), un numero
# ("2") o una posicion ("derecha", "izquierda", "arriba", "abajo", "centro").
set -uo pipefail
RAIZ=$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)
ALIAS_FICHERO=${JARVIS_ALIAS_PANTALLAS:-$HOME/.config/jarvis-pantallas.json}
hay_x11() { [ -n "${DISPLAY:-}" ] && command -v xdotool >/dev/null 2>&1; }
if ! hay_x11; then
en_wayland() { [ "${XDG_SESSION_TYPE:-}" = "wayland" ] || [ -n "${WAYLAND_DISPLAY:-}" ]; }
# En Wayland con Sway o Hyprland, delega en el backend propio (actua sobre la
# ventana activa). El verbo va en $1 y para las ordenes de ventana el destino
# (izquierda/derecha/centro/maximiza...) suele venir en $3; se le pasa el que
# tenga sentido.
if en_wayland && (command -v swaymsg >/dev/null 2>&1 || command -v hyprctl >/dev/null 2>&1); then
BACKEND="$(dirname "${BASH_SOURCE[0]:-$0}")/ventanas-wayland.sh"
case "${1:-}" in
zona) exec bash "$BACKEND" "${3:-centro}" ;; # zona <ventana> <izquierda|derecha|centro>
maximiza|restaura|minimiza) exec bash "$BACKEND" "$1" ;;
*) echo "en Wayland de momento van: maximiza, restaura, minimiza," \
"y mitades (izquierda/derecha/centro). El resto, en el roadmap."; exit 1 ;;
esac
fi
if en_wayland; then
echo "estas en Wayland (${XDG_CURRENT_DESKTOP:-tu compositor}) sin Sway ni" \
"Hyprland: mover ventanas ajenas no tiene via universal ahi. El resto" \
"de JARVIS funciona igual. Soporte por compositor: ver ROADMAP.md."
elif [ -n "${DISPLAY:-}" ]; then
echo "estas en X11 pero falta xdotool. Instalalo con tu gestor de paquetes."
else
echo "no hay sesion grafica (ni DISPLAY ni WAYLAND_DISPLAY)."
fi
exit 1
fi
# ---------------------------------------------------------------- pantallas
# Una linea por pantalla: salida<TAB>x<TAB>y<TAB>ancho<TAB>alto<TAB>numero
#
# Se usa `xrandr --listmonitors`, que da la geometria ya resuelta (con el
# escalado aplicado); `--query` daria los modos, que no es lo que hace falta.
# El numero es el orden de izquierda a derecha, no el que diga xrandr: es lo
# que espera cualquiera que diga "la pantalla dos".
pantallas_crudas() {
xrandr --listmonitors 2>/dev/null | tail -n +2 | while read -r _ _ geo salida; do
# geo viene como 1920/344x1080/194+0+0
local an al x y
an=${geo%%/*}; geo=${geo#*x}
al=${geo%%/*}; geo=${geo#*+}
x=${geo%%+*}; y=${geo##*+}
printf '%s\t%s\t%s\t%s\t%s\n' "$salida" "$x" "$y" "$an" "$al"
done | sort -t$'\t' -k2 -n | awk -F'\t' '{printf "%s\t%s\t%s\t%s\t%s\t%d\n", $1,$2,$3,$4,$5,NR}'
}
alias_de() {
[ -f "$ALIAS_FICHERO" ] || { echo ""; return; }
python3 -c "
import json, sys
try:
print(json.load(open('$ALIAS_FICHERO')).get(sys.argv[1], ''))
except Exception:
print('')
" "$1" 2>/dev/null
}
# izquierda / derecha / arriba / abajo / centro, deducido de donde cae el
# centro de esa pantalla respecto al centro de todas.
posicion_de() {
local x=$1 an=$2 y=$3 al=$4
local n; n=$(pantallas_crudas | wc -l)
[ "$n" -le 1 ] && { echo "unica"; return; }
local minx maxx miny maxy
minx=$(pantallas_crudas | awk -F'\t' 'NR==1||$2<m{m=$2}END{print m+0}')
maxx=$(pantallas_crudas | awk -F'\t' '{v=$2+$4; if(v>m)m=v}END{print m+0}')
miny=$(pantallas_crudas | awk -F'\t' 'NR==1||$3<m{m=$3}END{print m+0}')
maxy=$(pantallas_crudas | awk -F'\t' '{v=$3+$5; if(v>m)m=v}END{print m+0}')
local cx=$(( x + an/2 )) cy=$(( y + al/2 ))
local mx=$(( (minx+maxx)/2 )) my=$(( (miny+maxy)/2 ))
local anchura=$(( maxx-minx )) altura=$(( maxy-miny ))
# el reparto manda por el eje donde hay mas separacion
if [ $(( anchura - altura )) -gt 0 ]; then
[ "$cx" -lt $(( mx - anchura/8 )) ] && { echo izquierda; return; }
[ "$cx" -gt $(( mx + anchura/8 )) ] && { echo derecha; return; }
else
[ "$cy" -lt $(( my - altura/8 )) ] && { echo arriba; return; }
[ "$cy" -gt $(( my + altura/8 )) ] && { echo abajo; return; }
fi
echo centro
}
listar_pantallas() {
local salida x y an al num pos ali
while IFS=$'\t' read -r salida x y an al num; do
pos=$(posicion_de "$x" "$an" "$y" "$al")
ali=$(alias_de "$salida")
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
"$num" "$salida" "${ali:--}" "$pos" "$x" "$y" "$an" "$al"
done < <(pantallas_crudas)
}
# Resuelve lo que se dice a una pantalla. Orden a proposito: primero lo que el
# usuario ha nombrado el mismo, que es lo mas especifico, y la posicion la
# ultima porque es la mas facil de confundir.
resuelve_pantalla() {
local q; q=$(echo "$1" | tr 'A-ZÁÉÍÓÚ' 'a-záéíóú')
local num salida ali pos x y an al
# 1. alias
while IFS=$'\t' read -r num salida ali pos x y an al; do
[ "$ali" != "-" ] && [ "$(echo "$ali" | tr 'A-Z' 'a-z')" = "$q" ] && {
printf '%s\t%s\t%s\t%s\t%s\n' "$salida" "$x" "$y" "$an" "$al"; return 0; }
done < <(listar_pantallas)
# 2. salida exacta o su prefijo (hdmi, dp)
while IFS=$'\t' read -r num salida ali pos x y an al; do
local s; s=$(echo "$salida" | tr 'A-Z' 'a-z')
[ "$s" = "$q" ] || [ "${s%%-*}" = "$q" ] && {
printf '%s\t%s\t%s\t%s\t%s\n' "$salida" "$x" "$y" "$an" "$al"; return 0; }
done < <(listar_pantallas)
# 3. numero, en cifra o en letra
local n=""
case "$q" in
1|una|uno|primera) n=1;; 2|dos|segunda) n=2;; 3|tres|tercera) n=3;; 4|cuatro|cuarta) n=4;;
esac
if [ -n "$n" ]; then
while IFS=$'\t' read -r num salida ali pos x y an al; do
[ "$num" = "$n" ] && { printf '%s\t%s\t%s\t%s\t%s\n' "$salida" "$x" "$y" "$an" "$al"; return 0; }
done < <(listar_pantallas)
fi
# 4. posicion
while IFS=$'\t' read -r num salida ali pos x y an al; do
[ "$pos" = "$q" ] && { printf '%s\t%s\t%s\t%s\t%s\n' "$salida" "$x" "$y" "$an" "$al"; return 0; }
done < <(listar_pantallas)
return 1
}
# ----------------------------------------------------------------- ventanas
# El area util de la pantalla que contiene un punto: hay que descontar barras
# y paneles, o las ventanas acaban debajo de la barra de GNOME.
area_util() {
local v; v=$(xprop -root _NET_WORKAREA 2>/dev/null | sed 's/.*= //' | tr -d ' ')
if [ -n "$v" ]; then
echo "$(echo "$v" | cut -d, -f1) $(echo "$v" | cut -d, -f2) $(echo "$v" | cut -d, -f3) $(echo "$v" | cut -d, -f4)"
else
set -- $(xdotool getdisplaygeometry); echo "0 0 $1 $2"
fi
}
# Se descartan las que no son ventanas de verdad: el guardian de mutter, el
# propio shell y las de 1x1 que usan algunos programas como buzon.
listar_ventanas() {
local w n cls x y an al
for w in $(xdotool search --onlyvisible --name "." 2>/dev/null); do
n=$(xdotool getwindowname "$w" 2>/dev/null) || continue
[ -z "$n" ] && continue
case "$n" in "mutter guard window"|"gnome-shell") continue;; esac
eval "$(xdotool getwindowgeometry --shell "$w" 2>/dev/null)" || continue
[ "${WIDTH:-0}" -lt 50 ] && continue
cls=$(xprop -id "$w" WM_CLASS 2>/dev/null | sed 's/.*, *//' | tr -d '"')
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$w" "$n" "${cls:--}" "$X" "$Y" "$WIDTH" "$HEIGHT"
done
}
# Devuelve el id, o varios si hay empate: quien llama decide si preguntar.
resuelve_ventana() {
local q; q=$(echo "$1" | tr 'A-ZÁÉÍÓÚ' 'a-záéíóú')
case "$q" in
esta|activa|"esta ventana"|"la activa"|"ventana activa")
xdotool getactivewindow 2>/dev/null; return;;
esac
listar_ventanas | awk -F'\t' -v q="$q" '
BEGIN{IGNORECASE=1}
{ t=tolower($2); c=tolower($3);
if (index(t,q) || index(c,q)) print $1"\t"$2 }'
}
geo_de() { eval "$(xdotool getwindowgeometry --shell "$1" 2>/dev/null)"; echo "$X $Y $WIDTH $HEIGHT"; }
# Maximizar NO se hace con `xdotool windowstate --add MAXIMIZED`: comprobado,
# en mutter no hace nada. Lo que funciona es activar la ventana y mandar el
# atajo del escritorio. Y una ventana maximizada IGNORA windowmove, asi que
# hay que desmaximizar antes de mover.
desmaximiza() { xdotool windowactivate "$1" 2>/dev/null; sleep 0.2; xdotool key super+Down 2>/dev/null; sleep 0.4; }
maximiza() { xdotool windowactivate "$1" 2>/dev/null; sleep 0.2; xdotool key super+Up 2>/dev/null; sleep 0.4; }
coloca() { # coloca <id> <x> <y> <ancho> <alto>
desmaximiza "$1"
xdotool windowsize "$1" "$4" "$5" 2>/dev/null
xdotool windowmove "$1" "$2" "$3" 2>/dev/null
sleep 0.3
}
# --------------------------------------------------------------------- api
# Imprime el id, o una frase que se pueda decir en voz alta. Nada de
# marcadores tipo NINGUNA/VARIAS: lo que salga de aqui puede acabar en el
# altavoz tal cual, asi que tiene que ser castellano.
una_ventana() {
local r; r=$(resuelve_ventana "$1")
local n; n=$(echo "$r" | grep -c . )
if [ "$n" -eq 0 ]; then
echo "No encuentro ninguna ventana que sea $1, señor."; return 1
fi
if [ "$n" -gt 1 ]; then
# Ambiguo: se enumeran y decide la persona. Elegir una al azar es lo
# peor que puede hacer algo que mueve ventanas ajenas.
printf 'Hay %s que encajan, señor: %s. Digame cual.\n' "$n" \
"$(echo "$r" | cut -f2 | head -4 | paste -sd'|' - | sed 's/|/; /g')"
return 2
fi
echo "$r" | cut -f1
}
case "${1:-}" in
pantallas)
listar_pantallas | while IFS=$'\t' read -r num salida ali pos x y an al; do
printf 'pantalla %s: %s%s, %s, %sx%s\n' "$num" "$salida" \
"$([ "$ali" != "-" ] && echo " ($ali)")" "$pos" "$an" "$al"
done
;;
listar)
listar_ventanas | while IFS=$'\t' read -r w n cls x y an al; do
printf '%s (%s) %sx%s en %s,%s\n' "$n" "$cls" "$an" "$al" "$x" "$y"
done
;;
alias)
salida=$(resuelve_pantalla "${2:-}" | cut -f1)
[ -n "$salida" ] || { echo "No se cual es esa pantalla, señor"; exit 1; }
python3 - "$ALIAS_FICHERO" "$salida" "${3:-}" <<'FIN'
import json, os, sys
fichero, salida, nombre = sys.argv[1], sys.argv[2], sys.argv[3]
os.makedirs(os.path.dirname(fichero), exist_ok=True)
try:
d = json.load(open(fichero))
except Exception:
d = {}
d[salida] = nombre
json.dump(d, open(fichero, "w"), ensure_ascii=False, indent=2)
print(f"{salida} se llama ahora {nombre}")
FIN
;;
mover)
w=$(una_ventana "${2:-}") || { echo "$w"; exit 1; }
destino=$(resuelve_pantalla "${3:-}") || { echo "No se cual es esa pantalla, señor"; exit 1; }
IFS=$'\t' read -r salida px py pan pal <<< "$destino"
read -r ax ay aan aal <<< "$(area_util)"
# se centra en la pantalla destino, a tres cuartos de su tamaño
an=$(( pan * 3 / 4 )); al=$(( pal * 3 / 4 ))
x=$(( px + (pan - an) / 2 )); y=$(( py + (pal - al) / 2 ))
[ "$y" -lt "$ay" ] && y=$ay
coloca "$w" "$x" "$y" "$an" "$al"
# "eDP-1" en voz alta se lee letra por letra: se dice el mote si lo tiene,
# y si no el numero, que es lo que entiende cualquiera.
comodo=$(listar_pantallas | awk -F'\t' -v s="$salida" '$2==s {print ($3!="-") ? $3 : "numero " $1}')
echo "Movida a la pantalla ${comodo:-$salida}"
;;
zona)
w=$(una_ventana "${2:-}") || { echo "$w"; exit 1; }
read -r ax ay aan aal <<< "$(area_util)"
m2=$(( aan / 2 )); h2=$(( aal / 2 ))
case "$(echo "${3:-}" | tr 'A-ZÁÉÍÓÚ' 'a-záéíóú')" in
izquierda|"mitad izquierda") coloca "$w" "$ax" "$ay" "$m2" "$aal"; echo "Ya la tiene a la izquierda";;
derecha|"mitad derecha") coloca "$w" $((ax+m2)) "$ay" "$m2" "$aal"; echo "Ya la tiene a la derecha";;
arriba) coloca "$w" "$ax" "$ay" "$aan" "$h2"; echo "Ya la tiene arriba";;
abajo) coloca "$w" "$ax" $((ay+h2)) "$aan" "$h2"; echo "Ya la tiene abajo";;
"arriba izquierda") coloca "$w" "$ax" "$ay" "$m2" "$h2"; echo "Colocada arriba a la izquierda";;
"arriba derecha") coloca "$w" $((ax+m2)) "$ay" "$m2" "$h2"; echo "Colocada arriba a la derecha";;
"abajo izquierda") coloca "$w" "$ax" $((ay+h2)) "$m2" "$h2"; echo "Colocada abajo a la izquierda";;
"abajo derecha") coloca "$w" $((ax+m2)) $((ay+h2)) "$m2" "$h2"; echo "Colocada abajo a la derecha";;
centro|centrada) coloca "$w" $((ax+aan/4)) $((ay+aal/4)) $((aan/2)) $((aal/2)); echo "Ya la tiene centrada";;
*) echo "No conozco esa zona, señor"; exit 1;;
esac
;;
maximiza) w=$(una_ventana "${2:-}") || { echo "$w"; exit 1; }; maximiza "$w"; echo "Maximizada";;
restaura) w=$(una_ventana "${2:-}") || { echo "$w"; exit 1; }; desmaximiza "$w"; echo "Restaurada a su tamaño";;
minimiza) w=$(una_ventana "${2:-}") || { echo "$w"; exit 1; }
xdotool windowminimize "$w" 2>/dev/null; echo "Minimizada";;
escritorio)
w=$(una_ventana "${2:-}") || { echo "$w"; exit 1; }
n=${3:-1}
case "$n" in una|uno|primero) n=1;; dos|segundo) n=2;; tres|tercero) n=3;; cuatro) n=4;; esac
total=$(xdotool get_num_desktops 2>/dev/null || echo 1)
[ "$n" -ge 1 ] 2>/dev/null && [ "$n" -le "$total" ] || { echo "no hay escritorio $n, solo $total"; exit 1; }
xdotool set_desktop_for_window "$w" $(( n - 1 )) 2>/dev/null
echo "Movida al escritorio $n"
;;
*) sed -n '2,26p' "$0"; exit 1;;
esac