Subject: [PATCH] gnu: home: Add home-emacs-service-type.

From: David Wilson <[email-masked]>
Date: Thu, 12 Jan 2023 16:01:18 +0200

This commit continues work by ( to add a home-emacs-service-type for the
purpose of configuring Emacs.

The goal here is not to configure Emacs with Scheme forms, but to make it
possible to assemble init.el and early-init.el using files from one's own personal
configuration and other Emacs Lisp snippets for further customization.

The simplest usage of the service would be to use simple strings or local
files to build init.el and early-init.el:

  (home-environment
   (services (list
              (service home-emacs-service-type
                       (home-emacs-configuration
                        (init-file (list (local-file "init.el")))
                        (early-init-file '(";; This is early-init.el!\n")))))))

The `emacs-variable' function can be used to generate a g-expression which
will produce a `setq' form inside of an init  file:

  (home-emacs-configuration
   (init-file (list (emacs-variables
                      '((my/font-size . 24)
                        (my/tab-width . 2)
                        (inhibit-startup-message . #t)))
                    (local-file "init.el"))))

If you have an existing folder of Emacs Lisp files that you `require' into
your configuration, you can use the `load-paths' field to pull in the entire
folder or reference the existing files on your system:

  (home-emacs-configuration
   (init-file (list "(require 'my-init)"))
   (load-paths (list (local-file "emacs-modules"
                                 #:recursive #t))))

Since Emacs loves to write files to `user-emacs-directory', we must provide a
writeable path for it so that Emacs doesn't try to write files into the
read-only store.  The `user-emacs-directory' field can be used to customize
that; it defaults to `~/.cache/emacs'.

Other services can extend home-emacs-service-type with the
home-emacs-extension type to add further configuration snippets, packages, and
load paths:

  (home-environment
   (services
    (list (service home-emacs-service-type
                   (home-emacs-configuration
                    (init-file (list "(require 'my-module)\n"))))

          (simple-service 'home-emacs-extension-one
                           home-emacs-service-type
                           (home-emacs-extension
                            (packages (list (specification->package "emacs-evil")))
                            (init-file (list "(evil-mode +1)\n")))))))

Co-authored-by: ( <[email-masked]>
---
 doc/guix.texi               |  77 +++++++++++++++
 gnu/home/services/emacs.scm | 188 ++++++++++++++++++++++++++++++++++++
 gnu/local.mk                |   1 +
 3 files changed, 266 insertions(+)
 create mode 100644 gnu/home/services/emacs.scm

diff --git a/doc/guix.texi b/doc/guix.texi
index 751d0957d8..62fefde1ea 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -111,6 +111,7 @@ Copyright @copyright{} 2022 (@*
 Copyright @copyright{} 2022 John Kehayias@*
 Copyright @copyright{} 2022 Ivan Vilata-i-Balaguer@*
 Copyright @copyright{} 2023 Giacomo Leidi@*
+Copyright @copyright{} 2023 David Wilson@*

 Permission is granted to copy, distribute and/or modify this document
 under the terms of the GNU Free Documentation License, Version 1.3 or
@@ -41061,6 +41062,7 @@ services)}.
 * Shepherd: Shepherd Home Service.                     Managing User's Daemons.
 * SSH: Secure Shell.                                   Setting up the secure shell client.
 * Desktop: Desktop Home Services.                      Services for graphical environments.
+* Emacs: Emacs Home Services.                          Services for configuring Emacs.
 * Guix: Guix Home Services.                            Services for Guix.
 @end menu
 @c In addition to that Home Services can provide
@@ -41914,6 +41916,81 @@ The package providing the @code{/bin/dbus-daemon} command.
 @end table
 @end deftp

+@node Emacs Home Services
+@subsection Emacs Home Services
+
+@defvr {Scheme Variable} home-emacs-service-type
+This is the service type for configuring the Emacs text editor.  It
+enables you to assemble @file{init.el} and @file{early-init.el} files
+from snippets in your home configuration and other Emacs Lisp files you
+have in your personal configuration folder.
+
+This service can be extended using the @code{home-emacs-extension} type.
+
+Note that if you have an existing @file{~/.emacs} and/or
+@file{~/.emacs.d}, the configuration aspect of this service will not be
+loaded, as the former location takes precedence over
+@file{~/.config/emacs}.  This service uses the latter path in the
+interest of cleanliness.  To migrate to the XDG directory, run these
+commands:
+
+@example
+$ cp ~/.emacs.d $XDG_CONFIG_HOME/emacs
+$ cp ~/.emacs $XDG_CONFIG_HOME/emacs/init.el
+@end example
+@end defvr
+
+@deftp {Data Type} home-emacs-configuration
+The configuration record for @code{home-emacs-service-type}.
+
+@table @asis
+@item @code{emacs} (default: @code{emacs})
+The package providing the @file{/bin/emacs} command.
+
+@item @code{packages} (default: @code{'()})
+Additional packages required by the Emacs configuration.
+
+@item @code{user-emacs-directory} (default: @file{~/.cache/emacs})
+The directory beneath which additional per-user Emacs-specific files are placed.
+
+@item @code{init-file} (default: @code{'()})
+Configuration text or files to include in @file{init.el}.
+
+@item @code{early-init-file} (default: @code{'()})
+Configuration text or files to include in @file{early-init.el}.
+
+@item @code{load-paths} (default: @code{'()})
+Additional load paths to add to Emacs' @code{load-path} variable.  Lines
+will be inserted at the beginning of @file{early-init.el}.
+
+@item @code{native-compile?} (default: @code{#f})
+Whether to compile all @code{packages}, using the provided @code{emacs}
+package in place of @code{emacs-minimal}, which will enable native
+compilation if the @code{emacs} package supports it.  All
+non-@code{-minimal} Emacs packages at version 28 or above should support
+native compilation.
+@end table
+@end deftp
+
+@deftp {Data Type} home-emacs-extension
+The extension record for @code{home-emacs-service-type}.
+
+@table @asis
+@item @code{packages} (default: @code{'()})
+Additional packages required by the Emacs configuration.
+
+@item @code{init-file} (default: @code{'()})
+Configuration text or files to include in @file{init.el}.
+
+@item @code{early-init-file} (default: @code{'()})
+Configuration text or files to include in @file{early-init.el}.
+
+@item @code{load-paths} (default: @code{'()})
+Additional load paths to add to Emacs' @code{load-path} variable.  Lines
+will be inserted at the beginning of @file{early-init.el}.
+@end table
+@end deftp
+
 @node Guix Home Services
 @subsection Guix Home Services

diff --git a/gnu/home/services/emacs.scm b/gnu/home/services/emacs.scm
new file mode 100644
index 0000000000..45b4f2a5d6
--- /dev/null
+++ b/gnu/home/services/emacs.scm
@@ -0,0 +1,188 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2022 ( <[email-masked]>
+;;; Copyright © 2023 David Wilson <[email-masked]>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (gnu home services emacs)
+  #:use-module (gnu home services)
+  #:autoload   (gnu packages emacs) (emacs-minimal
+                                     emacs)
+  #:use-module (gnu services configuration)
+  #:use-module (guix gexp)
+  #:use-module (guix packages)
+  #:use-module (guix records)
+  #:use-module (ice-9 match)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-26)
+
+  #:export (emacs-variables
+            home-emacs-configuration
+            home-emacs-extension
+            home-emacs-service-type))
+
+(define list-of-file-likes?
+  (list-of file-like?))
+
+(define (string-or-file-like? val)
+  (or (string? val)
+      (file-like? val)))
+
+(define list-of-string-or-file-likes?
+  (list-of string-or-file-like?))
+
+(define-configuration/no-serialization home-emacs-configuration
+  (emacs
+   (file-like emacs)
+   "The package providing the @file{/bin/emacs} command.")
+  (packages
+   (list-of-file-likes '())
+   "Additional packages required by the Emacs configuration.")
+  (user-emacs-directory
+   (string "~/.cache/emacs")
+   "Directory beneath which additional per-user Emacs-specific files are placed.")
+  (init-file
+   (text-config '())
+   "Configuration text or files to include in init.el.")
+  (early-init-file
+   (text-config '())
+   "Configuration text or files to include in early-init.el.")
+  (load-paths
+   (list-of-string-or-file-likes '())
+   "Additional load paths to add to Emacs' @code{load-path} variable.  Lines
+will be inserted at the beginning of early-init.el.")
+  (native-compile?
+   (boolean #f)
+   "Whether to compile the @code{packages} using the Emacs package
+provided as the value of the @code{emacs} field, which will enable
+native compilation if the @code{emacs} package supports it."))
+
+(define (home-emacs-profile-packages config)
+  (cons (home-emacs-configuration-emacs config)
+        (home-emacs-configuration-packages config)))
+
+(define (home-emacs-transformed-packages config)
+  (map (if (home-emacs-configuration-native-compile? config)
+           (package-input-rewriting
+            `((,emacs-minimal
+              . ,(home-emacs-configuration-emacs config))))
+           identity)
+       (let ((packages (home-emacs-configuration-packages config)))
+         (concatenate
+          (cons packages
+                (map (compose (cute map second <>)
+                              package-transitive-propagated-inputs)
+                     packages))))))
+
+(define (serialize-emacs-load-paths config)
+  #~(string-append
+     ";; Additional load paths\n"
+     #$@(map (lambda (load-path)
+               #~(format #f "(add-to-list 'load-path \"~a\")" #$load-path))
+             (home-emacs-configuration-load-paths config))
+     "\n\n"))
+
+(define (serialize-emacs-user-directory config)
+  (format #f
+          ";; Set the `user-emacs-directory` to a writeable path\n(setq user-emacs-directory \"~a\")\n\n"
+          (home-emacs-configuration-user-emacs-directory config)))
+
+(define (home-emacs-xdg-configuration-files config)
+  `(("emacs/early-init.el"
+     ,(apply mixed-text-file
+             (cons* "early-init.el"
+                    (serialize-emacs-load-paths config)
+                    (serialize-emacs-user-directory config)
+                    (home-emacs-configuration-early-init-file config))))
+    ("emacs/init.el"
+     ,(apply mixed-text-file
+             (cons "init.el"
+                   (home-emacs-configuration-init-file config))))))
+
+(define-configuration/no-serialization home-emacs-extension
+  (packages
+   (list-of-file-likes '())
+   "Additional packages required by the Emacs configuration.")
+  (init-file
+   (text-config '())
+   "Configuration text or files to include in init.el.")
+  (early-init-file
+   (text-config '())
+   "Configuration text or files to include in early-init.el.")
+  (load-paths
+   (list-of-string-or-file-likes '())
+   "Additional load paths to add to Emacs' @code{load-path} variable.  Lines
+will be inserted at the beginning of early-init.el."))
+
+(define (home-emacs-extensions original-config extension-configs)
+  (match-record original-config <home-emacs-configuration>
+    (packages load-paths init-file early-init-file)
+    (home-emacs-configuration
+     (inherit original-config)
+     (packages
+      (append packages
+              (append-map
+               home-emacs-extension-packages extension-configs)))
+     (init-file
+      (append init-file
+              (append-map
+               home-emacs-extension-init-file extension-configs)))
+     (early-init-file
+      (append early-init-file
+              (append-map
+               home-emacs-extension-early-init-file extension-configs)))
+     (load-paths
+      (append load-paths
+              (append-map
+               home-emacs-extension-load-paths extension-configs))))))
+
+(define home-emacs-service-type
+  (service-type
+   (name 'home-emacs)
+   (extensions
+    (list (service-extension
+           home-profile-service-type
+           home-emacs-profile-packages)
+          (service-extension
+           home-shepherd-service-type
+           home-emacs-shepherd-services)
+          (service-extension
+           home-xdg-configuration-files-service-type
+           home-emacs-xdg-configuration-files)))
+   (default-value (home-emacs-configuration))
+   (compose identity)
+   (extend home-emacs-extensions)
+   (description
+    "Configure the GNU Emacs extensible text editor.")))
+
+(define scheme-value->emacs-value
+  (match-lambda (#t (quote 't))
+                (#f (quote 'nil))
+                (val val)))
+
+(define (emacs-variables var-alist)
+  "Converts an alist of variable names and values into a @code{setq}
+expression that can be used in an Emacs configuration.  Scheme values
+@code{#t} and @code{#f} will be converted into @code{t} and @code{nil},
+respectively."
+  #~(string-append
+     "(setq"
+     #$@(map (lambda (var)
+               #~(format #f "\n  ~a ~s"
+                         (quote #$(car var))
+                         #$(scheme-value->emacs-value (cdr var))))
+             var-alist)
+     ")\n\n"))
diff --git a/gnu/local.mk b/gnu/local.mk
index 184f43e753..35d88b4dd6 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -89,6 +89,7 @@ GNU_SYSTEM_MODULES =				\
   %D%/home/services.scm			\
   %D%/home/services/desktop.scm			\
   %D%/home/services/symlink-manager.scm		\
+  %D%/home/services/emacs.scm			\
   %D%/home/services/fontutils.scm		\
   %D%/home/services/guix.scm			\
   %D%/home/services/pm.scm			\
--
2.38.1