Cuaderno de GNUtas

EXWM, o GNU Emacs como gestor de ventanas

Más que un simple editor de texto (en el sentido moderno del término), GNU Emacs es un poderosísimo entorno de trabajo desde donde podemos hacer todo o casi todo. Una máquina Lisp, en suma, que hace una sola cosa y la hace bien: evaluar Lisp. Asumido esto, se suele dar entre muchos usuarios una curiosa evolución, o un camino de desprendimiento de todo lo accesorio, que uno ha acabado siguiendo casi escrupulosamente y sin apenas darse cuenta. Lo primero es dejar atrás los entornos de escritorio y saludar a los gestores de ventanas. En mi caso, y tras una larga estancia en Openbox, terminé recalando en Bspwn, un excelente y liviano gestor estilo «mosaico» con el que mantuve hasta hace no mucho un intenso y productivo idilio, pues Bspwm es muy poco intrusivo y se lleva a las mil maravillas con Emacs1. Pero el paso final era inevitable: ¿por qué no dar el salto a EXWM, es decir, a hacer de Emacs el propio gestor de ventanas? Si bien esta idea es poco menos que un sueño para un adicto a Emacs, lo estuve postergando indefinidamente, casi por elemental pereza, ya que me encontraba ciertamente muy a gusto en Bspwm. Hace poco, sin embargo, conseguí reunir algo de tiempo y valor y emprendí una escaramuza. La primera estancia, que se presumía de mero tanteo, me trajo una impresión tan grata que al final me he quedado a vivir allí (¿por cuanto tiempo?), diciendo adiós a mi antiguo y hasta la fecha cumplidor gestor de ventanas, y saludando con entusiasmo al Emacs X Window Manager.

Pero, ¿qué es realmente este EXWM y qué tiene de especial? Pues es, como dije arriba, ni más ni menos que un gestor de ventanas para Xorg basado en Emacs. De hecho, es Emacs, el Emacs de siempre, nuestro amado y cotidiano Emacs convertido en un gestor de ventanas. Las consecuencias de ello son tan evidentes como emocionantes: ahora cualquier aplicación que no sea Emacs (navegadores, editores de fotos, emuladores de terminal, procesadores de texto, reproductores multimedia, etc. En fin, todo lo que gestiona un gestor de ventanas al uso) se convierte por arte de magia lispiana en un búfer más de Emacs. La integración, por tanto, es total. Ahora ya podemos decir con orgullo que Emacs es con toda justicia nuestro entorno de escritorio. Así que, sin más distracción, paso a comentar y a compartir mi configuración inaugural de EXWM, que de momento me resulta bastante agradable. Pero vayamos por pasos, y comencemos por lo básico, que es la instalación.

Instalación de EXWM

La primera noticia grata es que no es necesario salir de Emacs para instalarlo. EXWM es un paquete de Emacs como otro cualquiera y podemos instalarlo sencillamente desde el repositorio Melpa. Y ya lo tenemos en un periquete. Por supuesto, hay que leerse el tutorial de su página de GitHub, donde viene todo muy bien explicado. No tardo en aprender que podemos empezar por una configuración básica y recomendada, así que pongo en mi init:

