La ventana que a Emacs le faltaba: flotantes de verdad en 40 líneas


21 de julio de 2026

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:

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íxeles

Con 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 estabas

La 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 = redimensionar

Para 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

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ú.

Comparte este post:

Es tu post

Estas seguro? Esto no se puede deshacer.

Comentarios (0)

Sin comentarios todavia. Se el primero!

Deja un comentario