La ventana que a Emacs le faltaba: flotantes de verdad en 40 líneas
Hay una sensación muy concreta cuando llevas poco con Emacs: la de que algo falta. No sabes muy bien qué, pero lo notas. Vienes de un editor moderno donde arrastras un panel a una esquina y lo dejas ahí flotando —la salida de un test, un log, una consola— mientras sigues currando debajo. Llegas a Emacs, lo buscas, y… no está. No hay "ventana flotante". Y piensas: ¿en serio esto no lo lleva?
Pues no. Y la respuesta a por qué es la mitad de bonita de esta historia. La otra mitad es que lo montamos nosotros, en una tarde, en 40 líneas. Vamos.
Por qué Emacs no tiene ventanas flotantes
Emacs nació en los 70 con un modelo mental que se llama tiling: mosaico. Las "ventanas" de Emacs (que no son las del sistema operativo, ojo, luego volvemos a esto) se reparten la pantalla sin solaparse jamás. Partes en dos, en cuatro, en seis, pero nunca hay una encima de otra. Durante cuarenta años, la idea misma de "algo flotando por encima" era ajena a su filosofía.
Eso cambió hace poco —en términos de Emacs, "poco" son unos años—. En Emacs 26 (2018) llegaron los child frames: marcos hijos que SÍ pueden flotar encima del resto. Pero no los metieron para que tú y yo flotáramos cosas a mano. Los metieron como fontanería para los popups: esos cuadritos de autocompletado que aparecen mientras escribes, el menú de ayuda de atajos, y poco más. Aparecen, los usas, desaparecen.
Nadie ató los cabos de "child frame + moverlo + redimensionarlo + una tecla para ponerlo y quitarlo" en algo de usar y tirar. La pieza estaba ahí, suelta. Faltaba alguien que la cogiera. Ese es justo el momento Emacs favorito: no esperas a que lo hagan, te lo construyes.
Primero, el vocabulario (que lía a todo el mundo)
Antes del código, tres palabras que en Emacs significan cosas MUY distintas de lo que crees:
- Frame (marco): esto SÍ es una ventana del sistema operativo, de las que tu gestor de ventanas mueve y coloca. Cuando abres Emacs, eso es un frame.
- Ventana (window): en Emacs es un panel DENTRO de un frame. Los splits.
- Child frame (marco hijo): un frame que vive DENTRO de otro frame, flotando encima. No es un panel (no se reparte el espacio) y no es una ventana del sistema (tu gestor de ventanas ni lo ve). Es el bicho que queremos.
Quédate con esto: un child frame flota dentro de Emacs, sin salir a tu gestor de ventanas. Es exactamente lo que buscábamos.
El corazón: crear el marco flotante
Todo se reduce a una función mágica: make-frame. Le pasas una lista de propiedades y
te devuelve un frame. La propiedad clave es parent-frame: si se la das, el frame nace
hijo de otro, y por tanto flota dentro de él.
(make-frame
`((parent-frame . ,(selected-frame)) ; <- esto lo hace HIJO (flota dentro)
(undecorated . t) ; sin barra de título del sistema
(minibuffer . nil) ; no necesita su propio minibuffer
(width . 92) (height . 26) ; tamaño inicial, en caracteres
(left . 700) (top . 80))) ; posición inicial, en píxelesCon eso solo ya tienes un recuadro flotando. Pero un recuadro vacío no sirve: hay que meterle el contenido que estás mirando. Eso son dos líneas más: seleccionas el frame nuevo y le dices que muestre el buffer actual.
(select-frame fr)
(switch-to-buffer buf) ; buf = el buffer donde estabasLa misma tecla que lo pone y lo quita (toggle)
Un botón que solo enciende es la mitad de un botón. Queremos que la misma tecla lo ponga y lo quite. El truco: guardar el frame en una variable. Si la variable apunta a un frame vivo, lo cerramos; si no, lo creamos.
(defvar my/float-frame nil "El marco flotante actual, o nil si no hay.")
(defun my/toggle-float-buffer ()
"Flota el buffer actual dentro de Emacs. Otra vez y lo cierra."
(interactive)
(if (and my/float-frame (frame-live-p my/float-frame))
;; Ya hay uno: cerrarlo.
(progn (delete-frame my/float-frame)
(setq my/float-frame nil))
;; No hay: crearlo con el buffer actual.
(let* ((buf (current-buffer))
(parent (selected-frame))
(fr (make-frame
`((parent-frame . ,parent)
(undecorated . t)
(minibuffer . nil)
(width . 92) (height . 26)
(left . 700) (top . 80)))))
(select-frame fr)
(switch-to-buffer buf)
(setq my/float-frame fr))))
(global-set-key (kbd "<f12>") #'my/toggle-float-buffer)frame-live-p comprueba que el frame
sigue vivo (no lo cerraste tú a mano). interactive es lo que convierte una función
normal en un comando que puedes lanzar con una tecla. Y
ya está: F12 flota, F12 cierra.
El detalle que lo hace usable: moverlo y cambiarlo de tamaño
Aquí viene la verdad incómoda que descubrimos probando: un child frame no se arrastra con el ratón como una ventana normal. ¿Por qué? Porque mover y redimensionar ventanas es trabajo del gestor de ventanas, y el child frame se lo salta a propósito (por eso vive dentro de Emacs). No puedes tener las dos cosas.
Así que hay que dárselo nosotros. Para redimensionar con el ratón hay un truco built-in precioso: un borde gordo y "agarrable".
;; en la lista de make-frame:
(internal-border-width . 8) ; borde de 8px, bien gordo para agarrarlo
(drag-internal-border . t) ; arrastrar ese borde = redimensionarPara moverlo, como el ratón no vale, tiramos de
teclado. Dos funciones que tocan la posición (left=/=top) y el tamaño (frame-width=/=frame-height) del frame:
(defun my/float-move (dx dy)
(when (frame-live-p my/float-frame)
(set-frame-parameter my/float-frame 'left
(+ (frame-parameter my/float-frame 'left) dx))
(set-frame-parameter my/float-frame 'top
(+ (frame-parameter my/float-frame 'top) dy))))
(defun my/float-resize (dw dh)
(when (frame-live-p my/float-frame)
(set-frame-width my/float-frame (max 24 (+ (frame-width my/float-frame) dw)))
(set-frame-height my/float-frame (max 6 (+ (frame-height my/float-frame) dh)))))La lección que costó dos intentos: no pises los atajos de nadie
El primer diseño ató "mover" a Alt+Shift+flechas. Craso error: esa combinación
YA la usa Emacs para saltar entre paneles. Al probarla, en vez de mover
el flotante, cambiaba el foco. Un atajo global es como aparcar en doble
fila: siempre molestas a alguien.
La solución elegante es un keymap transitorio: un teclado que solo existe durante unos segundos, mientras "colocas" el flotante. Entras con una tecla, y mientras estás dentro las flechas normales mueven y con Shift redimensionan. En cuanto pulsas cualquier otra tecla, el teclado temporal desaparece y todo vuelve a la normalidad. Cero conflictos con nada.
(defun my/float-adjust ()
"Modo colocar: flechas mueven, Shift+flechas cambian tamaño, otra tecla sale."
(interactive)
(unless (and (boundp 'my/float-frame) (frame-live-p my/float-frame))
(user-error "No hay flotante (pulsa F12 primero)"))
(let ((map (make-sparse-keymap)))
(define-key map (kbd "<left>") (lambda () (interactive) (my/float-move -60 0)))
(define-key map (kbd "<right>") (lambda () (interactive) (my/float-move 60 0)))
(define-key map (kbd "<up>") (lambda () (interactive) (my/float-move 0 -60)))
(define-key map (kbd "<down>") (lambda () (interactive) (my/float-move 0 60)))
(define-key map (kbd "S-<left>") (lambda () (interactive) (my/float-resize -6 0)))
(define-key map (kbd "S-<right>") (lambda () (interactive) (my/float-resize 6 0)))
(define-key map (kbd "S-<up>") (lambda () (interactive) (my/float-resize 0 -3)))
(define-key map (kbd "S-<down>") (lambda () (interactive) (my/float-resize 0 3)))
(message "COLOCAR: flechas=mover · Shift+flechas=tamaño · otra tecla=salir")
;; El corazón del truco: un keymap que vive solo hasta que pulsas algo fuera.
(set-transient-map map t)))
(global-set-key (kbd "M-<f12>") #'my/float-adjust)set-transient-map es la joya: instala
ese teclado "por encima" de todo, y el segundo argumento (t) le dice "sigue vivo mientras se pulsen teclas
tuyas; en cuanto pulsen otra cosa, muérete". Perfecto para un modo
puntual como este.
Cómo se usa, en tres teclas
F12→ flotar el buffer actual (o cerrarlo si ya flota).M-F12(Alt+F12) → entrar en modo colocar. Luego: flechas mueven, Shift+flechas cambian tamaño, cualquier otra tecla sale.- Con el ratón, arrastrar el borde grueso → redimensionar.
Y ya está. Abres el log o el test, F12, lo dejas flotando en una esquina, y sigues picando código debajo mientras lo vigilas de reojo. Justo lo que "faltaba".
La honestidad final
Esto es lo máximo que da un child frame nativo, sin instalar nada. Es más tosco que arrastrar una ventana de verdad, porque —insisto— quien mueve ventanas de verdad bien es tu gestor de ventanas, y el child frame se lo salta.
Si algún día quieres fluidez total, la alternativa honesta es la
contraria: abrir un frame de verdad (C-x 5 2) y decirle a tu gestor de ventanas que
ese frame concreto flote siempre. Ahí lo mueves y redimensionas con tus
atajos de siempre, natural del todo. El precio: es una ventana del
sistema aparte, aunque muestre tu mismo Emacs. Dos caminos, tú eliges
según lo que pese más: "dentro de Emacs" o "fluido como el gestor".
La moraleja
Empecé este post con la sensación de que a Emacs "le faltaba" algo. Y sí, le faltaba. Pero esa frase, en Emacs, no es una queja: es una invitación. En cualquier otro editor, "le falta X" significa esperar a que alguien lo implemente, o cambiar de editor. En Emacs significa abrir tu config y escribir cuarenta líneas.
No es que Emacs tenga todas las features. Es que Emacs es la máquina de fabricar features. Lo que hoy le faltaba, mañana es tuyo —commiteado, tuyo para siempre—. Y ese, después de la curva de aprendizaje, es el momento en que Emacs deja de ser difícil y empieza a ser tu casa.
Me quito el sombrero, no ante el código, sino ante la idea: el editor que te deja terminarlo tú.
Comentarios (0)
Sin comentarios todavia. Se el primero!
Deja un comentario