- 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.
320 lines
15 KiB
Bash
Executable file
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
|