Cuaderno de GNUtas

Más ardides de la Odisea: de «hipérboles» y versos

El paquete para Emacs Gnu Hyperbole es una biblioteca peculiar y bastante idiosincrásica. De cuando en vez surge alguna nueva versión de la misma, y el acontecimiento siempre acaba suscitando opiniones encendidas y encontradas entorno a su presunta complejidad o a si tiene o no tiene elementos o intereses comunes con Org Mode. En las discusiones habituales nunca me acababa de quedar claro a qué se dedica, en realidad, Hyperbole, y si aquello que hace —sea lo que sea— me sería útil. Pero como era tanta la polvareda que levantaba con cada reaparición, no pude resistirme a la curiosidad de instalarlo y darle una oportunidad. Lo cierto es que, tras unos cuantos trasteos, llegué a la conclusión de que este paquete tenía partes que, en efecto, me iban a resultar tremendamente prácticas, especialmente en mi traducción de la Odisea (work in progress). De eso, precisamente, tratará esta GNUta, y sirva también como pequeña introducción de Hyperbole, para que el posible y amable lector pueda extraer algunos ejemplos y aplicarlos a su uso personal. Insistiendo, desde luego, en lo de «pequeña introducción», ya que Hyperbole es casi tan variopinto y extenso como el modo del unicornio.

Es esa cualidad «multiforme», de hecho, la que está detrás de buena parte de los malentendidos o despistes en torno a Hyperbole. La diferencia esencial con el también multiforme Org estriba en que éste ha sabido ramificarse a partir de una suerte de lenguaje simple, limitado y articulado. Haga lo que haga Org y todas las bibliotecas que van surgiendo a su sombra, no es difícil para el usuario establecer conversaciones complejas después de aprender los primeros rudimentos. Con Hyperbole eso no es tan obvio o, al menos, no queda en la superficie. Pero, pasada esa primera barrera, nos damos cuenta de que el laberinto no es tan enrevesado como lo pintan. Simplemente, es un laberinto que no ha sabido venderse tan bien como Org. Por tanto, hay que entender Hyperbole como un conjunto de herramientas (variadas, eso sí, y con aparente poca relación entre ellas y su campo de acción) para la gestión de información y la navegación de dicha información (datos, textos, archivos) a través de GNU Emacs. Eso es, en cierto sentido, Org. La buena noticia es que ambos modos no tienen por qué ser incompatibles.

Hyperbole puede convertirse, pues, en un estupendo y utilísimo complemento para Org Mode, especialmente en una de las partes del primero que más me ha interesado: su sistema de enlaces implícitos. Algo que Org tiene ya ensayado tímidamente (y sólo a nivel de documento) en sus Radio Targets, pero que Hyperbole ha explotado de una forma admirable e increíblemente productiva.

Pero, ¿Qué entiende Hyperbole por un enlace o botón «implícito»? Pues, para decirlo en pocas palabras: cuando una parte del texto de nuestro documento o búfer cumple con una serie de contextos, se ejecutará una acción determinada si pulsamos sobre ese segmento de texto una suerte de secuencia activadora universal, lo que Hyperbole llama la «action key». Por supuesto, podemos asignar a esa «action key» el atajo que se nos antoje y, como ya decimos, sirve para activar cualquier tipo de botón implícito. El paquete ya viene con un nutrido repertorio de botones implícitos de fábrica, de tal forma que si activamos la «action key» sobre algo que nos parezca sospechoso de tener cualidades de enlace implícito, casi seguro acertaremos. Un ejemplo muy útil: pulsada esa «action key» sobre cualquier símbolo de una expresión Elisp, saltaremos automáticamente a la definición o declaración de ese símbolo (función, variable, lo que sea) en su correspondiente archivo fuente.

También podemos definir —y aquí está gran parte de la gracia— nuestros propios enlaces implícitos de una forma bastante sencilla y transparente. Yo aproveché para definir alguno que otro a fin de facilitar mi trabajo en la Odisea. Pero antes de detallar uno en concreto, describiré brevemente cómo tengo configurado el paquete Hyperbole en mi archivo de inicio.

Mi configuración del paquete Hyperbole

No es necesario realmente si lo instalamos todo desde GNU Elpa, pero por si acaso, nunca viene de más requerir la biblioteca:

