dev: Add 'notes_emacs_elpaca' (preliminary release)

main
Sergio Alvariño 18 hours ago
parent 855d42a6d8
commit 4e0831dd40

@ -0,0 +1,393 @@
---
weight: 4
title: "Mi configuración de Emacs con Elpaca"
date: 2024-12-03T16:22:18+0100
draft: false
summary: "Configuración de Emacs con el gestor de paquetes Elpaca y otras notas sueltas"
categories:
- notes
tags:
- emacs
- elpaca
- use-package
---
{{< admonition type=danger title="ARTÍCULO SIN TERMINAR" open=true >}}
Este artículo **NO ESTÁ TERMINADO**, pero yo creo que ya es útil. En cuanto termine la migración de la configuración a Elpaca añadiré un enlace a la misma. Puedes mandarme sugerencias si te apetece.
{{< /admonition >}}
Vamos a ver como configurar Emacs (lo estoy probando en versiones 29, 30 y 31) con el gestor de paquetes Elpaca.
### Gestión de paquetes en Emacs
#### package.el
[_package.el_](https://www.emacswiki.org/emacs/InstallingPackages) es el gestor de paquetes por defecto en Emacs. Los paquetes de _package.el_ son ficheros comprimidos estructurados de manera que se puedan descomprimir en el directorio de paquetes de tu Emacs simplificando la instalación.
_package.el_ funciona perfectamente y es la alternativa adecuada si quieres una gestión de paquetes tan sencilla como sea posible. Sólo sería recomendable usar una alternativa si te preocupa alguno de los siguientes puntos:
- Quieres instalar paquetes para Emacs desde fuentes específicas, como desde el repo del desarrollador por ejemplo
- Quieres instalar versiones específicas de los paquetes
- Quieres desarrollar tus propios paquetes, o quieres contribuir al desarrollo de algún paquete o quieres simplemente trastear con el código de paquetes instalados
#### use-package
[_use-package_](https://github.com/jwiegley/use-package) **no es un gestor de paquetes**, es una macro que facilita muchísimo la especificación de paquetes a instalar desde los ficheros de configuración de Emacs. _use-package_ no se encarga de instalar los paquetes, sólo nos da una sintáxis muy potente y sencilla para declarar los paquetes a instalar y sus configuraciones. La instalación propiamente dicha siempre la hará el gestor de paquetes que utilices.
La mayoría (sino todos) los gestores de paquetes para Emacs son compatibles con _use-package_. De hecho este paquete es tan útil y popular que ya viene incluido como _built-in_ desde la versión 29 de Emacs.
Merece mucho la pena leerse con atención toda la documentación de _use-package_ en [esta web](https://jwiegley.github.io/use-package/keywords/) hay una explicación detallada de cada palabra clave.
Una característica curiosa (al menos para mi) de _use-package_ es que podemos invocarlo con una _feature_ de Emacs, y no solo con un paquete. Es bastante habitual invocarlo con la _feature_ `emacs`, veamos un ejemplo:
```elisp
(use-package emacs
:ensure nil
:init
(prefer-coding-system 'utf-8))
```
Esta notación, con la _feature_ `emacs`, se usa en los ficheros de configuración para especificar opciones comunes. Pero también se puede invocar con cualquiera de las _features_ disponibles (comprueba el valor de la variable `features` en tu Emacs), por ejemplo:
```elisp
(use-package paren
:straight (:type built-in) ;; Esta es notación de straight, podríamos haber usado :ensure nil
:config
(show-paren-mode t)
:custom
(show-paren-delay 0))
```
Tampoco es difícil crear una _feature_ al vuelo, aquí configuramos las fuentes de nuestro editor, creando la _feature_ `slv-fonts`:
```elisp
(use-package slv-font
:no-require
:hook (after-init . setup-fonts)
:preface
(defun font-installed-p (font-name)
"Check if a font with FONT-NAME is available."
(find-font (font-spec :name font-name)))
(defun setup-fonts ()
(cond ((font-installed-p "Hack Nerd Font")
(set-face-attribute 'default nil :font "Hack Nerd Font" :height 158))
((font-installed-p "Iosevka Nerd Font")
(set-face-attribute 'default nil :font "Iosevka Nerd Font" :height 158)))
(when (font-installed-p "Iosevka Nerd Font")
(set-face-attribute 'variable-pitch nil :font "Iosevka Nerd Font" :height 158)))
(provide 'slv-font))
```
Hay otras formas de conseguir lo mismo en el fichero de configuración, pero se hace así unas veces para disponer de todas las facilidades que nos da `use-package` (por ejemplo sintáxis simplificada para asociar atajos de teclado, _hooks_, etc.), y otras simplemente por mantener toda la configuración organizada en bloques de `use-package` con una sintáxis coherente en todo el fichero.
#### Alternativas a _package.el_ (_straight.el_)
Tenemos muchas alternativas al gestor de paquetes por defecto de Emacs. Hay una excelente comparativa de gestores de paquetes en la [documentación de `straight.el`](https://github.com/radian-software/straight.el?tab=readme-ov-file#comparison-to-other-package-managers)
Yo empecé usando _package.el_ (como todos los que usamos Emacs, supongo) y me pasé a _straight.el_ para tener más facilidad al especificar los orígenes de los paquetes y poder curiosear en el código de los mismos. Además facilita bastante reproducir la configuración en distintos ordenadores (que pueden tener distintas versiones de Emacs y de sistema operativo). La principal diferencia al pasar a trabajar con _straight.el_ es que los paquetes se instalan mediante "recetas" (puedes consultar la receta de un paquete con `straight-get-recipe`). Con _straight.el_ los paquetes se gestionan clonando el código fuente con git e instalando el paquete, desde donde especique la correspondiente receta. Es fácil escribir nuestras propias recetas en el mismo fichero de configuración si queremos descargar paquetes de repos especiales o desde nuestros propios repos.
La pega que le veo a _straight_ es que no da ninguna facilidad al usuario para explorar los paquetes disponibles. Yo incluso mantenía la configuración de _package_, pese a no usarlo, para poder visitar la lista de paquetes.
La razón inicial para probar _elpaca_ fue precisamente que facilita de varias maneras explorar los paquetes disponibles. Es mucho más interactivo que _straight_. Y si a eso añadimos que es significativamente más rápido, tiene todas las papeletas para ser el sucesor de _straight_ en el futuro.
### Elpaca
#### ¿Qué tiene de especial Elpaca?
Como comentaba _elpaca.el_ es el descendiente directo de _straight.el_ y varios desarrolladores del segundo forman parte del equipo de desarrollo de _elpaca.el_.
Elpaca tiene las siguientes características:
- Instalación **asíncrona** y en paralelo de los paquetes. Gracias a esto la instalación de paquetes es sorprendentemente rápida
- Al contrario que `straight.el` que no incluye herramientas interactivas para explorar el espacio de paquetes, Elpaca si tiene un `elpaca-manager` con muchas facilidades para estudiar los paquetes disponibles, probarlos, instalarlos, actualizarlos, etc. etc.
- Permite la descarga de paquetes desde distintos orígenes incluyendo los repos de desarrollo
- Incluye miles de paquetes "de serie" (MELPA, NonGNU/GNU ELPA, Org/org-contrib)
- Facilita a los usuarios la creación de sus propios ELPA (Emacs Lisp Package Archive)
Al haber desarrolladores comunes en `straight.el` y `elpaca`, hay ciertas similitudes entre ambos gestores de paquetes, así que pasar de _straight_ a _elpaca_ es incluso más fácil. Pero ojo, tampoco es un drama pasar de _package_ a _elpaca_ si ya estás usando _use-package_.
#### Instalación
Para instalar `elpaca.el` es imprescindible:
- Tener instalado un **Emacs** con versión 27.1 o posterior
- Tener instalado git
La instalación de `elpaca` de forma análoga a la de `straight` se hace mediante un código _bootstrap_ que añadimos a nuestro fichero `init.el`:
1. En primer lugar en el fichero `early-init.el` inhibimos la carga de `package.el` conectamos
```elisp
;; Disable package.el in favor of elpaca.el
(setq package-enable-at-startup nil)
```
2. Y añadimos a nuestro fichero `init.el` el código _bootstrap_ de `elpaca`, en el momento de escribir esto sería la versión 0.8. Este código es mejor que lo copies del [github de `elpaca`](https://github.com/progfolio/elpaca) para que esté actualizado:
```elisp
(defvar elpaca-installer-version 0.8)
(defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory))
(defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory))
(defvar elpaca-repos-directory (expand-file-name "repos/" elpaca-directory))
(defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git"
:ref nil :depth 1
:files (:defaults "elpaca-test.el" (:exclude "extensions"))
:build (:not elpaca--activate-package)))
(let* ((repo (expand-file-name "elpaca/" elpaca-repos-directory))
(build (expand-file-name "elpaca/" elpaca-builds-directory))
(order (cdr elpaca-order))
(default-directory repo))
(add-to-list 'load-path (if (file-exists-p build) build repo))
(unless (file-exists-p repo)
(make-directory repo t)
(when (< emacs-major-version 28) (require 'subr-x))
(condition-case-unless-debug err
(if-let* ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*"))
((zerop (apply #'call-process `("git" nil ,buffer t "clone"
,@(when-let* ((depth (plist-get order :depth)))
(list (format "--depth=%d" depth) "--no-single-branch"))
,(plist-get order :repo) ,repo))))
((zerop (call-process "git" nil buffer t "checkout"
(or (plist-get order :ref) "--"))))
(emacs (concat invocation-directory invocation-name))
((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch"
"--eval" "(byte-recompile-directory \".\" 0 'force)")))
((require 'elpaca))
((elpaca-generate-autoloads "elpaca" repo)))
(progn (message "%s" (buffer-string)) (kill-buffer buffer))
(error "%s" (with-current-buffer buffer (buffer-string))))
((error) (warn "%s" err) (delete-directory repo 'recursive))))
(unless (require 'elpaca-autoloads nil t)
(require 'elpaca)
(elpaca-generate-autoloads "elpaca" repo)
(load "./elpaca-autoloads")))
(add-hook 'after-init-hook #'elpaca-process-queues)
(elpaca `(,@elpaca-order))
```
#### Mi configuración personal (peculiaridades)
Aquí tengo que hacer un inciso para explicar como es mi configuración personal, que tiene algunas peculiaridades, algunas probablemente poco comunes:
- Mis ficheros de configuración están en el directorio `~/.config/emacs`, es decir ese directorio es mi `user-emacs-directory` Emacs reconoce ese directorio (especificado en el estandar POSIX) (aunque Emacs 29 en **Termux** no me lo ha reconocido, me vi obligado a definir un alias)
- Uso el paquete `no-littering` para mantener mi `user-emacs-directory` limpio y almacenar exclusivamente los ficheros de configuración. El resto de ficheros los dejo en el directorio `~/.cache/emacs`
- Casi toda mi configuración está guardada en un fichero de tipo `org-mode` que se carga desde `init.el`
Con esas condiciones mi fichero `init.el` tiene los siguientes contenidos:
```elisp
;;; init.el --- The Emacs init file for Emacs 29, ELPACA version -*- lexical-binding: t -*-
;;
;;; Commentary:
;; This is the main configuration file for Emacs.
;; In this file:
;; - Set no-littering etc and var locations
;; - Set elpaca as default install method for use-package
;; - Set eln-cache location for no-littering
;; - Set the bootstratp for elpaca.el
;; - Install the latest release of org
;; - Load no-littering
;; - Set elpaca for no-littering
;; - Add elpaca-use-package-mode
;; - Load the file ~/.config/emacs/slv-config.org
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Code:
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Set alternative locations for no-littering
(defvar no-littering-etc-directory (expand-file-name "~/.cache/emacs/etc"))
(defvar no-littering-var-directory (expand-file-name "~/.cache/emacs/var"))
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Uncomment next line for use-package statistics
;; (defvar use-package-compute-statistics t)
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Set eln-cache dir **WORKS ONLY FOR EMACS 29 AND ABOVE**
(when (boundp 'native-comp-eln-load-path)
(startup-redirect-eln-cache (expand-file-name "eln-cache" no-littering-var-directory)))
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Needed to avoid elpaca warning
(setq elpaca-core-date '(20240623)) ;; set to the build date of Emacs
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Define elpaca bootstrap
(defvar elpaca-installer-version 0.8)
;; (defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory))
(defvar elpaca-directory (expand-file-name "elpaca/" no-littering-var-directory))
(defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory))
(defvar elpaca-repos-directory (expand-file-name "repos/" elpaca-directory))
(defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git"
:ref nil :depth 1
:files (:defaults "elpaca-test.el" (:exclude "extensions"))
:build (:not elpaca--activate-package)))
(let* ((repo (expand-file-name "elpaca/" elpaca-repos-directory))
(build (expand-file-name "elpaca/" elpaca-builds-directory))
(order (cdr elpaca-order))
(default-directory repo))
(add-to-list 'load-path (if (file-exists-p build) build repo))
(unless (file-exists-p repo)
(make-directory repo t)
(when (< emacs-major-version 28) (require 'subr-x))
(condition-case-unless-debug err
(if-let ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*"))
((zerop (apply #'call-process `("git" nil ,buffer t "clone"
,@(when-let ((depth (plist-get order :depth)))
(list (format "--depth=%d" depth) "--no-single-branch"))
,(plist-get order :repo) ,repo))))
((zerop (call-process "git" nil buffer t "checkout"
(or (plist-get order :ref) "--"))))
(emacs (concat invocation-directory invocation-name))
((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch"
"--eval" "(byte-recompile-directory \".\" 0 'force)")))
((require 'elpaca))
((elpaca-generate-autoloads "elpaca" repo)))
(progn (message "%s" (buffer-string)) (kill-buffer buffer))
(error "%s" (with-current-buffer buffer (buffer-string))))
((error) (warn "%s" err) (delete-directory repo 'recursive))))
(unless (require 'elpaca-autoloads nil t)
(require 'elpaca)
(elpaca-generate-autoloads "elpaca" repo)
(load "./elpaca-autoloads")))
(add-hook 'after-init-hook #'elpaca-process-queues)
(elpaca `(,@elpaca-order))
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Install elpaca use-package support
(elpaca elpaca-use-package
;; Enable use-package :ensure support for Elpaca.
(elpaca-use-package-mode))
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Install org-mode
(elpaca org)
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Keep emacs clean!
;; Use no-littering to automatically set common paths to the new user-emacs-directory
(use-package no-littering
:ensure t
:init
;; set paths for no-littering etc and var directories
;; instead of this paths, you could use
;; (setq user-emacs-directory (expand-file-name "~/.cache/emacs"))
(setq no-littering-etc-directory (expand-file-name "~/.cache/emacs/etc")
no-littering-var-directory (expand-file-name "~/.cache/emacs/var")
)
:config
;; set sensible defaults for backups
(no-littering-theme-backups)
;; set paths for url-history-file and custom-file
(setq url-history-file (no-littering-expand-etc-file-name "url/history")
custom-file (no-littering-expand-etc-file-name "custom.el"))
)
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; delight and which-key aux packages needed for configuration loading
(use-package delight
:ensure t)
(use-package which-key
:ensure t
:delight
:config
(which-key-mode)
(setq which-key-idle-delay 0.3)
(setq which-key-dont-use-unicode nil)
(setq which-key-separator " → " )
(setq which-key-ellipsis "…")
(add-to-list 'which-key-replacement-alist '(("TAB" . nil) . ("↹" . nil)))
(add-to-list 'which-key-replacement-alist '(("RET" . nil) . ("⏎" . nil)))
(add-to-list 'which-key-replacement-alist '(("DEL" . nil) . ("⇤" . nil)))
(add-to-list 'which-key-replacement-alist '(("SPC" . nil) . ("␣" . nil)))
(add-to-list 'which-key-replacement-alist '(("<left>" . nil) . ("←")))
(add-to-list 'which-key-replacement-alist '(("<right>" . nil) . ("→"))))
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Wait to every queued elpaca order to finish
(elpaca-wait)
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Load slv-config org file.
(let ((config-el (expand-file-name "slv-config.el" user-emacs-directory))
(config-org (expand-file-name "slv-config.org" user-emacs-directory)))
(if (and (file-exists-p config-el)
(file-exists-p config-org)
(time-less-p (file-attribute-modification-time (file-attributes config-org))
(file-attribute-modification-time (file-attributes config-el))))
(load-file config-el)
(if (file-exists-p config-org)
(org-babel-load-file config-org)
(error "No file `%s' found!! No configuration loaded!!" config-org))))
;;
(provide 'init)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; init.el ends here
```
#### Funcionamiento asíncrono de elpaca
Sea cual sea el modo en que instales `elpaca.el` (me refiero a las dos formas que he descrito arriba, con y sin _no-littering_); hay que fijarse que `elpaca` instala los paquetes de forma asíncrona. Esta es una característica distintiva de `elpaca` y hace que cambien algunas cosas:
- Emacs cargaría el fichero `init.el`, y en virtud del código _bootstrap_ que hemos añadido, instalaría `elpaca` si es que no lo ve ya instalado
- Tal y como especifica la línea `(add-hook 'after-init-hook #'elpaca-process-queues)`, casí al final del código _bootstrap_, `elpaca` procedería a instalar y cargar los paquetes especificados **después** de cargar el fichero `init.el` o, en el caso de mi configuración particular, después de cargar los dos ficheros `init.el` y `slv_config.el`
{{< admonition type=warning title="Adelantar la carga de paquetes" open=true >}}
Si queremos adelantar la carga de paquetes, es decir, forzar la instalación y la carga de los paquetes **antes** de terminar la carga de toda la configuración de **Emacs** podemos usar `(elpaca-wait)`
De hecho en mi configuración particular, quiero tener cargadas la compatibilidad de elpaca con `use-package`, quiero tener `org` actualizado y quiero tener `no-littering` instalado, antes de cargar mi fichero `slv_config.org`; también me interesa tener `delight` disponible ya que lo aplico en varias secciónes _use-package_ de mi configuración. Y por último para la versión 29 de Emacs quiero tener `which-key` disponible para definir mapas de teclado personalizados (en versiones posteriores de Emacs `which-key` ya es un _built-in_). Así que tengo que invocar a `(elpaca-wait)` para adelantar la instalación de esos paquetes.
Esa invocación hace que se procesen todas las actividades encoladas en `elpaca`, los paquetes especificados a posteriori volverán a dejarse en cola y se instalarán y cargarán al final de la ejecución de la configuración de **Emacs** (o en la siguiente invocación de `(elpaca-wait)` pero no es buena idea invocarlo muchas veces).
{{< /admonition >}}
La carga asíncrona de paquetes parece funcionar de maravilla con `elpaca`, la instalación de paquetes es rapidísima y los desarrolladores afirman que es gracias a la asincronía.
Con todo lo explicado pensé que merecía la pena encapsular toda mi configuración en bloques _use-package_ pensando que quizás se podría acelerar la carga de la configuración pero no funciona así, lo único que _use-package_ delega en _elpaca_ son las instalacione de paquetes. Podemos ver como se expande la macro ejecutando `pp-macroexpand-last-sexp` y comprobaremos que solo se invoca a _elpaca_ cuando hay que instalar un paquete.
#### Trabajando con _elpaca_
La mejor presentación de las facilidades que ofrece _elpaca_ [es este video](https://youtu.be/5Ud-TE3iIQY) de No Wayman. Pero de todas formas algunas pistas, aunque no son en absoluto exhaustivas:
| Comando | Descripción |
|:---------------|:------------------------------------------------------------------------------------------------------------------------------|
| elpaca-info | Nos da la información de un paquete, incluyendo entre otras cosas la receta de instalación |
| elpaca-log | Nos permite ver el log de elpaca |
| elpaca-recipe | Nos devuelve la receta para instalar un paquete |
| elpaca-try | |
| elpaca-manager | Este es el centro de operaciones de elpaca, nos permite hacer todo tipo de operaciones con los paquetes (ver tabla siguiente) |
Comandos disponibles dentro de `elpaca-manager`:
| atajo | Comando | Descripción |
|:------|:-------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| s | elpaca-ui-search | Especificar cadena de búsqueda, con prefijo: editar la búsqueda actual. Permite especificar criterios para cada columna, separados por `\|`, p.ej. `#unique !#installed theme \| dark \| 2024` |
| | | |
| i | elpaca-ui-install | Marca un paquete para instalación, estas instalaciones sólo valen para la sesión actual no persisten entre sesiones Emacs |
| f | elpaca-fetch | Baja del repo los últimos cambios del código (git fetch). También podemos usar `elpaca-fetch-all` |
| m | elpaca-merge | Actualiza el código local con los últimos cambios (git merge). También podemos usar `elpaca-merge-all` |
| p | elpaca-update | Combina las operaciones `fetch` y `merge`. También podemos usar `elpaca-update-all` pero no lo recomiendan |
| d | elpaca-delete | Marca un paquete para borrado |
| x | elpaca-ui-execute-marks | Ejecuta las acciones marcadas |
| gl | elpaca-log | Muestra el log de _elpaca_ |
| gi | elpaca-search-installed | Listar paquetes instalados |
| ga | elpaca-search-marked | Listar paquetes marcados |
| go | elpaca-search-orphaned | Listar paquetes huérfanos |
| gt | elpaca-search-tried | Listar paquetes probados |
| gr | elpaca-search-refresh | Refrescar listado (resultado de la búsqueda) |
| v | elpaca-ui-visit | Visitar el directorio del repo local del paquete. Con prefijo, visitar el directorio local del _Build_ del paquete |
| b | elpaca-ui-browse-package | Visitar con el navegador la página web del paquete |
#### Problemas encontrados
- Como ya hemos comentado hay que tener cuidado con cargar anticipadamente todos los paquetes que Emacs necesite para cargar la configuración sin errores. Yo al final opté por mover la carga de paquetes necesarios al fichero `init.el` antes de la llamada a `(elpaca-wait)` en la linea 131 del listado de `init.el`.
- He tenido problemas con Emacs 29 por que _elpaca_ no podía determinar la variable `elpaca-core-date` así que la he definido yo en mi fichero `init.el` para que coincida con la fecha de la release de Emacs 29 (ver linea 35 del listado arriba)
Loading…
Cancel
Save