Cuaderno de GNUtas

Navegación mejorada en Org Mode con org-speed-commands

Una funcionalidad muy potente en Org Mode, a la par que poco conocida a no ser que escarbemos con cierto celo en el Manual, es la de los comandos rápidos; o Speed Commands, como se los conoce en el original. Por defecto vienen desactivados, pero podemos activarlos con sólo aplicarle un valor non-nil a la variable org-use-speed-commands. ¿Y qué vienen a hacer estos comandos rápidos? Pues en su versión de fábrica, que ya de por sí es muy completa y podrá satisfacer al común de los usuarios, nos permite lanzar una serie de comandos relacionados, en principio, con los encabezados (aunque veremos también que pueden extenderse a más escenarios), pulsando una única tecla. Pero, y aquí reside lo esencial, siempre y cuando nos encontremos en un determinado contexto. Por defecto, tal contexto se describe cuando el cursor está justo al principio de la línea del encabezado sobre el que queramos ejecutar el comando de turno. O sea, se trata de una suerte de edición modal1 en pequeñas dosis y controlada.

Estos comandos están almacenados en la lista org-speed-commands, que es extensa ya de fábrica. La forma más rápida de saber qué comandos están incluidos allí y qué letra tienen asignada es pulsando «?» en el contexto de activación, o sea, a comienzo de línea del encabezado. Por supuesto, el uso habitual irá discriminando, y siempre podremos también modificar la lista a nuestro gusto, añadiendo y quitando comandos. Algunos son palmariamente útiles. Por ejemplo: si estamos en el contexto de activación (recuérdese, insisto, a comienzo de línea en un encabezado cualquiera) y pulsamos la letra «p», saltamos al encabezado anterior; con «n», vamos al siguiente encabezado; con «b», al encabezado anterior del mismo nivel; con «f», lo mismo pero al encabezado siguiente. Más ejemplos: «r» (de «right») nos baja un nivel el encabezado siguiente (o sea, va añadiendo más asteriscos); «l» (de «left»), lo mismo pero subiendo un nivel (quita asteriscos); las versiones de los dos anteriores comandos que bajan o suben de nivel el encabezado presente junto a todo el subárbol (consecuentemente), son las letras mayúsculas «R» y «L». Y algunos ejemplos más: «=» nos lanza la vista de columnas; «c» pliega o despliega el sub-árbol (lo equivalente al tabulador; para circular entre todas las vistas, que sería lo equivalente a mayúsculas más tabulador, bastaría con pulsar «C» mayúscula); «:» inserta etiquetas; «#» comenta el encabezado (o sea, añade la coletilla COMMENT al título). Y un largo etcétera.

Personalizando y extendiendo los comandos rápidos para los encabezados

Como vemos, se trata de una funcionalidad que puede resultar muy útil para la navegación a través de un documento de Org. Yo la venía usando bastante, pero últimamente se me ocurrió que no estaría de más un poco de personalización y ciertas modificaciones. Partí de la siguiente premisa: yo, que no soy especialmente devoto de la edición modal, podría admitir subir la dosis (pero manteniéndome dentro del control para no desmelenarlo todo), una vez asumido el hecho de que los encabezados es algo a que dedico poco tiempo de edición, después de que establezco el título. A decir verdad, paso más tiempo editando el «meta-contenido» de los encabezados que el contenido propiamente dicho: estados TODO, propiedades, etiquetas, posicionamiento, etc. Podríamos entender que el contenido en bruto del título es un metadato más, y que se podría editar llegado el caso como tal metadato. Así pues, con semejante premisa en mente, mi extensión de los comandos rápidos consistiría en estos tres puntos:

  1. Extender el contexto de activación a todo el encabezado, y no sólo al comienzo de la línea. Una vez que lo logramos, la velocidad de navegación se incrementa exponencialmente. Y tal vez, añadido a esto, sería buena idea extender el contexto de activación al comienzo del documento de Org (o sea, la posición «1» del búfer), que es donde nos aparece el cursor cada vez que visitamos un documento.
  2. Por defecto, cuando por descuido u olvido pulsamos una letra que no está asociada a un comando rápido, esta letra, simplemente, se imprime. Lo cual resulta muy incómodo y no parece consistente. A mi juicio, el comportamiento apropiado sería un modo dual más estricto: si la letra no está asociada a un comando, devolver un mensaje de error, pero nunca imprimir la letra. Necesitamos, por tanto, alternar en este modo dual entre el modo-comando y el modo-edición. Sí, lo asumo, estamos bordeando peligrosamente el infierno de la edición modal estilo Vim, pero repito que lo podemos seguir teniendo bajo control. O eso espero…
  3. Como consecuencia de lo anterior, claro, necesitaríamos una función que activara o desactivara los comandos rápidos a voluntad, una especie de «modo de edición», para poder escribir en el encabezado cuando lo necesitemos.