(require 'hyperbole)

Y cargamos el modo en el arranque:

(hyperbole-mode 1)

Con esto ya tendríamos suficiente para trabajar con Hyperbole sin contratiempos. Sin embargo, y ya que sólo existe una acción universal (una «action key», vaya), me hizo gracia la posibilidad de poder contar con una acción «secundaria» que ejecutase otra cosa en el mismo contexto. Estudiando un poco el código de Hyperbole encontré que no era muy difícil lograrlo. Bastaría, primero, con declarar una variable:

(defvar mi-hyp-alt-act nil)

y definir esta función, a la cual le asignaremos el atajo pertinente a nuestro gusto:

(defun mi-hyp-action-key-alt ()
  "Acción alternativa para `action-key'"
  (interactive)
  (let ((mi-hyp-alt-act t))
    (action-key)))

Un ejemplo de enlace implícito de Hyperbole en mi traducción de la Odisea

Funciones previas como acción para el enlaces

Mucho antes de trastear con Hyperbole yo ya tenía definidas un par de funciones que me permitían saltar del verso traducido al verso original (y viceversa). El verso de destino se resalta temporalmente con la siguiente función:

(defun mi-resaltado-temporal-linea ()
  (hl-line-mode)
  (run-with-timer 1 nil (lambda () (hl-line-mode -1))))

Esta sería la función que hace el camino griego-español. Téngase en cuenta que:

  • Cada canto en griego es un único archivo de Org independiente, nombrado siempre con una misma estructura;
  • La traducción de todos los cantos está en un solo documento de Org, llamada Odisea.org;
  • Para la función numero-verso-actual, véase esta otra GNUta, aquí.
(defun verso-odisea-gr-a-es ()
  "Salta al mismo número de verso en que está el cursor desde el
canto original a su traducción"
  (interactive)
  (let*
      ((num-verso (numero-verso-actual))
       (buf (buffer-name))
       (num-canto (string-to-number
                   (when
                       (string-match "\\([0-9]+\\)" buf)
                     (match-string 1 buf)))))
    (find-file-noselect "~/Git/Traduccion_Odisea/CANTOS/Odisea.org")
    (pop-to-buffer "Odisea.org")
    (org-overview)
    (goto-char (point-min))
    (re-search-forward "\\* .+ Odisea" nil t)
    (org-show-children)
    (org-next-visible-heading num-canto)
    (org-show-subtree)
    (re-search-forward "#\\+begin_verse\\|#\\+BEGIN_VERSE")
    (ir-a-verso-actual num-verso)
    (recenter 6)
    (mi-resaltado-temporal-linea)
    (message (format "Verso %s del canto %s" (number-to-string num-verso) (number-to-string num-canto)))))

Aquí, la función que hace el camino inverso. El número de canto lo extraigo en este caso de una propiedad ad hoc.

(defun verso-odisea-es-a-gr ()
  "Salta al mismo número de verso en que está el cursor desde el
canto original a su traducción (versión español a griego)"
  (interactive)
  (let*
      ((num-verso (numero-verso-actual))
       (canto (org-entry-get nil "Canto"))
       (buf-format (concat "~/Git/Traduccion_Odisea/CANTOS/canto" canto "gr.org")))
    (find-file-noselect buf-format)
    (pop-to-buffer (concat "canto" canto "gr.org"))
    (org-overview)
    (goto-char (point-min))
    (org-show-subtree)
    (re-search-forward "#\\+begin_verse\\|#\\+BEGIN_VERSE")
    (ir-a-verso-actual num-verso)
    (recenter 6)
    (mi-resaltado-temporal-linea)
    (message (format "Verso %s del canto %s" (number-to-string num-verso) (number-to-string num-canto)))))

Estas dos funciones cuentan con las dos siguientes variantes. La única diferencia aquí es que el verso se muestra en el minibúfer. Son para consulta más rápida, sin necesidad de saltar al archivo correspondiente. Es decir, que si tenemos el cursor en un verso determinado de la traducción, el minibúfer muestra el verso original. Y viceversa:

(defun verso-odisea-var-gr-a-es ()
  "Muestra en el minibúfer mismo número de verso en que está el
cursor desde el canto original a su traducción"
  (interactive)
  (let*
      ((num-verso (numero-verso-actual))
       (buf (buffer-name))
       (num-canto (string-to-number
                   (when
                       (string-match "\\([0-9]+\\)" buf)
                     (match-string 1 buf))))
       (linea (with-current-buffer
                  (find-file-noselect
                   "~/Git/Traduccion_Odisea/CANTOS/Odisea.org")
                (org-overview)
                (goto-char (point-min))
                (re-search-forward "\\* .+ Odisea" nil t)
                (org-show-children)
                (org-next-visible-heading num-canto)
                (org-show-subtree)
                (re-search-forward "#\\+begin_verse\\|#\\+BEGIN_VERSE")
                (ir-a-verso-actual num-verso)
                (donde-odisea)
                (buffer-substring (line-beginning-position)
                                  (line-end-position)))))
    (message linea)))

(defun verso-odisea-var-es-a-gr ()
  "Muestra en el minibúfer mismo número de verso en que está el
cursor desde el canto original a su traducción (versión español a
griego)"
  (interactive)
  (let*
      ((num-verso (numero-verso-actual))
       (canto (org-entry-get nil "Canto"))
       (buf-format (concat "~/Git/Traduccion_Odisea/CANTOS/canto" canto "gr.org"))
       (linea (with-current-buffer
                  (find-file-noselect buf-format)
                (org-overview)
                (goto-char (point-min))
                (org-show-subtree)
                (re-search-forward "#\\+begin_verse\\|#\\+BEGIN_VERSE")
                (ir-a-verso-actual num-verso)
                (buffer-substring (line-beginning-position)
                                  (line-end-position)))))
    (message linea)))

El enlace implícito de Hyperbole

Y aquí viene la parte que toca a Hyperbole. Hasta ahora, las funciones descritas en el anterior apartado las llamaba normalmente desde un atajo. Pero como lo acabé haciendo muy a menudo, y ya que el encuentro con Hyperbole estaba reciente, me pregunté por qué no convertir cada verso de la Odisea, original y traducción, en un enlace implícito que ejecutase esas funciones. Por supuesto, había que limitar un poco más el contexto, pero tampoco hacía falta ser demasiado puntilloso. Para que se ejecutase la acción, en suma, tendrían que cumplirse sólo estas dos condiciones:

  • Estar dentro del proyecto Git de mi traducción de la Odisea
  • Tener el cursor exactamente al principio de la línea.

No me hizo falta, realmente, afinar más en el contexto, pues yo ya sabía dónde iba a activar esos enlaces implícitos: justamente al principio de cada verso. Y esto me pareció un verdadero hallazgo de Hyperbole (y algo en concreto donde va más allá de Org): el enlace no está ahí, en el texto, esperándome (como sucede en Org), sino en la intención de quien lo va a activar. De ahí lo de «implícito».

Además, para aprovechar los dos juegos de funciones me vino aquí de perlas mi invento de la acción secundaria. De tal suerte que la acción «primaria» me mostraría el verso correspondiente en el minibúfer y la secundaria me llevaría al correspondiente documento.

La definición de nuevos enlaces implícitos en Hyperbole es bastante sencilla. Se realiza mediante la función defib, la cual tiene dos partes claramente separadas: la primera, para establecer los contextos y condicionales en que se debe ejecutar el nuevo enlace. La segunda, para la acción a ejutar una vez activado el enlace con la «action key». Ésta se aplica como argumento de la expresión hact (y, a su vez, podemos añadir los argumentos necesarios). Lo único a tener en cuenta en esta parte es que, si la acción se trata de una función, no puede pasar como símbolo sino con un apóstrofe o «quote», para evitar que se evalúe. Por supuesto, podemos también introducir una función anónima «lambda».

Mi enlace implícito, por tanto, quedaba tal que así (y se puede apreciar un ejemplo de uso en la fig. 1):

(defib od-versos ()
  "TODO"
  (when (and (equal (projectile-project-root)
                    (expand-file-name
                     "~/Git/Traduccion_Odisea/"))
             (eq (point) (line-beginning-position)))
    ;; las acciones primaria y secundaria a ejecutar
    (if (not mi-hyp-alt-act)
        (if (string-match-p "gr" (buffer-file-name))
            (hact 'verso-odisea-var-gr-a-es)
          (hact 'verso-odisea-var-es-a-gr))
      (if (string-match-p "gr" (buffer-file-name))
          (hact 'verso-odisea-gr-a-es)
        (hact 'verso-odisea-es-a-gr)))))

hyperbole-odisea.png

Figura 1: Traducción del verso en el minibúfer mediante un enlace implícito de Hyperbole

Publicado: 07/08/22

Última actualización: 16/08/23


Índice general

Acerca de...

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

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