(require 'exwm)
(require 'exwm-config)
(exwm-config-default)

Después de algunos trasteos, la configuración por defecto me resultó bastante satisfactoria (además de que ya viene con algunos atajos predefinidos), de modo que recomiendo dejarla como base y no empezar el edificio desde cero.

Atajos

Este es un aspecto muy peculiar, ya que es necesario definir algunos atajos que funcionen en cualquier búfer, tanto en los búferes nativos de Emacs como en los que albergan una aplicación externa, como un navegador por ejemplo. En general no hay sorpresas y todo suele ser lo esperable. Es decir, que si estamos dentro del Firefox y pulsamos C-x b se nos abrirá la habitual lista de búferes (Fierefox, insisto, es un búfer más). Si pulsamos C-x 2 dividiremos todo en dos ventanas verticales. Etc. Pero en seguida me di cuenta de que conviene añadir unos pocos atajos extra, como los de windmove para moverse fácilmente entre ventanas, ya que las definiciones que tenía para el Emacs «normal» no me servían en ventanas de aplicaciones externas. Para ello tenemos la variable exwm-input-set-key. Así es cómo quedaron mis atajos para windmove:

(exwm-input-set-key (kbd "S-<left>") 'windmove-left)
(push (elt (kbd "S-<left>") 0) exwm-input-prefix-keys)
(exwm-input-set-key (kbd "S-<right>") 'windmove-right)
(push (elt (kbd "S-<right>") 0) exwm-input-prefix-keys)
(exwm-input-set-key (kbd "S-<down>") 'windmove-down)
(push (elt (kbd "S-<down>") 0) exwm-input-prefix-keys)
(exwm-input-set-key (kbd "S-<up>") 'windmove-up)
(push (elt (kbd "S-<up>") 0) exwm-input-prefix-keys)

EXWM viene también con algunos atajos muy útiles para cuando estamos dentro de una aplicación externa. Por ejemplo, C-c C-t C-f nos permitirá alternar entre el modo «mosaico» (el predeterminado) y el modo «flotante». C-c C-h nos ocultará una ventana flotante. ¿Y qué ocurre si queremos usar los propios atajos que traiga la aplicación externa, como los de «copiar», «pegar», «cortar», etc.? Hay varias triquiñuelas. La que tenemos más a mano puede ser anteponer a los atajos nativos de la aplicación externa el prefijo C-c C-q, que nos llamará a la función exwm-input-send-next-key2. Pero si necesitamos algo más que un solo atajo puntual, podemos activar el llamado char-mode, mediante C-c C-k, que interrumpirá todos los atajos de Emacs (salvo los globales de EXWM) a fin de poder usar las combinaciones de la aplicación externa. Para salir del char-mode y recuperar el modo normal, el line-mode, pulsaremos entonces s-r.

Y otra cosa más, ¿tenemos escritorios virtuales? Sí, por supuesto, como en cualquier gestor de ventanas que se precie. Podemos navegar por ellos con el atajo s-w, donde «s» es la tecla «super», más conocida como la tecla Windows, que en Emacs se implica poco pero que aquí podemos hacer un uso muy generoso de ella para la gestión de las ventanas, tal como sucedía en Bspwm. Una vez pulsado ese atajo, seremos capaces de navegar por los escritorios virtuales desde el minibúfer.

Lanzando aplicaciones externas

La forma que viene de fábrica en EXWM para lanzar una aplicación externa es mediante el atajo s-&. Se recomienda usar éste en lugar de M-& (async-shell-command). Sin embargo, yo prefiero (ya que uso mucho el sistema de listas interactivas y autocompletivas Helm), llamar a una aplicación externa mediante helm-run-external-command, función a la que le definí este atajo mediante la sintaxis explicada más arriba:

(exwm-input-set-key (kbd "s-<") 'helm-run-external-command)
(push (elt (kbd "s-<") 0) exwm-input-prefix-keys)

Por supuesto, podemos también definir atajos para lanzar aplicaciones concretas. Se me ocurrió definir esto para lanzar el Firefox:

(exwm-input-set-key (kbd "s-f") (lambda ()
                                  (interactive)
                                  (let
                                      ((comando "firefox"))
                                    (start-process-shell-command comando nil comando))))

(push (elt (kbd "s-f") 0) exwm-input-prefix-keys)

¿Solo o acompañado?

Bien, ya tenemos más o menos configurado el EXWM a nuestro gusto. Como gestor de ventanas, desde luego, podemos usarlo solo. No está de más hacerlo así al principio, para una primera aproximación. Para ello, copiamos el contenido del archivo xinitrc de la carpeta exmw, que estará sin duda en el directorio ~/.emacs.d/elpa/exwm-0.23/ (o con el número de versión que sea) y lo pegamos en un archivo .xinitrc (ojo, con puntito delante) de nuestra carpeta local. Salimos de la sesión en que estemos, abrimos una consola (con control + alt + f2, por ejemplo) e invocamos el comando startx. Si todo va bien, como es esperable, nos hallaremos raudos en nuestro flamante Emacs como gestor de ventanas. Y así podemos dejarlo, si estamos satisfechos. Yo, sin embargo, he preferido usar de base un escritorio mínimo y ligero como LXDE. Y paso a explicar cómo lo he hecho.

Con LXDE

Partiendo de que ya hemos instalado LXDE y hemos lanzado una sesión a fin de que se nos generen todos los archivos de configuración necesarios, lo primero que tenemos que hacer es decirle a LXDE que use como gestor de ventanas EXWM y no Openbox. De modo que abrimos el fichero ~/.config/lxsession/LXDE/desktop.conf y ponemos:

[Session]
window_manager=emacs

A continuación, en ese mismo directorio, tenemos el archivo autostart, para apuntar las aplicaciones que queremos se inicien automáticamente con cada arranque de sesión. Allí anoté unas cuantas. Y, como no me gusta el panel de LXDE, puse en su lugar a Polybar (que ya venía usando con Bspwm). Pero eso ya va en gustos3. Mi fichero autostart luciría, entonces, tal que así:

@pcmanfm --desktop --profile LXDE
@polybar mibar
@lxpolkit
@redshift-gtk
@nm-applet
@picom -b -C
@dunst

Como se ve, uso el excelente gestor de notificaciones dunst. También añadí luego un script donde incluí una serie de apaños más (remapeo de algunas letras del teclado) que deseo incorporar al inicio de sesión. Y, nada, ya sólo queda iniciar LXDE, y se obra la magia.

exwm1.png

Figura 1: EXWM con transparencias activadas (necesitamos tener corriendo un compositor, como Picom)

exwm2.png

Figura 2: EXWM con transparencias y ventanas flotantes

Apéndice (actualización de 11/06/20)

Después de un tiempo usando en exclusiva este gestor de ventanas emacsiano puedo decir que estoy bastante satisfecho y que ha pasado a ser el gestor de ventanas por defecto en todos mis ordenadores de trabajo. Así que añado este apéndice para dar cuenta de ciertos apaños más que le he ido aplicando. A saber.

La cuestión del lanzador de aplicaciones

No he renunciado al estupendo Rofi, que tengo configurado de la siguiente forma para invocarlo con la secuencia s-d. Una vez abierto se muestra Rofi en modo lanzador, y si pulso la tecla «<», me muestra las ventanas abiertas de las aplicaciones externas:

(exwm-input-set-key (kbd "s-d") (lambda ()
                                    (interactive)
                                    (let
                                        ((rofi "rofi -modi drun,window -show drun -kb-mode-next \"less\" -kb-delete-entry \"control+q\" -show-icons"))
                                      (call-process-shell-command rofi))))

  (push (elt (kbd "s-d") 0) exwm-input-prefix-keys)

Pero, si bien lo mantengo, porque tampoco estorba, al final he optado por crearme mi propio lanzador con Ido, que ha venido a sustituir a helm-run-external-command. Primero definí la función que genera la lista de aplicaciones:

(defun crea-lista-programas ()
  (interactive)
  (seq-mapcat
   (lambda (bin)
     (directory-files bin nil "^[^.]"))
   exec-path))

Y a continuación el lanzador con ido propiamente dicho:

(defun ido-lanzador (comando)
  (interactive  (list (ido-completing-read "$: " (crea-lista-programas) nil t)))
  (start-process-shell-command comando nil comando))

Mostrar los títulos de las ventanas externas

Ver el título de cada aplicación externa en la lista de búferes abiertos, y no sólo su nombre genérico (como «firefox», «gimp», «libreoffice», etc.), era algo ciertamente deseable. Por suerte se puede conseguir si añadimos el siguiente código (tomado de aquí) a nuestro archivo de configuración:

(defun exwm-rename-buffer ()
  (interactive)
  (exwm-workspace-rename-buffer
   (concat exwm-class-name ":"
           (if (<= (length exwm-title) 50) exwm-title
             (concat (substring exwm-title 0 49) "...")))))

(add-hook 'exwm-update-class-hook 'exwm-rename-buffer)

(add-hook 'exwm-update-title-hook 'exwm-rename-buffer)

Si configuramos el firefox (por ejemplo) de manera que abra las nuevas pestañas como ventanas, podemos ver los títulos de todas las pestañas abiertas desde nuestra lista de búferes. Muy cómodo y emacscéntrico.

Salir de la sesión de Lxde

Para poder salir de la sesión de Lxde de forma segura (en términos de Emacs, quiero decir) incluyo esta función que aconseja la wiki de Arch. Si bien, ojo, hay que requerir antes recentf para evitar un error:

(require 'recentf)

  (defun exwm-logout ()
    (interactive)
    (recentf-save-list)
    (save-some-buffers)
    (start-process-shell-command "logout" nil "lxsession-logout"))

Aunque no está de más guardarse un as en la manga en caso de que tengamos que salir de urgencia desde la consola de comandos. Para esa posible situación tengo definido este alias en mi ~/.bashrc

alias mata_sesion='sudo pkill -9 -u juanmanuel'

Apéndice II (actualización de 01/09/20)

¿Un «salvapantallas»?

Hace poco escribí esta suerte de «salvapantallas», que incluye un reloj analógico (proporcionado por el excelente paquete svg-clock e introduce los primeros versos de la Odisea imitando el mecanografiado humano).

;; Definimos el estilo del texto de la Odisea. Se puede modificar a conveniencia

(defface texto-baskerville
  '((t :family "GFS Baskerville" :height 220))
  "")

;; Los primeros versos de la Odisea. Se puede sustituir por cualquier otro texto

(setq odi-prim-versos "Ἄνδρα μοι ἔννεπε, Μοῦσα, πολύτροπον, ὃς μάλα πολλὰ
πλάγχθη, ἐπεὶ Τροίης ἱερὸν πτολίεθρον ἔπερσε·
πολλῶν δ' ἀνθρώπων ἴδεν ἄστεα καὶ νόον ἔγνω,
πολλὰ δ' ὅ γ' ἐν πόντῳ πάθεν ἄλγεα ὃν κατὰ θυμόν,
ἀρνύμενος ἥν τε ψυχὴν καὶ νόστον ἑταίρων.
ἀλλ' οὐδ' ὣς ἑτάρους ἐρρύσατο, ἱέμενός περ·
αὐτῶν γὰρ σφετέρῃσιν ἀτασθαλίῃσιν ὄλοντο,
νήπιοι, οἳ κατὰ βοῦς Ὑπερίονος Ἠελίοιο
ἤσθιον· αὐτὰρ ὁ τοῖσιν ἀφείλετο νόστιμον ἦμαρ.
τῶν ἁμόθεν γε, θεά, θύγατερ Διός, εἰπὲ καὶ ἡμῖν.")

;; Esta función es para imitar el mecanografiado

(defun imita-tecleo (texto)
  (mapc (lambda (caracter)
      ;; Aquí se puede cambiar la velocidad de `tecleo'
          (sit-for 0.08)
          (insert caracter)
          (when
              (looking-back "\\(.\\)" nil)
            (replace-match (propertize (match-string 1) 'face 'texto-baskerville))))
        texto))

;; La función que activa el salvapantallas.
;; OJO: para que se vea el reloj analógico es necesario tener instalado
;; el paquete `svg-clock'. Para quietar el salvapantallas: pulsar tecla `q'

(defun mi-salvapantallas-reloj-fecha ()
  (interactive)
  (when (get-buffer "*fecha y hora*")
    (kill-buffer "*fecha y hora*"))
  (switch-to-buffer (get-buffer-create "*fecha y hora*"))
      ;; Descomentar la siguiente línea para añadir transparencia
    ;; (set-frame-parameter (selected-frame) 'alpha '(75 . 40))
  (let ((inhibit-read-only t))
    (buffer-disable-undo)
    (erase-buffer)
    (svg-clock-insert 350)
    (insert
     (concat "\n\n\n       " (format-time-string "%A %e de %B de %Y") "\n\n\n"))
    (imita-tecleo odi-prim-versos)
    (view-mode)))

;; Lo siguiente es necesario para recuperar
;; la opacidad al salir del salvapantallas pulsando  `q'
(defun opacidad ()
    (set-frame-parameter (selected-frame) 'alpha '(100 . 100)))

  (advice-add 'View-quit :after #'opacidad)

Y un vídeo de muestra, con un tema oscuro y otro claro (Leuven):

Apéndice III (actualización de 22/03/22)

Hacia una configuración más simple, sin entorno de escritorio

Llevaba un tiempo largo usando EXWM sobre un entorno de escritorio, primero con LXDE y posteriormente con Mate. La experiencia ha sido buena y placentera, desde luego, pero últimamente me apetecía mudar a un arranque de EXWM «puro», sólo con lo configurado en el archivo ~/.xinitrc, tal y como comentábamos más arriba. Por supuesto, en nuestro propio ~/.xinitrc es necesario cargar algunas aplicaciones externas necesarias, como el icono de NetworkManager para la red, el compositor Picom y otras más que queremos arrancar en el inicio de sesión. También he decidido iniciar Emacs como servidor, y cargar EXWM como cliente. En resumidas cuentas, éste sería mi archivo ~/.xinitrc:

xhost +SI:localuser:$USER

export _JAVA_AWT_WM_NONREPARENTING=1

xsetroot -cursor_name left_ptr

# Aplicaciones externas al inicio

/home/juanmanuel/Scripts/./teclado.sh &
/home/juanmanuel/Scripts/./nextcloudcmd.sh &
redshift &
picom &
dunst &
nm-applet &
xfce4-power-manager &
xset r rate 250 30 &
nitrogen --restore &

# iniciamos Emacs

emacs --daemon -f exwm-enable

exec emacsclient -c

Nótese que cargo también un par de scripts propios, uno para sincronizar el cliente de escritorio de Nextcloud (lo hago siempre en la versión línea de comandos, pues el cliente gráfico me resulta intrusivo), y otro para ciertas personalizaciones y remapeos de teclado. También cargo el gestor de fondos de pantalla Nitrogen (de vez en cuando me gusta usar Emacs con una transparencia más o menos translúcida, una frivolidad), el gestor de energía de Xfce, el gestor de notificaciones Dunst y una configuración del comando xset para la ratio de velocidad/repetición del teclado. Después de arrancar todo eso, iniciamos Emacs como servidor y ejecutamos emacsclient. Y una nota importante: antes lo ejecutaba mediante el comando dbus-launch con la opción --exit-with-session, pero en Arch me daba unos problemas horribles de colapso de sesión. Ignoro a qué se debe.

Y una vez editado y guardado en nuestra carpeta HOME el archivo ~/.xinitrc, ya sólo nos queda arrancar desde una tty con el comando startx. Pero si queremos seguir logueándonos con nuestro display manager, podemos hacer un script ejecutable con el contenido de nuestro ~/.xinitrc y guardarlo en la carpeta de binarios /usr/bin/. Posteriormente, añadiríamos un archivo *EXWM.desktop a /usr/share/xsessions, algo así como esto (la línea donde añadir nuestro script ejecutable sería ésta)4.

[Desktop Entry]
Name=EXWM
Comment=Executes the .xinitrc script in your home directory
Exec=<mi-script-ejecutable>
TryExec=xinitrcsession-helper
Type=Application

Apéndice IV (actualización de 09/08/22)

Teclas simuladoras

Merecían algo de atención aquí las teclas simuladoras de EXWM, es decir, un tipo especial de atajo que «simula» los atajos típicos de Emacs en la aplicación hospedada. Estoy bastante satisfecho de la siguiente configuración de teclas simuladoras que tengo en mi archivo de inicio (nótese que en mi Emacs tengo configuradas las secuencias M-j y M-SPC como «retroceso» y «control + retroceso», respectivamente. En las teclas simuladoras esos atajos están en las líneas 10 y 11):

 1: (setq exwm-input-simulation-keys
 2:       '(([?\C-b] . [left])
 3:         ([?\C-f] . [right])
 4:         ([?\C-p] . [up])
 5:         ([?\C-n] . [down])
 6:         ([?\C-a] . [home])
 7:         ([?\C-e] . [end])
 8:         ([?\M-v] . [prior])
 9:         ([?\C-v] . [next])
10:         ([?\M-j] . [backspace])
11:         ([?\M- ] . [C-backspace])
12:         ([?\C-m] . [return])
13:         ([?\C-d] . [delete])
14:         ([?\C-y] . [?\C-v]) ;pegar
15:         ([?\C-w] . [?\C-x]) ;cortar
16:         ([?\M-w] . [?\C-c]) ;copiar
17:         ([?\C-k] . [S-end delete])))

Publicado: 15/05/20

Última actualización: 09/08/22


Índice general

Acerca de...

Esta obra está bajo una licencia de Creative Commons Reconocimiento-NoComercial 4.0 Internacional.

Notas al pie de página:

1

Si tuviera que recomendar un gestor de estilo mosaico para uso general, sería Bspwm sin dudarlo, que me resulta mucho más usable, ligero y versátil que i3.

2

EXWM trae de fábrica también unos cuantos atajos para emular ciertas acciones de las aplicaciones externas. Con C-a, por ejemplo, tenemos la acción de la tecla «inicio», con C-e, la de la tecla «final».

3

Si no queremos contar con ningún panel ni nada parecido, y así dejar más espacio vertical en pantalla, podemos al menos tener la bandeja de sistema minimalista que EXWM provee de fábrica. Basta con evaluar al principio: (require 'exwm-systemtray) (exwm-systemtray-enable)

4

Tenemos también en el AUR de Arch Linux el paquete xinit-xsessio, que viene a hacer lo mismo.

© Juan Manuel Macías
Creado con esmero en
GNU Emacs