#!/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 la manda a esa pantalla # ventanas.sh zona mitad izquierda, cuarto, centro... # ventanas.sh escritorio la manda a ese escritorio virtual # ventanas.sh maximiza / restaura / minimiza # ventanas.sh alias le pone nombre a una salida # # puede ser: parte del titulo o de la clase ("terminal", "firefox"), # o "esta"/"activa" para la que tiene el foco. # 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 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: salidaxyanchoaltonumero # # 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||$2m)m=v}END{print m+0}') miny=$(pantallas_crudas | awk -F'\t' 'NR==1||$3m)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 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