Pasemos a describir cómo he ido logrando estas tres metas.

Extendiendo el contexto de activación

Hemos dicho que queremos activar nuestros comandos rápidos en cualquier parte del encabezado y en la posición «1» del búfer. Lograrlo es sencillo y el procedimiento está bien explicado en el doc string de org-use-speed-commands: bastaría con dar a esta variable una función anónima que incluyese un condicional, a fin de delimitar el contexto. Es decir, que devolviera valor non-nil si tenemos el puntero en el contexto deseado. Mi nueva configuración de la variable quedó, entonces, tal que así:

(setq org-use-speed-commands
      (lambda () (or (eq (point) 1)
                     (org-in-regexp "^\\*+\s+.+"))))

Activación «estricta» de los comandos

Ya hemos dicho que no queremos que se imprima la letra cuando pulsamos la tecla equivocada y no está asociada a ningún comando. Esto es, queremos el contexto de activación vedado a cualquier tipo de edición manual, a no ser que desactivemos momentáneamente la funcionalidad, como veremos más adelante. Para lograr este modo estricto tendremos que modificar ligeramente el final de la función org-speed-commands-activate. Escribimos nuestra versión modificada y la superponemos a la de fábrica mediante un advice-add con la palabra clave :override:

(defun mi-org-speed-command-activate (keys)
  "Mi versión modificada de `org-speed-command-activate' que
devuelve un mensaje de error si pulso una tecla no asociada a un
comando rápido"
  (when (or (and (bolp) (looking-at org-outline-regexp))
            (and (functionp org-use-speed-commands)
                 (funcall org-use-speed-commands)))
    (let ((defined (cdr (assoc keys
                               (if (boundp 'org-speed-commands-user)
                                   (append org-speed-commands
                                           org-speed-commands-user)
                                 org-speed-commands)))))
      (if defined defined
        (error "Comando rápido no definido: \"h\" para ayuda")))))

(advice-add 'org-speed-command-activate :override #'mi-org-speed-command-activate)

Permitir activar y desactivar rápidamente los comandos rápidos

Las modificaciones anteriores nos han dejado, como hemos anticipado, en una suerte de modo «dual». Por tanto, necesitamos una función que nos alterne rápidamente entre modo de edición y modo de comandos. Y —pequeña floritura— para que nos recuerde cuando estamos en el modo de edición, se encerrará el contenido del encabezado en una caja (como podemos observar en la fig. 1):

(defun mi-org-alterna-speed-commands ()
  (interactive)
  (if org-use-speed-commands
      (progn (setq org-use-speed-commands nil)
             (with-eval-after-load 'org
               (set-face-attribute 'org-level-1 nil :box t)
               (set-face-attribute 'org-level-2 nil :box t)
               (set-face-attribute 'org-level-3 nil :box t)
               (set-face-attribute 'org-level-4 nil :box t)
               (set-face-attribute 'org-level-5 nil :box t)
               (set-face-attribute 'org-level-6 nil :box t)
               (set-face-attribute 'org-level-7 nil :box t)
               (set-face-attribute 'org-level-8 nil :box t))
             (message "speed-commands desactivado"))
    (setq org-use-speed-commands
          (lambda () (or (eq (point) 1)
                         (org-in-regexp "^\\*+\s+.+"))))
    (with-eval-after-load 'org
      (set-face-attribute 'org-level-1 nil :box nil)
      (set-face-attribute 'org-level-2 nil :box nil)
      (set-face-attribute 'org-level-3 nil :box nil)
      (set-face-attribute 'org-level-4 nil :box nil)
      (set-face-attribute 'org-level-5 nil :box nil)
      (set-face-attribute 'org-level-6 nil :box nil)
      (set-face-attribute 'org-level-7 nil :box nil)
      (set-face-attribute 'org-level-8 nil :box nil))
    (message "speed-commands activado")))

Y, para acceder rápidamente a esta función, le asigno un atajo de teclado cómodo:

(with-eval-after-load 'org
   (define-key org-mode-map (kbd "M-i") 'mi-org-alterna-speed-commands))

speed-commands.png

Figura 1: Encabezados en «modo de edición»

Algunas mejoras más

Una vez añadidas estas modificaciones, podemos pensar en ciertas funciones personalizadas para incorporar a la lista org-speed-commands, así como en eliminar de esa variable las que no usemos demasiado. Naturalmente, eso ya depende de las preferencias y tipo de uso de cada cual. Aquí describiré algunas de las funciones que yo he incluido.

La primera que me vino a la mente, y ya que nuestro contexto de activación ahora está también en el comienzo del búfer (posición «1»), es la de saltar desde cualquier encabezado a esa posición:

("q" . beginning-of-buffer)

Continuando con mis funciones añadidas, aquí un par de funciones que insertan un nuevo encabezado justo a continuación del encabezado actual, preguntando por el contenido en el minibúfer. La segunda versión inserta un encabezado con valor TODO:

(defun mi-org-insert-heading ()
  (interactive)
  (let ((tit (read-from-minibuffer "título: ")))
    (call-interactively 'org-insert-heading-respect-content)
    (insert tit)))
(defun mi-org-insert-todo-heading ()
  (interactive)
  (let ((tit (read-from-minibuffer "título: ")))
    (call-interactively 'org-insert-todo-heading-respect-content)
    (insert tit)))

Y sus correspondientes teclas asignadas:

("i" . mi-org-insert-heading)
("y" . mi-org-insert-todo-heading)

Y ya, para terminar, porque no queremos ser demasiado prolijos con la lista, creo que en la variable de fábrica se echaban en falta dos acciones muy habituales (al menos en mi uso cotidiano), como son la de añadir y editar propiedades y la de abrir el menú de org-attach:

("P" . org-set-property)
("A" . org-attach)

Y con eso ya tendríamos nuestras mejoras incorporadas. Pero, ya que nos hemos venido arriba, descubrimos que el ámbito de los encabezados no es el único donde pueden operar los comandos rápidos, como veremos en la sección siguiente.

Comandos rápidos para los enlaces

La función org-speed-command-activate (que vimos más arriba) no es la única que se encarga de activar comandos rápidos. De hecho, ésta está incluida en el hook org-speed-command-hook, que agrupa todas las funciones destinadas a lanzar este tipo de comandos. Podemos escribir las nuestras propias y añadirlas allí, y a mí se me ocurrió la idea de escribir una específica para los enlaces de Org. ¿Qué acciones añadir a esta función de manera que se activen cuando tenemos el cursor dentro de un enlace? Las dos más obvias son la de editar el enlace y la de activarlo. Además yó ya tenía una hydra definida hace tiempo para agrupar varias funciones que previamente había ido escribiendo y que iban destinadas a ejecutarlas sobre un link: copiar el archivo-ruta de un link a otro directorio, renombrarlo, abrirlo con un programa externo (Gimp por defecto), adjuntarlo a un correo electrónico, eliminar el link, etc. Pero eso ya será objeto de otra GNUta. Aquí lo que interesa es definir una nueva lista de acciones para nuestros comandos rápidos en los enlaces, que serán sólo tres: activar el enlace, editarlo y activar mi hydra:

(setq mi-org-link-speed-commands
      '(("o" . org-open-at-point)
        ("e" . org-insert-link)
        ("v" . hydra-link-org/body)))

Y ya sólo nos quedaría la función para lanzarlos con el contexto adecuado. Pero, ojo, los enlaces no sólo pueden estar en el texto, también en un encabezado. Yo tengo cientos de encabezados que son enlaces. Así que nuestro condicional para activar los comandos rápidos en los enlaces debe dejar claro que no queremos que se activen cuando estamos en un encabezado, pues de lo contrario entraría en conflicto con la función para los comandos rápidos en los encabezados, que vimos en la sección anterior. Si queremos que los enlaces de los encabezados también se beneficien de los comandos rápidos, no nos queda otra que añadir estas tres acciones de nuestra nueva lista también a la lista org-speed-commands, pero teniendo cuidado, naturalmente, de no duplicar letras de activación. Lo ideal es que sean siempre las mismas letras («o», «e», «v»), de modo que tendremos que reasignar nuevas letras a otros comandos ya definidos en esa lista pare evitar ambigüedades.

Hechas, en fin, estas aclaraciones, nuestra función activadora de los comandos rápidos en los enlaces de Org quedaría tal que así:

(defun mi-org-speed-command-link-activate (keys)
  "Mi función para activar los comandos rápidos en los links,
siempre que no sea un link en el encabezado"
  (when (and (org-in-regexp org-any-link-re 100)
             (not (org-in-regexp "^\\*+\s+.+")))
    (let ((defined (cdr (assoc keys mi-org-link-speed-commands))))
      (if defined defined
        (error "Speed command not defined: \"h\" for help")))))

Por supuesto, esta función también tiene un modo estricto, de forma que si pulsamos la tecla equivocada nos devolverá un mensaje de error. Tendremos que volver a echar mano de la función para cambiar entre modo de edición y modo de comandos. Una vez más, estamos acercándonos peligrosamente a la edición modal, pero, como sucede con los encabezados, la edición manual de los enlaces es muy raro hacerla.

Y nos quedaría, por último, asignar nuestra nueva función al hook org-speed-command-hook:

(add-hook 'org-speed-command-hook #'mi-org-speed-command-link-activate)

Publicado: 07/05/22

Última actualización: 07/08/22


Índice general

Acerca de...

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

Notas:

1

O sea, al estilo Vi/Vim, donde se cuenta con un modo de edición, otro de inserción de texto, otro de navegación, etc.

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