XMonad, tres pantallas y un debugger de Haskell que no existe
Hay días en que te sientas a hacer "un cambio pequeño" y cuatro horas después estás leyendo el código fuente de XMonad, mirando fijamente cómo un String llamado "NSP" puede colapsar un programa entero en silencio. Este fue uno de esos días.
El problema: workspaces compartidos en multi-monitor
XMonad, por defecto, tiene un modelo de workspaces que puede
sorprender si vienes de GNOME o KDE: los workspaces son globales. Si
tienes tres pantallas y pulsas Mod+2, las
tres pantallas siguen mostrando lo mismo porque workspace 2 existe una
sola vez en todo el sistema.
Esto no es un bug, es una decisión de diseño. Pero cuando tienes un MacBook con pantalla Retina 2560x1600 conectado a un monitor ASUS de 27" y una tele Samsung de 1080p, quieres que cada pantalla tenga su propia vida. Que puedas trabajar en el editor en la pantalla principal mientras el navegador está en la secundaria sin que "viajar" al workspace 2 rompa tu layout.
La solución existe y se llama IndependentScreens. Está en el paquete XMonad.Layout.IndependentScreens y lo que hace
es sencillo de entender: multiplica tus workspaces por el número de
pantallas y añade un prefijo de pantalla. Si tienes 3 pantallas y 9
workspaces, en realidad tienes 27 workspaces internos con nombres como
0_1, 1_1,
2_1… pero tú sigues viendo solo el 1 al 9,
cada uno independiente en su pantalla.
El cambio en la configuración es aparentemente inocente:
import XMonad.Layout.IndependentScreens
-- Antes
myWorkspaces = map show [1..9]
-- Después
myWorkspaces = withScreens 3 (map show [1..9])Y luego ajustar los keybindings para que usen onCurrentScreen en lugar de los bindings
directos:
-- Para cambiar de workspace: solo la pantalla actual
[((m .|. modMask, k), windows $ onCurrentScreen f i)
| (i, k) <- zip (workspaces' conf) [xK_1..xK_9]
, (f, m) <- [(W.greedyView, 0), (W.shift, shiftMask)]]Hasta aquí, la teoría. La práctica fue otra cosa.
Primer bug: readProcess dentro del restart
El número de pantallas no es un valor estático en una config declarativa. Hoy tienes tres monitores, mañana enchufas el portátil solo. Así que lo lógico es detectar las pantallas al inicio.
La primera aproximación fue usar readProcess para ejecutar xrandr y contar las líneas con "connected".
Funciona perfectamente al arrancar XMonad. Pero al hacer mod+q (restart de XMonad), el proceso se
colgaba.
El motivo tiene que ver con cómo los sistemas Unix manejan señales
entre procesos padre e hijo. XMonad, durante el restart, sobreescribe el
handler de SIGCHLD. Cuando readProcess lanza un subprocess, espera esa
señal para saber que terminó. Si el handler está modificado, la espera
se convierte en un bucle infinito.
La solución correcta es no salir del proceso X11 para contar pantallas. XMonad ya tiene una función que lo hace nativo:
import XMonad.Layout.IndependentScreens (countScreens)
-- Correcto: usa X11 directamente, sin subprocess
nScreens <- countScreens
let myWorkspaces = withScreens nScreens (map show [1..9])countScreens consulta directamente al
servidor X sin ejecutar nada externo. Funciona en cualquier contexto,
incluido el restart.
Segundo bug: el
crash silencioso de marshallPP
Este fue el más entretenido de depurar.
XMonad tiene un logHook que comunica el
estado de los workspaces a xmobar (la barra de estado). Con IndependentScreens, los workspaces tienen
nombres internos como 0_1, 1_2… pero xmobar debe mostrar solo el número,
sin el prefijo de pantalla. Para eso existe marshallPP: convierte la PP (pretty-printer) de xmobar para que filtre y
transforme los nombres según la pantalla.
El setup era correcto. Los workspaces se mostraban bien. Pero había
un scratchpad configurado con XMonad.Util.NamedScratchpad, y ese scratchpad
tiene un workspace especial llamado… NSP.
El problema: marshallPP asume que todos
los workspaces tienen el formato N_nombre.
Cuando encuentra el workspace NSP e
intenta procesarlo con unmarshallS (que
extrae el prefijo de pantalla), hace un read sobre la cadena "NSP". Y read :: Int sobre "NSP" lanza una excepción en
tiempo de ejecución.
Excepción silenciosa. Sin mensaje de error visible. Solo xmobar dejaba de mostrar información.
La solución requiere que el filtro del workspace NSP se aplique antes de que marshallPP lo vea:
myLogHook xmproc0 xmproc1 = do
-- filterOutWsPP ANTES de marshallPP. El orden importa.
let pp screen handle = marshallPP screen $ filterOutWsPP [scratchpadWorkspaceTag] def
{ ppOutput = hPutStrLn handle
, ppCurrent = xmobarColor "#98be65" "" . wrap "[" "]"
, ppVisible = xmobarColor "#98be65" ""
, ppHidden = xmobarColor "#c678dd" "" . noScratchPad
, -- ...
}
multiPP (pp 0 xmproc0) (pp 1 xmproc1)Tercer
bug: prevWS' y nextWS' con workspaces no válidos
Mismo problema, diferente lugar. Las funciones de navegación prevWS' y nextWS'
(que saltan entre workspaces ignorando los vacíos) también llaman
internamente a unmarshallS. Si en la lista
de workspaces aparece NSP antes de ser
filtrado, mismo crash.
La solución aquí es filtrar antes de llamar a esas funciones, usando
isMarshalled para verificar que un
workspace tiene el formato correcto (es decir, que contiene el separador
'_'):
import XMonad.Layout.IndependentScreens (isMarshalled)
-- Solo navegar entre workspaces con formato válido (N_nombre)
prevWS' (isMarshalled . W.tag)
nextWS' (isMarshalled . W.tag)isMarshalled simplemente comprueba si
el nombre contiene un '_'. Trivial. Pero sin ese filtro, cualquier
workspace con nombre arbitrario explota en tiempo de ejecución.
autorandr declarativo: el punto final
Con los workspaces funcionando, quedaba el problema del DPI. La pantalla Retina del MacBook necesita escala 2x (DPI 192). El monitor externo va a DPI 96. Cuando conectas o desconectas monitores, el DPI debe cambiar.
La solución habitual es tener scripts sueltos de xrandr por ahí. La solución NixOS es usar autorandr con perfiles declarativos en
home-manager:
programs.autorandr = {
enable = true;
profiles = {
"laptop-solo" = {
fingerprints = {
eDP-1 = "hash-del-panel-interno";
};
config = {
eDP-1 = {
enable = true;
mode = "2560x1600";
rate = "60.00";
dpi = 192;
};
};
};
"dual-asus" = {
fingerprints = {
eDP-1 = "hash-del-panel";
HDMI-1 = "hash-del-asus";
};
config = {
eDP-1 = { enable = true; dpi = 96; position = "0x0"; };
HDMI-1 = { enable = true; mode = "2560x1440"; position = "2560x0"; };
};
};
};
};Cuando conectas un monitor, autorandr
detecta qué perfil corresponde por la huella de los monitores conectados
y aplica la configuración automáticamente. El DPI se ajusta solo.
Una nota sobre la compatibilidad con una pantalla
Antes de terminar, esto importa: con una sola pantalla, IndependentScreens se comporta exactamente igual
que la configuración original. Los workspaces internos son 0_1 a 0_9, pero tú
ves 1 a 9
como siempre. No hay diferencia perceptible.
Esto significa que la migración es segura en servidores o máquinas con un solo monitor. Cambias la config, reinicias XMonad, y nada cambia desde el punto de vista del usuario.
Conclusión
IndependentScreens es la forma correcta
de gestionar múltiples monitores en XMonad si quieres comportamiento
similar a GNOME o KDE. El módulo existe, está maduro, y funciona bien.
Los bugs que encontramos no son del módulo en sí sino de interacciones
con NamedScratchpad (el workspace NSP) y
con cómo inicializas el conteo de pantallas.
Las lecciones resumidas:
- Usar
countScreens(X11 nativo) en lugar dereadProcess+ xrandr filterOutWsPPsiempre antes demarshallPPisMarshalledpara filtrar antes deprevWS'=/=nextWS'autorandren home-manager es más robusto que scripts sueltos
El código completo vive en el repositorio de dotfiles. Y la configuración funciona en producción desde el mismo día que se escribió.
Comentarios (0)
Sin comentarios todavia. Se el primero!
Deja un comentario