GNU bug report logs - #64620
[PATCH] gnu: home: Add home-emacs-service-type.

Please note: This is a static page, with minimal formatting, updated once a day.
Click here to see this page with the latest information and nicer formatting.

Package: guix-patches; Reported by: fernseed@HIDDEN; Keywords: patch; dated Fri, 14 Jul 2023 15:50:02 UTC; Maintainer for guix-patches is guix-patches@HIDDEN.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 1 Apr 2024 08:28:58 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Mon Apr 01 04:28:58 2024
Received: from localhost ([127.0.0.1]:49475 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rrD2H-0001Rm-Uq
	for submit <at> debbugs.gnu.org; Mon, 01 Apr 2024 04:28:58 -0400
Received: from mailtransmit04.runbox.com ([2a0c:5a00:149::25]:54846)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <steve@HIDDEN>) id 1rrD2F-0001Qz-Kq
 for 64620 <at> debbugs.gnu.org; Mon, 01 Apr 2024 04:28:56 -0400
Received: from mailtransmit02.runbox ([10.9.9.162] helo=aibo.runbox.com)
 by mailtransmit04.runbox.com with esmtps (TLS1.2) tls
 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.93)
 (envelope-from <steve@HIDDEN>) id 1rrD26-00EDqH-LO
 for 64620 <at> debbugs.gnu.org; Mon, 01 Apr 2024 10:28:46 +0200
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed;
 d=futurile.net; s=selector1; h=Content-Type:MIME-Version:Message-ID:Subject:
 To:From:Date; bh=txCAmdi1hJQ4fBQbtIfV254iVd6CQmECn3KcqZ0gn14=; b=LQccwZb7H99d
 NHlqLHMDNAsUJaDbsDc+j2oDF/7KQgGx2C3Zub2pPC0fUATi7xlvY34DuehhcmSeTyAmrEryOLmN4
 DKU/Ra3n3AmOFSNvxMoxBWKFxvpviMK1Wk2CHU6leeNDV3nUMZEPvCxWHxoICXwoD0GNemsSTPaKM
 0l27fQM4rZ8BSvM0IYGv203gAX/DgRSytr5PaSsE7oGixyOq3SJStZa67mQJI74s2gClFxBhD7ksq
 FWvcgqa1CiF6PpCiiiCqTxInhU3AifODur+4Dgw5Z1NX4ZMWEl6j1nM60YI+dUHxVeL5sNXNqWjZi
 sHebIiFnkpCki+gu2DCQlw==;
Received: from [10.9.9.73] (helo=submission02.runbox)
 by mailtransmit02.runbox with esmtp (Exim 4.86_2)
 (envelope-from <steve@HIDDEN>) id 1rrD25-0007i0-5w
 for 64620 <at> debbugs.gnu.org; Mon, 01 Apr 2024 10:28:45 +0200
Received: by submission02.runbox with esmtpsa [Authenticated ID (641962)]
 (TLS1.2:ECDHE_SECP256R1__RSA_SHA256__AES_256_GCM:256) (Exim 4.93)
 id 1rrD1r-00CV83-8M
 for 64620 <at> debbugs.gnu.org; Mon, 01 Apr 2024 10:28:31 +0200
Date: Mon, 1 Apr 2024 09:28:30 +0100
From: Steve George <steve@HIDDEN>
To: 64620 <at> debbugs.gnu.org
Subject: Bumping - Add home-emacs-service-type
Message-ID: <ZgpwLtEvdTrbPt7Q@t25sg>
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Disposition: inline
X-Spam-Score: -0.7 (/)
X-Debbugs-Envelope-To: 64620
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.7 (-)

Hi,

Looks like a lot of work was done on this but it didn't get to a conclusion? 

Any way we can get it moving again? :-)

Futurile




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 13 Oct 2023 14:00:39 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Fri Oct 13 10:00:39 2023
Received: from localhost ([127.0.0.1]:47010 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qrIiU-0000s5-JQ
	for submit <at> debbugs.gnu.org; Fri, 13 Oct 2023 10:00:39 -0400
Received: from mout-y-209.mailbox.org ([2001:67c:2050:103:465::209]:45038)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <fernseed@HIDDEN>) id 1qrIiP-0000rk-Ox
 for 64620 <at> debbugs.gnu.org; Fri, 13 Oct 2023 10:00:37 -0400
Received: from smtp102.mailbox.org (smtp102.mailbox.org [10.196.197.102])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256)
 (No client certificate requested)
 by mout-y-209.mailbox.org (Postfix) with ESMTPS id 4S6SnG3v4Tz9v6S;
 Fri, 13 Oct 2023 16:00:02 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=fernseed.me; s=MBO0001;
 t=1697205602;
 h=from:from:reply-to:subject:subject:date:date:message-id:message-id:
 to:to:cc:cc:mime-version:mime-version:content-type:content-type:
 content-transfer-encoding:content-transfer-encoding:
 in-reply-to:in-reply-to:references:references;
 bh=uXnTpUfRnIvmXJHt8WANVCsmbIoMwk+Lu1SyZh04qAo=;
 b=HGnuzC5LLDI56Ajrb1T7DTsb7ey3+GymyNks1xxTxn0pt/2f/s5pQrgUZIf66BI89C5qSp
 ZSE7YFI2VEOjV6jQurTvlJ9HLop5WUQEnwMuV3QHTHQap0hvg/NecrNREz/+XXt672mYdG
 Z3YZGaBL+ZZyr1z/C+O0+hMX8rvPK551HVigz4MDN0tHcszOybCAYh4KoUR6eS6FHFaxHf
 LZ0Fcs3I7oG9O1NA0f98nnHC5PNtHkHPAJDGt3DfIgHQinc+Ct1rOhL8sA7zUS0SWTS1HE
 Q74TGNNVd78tGD7c/gDtvIS3BWSZxb8YOj0qnRhij82vLtsWmvc5V52crqS/lw==
From: Kierin Bell <fernseed@HIDDEN>
To: Liliana Marie Prikler <liliana.prikler@HIDDEN>
Subject: Re: [bug#64620] [PATCH] gnu: home: Add home-emacs-service-type.
In-Reply-To: <0dc55cac713686514f90ec04243f710894451a82.camel@HIDDEN>
 (Liliana Marie Prikler's message of "Fri, 13 Oct 2023 06:30:59 +0200")
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
 <87il945gzu.fsf@HIDDEN> <874jklyk38.fsf@HIDDEN>
 <873503hebs.fsf@HIDDEN> <87y1huvluu.fsf@HIDDEN>
 <87o7iqpeou.fsf@HIDDEN> <87a5spuoc5.fsf@HIDDEN>
 <8734yf4he4.fsf@HIDDEN>
 <0dc55cac713686514f90ec04243f710894451a82.camel@HIDDEN>
Date: Fri, 13 Oct 2023 09:59:57 -0400
Message-ID: <87o7h2lj1e.fsf@HIDDEN>
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
X-Spam-Score: 0.0 (/)
X-Debbugs-Envelope-To: 64620
Cc: "\(" <paren@HIDDEN>, Ludovic =?utf-8?Q?Court=C3=A8s?= <ludo@HIDDEN>,
 cox.katherine.e+guix@HIDDEN, 64620 <at> debbugs.gnu.org,
 Andrew Tropin <andrew@HIDDEN>
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.0 (-)

Hello,

Liliana Marie Prikler <liliana.prikler@HIDDEN> writes:

> Hi, Kierin
>
> Am Donnerstag, dem 12.10.2023 um 18:15 -0400 schrieb Kierin Bell:
[...]
> I think you should separate your concerns more clearly.  Rather than
> having home-emacs-service-type take packages and all that other fluff,
> you could just have init-directory as a list of file-likes.  Then you
> can have an init.el-file procedure (or syntax) to take care of
> packages.
>
> e.g.
>   (home-emacs-configuration
>     (emacs %my-custom-emacs)
>     (init-directory
>       (list (init.el-file (emacs-package =E2=80=A6) (elisp* =E2=80=A6) =
=E2=80=A6))))

Apologies!  I made some confusing errors in the example configurations
in previous message.  In the newer design, the `home-emacs-service-type'
would not directly deal with <emacs-package> objects at all.

Here would be a proper example of the newer semantics:

--8<---------------cut here---------------start------------->8---
(home-environment
 ;; ...
 (services
  (list
   ;; ...
   (service home-emacs-service-type
    (home-emacs-configuration
     (emacs %my-custom-emacs-version)
     (user-emacs-directory "~/.local/state/emacs/")
     (default-init
       (emacs-configuration
        (extra-init
         (elisp* . . . .))))
     ;; ... And possibly:
     (servers
      (list
       (emacs-server-with-packages
        (emacs-server
         (name "server"))
        (list
         (emacs-package
          ;; ...
          )))))))
   (service home-emacs-packages-service-type
    (emacs %my-custom-emacs-version)
    (serializer %emacs-use-package-serializer)
    (packages
     (list
      (emacs-package
       ;; ...
       )
      ;; ...
      ))))))
--8<---------------cut here---------------end--------------->8---


I think you're suggesting flatting the hierarchy from:

--8<---------------cut here---------------start------------->8---
(home-emacs-configuration
 (default-init
   (emacs-configuration
    (extra-init
     (elisp* . . . .))
    (extra-init-files    ; files symlinked and loaded with `load'
     (list
      (local-file "~/guix-config/extras.el")))
    (variables '((foo . #f))))))
--8<---------------cut here---------------end--------------->8---

...Into something like:

--8<---------------cut here---------------start------------->8---
(home-emacs-configuration
 (init-file
  (list
   (elisp* . . .)
   (local-file "~/guix-config/extras.el") ; file contents spliced in
                                          ; or symlinked and loaded?
   (emacs-variables '((foo . #f)))
   (emacs-packages
    (emacs-package
     ;; ...
     )))))
--8<---------------cut here---------------end--------------->8---

This is what David Wilson's patch[1] did.=20

There could easily be an explicit `init.el-file' function in there
instead.

> Now, admittedly, snarfing guix packages out of init.el-file might
> become an issue; I haven't thought about how to implement that
> concretely.
>
> The upside is, that you could reuse this structure for servers.  An
> emacs-server would just take another home-emacs-configuration and a
> server name.

The upside of the approach that uses <emacs-configuration> records is
that the encapsulation avoids the weirdness of using a
<home-emacs-configuration> that contains <emacs-server> objects to
create new server objects.  Also, obviously the introspection of
records.

The nesting is confusing, though, and I'd definitely like to work on
that.

A middle ground might be to keep the concept of the
<emacs-configuration> record (maybe even the
`home-emacs-packages-service-type'), but make an `init-file' field smart
enough to accept either Elisp expressions, file-like objects, or
<emacs-configuration> records.

I feel like this may be more of an Elispish way to do things than a
properly Schemic way, but it's a lot simpler to understand.

We could export functions/macros that explicitly convert alists,
<emacs-package> objects, etc. into Elisp, too, in addition.  These may
be useful in other contexts.  But I think it is probably less confusing
and better in principle to chose only one configuration paradigm as the
"proper", standardized way of doing things.

WDYT?

Footnotes:
[1]  https://issues.guix.gnu.org/60753

--=20
Kierin Bell
GPG Key: FCF2 5F08 EA4F 2E3D C7C3  0D41 D14A 8CD3 2D97 0B36




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 13 Oct 2023 04:31:35 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Fri Oct 13 00:31:35 2023
Received: from localhost ([127.0.0.1]:44581 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qr9pn-00017L-8P
	for submit <at> debbugs.gnu.org; Fri, 13 Oct 2023 00:31:35 -0400
Received: from mail-wm1-x343.google.com ([2a00:1450:4864:20::343]:48563)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <liliana.prikler@HIDDEN>) id 1qr9pj-000171-Ey
 for 64620 <at> debbugs.gnu.org; Fri, 13 Oct 2023 00:31:33 -0400
Received: by mail-wm1-x343.google.com with SMTP id
 5b1f17b1804b1-405417465aaso18139825e9.1
 for <64620 <at> debbugs.gnu.org>; Thu, 12 Oct 2023 21:31:08 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=gmail.com; s=20230601; t=1697171463; x=1697776263; darn=debbugs.gnu.org;
 h=mime-version:user-agent:content-transfer-encoding:references
 :in-reply-to:date:cc:to:from:subject:message-id:from:to:cc:subject
 :date:message-id:reply-to;
 bh=z7ttJaN1KuGIJhZh8ig/PKC9wev/9dhyqpWpb804fnc=;
 b=AmH71Rp3CtlsQsfUyYw0moVDRVILPRzuY007KA7TfmuzaShLo05BRrFbRwKC5xdmdf
 H4MX++MzCnoqmnwGYU/CUjbW3eBZYnDwSjJ7SB+u+vsp/equNdzYW9CIX25TMzPLDBlU
 RlfoCgrVjBmzd5ep8P4tZt1sAMJouzVUmYnIjR2luT1BN412ugAOUE8nNIEU9iWFZ+pu
 bEUTaGa5R2CTei67hhwOtsip+BqbmvWNPDVIczvqePnp+fGlL2tw/9YEM0B2QIpZniVV
 2B3fsLUGl1UxltrkRqFQIhcyaKqlNR5iy+Ky7bH82CG8fhBjBWQ627fTCgdeex+TdFxi
 V6Ng==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=1e100.net; s=20230601; t=1697171463; x=1697776263;
 h=mime-version:user-agent:content-transfer-encoding:references
 :in-reply-to:date:cc:to:from:subject:message-id:x-gm-message-state
 :from:to:cc:subject:date:message-id:reply-to;
 bh=z7ttJaN1KuGIJhZh8ig/PKC9wev/9dhyqpWpb804fnc=;
 b=Rjl5SOdYLm/XX+f+J9+BuebFpyO0ck763QILfsLQLCsvCgMEMB02heXyh21yXd5SMR
 WWqllgx8ybVHYzJ07TqKAUxiWeztBxh6lW+hETeUZMpRAIFkvKu7y8n39o6gG7ohm8zN
 /gGbf+hLbCgwfD7bDFAUbtftF1CnxJpcg8foni6oP8IIKROyzQ+gP9JKqxfDZ5uvzDf8
 7rPwfhD4uvlN1twUQDMJFVq3DDt2rzlZ7ezwo6l+oQyQban+xywwgR3c4Q5A0Fjho8eS
 M5O9EsT9QZZ2R2iCGBiJ1wN8PXm8JRWd0pJ2su/4Y16jQg0PZ4u/BYlawcxV7jepY4Jx
 te6Q==
X-Gm-Message-State: AOJu0YzSXNaTKzIfU2CHxCz1GfznrJHUDc1ciVrjve78s74LYy1kiVQh
 UmLSbsEM86livqZzHxlO2R0=
X-Google-Smtp-Source: AGHT+IHp8uobon7I0pI0kn6X3Zpk8UHqCiMfWAmjuG8CEyWZ+xuk4XAnNaXs7LyFMDoonX9p9HvhEA==
X-Received: by 2002:a05:6000:1b07:b0:31f:ec91:39a3 with SMTP id
 f7-20020a0560001b0700b0031fec9139a3mr22226567wrz.30.1697171462519; 
 Thu, 12 Oct 2023 21:31:02 -0700 (PDT)
Received: from lumine.fritz.box (85-127-52-93.dsl.dynamic.surfer.at.
 [85.127.52.93]) by smtp.gmail.com with ESMTPSA id
 q14-20020adfcb8e000000b003296b488961sm19130646wrh.31.2023.10.12.21.31.00
 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
 Thu, 12 Oct 2023 21:31:01 -0700 (PDT)
Message-ID: <0dc55cac713686514f90ec04243f710894451a82.camel@HIDDEN>
Subject: Re: [bug#64620] [PATCH] gnu: home: Add home-emacs-service-type.
From: Liliana Marie Prikler <liliana.prikler@HIDDEN>
To: Kierin Bell <fernseed@HIDDEN>, Ludovic =?ISO-8859-1?Q?Court=E8s?=
 <ludo@HIDDEN>
Date: Fri, 13 Oct 2023 06:30:59 +0200
In-Reply-To: <8734yf4he4.fsf@HIDDEN>
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
 <87il945gzu.fsf@HIDDEN> <874jklyk38.fsf@HIDDEN>
 <873503hebs.fsf@HIDDEN> <87y1huvluu.fsf@HIDDEN>
 <87o7iqpeou.fsf@HIDDEN> <87a5spuoc5.fsf@HIDDEN>
 <8734yf4he4.fsf@HIDDEN>
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable
User-Agent: Evolution 3.46.4 
MIME-Version: 1.0
X-Spam-Score: 0.0 (/)
X-Debbugs-Envelope-To: 64620
Cc: "\(" <paren@HIDDEN>, cox.katherine.e+guix@HIDDEN,
 64620 <at> debbugs.gnu.org, Andrew Tropin <andrew@HIDDEN>
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.0 (-)

Hi, Kierin

Am Donnerstag, dem 12.10.2023 um 18:15 -0400 schrieb Kierin Bell:
> [=E2=80=A6]
> The benefits are maintainability and usability --- users who don't
> want to use the package configuration interface don't have to deal
> with the cognitive dissonance.
>=20
> The downside is that Emacs package configuration becomes more
> cumbersome for more advanced use cases.
>=20
> One case, illustrated above, is that the
> `home-emacs-packages-service-type' doesn't know the Emacs version
> used by the `home-emacs-service-type' --- a non-default version of
> Emacs must be specified again, separately, for the packages service
> (that is, if it matters that the package serializer knows the Emacs
> version).
>=20
> Another case is that in order to configure Emacs packages for
> specific Emacs servers (created via the `servers' field of the
> `home-emacs-configuration'), there would either need to be a
> `servers' field in the `home-emacs-packages-configuration' record
> type (complicated to implement) or users would need to do this
> manually (with the help of a new function such as `emacs-server-with-
> packages').
>=20
> I'd appreciate hearing preferences or arguments for or against
> either. Also, suggestions for simplifying any part of the interface
> are welcome!
I think you should separate your concerns more clearly.  Rather than
having home-emacs-service-type take packages and all that other fluff,
you could just have init-directory as a list of file-likes.  Then you
can have an init.el-file procedure (or syntax) to take care of
packages.

e.g.
  (home-emacs-configuration
    (emacs %my-custom-emacs)
    (init-directory
      (list (init.el-file (emacs-package =E2=80=A6) (elisp* =E2=80=A6) =E2=
=80=A6))))
Now, admittedly, snarfing guix packages out of init.el-file might
become an issue; I haven't thought about how to implement that
concretely.

The upside is, that you could reuse this structure for servers.  An
emacs-server would just take another home-emacs-configuration and a
server name.

Cheers




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 12 Oct 2023 22:27:02 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Thu Oct 12 18:27:02 2023
Received: from localhost ([127.0.0.1]:44436 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qr48z-00082Y-Tq
	for submit <at> debbugs.gnu.org; Thu, 12 Oct 2023 18:27:02 -0400
Received: from mout-y-209.mailbox.org ([91.198.250.237]:60296)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <fernseed@HIDDEN>) id 1qr48u-00082B-FU
 for 64620 <at> debbugs.gnu.org; Thu, 12 Oct 2023 18:27:00 -0400
Received: from smtp102.mailbox.org (smtp102.mailbox.org [10.196.197.102])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256)
 (No client certificate requested)
 by mout-y-209.mailbox.org (Postfix) with ESMTPS id 4S64420Y6Cz9tfD;
 Fri, 13 Oct 2023 00:26:26 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=fernseed.me; s=MBO0001;
 t=1697149586;
 h=from:from:reply-to:subject:subject:date:date:message-id:message-id:
 to:to:cc:cc:mime-version:mime-version:content-type:content-type:
 content-transfer-encoding:content-transfer-encoding:
 in-reply-to:in-reply-to:references:references;
 bh=Rd78TF+9LxJt81NnGDRmun8vZCVO00xYPCQB/gsAfYw=;
 b=z9KROZjBbqNKp6bp7sJGYMNyBZpCVnIO4do1bBuOW/06vSzkWnXXTLP7YiAWEmrg7t037+
 twNG3ADwFXgWN47C2sZ0fH7ud7Cn4S7G6kyfoFJJxiBQ/8HTogy57p2znTXoARkZ+bZPUH
 gF4rZZl54I4Q6FqB9vlT6RysR3a+yVn1PYZ53zi3pk90jn/wWxgs3dEkIV8RB/w+oUtsSO
 bW889lWjfRljnOmJCKrY7G/z5yNEJFbx9YkHkJZNot5xYhZI5Nv6WcBK4nu1ByRFGJy6PZ
 RwKqHBE6LP6kENUFXb1tgyrZhuXIHwHvowP63/wfR9urKPqc1radxRVTs8G6Uw==
From: Kierin Bell <fernseed@HIDDEN>
To: Ludovic =?utf-8?Q?Court=C3=A8s?= <ludo@HIDDEN>
Subject: Re: [bug#64620] [PATCH] gnu: home: Add home-emacs-service-type.
In-Reply-To: <87lec9t890.fsf@HIDDEN> ("Ludovic =?utf-8?Q?Court=C3=A8s=22'?=
 =?utf-8?Q?s?= message of "Wed, 11 Oct 2023 18:48:59 +0200")
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
 <87lec9t890.fsf@HIDDEN>
Date: Thu, 12 Oct 2023 18:26:20 -0400
Message-ID: <871qdz4gvn.fsf@HIDDEN>
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
X-Spam-Score: 0.0 (/)
X-Debbugs-Envelope-To: 64620
Cc: Josselin Poiret <dev@HIDDEN>, Tobias Geerinckx-Rice <me@HIDDEN>,
 Simon Tournier <zimon.toutoune@HIDDEN>, paren@HIDDEN,
 Christopher Baines <mail@HIDDEN>, 64620 <at> debbugs.gnu.org,
 Andrew Tropin <andrew@HIDDEN>, Ricardo Wurmus <rekado@HIDDEN>,
 Mathieu Othacehe <othacehe@HIDDEN>
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.0 (-)

Ludovic Court=C3=A8s <ludo@HIDDEN> writes:

> So, here is a short review of the parts I=E2=80=99m most familiar with.
>
> fernseed@HIDDEN skribis:
>
>
> [...]
>
>> +(define-module (gnu home services emacs)
>> +  #:use-module (gnu home services)
>
> [...]
>
>> +  #:re-export (blank?
>> +
>> +               vertical-space
>> +               vertical-space?
>
> Why re-export these things from here?  Sounds surprising because we=E2=80=
=99re
> in a service module.
>
>

IIRC, my rationale was that the `<elisp>' objects can contain <blank>
records, e.g., `elisp->sexp' can return them.  But users won't normally
be manipulating them, so I should probably remove this.

>
> For clarity/conciseness, you can use #~ and #$ in this code.
>

Thanks, fixed.

>
> I think you don=E2=80=99t need to fiddle with the monadic interface.  I=
=E2=80=99d
> suggest removing the <elisp-file> type and gexp compiler and instead
> defining =E2=80=98elisp-file=E2=80=99 in terms of =E2=80=98computed-file=
=E2=80=99.  WDYT?
>

This would prevent people from being able to reference Elisp files in
G-expressions, or from writing something like:

--8<---------------cut here---------------start------------->8---
(elisp-file "elisp-file-with-elisp-file"
            (list (elisp (load (unelisp %another-elisp-file)))))
--8<---------------cut here---------------end--------------->8---

...Right?

As long as the Emacs home service type offers a better way to load
files, which I think it does, I'm OK with losing this capability.

>> +(define (record-value rec field)
>> +  "Return the value of field named FIELD in record REC."
>> +  ((record-accessor (record-type-descriptor rec) field) rec))
>> +
>> +(define-syntax extend-record
>> +  ;; Extend record ORIGINAL by creating a new copy using CONSTRUCTOR,
>> +  ;; replacing each field specified by ORIG-FIELD with the evaluation o=
f (PROC
>> +  ;; ORIG-VAL EXT-VALS), where ORIG-VAL is the value of ORIG-FIELD in O=
RIGINAL
>> +  ;; and EXT-VALS is the list of values of EXT-FIELD in EXTENSIONS.
>> +  (lambda (s)
>> +    (syntax-case s ()
>> +      ((_ constructor original extensions (proc orig-field ext-field) .=
..)
>> +       (with-syntax (((field-specs ...)
>> +                      (map
>> +                       (lambda (spec)
>> +                         (syntax-case spec ()
>> +                           ((proc orig-field ext-field)
>> +                            #'(orig-field
>> +                               (apply
>> +                                proc
>> +                                (list
>> +                                 (record-value original 'orig-field)
>
> I would advice against accessing record fields by name, with run-time
> field name resolution.
>
> The spirit of records, unlike alists, is that there=E2=80=99s a
> statically-defined mapping of fields to their offsets in the struct;
> without having access to record accessors, you=E2=80=99re not supposed to=
 be
> able to access the record (I know Guile has =E2=80=98struct-ref=E2=80=99,
> =E2=80=98record-accessor=E2=80=99, etc., but these are abstraction-breaki=
ng primitives
> that should be avoided IMO).
>
> How could this code be adjusted accordingly?  I guess you=E2=80=99re look=
ing for
> a way to iterate over fields?
>

From what I remember, the alternatives are all complicated.

I do find `extend-record' to be very useful, especially when services
need to extend configuration records that have nested records.  It's
useful enough that I'd like to see it exported from somewhere, but `(gnu
home services emacs)' hardly seems like the place.

I propose that we put `extend-record' and a few of the most useful
`extend-record-*' procedures --- like `extend-alist-merge' and
`extend-record-field-default' (which I rewrote since v1 of the patch
because there was a bug) --- into `(guix records)'.

There, it could use `lookup-field+wrapper' to manually find the offset
like `match-record'.  This isn't exactly ideal, as it would still need
to then use `struct-ref' or similar, but at least `match-record' does
this, too.

WDYT?

>> +;;; Elisp reader extension.
>> +;;;
>> +
>> +(eval-when (expand load eval)
>> +
>> +  (define (read-elisp-extended port)
>> +    (read-with-comments port
>> +                        #:blank-line? #f
>> +                        #:elisp? #t
>> +                        #:unelisp-extensions? #t))
>> +
>> +  (define (read-elisp-expression chr port)
>> +    `(elisp ,(read-elisp-extended port)))
>> +
>> +  (read-hash-extend #\% read-elisp-expression))
>
> I=E2=80=99d lean towards not having a reader extension because they don=
=E2=80=99t
> compose and it=E2=80=99s easy to end up colliding with another, unrelated
> extension.  I think it=E2=80=99s okay if people write:
>
>   (elisp =E2=80=A6)
>
> rather than:
>
>   #%(=E2=80=A6)
>
> It=E2=80=99s also arguably easier to understand for a newcomer.
>

I'm OK with losing the reader extension.  After some experience, I find
that I'd rather use `elisp'.  Being able to use semicolons for comments
via reader extension is impressive but also weirdly unsettling.

Also, anyone who wants to seriously write a lot of Elisp-in-Scheme will
want to instead use the `elisp*' macro, which can contain multiple
s-exps.  And in order to use the reader extension there, you'd need to
write something like:

(elisp*
  ;; ...
  (unelisp #%SEXP-1)
  (unelisp #%SEXP-2)
  ;; ...
  )

>> +++ b/guix/read-print.scm
>
> This part is the most =E2=80=9Cproblematic=E2=80=9D for me: I=E2=80=99m a=
lready dissatisfied
> with the current state of things (the pretty-printer in particular is
> too complex and hard to work with), and this change brings more
> complexity and lacks orthogonality.
>
> What I=E2=80=99d like to see, ideally, is a clear separation between elisp
> concerns and Scheme concerns in the reader and in the pretty printer.
>
> Probably, a preliminary step (I could look into it) would be to rewrite
> the pretty printer based on Wadler=E2=80=99s =E2=80=9Cprettier printer=E2=
=80=9D paper and/or
> Shinn=E2=80=99s formatting combinators=C2=B9.
>
> WDYT?
>

I can honestly say that I'm dreading splitting the `(guix read-print)'
changes I've made into multiple commits and annotating them properly.
Writing the pretty printer from scratch sounds a lot less confusing and
painful at this point.

I'm imagining a stripped-down version of the "formatting combinators"
from SRFI-159/166 specifically adapted for pulling into service modules
to write pretty-printers, not just for Elisp but also for other
configuration languages.

It's too bad that Guile doesn't have SRFI-159/166 and the requisite
"environment monads" and delayed computation from SRFI-165 built-in.

My first design question would be: Would this be a suitable application
for `(guix monads)' [to create a formatting "environment monad"], or
does that entail more trouble than it's worth?

I'll work on the unit tests, as well, especially once more progress has
been made on the pretty-printer situation.

> Thanks,
> Ludo=E2=80=99.

Thanks, I appreciate the feedback!

--=20
Kierin Bell
GPG Key: FCF2 5F08 EA4F 2E3D C7C3  0D41 D14A 8CD3 2D97 0B36




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 12 Oct 2023 22:15:55 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Thu Oct 12 18:15:55 2023
Received: from localhost ([127.0.0.1]:44430 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qr3yE-0007cz-BY
	for submit <at> debbugs.gnu.org; Thu, 12 Oct 2023 18:15:54 -0400
Received: from mout-y-111.mailbox.org ([91.198.250.236]:40546)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <fernseed@HIDDEN>) id 1qr3yB-0007ck-Lk
 for 64620 <at> debbugs.gnu.org; Thu, 12 Oct 2023 18:15:53 -0400
Received: from smtp2.mailbox.org (smtp2.mailbox.org [10.196.197.2])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256)
 (No client certificate requested)
 by mout-y-111.mailbox.org (Postfix) with ESMTPS id 4S63qF33nxz9sYY;
 Fri, 13 Oct 2023 00:15:21 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=fernseed.me; s=MBO0001;
 t=1697148921;
 h=from:from:reply-to:subject:subject:date:date:message-id:message-id:
 to:to:cc:cc:mime-version:mime-version:content-type:content-type:
 content-transfer-encoding:content-transfer-encoding:
 in-reply-to:in-reply-to:references:references;
 bh=BwOZn6IYZhOD8WmZN+vZbTjEt/VPzmgOvq4Fitedt44=;
 b=x7rOlcmukYco1fdN+fstpTD1AVVMzrEQQs6OyJcdXBL8myXquhToZVoE+QWOdoooXDzXbI
 LuSKUBI//f0lP2DfTmL7GLRXyCyPaYTFha2iOXEEGzSIjpyhjOzkO65srBKm30sjqv8QWn
 s35K9C6IXxrf96q0lsaIwC16Fvr8JU0sCCsJZO6eBdte2aYxgLp90dz9VuSZXWxrCT+nmK
 BzDhRN7QJWR/Z0i30sYTh2jdyVzvOAYa00SihM+LweL1wR4arI4a4h2moPvxxGT8h23kfq
 BFEq+R22dAMpVk33oowXOCQQxZHj+roftyuUouT5jsUDFlJIa1Zvrmk+x6TRQA==
From: Kierin Bell <fernseed@HIDDEN>
To: Ludovic =?utf-8?Q?Court=C3=A8s?= <ludo@HIDDEN>
Subject: Re: [bug#64620] [PATCH] gnu: home: Add home-emacs-service-type.
In-Reply-To: <87a5spuoc5.fsf@HIDDEN> ("Ludovic =?utf-8?Q?Court=C3=A8s=22'?=
 =?utf-8?Q?s?= message of "Wed, 11 Oct 2023 18:16:10 +0200")
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
 <87il945gzu.fsf@HIDDEN> <874jklyk38.fsf@HIDDEN>
 <873503hebs.fsf@HIDDEN> <87y1huvluu.fsf@HIDDEN>
 <87o7iqpeou.fsf@HIDDEN> <87a5spuoc5.fsf@HIDDEN>
Date: Thu, 12 Oct 2023 18:15:15 -0400
Message-ID: <8734yf4he4.fsf@HIDDEN>
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
X-Spam-Score: 0.0 (/)
X-Debbugs-Envelope-To: 64620
Cc: cox.katherine.e+guix@HIDDEN, "\(" <paren@HIDDEN>,
 Andrew Tropin <andrew@HIDDEN>, 64620 <at> debbugs.gnu.org,
 Liliana Marie Prikler <liliana.prikler@HIDDEN>
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.0 (-)


Hello,

Ludovic Court=C3=A8s <ludo@HIDDEN> writes:

> Hello!
>
> If there=E2=80=99s consensus, I think we should go ahead with this patch =
series.
> Worst that could happen is that people will think of ways to change the
> service in one way or another, and that=E2=80=99s fine!
>
> Two general comments:
>
>   =E2=80=A2 As I wrote earlier, I think it=E2=80=99d be nice to have inte=
gration tests
>     for this, in addition to the unit tests the patch already adds.
>
>   =E2=80=A2 We may want to split the patch into sizable, self-contained b=
ites.
>     For instance, the (guix read-print) changes should probably be
>     separated out.
>
> I=E2=80=99ll provide more specific comments about the code.
>
> To Emacs team members: please review the Emacs bits of the series!
>
> Thanks,
> Ludo=E2=80=99.
>
>

I have been working on getting v2 ready!

I'll address the comments specific to the reader, printer and
G-expression parts in a reply to the other message.

Specifically regarding the `home-emacs-service-type' interface, most of
it has not changed since July, but I have a few pertinent comments here.

First, I've made a few obvious improvements:

1. The package serializers no longer automatically try to add `-hook'
   suffixes to hook symbols specified in the `hooks' field of the
   `emacs-package' record type (=C3=A0 la `use-package').  This bites back
   when we want to use hooks whose names end in `-functions'.

2. In order to achieve (1), the `%emacs-use-package-serializer' needs to
   set the relevant options for `use-package' so that it does not add
   `-hook' suffixes.  Hence, I've added a new field to the
   `emacs-package-serializer' record type for any Elisp that must be
   evaluated in order for serialized package configuration to work
   properly.

3. Writing `(list (elisp .) (elisp .))' is cumbersome, so I implemented
   a new `elisp*' macro that splices multiple s-exps together.  We can
   achieve the same effect as above by writing `(elisp* . .)'.

4. I'm implementing two new functions,
   `make-simple-package-elisp-serializer' and
   `make-use-package-elisp-serializer', such that with no arguments they
   return the default package serializer procedures, but:
=20=20
     (make-use-package-elisp-serializer EXTRA-KEYWORD-HANDLER)
=20
   ...Returns a version that serializes the `extra-keywords' field of
   any `emacs-package' record according to the function
   EXTRA-KEYWORD-HANDLER.  I'm using this, for example, in my own config
   to define an `auto-faces' keyword that lets me specify faces on a
   per-theme basis.

5. I'm adding an `extra-init-files' field to the `emacs-package' record
   type that mirrors that of the `emacs-configuration' record type.  The
   rationale is that it is often convenient to have a complex
   configuration for a specific package in a self-contained Elisp file,
   which via this field can be loaded in the main Emacs user init file.

Second, I understand that the 1.3kloc implementation of the interface
for configuring Emacs packages in Scheme is rather opinionated.  Some
of the changes described above arguably add to this even more.

To simplify things, I've been playing around with splitting this
functionality into a `home-emacs-packages-service-type', which would
extend the `home-emacs-service-type'.  This could go in unofficial
channels, but ideally I'd like to see it included with this patch set.

The old configuration interface looks like this:
--8<---------------cut here---------------start------------->8---
(home-environment
 ;; ...
 (services
  (list
   ;; ...
   (service home-emacs-service-type
    (home-emacs-configuration
     (user-emacs-directory "~/.local/state/emacs/")
     (package-serializer %emacs-use-package-serializer)
     (configured-packages
      (list
      (emacs-package
       ;; ...
       )
       ;; ... Lots more stuff here ...
       )))))))
--8<---------------cut here---------------end--------------->8---

And the modularized configuration would look like this:
--8<---------------cut here---------------start------------->8---
(home-environment
 ;; ...
 (services
  (list
   ;; ...
   (service home-emacs-service-type
    (home-emacs-configuration
     (emacs %my-custom-emacs-version)
     (user-emacs-directory "~/.local/state/emacs/")
     (configured-packages
      (list
       (emacs-package
        ;; ...
        )
       ;; ... Lots more stuff here ...
       ))))
   (service home-emacs-packages-service-type
    (emacs %my-custom-emacs-version)
    (serializer %emacs-use-package-serializer)
    (packages
     (emacs-package
      ;; ...
      )
     ;; ... Lots of stuff here ...
     )))))
--8<---------------cut here---------------end--------------->8---

The benefits are maintainability and usability --- users who don't want
to use the package configuration interface don't have to deal with the
cognitive dissonance.

The downside is that Emacs package configuration becomes more cumbersome
for more advanced use cases.

One case, illustrated above, is that the
`home-emacs-packages-service-type' doesn't know the Emacs version used
by the `home-emacs-service-type' --- a non-default version of Emacs must
be specified again, separately, for the packages service (that is, if it
matters that the package serializer knows the Emacs version).

Another case is that in order to configure Emacs packages for specific
Emacs servers (created via the `servers' field of the
`home-emacs-configuration'), there would either need to be a `servers'
field in the `home-emacs-packages-configuration' record type
(complicated to implement) or users would need to do this manually (with
the help of a new function such as `emacs-server-with-packages').

I'd appreciate hearing preferences or arguments for or against either.
Also, suggestions for simplifying any part of the interface are welcome!

Thanks.
--=20
Kierin Bell
GPG Key: FCF2 5F08 EA4F 2E3D C7C3  0D41 D14A 8CD3 2D97 0B36




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 11 Oct 2023 16:49:39 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Wed Oct 11 12:49:39 2023
Received: from localhost ([127.0.0.1]:39923 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qqcOx-0008A3-73
	for submit <at> debbugs.gnu.org; Wed, 11 Oct 2023 12:49:39 -0400
Received: from eggs.gnu.org ([2001:470:142:3::10]:48428)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <ludo@HIDDEN>) id 1qqcOs-00089f-4b
 for 64620 <at> debbugs.gnu.org; Wed, 11 Oct 2023 12:49:38 -0400
Received: from fencepost.gnu.org ([2001:470:142:3::e])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <ludo@HIDDEN>)
 id 1qqcOM-0008Uf-KJ; Wed, 11 Oct 2023 12:49:02 -0400
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org;
 s=fencepost-gnu-org; h=MIME-Version:Date:References:In-Reply-To:Subject:To:
 From; bh=WrTRrGN5J87hU86tkml5ZTqURhc1d4j7xJpQ3mZLtZg=; b=IsKlXDbeYVnjVImijP2n
 4wIqJoxhoAMKhhY46cTGyZfefFa+xaAys2CaDQpL+lBFMlUisbUA3LMNoO5Hn09bwnSN79csi3aM7
 LYsvMARAA8MtqjqXZVvENJqYHOfebf+3/A+gwaONaLXLSnBu35lzHYesNKihaC5UfheAJS7HZsWXg
 3L5AsixzroNgP2n9iju8PtE8P5RRva1RH3C4Wh+kQwKGF6+uJermIJ4JQ5xCO0JzJO2Ef2iwaVKnn
 VJtxzkESU+RvivpCWN5g+XIedLVRSD8n2Hqe2CnOYzF1YxFmrYz7BFSKqNYvOW8YR3D8an37Pu71H
 Utjf5aT6VfbVVA==;
From: =?utf-8?Q?Ludovic_Court=C3=A8s?= <ludo@HIDDEN>
To: fernseed@HIDDEN
Subject: Re: [bug#64620] [PATCH] gnu: home: Add home-emacs-service-type.
In-Reply-To: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
 (fernseed@HIDDEN's message of "Fri, 14 Jul 2023 11:12:31 -0400")
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
Date: Wed, 11 Oct 2023 18:48:59 +0200
Message-ID: <87lec9t890.fsf@HIDDEN>
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
X-Spam-Score: -2.3 (--)
X-Debbugs-Envelope-To: 64620
Cc: Josselin Poiret <dev@HIDDEN>, Tobias Geerinckx-Rice <me@HIDDEN>,
 Simon Tournier <zimon.toutoune@HIDDEN>, paren@HIDDEN,
 Christopher Baines <mail@HIDDEN>, 64620 <at> debbugs.gnu.org,
 Andrew Tropin <andrew@HIDDEN>, Ricardo Wurmus <rekado@HIDDEN>,
 Mathieu Othacehe <othacehe@HIDDEN>
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -3.3 (---)

So, here is a short review of the parts I=E2=80=99m most familiar with.

fernseed@HIDDEN skribis:


[...]

> +(define-module (gnu home services emacs)
> +  #:use-module (gnu home services)

[...]

> +  #:re-export (blank?
> +
> +               vertical-space
> +               vertical-space?

Why re-export these things from here?  Sounds surprising because we=E2=80=
=99re
in a service module.


[...]

> +(define* (elisp->file-builder exps #:key (special-forms '()))
> +  "Return a G-expression that builds a file containing the Elisp
> +expressions (<elisp> objects or s-expressions) or G-epxressions in list =
EXPS.
> +See `elisp-file' for a description of SPECIAL-FORMS."

[...]

> +  (with-imported-modules (source-module-closure
> +                          '((guix read-print)))
> +    (gexp (begin
> +            (use-modules (guix read-print))
> +            (call-with-output-file (ungexp output "out")
> +              (lambda (port)

For clarity/conciseness, you can use #~ and #$ in this code.

> +(define-gexp-compiler (elisp-file-compiler (elisp-file <elisp-file>)
> +                                           system target)
> +  (match-record elisp-file <elisp-file>
> +                (name gexp)
> +    (with-monad %store-monad
> +      (gexp->derivation name gexp
> +                        #:system system
> +                        #:target target
> +                        #:local-build? #t
> +                        #:substitutable? #f))))
> +
> +(define* (elisp-file* name exps #:key (special-forms '()))
> +  "Return as a monadic value a derivation that builds an Elisp file name=
d NAME
> +containing the expressions in EXPS, a list of Elisp expression objects or
> +G-expressions.
> +
> +This is the monadic counterpart of `elisp-file', which see for a descrip=
tion
> +of SPECIAL-FORMS,"
> +  (define builder
> +    (elisp->file-builder exps
> +                         #:special-forms special-forms))
> +
> +  (gexp->derivation name builder
> +                    #:local-build? #t
> +                    #:substitutable? #f))

I think you don=E2=80=99t need to fiddle with the monadic interface.  I=E2=
=80=99d
suggest removing the <elisp-file> type and gexp compiler and instead
defining =E2=80=98elisp-file=E2=80=99 in terms of =E2=80=98computed-file=E2=
=80=99.  WDYT?

> +(define (record-value rec field)
> +  "Return the value of field named FIELD in record REC."
> +  ((record-accessor (record-type-descriptor rec) field) rec))
> +
> +(define-syntax extend-record
> +  ;; Extend record ORIGINAL by creating a new copy using CONSTRUCTOR,
> +  ;; replacing each field specified by ORIG-FIELD with the evaluation of=
 (PROC
> +  ;; ORIG-VAL EXT-VALS), where ORIG-VAL is the value of ORIG-FIELD in OR=
IGINAL
> +  ;; and EXT-VALS is the list of values of EXT-FIELD in EXTENSIONS.
> +  (lambda (s)
> +    (syntax-case s ()
> +      ((_ constructor original extensions (proc orig-field ext-field) ..=
.)
> +       (with-syntax (((field-specs ...)
> +                      (map
> +                       (lambda (spec)
> +                         (syntax-case spec ()
> +                           ((proc orig-field ext-field)
> +                            #'(orig-field
> +                               (apply
> +                                proc
> +                                (list
> +                                 (record-value original 'orig-field)

I would advice against accessing record fields by name, with run-time
field name resolution.

The spirit of records, unlike alists, is that there=E2=80=99s a
statically-defined mapping of fields to their offsets in the struct;
without having access to record accessors, you=E2=80=99re not supposed to be
able to access the record (I know Guile has =E2=80=98struct-ref=E2=80=99,
=E2=80=98record-accessor=E2=80=99, etc., but these are abstraction-breaking=
 primitives
that should be avoided IMO).

How could this code be adjusted accordingly?  I guess you=E2=80=99re lookin=
g for
a way to iterate over fields?

> +;;; Elisp reader extension.
> +;;;
> +
> +(eval-when (expand load eval)
> +
> +  (define (read-elisp-extended port)
> +    (read-with-comments port
> +                        #:blank-line? #f
> +                        #:elisp? #t
> +                        #:unelisp-extensions? #t))
> +
> +  (define (read-elisp-expression chr port)
> +    `(elisp ,(read-elisp-extended port)))
> +
> +  (read-hash-extend #\% read-elisp-expression))

I=E2=80=99d lean towards not having a reader extension because they don=E2=
=80=99t
compose and it=E2=80=99s easy to end up colliding with another, unrelated
extension.  I think it=E2=80=99s okay if people write:

  (elisp =E2=80=A6)

rather than:

  #%(=E2=80=A6)

It=E2=80=99s also arguably easier to understand for a newcomer.

> +++ b/guix/read-print.scm

This part is the most =E2=80=9Cproblematic=E2=80=9D for me: I=E2=80=99m alr=
eady dissatisfied
with the current state of things (the pretty-printer in particular is
too complex and hard to work with), and this change brings more
complexity and lacks orthogonality.

What I=E2=80=99d like to see, ideally, is a clear separation between elisp
concerns and Scheme concerns in the reader and in the pretty printer.

Probably, a preliminary step (I could look into it) would be to rewrite
the pretty printer based on Wadler=E2=80=99s =E2=80=9Cprettier printer=E2=
=80=9D paper and/or
Shinn=E2=80=99s formatting combinators=C2=B9.

WDYT?

Thanks,
Ludo=E2=80=99.

=C2=B9 https://srfi.schemers.org/srfi-159/srfi-159.html




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 11 Oct 2023 16:16:49 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Wed Oct 11 12:16:49 2023
Received: from localhost ([127.0.0.1]:39899 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qqbtB-0006mS-HZ
	for submit <at> debbugs.gnu.org; Wed, 11 Oct 2023 12:16:49 -0400
Received: from eggs.gnu.org ([2001:470:142:3::10]:34802)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <ludo@HIDDEN>) id 1qqbt6-0006m3-ST
 for 64620 <at> debbugs.gnu.org; Wed, 11 Oct 2023 12:16:48 -0400
Received: from fencepost.gnu.org ([2001:470:142:3::e])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <ludo@HIDDEN>)
 id 1qqbsb-0001eM-8X; Wed, 11 Oct 2023 12:16:14 -0400
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org;
 s=fencepost-gnu-org; h=MIME-Version:Date:References:In-Reply-To:Subject:To:
 From; bh=lOKNXzrH0vEsxjto/WJ2mvceOOPukypQNo3OweYk58Y=; b=SH8Hy/yc3n1c40NVCJqW
 pV53JHOKzzpMxlATHutFCiNrdw73cr0YhprTuvQoTzCU42lvN3d/hUCGMypUna3n9PGFxH042JvB9
 LjADdEkdJrTuw0ClxzTQPRBFQk2nA1y2qkVifQcn71JbY7/JU+eJgvn7BmoRd4iKXS+PuouhQ/Ejq
 mRu9M/jMuhnrH5e6NYAKyFlecybY3QWoBdYCpK7F8WxGYmGR2DVyPe5eMUqrqrqY+oumjjSZ+fFOI
 T+1YnO69rdXEZCMRQf/YCMJd6HIA42xNWj9ockZVFvKVFulfoc7VvyNcwETP5u9WCrRWdDlQML+H3
 byQiWNNERPYk6A==;
From: =?utf-8?Q?Ludovic_Court=C3=A8s?= <ludo@HIDDEN>
To: Kierin Bell <fernseed@HIDDEN>
Subject: Re: [bug#64620] [PATCH] gnu: home: Add home-emacs-service-type.
In-Reply-To: <87o7iqpeou.fsf@HIDDEN> (paren@HIDDEN's message of
 "Tue, 29 Aug 2023 07:03:59 +0100")
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
 <87il945gzu.fsf@HIDDEN> <874jklyk38.fsf@HIDDEN>
 <873503hebs.fsf@HIDDEN> <87y1huvluu.fsf@HIDDEN>
 <87o7iqpeou.fsf@HIDDEN>
Date: Wed, 11 Oct 2023 18:16:10 +0200
Message-ID: <87a5spuoc5.fsf@HIDDEN>
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
X-Spam-Score: -2.3 (--)
X-Debbugs-Envelope-To: 64620
Cc: cox.katherine.e+guix@HIDDEN, "\(" <paren@HIDDEN>,
 Andrew Tropin <andrew@HIDDEN>, 64620 <at> debbugs.gnu.org,
 Liliana Marie Prikler <liliana.prikler@HIDDEN>
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -3.3 (---)

Hello!

If there=E2=80=99s consensus, I think we should go ahead with this patch se=
ries.
Worst that could happen is that people will think of ways to change the
service in one way or another, and that=E2=80=99s fine!

Two general comments:

  =E2=80=A2 As I wrote earlier, I think it=E2=80=99d be nice to have integr=
ation tests
    for this, in addition to the unit tests the patch already adds.

  =E2=80=A2 We may want to split the patch into sizable, self-contained bit=
es.
    For instance, the (guix read-print) changes should probably be
    separated out.

I=E2=80=99ll provide more specific comments about the code.

To Emacs team members: please review the Emacs bits of the series!

Thanks,
Ludo=E2=80=99.




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 29 Aug 2023 06:04:38 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Tue Aug 29 02:04:38 2023
Received: from localhost ([127.0.0.1]:49382 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qarq9-0001Dz-W8
	for submit <at> debbugs.gnu.org; Tue, 29 Aug 2023 02:04:38 -0400
Received: from layka.disroot.org ([178.21.23.139]:52372)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <paren@HIDDEN>) id 1qarq7-0001Dl-Tt
 for 64620 <at> debbugs.gnu.org; Tue, 29 Aug 2023 02:04:37 -0400
Received: from localhost (localhost [127.0.0.1])
 by disroot.org (Postfix) with ESMTP id 4FF2A41468;
 Tue, 29 Aug 2023 08:04:27 +0200 (CEST)
X-Virus-Scanned: SPAM Filter at disroot.org
Received: from layka.disroot.org ([127.0.0.1])
 by localhost (disroot.org [127.0.0.1]) (amavisd-new, port 10024)
 with ESMTP id 1liNN_EynG8e; Tue, 29 Aug 2023 08:04:26 +0200 (CEST)
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
 <87il945gzu.fsf@HIDDEN> <874jklyk38.fsf@HIDDEN>
 <873503hebs.fsf@HIDDEN> <87y1huvluu.fsf@HIDDEN>
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=disroot.org; s=mail;
 t=1693289066; bh=XkI6tqwCs8BAe0Qpe5ZZPTLIGBk9Y1YgTrAdt81nFLA=;
 h=References:From:To:Cc:Subject:Date:In-reply-to;
 b=lP5F4d8+35gfylnfzGwuGTV4VZdZeaK52MXb2q/zWH3zPQm++Sq3h4I55QAoT5jJJ
 jAODbg6bHuU+TjXjGI5Z63XJ6jBzf/ACRNbw2RZfr3F0m6SFcq5HrHPrsdlMHXI9Is
 gb5U3xZUetT0YmMEMl7PKnfn3wOZXsKyIQ5NSEZYChQxt4tQifbA5gUQutd/HEjkYC
 bKrZdcECghWHtfi6KXDT/yb/L9CP4ZAfMksRKjzoM6Ywkt70l2WKZL37AKLpGklT9B
 dxDsy5tpEkrMRd/LPc2tNQ1S/qMgAZwEhzQm/wZtbXT3yoxN6tSF3Ki+pWeipnywtn
 7exBebAFjfAyg==
From: "(" <paren@HIDDEN>
To: Kierin Bell <fernseed@HIDDEN>
Subject: Re: [bug#64620] [PATCH] gnu: home: Add home-emacs-service-type.
Date: Tue, 29 Aug 2023 07:03:59 +0100
In-reply-to: <87y1huvluu.fsf@HIDDEN>
Message-ID: <87o7iqpeou.fsf@HIDDEN>
MIME-Version: 1.0
Content-Type: text/plain
X-Spam-Score: 0.0 (/)
X-Debbugs-Envelope-To: 64620
Cc: 64620 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.0 (-)

Kierin Bell <fernseed@HIDDEN> writes:
> "(" <paren@HIDDEN> writes:
> Ah, got it!  The confusion was because I actually did borrow your
> NATIVE-COMP? field and package transformation idea.  They're already
> included in the patch!  (We still have to add custom profiles, though
> :))

Oops :)

  -- (




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 29 Aug 2023 05:57:22 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Tue Aug 29 01:57:21 2023
Received: from localhost ([127.0.0.1]:49374 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qarj7-0000zJ-8b
	for submit <at> debbugs.gnu.org; Tue, 29 Aug 2023 01:57:21 -0400
Received: from relay2-d.mail.gandi.net ([217.70.183.194]:47039)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <fernseed@HIDDEN>) id 1qaknL-0001ZK-Sv
 for 64620 <at> debbugs.gnu.org; Mon, 28 Aug 2023 18:33:16 -0400
Received: by mail.gandi.net (Postfix) with ESMTPSA id 4B0DA40009;
 Mon, 28 Aug 2023 22:33:01 +0000 (UTC)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=fernseed.me; s=gm1;
 t=1693261983;
 h=from:from:reply-to:subject:subject:date:date:message-id:message-id:
 to:to:cc:cc:mime-version:mime-version:content-type:content-type:
 in-reply-to:in-reply-to:references:references;
 bh=97UZesGnE/OM/fnV0ceh3pIfCLaIOuYe/LFNspIQoAU=;
 b=caf11sf3m9C+MbcqaupPju04M2/Fc1XmXOjMvsFrhQ5bKxphaZgupsdC1ioczKFM/qFo3k
 xgSfKWtZealTIQlnX42hryqpB/81OoG0fMU+JkJD3EZcOnqV0u2OulcmFdSmEVNQNQlf+f
 gz1/uMkqeUaf8qyMYqKe5cxAWiTj1GM2hg2LBvjKvw6hecXhXx/QMn3BubZOSb3psjuiee
 DBvW0Gch9V0g4+h/yYPao2A36avcvrnzLUly2671dfvSctngTDB+yaFsOrgmO3sEWS7AI7
 dVTmueYKaPCkjT/Nvdxq+QM8KyVPtV35PPag8sMihKEtJtr++C/nySeuJaPkrw==
From: Kierin Bell <fernseed@HIDDEN>
To: "(" <paren@HIDDEN>
Subject: Re: [bug#64620] [PATCH] gnu: home: Add home-emacs-service-type.
In-Reply-To: <873503hebs.fsf@HIDDEN> (paren@HIDDEN's message of
 "Mon, 28 Aug 2023 07:24:48 +0100")
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
 <87il945gzu.fsf@HIDDEN> <874jklyk38.fsf@HIDDEN>
 <873503hebs.fsf@HIDDEN>
Date: Mon, 28 Aug 2023 18:32:57 -0400
Message-ID: <87y1huvluu.fsf@HIDDEN>
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain
X-GND-Sasl: fernseed@HIDDEN
X-Spam-Score: -0.7 (/)
X-Debbugs-Envelope-To: 64620
X-Mailman-Approved-At: Tue, 29 Aug 2023 01:57:18 -0400
Cc: 64620 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.7 (-)

"(" <paren@HIDDEN> writes:

> Kierin Bell <fernseed@HIDDEN> writes:
>> Although there is an EXTRA-PACKAGES field for Guix packages (I use it,
>> e.g., for font packages) and a CONFIGURED-PACKAGES field specifically
>> for installing Emacs packages, I do like the idea of having a separate
>> Emacs profile, or even a separate profile for each Emacs server.
>
> Oh, nice.  That means native-comp could be supported quite easily, just
> by transforming the packages.  (I meant that easy native-comp was made
> possible by a PACKAGES field, not by having an Emacs profile :))
>
>> Looking at the Emacs build system modules, I'm not sure exactly how
>> building separate profiles would allow native-comp without transforming
>> the package inputs.  Isn't the build system basically hard-coded to use
>> `emacs-minimal' for building Emacs packages (i.e., unless we manually
>> transform the package inputs)?
>
> Yes, that's what I meant; map a transformation procedure over the Emacs
> packages given :)
>
>   -- (
>
>

Ah, got it!  The confusion was because I actually did borrow your
NATIVE-COMP? field and package transformation idea.  They're already
included in the patch!  (We still have to add custom profiles, though
:))

Thanks.

-- 
Kierin Bell
GPG Key: FCF2 5F08 EA4F 2E3D C7C3  0D41 D14A 8CD3 2D97 0B36




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 29 Aug 2023 05:57:21 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Tue Aug 29 01:57:21 2023
Received: from localhost ([127.0.0.1]:49372 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qarj5-0000zD-Ot
	for submit <at> debbugs.gnu.org; Tue, 29 Aug 2023 01:57:21 -0400
Received: from relay3-d.mail.gandi.net ([217.70.183.195]:60493)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <fernseed@HIDDEN>) id 1qaki7-0001Nz-So
 for 64620 <at> debbugs.gnu.org; Mon, 28 Aug 2023 18:27:53 -0400
Received: by mail.gandi.net (Postfix) with ESMTPSA id 0972660004;
 Mon, 28 Aug 2023 22:27:37 +0000 (UTC)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=fernseed.me; s=gm1;
 t=1693261658;
 h=from:from:reply-to:subject:subject:date:date:message-id:message-id:
 to:to:cc:cc:mime-version:mime-version:content-type:content-type:
 content-transfer-encoding:content-transfer-encoding:
 in-reply-to:in-reply-to:references:references;
 bh=ryWGn5JT9FKxDLJ7RCVk3dscen+8fp2drD/FKKy2g2w=;
 b=ko1oAM+oblHJOzMdUjMaTaDb+iH/3ItdN0WwpMUMbHcM1Hm6mQGv5Z8L/tHSEIMnI/6xFn
 2B+h54xpmN7Aqa/Sd4jwFTuT0gMv/PAX6Mmt4OFD0fIOdFF3D2Um45/QTjnBnhgL8LvJUo
 PFqbSSc7lZMWambGMt0sOZtRvVUmyU/eyVBA1WJJ3O4KABzD5K/ah6umjkaQG8B+9GIvzu
 N/idct7JRdjfIzXmXcRi1MBmgVpdulkef/Ad1MuTAKzDw+MZqHWDcBCjdB41eDoa4vVD55
 i/nGZdwTolZMUw37mo/w6qAM86TcCzmZtt1zkxxZVIvn+Qz03Rb9dzHNmXXSgw==
From: Kierin Bell <fernseed@HIDDEN>
To: Liliana Marie Prikler <liliana.prikler@HIDDEN>
Subject: Re: [PATCH] gnu: home: Add home-emacs-service-type.
In-Reply-To: <c8846b0bafd00ed0df93fd3ec3aa0cccfaefa90e.camel@HIDDEN>
 (Liliana Marie Prikler's message of "Sat, 26 Aug 2023 22:01:46 +0200")
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
 <c8846b0bafd00ed0df93fd3ec3aa0cccfaefa90e.camel@HIDDEN>
Date: Mon, 28 Aug 2023 18:27:33 -0400
Message-ID: <873502x0oa.fsf@HIDDEN>
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
X-GND-Sasl: fernseed@HIDDEN
X-Spam-Score: -0.7 (/)
X-Debbugs-Envelope-To: 64620
X-Mailman-Approved-At: Tue, 29 Aug 2023 01:57:18 -0400
Cc: 64620 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.7 (-)

Liliana Marie Prikler <liliana.prikler@HIDDEN> writes:

> Am Freitag, dem 14.07.2023 um 11:12 -0400 schrieb Kierin Bell
> <fernseed@HIDDEN>
>>=20
>> * gnu/home/services/emacs.scm: New file.
>> * gnu/local.mk (GNU_SYSTEM_MODULES): Add new file.
>> * tests/home/services/emacs.scm: New tests file.
>> * Makefile.am (SCM_TESTS): Add new tests file.
> AFAIK we use "Register" instead of "Add".

Got it; will use "Register" for changes to gnu/local.mk.

>> * doc/guix.texi (Emacs Home Services): New node.
> I'd cut this series into two (or more, see below) patches right around
> here, putting everything below this line into the first patch(es) and
> everything above into the second/nth.
>> * guix/read-print.scm (read-with-comments, read-with-
>> comments/sequence):
>> Add new ELISP? and UNELISP-EXTENSIONS? keyword arguments to support
>> reading Elisp.
>> (%newline-forms): Add `home-emacs-configuration'.
>> (%elisp-special-forms, %elisp-natural-whitespace-string-forms)
>> (%elisp-special-symbol-chars, %elisp-confusable-number-symbols)
>> (%elisp-basic-chars, %elisp-simple-escape-chars): New variables.
>> (special-form-lead, printed-string, symbol->display-string): Add new
>> ELISP? keyword argument.
>> (atom->elisp-string): New helper function.
>> (pretty-print-with-comments): New ELISP? and SPECIAL-FORMS keyword
>> arguments to support serialization to Elisp.=C2=A0=C2=A0
>
>> General improvements:
>> enable pretty-printing of alists and improper lists; only print lists
>> of
>> constants with one element per line when length exceeds LONG-LIST; do
>> not print newline before special read syntax forms (e.g., `'', `#~',
>> etc.) unless they would exceed MAX-WIDTH; include backslashes when
>> calculating whether a string would exceed MAX-WIDTH; do not print
>> extraneous newline when special form has an empty body; print
>> newlines
>> after list arguments of special forms; print first argument after
>> function on newline with same indentation as function when it would
>> exceed MAX-WIDTH.
>> * tests/read-print.scm: Add new tests and update old tests which fail
>> due to improvements.
> These general improvements should perhaps also been given their own
> patch(es).  Also, since read-print is used in guix style, I'd be
> interested in seeing how the output improves from your changes.  Do you
> have easy comparisons?
>

So when ready, I will open a new issue and send a patch series as per
the manual.

I'll also explore opening a separate issue for the "general
improvements" to (guix read-print) that are not strictly part of the
Elisp serialization functionality.  I'll try to find a way to clearly
annotate that patch with examples of each change and how it affects
output.

Many of the changes I'm calling "general improvements" seem to affect
Elisp output more than Scheme.  E.g., improper lists and alists aren't
used as extensively in Scheme, none of the defined %SPECIAL-FORMS for
Scheme accept list arguments in the right places or empty bodies, etc.

But you make a good point re: guix style.  I managed to contrive an
example package definition that demonstrates most of the changes.

Here is the output of guix style without the patch:

--8<---------------cut here---------------start------------->8---
(define-public foo
  (package
    (name "foo")
    ;; ...
    (arguments
     (list
      ;; *** (1) ***
      #:make-flags #~(list "VERBOSE=3D1"
                           #~more-flags "..."
                           #~(long-gexp-that-would-protrude-beyond-max-widt=
h))
      #:phases #~(modify-phases %standard-phases
                   (add-after 'install 'foo-fix
                     (lambda _
                       (substitute* "some-file"
                         (("match1")
                          ;; *** (2) ***
                          (string-join (list
                                        "list would protrude if not precede=
d by newline")))
                         (("match2")
                          "replacement")))))))
    ;; *** (3) ***
    (inputs `(,bar ,baz
              ,quux
              ,quuux
              ,quuuux
              ,quuuuux))
    (native-search-paths
     (list (search-path-specification
            (variable "FOO-PATH")
            (files '("foo-dir"))
            ;; *** (4) ***
            (file-pattern
                          "^string\\ with\\ backlashes\\ that\\ would\\ pro=
trude$")
            (file-type 'regular))))
    (properties '((tunable? . #t)
                  ;; *** (5) ***
                  (upstream-name . "foo-with-a-long-upstream-name-that-woul=
d-protrude")))
    ;; ...
    (license gpl3+)))
--8<---------------cut here---------------end--------------->8---


...And here it is with the patch:

--8<---------------cut here---------------start------------->8---
(define-public foo
  (package
    (name "foo")
    ;; ...
    (arguments
     (list
      ;; (1) No newline before special read syntaxes when they would not
      ;;     protrude beyond MAX-WIDTH.
      ;;
      ;;     [ Only relevant where a special read syntax occurs after
      ;;     the first argument in a function call and is not preceded
      ;;     by a keyword. ]
      #:make-flags #~(list "VERBOSE=3D1" #~more-flags "..."
                           #~(long-gexp-that-would-protrude-beyond-max-widt=
h))
      #:phases #~(modify-phases %standard-phases
                   (add-after 'install 'foo-fix
                     (lambda _
                       (substitute* "some-file"
                         (("match1")
                          ;; (2) Newline and proper indentation before
                          ;;     first argument of function call when
                          ;;     it would protrude beyond MAX-WIDTH.
                          ;;
                          ;;     [ Only relevant when first argument
                          ;;     of function call is a list that has
                          ;;     elements that would protrude beyond
                          ;;     MAX-WIDTH. ]
                          (string-join
                           (list
                            "list would protrude if not preceded by newline=
")))
                         ;; XXX: Should there be a newline after
                         ;; `("match2")'?  In Elisp, newlines like
                         ;; that seemed to get annoying, but perhaps
                         ;; it would actually be better here.
                         (("match2") "replacement")))))))
    ;; (3) Quoted lists longer than LONG-LIST with second element on
    ;;     its own line, like the remaining elements.
    ;;
    ;;     [ Fixes an obvious bug. ]
    (inputs `(,bar
              ,baz
              ,quux
              ,quuux
              ,quuuux
              ,quuuuux))
    (native-search-paths
     (list (search-path-specification
            (variable "FOO-PATH")
            (files '("foo-dir"))
            ;; (4) Newline and proper indentation before string with
            ;;     backslashes that would protrude.
            ;;
            ;;    [ Fixes obvious bug --- backslashes must be
            ;;    accounted for in strings to avoid weird issues. ]
            (file-pattern
             "^string\\ with\\ backlashes\\ that\\ would\\ protrude$")
            (file-type 'regular))))
    (properties '((tunable? . #t)
                  ;; (5) Newline before the dot and end of improper lists.
                  (upstream-name
                   . "foo-with-a-long-upstream-name-that-would-protrude")))
    ;; ...
    (license gpl3+)))
--8<---------------cut here---------------end--------------->8---

...Again, these improvements become much more important in a 2k line
Elisp init file.

>> ---
>>=20
>> This patch builds on patches from ( and David Wilson for a
>> `home-emacs-service-type' (https://issues.guix.gnu.org/58693,
>> https://issues.guix.gnu.org/60753,
>> https://issues.guix.gnu.org/62549).
>>=20
>> Many of the features of the prior patches have been included, but the
>> major focus here is to configure Emacs in Scheme rather than
>> symlinking
>> to existing configuration files.
>>=20
>> Here are some of the broad strokes:
>>=20
>> * The following record types have been introduced to encapsulate
>> =C2=A0 configuration for Emacs: `emacs-configuration' (for general
>> =C2=A0 configuration), `emacs-package' (for package-specific
>> configuration),
>> =C2=A0 `emacs-keymap' (for configuration of local keymaps), and
>> =C2=A0 `emacs-server' (for configuration of Emacs servers).
>>=20
>> * Most configuration fields are either flat lists or alists that are
>> =C2=A0 considerably abstracted from their final serialized Elisp
>> =C2=A0 representation, but escape hatches are provided for both pulling =
in
>> =C2=A0 existing configuration files and specifying s-expressions directl=
y.
>>=20
>> * All serialized Elisp is pretty-printed much how we would expect to
>> see
>> =C2=A0 it in Emacs (for example, with proper indentation according to the
>> =C2=A0 `lisp-indent-function' symbol property, etc.).=C2=A0 This has been
>> =C2=A0 accomplished by adding a new keyword argument to
>> =C2=A0 `pretty-print-with-comments' from `(guix read-print)', among other
>> =C2=A0 improvements.
>>=20
>> * Emacs package configuration can either be serialized as `use-
>> package'
>> =C2=A0 forms or as equivalent, more minimalist s-expressions.=C2=A0 User=
s can
>> =C2=A0 define their own package serializers, too.
>>=20
>> * For specifying s-expressions, an "Elisp expression" syntax has been
>> =C2=A0 implemented that is essentially a lighter-weight version G-
>> expressions.
>> =C2=A0 (I try to explain why this is helpful in the documentation.)
>>=20
>> * A reader extension has been implemented that allows for "Elisp
>> =C2=A0 expressions" to be specified directly with Elisp read syntax, and
>> =C2=A0 Scheme values (including file-like objects or G-expressions) can =
in
>> =C2=A0 turn be "unquoted" within that Elisp code.=C2=A0 Also, comments a=
nd
>> =C2=A0 whitespace can be included within the Elisp code via the `#;'
>> =C2=A0 (comment), `#>' (newline), and `;^L' (page break) forms.
>>=20
>> * Each Emacs server has its own user init and early init files, which
>> =C2=A0 can optionally inherit configuration from the init files used by
>> =C2=A0 non-server Emacsen.=C2=A0 Each server can also inherit the "main"
>> =C2=A0 `user-emacs-directory', or it can use its own subdirectory.
>>=20
>> * The `home-emacs-service-type' can be extended, with subordinate
>> =C2=A0 configuration records being merged intelligently when possible.
>>=20
>> * A utility function has been provided for generating the
>> aforementioned
>> =C2=A0 Scheme records from an existing Emacs init file:
>> =C2=A0 `elisp-file->home-emacs-configuration'.
>>=20
>> Here's an example configuration for the `home-emacs-service-type'
>> demonstrating some of these features:
>>=20
>> --8<---------------cut here---------------start------------->8---
>> (use-modules (gnu home)
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
 (gnu services)
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
 (guix gexp)
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
 (gnu home services)
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
 (gnu home services emacs)
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
 (gnu packages emacs-xyz)
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
 (gnu packages file)
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
 (gnu packages compression))
>>=20
>> (define %my-function-name 'my--compose-mail)
>>=20
>> (define %gnus-init-file
>> =C2=A0 (elisp-file "gnus.el"
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0 (list
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0 (elisp (setq gnus-select-method '(nnnil "")))
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0 (elisp (setq gnus-secondary-select-methods
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=
=A0=C2=A0=C2=A0 '((nnml "")
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=
=A0=C2=A0=C2=A0=C2=A0=C2=A0 (nntp "news.gmane.io"))))
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0 (elisp (setq mail-sources
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=
=A0=C2=A0=C2=A0 '((imap :server "mail.example.net"
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=
=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 :user "user=
@example.net"
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=
=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 :port 993
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=
=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 :stream tls=
))))
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0 ;; Elisp reader extension
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0 #%(define-key global-map [remap compose-mail]
>> #;comment
>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 '#$%my-function-name nil))))
> I assume that each elisp or #% only handles a single expression, am I
> correct?  Or do we also have (elisp a b) and #%@(a b)?
>

Yes, `elisp' and `#%' are just like `gexp' and `#~'.  `(elisp a b)'
would be a syntax error.  (And #%#$@(a b) is interesting; hadn't tried
that one :) --- but it doesn't work.)

I plan on adding a convenience macro `elisp*' so that (elisp* a b)
expands to (list (elisp a) (elisp b)).  This would make, e.g., the above
`elisp-file' invocation much nicer.

Thanks!

--=20
Kierin Bell
GPG Key: FCF2 5F08 EA4F 2E3D C7C3  0D41 D14A 8CD3 2D97 0B36




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 29 Aug 2023 04:24:23 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Tue Aug 29 00:24:23 2023
Received: from localhost ([127.0.0.1]:49331 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qaqH8-0003bt-7U
	for submit <at> debbugs.gnu.org; Tue, 29 Aug 2023 00:24:23 -0400
Received: from mail-ed1-x541.google.com ([2a00:1450:4864:20::541]:47149)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <liliana.prikler@HIDDEN>) id 1qaqH5-0003bY-Kw
 for 64620 <at> debbugs.gnu.org; Tue, 29 Aug 2023 00:24:20 -0400
Received: by mail-ed1-x541.google.com with SMTP id
 4fb4d7f45d1cf-52a06f5f556so5188955a12.2
 for <64620 <at> debbugs.gnu.org>; Mon, 28 Aug 2023 21:24:12 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=gmail.com; s=20221208; t=1693283046; x=1693887846;
 h=mime-version:user-agent:content-transfer-encoding:references
 :in-reply-to:date:cc:to:from:subject:message-id:from:to:cc:subject
 :date:message-id:reply-to;
 bh=Yfn7s74ZxdcN65/JIP/GbBPIHN/o0ec7CbkxdqhVbFo=;
 b=k3LDRbRNgGMQB3bKWG/ErDm8FAZ+eEJp5pkaUQPiYvY8i+zLKuewDpLs6bZLfmMYyT
 R4erjx5IpwQF+Nf5WTQQkyH56O5uZQobQBV8+osII9iF/1JnWBOIjBJyZ4sRu/y4RYn8
 KkTUz43eJseDP+YfnhGLYB6dcnR1Snd0RXaNZLAH3HhlffBo/LRUQEWpgqKqqnOpOQ69
 Y88FH9HGrXxmd3z3WNkmTySFdl3VEYu91GVx6+9AsgyJmp16U1qc0m20dWN90Im5RPfP
 djB6PRzorWWdNJk0gQh6bGr7sBchen1i/V1CKDeNp1qtqhOytHJPjJlFjiITqVF/F2N7
 LftA==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=1e100.net; s=20221208; t=1693283046; x=1693887846;
 h=mime-version:user-agent:content-transfer-encoding:references
 :in-reply-to:date:cc:to:from:subject:message-id:x-gm-message-state
 :from:to:cc:subject:date:message-id:reply-to;
 bh=Yfn7s74ZxdcN65/JIP/GbBPIHN/o0ec7CbkxdqhVbFo=;
 b=IcOetkfwShQEUgEU4CU9wQGKWGdbfyI6iyL1+GKgS4fF0jOw1ogjxl8RWrz0IB3qRh
 1TK2qz5jaBx73COB0iVNQIPq9bvsma3yBrd/wmVrUmr1cym/fKodojjKQx9D3hvjfa2F
 n2moq0F60qA4T2bkczA2sYB6uVv9k/qBA/FqtHHKhhmhl8DnwE33Hb0raAS+gKY3F91U
 l+A/YERIhZn5o0VDnYE5fl4ZcdMUgN9PIKUMKZuLvSm4MXpwaDf0coUYF5Sn1NcrRvER
 JWOQdyAQMPwG/V0sQvZLxELmaQY7xhMSzc/MALObuymsiYnjfBa0SJxXa72y0fu7MAmW
 ZqiA==
X-Gm-Message-State: AOJu0Yz7y4ylSsYs/PBG4GUUuNQfP1mY+wX1dlb8FOch1gD3HCx7iEOb
 wv/Ok0E/2vtrIXwW4ZHKcEFVJ/H/iKEvV9re
X-Google-Smtp-Source: AGHT+IEOfoIH5e1b/Mb852m+8RcJIIOCjKoqsjVAMQgn46+9vO/b8fIqRVydWqKJGojmVZlb6q940Q==
X-Received: by 2002:a05:6402:2051:b0:522:b1cb:e6c with SMTP id
 bc17-20020a056402205100b00522b1cb0e6cmr20142239edb.38.1693283046337; 
 Mon, 28 Aug 2023 21:24:06 -0700 (PDT)
Received: from lumine.fritz.box (85-127-52-93.dsl.dynamic.surfer.at.
 [85.127.52.93]) by smtp.gmail.com with ESMTPSA id
 r6-20020aa7d146000000b0051dd19d6d6esm5233899edo.73.2023.08.28.21.24.04
 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
 Mon, 28 Aug 2023 21:24:05 -0700 (PDT)
Message-ID: <6bc35e49d033ff9947a54599b0b83c6a3076fb83.camel@HIDDEN>
Subject: Re: [PATCH] gnu: home: Add home-emacs-service-type.
From: Liliana Marie Prikler <liliana.prikler@HIDDEN>
To: Kierin Bell <fernseed@HIDDEN>
Date: Tue, 29 Aug 2023 06:24:03 +0200
In-Reply-To: <873502x0oa.fsf@HIDDEN>
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
 <c8846b0bafd00ed0df93fd3ec3aa0cccfaefa90e.camel@HIDDEN>
 <873502x0oa.fsf@HIDDEN>
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: base64
User-Agent: Evolution 3.46.4 
MIME-Version: 1.0
X-Spam-Score: 0.0 (/)
X-Debbugs-Envelope-To: 64620
Cc: 64620 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.0 (-)

QW0gTW9udGFnLCBkZW0gMjguMDguMjAyMyB1bSAxODoyNyAtMDQwMCBzY2hyaWViIEtpZXJpbiBC
ZWxsOgo+ID4gVGhlc2UgZ2VuZXJhbCBpbXByb3ZlbWVudHMgc2hvdWxkIHBlcmhhcHMgYWxzbyBi
ZWVuIGdpdmVuIHRoZWlyIG93bgo+ID4gcGF0Y2goZXMpLsKgIEFsc28sIHNpbmNlIHJlYWQtcHJp
bnQgaXMgdXNlZCBpbiBndWl4IHN0eWxlLCBJJ2QgYmUKPiA+IGludGVyZXN0ZWQgaW4gc2VlaW5n
IGhvdyB0aGUgb3V0cHV0IGltcHJvdmVzIGZyb20geW91ciBjaGFuZ2VzLsKgIERvCj4gPiB5b3Ug
aGF2ZSBlYXN5IGNvbXBhcmlzb25zPwo+ID4gCj4gCj4gU28gd2hlbiByZWFkeSwgSSB3aWxsIG9w
ZW4gYSBuZXcgaXNzdWUgYW5kIHNlbmQgYSBwYXRjaCBzZXJpZXMgYXMgcGVyCj4gdGhlIG1hbnVh
bC4KTm8gbmVlZCwgeW91IGNhbiByZXVzZSB0aGlzIG9uZS4KCj4gSSdsbCBhbHNvIGV4cGxvcmUg
b3BlbmluZyBhIHNlcGFyYXRlIGlzc3VlIGZvciB0aGUgImdlbmVyYWwKPiBpbXByb3ZlbWVudHMi
IHRvIChndWl4IHJlYWQtcHJpbnQpIHRoYXQgYXJlIG5vdCBzdHJpY3RseSBwYXJ0IG9mIHRoZQo+
IEVsaXNwIHNlcmlhbGl6YXRpb24gZnVuY3Rpb25hbGl0eS7CoCBJJ2xsIHRyeSB0byBmaW5kIGEg
d2F5IHRvIGNsZWFybHkKPiBhbm5vdGF0ZSB0aGF0IHBhdGNoIHdpdGggZXhhbXBsZXMgb2YgZWFj
aCBjaGFuZ2UgYW5kIGhvdyBpdCBhZmZlY3RzCj4gb3V0cHV0LgpTYW1lIGhlcmUsIGp1c3QgdXNl
IC0tcmVyb2xsLWNvdW50PTIgb3IgLXYgMiBvbiBnaXQgc2VuZC1lbWFpbC4KCj4gTWFueSBvZiB0
aGUgY2hhbmdlcyBJJ20gY2FsbGluZyAiZ2VuZXJhbCBpbXByb3ZlbWVudHMiIHNlZW0gdG8gYWZm
ZWN0Cj4gRWxpc3Agb3V0cHV0IG1vcmUgdGhhbiBTY2hlbWUuwqAgRS5nLiwgaW1wcm9wZXIgbGlz
dHMgYW5kIGFsaXN0cwo+IGFyZW4ndCB1c2VkIGFzIGV4dGVuc2l2ZWx5IGluIFNjaGVtZSwgbm9u
ZSBvZiB0aGUgZGVmaW5lZCAlU1BFQ0lBTC0KPiBGT1JNUyBmb3IgU2NoZW1lIGFjY2VwdCBsaXN0
IGFyZ3VtZW50cyBpbiB0aGUgcmlnaHQgcGxhY2VzIG9yIGVtcHR5Cj4gYm9kaWVzLCBldGMuCj4g
Cj4gQnV0IHlvdSBtYWtlIGEgZ29vZCBwb2ludCByZTogZ3VpeCBzdHlsZS7CoCBJIG1hbmFnZWQg
dG8gY29udHJpdmUgYW4KPiBleGFtcGxlIHBhY2thZ2UgZGVmaW5pdGlvbiB0aGF0IGRlbW9uc3Ry
YXRlcyBtb3N0IG9mIHRoZSBjaGFuZ2VzLgo+IAo+IEhlcmUgaXMgdGhlIG91dHB1dCBvZiBndWl4
IHN0eWxlIHdpdGhvdXQgdGhlIHBhdGNoOgo+IAo+IC0tODwtLS0tLS0tLS0tLS0tLS1jdXQgaGVy
ZS0tLS0tLS0tLS0tLS0tLXN0YXJ0LS0tLS0tLS0tLS0tLT44LS0tCj4gKGRlZmluZS1wdWJsaWMg
Zm9vCj4gwqAgKHBhY2thZ2UKPiDCoMKgwqAgKG5hbWUgImZvbyIpCj4gwqDCoMKgIDs7IC4uLgo+
IMKgwqDCoCAoYXJndW1lbnRzCj4gwqDCoMKgwqAgKGxpc3QKPiDCoMKgwqDCoMKgIDs7ICoqKiAo
MSkgKioqCj4gwqDCoMKgwqDCoCAjOm1ha2UtZmxhZ3MgI34obGlzdCAiVkVSQk9TRT0xIgo+IMKg
wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgI35tb3Jl
LWZsYWdzICIuLi4iCj4gwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg
wqDCoMKgwqDCoCAjfihsb25nLWdleHAtdGhhdC13b3VsZC1wcm90cnVkZS1iZXlvbmQtCj4gbWF4
LXdpZHRoKSkKPiDCoMKgwqDCoMKgICM6cGhhc2VzICN+KG1vZGlmeS1waGFzZXMgJXN0YW5kYXJk
LXBoYXNlcwo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoYWRkLWFmdGVy
ICdpbnN0YWxsICdmb28tZml4Cj4gwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg
wqDCoCAobGFtYmRhIF8KPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg
wqDCoCAoc3Vic3RpdHV0ZSogInNvbWUtZmlsZSIKPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC
oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKCgibWF0Y2gxIikKPiDCoMKgwqDCoMKgwqDCoMKgwqDC
oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCA7OyAqKiogKDIpICoqKgo+IMKgwqDCoMKg
wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChzdHJpbmctam9pbiAo
bGlzdAo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg
wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAibGlzdCB3b3VsZCBwcm90cnVkZSBpZiBub3QK
PiBwcmVjZWRlZCBieSBuZXdsaW5lIikpKQo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC
oMKgwqDCoMKgwqDCoMKgwqDCoCAoKCJtYXRjaDIiKQo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC
oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJyZXBsYWNlbWVudCIpKSkpKSkpCj4gwqDCoMKg
IDs7ICoqKiAoMykgKioqCj4gwqDCoMKgIChpbnB1dHMgYCgsYmFyICxiYXoKPiDCoMKgwqDCoMKg
wqDCoMKgwqDCoMKgwqDCoCAscXV1eAo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICxxdXV1
eAo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICxxdXV1dXgKPiDCoMKgwqDCoMKgwqDCoMKg
wqDCoMKgwqDCoCAscXV1dXV1eCkpCj4gwqDCoMKgIChuYXRpdmUtc2VhcmNoLXBhdGhzCj4gwqDC
oMKgwqAgKGxpc3QgKHNlYXJjaC1wYXRoLXNwZWNpZmljYXRpb24KPiDCoMKgwqDCoMKgwqDCoMKg
wqDCoMKgICh2YXJpYWJsZSAiRk9PLVBBVEgiKQo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGZp
bGVzICcoImZvby1kaXIiKSkKPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIDs7ICoqKiAoNCkgKioq
Cj4gwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZmlsZS1wYXR0ZXJuCj4gwqDCoMKgwqDCoMKgwqDC
oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgIl5zdHJpbmdcXCB3aXRoXFwgYmFj
a2xhc2hlc1xcIHRoYXRcXAo+IHdvdWxkXFwgcHJvdHJ1ZGUkIikKPiDCoMKgwqDCoMKgwqDCoMKg
wqDCoMKgIChmaWxlLXR5cGUgJ3JlZ3VsYXIpKSkpCj4gwqDCoMKgIChwcm9wZXJ0aWVzICcoKHR1
bmFibGU/IC4gI3QpCj4gwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCA7OyAqKiog
KDUpICoqKgo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHVwc3RyZWFtLW5h
bWUgLiAiZm9vLXdpdGgtYS1sb25nLXVwc3RyZWFtLW5hbWUtCj4gdGhhdC13b3VsZC1wcm90cnVk
ZSIpKSkKPiDCoMKgwqAgOzsgLi4uCj4gwqDCoMKgIChsaWNlbnNlIGdwbDMrKSkpCj4gLS04PC0t
LS0tLS0tLS0tLS0tLWN1dCBoZXJlLS0tLS0tLS0tLS0tLS0tZW5kLS0tLS0tLS0tLS0tLS0tPjgt
LS0KPiAKPiAKPiAuLi5BbmQgaGVyZSBpdCBpcyB3aXRoIHRoZSBwYXRjaDoKPiAKPiAtLTg8LS0t
LS0tLS0tLS0tLS0tY3V0IGhlcmUtLS0tLS0tLS0tLS0tLS1zdGFydC0tLS0tLS0tLS0tLS0+OC0t
LQo+IChkZWZpbmUtcHVibGljIGZvbwo+IMKgIChwYWNrYWdlCj4gwqDCoMKgIChuYW1lICJmb28i
KQo+IMKgwqDCoCA7OyAuLi4KPiDCoMKgwqAgKGFyZ3VtZW50cwo+IMKgwqDCoMKgIChsaXN0Cj4g
wqDCoMKgwqDCoCA7OyAoMSkgTm8gbmV3bGluZSBiZWZvcmUgc3BlY2lhbCByZWFkIHN5bnRheGVz
IHdoZW4gdGhleSB3b3VsZAo+IG5vdAo+IMKgwqDCoMKgwqAgOzvCoMKgwqDCoCBwcm90cnVkZSBi
ZXlvbmQgTUFYLVdJRFRILgo+IMKgwqDCoMKgwqAgOzsKPiDCoMKgwqDCoMKgIDs7wqDCoMKgwqAg
WyBPbmx5IHJlbGV2YW50IHdoZXJlIGEgc3BlY2lhbCByZWFkIHN5bnRheCBvY2N1cnMgYWZ0ZXIK
PiDCoMKgwqDCoMKgIDs7wqDCoMKgwqAgdGhlIGZpcnN0IGFyZ3VtZW50IGluIGEgZnVuY3Rpb24g
Y2FsbCBhbmQgaXMgbm90Cj4gcHJlY2VkZWQKPiDCoMKgwqDCoMKgIDs7wqDCoMKgwqAgYnkgYSBr
ZXl3b3JkLiBdCj4gwqDCoMKgwqDCoCAjOm1ha2UtZmxhZ3MgI34obGlzdCAiVkVSQk9TRT0xIiAj
fm1vcmUtZmxhZ3MgIi4uLiIKPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC
oMKgwqDCoMKgwqDCoMKgICN+KGxvbmctZ2V4cC10aGF0LXdvdWxkLXByb3RydWRlLWJleW9uZC0K
PiBtYXgtd2lkdGgpKQo+IMKgwqDCoMKgwqAgIzpwaGFzZXMgI34obW9kaWZ5LXBoYXNlcyAlc3Rh
bmRhcmQtcGhhc2VzCj4gwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChhZGQt
YWZ0ZXIgJ2luc3RhbGwgJ2Zvby1maXgKPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC
oMKgwqDCoMKgIChsYW1iZGEgXwo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC
oMKgwqDCoMKgIChzdWJzdGl0dXRlKiAic29tZS1maWxlIgo+IMKgwqDCoMKgwqDCoMKgwqDCoMKg
wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoKCJtYXRjaDEiKQo+IMKgwqDCoMKgwqDCoMKg
wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIDs7ICgyKSBOZXdsaW5lIGFuZCBw
cm9wZXIgaW5kZW50YXRpb24KPiBiZWZvcmUKPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg
wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCA7O8KgwqDCoMKgIGZpcnN0IGFyZ3VtZW50IG9mIGZ1bmN0
aW9uIGNhbGwgd2hlbgo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC
oMKgwqDCoMKgIDs7wqDCoMKgwqAgaXQgd291bGQgcHJvdHJ1ZGUgYmV5b25kIE1BWC1XSURUSC4K
PiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCA7Owo+
IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIDs7wqDC
oMKgwqAgWyBPbmx5IHJlbGV2YW50IHdoZW4gZmlyc3QgYXJndW1lbnQKPiDCoMKgwqDCoMKgwqDC
oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCA7O8KgwqDCoMKgIG9mIGZ1bmN0
aW9uIGNhbGwgaXMgYSBsaXN0IHRoYXQgaGFzCj4gwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC
oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgOzvCoMKgwqDCoCBlbGVtZW50cyB0aGF0IHdvdWxkIHBy
b3RydWRlIGJleW9uZAo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC
oMKgwqDCoMKgIDs7wqDCoMKgwqAgTUFYLVdJRFRILiBdCj4gwqDCoMKgwqDCoMKgwqDCoMKgwqDC
oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHN0cmluZy1qb2luCj4gwqDCoMKgwqDCoMKg
wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdAo+IMKgwqDCoMKg
wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAibGlzdCB3b3Vs
ZCBwcm90cnVkZSBpZiBub3QgcHJlY2VkZWQgYnkKPiBuZXdsaW5lIikpKQo+IMKgwqDCoMKgwqDC
oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCA7OyBYWFg6IFNob3VsZCB0aGVy
ZSBiZSBhIG5ld2xpbmUgYWZ0ZXIKPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg
wqDCoMKgwqDCoMKgwqAgOzsgYCgibWF0Y2gyIiknP8KgIEluIEVsaXNwLCBuZXdsaW5lcyBsaWtl
Cj4gwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIDs7IHRo
YXQgc2VlbWVkIHRvIGdldCBhbm5veWluZywgYnV0IHBlcmhhcHMKPiDCoMKgwqDCoMKgwqDCoMKg
wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgOzsgaXQgd291bGQgYWN0dWFsbHkgYmUg
YmV0dGVyIGhlcmUuCj4gwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg
wqDCoMKgICgoIm1hdGNoMiIpICJyZXBsYWNlbWVudCIpKSkpKSkpCj4gwqDCoMKgIDs7ICgzKSBR
dW90ZWQgbGlzdHMgbG9uZ2VyIHRoYW4gTE9ORy1MSVNUIHdpdGggc2Vjb25kIGVsZW1lbnQgb24K
PiDCoMKgwqAgOzvCoMKgwqDCoCBpdHMgb3duIGxpbmUsIGxpa2UgdGhlIHJlbWFpbmluZyBlbGVt
ZW50cy4KPiDCoMKgwqAgOzsKPiDCoMKgwqAgOzvCoMKgwqDCoCBbIEZpeGVzIGFuIG9idmlvdXMg
YnVnLiBdCj4gwqDCoMKgIChpbnB1dHMgYCgsYmFyCj4gwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg
wqAgLGJhego+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICxxdXV4Cj4gwqDCoMKgwqDCoMKg
wqDCoMKgwqDCoMKgwqAgLHF1dXV4Cj4gwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgLHF1dXV1
eAo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICxxdXV1dXV4KSkKPiDCoMKgwqAgKG5hdGl2
ZS1zZWFyY2gtcGF0aHMKPiDCoMKgwqDCoCAobGlzdCAoc2VhcmNoLXBhdGgtc3BlY2lmaWNhdGlv
bgo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHZhcmlhYmxlICJGT08tUEFUSCIpCj4gwqDCoMKg
wqDCoMKgwqDCoMKgwqDCoCAoZmlsZXMgJygiZm9vLWRpciIpKQo+IMKgwqDCoMKgwqDCoMKgwqDC
oMKgwqAgOzsgKDQpIE5ld2xpbmUgYW5kIHByb3BlciBpbmRlbnRhdGlvbiBiZWZvcmUgc3RyaW5n
IHdpdGgKPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIDs7wqDCoMKgwqAgYmFja3NsYXNoZXMgdGhh
dCB3b3VsZCBwcm90cnVkZS4KPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIDs7Cj4gwqDCoMKgwqDC
oMKgwqDCoMKgwqDCoCA7O8KgwqDCoCBbIEZpeGVzIG9idmlvdXMgYnVnIC0tLSBiYWNrc2xhc2hl
cyBtdXN0IGJlCj4gwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCA7O8KgwqDCoCBhY2NvdW50ZWQgZm9y
IGluIHN0cmluZ3MgdG8gYXZvaWQgd2VpcmQgaXNzdWVzLiBdCj4gwqDCoMKgwqDCoMKgwqDCoMKg
wqDCoCAoZmlsZS1wYXR0ZXJuCj4gwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgICJec3RyaW5nXFwg
d2l0aFxcIGJhY2tsYXNoZXNcXCB0aGF0XFwgd291bGRcXAo+IHByb3RydWRlJCIpCj4gwqDCoMKg
wqDCoMKgwqDCoMKgwqDCoCAoZmlsZS10eXBlICdyZWd1bGFyKSkpKQo+IMKgwqDCoCAocHJvcGVy
dGllcyAnKCh0dW5hYmxlPyAuICN0KQo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg
wqAgOzsgKDUpIE5ld2xpbmUgYmVmb3JlIHRoZSBkb3QgYW5kIGVuZCBvZiBpbXByb3Blcgo+IGxp
c3RzLgo+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKHVwc3RyZWFtLW5hbWUK
PiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgLiAiZm9vLXdpdGgtYS1sb25n
LXVwc3RyZWFtLW5hbWUtdGhhdC13b3VsZC0KPiBwcm90cnVkZSIpKSkKPiDCoMKgwqAgOzsgLi4u
Cj4gwqDCoMKgIChsaWNlbnNlIGdwbDMrKSkpCj4gLS04PC0tLS0tLS0tLS0tLS0tLWN1dCBoZXJl
LS0tLS0tLS0tLS0tLS0tZW5kLS0tLS0tLS0tLS0tLS0tPjgtLS0KPiAKPiAuLi5BZ2FpbiwgdGhl
c2UgaW1wcm92ZW1lbnRzIGJlY29tZSBtdWNoIG1vcmUgaW1wb3J0YW50IGluIGEgMmsgbGluZQo+
IEVsaXNwIGluaXQgZmlsZS4KSSdkIGltYWdpbmUsIGJ1dCB0aGVyZSBhcmUgc29tZSBnb29kIHRo
aW5ncyBpbiBpdCBmb3IgKGd1aXggc3R5bGUpIHRvby4KU29tZSBidWdzIHN0aWxsIHJlbWFpbiwg
bGlrZSBub3Qgc3BsaXR0aW5nIGFmdGVyICM6cGhhc2VzIHRvIGtlZXAgbG9uZwpsaW5lcyBpbiBj
aGVjaywgYnV0IHRoYXQncyBiZXlvbmQgdGhlIHNjb3BlLgoKPiA+ID4gLS0tCj4gPiA+IAo+ID4g
PiBUaGlzIHBhdGNoIGJ1aWxkcyBvbiBwYXRjaGVzIGZyb20gKCBhbmQgRGF2aWQgV2lsc29uIGZv
ciBhCj4gPiA+IGBob21lLWVtYWNzLXNlcnZpY2UtdHlwZScgKGh0dHBzOi8vaXNzdWVzLmd1aXgu
Z251Lm9yZy81ODY5MywKPiA+ID4gaHR0cHM6Ly9pc3N1ZXMuZ3VpeC5nbnUub3JnLzYwNzUzLAo+
ID4gPiBodHRwczovL2lzc3Vlcy5ndWl4LmdudS5vcmcvNjI1NDkpLgo+ID4gPiAKPiA+ID4gTWFu
eSBvZiB0aGUgZmVhdHVyZXMgb2YgdGhlIHByaW9yIHBhdGNoZXMgaGF2ZSBiZWVuIGluY2x1ZGVk
LCBidXQKPiA+ID4gdGhlCj4gPiA+IG1ham9yIGZvY3VzIGhlcmUgaXMgdG8gY29uZmlndXJlIEVt
YWNzIGluIFNjaGVtZSByYXRoZXIgdGhhbgo+ID4gPiBzeW1saW5raW5nCj4gPiA+IHRvIGV4aXN0
aW5nIGNvbmZpZ3VyYXRpb24gZmlsZXMuCj4gPiA+IAo+ID4gPiBIZXJlIGFyZSBzb21lIG9mIHRo
ZSBicm9hZCBzdHJva2VzOgo+ID4gPiAKPiA+ID4gKiBUaGUgZm9sbG93aW5nIHJlY29yZCB0eXBl
cyBoYXZlIGJlZW4gaW50cm9kdWNlZCB0byBlbmNhcHN1bGF0ZQo+ID4gPiDCoCBjb25maWd1cmF0
aW9uIGZvciBFbWFjczogYGVtYWNzLWNvbmZpZ3VyYXRpb24nIChmb3IgZ2VuZXJhbAo+ID4gPiDC
oCBjb25maWd1cmF0aW9uKSwgYGVtYWNzLXBhY2thZ2UnIChmb3IgcGFja2FnZS1zcGVjaWZpYwo+
ID4gPiBjb25maWd1cmF0aW9uKSwKPiA+ID4gwqAgYGVtYWNzLWtleW1hcCcgKGZvciBjb25maWd1
cmF0aW9uIG9mIGxvY2FsIGtleW1hcHMpLCBhbmQKPiA+ID4gwqAgYGVtYWNzLXNlcnZlcicgKGZv
ciBjb25maWd1cmF0aW9uIG9mIEVtYWNzIHNlcnZlcnMpLgo+ID4gPiAKPiA+ID4gKiBNb3N0IGNv
bmZpZ3VyYXRpb24gZmllbGRzIGFyZSBlaXRoZXIgZmxhdCBsaXN0cyBvciBhbGlzdHMgdGhhdAo+
ID4gPiBhcmUKPiA+ID4gwqAgY29uc2lkZXJhYmx5IGFic3RyYWN0ZWQgZnJvbSB0aGVpciBmaW5h
bCBzZXJpYWxpemVkIEVsaXNwCj4gPiA+IMKgIHJlcHJlc2VudGF0aW9uLCBidXQgZXNjYXBlIGhh
dGNoZXMgYXJlIHByb3ZpZGVkIGZvciBib3RoCj4gPiA+IHB1bGxpbmcgaW4KPiA+ID4gwqAgZXhp
c3RpbmcgY29uZmlndXJhdGlvbiBmaWxlcyBhbmQgc3BlY2lmeWluZyBzLWV4cHJlc3Npb25zCj4g
PiA+IGRpcmVjdGx5Lgo+ID4gPiAKPiA+ID4gKiBBbGwgc2VyaWFsaXplZCBFbGlzcCBpcyBwcmV0
dHktcHJpbnRlZCBtdWNoIGhvdyB3ZSB3b3VsZCBleHBlY3QKPiA+ID4gdG8KPiA+ID4gc2VlCj4g
PiA+IMKgIGl0IGluIEVtYWNzIChmb3IgZXhhbXBsZSwgd2l0aCBwcm9wZXIgaW5kZW50YXRpb24g
YWNjb3JkaW5nIHRvCj4gPiA+IHRoZQo+ID4gPiDCoCBgbGlzcC1pbmRlbnQtZnVuY3Rpb24nIHN5
bWJvbCBwcm9wZXJ0eSwgZXRjLikuwqAgVGhpcyBoYXMgYmVlbgo+ID4gPiDCoCBhY2NvbXBsaXNo
ZWQgYnkgYWRkaW5nIGEgbmV3IGtleXdvcmQgYXJndW1lbnQgdG8KPiA+ID4gwqAgYHByZXR0eS1w
cmludC13aXRoLWNvbW1lbnRzJyBmcm9tIGAoZ3VpeCByZWFkLXByaW50KScsIGFtb25nCj4gPiA+
IG90aGVyCj4gPiA+IMKgIGltcHJvdmVtZW50cy4KPiA+ID4gCj4gPiA+ICogRW1hY3MgcGFja2Fn
ZSBjb25maWd1cmF0aW9uIGNhbiBlaXRoZXIgYmUgc2VyaWFsaXplZCBhcyBgdXNlLQo+ID4gPiBw
YWNrYWdlJwo+ID4gPiDCoCBmb3JtcyBvciBhcyBlcXVpdmFsZW50LCBtb3JlIG1pbmltYWxpc3Qg
cy1leHByZXNzaW9ucy7CoCBVc2Vycwo+ID4gPiBjYW4KPiA+ID4gwqAgZGVmaW5lIHRoZWlyIG93
biBwYWNrYWdlIHNlcmlhbGl6ZXJzLCB0b28uCj4gPiA+IAo+ID4gPiAqIEZvciBzcGVjaWZ5aW5n
IHMtZXhwcmVzc2lvbnMsIGFuICJFbGlzcCBleHByZXNzaW9uIiBzeW50YXggaGFzCj4gPiA+IGJl
ZW4KPiA+ID4gwqAgaW1wbGVtZW50ZWQgdGhhdCBpcyBlc3NlbnRpYWxseSBhIGxpZ2h0ZXItd2Vp
Z2h0IHZlcnNpb24gRy0KPiA+ID4gZXhwcmVzc2lvbnMuCj4gPiA+IMKgIChJIHRyeSB0byBleHBs
YWluIHdoeSB0aGlzIGlzIGhlbHBmdWwgaW4gdGhlIGRvY3VtZW50YXRpb24uKQo+ID4gPiAKPiA+
ID4gKiBBIHJlYWRlciBleHRlbnNpb24gaGFzIGJlZW4gaW1wbGVtZW50ZWQgdGhhdCBhbGxvd3Mg
Zm9yICJFbGlzcAo+ID4gPiDCoCBleHByZXNzaW9ucyIgdG8gYmUgc3BlY2lmaWVkIGRpcmVjdGx5
IHdpdGggRWxpc3AgcmVhZCBzeW50YXgsCj4gPiA+IGFuZAo+ID4gPiDCoCBTY2hlbWUgdmFsdWVz
IChpbmNsdWRpbmcgZmlsZS1saWtlIG9iamVjdHMgb3IgRy1leHByZXNzaW9ucykKPiA+ID4gY2Fu
IGluCj4gPiA+IMKgIHR1cm4gYmUgInVucXVvdGVkIiB3aXRoaW4gdGhhdCBFbGlzcCBjb2RlLsKg
IEFsc28sIGNvbW1lbnRzIGFuZAo+ID4gPiDCoCB3aGl0ZXNwYWNlIGNhbiBiZSBpbmNsdWRlZCB3
aXRoaW4gdGhlIEVsaXNwIGNvZGUgdmlhIHRoZSBgIzsnCj4gPiA+IMKgIChjb21tZW50KSwgYCM+
JyAobmV3bGluZSksIGFuZCBgO15MJyAocGFnZSBicmVhaykgZm9ybXMuCj4gPiA+IAo+ID4gPiAq
IEVhY2ggRW1hY3Mgc2VydmVyIGhhcyBpdHMgb3duIHVzZXIgaW5pdCBhbmQgZWFybHkgaW5pdCBm
aWxlcywKPiA+ID4gd2hpY2gKPiA+ID4gwqAgY2FuIG9wdGlvbmFsbHkgaW5oZXJpdCBjb25maWd1
cmF0aW9uIGZyb20gdGhlIGluaXQgZmlsZXMgdXNlZAo+ID4gPiBieQo+ID4gPiDCoCBub24tc2Vy
dmVyIEVtYWNzZW4uwqAgRWFjaCBzZXJ2ZXIgY2FuIGFsc28gaW5oZXJpdCB0aGUgIm1haW4iCj4g
PiA+IMKgIGB1c2VyLWVtYWNzLWRpcmVjdG9yeScsIG9yIGl0IGNhbiB1c2UgaXRzIG93biBzdWJk
aXJlY3RvcnkuCj4gPiA+IAo+ID4gPiAqIFRoZSBgaG9tZS1lbWFjcy1zZXJ2aWNlLXR5cGUnIGNh
biBiZSBleHRlbmRlZCwgd2l0aCBzdWJvcmRpbmF0ZQo+ID4gPiDCoCBjb25maWd1cmF0aW9uIHJl
Y29yZHMgYmVpbmcgbWVyZ2VkIGludGVsbGlnZW50bHkgd2hlbiBwb3NzaWJsZS4KPiA+ID4gCj4g
PiA+ICogQSB1dGlsaXR5IGZ1bmN0aW9uIGhhcyBiZWVuIHByb3ZpZGVkIGZvciBnZW5lcmF0aW5n
IHRoZQo+ID4gPiBhZm9yZW1lbnRpb25lZAo+ID4gPiDCoCBTY2hlbWUgcmVjb3JkcyBmcm9tIGFu
IGV4aXN0aW5nIEVtYWNzIGluaXQgZmlsZToKPiA+ID4gwqAgYGVsaXNwLWZpbGUtPmhvbWUtZW1h
Y3MtY29uZmlndXJhdGlvbicuCj4gPiA+IAo+ID4gPiBIZXJlJ3MgYW4gZXhhbXBsZSBjb25maWd1
cmF0aW9uIGZvciB0aGUgYGhvbWUtZW1hY3Mtc2VydmljZS10eXBlJwo+ID4gPiBkZW1vbnN0cmF0
aW5nIHNvbWUgb2YgdGhlc2UgZmVhdHVyZXM6Cj4gPiA+IAo+ID4gPiAtLTg8LS0tLS0tLS0tLS0t
LS0tY3V0IGhlcmUtLS0tLS0tLS0tLS0tLS1zdGFydC0tLS0tLS0tLS0tLS0+OC0tLQo+ID4gPiAo
dXNlLW1vZHVsZXMgKGdudSBob21lKQo+ID4gPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGdu
dSBzZXJ2aWNlcykKPiA+ID4gwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChndWl4IGdleHApCj4g
PiA+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZ251IGhvbWUgc2VydmljZXMpCj4gPiA+IMKg
wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZ251IGhvbWUgc2VydmljZXMgZW1hY3MpCj4gPiA+IMKg
wqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAoZ251IHBhY2thZ2VzIGVtYWNzLXh5eikKPiA+ID4gwqDC
oMKgwqDCoMKgwqDCoMKgwqDCoMKgIChnbnUgcGFja2FnZXMgZmlsZSkKPiA+ID4gwqDCoMKgwqDC
oMKgwqDCoMKgwqDCoMKgIChnbnUgcGFja2FnZXMgY29tcHJlc3Npb24pKQo+ID4gPiAKPiA+ID4g
KGRlZmluZSAlbXktZnVuY3Rpb24tbmFtZSAnbXktLWNvbXBvc2UtbWFpbCkKPiA+ID4gCj4gPiA+
IChkZWZpbmUgJWdudXMtaW5pdC1maWxlCj4gPiA+IMKgIChlbGlzcC1maWxlICJnbnVzLmVsIgo+
ID4gPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobGlzdAo+ID4gPiDCoMKgwqDCoMKgwqDC
oMKgwqDCoMKgwqDCoMKgIChlbGlzcCAoc2V0cSBnbnVzLXNlbGVjdC1tZXRob2QgJyhubm5pbCAi
IikpKQo+ID4gPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIChlbGlzcCAoc2V0cSBnbnVz
LXNlY29uZGFyeS1zZWxlY3QtbWV0aG9kcwo+ID4gPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC
oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgJygobm5tbCAiIikKPiA+ID4gwqDCoMKgwqDC
oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAobm50cCAi
bmV3cy5nbWFuZS5pbyIpKSkpCj4gPiA+IMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgKGVs
aXNwIChzZXRxIG1haWwtc291cmNlcwo+ID4gPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg
wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAgJygoaW1hcCA6c2VydmVyICJtYWlsLmV4YW1wbGUu
bmV0Igo+ID4gPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg
wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIDp1c2VyICJ1c2VyQGV4YW1wbGUubmV0Igo+ID4gPiDC
oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg
wqDCoMKgwqDCoMKgIDpwb3J0IDk5Mwo+ID4gPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg
wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIDpzdHJlYW0gdGxzKSkp
KQo+ID4gPiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgIDs7IEVsaXNwIHJlYWRlciBleHRl
bnNpb24KPiA+ID4gwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAjJShkZWZpbmUta2V5IGds
b2JhbC1tYXAgW3JlbWFwIGNvbXBvc2UtbWFpbF0KPiA+ID4gIztjb21tZW50Cj4gPiA+IMKgwqDC
oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoCAnIyQlbXktZnVuY3Rpb24tbmFtZSBuaWwp
KSkpCj4gPiBJIGFzc3VtZSB0aGF0IGVhY2ggZWxpc3Agb3IgIyUgb25seSBoYW5kbGVzIGEgc2lu
Z2xlIGV4cHJlc3Npb24sIGFtCj4gPiBJIGNvcnJlY3Q/wqAgT3IgZG8gd2UgYWxzbyBoYXZlIChl
bGlzcCBhIGIpIGFuZCAjJUAoYSBiKT8KPiA+IAo+IAo+IFllcywgYGVsaXNwJyBhbmQgYCMlJyBh
cmUganVzdCBsaWtlIGBnZXhwJyBhbmQgYCN+Jy7CoCBgKGVsaXNwIGEgYiknCj4gd291bGQgYmUg
YSBzeW50YXggZXJyb3IuwqAgKEFuZCAjJSMkQChhIGIpIGlzIGludGVyZXN0aW5nOyBoYWRuJ3QK
PiB0cmllZCB0aGF0IG9uZSA6KSAtLS0gYnV0IGl0IGRvZXNuJ3Qgd29yay4pCj4gCj4gSSBwbGFu
IG9uIGFkZGluZyBhIGNvbnZlbmllbmNlIG1hY3JvIGBlbGlzcConIHNvIHRoYXQgKGVsaXNwKiBh
IGIpCj4gZXhwYW5kcyB0byAobGlzdCAoZWxpc3AgYSkgKGVsaXNwIGIpKS7CoCBUaGlzIHdvdWxk
IG1ha2UsIGUuZy4sIHRoZQo+IGFib3ZlIGBlbGlzcC1maWxlJyBpbnZvY2F0aW9uIG11Y2ggbmlj
ZXIuClNHVE0uCgpDaGVlcnMK





Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 28 Aug 2023 06:27:17 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Mon Aug 28 02:27:17 2023
Received: from localhost ([127.0.0.1]:46752 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qaViW-0008AK-MX
	for submit <at> debbugs.gnu.org; Mon, 28 Aug 2023 02:27:16 -0400
Received: from layka.disroot.org ([178.21.23.139]:54206)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <paren@HIDDEN>) id 1qaViS-0008A7-7u
 for 64620 <at> debbugs.gnu.org; Mon, 28 Aug 2023 02:27:15 -0400
Received: from localhost (localhost [127.0.0.1])
 by disroot.org (Postfix) with ESMTP id 74525413F3;
 Mon, 28 Aug 2023 08:27:04 +0200 (CEST)
X-Virus-Scanned: SPAM Filter at disroot.org
Received: from layka.disroot.org ([127.0.0.1])
 by localhost (disroot.org [127.0.0.1]) (amavisd-new, port 10024)
 with ESMTP id Dj68nf4tssVX; Mon, 28 Aug 2023 08:27:03 +0200 (CEST)
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
 <87il945gzu.fsf@HIDDEN> <874jklyk38.fsf@HIDDEN>
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=disroot.org; s=mail;
 t=1693204023; bh=pBAowQtZf3HacfVZC8EXspIJ8q0FNv01pyeaxMaDo9g=;
 h=References:From:To:Cc:Subject:Date:In-reply-to;
 b=ii2JnpFZWPiyv6Ml2Te+Qut0t2a3kkryFwRazp1he+Z4mzOqrx3XurG9lJVnlUp02
 SNo64fHxIZ1B0crqvYVREHsz9GYx3Jn3ikUvwRc4YLPc8suMtx3GtuqycLfjayCEHf
 l7Iw920u1ETXFkqRE78A6R6b/NEVOCs2emS95jEfMt45KXdGH2BLCZitumWswNAH6V
 seOVhZAUd+HD/r3b2LmkAbipAVmw9uXBmxAc/6CRDqr3r0+/Y58p0nI9PY/XVVXmDN
 HQoV8PIfEckm7/rL0wzulAnMwg+Dvr3R7FMz37oyOPkbPZTdzZCsVualCQ3E4UOVYs
 lM4Hz3qh3AF0Q==
From: "(" <paren@HIDDEN>
To: Kierin Bell <fernseed@HIDDEN>
Subject: Re: [bug#64620] [PATCH] gnu: home: Add home-emacs-service-type.
Date: Mon, 28 Aug 2023 07:24:48 +0100
In-reply-to: <874jklyk38.fsf@HIDDEN>
Message-ID: <873503hebs.fsf@HIDDEN>
MIME-Version: 1.0
Content-Type: text/plain
X-Spam-Score: 0.0 (/)
X-Debbugs-Envelope-To: 64620
Cc: 64620 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.0 (-)

Kierin Bell <fernseed@HIDDEN> writes:
> Although there is an EXTRA-PACKAGES field for Guix packages (I use it,
> e.g., for font packages) and a CONFIGURED-PACKAGES field specifically
> for installing Emacs packages, I do like the idea of having a separate
> Emacs profile, or even a separate profile for each Emacs server.

Oh, nice.  That means native-comp could be supported quite easily, just
by transforming the packages.  (I meant that easy native-comp was made
possible by a PACKAGES field, not by having an Emacs profile :))

> Looking at the Emacs build system modules, I'm not sure exactly how
> building separate profiles would allow native-comp without transforming
> the package inputs.  Isn't the build system basically hard-coded to use
> `emacs-minimal' for building Emacs packages (i.e., unless we manually
> transform the package inputs)?

Yes, that's what I meant; map a transformation procedure over the Emacs
packages given :)

  -- (




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 26 Aug 2023 20:02:08 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Aug 26 16:02:08 2023
Received: from localhost ([127.0.0.1]:43492 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qZzTx-0000x3-R9
	for submit <at> debbugs.gnu.org; Sat, 26 Aug 2023 16:02:08 -0400
Received: from mail-ed1-x541.google.com ([2a00:1450:4864:20::541]:48208)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <liliana.prikler@HIDDEN>) id 1qZzTs-0000wV-Vl
 for 64620 <at> debbugs.gnu.org; Sat, 26 Aug 2023 16:02:04 -0400
Received: by mail-ed1-x541.google.com with SMTP id
 4fb4d7f45d1cf-52683b68c2fso2782549a12.0
 for <64620 <at> debbugs.gnu.org>; Sat, 26 Aug 2023 13:01:55 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=gmail.com; s=20221208; t=1693080109; x=1693684909;
 h=mime-version:user-agent:content-transfer-encoding:references
 :in-reply-to:date:to:from:subject:message-id:from:to:cc:subject:date
 :message-id:reply-to;
 bh=Vo+9dB/zoPxbf+fx+J7lyK+EnhZjIXzga2vDzMkVjUA=;
 b=jg1zbPMpY7P2eisik/ydLu5PrgJSWJ24R3Po0OMi0XxjJVj3fndXw2+1XurRthxSQV
 vL1WOKJJ6RBpNzD2/aLzJKmkprRlMnK11fWJfK00xAl3kWCrHQimkrsPp7NC8IAb+3aM
 n2OY7gSmedbwJpmRcIPQV9/lYUaksebBPy3UEzYkVsWW1I9RQoVjJrAuWFOjEr+sEzn0
 OkUjhuwcXEBBUGDFme4YDhEj8erksxKsf51kMgwSrx71j5uODeUhCfWjOkMnuZYDZWrZ
 FrLUjybUgDI2pIq+ioiq1JJTn6ePbHwRW6/5qSy+xYUiW8DNnZuAvg3NVX8LzF4CKFQg
 n/GQ==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=1e100.net; s=20221208; t=1693080109; x=1693684909;
 h=mime-version:user-agent:content-transfer-encoding:references
 :in-reply-to:date:to:from:subject:message-id:x-gm-message-state:from
 :to:cc:subject:date:message-id:reply-to;
 bh=Vo+9dB/zoPxbf+fx+J7lyK+EnhZjIXzga2vDzMkVjUA=;
 b=DPFpQEYKxDB4G2QjKq+t2RYn6Cua+hXbTD9yfX9iQlp9USt0d+ddrtCMai87/28jkQ
 ysJVh9NVoo9MQrMAZWjYQMsJSySj2lgSuHzftuo/2VGVn5fBOXY+PxKNGt6n7snG7UWk
 0m+YLPI04LeqdnlMJosbwD9tuuDcWb9mTVa5e3gNDWPh839JCBBmICE44DEeejcSN8U2
 mC/HVLX23VW05UsduRuuzk7439EOH9ILfCveXASLOLqa3vpvJ436wo/DEcDdEuoMIYlE
 bdrgnTzsHcQFKxEz9J0NCppJGstPWGNmF40OtGVBiLfVlSom+Erpi0GcbzApQyjR8NXc
 nqAg==
X-Gm-Message-State: AOJu0Yzl2A9CioPbHZi88OcWbB6RngrR1xGouJQJiiXjMfkefehWaoBZ
 VnwlE4vPbBz66jxgjDR4UJursa1cuIGm9A==
X-Google-Smtp-Source: AGHT+IGjSbVDX4HW8L1t8KNEVpmMHenTveRNJbE5y7pbG1kyeFJv9FEBG4lf0YhEFIZO9jn7s9tMyw==
X-Received: by 2002:aa7:d5c5:0:b0:523:1410:b611 with SMTP id
 d5-20020aa7d5c5000000b005231410b611mr16261415eds.25.1693080108748; 
 Sat, 26 Aug 2023 13:01:48 -0700 (PDT)
Received: from lumine.fritz.box (85-127-52-93.dsl.dynamic.surfer.at.
 [85.127.52.93]) by smtp.gmail.com with ESMTPSA id
 da25-20020a056402177900b0052a626daf4csm142581edb.47.2023.08.26.13.01.47
 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
 Sat, 26 Aug 2023 13:01:47 -0700 (PDT)
Message-ID: <c8846b0bafd00ed0df93fd3ec3aa0cccfaefa90e.camel@HIDDEN>
Subject: Re: [PATCH] gnu: home: Add home-emacs-service-type.
From: Liliana Marie Prikler <liliana.prikler@HIDDEN>
To: Kierin Bell <fernseed@HIDDEN>, 64620 <at> debbugs.gnu.org
Date: Sat, 26 Aug 2023 22:01:46 +0200
In-Reply-To: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable
User-Agent: Evolution 3.46.4 
MIME-Version: 1.0
X-Spam-Score: 0.0 (/)
X-Debbugs-Envelope-To: 64620
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.0 (-)

Am Freitag, dem 14.07.2023 um 11:12 -0400 schrieb Kierin Bell
<fernseed@HIDDEN>
>=20
> * gnu/home/services/emacs.scm: New file.
> * gnu/local.mk (GNU_SYSTEM_MODULES): Add new file.
> * tests/home/services/emacs.scm: New tests file.
> * Makefile.am (SCM_TESTS): Add new tests file.
AFAIK we use "Register" instead of "Add".
> * doc/guix.texi (Emacs Home Services): New node.
I'd cut this series into two (or more, see below) patches right around
here, putting everything below this line into the first patch(es) and
everything above into the second/nth.
> * guix/read-print.scm (read-with-comments, read-with-
> comments/sequence):
> Add new ELISP? and UNELISP-EXTENSIONS? keyword arguments to support
> reading Elisp.
> (%newline-forms): Add `home-emacs-configuration'.
> (%elisp-special-forms, %elisp-natural-whitespace-string-forms)
> (%elisp-special-symbol-chars, %elisp-confusable-number-symbols)
> (%elisp-basic-chars, %elisp-simple-escape-chars): New variables.
> (special-form-lead, printed-string, symbol->display-string): Add new
> ELISP? keyword argument.
> (atom->elisp-string): New helper function.
> (pretty-print-with-comments): New ELISP? and SPECIAL-FORMS keyword
> arguments to support serialization to Elisp.=C2=A0=C2=A0

> General improvements:
> enable pretty-printing of alists and improper lists; only print lists
> of
> constants with one element per line when length exceeds LONG-LIST; do
> not print newline before special read syntax forms (e.g., `'', `#~',
> etc.) unless they would exceed MAX-WIDTH; include backslashes when
> calculating whether a string would exceed MAX-WIDTH; do not print
> extraneous newline when special form has an empty body; print
> newlines
> after list arguments of special forms; print first argument after
> function on newline with same indentation as function when it would
> exceed MAX-WIDTH.
> * tests/read-print.scm: Add new tests and update old tests which fail
> due to improvements.
These general improvements should perhaps also been given their own
patch(es).  Also, since read-print is used in guix style, I'd be
interested in seeing how the output improves from your changes.  Do you
have easy comparisons?

> ---
>=20
> This patch builds on patches from ( and David Wilson for a
> `home-emacs-service-type' (https://issues.guix.gnu.org/58693,
> https://issues.guix.gnu.org/60753,
> https://issues.guix.gnu.org/62549).
>=20
> Many of the features of the prior patches have been included, but the
> major focus here is to configure Emacs in Scheme rather than
> symlinking
> to existing configuration files.
>=20
> Here are some of the broad strokes:
>=20
> * The following record types have been introduced to encapsulate
> =C2=A0 configuration for Emacs: `emacs-configuration' (for general
> =C2=A0 configuration), `emacs-package' (for package-specific
> configuration),
> =C2=A0 `emacs-keymap' (for configuration of local keymaps), and
> =C2=A0 `emacs-server' (for configuration of Emacs servers).
>=20
> * Most configuration fields are either flat lists or alists that are
> =C2=A0 considerably abstracted from their final serialized Elisp
> =C2=A0 representation, but escape hatches are provided for both pulling i=
n
> =C2=A0 existing configuration files and specifying s-expressions directly=
.
>=20
> * All serialized Elisp is pretty-printed much how we would expect to
> see
> =C2=A0 it in Emacs (for example, with proper indentation according to the
> =C2=A0 `lisp-indent-function' symbol property, etc.).=C2=A0 This has been
> =C2=A0 accomplished by adding a new keyword argument to
> =C2=A0 `pretty-print-with-comments' from `(guix read-print)', among other
> =C2=A0 improvements.
>=20
> * Emacs package configuration can either be serialized as `use-
> package'
> =C2=A0 forms or as equivalent, more minimalist s-expressions.=C2=A0 Users=
 can
> =C2=A0 define their own package serializers, too.
>=20
> * For specifying s-expressions, an "Elisp expression" syntax has been
> =C2=A0 implemented that is essentially a lighter-weight version G-
> expressions.
> =C2=A0 (I try to explain why this is helpful in the documentation.)
>=20
> * A reader extension has been implemented that allows for "Elisp
> =C2=A0 expressions" to be specified directly with Elisp read syntax, and
> =C2=A0 Scheme values (including file-like objects or G-expressions) can i=
n
> =C2=A0 turn be "unquoted" within that Elisp code.=C2=A0 Also, comments an=
d
> =C2=A0 whitespace can be included within the Elisp code via the `#;'
> =C2=A0 (comment), `#>' (newline), and `;^L' (page break) forms.
>=20
> * Each Emacs server has its own user init and early init files, which
> =C2=A0 can optionally inherit configuration from the init files used by
> =C2=A0 non-server Emacsen.=C2=A0 Each server can also inherit the "main"
> =C2=A0 `user-emacs-directory', or it can use its own subdirectory.
>=20
> * The `home-emacs-service-type' can be extended, with subordinate
> =C2=A0 configuration records being merged intelligently when possible.
>=20
> * A utility function has been provided for generating the
> aforementioned
> =C2=A0 Scheme records from an existing Emacs init file:
> =C2=A0 `elisp-file->home-emacs-configuration'.
>=20
> Here's an example configuration for the `home-emacs-service-type'
> demonstrating some of these features:
>=20
> --8<---------------cut here---------------start------------->8---
> (use-modules (gnu home)
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =
(gnu services)
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =
(guix gexp)
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =
(gnu home services)
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =
(gnu home services emacs)
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =
(gnu packages emacs-xyz)
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =
(gnu packages file)
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =
(gnu packages compression))
>=20
> (define %my-function-name 'my--compose-mail)
>=20
> (define %gnus-init-file
> =C2=A0 (elisp-file "gnus.el"
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0 (list
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0 (elisp (setq gnus-select-method '(nnnil "")))
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0 (elisp (setq gnus-secondary-select-methods
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=
=A0=C2=A0=C2=A0 '((nnml "")
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=
=A0=C2=A0=C2=A0=C2=A0=C2=A0 (nntp "news.gmane.io"))))
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0 (elisp (setq mail-sources
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=
=A0=C2=A0=C2=A0 '((imap :server "mail.example.net"
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=
=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 :user "user=
@example.net"
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=
=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 :port 993
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=
=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 :stream tls=
))))
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0 ;; Elisp reader extension
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0 #%(define-key global-map [remap compose-mail]
> #;comment
> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 '#$%my-function-name nil))))
I assume that each elisp or #% only handles a single expression, am I
correct?  Or do we also have (elisp a b) and #%@(a b)?

Cheers




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 26 Aug 2023 16:45:10 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Aug 26 12:45:10 2023
Received: from localhost ([127.0.0.1]:43364 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qZwPM-00042K-B9
	for submit <at> debbugs.gnu.org; Sat, 26 Aug 2023 12:45:10 -0400
Received: from relay3-d.mail.gandi.net ([2001:4b98:dc4:8::223]:42743)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <fernseed@HIDDEN>) id 1qZtvi-0008QF-Kf
 for 64620 <at> debbugs.gnu.org; Sat, 26 Aug 2023 10:06:26 -0400
Received: by mail.gandi.net (Postfix) with ESMTPSA id DB67360003;
 Sat, 26 Aug 2023 14:06:08 +0000 (UTC)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=fernseed.me; s=gm1;
 t=1693058769;
 h=from:from:reply-to:subject:subject:date:date:message-id:message-id:
 to:to:cc:cc:mime-version:mime-version:content-type:content-type:
 in-reply-to:in-reply-to:references:references;
 bh=chuODUrpcO/O0fuyw9SEEMQIpwUmMJ9e6RYtEAVL2oU=;
 b=DTGGsDBunXtYC46PObw1jeSRYgXyshFrtg0vNhbUAy/TgnZ8jOC09D3PRV8B19I8bbvfFh
 1DpRdDaghic/7H65XmA5SLwpJwdSOb17Zln0L+uOmjrLZlrX6C5CzCV+zyN467jnkdyWol
 RdkG9OAzB0zLO/0A3V97ZVk/FD8A54ssKCH0WLsfsH1AAja+8pI+xMy9wP0hV7ZKRfMG6V
 Id/ajRpaZmpGNEvXWWW+zURUmuqzrpCjvkuINXqlhxsi3/6LmsrUBFwrmnnI2BbOmmAz90
 ywd0lZ+IyK99DxihYLU+LtfIwgRHgfcU4V5f0MOiy2/slMfHlLg33BgelkjXXw==
From: Kierin Bell <fernseed@HIDDEN>
To: "(" <paren@HIDDEN>
Subject: Re: [bug#64620] [PATCH] gnu: home: Add home-emacs-service-type.
In-Reply-To: <87il945gzu.fsf@HIDDEN> (paren@HIDDEN's message of
 "Thu, 24 Aug 2023 21:00:49 +0100")
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
 <87il945gzu.fsf@HIDDEN>
Date: Sat, 26 Aug 2023 10:06:03 -0400
Message-ID: <874jklyk38.fsf@HIDDEN>
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain
X-GND-Sasl: fernseed@HIDDEN
X-Spam-Score: -0.7 (/)
X-Debbugs-Envelope-To: 64620
X-Mailman-Approved-At: Sat, 26 Aug 2023 12:45:07 -0400
Cc: 64620 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.7 (-)

Hi (,

"(" <paren@HIDDEN> writes:

> *Very* nice!  I think it would be nice to support a PACKAGES field with
> a list of Guix packages to make available to Emacs, though that would
> complicate the code a bit.
>
> You'd probably have to do something like what I did[1], which is a
> little hacky, and you'd want to somehow redirect the output from
> building the Emacs profile to a log file in '$XDG_STATE_HOME/log/emacs',
> but it'd let people use native-comp without the hassle of transforming
> the packages themselves (with appropriate EMACS and NATIVE-COMP? fields,
> of course :)) Also lets you isolate your emacs environment from your
> regular environment, if you wanted to do that for... reasons.
>
> If you'd rather not implement that for now, it's okay; I'm very much
> willing to submit a followup if/when this is merged.
>
> [1] https://git.sr.ht/~whereiseveryone/guixrus/tree/master/item/guixrus/home/services/emacs.scm#L113
>
>   -- (
>
>

Although there is an EXTRA-PACKAGES field for Guix packages (I use it,
e.g., for font packages) and a CONFIGURED-PACKAGES field specifically
for installing Emacs packages, I do like the idea of having a separate
Emacs profile, or even a separate profile for each Emacs server.

Part of me wanted to find a way to do that more "properly", with some
sort of lower-level `home-emacs-environment-service-type' that sets up
the profiles (hopefully in a more orthodox way), etc.  But that turned
out to be even more hacky in the end.

Looking at the Emacs build system modules, I'm not sure exactly how
building separate profiles would allow native-comp without transforming
the package inputs.  Isn't the build system basically hard-coded to use
`emacs-minimal' for building Emacs packages (i.e., unless we manually
transform the package inputs)?

At the very least, I'd like to set things up the best I can for this,
even if I end up leaving it to people with more expertise to implement.

Thanks (!

-- 
Kierin Bell
GPG Key: FCF2 5F08 EA4F 2E3D C7C3  0D41 D14A 8CD3 2D97 0B36




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 24 Aug 2023 20:21:19 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Thu Aug 24 16:21:19 2023
Received: from localhost ([127.0.0.1]:38678 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qZGpT-0000hX-HN
	for submit <at> debbugs.gnu.org; Thu, 24 Aug 2023 16:21:19 -0400
Received: from layka.disroot.org ([178.21.23.139]:53750)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <paren@HIDDEN>) id 1qZGpQ-0000hN-MK
 for 64620 <at> debbugs.gnu.org; Thu, 24 Aug 2023 16:21:17 -0400
Received: from localhost (localhost [127.0.0.1])
 by disroot.org (Postfix) with ESMTP id 596A940CA9;
 Thu, 24 Aug 2023 22:21:11 +0200 (CEST)
X-Virus-Scanned: SPAM Filter at disroot.org
Received: from layka.disroot.org ([127.0.0.1])
 by localhost (disroot.org [127.0.0.1]) (amavisd-new, port 10024)
 with ESMTP id jjKpE4CxBY0Z; Thu, 24 Aug 2023 22:21:10 +0200 (CEST)
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=disroot.org; s=mail;
 t=1692908470; bh=sEIQXEpVqBMplKfgOi9UoRdlopwALioGJvdO9q8LMB0=;
 h=References:From:To:Cc:Subject:Date:In-reply-to;
 b=EwuSk201bmW0VoDd3duecjAQuyYzmwcMR8VRqnFKK7vEkGEACSZsqLU/OdjgmMcAE
 23/VVi93bCrecIDZ4PPvpJJGH4hjpLomNWoaKDaNylzRL60arCadGvJjVbzKYPRpzE
 wv4dZH88LMYjMP8ujKnzTGdADQRAww8co3Rd164pcFRU1jpV3l6F5kXCylOr0e39ZS
 29DHtcVgvJp9MkFZt9Kg5TMhsSs0xhw7NRAX/8T2nZiATG6uDCVbRaybuAbKeYCR9z
 W59YxXgZyk0lTElN5KAvlfhnEc71g5REwxiB/Jaze6fxslkcVRPjtv99Db6S8uk0R4
 ZOJgfyHwK6RxQ==
From: "(" <paren@HIDDEN>
To: fernseed@HIDDEN
Subject: Re: [bug#64620] [PATCH] gnu: home: Add home-emacs-service-type.
Date: Thu, 24 Aug 2023 21:00:49 +0100
In-reply-to: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
Message-ID: <87il945gzu.fsf@HIDDEN>
MIME-Version: 1.0
Content-Type: text/plain
X-Spam-Score: 0.0 (/)
X-Debbugs-Envelope-To: 64620
Cc: 64620 <at> debbugs.gnu.org, guix-patches@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.0 (-)


*Very* nice!  I think it would be nice to support a PACKAGES field with
a list of Guix packages to make available to Emacs, though that would
complicate the code a bit.

You'd probably have to do something like what I did[1], which is a
little hacky, and you'd want to somehow redirect the output from
building the Emacs profile to a log file in '$XDG_STATE_HOME/log/emacs',
but it'd let people use native-comp without the hassle of transforming
the packages themselves (with appropriate EMACS and NATIVE-COMP? fields,
of course :)) Also lets you isolate your emacs environment from your
regular environment, if you wanted to do that for... reasons.

If you'd rather not implement that for now, it's okay; I'm very much
willing to submit a followup if/when this is merged.

[1] https://git.sr.ht/~whereiseveryone/guixrus/tree/master/item/guixrus/home/services/emacs.scm#L113

  -- (




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at submit <at> debbugs.gnu.org:


Received: (at submit) by debbugs.gnu.org; 24 Aug 2023 20:21:28 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Thu Aug 24 16:21:28 2023
Received: from localhost ([127.0.0.1]:38681 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qZGpb-0000hv-Pw
	for submit <at> debbugs.gnu.org; Thu, 24 Aug 2023 16:21:28 -0400
Received: from lists.gnu.org ([2001:470:142::17]:43216)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <paren@HIDDEN>) id 1qZGpZ-0000hf-HG
 for submit <at> debbugs.gnu.org; Thu, 24 Aug 2023 16:21:26 -0400
Received: from eggs.gnu.org ([2001:470:142:3::10])
 by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <paren@HIDDEN>) id 1qZGpP-00015u-Q3
 for guix-patches@HIDDEN; Thu, 24 Aug 2023 16:21:15 -0400
Received: from layka.disroot.org ([178.21.23.139])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <paren@HIDDEN>) id 1qZGpN-0002dy-90
 for guix-patches@HIDDEN; Thu, 24 Aug 2023 16:21:15 -0400
Received: from localhost (localhost [127.0.0.1])
 by disroot.org (Postfix) with ESMTP id 596A940CA9;
 Thu, 24 Aug 2023 22:21:11 +0200 (CEST)
X-Virus-Scanned: SPAM Filter at disroot.org
Received: from layka.disroot.org ([127.0.0.1])
 by localhost (disroot.org [127.0.0.1]) (amavisd-new, port 10024)
 with ESMTP id jjKpE4CxBY0Z; Thu, 24 Aug 2023 22:21:10 +0200 (CEST)
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=disroot.org; s=mail;
 t=1692908470; bh=sEIQXEpVqBMplKfgOi9UoRdlopwALioGJvdO9q8LMB0=;
 h=References:From:To:Cc:Subject:Date:In-reply-to;
 b=EwuSk201bmW0VoDd3duecjAQuyYzmwcMR8VRqnFKK7vEkGEACSZsqLU/OdjgmMcAE
 23/VVi93bCrecIDZ4PPvpJJGH4hjpLomNWoaKDaNylzRL60arCadGvJjVbzKYPRpzE
 wv4dZH88LMYjMP8ujKnzTGdADQRAww8co3Rd164pcFRU1jpV3l6F5kXCylOr0e39ZS
 29DHtcVgvJp9MkFZt9Kg5TMhsSs0xhw7NRAX/8T2nZiATG6uDCVbRaybuAbKeYCR9z
 W59YxXgZyk0lTElN5KAvlfhnEc71g5REwxiB/Jaze6fxslkcVRPjtv99Db6S8uk0R4
 ZOJgfyHwK6RxQ==
From: "(" <paren@HIDDEN>
To: fernseed@HIDDEN
Subject: Re: [bug#64620] [PATCH] gnu: home: Add home-emacs-service-type.
Date: Thu, 24 Aug 2023 21:00:49 +0100
In-reply-to: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
Message-ID: <87il945gzu.fsf@HIDDEN>
MIME-Version: 1.0
Content-Type: text/plain
Received-SPF: pass client-ip=178.21.23.139; envelope-from=paren@HIDDEN;
 helo=layka.disroot.org
X-Spam_score_int: -20
X-Spam_score: -2.1
X-Spam_bar: --
X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1,
 DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, SPF_HELO_NONE=0.001,
 SPF_PASS=-0.001 autolearn=ham autolearn_force=no
X-Spam_action: no action
X-Spam-Score: 0.9 (/)
X-Debbugs-Envelope-To: submit
Cc: 64620 <at> debbugs.gnu.org, guix-patches@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.1 (/)


*Very* nice!  I think it would be nice to support a PACKAGES field with
a list of Guix packages to make available to Emacs, though that would
complicate the code a bit.

You'd probably have to do something like what I did[1], which is a
little hacky, and you'd want to somehow redirect the output from
building the Emacs profile to a log file in '$XDG_STATE_HOME/log/emacs',
but it'd let people use native-comp without the hassle of transforming
the packages themselves (with appropriate EMACS and NATIVE-COMP? fields,
of course :)) Also lets you isolate your emacs environment from your
regular environment, if you wanted to do that for... reasons.

If you'd rather not implement that for now, it's okay; I'm very much
willing to submit a followup if/when this is merged.

[1] https://git.sr.ht/~whereiseveryone/guixrus/tree/master/item/guixrus/home/services/emacs.scm#L113

  -- (




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 24 Aug 2023 15:38:22 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Thu Aug 24 11:38:22 2023
Received: from localhost ([127.0.0.1]:38435 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qZCPd-0001Js-A1
	for submit <at> debbugs.gnu.org; Thu, 24 Aug 2023 11:38:22 -0400
Received: from relay8-d.mail.gandi.net ([217.70.183.201]:60217)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <fernseed@HIDDEN>) id 1qZA9P-0005O6-7S
 for 64620 <at> debbugs.gnu.org; Thu, 24 Aug 2023 09:13:28 -0400
Received: by mail.gandi.net (Postfix) with ESMTPSA id 1CF7E1BF20C;
 Thu, 24 Aug 2023 13:13:15 +0000 (UTC)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=fernseed.me; s=gm1;
 t=1692882796;
 h=from:from:reply-to:subject:subject:date:date:message-id:message-id:
 to:to:cc:cc:mime-version:mime-version:content-type:content-type:
 in-reply-to:in-reply-to:references:references;
 bh=3toaC3x/OFxQCpYyasdgsMo/LUkRfDYiYtM7YPn2sqE=;
 b=CSEU3noS65ittTgKYQnaDk0ykubbfgykSElea+6j+ozszHldk31k5b5RbnGmU8TaEMHFhY
 KIkR/g5WAVXpXgdCpjt2q4bK+d88slXgGJGjpvfZtaepSSMZIcsmezPBfi/0LMzDH+wId0
 8iuoJ1g+DS+FeClsNhTrtmqXmKhBpLB8+UankblgwCjzvLd56D2VZmGEgUOSNeujtNYzle
 PzU9g+UiUnBdXnTxbD3aJ7pUTG7xSZ5VapaJQDrg5WMQwTiscbmzZ870bAeyWUu7EsPedN
 9nuUrsO1n70xT7XTNsSeaGLabwAi3mxPUbFnK/Lb+VvUV+l/l7qCraF+IOcC8A==
From: Kierin Bell <fernseed@HIDDEN>
To: Hilton Chain <hako@HIDDEN>
Subject: Re: [bug#64620] [PATCH] gnu: home: Add home-emacs-service-type.
In-Reply-To: <87wmxkhbin.wl-hako@HIDDEN> (Hilton Chain's message of
 "Thu, 24 Aug 2023 20:26:24 +0800")
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
 <87wmxkhbin.wl-hako@HIDDEN>
Date: Thu, 24 Aug 2023 09:13:10 -0400
Message-ID: <875y54ziqh.fsf@HIDDEN>
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain
X-GND-Sasl: fernseed@HIDDEN
X-Spam-Score: -0.7 (/)
X-Debbugs-Envelope-To: 64620
X-Mailman-Approved-At: Thu, 24 Aug 2023 11:38:18 -0400
Cc: 64620 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.7 (-)


Hi Hilton,

Hilton Chain <hako@HIDDEN> writes:

> Hi Kierin,
> [...]
> Tried to convert my config to home-emacs-configuration [1], my
> original configuration wasn't well written, so the rewrite took a
> while.  Luckily it doesn't have too many lines. :)
>
> [1]:
> https://codeberg.org/hako/Testament/src/branch/test-home-emacs/dorphine-home.scm#L452-L858
>
>

It's nice to see this!

What strikes me immediately is that I can easily understand your Emacs
config, because configuration via Scheme records is so consistent and
uniform.  I can't usually say that about Emacs init files.

Granted, it does take a long time to get used to the format coming from
Elisp.  (And the import function isn't perfect.)

After thinking about what Ludo' said, I'm exploring the possibility of
removing the `configured-packages' fields and `emacs-package' record
type from the core `home-emacs-service-type'.

I could then put this functionality (~1300 lines) in a
`home-emacs-package-configuration-service-type' that extends the Emacs
home service.  That could go in a separate channel somewhere, but if it
is upstreamed then we can create a de facto standard that encourages
innovation and collaboration.

A bit of control over how the final serialized Elisp is formatted would
be lost by taking the package configuration functionality out of the
core Emacs home service, but it is not very significant (especially
considering that the point of using the feature would be to configure
Emacs in Scheme).  And it seems like there would be benefits to a
modularized approach from usability and implementation standpoints.

Anyway, thanks for testing!

-- 
Kierin Bell
GPG Key: FCF2 5F08 EA4F 2E3D C7C3  0D41 D14A 8CD3 2D97 0B36




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 24 Aug 2023 12:27:05 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Thu Aug 24 08:27:05 2023
Received: from localhost ([127.0.0.1]:36316 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qZ9QX-00047t-8A
	for submit <at> debbugs.gnu.org; Thu, 24 Aug 2023 08:27:05 -0400
Received: from mail.boiledscript.com ([144.168.59.46]:33594)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <hako@HIDDEN>) id 1qZ9QV-00047l-Bq
 for 64620 <at> debbugs.gnu.org; Thu, 24 Aug 2023 08:27:04 -0400
Date: Thu, 24 Aug 2023 20:26:24 +0800
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=ultrarare.space;
 s=dkim; t=1692879983;
 h=from:from:reply-to:subject:subject:date:date:message-id:message-id:
 to:to:cc:cc:mime-version:mime-version:content-type:content-type:
 in-reply-to:in-reply-to:references:references;
 bh=uyenu0aadQLN6/6uSoYGmIqm8JS3jI5ed6gG3FJvB1Q=;
 b=rS87kv7qcWVcNZPP6CiramxFMAEZ89xl/q1mt0r7OIAv5qNDCYsgDqSs7KDLLmOx10UEMa
 Y7xNP5XM6NaUBbbrN8Ns8xCHdPAPSSlxpFQVn4BE/2up0+oMAe0VK35idj51AJrOujaHjq
 L+PwFSffHQ9gwkrrd7A+bM/KPPPYsypgZLz1qK0B0zAkPCq5U8cbWhk9hpNn1QA3LuybOQ
 Vcl/u0u81iwjW+shrPy8Wm2gPLx6iC5AjQ38pcsvZdG0wIpqorPKVGlMajI3e+yu27axbo
 lMfr/KHsR3cg0k6TXIGyavlq6HDloqQk//mUJSl+whG8Om6VEjAfiGByfZWZoQ==
Authentication-Results: mail.boiledscript.com;
 auth=pass smtp.mailfrom=hako@HIDDEN
Message-ID: <87wmxkhbin.wl-hako@HIDDEN>
From: Hilton Chain <hako@HIDDEN>
To: Kierin Bell <fernseed@HIDDEN>
Subject: Re: [bug#64620] [PATCH] gnu: home: Add home-emacs-service-type.
In-Reply-To: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
MIME-Version: 1.0
Content-Type: text/plain; charset=US-ASCII
X-Spamd-Bar: /
X-Spam-Score: 0.0 (/)
X-Debbugs-Envelope-To: 64620
Cc: 64620 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.0 (-)

Hi Kierin,

On Fri, 14 Jul 2023 23:12:31 +0800,
fernseed@HIDDEN wrote:
>
> From: Kierin Bell <fernseed@HIDDEN>
>
> * gnu/home/services/emacs.scm: New file.
> * gnu/local.mk (GNU_SYSTEM_MODULES): Add new file.
> * tests/home/services/emacs.scm: New tests file.
> * Makefile.am (SCM_TESTS): Add new tests file.
> * doc/guix.texi (Emacs Home Services): New node.
> * guix/read-print.scm (read-with-comments, read-with-comments/sequence):
> Add new ELISP? and UNELISP-EXTENSIONS? keyword arguments to support
> reading Elisp.
> (%newline-forms): Add `home-emacs-configuration'.
> (%elisp-special-forms, %elisp-natural-whitespace-string-forms)
> (%elisp-special-symbol-chars, %elisp-confusable-number-symbols)
> (%elisp-basic-chars, %elisp-simple-escape-chars): New variables.
> (special-form-lead, printed-string, symbol->display-string): Add new
> ELISP? keyword argument.
> (atom->elisp-string): New helper function.
> (pretty-print-with-comments): New ELISP? and SPECIAL-FORMS keyword
> arguments to support serialization to Elisp.  General improvements:
> enable pretty-printing of alists and improper lists; only print lists of
> constants with one element per line when length exceeds LONG-LIST; do
> not print newline before special read syntax forms (e.g., `'', `#~',
> etc.) unless they would exceed MAX-WIDTH; include backslashes when
> calculating whether a string would exceed MAX-WIDTH; do not print
> extraneous newline when special form has an empty body; print newlines
> after list arguments of special forms; print first argument after
> function on newline with same indentation as function when it would
> exceed MAX-WIDTH.
> * tests/read-print.scm: Add new tests and update old tests which fail
> due to improvements.
> ---

Great work!

Tried to convert my config to home-emacs-configuration [1], my
original configuration wasn't well written, so the rewrite took a
while.  Luckily it doesn't have too many lines. :)

[1]:
https://codeberg.org/hako/Testament/src/branch/test-home-emacs/dorphine-home.scm#L452-L858




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 24 Aug 2023 06:57:48 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Thu Aug 24 02:57:48 2023
Received: from localhost ([127.0.0.1]:36000 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qZ4Hq-0000nN-FY
	for submit <at> debbugs.gnu.org; Thu, 24 Aug 2023 02:57:48 -0400
Received: from relay2-d.mail.gandi.net ([2001:4b98:dc4:8::222]:51077)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <fernseed@HIDDEN>) id 1qYqVl-0000CQ-JG
 for 64620 <at> debbugs.gnu.org; Wed, 23 Aug 2023 12:15:16 -0400
Received: by mail.gandi.net (Postfix) with ESMTPSA id CF0BE40006;
 Wed, 23 Aug 2023 16:15:02 +0000 (UTC)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=fernseed.me; s=gm1;
 t=1692807303;
 h=from:from:reply-to:subject:subject:date:date:message-id:message-id:
 to:to:cc:cc:mime-version:mime-version:content-type:content-type:
 content-transfer-encoding:content-transfer-encoding:
 in-reply-to:in-reply-to:references:references;
 bh=DvqZzLx3cdAYyh8v32caOcZil1WbR/KbyPP7KyvDzCY=;
 b=Em5Ec2kloRP2zHe0wNNqZUB6WAZo8gs6AsDvynFOlj/yEa8ZPhGi2HyUyXwDr3pOEwjALx
 orxa1LUNwBIELEiV9S4PBqHc77cW/2V6OmLpRv8XktJAUBNTGA1WfTp5OKzs40Ipq7JeAD
 aWwoU4rehSuaqytgu8xz3844zSCgLF0720LXXOB5I3UQW3m4jT6e+dnL9VJNjsvfWeSvld
 Xs6zkzVAJ6iEQAO07zWZ/n1qCYR0aERMcRQYs+W3G2Rsi5vkXVmvmKhZz4f0ApZ/fwh0Rh
 PVxymFF5nnr4KdbwGk+wygPl/L4fllIyZbaqiIJ6k318Cbj/TXXw8n/lPaTt6Q==
From: Kierin Bell <fernseed@HIDDEN>
To: Ludovic =?utf-8?Q?Court=C3=A8s?= <ludo@HIDDEN>
Subject: Re: bug#64620: [PATCH] gnu: home: Add home-emacs-service-type.
In-Reply-To: <87v8d613hs.fsf@HIDDEN> ("Ludovic =?utf-8?Q?Court=C3=A8s=22'?=
 =?utf-8?Q?s?= message of "Wed, 23 Aug 2023 12:01:51 +0200")
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
 <87v8d613hs.fsf@HIDDEN>
Date: Wed, 23 Aug 2023 12:14:57 -0400
Message-ID: <87o7ixzqf2.fsf@HIDDEN>
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
X-GND-Sasl: fernseed@HIDDEN
X-Spam-Score: -0.7 (/)
X-Debbugs-Envelope-To: 64620
X-Mailman-Approved-At: Thu, 24 Aug 2023 02:57:44 -0400
Cc: 64620 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.7 (-)


Hi Ludo=E2=80=99,

Ludovic Court=C3=A8s <ludo@HIDDEN> writes:

> Hi Kierin,
>
> This is a truly impressive piece of work!
>
> fernseed@HIDDEN skribis:
>
>> This patch builds on patches from ( and David Wilson for a
>> `home-emacs-service-type' (https://issues.guix.gnu.org/58693,
>> https://issues.guix.gnu.org/60753, https://issues.guix.gnu.org/62549).
>>
>> Many of the features of the prior patches have been included, but the
>> major focus here is to configure Emacs in Scheme rather than symlinking
>> to existing configuration files.
>
> OK, interesting.  This seems to be one of the main questions: how far
> should we go on the Scheme side?
>
> In <https://issues.guix.gnu.org/62549>, unmatched-paren chose to not
> generate elisp at all from Scheme.  The advantage is that the
> implementation is simpler; as a user, the model one has to have in mind
> is also simpler: you=E2=80=99re still configuring most things the traditi=
onal
> way in elisp.  That=E2=80=99s also its downside: you have to do plumbing =
on the
> elisp side, when Guix Home in some cases would know what to do.
>
> I don=E2=80=99t have the answer and I=E2=80=99m not sure what I=E2=80=99d=
 prefer, but I=E2=80=99m trying
> to see the tradeoffs and to map out the design space.

My philosophy here, I think, is that we can do both.  ('s approach
likely provides all of the integration between Guix and Emacs that many
would want.  David Wilson's patch goes a step further by providing a
configuration option to change the Emacs user directory, which I think
is a reasonable option to have.  (Testing this, though, it turns out
that changing the Emacs user directory is not so simple, so the
implementation in my patch is more involved).  In any case, as we saw,
Guix Home does need to serialize some Elisp to do this (or to do
anything similar).

So, ideally, we would provide some useful mechanisms for serializing
Elisp, but not go overboard.  It's Scheme, so users can build on things
later if reasonable foundations are there.

We may want to work on simplifying the implementation here by removing
features that add too much complexity.  For example, maybe all of the
fined-tuned controls over how Emacs servers "inherit" configuration
aren't justified given the complexity of the implementation ---
especially given that configuration is done via Scheme records, so users
can duplicate configuration themselves by just reusing records.  (But I
do think it makes sense to at least include a subset of those features.)

>> Here are some of the broad strokes:
>>
>> * The following record types have been introduced to encapsulate
>>   configuration for Emacs: `emacs-configuration' (for general
>>   configuration), `emacs-package' (for package-specific configuration),
>>   `emacs-keymap' (for configuration of local keymaps), and
>>   `emacs-server' (for configuration of Emacs servers).
>
> Why special-case keymaps, of all the things one might one to configure
> in Emacs?  I understand it=E2=80=99s one of the first things one may want=
 to
> tweak, but then why not add <emacs-theme> as well, etc.; IOW, where do
> we draw the line?
>

For `emacs-keymap', I created a record type to avoid having nested
alists like:

'((foo-map . (("C-c a" . foo) ...))
  (bar-map . ...)
  ...)

Also, the `emacs-keymap' record has a `repeat?' field, so it can serve
the purpose of something like use-package's `:repeat-map' keyword.

...I do like the idea of `<emacs-theme>', though...
Just kidding.

>> * Most configuration fields are either flat lists or alists that are
>>   considerably abstracted from their final serialized Elisp
>>   representation, but escape hatches are provided for both pulling in
>>   existing configuration files and specifying s-expressions directly.
>
> Are seasoned Emacsers not going to be frustrated because of this?  :-)
>
> They might prefer to have full access to elisp.
>

I think it probably would be frustrating if it wasn't clear that users
do have access to Elisp via the escape hatch fields.  Maybe these should
be mentioned more prominently in the documentation.

They can also specify Elisp directly by using "Elisp expressions" as
values in some of the alists --- for example, those that set variables
(examples given in the configuration snippets).

>> * All serialized Elisp is pretty-printed much how we would expect to see
>>   it in Emacs (for example, with proper indentation according to the
>>   `lisp-indent-function' symbol property, etc.).  This has been
>>   accomplished by adding a new keyword argument to
>>   `pretty-print-with-comments' from `(guix read-print)', among other
>>   improvements.
>
> Fun.  I=E2=80=99d like to see how we can avoid spreading elisp conditiona=
ls in
> (guix read-print).
>

I think the Elisp reader extension could be implemented completely on
its own, outside of (guix read-print).  The (guix read-print) pretty
printer is very nice, though, and it seems relatively simple to adapt it
to print Elisp.  Though I'm open to better ideas.

Most of the changes to `pretty-print-with-comments' are actually fixes
that are not Elisp-specific, like preventing newlines where they
definitely don't belong.  Maybe that could go in a separate commit?

>> * Emacs package configuration can either be serialized as `use-package'
>>   forms or as equivalent, more minimalist s-expressions.  Users can
>>   define their own package serializers, too.
>>
>> * For specifying s-expressions, an "Elisp expression" syntax has been
>>   implemented that is essentially a lighter-weight version G-expressions.
>>   (I try to explain why this is helpful in the documentation.)
>>
>> * A reader extension has been implemented that allows for "Elisp
>>   expressions" to be specified directly with Elisp read syntax, and
>>   Scheme values (including file-like objects or G-expressions) can in
>>   turn be "unquoted" within that Elisp code.  Also, comments and
>>   whitespace can be included within the Elisp code via the `#;'
>>   (comment), `#>' (newline), and `;^L' (page break) forms.
>
> Great that you=E2=80=99re putting (language elisp parser) to good use!
>
>> * Each Emacs server has its own user init and early init files, which
>>   can optionally inherit configuration from the init files used by
>>   non-server Emacsen.  Each server can also inherit the "main"
>>   `user-emacs-directory', or it can use its own subdirectory.
>>
>> * The `home-emacs-service-type' can be extended, with subordinate
>>   configuration records being merged intelligently when possible.
>
> Very nice.
>
>> * A utility function has been provided for generating the aforementioned
>>   Scheme records from an existing Emacs init file:
>>   `elisp-file->home-emacs-configuration'.
>
> Neat; perhaps =E2=80=98guix home import=E2=80=99 could use it?
>

I looked into modifying `guix home import', but didn't have time to
figure out exactly how to make that work.

>> (define %gnus-init-file
>>   (elisp-file "gnus.el"
>>               (list
>>                (elisp (setq gnus-select-method '(nnnil "")))
>>                (elisp (setq gnus-secondary-select-methods
>>                             '((nnml "")
>>                               (nntp "news.gmane.io"))))
>>                (elisp (setq mail-sources
>>                             '((imap :server "mail.example.net"
>>                                     :user "user@HIDDEN"
>>                                     :port 993
>>                                     :stream tls))))
>>                ;; Elisp reader extension
>>                #%(define-key global-map [remap compose-mail] #;comment
>>                    '#$%my-function-name nil))))
>
> Could I write:
>
>   #%(progn
>       (setq x =E2=80=A6)
>       (setq y =E2=80=A6)
>       (define-key =E2=80=A6))
>
> ?  That would seem nicer.
>

I'm thinking about a way to create a "splicing" version of the `elisp'
macro, or something similar, so you could do something like that without
serializing the actual `progn'.

> #%(body =E2=80=A6) is short for (elisp body =E2=80=A6) right?
>
>
> [...]
>

Yes, they are equivalent. Most users would probably use the reader
extension, unless they want to use Scheme-specific syntax (like `#(...)'
for vectors versus `[...]').

>>      (configured-packages
>>       (list
>>        (emacs-package
>>         (name 'windmove)
>>         ;; Autoload a function used by `my--display-buffer-down'.
>>         (autoloads '(windmove-display-in-direction))
>>         (keys-override
>>          '(("C-M-<left>" . windmove-left)
>>            ("C-M-<right>" . windmove-right)
>>            ("C-M-<up>" . windmove-up)
>>            ("C-M-<down>" . windmove-down)
>>            ("C-x <down>"
>>             . my--display-buffer-down)))
>>         (keys-local
>>          (list
>>           (emacs-keymap
>>            (name 'windmove-repeat-map)
>>            (repeat? #t)
>>            (keys '(("<left>" . windmove-left)
>>                    ("<right>" . windmove-right)
>>                    ("<up>" . windmove-up)
>>                    ("<down>" . windmove-down))))))
>
> My first reaction is that I don=E2=80=99t see myself my 2K lines (or a su=
bset
> thereof) of .emacs and .gnus in that style.  I can foresee potential
> benefits in terms of composability, but the barrier to entry looks too
> high.  WDYT?
>

I have about 2K lines of it (a lot of it auto-generated by the import
function).  As for the barrier to entry, I think we should hear more
from others.

In my opinion, the benefits of configuring Emacs with Scheme records
like this go beyond composability.  I think that it is cognitively
easier to manage configuration when it is uniform, and the structure
forces us to be more deliberate about what we include and why.  But
maybe that's more of a philosophical debate.

>> Finally, unit tests have been added for the new `(guix read-print)'
>> functionality, and for the "Elisp expression" syntax.  I couldn't make
>> unit tests for anything that builds derivations serializing Elisp,
>> because '%bootstrap-guile' is apparently too old to load `(guix
>> read-print)' on the derivation side.  But most of this has gotten quite
>> a bit of testing, as all of my personal Emacs config is now generated
>> from Scheme.
>
> I think you could write tests using =E2=80=98guix home container=E2=80=99=
 and the host=E2=80=99s
> store, similar to what =E2=80=98tests/guix-home.sh=E2=80=99 is doing.  We=
 don=E2=80=99t have a
> testing strategy for Home services yet, but we should definitely work on
> it.
>

Will look into it.  Testing the `elisp-file' function is important,
because it does all of the serialization.

> That=E2=80=99s it for my initial feedback.  I hope others in the Home and=
 Emacs
> teams will chime in!
>
> Thanks,
> Ludo=E2=80=99.
>
>

Thanks, I appreciate the feedback!

I have a second version of the patch in the works that fixes 3 cosmetic
issues, in case people noticed: 2 comments are confusing and shouldn't
be there (code changed but comments didn't), and one line of the
documentation in `guix.texi' wasn't properly filled.  The actual code
has been unchanged for 2 months, even with regular use.

Also, the example snippet I gave in the original patch message has an
error:

--8<---------------cut here---------------start------------->8---
       ;; ...
       (emacs-server
        (name "sandbox")
        ;; Server gets its own subdirectory of `user-emacs-directory'
        ;; when inheritance is disabled.
        (inherit-directory? #f)
        ;; Server still inherits configuration from non-server Emacsen
        ;; unless inheritance is explicitly disabled.
        (inherit-init? #f)
        ;; ...
        (default-init
          (emacs-configuration
           (variables
            `(;; ...
              ;; Individualized `user-emacs-directory' gets symlinks
              ;; to all `extra-files' from the `emacs-configuration'
              ;; used by other Emacsen, so the files can still be
              ;; referenced.
              (mail-signature-file
               . ,(elisp (locate-user-emacs-file
                          "signature")))))
              ;; ...
        ))
        ;; ...
        )
--8<---------------cut here---------------end--------------->8---

`inherit-init?' should be set to true. When `inherit-init?' is false,
the `signature' file (which was previously created for the non-server
Emacsen) will not be created automatically in the Emacs server's user
directory.  When it is true, all of the files that Guix Home manages in
the main Emacs user directory are duplicated in the server's user
directory, to ensure that any references to those files in the inherited
configuration are still valid.

...Again, maybe this is one of the confusing features that could be
simplified.

--=20
Kierin Bell
GPG Key: FCF2 5F08 EA4F 2E3D C7C3  0D41 D14A 8CD3 2D97 0B36




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 23 Aug 2023 10:02:22 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Wed Aug 23 06:02:22 2023
Received: from localhost ([127.0.0.1]:32865 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qYkgv-0003WZ-RP
	for submit <at> debbugs.gnu.org; Wed, 23 Aug 2023 06:02:22 -0400
Received: from eggs.gnu.org ([2001:470:142:3::10]:54970)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <ludo@HIDDEN>) id 1qYkgt-0003WK-34
 for 64620 <at> debbugs.gnu.org; Wed, 23 Aug 2023 06:02:20 -0400
Received: from fencepost.gnu.org ([2001:470:142:3::e])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <ludo@HIDDEN>)
 id 1qYkgh-0005ZT-Is; Wed, 23 Aug 2023 06:02:07 -0400
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org;
 s=fencepost-gnu-org; h=MIME-Version:In-Reply-To:Date:References:Subject:To:
 From; bh=5PTwwTikLnFalgK5yBgyBOIKzmmEX9KheoCHiT1mq3I=; b=r0pG5iwA3t5XZKNS8vxX
 VjzsM2UjJthlrCvVLPZ/8Zl5OD/IgYbfHFJrlT7fJjwxCz4fdnQXoQr7IqwJzScJgIFqG2LrJRKdb
 5vMxR6Z1p/cyKAaWjRKjIdlo2wJnRRmbBKb2MY6R3cyZCGts/ZzSRd1go9uMC1ByXif0RY3YlWm7g
 yyxPKgcILvDuAmtg7+R9txJHsP9uIglLiMFEuIAj1XO0zCt/NwQ7Zwm8VOoJukLwbDVSa7r9hlazX
 eK8WHk4tIE+Vb4mV7FhMzzAqiKuwA1OJ31Vr2vjJJkMqmqxw9fM2/aO/P3FtXWcKj+657St4DuuL/
 srzLoQ7dxabTRQ==;
From: =?utf-8?Q?Ludovic_Court=C3=A8s?= <ludo@HIDDEN>
To: fernseed@HIDDEN
Subject: Re: bug#64620: [PATCH] gnu: home: Add home-emacs-service-type.
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
Date: Wed, 23 Aug 2023 12:01:51 +0200
In-Reply-To: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
 (fernseed@HIDDEN's message of "Fri, 14 Jul 2023 11:12:31 -0400")
Message-ID: <87v8d613hs.fsf@HIDDEN>
User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/28.2 (gnu/linux)
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
X-Spam-Score: -2.3 (--)
X-Debbugs-Envelope-To: 64620
Cc: , 64620 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -3.3 (---)

Hi Kierin,

This is a truly impressive piece of work!

fernseed@HIDDEN skribis:

> This patch builds on patches from ( and David Wilson for a
> `home-emacs-service-type' (https://issues.guix.gnu.org/58693,
> https://issues.guix.gnu.org/60753, https://issues.guix.gnu.org/62549).
>
> Many of the features of the prior patches have been included, but the
> major focus here is to configure Emacs in Scheme rather than symlinking
> to existing configuration files.

OK, interesting.  This seems to be one of the main questions: how far
should we go on the Scheme side?

In <https://issues.guix.gnu.org/62549>, unmatched-paren chose to not
generate elisp at all from Scheme.  The advantage is that the
implementation is simpler; as a user, the model one has to have in mind
is also simpler: you=E2=80=99re still configuring most things the tradition=
al
way in elisp.  That=E2=80=99s also its downside: you have to do plumbing on=
 the
elisp side, when Guix Home in some cases would know what to do.

I don=E2=80=99t have the answer and I=E2=80=99m not sure what I=E2=80=99d p=
refer, but I=E2=80=99m trying
to see the tradeoffs and to map out the design space.

> Here are some of the broad strokes:
>
> * The following record types have been introduced to encapsulate
>   configuration for Emacs: `emacs-configuration' (for general
>   configuration), `emacs-package' (for package-specific configuration),
>   `emacs-keymap' (for configuration of local keymaps), and
>   `emacs-server' (for configuration of Emacs servers).

Why special-case keymaps, of all the things one might one to configure
in Emacs?  I understand it=E2=80=99s one of the first things one may want to
tweak, but then why not add <emacs-theme> as well, etc.; IOW, where do
we draw the line?

> * Most configuration fields are either flat lists or alists that are
>   considerably abstracted from their final serialized Elisp
>   representation, but escape hatches are provided for both pulling in
>   existing configuration files and specifying s-expressions directly.

Are seasoned Emacsers not going to be frustrated because of this?  :-)

They might prefer to have full access to elisp.

> * All serialized Elisp is pretty-printed much how we would expect to see
>   it in Emacs (for example, with proper indentation according to the
>   `lisp-indent-function' symbol property, etc.).  This has been
>   accomplished by adding a new keyword argument to
>   `pretty-print-with-comments' from `(guix read-print)', among other
>   improvements.

Fun.  I=E2=80=99d like to see how we can avoid spreading elisp conditionals=
 in
(guix read-print).

> * Emacs package configuration can either be serialized as `use-package'
>   forms or as equivalent, more minimalist s-expressions.  Users can
>   define their own package serializers, too.
>
> * For specifying s-expressions, an "Elisp expression" syntax has been
>   implemented that is essentially a lighter-weight version G-expressions.
>   (I try to explain why this is helpful in the documentation.)
>
> * A reader extension has been implemented that allows for "Elisp
>   expressions" to be specified directly with Elisp read syntax, and
>   Scheme values (including file-like objects or G-expressions) can in
>   turn be "unquoted" within that Elisp code.  Also, comments and
>   whitespace can be included within the Elisp code via the `#;'
>   (comment), `#>' (newline), and `;^L' (page break) forms.

Great that you=E2=80=99re putting (language elisp parser) to good use!

> * Each Emacs server has its own user init and early init files, which
>   can optionally inherit configuration from the init files used by
>   non-server Emacsen.  Each server can also inherit the "main"
>   `user-emacs-directory', or it can use its own subdirectory.
>
> * The `home-emacs-service-type' can be extended, with subordinate
>   configuration records being merged intelligently when possible.

Very nice.

> * A utility function has been provided for generating the aforementioned
>   Scheme records from an existing Emacs init file:
>   `elisp-file->home-emacs-configuration'.

Neat; perhaps =E2=80=98guix home import=E2=80=99 could use it?

> (define %gnus-init-file
>   (elisp-file "gnus.el"
>               (list
>                (elisp (setq gnus-select-method '(nnnil "")))
>                (elisp (setq gnus-secondary-select-methods
>                             '((nnml "")
>                               (nntp "news.gmane.io"))))
>                (elisp (setq mail-sources
>                             '((imap :server "mail.example.net"
>                                     :user "user@HIDDEN"
>                                     :port 993
>                                     :stream tls))))
>                ;; Elisp reader extension
>                #%(define-key global-map [remap compose-mail] #;comment
>                    '#$%my-function-name nil))))

Could I write:

  #%(progn
      (setq x =E2=80=A6)
      (setq y =E2=80=A6)
      (define-key =E2=80=A6))

?  That would seem nicer.

#%(body =E2=80=A6) is short for (elisp body =E2=80=A6) right?


[...]

>      (configured-packages
>       (list
>        (emacs-package
>         (name 'windmove)
>         ;; Autoload a function used by `my--display-buffer-down'.
>         (autoloads '(windmove-display-in-direction))
>         (keys-override
>          '(("C-M-<left>" . windmove-left)
>            ("C-M-<right>" . windmove-right)
>            ("C-M-<up>" . windmove-up)
>            ("C-M-<down>" . windmove-down)
>            ("C-x <down>"
>             . my--display-buffer-down)))
>         (keys-local
>          (list
>           (emacs-keymap
>            (name 'windmove-repeat-map)
>            (repeat? #t)
>            (keys '(("<left>" . windmove-left)
>                    ("<right>" . windmove-right)
>                    ("<up>" . windmove-up)
>                    ("<down>" . windmove-down))))))

My first reaction is that I don=E2=80=99t see myself my 2K lines (or a subs=
et
thereof) of .emacs and .gnus in that style.  I can foresee potential
benefits in terms of composability, but the barrier to entry looks too
high.  WDYT?

> Finally, unit tests have been added for the new `(guix read-print)'
> functionality, and for the "Elisp expression" syntax.  I couldn't make
> unit tests for anything that builds derivations serializing Elisp,
> because '%bootstrap-guile' is apparently too old to load `(guix
> read-print)' on the derivation side.  But most of this has gotten quite
> a bit of testing, as all of my personal Emacs config is now generated
> from Scheme.

I think you could write tests using =E2=80=98guix home container=E2=80=99 a=
nd the host=E2=80=99s
store, similar to what =E2=80=98tests/guix-home.sh=E2=80=99 is doing.  We d=
on=E2=80=99t have a
testing strategy for Home services yet, but we should definitely work on
it.

That=E2=80=99s it for my initial feedback.  I hope others in the Home and E=
macs
teams will chime in!

Thanks,
Ludo=E2=80=99.




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at 64620 <at> debbugs.gnu.org:


Received: (at 64620) by debbugs.gnu.org; 22 Jul 2023 02:45:41 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Fri Jul 21 22:45:41 2023
Received: from localhost ([127.0.0.1]:35139 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qN2cm-0008CV-OW
	for submit <at> debbugs.gnu.org; Fri, 21 Jul 2023 22:45:41 -0400
Received: from mail.envs.net ([5.199.136.28]:52336)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <iyzsong@HIDDEN>) id 1qN2cj-000809-Ra
 for 64620 <at> debbugs.gnu.org; Fri, 21 Jul 2023 22:45:39 -0400
Received: from localhost (mail.envs.net [127.0.0.1])
 by mail.envs.net (Postfix) with ESMTP id 14CFE38A18D8;
 Sat, 22 Jul 2023 02:45:35 +0000 (UTC)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=envs.net; s=modoboa;
 t=1689993935; bh=p9nITU2vGDjAs4LWee9oEcrnWuIhzBuIf8txtpG2fqw=;
 h=From:To:Cc:Subject:References:Date:In-Reply-To:From;
 b=fpdMmx3fx+H5/KStbVnkg7fEz/s+Es5Cc0Y/EPdJIbytlPaNIm+tWgE2l40iCW2RS
 WIu1sEaOWN94PL351he9XrgvfAZ5LoYfwzt7Qbut5mb49pNWLIPfVD4iyyItY9qAhI
 n+KL0cjkiPWzPM45tBYTK68toeRA/1bh34auMQnZqZxc95NWvqJts1ZUo78OcaE9/J
 2jgPLH4TU43yMfdMCcFtoL8KBu1npWqJRgcqGuadcAL/tEjvrGnSZamFGSZOUSafM7
 x7dU9hyMjCWLSiic+bgf59yUIzJi756RW2dnYdn5vvmEXfJHMU4/tMjlsrOYhHg4wM
 j/SsHenvijNsFjullpac0W00ToaU4I/yWIBeIGrh2YS/5rOX0Y9iomjjy6+rSsErwH
 s3MfQxy7i3h7VYkwhUyNsX9cmKT8slqJC++A6y48f8JKA6bYmeTOf8MRzGWg8a1vMM
 3HLffIC2kmUfxTpPed+YRhISOt4v2zAd1jou0Apy4gNpU0UZ/s+xit3UYdV3jDTNnk
 fDHvjF/vff12d043D1k/yTtwkwGihkmYsP9kAJS6dGOJY+Bg8Tg5kN2ZHlZLUL2v1A
 xMyUSehtIpmC+rTSUZjtCkr+E0A7oHoRgwo5Ikd9eqEf0p6CUHcqtMs7B0iyQuzMgN
 eq2vSiELKD4Re6r0YJsOOtfY=
X-Virus-Scanned: Debian amavisd-new at mail.envs.net
Received: from mail.envs.net ([127.0.0.1])
 by localhost (mail.envs.net [127.0.0.1]) (amavisd-new, port 10026)
 with ESMTP id HvabGmnjJkFB; Sat, 22 Jul 2023 02:45:31 +0000 (UTC)
Received: from localhost (unknown [117.174.235.109])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange ECDHE (P-256) server-signature RSA-PSS (2048 bits) server-digest
 SHA256) (No client certificate requested)
 by mail.envs.net (Postfix) with ESMTPSA;
 Sat, 22 Jul 2023 02:45:29 +0000 (UTC)
Received: from localhost (localhost [local])
 by localhost (OpenSMTPD) with ESMTPA id 2781502c;
 Sat, 22 Jul 2023 02:45:20 +0000 (UTC)
From: =?utf-8?B?5a6L5paH5q2m?= <iyzsong@HIDDEN>
To: fernseed@HIDDEN
Subject: Re: bug#64620: [PATCH] gnu: home: Add home-emacs-service-type.
References: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
Date: Sat, 22 Jul 2023 10:45:20 +0800
In-Reply-To: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
 (fernseed@HIDDEN's message of "Fri, 14 Jul 2023 11:12:31 -0400")
Message-ID: <878rb8mzpb.fsf@HIDDEN>
User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/28.2 (gnu/linux)
MIME-Version: 1.0
Content-Type: text/plain
X-Spam-Score: -0.0 (/)
X-Debbugs-Envelope-To: 64620
Cc: 64620 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.0 (-)

fernseed@HIDDEN writes:

> This patch builds on patches from ( and David Wilson for a
> `home-emacs-service-type' (https://issues.guix.gnu.org/58693,
> https://issues.guix.gnu.org/60753, https://issues.guix.gnu.org/62549).
>
> Many of the features of the prior patches have been included, but the
> major focus here is to configure Emacs in Scheme rather than symlinking
> to existing configuration files.

Indeed a lot, I'm very interested in this, will play with it for some
time.

Thank you!




Information forwarded to guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.

Message received at submit <at> debbugs.gnu.org:


Received: (at submit) by debbugs.gnu.org; 14 Jul 2023 15:49:13 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Fri Jul 14 11:49:13 2023
Received: from localhost ([127.0.0.1]:43279 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1qKL2e-0005IW-VZ
	for submit <at> debbugs.gnu.org; Fri, 14 Jul 2023 11:49:13 -0400
Received: from lists.gnu.org ([2001:470:142::17]:35864)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <fernseed@HIDDEN>) id 1qKKU2-0004NY-H2
 for submit <at> debbugs.gnu.org; Fri, 14 Jul 2023 11:13:26 -0400
Received: from eggs.gnu.org ([2001:470:142:3::10])
 by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <fernseed@HIDDEN>)
 id 1qKKTw-00066P-Rb
 for guix-patches@HIDDEN; Fri, 14 Jul 2023 11:13:21 -0400
Received: from relay9-d.mail.gandi.net ([2001:4b98:dc4:8::229])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <fernseed@HIDDEN>)
 id 1qKKTq-0002Kk-Mn
 for guix-patches@HIDDEN; Fri, 14 Jul 2023 11:13:20 -0400
Received: by mail.gandi.net (Postfix) with ESMTPSA id 30410FF80A;
 Fri, 14 Jul 2023 15:13:07 +0000 (UTC)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=fernseed.me; s=gm1;
 t=1689347589;
 h=from:from:reply-to:subject:subject:date:date:message-id:message-id:
 to:to:cc:cc:mime-version:mime-version:content-type:content-type:
 content-transfer-encoding:content-transfer-encoding;
 bh=3oKlRbz4aPm4+/eMNnPt5pWIkX+cEzh4XNqnBqIEft0=;
 b=SiXrzqmAyDe8KmmgzR0QoSF4Ri48Hrc/zsDCRAyRS1rKqOruDp1ywZ1RMdeL7AGunTJuKf
 FsFFswYCouiFrPB6tJmHw5Ibe8LnQwR5x3rzGLlz34uBJ6+2Fq9nL+GWgCmYMWuDouJvEj
 mSN8CnH48BY8wxVsXtfRZLqYIzQojnJhhf2pvDpX2U/z2dm4ZFovPJhyfNdeMFqu6yDzQ7
 uzq8OgI51g8tOg8bIkRwZhjQIudkjytiGszU93SCXEQGs7LSlcLQ3R8Kqk8Bn/UOsE9gkV
 SIer1wTtKj4o48WIqGjPDtLLgLK1+hMr4M8y5pVeaBERPUyP2vLEj1B5l0hBIA==
From: fernseed@HIDDEN
To: guix-patches@HIDDEN
Subject: [PATCH] gnu: home: Add home-emacs-service-type.
Date: Fri, 14 Jul 2023 11:12:31 -0400
Message-Id: <0173e076aafb6ec389a7ebca5d56b7f4e8a02b6e.1689347338.git.fernseed@HIDDEN>
X-Mailer: git-send-email 2.40.1
MIME-Version: 1.0
X-Debbugs-Cc: ( <paren@HIDDEN>, Andrew Tropin <andrew@HIDDEN>, Christopher Baines <mail@HIDDEN>, Josselin Poiret <dev@HIDDEN>, Ludovic Courtès <ludo@HIDDEN>, Mathieu Othacehe <othacehe@HIDDEN>, Ricardo Wurmus <rekado@HIDDEN>, Simon Tournier <zimon.toutoune@HIDDEN>, Tobias Geerinckx-Rice <me@HIDDEN>
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
X-GND-Sasl: fernseed@HIDDEN
Received-SPF: pass client-ip=2001:4b98:dc4:8::229;
 envelope-from=fernseed@HIDDEN; helo=relay9-d.mail.gandi.net
X-Spam_score_int: -23
X-Spam_score: -2.4
X-Spam_bar: --
X-Spam_report: (-2.4 / 5.0 requ) BAYES_00=-1.9, DKIM_INVALID=0.1,
 DKIM_SIGNED=0.1, RCVD_IN_DNSWL_LOW=-0.7, SPF_HELO_PASS=-0.001, SPF_PASS=-0.001,
 T_SCC_BODY_TEXT_LINE=-0.01 autolearn=ham autolearn_force=no
X-Spam_action: no action
X-Debbugs-Envelope-To: submit
X-Mailman-Approved-At: Fri, 14 Jul 2023 11:49:11 -0400
Cc: Kierin Bell <fernseed@HIDDEN>
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>

From: Kierin Bell <fernseed@HIDDEN>

* gnu/home/services/emacs.scm: New file.
* gnu/local.mk (GNU_SYSTEM_MODULES): Add new file.
* tests/home/services/emacs.scm: New tests file.
* Makefile.am (SCM_TESTS): Add new tests file.
* doc/guix.texi (Emacs Home Services): New node.
* guix/read-print.scm (read-with-comments, read-with-comments/sequence):
Add new ELISP? and UNELISP-EXTENSIONS? keyword arguments to support
reading Elisp.
(%newline-forms): Add `home-emacs-configuration'.
(%elisp-special-forms, %elisp-natural-whitespace-string-forms)
(%elisp-special-symbol-chars, %elisp-confusable-number-symbols)
(%elisp-basic-chars, %elisp-simple-escape-chars): New variables.
(special-form-lead, printed-string, symbol->display-string): Add new
ELISP? keyword argument.
(atom->elisp-string): New helper function.
(pretty-print-with-comments): New ELISP? and SPECIAL-FORMS keyword
arguments to support serialization to Elisp.  General improvements:
enable pretty-printing of alists and improper lists; only print lists of
constants with one element per line when length exceeds LONG-LIST; do
not print newline before special read syntax forms (e.g., `'', `#~',
etc.) unless they would exceed MAX-WIDTH; include backslashes when
calculating whether a string would exceed MAX-WIDTH; do not print
extraneous newline when special form has an empty body; print newlines
after list arguments of special forms; print first argument after
function on newline with same indentation as function when it would
exceed MAX-WIDTH.
* tests/read-print.scm: Add new tests and update old tests which fail
due to improvements.
---

This patch builds on patches from ( and David Wilson for a
`home-emacs-service-type' (https://issues.guix.gnu.org/58693,
https://issues.guix.gnu.org/60753, https://issues.guix.gnu.org/62549).

Many of the features of the prior patches have been included, but the
major focus here is to configure Emacs in Scheme rather than symlinking
to existing configuration files.

Here are some of the broad strokes:

* The following record types have been introduced to encapsulate
  configuration for Emacs: `emacs-configuration' (for general
  configuration), `emacs-package' (for package-specific configuration),
  `emacs-keymap' (for configuration of local keymaps), and
  `emacs-server' (for configuration of Emacs servers).

* Most configuration fields are either flat lists or alists that are
  considerably abstracted from their final serialized Elisp
  representation, but escape hatches are provided for both pulling in
  existing configuration files and specifying s-expressions directly.

* All serialized Elisp is pretty-printed much how we would expect to see
  it in Emacs (for example, with proper indentation according to the
  `lisp-indent-function' symbol property, etc.).  This has been
  accomplished by adding a new keyword argument to
  `pretty-print-with-comments' from `(guix read-print)', among other
  improvements.

* Emacs package configuration can either be serialized as `use-package'
  forms or as equivalent, more minimalist s-expressions.  Users can
  define their own package serializers, too.

* For specifying s-expressions, an "Elisp expression" syntax has been
  implemented that is essentially a lighter-weight version G-expressions.
  (I try to explain why this is helpful in the documentation.)

* A reader extension has been implemented that allows for "Elisp
  expressions" to be specified directly with Elisp read syntax, and
  Scheme values (including file-like objects or G-expressions) can in
  turn be "unquoted" within that Elisp code.  Also, comments and
  whitespace can be included within the Elisp code via the `#;'
  (comment), `#>' (newline), and `;^L' (page break) forms.

* Each Emacs server has its own user init and early init files, which
  can optionally inherit configuration from the init files used by
  non-server Emacsen.  Each server can also inherit the "main"
  `user-emacs-directory', or it can use its own subdirectory.

* The `home-emacs-service-type' can be extended, with subordinate
  configuration records being merged intelligently when possible.

* A utility function has been provided for generating the aforementioned
  Scheme records from an existing Emacs init file:
  `elisp-file->home-emacs-configuration'.

Here's an example configuration for the `home-emacs-service-type'
demonstrating some of these features:

--8<---------------cut here---------------start------------->8---
(use-modules (gnu home)
             (gnu services)
             (guix gexp)
             (gnu home services)
             (gnu home services emacs)
             (gnu packages emacs-xyz)
             (gnu packages file)
             (gnu packages compression))

(define %my-function-name 'my--compose-mail)

(define %gnus-init-file
  (elisp-file "gnus.el"
              (list
               (elisp (setq gnus-select-method '(nnnil "")))
               (elisp (setq gnus-secondary-select-methods
                            '((nnml "")
                              (nntp "news.gmane.io"))))
               (elisp (setq mail-sources
                            '((imap :server "mail.example.net"
                                    :user "user@HIDDEN"
                                    :port 993
                                    :stream tls))))
               ;; Elisp reader extension
               #%(define-key global-map [remap compose-mail] #;comment
                   '#$%my-function-name nil))))

(home-environment
 ;; ...
 (services
  (list
   ;; ...
   (service
    home-emacs-service-type
    (home-emacs-configuration
     (user-emacs-directory "~/.local/state/emacs/")
     (package-serializer %emacs-use-package-serializer)
     (default-init
       (emacs-configuration
        ;; File-likes specified here symlinked in ~/.config/emacs and
        ;; loaded when Emacs starts.
        (extra-init-files
         `(("extra.el"
            . ,(local-file "extra.el"))))
        (variables
         '((initial-scratch-message . #f)
           ;; Symbols values for variables quoted when serialized.
           (confirm-kill-emacs . y-or-n-p)
           ;; Boolean values for variables serialized properly in Elisp.
           (visible-bell . #t)
           ;; Elisp expressions serialized as-is, with no quoting.
           (message-signature-file
            . ,(elisp mail-signature-file))))
        (modes
         '((tool-bar-mode . #f)
           (menu-bar-mode . #f)
           (fringe-mode . 16)
           (repeat-mode . #t)))
        (keys
         '(("C-x C-b" . ibuffer)))
        (keys-override
         '(("M-<up>" . scroll-down-line)
           ("M-<down>" . scroll-up-line)
           ("C-M-S-<up>"
            . my--scroll-other-window-down)
           ("C-M-S-<down>"
            . my--scroll-other-window)))
        (extra-init
         (list
          (elisp (defun my--scroll-other-window-down ()
                   (interactive)
                   (scroll-other-window-down 1)))
          (elisp (defun my--scroll-other-window ()
                   (interactive)
                   (scroll-other-window 1)))))))
     (configured-packages
      (list
       (emacs-package
        (name 'windmove)
        ;; Autoload a function used by `my--display-buffer-down'.
        (autoloads '(windmove-display-in-direction))
        (keys-override
         '(("C-M-<left>" . windmove-left)
           ("C-M-<right>" . windmove-right)
           ("C-M-<up>" . windmove-up)
           ("C-M-<down>" . windmove-down)
           ("C-x <down>"
            . my--display-buffer-down)))
        (keys-local
         (list
          (emacs-keymap
           (name 'windmove-repeat-map)
           (repeat? #t)
           (keys '(("<left>" . windmove-left)
                   ("<right>" . windmove-right)
                   ("<up>" . windmove-up)
                   ("<down>" . windmove-down))))))
        (extra-init
         (list
          (elisp
           (defun my--display-buffer-down (&optional arg buf)
             (interactive
              "P\nbSwitch to buffer in window below: ")
             (windmove-display-in-direction 'down arg)
             (switch-to-buffer buf))))))
       (emacs-package
        (name 'dired)
        ;; External packages used by Dired
        (extra-packages (list file unzip)))))))
   (simple-service
    'emacs-mail-service
    home-emacs-service-type
    (home-emacs-extension
     (default-init
       (emacs-configuration
        ;; File-likes symlinked into `user-emacs-directory', but not
        ;; loaded automatically.
        (extra-files
         `(("gnus.el" . ,%gnus-init-file)
           ("signature" . ,(local-file "signature"))))
        (variables
         `((gnus-init-file
            . ,(elisp (locate-user-emacs-file "gnus.el")))
           (mail-user-agent . gnus-user-agent)
           (read-mail-command . gnus)))))
     (configured-packages
      (list
       (emacs-package
        (name 'message)
        (options
         `((message-send-mail-function
            . smtpmail-send-it)
           (message-signature-file
            . ,(elisp (locate-user-emacs-file
                       "signature"))))))))
     (servers
      (list
       ;; Servers inherit `user-emacs-directory' and init file
       ;; configuration from non-server Emacsen by default.
       (emacs-server
        (name "mail")
        (default-init
          (emacs-configuration
           (extra-init
            (list
             (elisp (add-hook 'server-after-make-frame-hook
                              (function gnus))))))))))))
   (simple-service
    'emacs-sandbox-service
    home-emacs-service-type
    (home-emacs-extension
     (servers
      (list
       (emacs-server
        (name "sandbox")
        ;; Server gets its own subdirectory of `user-emacs-directory'
        ;; when inheritance is disabled.
        (inherit-directory? #f)
        ;; Server still inherits configuration from non-server Emacsen
        ;; unless inheritance is explicitly disabled.
        (inherit-init? #f)
        (inherit-configured-packages? #f)
        ;; Server is started via a Shepherd service automatically,
        ;; unless disabled.
        (auto-start? #f)
        (default-init
          (emacs-configuration
           (variables
            `((initial-scratch-message . #f)
              ;; Individualized `user-emacs-directory' gets symlinks
              ;; to all `extra-files' from the `emacs-configuration'
              ;; used by other Emacsen, so the files can still be
              ;; referenced.
              (mail-signature-file
               . ,(elisp (locate-user-emacs-file
                          "signature")))))
           (extra-init (list (elisp (ding))))))
        (configured-packages
         (list
          ;; Configure a theme specifically for the "sandbox" server.
          (emacs-package
           (name 'modus-themes)
           (package emacs-modus-themes)
           (extra-init
            (list
             (elisp (load-theme 'modus-operandi-tinted)))))))))))))))
--8<---------------cut here---------------end--------------->8---

Finally, unit tests have been added for the new `(guix read-print)'
functionality, and for the "Elisp expression" syntax.  I couldn't make
unit tests for anything that builds derivations serializing Elisp,
because '%bootstrap-guile' is apparently too old to load `(guix
read-print)' on the derivation side.  But most of this has gotten quite
a bit of testing, as all of my personal Emacs config is now generated
from Scheme.

The patch is to the point where I'd like to get some feedback, and see
if this is something that could be included into Guix.

 Makefile.am                   |    2 +
 doc/guix.texi                 | 1178 +++++++++++++
 gnu/home/services/emacs.scm   | 3040 +++++++++++++++++++++++++++++++++
 gnu/local.mk                  |    2 +
 guix/read-print.scm           |  995 +++++++++--
 tests/home/services/emacs.scm |  345 ++++
 tests/read-print.scm          |  239 ++-
 7 files changed, 5654 insertions(+), 147 deletions(-)
 create mode 100644 gnu/home/services/emacs.scm
 create mode 100644 tests/home/services/emacs.scm

diff --git a/Makefile.am b/Makefile.am
index a386e6033c..7b5c67e26b 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -17,6 +17,7 @@
 # Copyright © 2020, 2021, 2023 Maxim Cournoyer <maxim.cournoyer@HIDDEN>
 # Copyright © 2021 Chris Marusich <cmmarusich@HIDDEN>
 # Copyright © 2021 Andrew Tropin <andrew@HIDDEN>
+# Copyright © 2023 Kierin Bell <fernseed@HIDDEN>
 #
 # This file is part of GNU Guix.
 #
@@ -524,6 +525,7 @@ SCM_TESTS =					\
   tests/hackage.scm				\
   tests/home-import.scm				\
   tests/home-services.scm			\
+  tests/home/services/emacs.scm			\
   tests/http-client.scm				\
   tests/import-git.scm				\
   tests/import-github.scm			\
diff --git a/doc/guix.texi b/doc/guix.texi
index 9af1b4417b..f1958cc695 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -116,6 +116,7 @@
 Copyright @copyright{} 2023 Karl Hallsby@*
 Copyright @copyright{} 2023 Nathaniel Nicandro@*
 Copyright @copyright{} 2023 Tanguy Le Carrour@*
+Copyright @copyright{} 2023 Kierin Bell@*
 
 Permission is granted to copy, distribute and/or modify this document
 under the terms of the GNU Free Documentation License, Version 1.3 or
@@ -440,6 +441,7 @@ Top
 * SSH: Secure Shell.            Setting up the secure shell client.
 * GPG: GNU Privacy Guard.       Setting up GPG and related tools.
 * Desktop: Desktop Home Services.  Services for graphical environments.
+* Emacs: Emacs Home Services.   Services for configuring Emacs.
 * Guix: Guix Home Services.     Services for Guix.
 * Fonts: Fonts Home Services.   Services for managing User's fonts.
 * Sound: Sound Home Services.   Dealing with audio.
@@ -42593,6 +42595,7 @@ Home Services
 * SSH: Secure Shell.            Setting up the secure shell client.
 * GPG: GNU Privacy Guard.       Setting up GPG and related tools.
 * Desktop: Desktop Home Services.  Services for graphical environments.
+* Emacs: Emacs Home Services.   Services for configuring Emacs.
 * Guix: Guix Home Services.     Services for Guix.
 * Fonts: Fonts Home Services.   Services for managing User's fonts.
 * Sound: Sound Home Services.   Dealing with audio.
@@ -43819,6 +43822,1181 @@ Desktop Home Services
 @end table
 @end deftp
 
+@node Emacs Home Services
+@subsection Emacs Home Services
+
+The @code{(gnu home services emacs)} module provides services for
+configuring the GNU Emacs extensible text editor.
+
+@cindex Elisp expressions, for Emacs home services
+Emacs is configured by providing @dfn{initialization files} that contain
+@dfn{s-expressions} written in @dfn{Emacs Lisp} (abbreviated as
+@dfn{Elisp}) which are evaluated when Emacs is started (@pxref{Init
+File,,, emacs, The GNU Emacs Manual}).
+
+The main home service type for configuring Emacs, the
+@code{home-emacs-service-type} (see below), provides three ways to
+specify expressions for Emacs initialization files:
+
+@itemize
+@item
+File-like objects that contain Elisp can be directly referenced so that
+Emacs will evaluate their contents upon initialization (see also
+@code{elisp-file} below, which specifically creates Emacs Lisp files).
+
+@item
+Expressions can be written in Scheme using a special syntax (see the
+@code{elisp} form below), so that they will be serialized to Emacs
+initialization files---with some minor transformations---as Elisp.  This
+is possible because Scheme and Emacs Lisp have very similar read
+syntaxes.
+
+@item
+Finally, some configuration fields provide an additional layer of
+abstraction that transforms Scheme values into more complex Elisp
+expressions that do meaningful things with those values.
+@end itemize
+
+For the latter two options, the @code{(gnu home services emacs)} module
+introduces a mechanism for explicitly specifying an s-expression that
+should be serialized as Elisp and evaluated by Emacs: @dfn{Elisp
+expressions}.  Elisp expressions have their own data type (see
+@code{elisp?}), and they must be created by using the @code{elisp} or
+@code{#%} forms (see below), or by using other functions provided by the
+module for constructing them.  Whenever the term ``Elisp expression''
+occurs in the documentation for the Emacs home service, it is an
+indication that Elisp expressions of this type should be used or that
+they have a special meaning compared with other Scheme values like
+symbols or lists.
+
+To illustrate why we would need to use Elisp expression objects when
+configuring Emacs instead of simply writing s-expressions as we normally
+would in Scheme, consider the @code{variables} field of the
+@code{emacs-configuration} record type, an association list that
+associates Emacs variable names---given as Scheme symbols---with values.
+
+When this field is serialized to the Emacs user initialization file,
+Elisp expressions that set the variables to their corresponding values
+are generated from this association list.  But there is a problem: How
+do we differentiate values that should be serialized as constants
+---e.g., by using the @code{quote} syntax---from s-expressions that
+should be @emph{evaluated} by Emacs?  If a given value is of a Scheme
+data type that corresponds to a self-quoting data type in Elisp---for
+example, a number or a string---then there is no ambiguity.  But what if
+a symbol or a list value is given?  Should it be interpreted by Emacs as
+a quoted constant, or should it be interpreted as an unquoted
+s-expression to be evaluated at initialization time?
+
+The solution chosen here is that all values of fields for which this
+ambiguity exists are serialized to Elisp as constants, unless an Elisp
+expression is explicitly used.  Whenever explicit Elisp expressions
+occur in the configuration for a service, though, we can be sure that
+they will be serialized directly to Emacs initialization files as
+s-expressions that will be evaluated by Emacs.
+
+This is best illustrated by an example.  The following configuration
+produces Elisp code that sets the @code{message-signature-file} Emacs
+variable to the value of the @code{mail-signature-file} variable when
+Emacs is initialized:
+
+@lisp
+(emacs-configuration
+ (variables `((message-signature-file
+               . ,(elisp mail-signature-file)))))
+@end lisp
+
+@noindent
+while the example below sets the @code{message-signature-file} variable to the @emph{symbol} @code{mail-signature-file}, which is not what we want:
+
+@lisp
+(emacs-configuration
+ (variables '((message-signature-file
+               . mail-signature-file))))
+@end lisp
+
+Additionally, Elisp expressions can be specified using the @code{#%}
+form, which allows for Elisp code to be embedded within Scheme (see
+documemtation below for more).  The following is equivalent to the first
+example above:
+
+@lisp
+(emacs-configuration
+ (variables `((message-signature-file
+               . ,#%mail-signature-file)))))
+@end lisp
+
+In many ways, Elisp expressions are similar to G-expressions
+(@pxref{G-Expressions}).  Elisp expressions can in fact be thought of as
+an abstraction around G-expressions.  After all, before any Elisp
+expression can be serialized to a file by a service, it must first be
+transformed into a G-expression so that a derivation can be generated
+(@pxref{Derivations}).
+
+For this reason, any value that is a valid input for a G-expression can
+be referenced within an Elisp expression (see @code{unelisp} and
+@code{unelisp-splicing} below).  Data types that ``compile'' and are
+specially substituted in G-expressions, such as file-like objects
+(@pxref{G-Expressions, file-like objects}), will be substituted in the
+same exact way when they are referenced within Elisp expressions.  Even
+G-expressions themselves can be embedded within Elisp expressions.
+
+On the other hand, when Elisp expressions are referenced manually within
+G-expressions (e.g., with @code{ungexp}), some of the expressive power
+of Elisp expressions is lost, as explained below: comments, newlines,
+and page-breaks are stripped.
+
+@defmac #%@var{exp}
+@defmacx (elisp @var{exp})
+Return an Elisp expression containing @var{exp}.
+
+The @code{#%} form is special: the s-expression that follows it will not
+be read as Scheme; it will be read as Elisp.  In this way, it is
+possible to fully embed Elisp code within a Scheme file. For example, the following Elisp expressions are equivalent when serialized:
+
+@lisp
+(elisp (define-key global-map #(remap list-buffers) 'ibuffer #f))
+@end lisp
+
+@lisp
+#%(define-key global-map [remap list-buffers] 'ibuffer nil)
+@end lisp
+
+But, keep in mind, any Scheme syntax that is invalid in Elisp will cause
+an error to be signaled by Guile's Elisp reader.  To embed Scheme within
+Elisp that is in turn embedded in Scheme, you can use the @code{#$} and
+@code{#$@@} reader extensions (see below).
+
+Expressions within @var{exp} are constants rather than expressions that
+are evaluated for their Scheme values---as if the expressions were
+quoted using the @code{quote} syntax---unless they are ``unquoted'' with
+one of the following two forms:
+
+@table @code
+@item #$@var{exp}
+@itemx (unelisp @var{exp})
+Include the value of @var{exp} in an @code{elisp} or @code{#%} form.
+
+@var{exp} is an s-expression, given in Scheme, that is evaluated, and
+the resulting value is included within the containing form.  Any values
+that may appear within G-expressions are valid, and any substitutions
+that would be made when ``compiling'' a G-expression containing
+references to the given values will also be made when the resulting
+Elisp expression is serialized to a file.
+
+If the result of evaluating @var{exp} is a list, it is traversed and all
+relevant substitutions are similarly performed.
+
+If the result of evaluating @var{exp} is another Elisp expression, its
+contents are inserted, with the relevant references included as above.
+
+@item #$@@@var{lst}
+@itemx (unelisp-splicing @var{lst})
+Like the above, but splices the contents of @var{lst} inside the
+containing expression (which must itself be a list).
+@end table
+
+Additionally, the following forms allow for the inclusion of comments
+and whitespace into Elisp expressions:
+
+@table @code
+@item #;@var{comment}
+@itemx (unelisp-comment @var{comment})
+Insert a comment containing the string @var{comment} into the containing
+expression.
+
+With the @code{#;} form, @var{comment} comprises all text up to the
+first newline, whereas with the @code{unelisp-comment} form,
+@var{comment} must be a proper string that begins with @samp{;} and ends
+with a newline character.  Thus, the following two Elisp expressions are
+equivalent (note the newline at the end of the second example):
+
+@lisp
+(elisp (unelisp-comment ";;; Comment\n"))
+@end lisp
+
+@lisp
+#%#;;; Comment
+
+@end lisp
+
+@noindent
+When the containing Elisp expression is serialized to an Elisp file (see
+@code{elisp-file}), the comment is pretty-printed as it occurs.
+However, when an Elisp expression is referenced within a G-expression
+manually (e.g., using the @code{ungexp} syntax), all comments specified
+with these forms are lost.  This is because comments cannot normally be
+``compiled'' into a substitution while lowering a G-expression.
+
+@item #>
+@itemx (unelisp-newline)
+Insert a newline into the containing expression.
+
+When an Elisp expression is serialized to an Elisp file, newlines are
+inserted where they occur.  But, as with @code{unelisp-comment},
+newlines specified using this syntax are removed when an Elisp
+expression is referenced manually within a G-expression.
+
+@item #^L
+@itemx (unelisp-page-break)
+Insert a page-break character into the containing expression.
+
+When an Elisp expression is serialized to an Elisp file, page-break
+characters are inserted where they occur, but, again, they are removed
+when an Elisp expression is manually referenced within a G-expression.
+@end table
+@end defmac
+
+@deffn {Procedure} elisp? obj
+Return true if @var{obj} is an Elisp expression object.
+@end deffn
+
+@deffn {Procedure} elisp->sexp exp
+Return an s-expression containing the contents of Elisp expression
+@var{exp}.
+@end deffn
+
+@deffn {Procedure} sexp->elisp sexp
+Return an Elisp expression object containing @var{sexp}.
+@end deffn
+
+@cindex Elisp files
+Once we have some Elisp expressions, we need to be able to serialize
+them to an Elisp file.  Usually, we provide Elisp expressions as values
+of configuration fields for the Emacs home service, which automatically
+serializes them to the appropriate Emacs initialization files.  However,
+we can also serialize Elisp expressions directly to arbitrary files
+ourselves.  The @code{elisp-file} procedure takes Elisp expressions and
+returns a file-like object ensuring that the expressions will be
+pretty-printed as Elisp---comments, newlines and all.
+
+@deffn {Procedure} elisp-file name exps [#:special-forms ()]
+Return an object representing the store file @var{name}, an Emacs Lisp
+file that contains @var{exps}, a list of Elisp expression objects or
+G-expressions.
+
+Custom indentation rules can be specified with @var{special-forms}, an
+association list where each entry is of the form:
+
+@lisp
+(@var{symbol} . @var{indent})
+@end lisp
+
+@noindent
+When @var{symbol} occurs at the beginning of a list in an expression in
+@var{exps}, the first @var{indent} expressions after @var{symbol} are
+indented as arguments and the remainder are indented as body
+expressions, as if @var{indent} was the value of the
+@code{lisp-indent-function} symbol property for @var{symbol} in Emacs
+(@pxref{Indenting Macros,,,elisp,The Emacs Lisp Manual}).  As in Emacs,
+argument expressions, if they cannot be pretty-printed on the same line
+as @var{symbol}, are indented 4 columns beyond the base indentation of
+the enclosing list, and body expressions are indented 2 columns beyond
+the base indentation.
+
+This is the declarative counterpart of @code{elisp-file*}.
+@end deffn
+
+@deffn {Procedure} elisp-file* name exps [#:special-forms ()]
+Return as a monadic value a derivation that builds an Elisp file named
+@var{name} containing the expressions in @var{exps}, a list of Elisp
+expression objects or G-expressions.
+
+This is the monadic counterpart of @code{elisp-file}, which see for a
+description of @var{special-forms}.
+@end deffn
+
+@deffn {Procedure} elisp-file? obj
+Return true if @var{obj} is an Elisp file object.
+@end deffn
+
+@defvar home-emacs-service-type
+This is the primary service type for configuring Emacs. Its value is a
+@code{home-emacs-configuration} record, which in turn can contain up to
+four subordinate configuration record types:
+
+@itemize
+@item
+@code{emacs-package}, a record type that associates an Emacs package or
+library---specifically, a ``named feature'' provided by an Emacs package
+(@pxref{Named Features,,,elisp,The Emacs Lisp Manual})---with a Guix
+package and any relevant Elisp configuration.  Thus, the
+@code{emacs-package} record can encapsulate all configuration for an
+Emacs package in an atomic way.
+
+@item
+@code{emacs-configuration}, a record type used to specify general
+configuration for the Emacs initialization files, such as configuration
+that is not specific to any single Emacs package.
+
+@item
+@code{emacs-server}, a record type used to specify configuration for
+Emacs servers (@pxref{Emacs Server,,, emacs,The GNU Emacs Manual}).
+
+@item
+@code{emacs-package-serializer}, a record type used to control how
+@code{emacs-package} objects are serialized to the Emacs user
+initialization file. There are currently two predefined package
+serializers to choose from, @code{%emacs-simple-package-serializer} and
+@code{%emacs-use-package-serializer}, but you can define your own as
+well.
+@end itemize
+
+Here is a sample Guix home configuration that utilizes the
+@code{home-emacs-service-type}:
+
+@lisp
+(use-modules (gnu home)
+             (gnu services)
+             (guix gexp)
+             (gnu home services)
+             (gnu home services emacs)
+             (gnu packages emacs-xyz)
+             (gnu packages aspell)
+             (gnu packages file)
+             (gnu packages compression))
+
+(home-environment
+ (services
+  (list
+   (service
+    home-emacs-service-type
+    (home-emacs-configuration
+     (user-emacs-directory "~/.local/state/emacs/")
+     (package-serializer %emacs-use-package-serializer)
+     (default-init
+       (emacs-configuration
+        (variables
+         '((confirm-kill-emacs . y-or-n-p)
+           (visible-bell . #t)
+           (initial-scratch-message . #f)))
+        (modes
+         '((tool-bar-mode . #f)
+           (tooltip-mode . #f)
+           (menu-bar-mode . #f)
+           (fringe-mode . 16)
+           (repeat-mode . #t)))
+        (keys
+         '(("C-x C-b" . ibuffer)))
+        (keys-override
+         '(("M-<up>" . scroll-down-line)
+           ("M-<down>" . scroll-up-line)
+           ("C-M-S-<up>"
+            . my--scroll-other-window-down)
+           ("C-M-S-<down>"
+            . my--scroll-other-window)))
+        (extra-init
+         (list
+          (elisp (defun my--scroll-other-window-down ()
+                   (interactive)
+                   (scroll-other-window-down 1)))
+          (elisp (defun my--scroll-other-window ()
+                   (interactive)
+                   (scroll-other-window 1)))))))
+     (configured-packages
+      (list
+       (emacs-package
+        (name 'flyspell)
+        ;; `flyspell' is built into Emacs, but the dependencies
+        ;; `aspell' and `aspell-dict-en' are not.
+        (extra-packages
+         (list aspell aspell-dict-en))
+        (hooks
+         '((prog-mode-hook . flyspell-prog-mode))))
+       (emacs-package
+        (name 'windmove)
+        ;; Autoload a function used by `my--display-buffer-down'.
+        (autoloads
+         '(windmove-display-in-direction))
+        (keys-override
+         '(("C-M-<left>" . windmove-left)
+           ("C-M-<right>" . windmove-right)
+           ("C-M-<up>" . windmove-up)
+           ("C-M-<down>" . windmove-down)
+           ("C-x <down>"
+            . my--display-buffer-down)))
+        (keys-local
+         (list
+          (emacs-keymap
+           (name 'windmove-repeat-map)
+           ;; Make a repeat map for the windmove commands.
+           (repeat? #t)
+           (keys '(("<left>" . windmove-left)
+                   ("<right>" . windmove-right)
+                   ("<up>" . windmove-up)
+                   ("<down>" . windmove-down))))))
+        (extra-init
+         (list
+          (elisp (defun my--display-buffer-down (&optional arg
+                                                           buf)
+                   (interactive
+                    "P\nbSwitch to buffer in window below: ")
+                   (windmove-display-in-direction 'down arg)
+                   (switch-to-buffer buf))))))
+       (emacs-package
+        (name 'dired)
+        ;; External packages used by Dired
+        (extra-packages (list file unzip))
+        (keys-local
+         (list
+          (emacs-keymap
+           (name 'dired-mode-map)
+           (keys '(("b"
+                    . dired-create-empty-file))))))
+        (options
+         '((dired-isearch-filenames . dwim)
+           (dired-kill-when-opening-new-dired-buffer . #t))))
+       (emacs-package
+        (name 'vertico)
+        (package emacs-vertico)
+        (options
+         '((vertico-cycle . #t)))
+        (extra-init
+         (list (elisp (vertico-mode))))))))))))
+@end lisp
+
+@noindent
+The configuration above will install and configure Emacs, additionally
+installing the @code{aspell}, @code{aspell-dict-en}, @code{file},
+@code{unzip}, and @code{emacs-vertico} packages.
+
+The @code{home-emacs-service-type} can be extended using the
+@code{home-emacs-extension} record type.  Here is an example:
+
+@lisp
+(define %gnus-init-file
+  (elisp-file "gnus.el"
+              (list
+               (elisp (setq gnus-select-method '(nnnil "")))
+               (elisp (setq gnus-secondary-select-methods
+                            '((nnml "")
+                              (nntp "news.gmane.io"))))
+               (elisp (setq mail-sources
+                            '((imap :server "mail.example.net"
+                                    :user "user@@example.net"
+                                    :port 993
+                                    :stream tls)))))))
+
+(home-environment
+ (services
+  (list
+   ;; ...
+   (simple-service
+    'emacs-mail-service
+    home-emacs-service-type
+    (home-emacs-extension
+     (default-init
+       (emacs-configuration
+        (extra-files
+         `(("gnus.el" . ,%gnus-init-file)
+           ("signature"
+            . ,(local-file
+                "/home/user/src/guix-config/files/signature"))))
+        (variables
+         `((gnus-init-file
+            . ,(elisp (locate-user-emacs-file
+                       "gnus.el")))
+           (mail-user-agent . gnus-user-agent)
+           (read-mail-command . gnus)))))
+     (configured-packages
+      (list
+       (emacs-package
+        (name 'smtpmail)
+        (options
+         '((smtpmail-servers-requiring-authorization
+            . "mail\\.example\\.net")
+           (smtpmail-smtp-server . "mail.example.net")
+           (smtpmail-smtp-service . 587)
+           (smtpmail-smtp-user
+            . "user@@example.net"))))
+       (emacs-package
+        (name 'message)
+        (options
+         `((message-send-mail-function
+            . smtpmail-send-it)
+           (message-signature-file
+            . ,(elisp (locate-user-emacs-file
+                       "signature"))))))))
+     (servers
+      (list
+       (emacs-server
+        (name "mail")
+        (default-init
+          (emacs-configuration
+           (extra-init
+            (list
+             (elisp (add-hook 'server-after-make-frame-hook
+                              (function gnus))))))))))))))))
+@end lisp
+
+@noindent
+The configuration above extends the Emacs home service by configuring
+the @code{smtpmail} and @code{message} packages, creating a
+configuration file for the Emacs Gnus Newsreader, and creating a
+Shepherd service called @samp{emacs-mail} which runs an Emacs server
+that opens Gnus when a client frame is started.
+@end defvar
+
+The record types for configuring the Emacs home service are described in
+detail below.
+
+Note that for these record types, configuration fields that accept
+``association list'' values expect proper assocation lists.  An entry of
+the form @code{(@var{a} . @var{b})} specifies a value of @var{b} for key
+@var{a}, but an entry of the form @code{(@var{a} @var{b})} specifies a
+value that is a list which contains @var{b} as its only element (this
+can also be written as @code{(@var{a} . (@var{b}))}).
+
+@deftp {Data Type} home-emacs-configuration
+Available @code{home-emacs-configuration} fields are:
+
+@table @asis
+@item @code{emacs} (default: @code{emacs}) (type: package)
+The package providing the @file{/bin/emacs} command.
+
+@item @code{user-emacs-directory} (default: @code{"~/.config/emacs/"}) (type: string)
+Directory beneath which additional Emacs user files are placed.  By
+default, this is also the directory that contains the @file{init.el} and
+@file{early-init.el} Emacs initialization files, but you can change this
+field to specify any directory of your choosing; initialization files
+generated by this service will still be loaded.
+
+@item @code{native-compile?} (default: @code{#t}) (type: boolean)
+Whether to enable native-compilation of Emacs packages by building them
+with the Emacs specified by the @code{emacs} field rather than
+@code{emacs-minimal}.
+
+@item @code{load-custom?} (default: @code{#t}) (type: boolean)
+Whether to load customizations created with the Emacs customization
+interface.  Because all configuration files created by this service are
+effectively read-only, the service modifies the default behavior of
+Emacs so that customizations are always saved in a separate
+@file{custom.el} file, which will be loaded when Emacs is initialized if
+this field is true.
+
+@item @code{extra-packages} (default: @code{()}) (type: list-of-file-likes)
+A list of additional Emacs-related packages or file-like objects to
+install.  If a package is specified in @code{configured-packages}, it
+does not need to be specified here.
+
+@item @code{package-serializer} (default: @code{%emacs-simple-package-serializer}) (type: emacs-package-serializer)
+The serializer to use for configuration specified by
+@code{emacs-package} objects.
+
+@deftp {Data Type} emacs-package-serializer
+Available @code{emacs-package-serializer} fields are:
+
+@table @asis
+@item @code{name} (type: symbol)
+A symbol identifying the serializer.
+
+@item @code{procedure} (type: procedure)
+A procedure that takes two arguments, an @code{emacs-package} object and
+the @code{package} object providing GNU Emacs for the Emacs home
+service, and that should return a list of @code{elisp} objects or
+G-expressions containing package-specific configuration to serialize to
+the Emacs user initialization file.
+
+@item @code{dependencies} (default: @code{()}) (type: alist)
+An association list of additional packages to install whenever this
+serializer is used and predicates to determine whether to install them.
+Each predicate should be a procedure that accepts one argument, the
+@code{package} object providing the GNU Emacs for the Emacs home
+service.
+
+@item @code{indent-forms} (default: @code{()}) (type: alist)
+An association list of symbols and indentation rules.  Each entry is of
+the form (@var{symbol} .  @var{indent}), where @var{symbol} is a symbol
+and @var{indent} is an integer.  Values have the same effect as the
+@code{indent-forms} field in the @code{home-emacs-configuration} record.
+
+Note that indentation rules specified here will subsequently affect all
+Emacs Lisp expressions serialized by the Emacs home service, not just
+package-specific configuration.
+
+@item @code{description} (default: @code{""}) (type: string)
+A brief description of the serializer.
+
+@end table
+
+@end deftp
+
+@item @code{indent-forms} (default: @code{()}) (type: alist)
+An association list of symbols and indentation rules.  Each entry is of
+the form (@var{symbol} .  @var{indent}), where @var{symbol} is a symbol
+and @var{indent} is an integer.
+
+When @var{symbol} occurs at the beginning of a list in an Emacs Lisp
+file, the first @var{indent} expressions are indented as arguments and
+the remainder as body expressions, as if @var{indent} was supplied as
+the @code{lisp-indent-function} symbol property for @var{symbol} in
+Emacs.  Argument expressions are either printed on the same line as
+@var{symbol} or indented 4 columns beyond the base indentation of the
+enclosing list, and body expressions are indented 2 columns beyond the
+base indentation.
+
+@item @code{propagated-init} (default: @code{()}) (type: list-of-elisp-or-gexps)
+A list of Elisp expressions or G-expressions that should be evaluated by
+all Emacsen during initialization, including servers.  These expressions
+are serialized to the beginning of the Emacs user initialization file.
+
+@item @code{default-init} (type: emacs-configuration)
+General configuration used to create Emacs initialization files.  Emacsen
+will use this configuration by default, in addition to any
+package-specific configuration specified in the
+@code{configured-packages} field and any appropriate configuration for
+specific servers.
+
+@deftp {Data Type} emacs-configuration
+Available @code{emacs-configuration} fields are:
+
+@table @asis
+@item @code{early-init} (default: @code{()}) (type: list-of-elisp-or-gexps)
+A list of Elisp expressions or G-expressions to serialize to the Emacs
+early init file, the @file{early-init.el} file in the appropriate Emacs
+configuration directory.
+
+@item @code{extra-init-files} (default: @code{()}) (type: alist)
+An association list of filenames and file-like objects containing Emacs
+Lisp to load when Emacs is initialized.  For each entry, a file with the
+text contents of the file-like object, or the combined text contents of
+all of the file-like objects in a list if a list is specified, will be
+created with the given filename in the appropriate Emacs configuration
+directory (the directory where the @file{early-init.el} and
+@file{init.el} files are located).  These files will then be loaded when
+Emacs is initialized, before the expressions specified in
+@code{extra-init} are evaluated.
+
+Note that it is an error to specify files with the filenames
+@samp{init.el} and @samp{early-init.el}, because these files are already
+generated by the Emacs home service.
+
+@item @code{extra-files} (default: @code{()}) (type: alist)
+An association list of filenames and file-like objects specifying files
+to create in the Emacs user directory.  For each entry, a file with the
+given filename will be created with the contents of the file-like
+object.  If a list of file-like objects is given for an entry, the new
+file will contain the combined text contents of all of the file-like
+objects in the list.  This field can be used to add configuration files
+for Emacs that should not be automatically loaded when Emacs is
+initialized.
+
+Note that the Emacs user directory, which can be specified using the
+@code{user-emacs-directory} field of the @code{home-emacs-configuration}
+record for the service, may not be the same as the directory containing
+Emacs configuration files, such as the Emacs user initialization file or
+files created according to the @code{extra-init-files} field.
+
+@item @code{variables} (default: @code{()}) (type: alist)
+An association list of Emacs variables and values to set in the Emacs
+initialization file.  Variables should be symbols naming Emacs
+variables, and values can be any objects that can be serialized to
+Elisp.  For values, primitive Scheme data types are implicitly quoted,
+including lists and symbols.  To instead set an option to the value of
+an expression to be evaluated at Emacs initialization time, use either
+an Elisp expression (e.g., specified with the @code{elisp} form) or a
+G-expression as a value.  Note that it is an error to specify an Elisp
+expression value that contains only comments or whitespace for this
+field.
+
+@item @code{modes} (default: @code{()}) (type: alist)
+An association list of global minor modes and arguments.  When an
+argument is true or false, enable or disable the mode, respectively,
+when Emacs is initialized.  Otherwise, the argument will be passed to
+the mode's toggle function.  For example, to disable
+@code{tool-bar-mode}, enable @code{pixel-scroll-precision-mode}, and
+enable @code{fringe-mode} with the argument @code{20}, you could use:
+
+@lisp
+'((tool-bar-mode .  #f)
+  (pixel-scroll-precision-mode .  #t)
+  (fringe-mode .  20))
+@end lisp
+
+@noindent
+Arguments given as lists and symbols are implicitly quoted.  Use Elisp
+expressions (e.g., specified with the @code{elisp} form) or
+G-expressions to specify arguments that should be evaluated at Emacs
+initialization time.
+
+@item @code{keys} (default: @code{()}) (type: alist)
+An association list of key bindings for the Emacs global keymap.
+Entries are pairs of key sequences and binding definitions.  Key
+sequences are Emacs-specific string or vector representations of
+sequences of keystrokes or events.  Strings should be valid arguments to
+the Emacs function @code{kbd}, and they are preferred over the low-level
+vector representations.  Here are some examples of valid string values:
+@samp{"C-c a"}, @samp{"M-RET"}, @samp{"M-<up>"}, @samp{"<remap> <foo>"},
+and @samp{"<t>"} (@pxref{Keymaps,,, elisp,The Emacs Lisp Manual}).
+Binding definitions should be symbols for Emacs commands.
+
+@item @code{keys-override} (default: @code{()}) (type: alist)
+An association list of key sequences and Emacs commands to bind in the
+global override map.  These key bindings have a higher precedence than
+local and global keybindings.
+
+@item @code{extra-init} (default: @code{()}) (type: list-of-elisp-or-gexps)
+A list additional of Elisp expressions or G-expressions to serialize to
+the Emacs user initialization file, the @file{init.el} file in the
+appropriate Emacs configuration directory.  These expressions will occur
+in the serialized file after those corresponding to the above fields.
+
+@end table
+
+@end deftp
+
+@item @code{configured-packages} (default: @code{()}) (type: list-of-emacs-packages)
+A list of Emacs-related packages to install and associated configuration
+for the Emacs user initialization file.  @code{emacs-package} objects
+encapsulate lists of packages to install along with relevant
+configuration.
+
+@deftp {Data Type} emacs-package
+Available @code{emacs-package} fields are:
+
+@table @asis
+@item @code{name} (type: symbol)
+The symbol naming the Emacs package or library, as would be used with
+Emacs @code{require}.
+
+@item @code{package} (default: @code{()}) (type: package-or-null)
+A Guix package providing the Emacs package specified by @code{name}.  If
+the package is built into Emacs, or if there is no associated Guix
+package, this field should be set to the empty list (the default).
+
+@item @code{extra-packages} (default: @code{()}) (type: list-of-file-likes)
+A list of packages or file-like objects that provide additional
+functionality used by this package, but which are not installed
+automatically by the Guix package manager as propagated inputs of
+@code{package}.
+
+@item @code{extra-files} (default: @code{()}) (type: alist)
+An association list of filenames and file-like objects specifying files
+to create in the Emacs user directory.  For each entry, a file with the
+given filename will be created in the Emacs user directory with the
+contents of the file-like object.  If a list of file-like objects is
+given for an entry, the new file will contain the combined text contents
+of all of the file-like objects in the list.  This field should be used
+to add per-package files to the Emacs user directory.
+
+@item @code{install?} (default: @code{#t}) (type: boolean)
+Whether to install @code{package} and @code{extra-packages}.
+
+@item @code{load-force?} (default: @code{#f}) (type: boolean)
+Whether to force loading of this package immediately when Emacs is
+initialized, rather than deferring loading, for example, until an
+autoloaded function is invoked.  This is similar in effect to the
+keyword @code{:demand} from @code{use-package} and to the inverse of the
+keyword @code{:defer}.  The difference is that when this field is false,
+package loading should always be deferred; @code{use-package} normally
+does not defer loading when it does not set up autoloads, because it
+doesn't know that Guix handles autoloads on its own.
+
+@item @code{load-predicates} (default: @code{()}) (type: list-of-elisp-or-gexps)
+A list predicate expressions to evaluate when Emacs is initialized to
+determine whether to evaluate the configuration for this package.  When
+this list is not empty, @emph{all} other configuration for this package
+should be effectively surrounded in the Emacs user initialization file
+by a block of the form: @code{(when @var{load-predicates} @dots{})}.
+This is the supercharged Guix version of the @code{use-package}
+@code{:if} keyword!
+
+If multiple load predicates are specified, the behavior is determined by
+the package configuration serializer.  Both
+@code{%emacs-use-package-serializer} and the
+@code{%emacs-use-package-serializer} compose load predicates using
+@code{and}, so that all load predicates in the list must be satisfied in
+order for the package configuration to be evaluated.
+
+@item @code{load-after-packages} (default: @code{()}) (type: list-of-symbols)
+A list of symbols for Emacs packages that must be loaded before this
+package is loaded.  Only after all of the packages in the list have been
+loaded by Emacs should configuration for this package be evaluated.
+This is similar to a simplified version of the @code{:after} keyword
+from @code{use-package}.
+
+@item @code{load-paths} (default: @code{()}) (type: list-of-string-or-file-likes)
+A list of additional load paths to add to the Emacs @code{load-paths}
+variable.  Load paths can be specified either as strings or as file-like
+objects, in which case a path to the respective store item is
+substituted.
+
+@item @code{autoloads} (default: @code{()}) (type: list-of-symbols)
+A list of Emacs functions from the package to autoload.  This can be
+useful, for example, when defining custom commands in the Emacs user
+initialization file that use functions which are not autoloaded by
+default.
+
+@item @code{autoloads-interactive} (default: @code{()}) (type: list-of-symbols)
+A list of additional Emacs interactive commands from the package to
+autoload, so that they can be invoked interactively before the package
+is loaded.
+
+@item @code{keys-global} (default: @code{()}) (type: alist)
+An association list of key sequences (as strings or vectors) and Emacs
+commands to bind in the global keymap.
+
+@item @code{keys-global-keymaps} (default: @code{()}) (type: alist)
+An association list of key sequences and Emacs keymap variables to bind
+to them in the global keymap.  The keymap variables should be symbols
+that define keymaps in the package; they can be effectively autoloaded
+using this assumption.
+
+@item @code{keys-override} (default: @code{()}) (type: alist)
+An association list of key sequences and symbols naming Emacs commands
+to bind in the global override map.  These key bindings have a higher
+precedence than local and global keybindings.
+
+@item @code{keys-local} (default: @code{()}) (type: list-of-emacs-keymaps)
+A list of key binding configurations for specific keymaps, each
+contained in an @code{emacs-keymap} object.
+
+@deftp {Data Type} emacs-keymap
+Available @code{emacs-keymap} fields are:
+
+@table @asis
+@item @code{name} (default: @code{global-map}) (type: symbol)
+The symbol of the Emacs keymap in which to bind keys.
+
+@item @code{package-name} (default: @code{()}) (type: symbol-or-null)
+The symbol naming the Emacs package providing the keymap, as would be
+used with Emacs @code{require}.  If this field is null (the default),
+then the package for which the keymap is being configured should define
+the keymap or the keymap should otherwise be defined by the time the
+configuration for the package is evaluated.
+
+@item @code{repeat?} (default: @code{#f}) (type: boolean)
+Whether to make this keymap a repeat map (@pxref{Repeating,,, emacs,The
+GNU Emacs Manual}).  Repeat maps are created by setting the
+@code{repeat-map} symbol property for each key definition in @code{keys}
+to the @code{name} of this keymap.  Use the @code{repeat-exit} field to
+override this setting for specific bindings.
+
+@item @code{repeat-exit} (default: @code{()}) (type: list-of-symbols)
+A list of commands that exit the repeat map.  When @code{repeat?} is
+true, these commands do not get the @code{repeat-map} property.  The
+meaning of this field is similar to that of the @code{:exit} keyword
+used by the @code{defvar-keymap} function in Emacs.  This field has no
+effect when @code{repeat?} is false.
+
+@item @code{repeat-enter} (default: @code{()}) (type: list-of-symbols)
+A list of additional commands that enter the repeat map.  When
+@code{repeat?} is true, these commands get the @code{repeat-map}
+property, even when they are not bound in the keymap.  This is only
+useful when a command is not bound in @code{name}, but the repeat map
+should be accessible after that command is invoked (e.g., with
+@kbd{M-x}).  The meaning of this field is similar to that of the
+@code{:enter} keyword used by the @code{defvar-keymap} function in
+Emacs.  This field has no effect when @code{repeat?} is false.
+
+@item @code{disabled-commands} (default: @code{()}) (type: alist)
+An association list of command symbols and whether to disable them.
+When a disabled command is interactively invoked, Emacs asks for
+confirmation from the user (@pxref{Disabling,,, emacs,The GNU Emacs
+Manual}).  The values of this alist should be booleans, which will be
+stored as the value of the @code{disabled} property of each respective
+command symbol.  Thus, to disable the @code{transpose-chars} command and
+enable the @code{erase-buffer} command, you can use:
+
+@lisp
+'((transpose-chars .  #t)
+  (erase-buffer .  #f))
+@end lisp
+
+@item @code{keys} (default: @code{()}) (type: alist)
+An association list of key sequences and binding definitions.  Key
+sequences are Emacs-specific string or vector representations of
+sequences of keystrokes or events.  Strings should be valid arguments to
+the Emacs function @code{kbd}, and they are preferred over the low-level
+vector representations (@pxref{Keymaps,,, elisp, The Emacs Lisp
+Manual}).  Binding definitions should be Emacs command symbols.  As a
+special case, when a binding definition is the boolean false, the key is
+unset in the keymap.
+@end table
+
+@end deftp
+
+@item @code{options} (default: @code{()}) (type: alist)
+An association list of user options and values for this package.
+Options should be symbols naming Emacs variables, and values can be any
+object that can be serialized to Elisp.  For values, primitive Scheme
+data types are implicitly quoted, including lists and symbols.  To
+instead set an option to the value of an expression to be evaluated at
+Emacs initialization time, either use an Elisp expression (e.g.,
+specified with the @code{elisp} form) or a G-expression for a value.
+
+@item @code{faces} (default: @code{()}) (type: alist)
+An association list of face symbols and face specs.  @xref{Defining
+Faces,,,elisp,The Emacs Lisp Manual} for the format of face specs.
+
+@item @code{hooks} (default: @code{()}) (type: alist)
+An association list of hooks and functions to add to them.  Each entry
+is a pair of symbols.  Hook symbols in Emacs should end in @samp{-hook},
+but the @code{%emacs-simple-package-serializer} and
+@code{%emacs-use-package-serializer} serializers effectively add this
+suffix when necessary.
+
+@item @code{auto-modes} (default: @code{()}) (type: alist)
+An association list of filename patterns as regular expression strings
+and Emacs mode functions to call when visiting files with filenames that
+match the patterns.  @xref{Auto Major Mode,,,elisp,The Emacs Lisp
+Manual}, for details.
+
+@item @code{magic-modes} (default: @code{()}) (type: alist)
+An association list regular expression strings and Emacs mode functions
+to call when visiting files that begin with matching text.  @xref{Auto
+Major Mode,,,elisp,The Emacs Lisp Manual}, for details.
+
+@item @code{extra-after-load} (default: @code{()}) (type: list-of-elisp-or-gexps)
+A list of Elisp expressions or G-expressions to evaluate after the
+package is loaded, as with the Emacs @code{eval-after-load} function.
+Elisp expressions can be specified using the @code{elisp} syntax or
+the @code{#%} reader extension.
+
+@item @code{extra-init} (default: @code{()}) (type: list-of-elisp-or-gexps)
+A list of Elisp expressions or G-expressions to evaluate immediately
+when Emacs is initialized, even if loading is deferred due to the
+@code{load-force?} field.  Note that the @code{load-predicates} field
+should still determine whether these expressions are evaluated, and they
+will only be evaluated after all packages specified in the
+@code{load-after-packages} field have been loaded.
+
+@item @code{extra-keywords} (default: @code{()}) (type: alist)
+An association list of keys and lists of extra Elisp expressions or
+G-expressions.  Keys can potentially be any keyword or symbol object;
+keywords are automatically serialized to their Emacs Lisp equivalent
+(e.g., @code{#:keyword} is serialized as @code{:keyword}).  The meanings
+of entries is specific to each package serializer, and any key may be
+ignored by a package serializer.  This field is currently ignored by the
+@code{%emacs-simple-package-serializer}.  Entries in this list matching
+@code{use-package} keywords will be spliced by the
+@code{%emacs-use-package-serializer} into the @code{use-package} body,
+after all other forms.
+
+@end table
+
+@end deftp
+
+@item @code{servers} (default: @code{()}) (type: list-of-emacs-servers)
+A list of configurations for Emacs servers.
+
+@deftp {Data Type} emacs-server
+Available @code{emacs-server} fields are:
+
+@table @asis
+@item @code{name} (type: string)
+A string naming the server.  Users will subsequently be able to start
+the new server by using the command @code{herd start emacs-@var{name}}.
+To create Emacs client frames for the sever, users can use commands like:
+@code{emacsclient --create-frame --socket-name=@var{name}}.
+
+Because this string is meant for use in shell commands (and filenames),
+it should not contain any characters other than letters and digits and
+the characters @samp{-}, @samp{_}, and @samp{.}.
+
+@item @code{inherit-directory?} (default: @code{#t}) (type: boolean)
+Whether the server should share its Emacs user directory with that of
+the Emacs home service.  When false, the server will use a subdirectory
+of the one used by the service for its own user directory.  When true
+(the default), the @code{user-emacs-directory} Emacs variable for the
+server will be set to that of the Emacs home service, but the server
+will still load its own @file{early-init.el} and @file{init.el} files.
+See the @code{inherit-init?} and @code{inherit-configured-packages?}
+fields for how to inherit configuration from other Emacsen.
+
+@item @code{inherit-init?} (default: @code{#t}) (type: boolean)
+Whether to load the default configuration used by the Emacs home
+service, that is, the initialization expressions specified by the
+@code{default-init} field of the @code{home-emacs-configuration} value
+for the service.  This is loaded in addition to any configuration
+specified in the @code{default-init} field for this specific server.
+
+Note that if @code{inherit-directory?} is false, this also results in
+the creation of duplicate copies in the Emacs user directory for the
+server of any files specified by the @code{extra-files} field of the
+@code{emacs-configuration} record for the
+@code{home-emacs-configuration} of the service.  This ensures that any
+references to those files in the inherited configuration expressions
+will not fail in unexpected ways.
+
+@item @code{inherit-configured-packages?} (default: @code{#t}) (type: boolean)
+Whether to load configuration for packages used by the Emacs home
+service, that is, the package configuration specified in the
+@code{configured-packages} field of the @code{home-emacs-configuration}
+value for the service.  This is loaded in addition to any configuration
+specified with the @code{configured-packages} field for this specific
+server.
+
+Note that if @code{inherit-directory?} is false, this also results in
+the creation of duplicate copies in the Emacs user directory for the
+server of any files specified by the @code{extra-files} fields of
+@code{emacs-package} records from the @code{configured-packages} field
+of the @code{home-emacs-configuration} of the service.
+
+@item @code{load-custom?} (default: @code{#t}) (type: boolean)
+Whether to load customizations created with the Emacs customization
+interface.  When @code{inherit-directory?} is true, customizations made
+within this specific server affect the Emacs home service, and vice
+versa.  Otherwise, the server has its own separate set of
+customizations.
+
+@item @code{extra-packages} (default: @code{()}) (type: list-of-file-likes)
+A list of extra packages or file-like objects to install, without
+associated configuration.
+
+@item @code{auto-start?} (default: @code{#t}) (type: boolean)
+Whether to start the server automatically.
+
+@item @code{debug?} (default: @code{#f}) (type: boolean)
+Whether to enable the Emacs Lisp debugger for errors in the
+initialization files of the server.
+
+@item @code{shepherd-requirements} (default: @code{()}) (type: list-of-symbols)
+A list of symbols specifying Shepherd services that must be started
+before the service for the Emacs server can be started (@pxref{Defining
+Services,,, shepherd,The GNU Shepherd Manual}).
+
+@item @code{default-init} (type: emacs-configuration)
+Configuration used to create initialization files specifically for this
+server.
+
+@item @code{configured-packages} (default: @code{()}) (type: list-of-emacs-packages)
+A list of @code{emacs-package} objects specifying Emacs packages to
+install and configure in the Emacs user initialization file for the
+server.
+
+@end table
+
+@end deftp
+
+@end table
+
+@end deftp
+
+@deftp {Data Type} home-emacs-extension
+Available @code{home-emacs-extension} fields are:
+
+@table @asis
+@item @code{extra-packages} (default: @code{()}) (type: list-of-file-likes)
+A list of additional Emacs-related packages or file-like objects to
+install.  If a package is specified in @code{configured-packages}, it
+does not need to be specified here.
+
+@item @code{indent-forms} (default: @code{()}) (type: alist)
+An association list of symbols and indentation rules.  Each entry is of
+the form (@var{symbol} .  @var{indent}), where @var{symbol} is a symbol
+and @var{indent} is an integer specifying the number of argument
+expressions for @var{symbol}.
+
+@item @code{servers} (default: @code{()}) (type: list-of-emacs-servers)
+A list of configurations for Emacs servers.  It is an error to specify
+multiple @code{emacs-server} objects with equivalent @code{name} fields.
+
+@item @code{default-init} (type: emacs-configuration)
+General configuration used to create the Emacs initialization files.
+Emacsen will use this configuration by default, in addition to any
+package-specific configuration specified in the
+@code{configured-packages} field and any relevant configuration for
+specific servers.
+
+@item @code{configured-packages} (default: @code{()}) (type: list-of-emacs-packages)
+A list of Emacs-related packages and associated configuration for the
+Emacs user initialization file.  Configuration for multiple
+@code{emacs-package} objects with equivalent @code{name} fields is
+merged when possible; an error is signaled otherwise.
+
+@end table
+
+@end deftp
+
+@cindex Emacs package serializers, for Emacs home services
+As we have seen, we can customize how configuration for Emacs packages
+is serialized to the Emacs user initialization file by using the
+@code{package-serializer} field of the @code{home-emacs-configuration}
+record.  There are two predefined package serializers, the
+@code{%emacs-simple-package-serializer} and the
+@code{%emacs-use-package-serializer}.
+
+@defvar %emacs-simple-package-serializer
+An Emacs package configuration serializer that configures Emacs using
+minimal, built-in Emacs mechanisms, instead of complex macros such as
+@code{use-package}.
+@end defvar
+
+@defvar %emacs-use-package-serializer
+An Emacs package configuration serializer that configures Emacs with the
+@code{use-package} macro.
+@end defvar
+
+@cindex Emacs package serializers, defining
+We can also create custom package serializers by defining our own
+@code{emacs-package-serializer} records.  The two mandatory fields are
+the @code{name} and the @code{procedure} fields.  @code{name} is an
+arbitrary symbol (currently unused), and @code{procedure} is a procedure
+that takes two arguments, an @code{emacs-package} object to be
+serialized and the @code{package} object providing GNU Emacs for the
+home profile.  The procedure must return a list of Elisp expressions or
+G-expressions.
+
+The second argument to the package serializer procedure is useful if we
+want to condition the serialized output based on the Emacs version in
+use, for example, to use features introduced in newer versions of Emacs
+when they available.  Many new features are introduced in Emacs release
+29.1, and a predicate for handling this is provided by the @code{(gnu
+home services emacs)} module: @code{emacs-version-<29?} (see below).
+
+When defining a new package serializer, it is advisable to refer to the
+documentation for the @code{emacs-package} record type, which lays out
+some implementation guidelines that package serializers should follow
+for each field.  It is up to you to implement your serializer in a way
+that is consistent with those guidelines, or not.
+
+Here is an example of a hypothetical @code{%emacs-null-package-serializer} that only serializes a simple comment naming each package:
+
+@lisp
+(define (emacs-package->null-elisp config emacs)
+  "Return from `emacs-package' CONFIG a list of Elisp expressions that
+configures EMACS by serializing only comments."
+  (match-record config <emacs-package>
+                (name)
+    (let ((comment-string (string-append ";;; "
+                                         (symbol->string name)
+                                         "\n")))
+      (list (elisp (unelisp-comment comment-string)))))))
+
+(define %emacs-null-package-serializer
+  (emacs-package-serializer
+   (name 'emacs-null-package)
+   (procedure emacs-package->null-elisp)
+   (description "An Emacs package serializer that doesn't do anything.")))
+@end lisp
+
+@deffn {Procedure} emacs-version-<29? emacs
+Return true if the version of @var{emacs}, a @code{package} object, is
+less than 29, and return false otherwise.
+@end deffn
+
+@cindex importing configuration, for Emacs home services
+
+You may have existing Emacs initialization files, but translating them
+into Scheme configuration records can be tedious.  The @code{(gnu home
+services emacs)} module provides a utility function to aid in this
+process: @code{elisp-file->home-emacs-configuration}.
+
+The following example prints a Scheme snippet that returns a
+@code{home-emacs-configuration} record corresponding to the given Emacs
+initialization file:
+
+@lisp
+(elisp-file->home-emacs-configuration (current-output-port)
+                                      "/home/user/.config/emacs/init.el")
+@end lisp
+
+@deffn {Procedure} elisp-file->home-emacs-configuration port file
+Write to @var{port} a Scheme snippet creating a
+@code{home-emacs-configuration} record from the Elisp file named
+@var{file}.
+@end deffn
+
 @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..300b5ec53f
--- /dev/null
+++ b/gnu/home/services/emacs.scm
@@ -0,0 +1,3040 @@
+;;; GNU Guix --- Functional package management for GNU
+
+;;; Copyright © 2023 ( <paren@HIDDEN>
+;;; Copyright © 2023 David Wilson <david@HIDDEN>
+;;; Copyright © 2023 Kierin Bell <fernseed@HIDDEN>
+;;;
+;;; 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)
+  #:use-module (gnu home services shepherd)
+  #:autoload   (gnu packages emacs) (emacs emacs-minimal)
+  #:autoload   (gnu packages emacs-xyz) (emacs-use-package)
+  #:use-module (gnu services configuration)
+  #:use-module (guix gexp)
+  #:use-module (guix records)
+  #:use-module (guix packages)
+  #:use-module (guix utils)
+  #:use-module (guix modules)
+  #:use-module (guix read-print)
+  #:use-module (guix store)
+  #:use-module (guix monads)
+  #:use-module (guix i18n)
+  #:use-module ((guix diagnostics)
+                #:select (formatted-message))
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-26)
+  #:use-module (srfi srfi-35)
+  #:use-module (ice-9 match)
+  #:use-module (ice-9 receive)
+  #:use-module (ice-9 rdelim)
+  #:use-module (ice-9 control)
+  #:use-module (ice-9 regex)
+  #:use-module (language elisp parser)
+  #:re-export (blank?
+
+               vertical-space
+               vertical-space?
+               vertical-space-height
+
+               page-break
+               page-break?
+
+               comment
+               comment?
+               comment->string
+               comment-margin?)
+  #:export (elisp?
+            elisp->sexp
+            sexp->elisp
+            elisp
+
+            elisp-file*
+            elisp-file
+            elisp-file?
+
+            emacs-keymap
+            emacs-keymap?
+            emacs-keymap-name
+            emacs-keymap-package-name
+            emacs-keymap-repeat?
+            emacs-keymap-repeat-exit
+            emacs-keymap-repeat-enter
+            emacs-keymap-keys
+
+            emacs-package
+            emacs-package?
+            emacs-package-name
+            emacs-package-package
+            emacs-package-extra-packages
+            emacs-package-extra-files
+            emacs-package-install?
+            emacs-package-load-force?
+            emacs-package-load-predicates
+            emacs-package-load-after-packages
+            emacs-package-load-paths
+            emacs-package-autoloads
+            emacs-package-autoloads-interactive
+            emacs-package-keys-global
+            emacs-package-keys-global-keymaps
+            emacs-package-keys-override
+            emacs-package-keys-local
+            emacs-package-options
+            emacs-package-faces
+            emacs-package-hooks
+            emacs-package-auto-modes
+            emacs-package-magic-modes
+            emacs-package-extra-after-load
+            emacs-package-extra-init
+            emacs-package-extra-keywords
+
+            emacs-configuration
+            emacs-configuration?
+            emacs-configuration-early-init
+            emacs-configuration-extra-init-files
+            emacs-configuration-extra-files
+            emacs-configuration-variables
+            emacs-configuration-modes
+            emacs-configuration-keys
+            emacs-configuration-keys-override
+            emacs-configuration-extra-init
+
+            emacs-server
+            emacs-server?
+            emacs-server-name
+            emacs-server-inherit-directory?
+            emacs-server-inherit-init?
+            emacs-server-inherit-configured-packages?
+            emacs-server-load-custom?
+            emacs-server-extra-packages
+            emacs-server-auto-start?
+            emacs-server-debug?
+            emacs-server-shepherd-requirements
+            emacs-server-default-init
+            emacs-server-configured-packages
+
+            emacs-package-serializer
+            emacs-package-serializer?
+            emacs-package-serializer-name
+            emacs-package-serializer-procedure
+            emacs-package-serializer-dependencies
+            emacs-package-serializer-indent-forms
+            emacs-package-serializer-description
+
+            emacs-package->simple-elisp
+            %emacs-simple-package-serializer
+            emacs-package->use-package-elisp
+            %emacs-use-package-serializer
+
+            home-emacs-configuration
+            home-emacs-configuration?
+            home-emacs-configuration-emacs
+            home-emacs-configuration-user-emacs-directory
+            home-emacs-configuration-native-compile?
+            home-emacs-configuration-load-custom?
+            home-emacs-configuration-extra-packages
+            home-emacs-configuration-package-serializer
+            home-emacs-configuration-indent-forms
+            home-emacs-configuration-propagated-init
+            home-emacs-configuration-default-init
+            home-emacs-configuration-configured-packages
+            home-emacs-configuration-servers
+
+            home-emacs-extension
+            home-emacs-extension?
+            home-emacs-extension-extra-packages
+            home-emacs-extension-indent-forms
+            home-emacs-extension-servers
+            home-emacs-extension-default-init
+            home-emacs-extension-configured-packages
+
+            emacs-server->provision
+
+            home-emacs-service-type
+
+            elisp-file->home-emacs-configuration))
+
+;;; Commentary:
+;;;
+;;; Services for the GNU Emacs extensible text editor.
+;;;
+;;; Code:
+
+
+;;;
+;;; Elisp expression objects.
+;;;
+
+(define-record-type* <elisp> %elisp
+  make-elisp
+  elisp?
+  this-elisp
+  (sexp elisp-sexp))
+
+(define (dotted-list?* obj)
+  (and (pair? obj)
+       (dotted-list? obj)))
+
+(define list->dotted-list
+  (match-lambda
+    ((? list? lst)
+     (match (last-pair lst)
+       (((? pair?) . ())
+        ;; Prevent, e.g., '((quote a) (quote b)) -> '((quote a) quote b).
+        lst)
+       (_
+        (apply cons* lst))))
+    (x
+     x)))
+
+(define (fold-right/elisp fhere fup fcons seed exp)
+  "Recurse into subexpressions and `elisp' objects in EXP, applying FHERE, FUP
+and FCONS.  FHERE transforms atoms, FUP transforms accumulators after
+traversing lists, and FCONS joins atoms or lists with accumulators while
+traversing lists.  FHERE and FCONS each take two arguments: an element and an
+accumulator.  For FUP, the second argument is the accumulator, and the first
+argument is either the empty list or false if its first argument was derived
+from a dotted list.  The accumulator starts as SEED."
+  (define (reverse* lst)
+    (let loop ((lst lst)
+               (acc '()))
+      (match lst
+        ((? null?)
+         acc)
+        ((not (? pair?))
+         ;; Convert dotted lists into proper lists.
+         (cons lst acc))
+        ((head . tail)
+         (loop tail (cons head acc))))))
+
+  (match exp
+    ((? elisp?)
+     (fold-right/elisp fhere fup fcons seed (elisp-sexp exp)))
+    ((or (not (? pair?))
+         (? null?))
+     ;; The empty list should be passed to FHERE along with atoms.
+     (fhere exp seed))
+    (_
+     (let loop ((exp (reverse* exp))
+                (acc seed)
+                (dotted? (dotted-list?* exp)))
+       (match exp
+         ((? null?)
+          ;; XXX: FUP must handle any transformation of ACC back into a
+          ;; dotted list, since FHERE could have transformed the last
+          ;; element of ACC into a list, in which case we can't just use
+          ;; `list->dotted-list' to get a dotted list back.
+          (fup (if dotted? (not dotted?) exp) acc))
+         ((head . tail)
+          (loop tail
+                (fcons (fold-right/elisp fhere fup fcons seed head)
+                       acc)
+                dotted?)))))))
+
+(define (elisp->sexp exp)
+  "Return an s-expression containing the contents of Elisp expression EXP."
+  (fold-right/elisp (lambda (t s) t)
+                    (lambda (t s)
+                      (if (not t) (list->dotted-list s) s))
+                    cons
+                    '()
+                    exp))
+
+(define (elisp->lowerable-sexp exp)
+  "Return an s-expression from EXP that is lowerable within a G-expression,
+that is, strip `<vertical-space>', `<page-break>', and `<comment>' objects."
+  (let ((result (fold-right/elisp (lambda (t s) t)
+                                  (lambda (t s)
+                                    (if (not t) (list->dotted-list s) s))
+                                  (lambda (t s) (if (blank? t) s (cons t s)))
+                                  '()
+                                  exp)))
+    ;; XXX: What to do when an Elisp expression *is* a <blank>?
+    (if (blank? result) '() result)))
+
+(define-gexp-compiler (elisp-compiler (elisp <elisp>) system target)
+  ;; "Compile" an `elisp' object by stripping `<vertical-space>',
+  ;; `<page-break>', and `<comment>' objects, so that it can be `ungexp'd
+  ;; within a G-expression.
+  (with-monad %store-monad
+    (return (elisp->lowerable-sexp elisp))))
+
+(define (sexp->elisp sexp)
+  "Return an Elisp expression object containing SEXP."
+  (%elisp (sexp sexp)))
+
+(define-syntax elisp
+  ;; Create an `<elisp>' object from EXP, including any substitutions made
+  ;; with `unelisp', `unelisp-splicing', `unelisp-comment', `unelisp-newline',
+  ;; or `unelisp-page-break'.
+  ;; Modified from the `gexp' macro in `(guix gexp)'.
+  (lambda (s)
+
+    (define (substitute-unelisp e)
+      (syntax-case e (unelisp
+                      unelisp-splicing
+                      unelisp-comment
+                      unelisp-newline
+                      unelisp-page-break)
+        ((unelisp exp)
+         #'exp)
+        (((unelisp-splicing exp) rest ...)
+         #`(append exp #,(substitute-unelisp #'(rest ...))))
+        ((unelisp-comment str)
+         #'(comment str))
+        ((unelisp-newline)
+         #'(vertical-space 0))
+        ((unelisp-page-break)
+         #'(page-break))
+        ((exp0 . exp)
+         #`(cons #,(substitute-unelisp #'exp0)
+                 #,(substitute-unelisp #'exp)))
+        (x #''x)))
+
+    (syntax-case s ()
+      ((_ exp)
+       (let ((sexp* (substitute-unelisp #'exp)))
+         #`(%elisp (sexp #,sexp*)))))))
+
+
+;;;
+;;; Elisp files.
+;;;
+
+(define-record-type* <elisp-file> %elisp-file
+  make-elisp-file
+  elisp-file?
+  this-elisp-file
+  (name elisp-file-name)
+  (gexp elisp-file-gexp))
+
+(define (constant? obj)
+  "Return whether OBJ is self-quoting."
+  (or (boolean? obj)
+      (char? obj)
+      (string? obj)
+      (keyword? obj)
+      (number? obj)
+      (array? obj)))
+
+(define* (elisp->file-builder exps #:key (special-forms '()))
+  "Return a G-expression that builds a file containing the Elisp
+expressions (<elisp> objects or s-expressions) or G-epxressions in list EXPS.
+See `elisp-file' for a description of SPECIAL-FORMS."
+
+  (define (object->pp-quoted exp)
+    (match exp
+      ((? vertical-space?)
+       `((@ (guix read-print) vertical-space)
+         ,(vertical-space-height exp)))
+      ((? page-break?)
+       '((@ (guix read-print) page-break)))
+      ((? comment?)
+       `((@ (guix read-print) comment)
+         ,(comment->string exp)))
+      ((? constant?)
+       exp)
+      (_
+       (list 'quote exp))))
+
+  (define (elisp->pp-arg exp)
+    ;; Doing some of this on the derivation side with a macro similar to
+    ;; `quasiquote' might be cleaner, at the expense of an extra tree
+    ;; traversal.
+    (fold-right/elisp (lambda (t s)
+                        (object->pp-quoted t))
+                      (lambda (t s)
+                        (if (not t)
+                            ;; Transform S back into a dotted list, but only
+                            ;; after Scheme `quote' forms have been evaluated.
+                            ;; Also, never allow <comment>, <vertical-space>,
+                            ;; or <page-break> record constructors to
+                            ;; terminate a dotted list
+                            ;; (`pretty-print-with-comments' shouldn't print
+                            ;; them anyway).
+                            (match (last-pair s)
+                              ((`((@ (guix read-print) ,(or 'vertical-space
+                                                            'page-break
+                                                            'comment)) . ,_)
+                                . ())
+                               `(list ,@s))
+                              (_ `(apply cons* (list ,@s))))
+                            `(list ,@s)))
+                      cons
+                      '()
+                      exp))
+
+  (with-imported-modules (source-module-closure
+                          '((guix read-print)))
+    (gexp (begin
+            (use-modules (guix read-print))
+            (call-with-output-file (ungexp output "out")
+              (lambda (port)
+                (set-port-encoding! port "UTF-8")
+                (ungexp-splicing (append-map
+                                  (match-lambda
+                                    ((? gexp? exp)
+                                     (list (gexp (pretty-print-with-comments
+                                                  port
+                                                  (quote (ungexp exp))
+                                                  #:elisp? #t
+                                                  #:special-forms
+                                                  '(ungexp special-forms)))
+                                           (gexp (display "\n" port))))
+                                    ((? elisp? exp)
+                                     (append
+                                      (list
+                                       (gexp (pretty-print-with-comments
+                                              port
+                                              (ungexp (elisp->pp-arg exp))
+                                              #:format-comment
+                                              canonicalize-comment
+                                              #:elisp? #t
+                                              #:special-forms
+                                              '(ungexp special-forms))))
+                                      (if (comment? (elisp->sexp exp))
+                                          '()
+                                          (list
+                                           (gexp (display "\n" port))))))
+                                    (exp
+                                     ;; We can use S-exps internally to
+                                     ;; avoid overhead of converting `elisp'
+                                     ;; objects back into S-exps.
+                                     (append
+                                      (list
+                                       (gexp (pretty-print-with-comments
+                                              port
+                                              (ungexp (elisp->pp-arg exp))
+                                              #:format-comment
+                                              canonicalize-comment
+                                              #:elisp? #t
+                                              #:special-forms
+                                              '(ungexp special-forms))))
+                                      (if (comment? exp)
+                                          '()
+                                          (list
+                                           (gexp (display "\n" port)))))))
+                                  exps))))))))
+
+(define* (elisp-file name exps #:key (special-forms '()))
+  "Return an object representing the store file NAME, an Emacs Lisp file that
+contains EXPS, a list of Elisp expression objects or G-expressions.
+
+Custom indentation rules can be specified with SPECIAL-FORMS, an association
+list where each entry is of the form (SYMBOL . INDENT).  When SYMBOL occurs at
+the beginning of a list in an expression in EXPS, the first INDENT expressions
+after SYMBOL are indented as arguments and the remainder are indented as body
+expressions, as if INDENT was the value of the `lisp-indent-function' symbol
+property for SYMBOL in Emacs.  As in Emacs, argument expressions, if they
+cannot be pretty-printed on the same line as SYMBOL, are indented 4 columns
+beyond the base indentation of the enclosing list, and body expressions are
+indented 2 columns beyond the base indentation.
+
+This is the declarative counterpart of `elisp-file*'."
+  (%elisp-file (name name)
+               (gexp (elisp->file-builder exps
+                                          #:special-forms special-forms))))
+
+(define-gexp-compiler (elisp-file-compiler (elisp-file <elisp-file>)
+                                           system target)
+  (match-record elisp-file <elisp-file>
+                (name gexp)
+    (with-monad %store-monad
+      (gexp->derivation name gexp
+                        #:system system
+                        #:target target
+                        #:local-build? #t
+                        #:substitutable? #f))))
+
+(define* (elisp-file* name exps #:key (special-forms '()))
+  "Return as a monadic value a derivation that builds an Elisp file named NAME
+containing the expressions in EXPS, a list of Elisp expression objects or
+G-expressions.
+
+This is the monadic counterpart of `elisp-file', which see for a description
+of SPECIAL-FORMS,"
+  (define builder
+    (elisp->file-builder exps
+                         #:special-forms special-forms))
+
+  (gexp->derivation name builder
+                    #:local-build? #t
+                    #:substitutable? #f))
+
+
+;;;
+;;; Helper functions
+;;;
+
+(define (ensure-list obj)
+  "Return OBJ as a list."
+  (if (list? obj) obj (list obj)))
+
+(define (file-name-concat dir . rest)
+  "Concatenate DIR and REST filename components.  Any final slashes are
+stripped from the resulting filename."
+  (string-join (append (list (string-trim-right dir #\/))
+                       (map (cut string-trim-both <> #\/)
+                            rest))
+               "/"))
+
+(define (record-value rec field)
+  "Return the value of field named FIELD in record REC."
+  ((record-accessor (record-type-descriptor rec) field) rec))
+
+(define-syntax extend-record
+  ;; Extend record ORIGINAL by creating a new copy using CONSTRUCTOR,
+  ;; replacing each field specified by ORIG-FIELD with the evaluation of (PROC
+  ;; ORIG-VAL EXT-VALS), where ORIG-VAL is the value of ORIG-FIELD in ORIGINAL
+  ;; and EXT-VALS is the list of values of EXT-FIELD in EXTENSIONS.
+  (lambda (s)
+    (syntax-case s ()
+      ((_ constructor original extensions (proc orig-field ext-field) ...)
+       (with-syntax (((field-specs ...)
+                      (map
+                       (lambda (spec)
+                         (syntax-case spec ()
+                           ((proc orig-field ext-field)
+                            #'(orig-field
+                               (apply
+                                proc
+                                (list
+                                 (record-value original 'orig-field)
+                                 (map
+                                  (lambda (e)
+                                    (record-value e 'ext-field))
+                                  extensions)))))))
+                       #'((proc orig-field ext-field) ...))))
+         #'(constructor
+            (inherit original)
+            field-specs ...))))))
+
+(define (extend-list original extensions)
+  "Extend list ORIGINAL with list of lists EXTENSIONS."
+  (apply append original extensions))
+
+(define (extend-list-merge original extensions)
+  "Extend list ORIGINAL with list of lists EXTENSIONS, deleting duplicates."
+  (delete-duplicates (apply append original extensions) equal?))
+
+(define (extend-alist-merge original extensions)
+  "Extend association list ORIGINAL with list of association lists EXTENSIONS,
+merging the values for any duplicate keys into a single list value.  Key
+comparison is done with `equal?'."
+  (fold-right (lambda (elem ret)
+                (let ((entry (assoc (car elem) ret)))
+                  (if entry
+                      (acons (car elem)
+                             (append (ensure-list (cdr elem))
+                                     (ensure-list (cdr entry)))
+                             (assoc-remove! ret (car elem)))
+                      (cons elem ret))))
+              '()
+              (apply append original extensions)))
+
+
+(define* (extend-record-list-merge original extensions cmp-field proc
+                                   #:key (type? (const #t)) (= equal?))
+ "Extend list of records ORIGINAL with list of records EXTENSIONS by merging
+all records whose CMP-FIELDs are equal according to equality predicate = using
+PROC, a procedure that takes a record as its first argument and a list of
+records as its second argument and should return a single record object.  All
+objects that do not satisfy type predicate TYPE? are added to the returned
+list without comparison."
+  (let loop ((lst (apply append original extensions))
+             (acc '()))
+    (cond
+     ((null? lst) (reverse acc))
+     ((not (type? (car lst)))
+      (loop (cdr lst)
+            (cons (car lst) acc)))
+     ((partition
+       (lambda (ext)
+         (and (type? ext)
+              (= (record-value ext cmp-field)
+                 (record-value (car lst) cmp-field))))
+       (cdr lst))
+      (lambda (matches rest) (not (null? matches)))
+      => (lambda (matches rest)
+           (loop rest
+                 (cons (apply proc (car lst) (list matches))
+                       acc))))
+     (else (loop (cdr lst)
+                 (cons (car lst) acc))))))
+
+(define* (extend-record-field-default original extensions
+                                      default-record field
+                                      #:key (= eq?))
+  "Extend the value of ORIGINAL with any value in the list of EXTENSIONS that
+is not equal to the value of FIELD in DEFAULT-RECORD, signaling an error if
+there is any value in EXTENSIONS that is not equal to either ORIGINAL or the
+default according to equality predicate = (which defaults conservatively to
+`eq?').  For example, if the default value of FIELD and the value of FIELD in
+ORIGINAL are both #f, and at least one element of EXTENSIONS is #t, return #t,
+but if the default value of FIELD is 'foo, ORIGINAL is 'bar, and EXTENSIONS
+contains a value 'baz, then signal an error."
+  (let* ((def (record-value default-record field))
+         (new (fold (lambda (elem ret)
+                      (cond
+                       ((= elem original) ret)
+                       ((= elem def) elem)
+                       (else (configuration-field-error
+                              #f field elem))))
+                    '()
+                    extensions)))
+    (if (null? new) original new)))
+
+(define (elisp-or-gexp? val)
+  (or (elisp? val)
+      (gexp? val)))
+
+(define-syntax alist-sanitizer
+  ;; Construct a lambda expression that matches each KEY-PAT and VALUE-PAT
+  ;; pair against each entry of its argument, an alist.  If no pair matches,
+  ;; or if its argument is not an alist, the lambda signals an error
+  ;; displaying FIELD-NAME and the value of its argument.  Otherwise, the
+  ;; return value of the lambda is its argument.
+  (lambda (s)
+    (syntax-case s ()
+      ((_ field-name (key-pat . value-pat) ...)
+       (with-syntax (((clauses ...)
+                      (map
+                       (lambda (spec)
+                         (syntax-case spec ()
+                           ((key-pat . value-pat)
+                            ;; Note that entries of the form (A B) are
+                            ;; equivalent to (A . (B))---i.e., the value is
+                            ;; really a list, not an atom.  However, (A B)
+                            ;; where B is an Elisp expression is converted
+                            ;; into (A . B).
+                            #'(((and key-pat key)
+                                . (and value-pat value))
+                               (cons key value)))))
+                       #'((key-pat . value-pat) ...))))
+         #'(lambda (val)
+             (map
+              (lambda (expr)
+                (match expr
+                  clauses ...
+                  (_ (configuration-field-error #f 'field-name val))))
+              val))))
+      ((_ field-name '(key-pat . value-pat) ...)
+       #'(alist-sanitizer field-name (key-pat . value-pat) ...))
+      ((_ field-name)
+       #'(alist-sanitizer field-name (_ . _))))))
+
+(define (symbol-or-false? val)
+  (or (symbol? val)
+      (not val)))
+
+(define (symbol-or-null? val)
+  (or (symbol? val)
+      (null? val)))
+
+(define (string-or-file-like? val)
+  (or (string? val)
+      (file-like? val)))
+
+(define (string-or-vector? val)
+  (or (string? val)
+      (vector? val)))
+
+(define (package-or-null? val)
+  (or (package? val)
+      (null? val)))
+
+(define (keyword-or-symbol? val)
+  (or (keyword? val)
+      (symbol? val)))
+
+(define list-of-symbols?
+  (list-of symbol?))
+
+(define list-of-file-likes?
+  (list-of file-like?))
+
+(define list-of-string-or-file-likes?
+  (list-of string-or-file-like?))
+
+(define list-of-elisp-or-gexps?
+  (list-of elisp-or-gexp?))
+
+(define (elispifiable-quoted? val)
+  "Return whether VAL can be serialized as Elisp, but needs to be quoted."
+  (or (symbol? val)
+      (pair? val)))
+
+(define (elispifiable? val)
+  "Return whether VAL can be serialized as Elisp."
+  (or (constant? val)
+      (elispifiable-quoted? val)
+      (gexp? val)
+      (file-like? val)
+      (and (elisp? val)
+           (not (blank? (elisp->sexp val))))))
+
+(define elispifiable->elisp
+  (match-lambda
+    ((? elisp? obj)
+     obj)
+    ((? elispifiable-quoted? obj)
+     (sexp->elisp `(quote ,obj)))
+    (obj
+     (sexp->elisp obj))))
+
+(define-syntax keys-field-sanitizer
+  (syntax-rules ()
+    ((_ field-name)
+ (alist-sanitizer field-name
+                      ((? string-or-vector?) . (? symbol-or-false?))))))
+
+(define (composite-file name . files)
+  "Return an object representing store file NAME containing the text contents
+of all file-like objects in FILES."
+  (define builder
+    (with-imported-modules (source-module-closure
+                            '((ice-9 rdelim)))
+      (gexp (begin
+              (use-modules (ice-9 rdelim))
+              (call-with-output-file (ungexp output "out")
+                (lambda (port)
+                  (set-port-encoding! port "UTF-8")
+                  (ungexp-splicing
+                   (interpose
+                    (map (lambda (file)
+                           (gexp (display (with-input-from-file
+                                              (ungexp file)
+                                            read-string)
+                                          port)))
+                         files)
+                    (gexp (display "\n" port))
+                    'suffix))))))))
+
+  (computed-file name builder))
+
+
+;;;
+;;; Emacs configuration records.
+;;;
+
+(define %default-emacs emacs)
+(define %default-emacs-config-dir "~/.config/emacs/")
+(define %emacs-user-init-filename "init.el")
+(define %emacs-early-init-filename "early-init.el")
+
+(define-configuration/no-serialization emacs-keymap
+  (name
+   (symbol 'global-map)
+   "The symbol of the Emacs keymap in which to bind keys.")
+  (package-name
+    (symbol-or-null '())
+    "The symbol naming the Emacs package providing the keymap, as would be
+used with Emacs @code{require}.  If this field is null (the default), then the
+package for which the keymap is being configured should define the keymap or
+the keymap should otherwise be defined by the time the configuration for the
+package is evaluated.")
+  (repeat?
+   (boolean #f)
+   "Whether to make this keymap a repeat map (@pxref{Repeating,,, emacs, The
+GNU Emacs Manual}).  Repeat maps are created by setting the @code{repeat-map}
+symbol property for each key definition in @code{keys} to the @code{name} of
+this keymap.  Use the @code{repeat-exit} field to override this setting for
+specific bindings.")
+  (repeat-exit
+   (list-of-symbols '())
+   "A list of commands that exit the repeat map.  When @code{repeat?} is true,
+these commands do not get the @code{repeat-map} property.  The meaning of this
+field is similar to that of the @code{:exit} keyword used by the
+@code{defvar-keymap} function in Emacs.  This field has no effect when
+@code{repeat?} is false.")
+  (repeat-enter
+   (list-of-symbols '())
+   "A list of additional commands that enter the repeat map.  When
+@code{repeat?} is true, these commands get the @code{repeat-map} property,
+even when they are not bound in the keymap.  This is only useful when a
+command is not bound in @code{name}, but the repeat map should be accessible
+after that command is invoked (e.g., with @kbd{M-x}).  The meaning of this
+field is similar to that of the @code{:enter} keyword used by the
+@code{defvar-keymap} function in Emacs.  This field has no effect when
+@code{repeat?} is false.")
+  (disabled-commands
+   (alist '())
+   "An association list of command symbols and whether to disable them.  When
+a disabled command is interactively invoked, Emacs asks for confirmation from
+the user (@pxref{Disabling,,, emacs, The GNU Emacs Manual}).  The values of
+this alist should be booleans, which will be stored as the value of the
+@code{disabled} property of each respective command symbol.  Thus, to disable
+the @code{transpose-chars} command and enable the @code{erase-buffer} command,
+you can use:
+
+@lisp
+'((transpose-chars . #t)
+  (erase-buffer . #f))
+@end lisp
+"
+   (sanitizer
+    (alist-sanitizer disabled-commands
+                     ((? symbol?) . (? boolean?)))))
+  (keys
+   (alist '())
+   "An association list of key sequences and binding definitions.  Key
+sequences are Emacs-specific string or vector representations of sequences of
+keystrokes or events.  Strings should be valid arguments to the Emacs function
+@code{kbd}, and they are preferred over the low-level vector
+representations (@pxref{Keymaps,,, elisp, The Emacs Lisp Manual}).  Binding
+definitions should be Emacs command symbols.  As a special case, when a
+binding definition is the boolean false, the key is unset in the keymap."
+   (sanitizer (keys-field-sanitizer keys))))
+
+(define list-of-emacs-keymaps?
+  (list-of emacs-keymap?))
+
+(define-configuration/no-serialization emacs-package
+  (name
+   (symbol)
+   "The symbol naming the Emacs package or library, as would be used with
+Emacs @code{require}.")
+  (package
+    (package-or-null '())
+    "A Guix package providing the Emacs package specified by @code{name}.  If
+the package is built into Emacs, or if there is no associated Guix package,
+this field should be set to the empty list (the default).")
+  (extra-packages
+   (list-of-file-likes '())
+   "A list of packages or file-like objects that provide additional
+functionality used by this package, but which are not installed automatically
+by the Guix package manager as propagated inputs of @code{package}.")
+  (extra-files
+   (alist '())
+   "An association list of filenames and file-like objects specifying files to
+create in the Emacs user directory.  For each entry, a file with the given
+filename will be created in the Emacs user directory with the contents of the
+file-like object.  If a list of file-like objects is given for an entry, the
+new file will contain the combined text contents of all of the file-like
+objects in the list.  This field should be used to add per-package files to
+the Emacs user directory."
+   (sanitizer (alist-sanitizer extra-files
+                               ((? string?)
+                                . (or (? file-like?)
+                                      (? list-of-file-likes?))))))
+  (install?
+   (boolean #t)
+   "Whether to install @code{package} and @code{extra-packages}.")
+  (load-force?
+   (boolean #f)
+   "Whether to force loading of this package immediately when Emacs is
+initialized, rather than deferring loading, for example, until an autoloaded
+function is invoked.  This is similar in effect to the keyword @code{:demand}
+from @code{use-package} and to the inverse of the keyword @code{:defer}.  The
+difference is that when this field is false, package loading should always be
+deferred; @code{use-package} normally does not defer loading when it does not
+set up autoloads, because it doesn't know that Guix handles autoloads on its
+own.")
+  (load-predicates
+   (list-of-elisp-or-gexps '())
+   "A list predicate expressions to evaluate when Emacs is initialized to
+determine whether to evaluate the configuration for this package.  When this
+list is not empty, @emph{all} other configuration for this package should be
+effectively surrounded in the Emacs user initialization file by a block of the
+form: @code{(when @var{load-predicates} @dots{})}.  This is the supercharged
+Guix version of the @code{use-package} @code{:if} keyword!
+
+If multiple load predicates are specified, the behavior is determined by the
+package configuration serializer.  Both @code{%emacs-use-package-serializer}
+and the @code{%emacs-use-package-serializer} compose load predicates using
+@code{and}, so that all load predicates in the list must be satisfied in order
+for the package configuration to be evaluated.")
+  (load-after-packages
+   (list-of-symbols '())
+   "A list of symbols for Emacs packages that must be loaded before this
+package is loaded.  Only after all of the packages in the list have been
+loaded by Emacs should configuration for this package be evaluated.  This is
+similar to a simplified version of the @code{:after} keyword from
+@code{use-package}.")
+  (load-paths
+   (list-of-string-or-file-likes '())
+   "A list of additional load paths to add to the Emacs @code{load-paths}
+variable.  Load paths can be specified either as strings or as file-like
+objects, in which case a path to the respective store item is substituted.")
+  (autoloads
+   (list-of-symbols '())
+   "A list of Emacs functions from the package to autoload.  This can be
+useful, for example, when defining custom commands in the Emacs user
+initialization file that use functions which are not autoloaded by default.")
+  (autoloads-interactive
+   (list-of-symbols '())
+   "A list of additional Emacs interactive commands from the package to
+autoload, so that they can be invoked interactively before the package is
+loaded.")
+  (keys-global
+   (alist '())
+   "An association list of key sequences (as strings or vectors) and Emacs
+commands to bind in the global keymap."
+   (sanitizer (keys-field-sanitizer keys-global)))
+  (keys-global-keymaps
+   (alist '())
+   "An association list of key sequences and Emacs keymap variables to bind to
+them in the global keymap.  The keymap variables should be symbols that define
+keymaps in the package; they can be effectively autoloaded using this
+assumption."
+   (sanitizer (alist-sanitizer field-name
+                               ((? string-or-vector?) . (? symbol?)))))
+  (keys-override
+   (alist '())
+   "An association list of key sequences and symbols naming Emacs commands to
+bind in the global override map.  These key bindings have a higher precedence
+than local and global keybindings."
+   (sanitizer (keys-field-sanitizer keys-override)))
+  (keys-local
+   (list-of-emacs-keymaps '())
+   "A list of key binding configurations for specific keymaps, each contained
+in an @code{emacs-keymap} object.")
+  (options
+   (alist '())
+   "An association list of user options and values for this package.
+Options should be symbols naming Emacs variables, and values can be any object
+that can be serialized to Elisp.  For values, primitive Scheme data types are
+implicitly quoted, including lists and symbols.  To instead set an option to
+the value of an expression to be evaluated at Emacs initialization time,
+either use an Elisp expression
+(e.g., specified with the @code{elisp} form) or a G-expression for a value."
+   (sanitizer (alist-sanitizer options
+                               ((? symbol?) . (? elispifiable?)))))
+  (faces
+   (alist '())
+   "An association list of face symbols and face specs.  @xref{Defining
+Faces,,, elisp, The Emacs Lisp Manual} for the format of face specs."
+   (sanitizer (alist-sanitizer
+               faces
+               ((? symbol?)
+                . (((or 'default #t 't (? list?)) . (prop . rest)) ..1)))))
+  (hooks
+   (alist '())
+   "An association list of hooks and functions to add to them.  Each entry is
+a pair of symbols.  Hook symbols in Emacs should end in @samp{-hook}, but the
+@code{%emacs-simple-package-serializer} and
+@code{%emacs-use-package-serializer} serializers effectively add this suffix
+when necessary."
+   (sanitizer (alist-sanitizer hooks
+                               ((? symbol?) . (? symbol?)))))
+  (auto-modes
+   (alist '())
+   "An association list of filename patterns as regular expression strings and
+Emacs mode functions to call when visiting files with filenames that match the
+patterns.  @xref{Auto Major Mode,,, elisp, The Emacs Lisp Manual}, for
+details."
+   (sanitizer (alist-sanitizer auto-modes
+                               ((? string?) . (? symbol?)))))
+  (magic-modes
+   (alist '())
+   "An association list regular expression strings and Emacs mode functions to
+call when visiting files that begin with matching text.  @xref{Auto Major
+Mode,,, elisp, The Emacs Lisp Manual}, for details."
+   (sanitizer (alist-sanitizer magic-modes
+                               ((? string?) . (? symbol?)))))
+  (extra-after-load
+   (list-of-elisp-or-gexps '())
+   "A list of Elisp expressions or G-expressions to evaluate after the package
+is loaded, as with the Emacs @code{eval-after-load} function.  Elisp
+expressions can be specified using the @code{elisp} syntax or the @code{#%}
+reader extension.")
+  (extra-init
+   (list-of-elisp-or-gexps '())
+   "A list of Elisp expressions or G-expressions to evaluate immediately when
+Emacs is initialized, even if loading is deferred due to the
+@code{load-force?} field.  Note that the @code{load-predicates} field should
+still determine whether these expressions are evaluated, and they will only be
+evaluated after all packages specified in the @code{load-after-packages} field
+have been loaded.")
+  (extra-keywords
+   (alist '())
+   "An association list of keys and lists of extra Elisp expressions or
+G-expressions.  Keys can potentially be any keyword or symbol object; keywords
+are automatically serialized to their Emacs Lisp equivalent (e.g.,
+@code{#:keyword} is serialized as @code{:keyword}).  The meanings of entries
+is specific to each package serializer, and any key may be ignored by a
+package serializer.  This field is currently ignored by the
+@code{%emacs-simple-package-serializer}.  Entries in this list matching
+@code{use-package} keywords will be spliced by the
+@code{%emacs-use-package-serializer} into the @code{use-package} body, after
+all other forms."
+   (sanitizer (alist-sanitizer extra-keywords
+                               ((? keyword-or-symbol? key)
+                                . (? list-of-elisp-or-gexps? val))))))
+
+(define list-of-emacs-packages?
+  (list-of emacs-package?))
+
+(define-configuration/no-serialization emacs-configuration
+  (early-init
+   (list-of-elisp-or-gexps '())
+   "A list of Elisp expressions or G-expressions to serialize to the Emacs
+early init file, the @file{early-init.el} file in the appropriate Emacs
+configuration directory.")
+  (extra-init-files
+   (alist '())
+   "An association list of filenames and file-like objects containing Emacs
+Lisp to load when Emacs is initialized.  For each entry, a file with the text
+contents of the file-like object, or the combined text contents of all of the
+file-like objects in a list if a list is specified, will be created with the
+given filename in the appropriate Emacs configuration directory (the directory
+where the @file{early-init.el} and @file{init.el} files are located).  These
+files will then be loaded when Emacs is initialized, before the expressions
+specified in @code{extra-init} are evaluated.
+
+Note that it is an error to specify files with the filenames @samp{init.el}
+and @samp{early-init.el}, because these files are already generated by the
+Emacs home service."
+   (sanitizer (alist-sanitizer extra-init-files
+                               ((? string?)
+                                . (or (? file-like?)
+                                      (? list-of-file-likes?))))))
+  (extra-files
+   (alist '())
+   "An association list of filenames and file-like objects specifying files to
+create in the Emacs user directory.  For each entry, a file with the given
+filename will be created with the contents of the file-like object.  If a list
+of file-like objects is given for an entry, the new file will contain the
+combined text contents of all of the file-like objects in the list.  This
+field can be used to add configuration files for Emacs that should not be
+automatically loaded when Emacs is initialized.
+
+Note that the Emacs user directory, which can be specified using the
+@code{user-emacs-directory} field of the @code{home-emacs-configuration}
+record for the service, may not be the same as the directory containing Emacs
+configuration files, such as the Emacs user initialization file or files
+created according to the @code{extra-init-files} field."
+   (sanitizer (alist-sanitizer extra-files
+                               ((? string?)
+                                . (or (? file-like?)
+                                      (? list-of-file-likes?))))))
+  (variables
+   (alist '())
+   "An association list of Emacs variables and values to set in the Emacs
+initialization file.  Variables should be symbols naming Emacs variables, and
+values can be any objects that can be serialized to Elisp.  For values,
+primitive Scheme data types are implicitly quoted, including lists and
+symbols.  To instead set an option to the value of an expression to be
+evaluated at Emacs initialization time, use either an Elisp expression (e.g.,
+specified with the @code{elisp} form) or a G-expression as a value.  For
+convenience, a file-like object can be given directly as a value, in which
+case it will be substituted with a path name in the store as if it was
+included within an Elisp expression or G-expression.  Note that it is an error
+to specify an Elisp expression value that contains only comments or whitespace
+for this field."
+   (sanitizer (alist-sanitizer variables
+                               ((? symbol?) . (? elispifiable?)))))
+  (modes
+   (alist '())
+   "An association list of global minor modes and arguments.  When an argument
+is true or false, enable or disable the mode, respectively, when Emacs is
+initialized.  Otherwise, the argument will be passed to the mode's toggle
+function.  For example, to disable @code{tool-bar-mode}, enable
+@code{pixel-scroll-precision-mode}, and enable @code{fringe-mode} with the
+argument @code{20}, you could use:
+
+@lisp
+'((tool-bar-mode . #f)
+  (pixel-scroll-precision-mode . #t)
+  (fringe-mode . 20))
+@end lisp
+
+@noindent.  Arguments given as lists and symbols are implicitly quoted.  Use
+Elisp expressions (e.g., specified with the @code{elisp} form) or
+G-expressions to specify arguments that should be evaluated at Emacs
+initialization time."
+   (sanitizer (alist-sanitizer modes
+                               ((? symbol?) . (? elispifiable?)))))
+  (keys
+   (alist '())
+   "An association list of key bindings for the Emacs global keymap.
+Entries are pairs of key sequences and binding definitions.  Key sequences are
+Emacs-specific string or vector representations of sequences of keystrokes or
+events.  Strings should be valid arguments to the Emacs function @code{kbd},
+and they are preferred over the low-level vector representations.  Here are
+some examples of valid string values: @samp{\"C-c a\"}, @samp{\"M-RET\"},
+@samp{\"M-<up>\"}, @samp{\"<remap> <foo>\"}, and
+@samp{\"<t>\"} (@pxref{Keymaps,,, elisp,The Emacs Lisp Manual}).  Binding
+definitions should be symbols for Emacs commands."
+   (sanitizer (keys-field-sanitizer keys)))
+  (keys-override
+   (alist '())
+   "An association list of key sequences and Emacs commands to bind in the
+global override map.  These key bindings have a higher precedence than local
+and global keybindings."
+   (sanitizer (keys-field-sanitizer keys-override)))
+  (extra-init
+   (list-of-elisp-or-gexps '())
+   "A list additional of Elisp expressions or G-expressions to serialize to
+the Emacs user initialization file, the @file{init.el} file in the appropriate
+Emacs configuration directory.  These expressions will occur in the serialized
+file after those corresponding to the above fields."))
+
+(define-configuration/no-serialization emacs-server
+  (name
+   (string)
+   "A string naming the server.  Users will subsequently be able to start the
+new server by using the command @code{herd start emacs-@var{name}}.  To create
+Emacs client frames for the sever, users can use commands like:
+@code{emacsclient --create-frame --socket-name=@var{name}}.
+
+Because this string is meant for use in shell commands (and filenames), it
+should not contain any characters other than letters and digits and the
+characters @samp{-}, @samp{_}, and @samp{.}."
+   (sanitizer
+    (lambda (str)
+      (cond
+       ((not (string? str))
+        (configuration-field-error #f 'name str))
+       ((string-any (char-set-complement
+                     (char-set-union char-set:letter+digit
+                                     (char-set #\- #\_ #\.)))
+                    str)
+        (configuration-field-error #f 'name str))
+       (else str)))))
+  (inherit-directory?
+   (boolean #t)
+   "Whether the server should share its Emacs user directory with that of
+the Emacs home service.  When false, the server will use a subdirectory
+of the one used by the service for its own user directory.  When true
+(the default), the @code{user-emacs-directory} Emacs variable for the server
+will be set to that of the Emacs home service, but the server will still load
+its own @file{early-init.el} and @file{init.el} files.  See the
+@code{inherit-init?} and @code{inherit-configured-packages?}  fields for how
+to inherit configuration from other Emacsen.")
+  (inherit-init?
+   (boolean #t)
+   "Whether to load the default configuration used by the Emacs home service,
+that is, the initialization expressions specified by the @code{default-init}
+field of the @code{home-emacs-configuration} value for the service.  This is
+loaded in addition to any configuration specified in the @code{default-init}
+field for this specific server.
+
+Note that if @code{inherit-directory?} is false, this also results in the
+creation of duplicate copies in the Emacs user directory for the server of any
+files specified by the @code{extra-files} field of the
+@code{emacs-configuration} record for the @code{home-emacs-configuration} of
+the service.  This ensures that any references to those files in the inherited
+configuration expressions will not fail in unexpected ways.")
+  (inherit-configured-packages?
+   (boolean #t)
+   "Whether to load configuration for packages used by the Emacs home service,
+that is, the package configuration specified in the @code{configured-packages}
+field of the @code{home-emacs-configuration} value for the service.  This is
+loaded in addition to any configuration specified with the
+@code{configured-packages} field for this specific server.
+
+Note that if @code{inherit-directory?} is false, this also results in the
+creation of duplicate copies in the Emacs user directory for the server of any
+files specified by the @code{extra-files} fields of @code{emacs-package}
+records from the @code{configured-packages} field of the
+@code{home-emacs-configuration} of the service.")
+  (load-custom?
+   (boolean #t)
+   "Whether to load customizations created with the Emacs customization
+interface.  When @code{inherit-directory?} is true, customizations made within
+this specific server affect the Emacs home service, and vice versa.
+Otherwise, the server has its own separate set of customizations.")
+  (extra-packages
+   (list-of-file-likes '())
+   "A list of extra packages or file-like objects to install, without
+associated configuration.")
+  (auto-start?
+   (boolean #t)
+   "Whether to start the server automatically.")
+  (debug?
+   (boolean #f)
+   "Whether to enable the Emacs Lisp debugger for errors in the initialization
+files of the server.")
+  (shepherd-requirements
+   (list-of-symbols '())
+   "A list of symbols specifying Shepherd services that must be started before
+the service for the Emacs server can be started (@pxref{Defining Services,,,
+shepherd, The GNU Shepherd Manual}).")
+  (default-init
+    (emacs-configuration (emacs-configuration))
+    "Configuration used to create initialization files specifically for this
+server.")
+  (configured-packages
+   (list-of-emacs-packages '())
+   "A list of @code{emacs-package} objects specifying Emacs packages to
+install and configure in the Emacs user initialization file for the server."))
+
+(define list-of-emacs-servers?
+  (list-of emacs-server?))
+
+
+;;;
+;;; Emacs package configuration serializers.
+;;;
+
+(define-configuration/no-serialization emacs-package-serializer
+  (name
+   (symbol)
+   "A symbol identifying the serializer.")
+  (procedure
+   (procedure)
+   "A procedure that takes two arguments, an @code{emacs-package} object and
+the @code{package} object providing GNU Emacs for the Emacs home service, and
+that should return a list of @code{elisp} objects or G-expressions containing
+package-specific configuration to serialize to the Emacs user initialization
+file.")
+  (dependencies
+   (alist '())
+   "An association list of additional packages to install whenever this
+serializer is used and predicates to determine whether to install them.  Each
+predicate should be a procedure that accepts one argument, the @code{package}
+object providing the GNU Emacs for the Emacs home service."
+   (sanitizer (alist-sanitizer dependencies
+                               ((? file-like?) . (? procedure?)))))
+  (indent-forms
+   (alist '())
+   "An association list of symbols and indentation rules.  Each entry is of
+the form (@var{symbol} .  @var{indent}), where @var{symbol} is a symbol and
+@var{indent} is an integer.  Values have the same effect as the
+@code{indent-forms} field in the @code{home-emacs-configuration} record.
+
+ Note that indentation rules specified here will subsequently affect all Emacs
+Lisp expressions serialized by the Emacs home service, not just
+package-specific configuration."
+   (sanitizer (alist-sanitizer indent-forms
+                               ((? symbol?) . (? integer?)))))
+  (description
+   (string "")
+   "A brief description of the serializer."))
+
+(define (emacs-version-<29? emacs)
+  "Return true if the version of EMACS, a `package' object, is less than 29,
+and return false otherwise."
+  (eq? (version-compare (package-version emacs) "29") '<))
+
+(define (compose-load-predicates-lambda composer)
+  "Return a lambda that composes multiple load predicates into a single
+s-expression beginning with symbol COMPOSER."
+  (match-lambda
+    (() '())
+    (lst
+     (if (> (length lst) 1)
+         `(,composer ,@lst)
+         (first lst)))))
+
+(define (emacs-package->simple-elisp config emacs)
+  "Return from `emacs-package' object CONFIG a list containing Elisp
+expressions that configure EMACS using only minimal built-in functionality."
+  (let ((<29? (emacs-version-<29? emacs)))
+    (define (load-path->sexp obj)
+      `(add-to-list 'load-path ,obj))
+
+    (define keys-global->sexp
+      (match-lambda  (((? vector? k) . s)
+                      `(global-set-key ,k ,(elispifiable->elisp s)))
+                     (((? string? k) . s)
+                      (if <29?
+                          `(global-set-key (kbd ,k) ,(elispifiable->elisp s))
+                          `(keymap-global-set ,k ,(elispifiable->elisp s))))))
+
+    (define keys-override->sexp
+      (match-lambda ((k . s)
+                     `(bind-key* ,k ,(elispifiable->elisp s)))))
+
+    (define (keys-local->sexp keymap)
+      (match-record keymap <emacs-keymap>
+                    (name
+                     package-name
+                     repeat?
+                     repeat-exit
+                     repeat-enter
+                     disabled-commands
+                     keys)
+        (let ((keydefs (append
+                        (map (match-lambda
+                               (((? vector? k) . s)
+                                `(define-key ,name ,k
+                                   ,(elispifiable->elisp s)))
+                               (((? string? k) . s)
+                                (if <29?
+                                    `(define-key ,name (kbd ,k)
+                                       ,(elispifiable->elisp s))
+                                    `(keymap-set ,name ,k
+                                                 ,(elispifiable->elisp s)))))
+                             keys))))
+          (append
+           (if repeat?
+               (list `(progn
+                       (defvar ,name
+                         (make-sparse-keymap))
+                       ,@keydefs
+                       ,@(map (lambda (s)
+                                `(put ',s 'repeat-map ',name))
+                              (delete-duplicates
+                               (append
+                                (filter-map (match-lambda
+                                              ((_ . s)
+                                               (if (or (not s)
+                                                       (memq s repeat-exit))
+                                                   #f
+                                                   s)))
+                                            keys)
+                                repeat-enter)
+                               eq?))))
+               (list `(if (boundp ',name)
+                          (progn
+                           ,@keydefs)
+                          (with-eval-after-load
+                           ',(if (not (null? package-name))
+                                 package-name
+                                 (emacs-package-name config))
+                           ,@keydefs))))
+           (map (match-lambda
+                  ((command . val)
+                   `(put ',command 'disabled ,val)))
+                disabled-commands)))))
+
+    (define option->sexp
+      (match-lambda ((key . val)
+                     (if <29?
+                         `(setq ,key ,(elispifiable->elisp val))
+                         `(setopt ,key ,(elispifiable->elisp val))))))
+
+    (define face->sexp
+      (match-lambda ((face . spec)
+                     `(face-spec-set ',face ',spec))))
+
+    (define hook->sexp
+      (match-lambda ((hook . func)
+                     (let* ((str (symbol->string hook))
+                            (hook* (string->symbol
+                                    (if (not (string-suffix? "-hook" str))
+                                        (string-append str "-hook")
+                                        str))))
+                       `(add-hook ',hook* (function ,func))))))
+
+    (define auto-mode->sexp
+      (match-lambda ((pat . mode)
+                     `(add-to-list 'auto-mode-alist '(,pat . ,mode)))))
+
+    (define magic-mode->sexp
+      (match-lambda ((pat . mode)
+                     `(add-to-list 'magic-mode-alist '(,pat . ,mode)))))
+
+    (match-record config <emacs-package>
+                  (name
+                   load-force?
+                   load-predicates
+                   load-after-packages
+                   load-paths
+                   autoloads
+                   autoloads-interactive
+                   keys-global
+                   keys-global-keymaps
+                   keys-override
+                   keys-local
+                   options
+                   faces
+                   hooks
+                   auto-modes
+                   magic-modes
+                   extra-after-load
+                   extra-init
+                   extra-keywords)
+
+      (define (autoload->sexp* obj interactive)
+        `(autoload (function ,obj) ,(symbol->string name) #f ,interactive))
+
+      (define autoload->sexp
+        (cut autoload->sexp* <> #f))
+
+      (define autoload-interactive->sexp
+        (cut autoload->sexp* <> #t))
+
+      (define keys-global-keymaps->sexp
+        (match-lambda (((? vector? ks) . obj)
+                       `(progn
+                         (autoload ',obj
+                                   ,(symbol->string name)
+                                   #f #f 'keymap)
+                         (global-set-key ,ks ,obj)))
+                      (((? string? ks) . obj)
+                       `(progn
+                         (autoload ',obj
+                                   ,(symbol->string name)
+                                   #f #f 'keymap)
+                         ,(if <29?
+                              `(global-set-key (kbd ,ks) ,obj)
+                              `(keymap-global-set ,ks ,obj))))))
+
+      (define (load-after-packages->sexp load-after extra)
+        (let loop ((load-after (reverse load-after))
+                   (acc '()))
+          (if (null? load-after)
+              acc
+              (loop (cdr load-after)
+                    (cons 'with-eval-after-load
+                          (cons (list 'quote (car load-after))
+                                (if (null? acc)
+                                    extra
+                                    (list acc))))))))
+
+      (let* ((load-predicates* (apply (compose-load-predicates-lambda 'and)
+                                      (list load-predicates)))
+             (load-after-packages* load-after-packages)
+             (load-paths* (map load-path->sexp load-paths))
+             (autoloads* (map autoload->sexp autoloads))
+             (autoloads-interactive* (map autoload-interactive->sexp
+                                          autoloads-interactive))
+             (keys-global* (map keys-global->sexp keys-global))
+             (keys-global-keymaps* (map keys-global-keymaps->sexp
+                                        keys-global-keymaps))
+             (keys-override* (map keys-override->sexp keys-override))
+             (keys-local* (append-map keys-local->sexp keys-local))
+             (options* (map option->sexp options))
+             (faces* (map face->sexp faces))
+             (hooks* (map hook->sexp hooks))
+             (auto-modes* (map auto-mode->sexp auto-modes))
+             (magic-modes* (map magic-mode->sexp magic-modes))
+             (extra-after-load* (cond
+                                 (load-force?
+                                  (list
+                                   `(if (not (require ',name nil t))
+                                        (display-warning
+                                         'initialization
+                                         (format "Failed to load %s" ',name)
+                                         :error)
+                                        ,@extra-after-load)))
+                                 ((not (null? extra-after-load))
+                                  (list `(with-eval-after-load
+                                          (quote ,name)
+                                          ,@extra-after-load)))
+                                 (else '())))
+             (after-packages-sexps (append autoloads*
+                                           autoloads-interactive*
+                                           keys-global*
+                                           keys-override*
+                                           keys-global-keymaps*
+                                           keys-local*
+                                           options*
+                                           faces*
+                                           hooks*
+                                           auto-modes*
+                                           magic-modes*
+                                           extra-after-load*
+                                           extra-init))
+             (combined-sexps (append load-paths*
+                                     (if (null? load-after-packages*)
+                                         after-packages-sexps
+                                         (list (load-after-packages->sexp
+                                                load-after-packages*
+                                                after-packages-sexps)))))
+             (comment-string (string-append ";;; Package "
+                                            (symbol->string name)
+                                            "\n")))
+        (if (null? combined-sexps)
+            '()
+            (append
+             (list (elisp (unelisp-comment comment-string)))
+             (if (null? load-predicates*)
+                 (map sexp->elisp combined-sexps)
+                 (list (sexp->elisp `(when ,load-predicates*
+                                       ,@combined-sexps))))))))))
+
+(define %emacs-simple-package-serializer
+  (emacs-package-serializer
+   (name 'emacs-simple-package)
+   (procedure emacs-package->simple-elisp)
+   (description "An Emacs package configuration serializer that configures
+Emacs using minimal, built-in Emacs mechanisms, instead of complex macros such
+as @code{use-package}.")))
+
+(define (emacs-package->use-package-elisp config emacs)
+  "Return from `emacs-package' object CONFIG a list containing Elisp
+expressions that configures EMACS using the `use-package' macro."
+
+  (define-syntax unless-null
+    (syntax-rules ()
+      ((_ var exp)
+       (if (null? var)
+           '()
+           exp))
+      ((_ var)
+       var)))
+
+  (define (keys-local->sexp config)
+    (match-record config <emacs-keymap>
+                  (name repeat? repeat-exit keys)
+      (cond
+       ((null? keys) '())
+       (repeat?
+        (receive (exit rest)
+            (partition (match-lambda
+                         ((_ . binding)
+                          (memq binding repeat-exit)))
+                       keys)
+          `(:repeat-map ,name
+            ,@rest
+            ,@(if (null? exit)
+                  '()
+                  `(:exit
+                    ,(elisp (unelisp-newline))
+                    ,@exit)))))
+       (else `(:map ,name
+               ,@keys)))))
+
+  (define (keys-local->extra-sexps config)
+    (match-record config <emacs-keymap>
+                  (name repeat? repeat-enter disabled-commands keys)
+      (append
+       (if (and repeat?
+                (not (null? keys)))
+           (map
+            (lambda (symbol)
+              `(put ',symbol 'repeat-map ',name))
+            repeat-enter)
+           '())
+       (map (match-lambda ((command . val)
+                           `(put ',command 'disabled ,val)))
+            disabled-commands))))
+
+  (define option->sexp
+    (match-lambda ((key . val)
+                   `(,key ,(elispifiable->elisp val)))))
+
+  (define face->sexp
+    (match-lambda ((key . val)
+                   `(,key ,val))))
+
+  (define hook->sexp
+    (match-lambda ((hook . func)
+                   (let* ((str (symbol->string hook))
+                          (hook* (string->symbol
+                                  (if (string-suffix? "-hook" str)
+                                      (string-drop-right str 5)
+                                      str))))
+                     `(,hook* . ,func)))))
+
+  (define use-package-keywords '(#:after
+                                 #:autoload
+                                 #:bind
+                                 #:bind*
+                                 #:bind-keymap
+                                 #:bind-keymap*
+                                 #:catch
+                                 #:commands
+                                 #:config
+                                 #:custom
+                                 #:custom-face
+                                 #:defer
+                                 #:defines
+                                 #:demand
+                                 #:disabled
+                                 #:functions
+                                 #:hook
+                                 #:if
+                                 #:init
+                                 #:interpreter
+                                 #:load
+                                 #:load-path
+                                 #:magic
+                                 #:magic-fallback
+                                 #:mode
+                                 #:no-require
+                                 #:preface
+                                 #:requires
+                                 #:unless
+                                 #:when))
+
+  (define symbol->keyword*
+    (match-lambda
+      ((? symbol? kw)
+       (let* ((str (symbol->string kw))
+              (str* (if (string-prefix? ":" str)
+                        (string-drop str 1)
+                        str)))
+         (symbol->keyword (string->symbol str*))))
+      ((? keyword? kw)
+       kw)))
+
+  (define (use-package-keyword? obj)
+    (memq (symbol->keyword* obj) use-package-keywords))
+
+  (define extra-keyword->sexp
+    (match-lambda
+      (((? use-package-keyword? kw) . exps)
+       `(,(symbol->keyword* kw) ,@exps))
+      (_ #f)))
+
+  (match-record config <emacs-package>
+                (name
+                 load-force?
+                 load-predicates
+                 load-after-packages
+                 load-paths
+                 autoloads
+                 autoloads-interactive
+                 keys-global
+                 keys-global-keymaps
+                 keys-override
+                 keys-local
+                 options
+                 faces
+                 hooks
+                 auto-modes
+                 magic-modes
+                 extra-after-load
+                 extra-init
+                 extra-keywords)
+    (let* ((load-predicates* (apply (compose-load-predicates-lambda 'and)
+                                    (list load-predicates)))
+           (load-after-packages* load-after-packages)
+           (autoloads* autoloads)
+           (autoloads-interactive* autoloads-interactive)
+           (load-paths* load-paths)
+           (keys-global+local (append keys-global
+                                      (append-map keys-local->sexp
+                                                  keys-local)))
+           (keys-global-keymaps* keys-global-keymaps)
+           (keys-override* keys-override)
+           (options* (map option->sexp options))
+           (faces* (map face->sexp faces))
+           (hooks* (map hook->sexp hooks))
+           (auto-modes* auto-modes)
+           (magic-modes* magic-modes)
+           (extra-after-load* extra-after-load)
+           (extra-init* (append extra-init
+                                (append-map keys-local->extra-sexps
+                                            keys-local)))
+           (extra-keywords* (apply append (filter-map extra-keyword->sexp
+                                                      extra-keywords)))
+           (comment-string (string-append ";;; Package "
+                                          (symbol->string name)
+                                          "\n"))
+           (combined-sexps (append
+                            (list
+                             `(use-package
+                               ,name
+                               ,@(if load-force?
+                                     '(:demand t)
+                                     '(:defer t))
+                               ,@(unless-null load-after-packages*
+                                              `(:after ,load-after-packages*))
+                               ,@(unless-null load-paths*
+                                              `(:load-path ,load-paths*))
+                               ,@(unless-null autoloads*
+                                              `(:autoload ,autoloads*))
+                               ,@(unless-null autoloads-interactive*
+                                              `(:commands
+                                                ,autoloads-interactive*))
+                               ,@(unless-null keys-global+local
+                                              `(:bind ,keys-global+local))
+                               ,@(unless-null keys-override*
+                                              `(#:bind* ,keys-override*))
+                               ,@(unless-null keys-global-keymaps*
+                                              `(:bind-keymap
+                                                ,keys-global-keymaps*))
+                               ,@(unless-null hooks*
+                                              `(:hook ,hooks*))
+                               ,@(unless-null auto-modes*
+                                              `(:mode ,auto-modes*))
+                               ,@(unless-null magic-modes*
+                                              `(:magic ,magic-modes*))
+                               ,@(unless-null faces*
+                                              `(:custom-face
+                                                ,@(append (list
+                                                           (elisp
+                                                            (unelisp-newline)))
+                                                          faces*)))
+                               ,@(unless-null options*
+                                              `(:custom
+                                                ,@(append (list
+                                                           (elisp
+                                                            (unelisp-newline)))
+                                                          options*)))
+                               ,@(unless-null extra-after-load*
+                                              `(:config
+                                                ,@(append (list
+                                                           (elisp
+                                                            (unelisp-newline)))
+                                                          extra-after-load*)))
+                               ,@extra-keywords*
+                               ,@(unless-null extra-init*
+                                              `(:init
+                                                ,@(append (list
+                                                           (elisp
+                                                            (unelisp-newline)))
+                                                          extra-init*))))))))
+      (if (null? combined-sexps)
+          '()
+          (append
+           (list (elisp (unelisp-comment comment-string)))
+           (if (null? load-predicates*)
+               (map sexp->elisp combined-sexps)
+               (list (sexp->elisp `(when ,load-predicates*
+                                     ,@combined-sexps)))))))))
+
+(define %emacs-use-package-serializer
+  (emacs-package-serializer
+   (name 'emacs-use-package)
+   (procedure emacs-package->use-package-elisp)
+   (indent-forms '((use-package . 1)))
+   (dependencies `((,emacs-use-package . ,emacs-version-<29?)))
+   (description "An Emacs package configuration serializer that configures
+Emacs with the @code{use-package} macro.")))
+
+
+;;;
+;;; Emacs home service.
+;;;
+
+(define-configuration/no-serialization home-emacs-configuration
+  (emacs
+   (package %default-emacs)
+   "The package providing the @file{/bin/emacs} command.")
+  (user-emacs-directory
+   (string "~/.config/emacs/")
+   "Directory beneath which additional Emacs user files are placed.
+By default, this is also the directory that contains the @file{init.el} and
+@file{early-init.el} Emacs initialization files, but you can change this field
+to specify any directory of your choosing; initialization files generated by
+this service will still be loaded."
+   (sanitizer
+    (lambda (str)
+      ;; Ensure that the path name ends with a '/', as some low-level Emacs
+      ;; libraries use the value of `user-emacs-directory' with this
+      ;; expectation.
+      (cond
+       ((not (string? str))
+        (configuration-field-error #f 'user-emacs-directory str))
+       ((not (string-suffix? "/" str))
+        (string-append str "/"))
+       (else str)))))
+  (native-compile?
+   (boolean #t)
+   "Whether to enable native-compilation of Emacs packages by building them
+with the Emacs specified by the @code{emacs} field rather than
+@code{emacs-minimal}.")
+  (load-custom?
+   (boolean #t)
+   "Whether to load customizations created with the Emacs customization
+interface.  Because all configuration files created by this service are
+effectively read-only, the service modifies the default behavior of Emacs so
+that customizations are always saved in a separate @file{custom.el} file,
+which will be loaded when Emacs is initialized if this field is true.")
+  (extra-packages
+   (list-of-file-likes '())
+   "A list of additional Emacs-related packages or file-like objects to
+install.  If a package is specified in @code{configured-packages}, it does not
+need to be specified here.")
+  (package-serializer
+   (emacs-package-serializer %emacs-simple-package-serializer)
+   "The serializer to use for configuration specified by @code{emacs-package}
+objects.")
+  (indent-forms
+   (alist '())
+   "An association list of symbols and indentation rules.  Each entry is of
+the form (@var{symbol} .  @var{indent}), where @var{symbol} is a symbol and
+@var{indent} is an integer.
+
+When @var{symbol} occurs at the beginning of a list in an Emacs Lisp file, the
+first @var{indent} expressions are indented as arguments and the remainder as
+body expressions, as if @var{indent} was supplied as the
+@code{lisp-indent-function} symbol property for @var{symbol} in Emacs.
+Argument expressions are either printed on the same line as @var{symbol} or
+indented 4 columns beyond the base indentation of the enclosing list, and body
+expressions are indented 2 columns beyond the base indentation."
+   (sanitizer (alist-sanitizer indent-forms
+                               ((? symbol?) . (? integer?)))))
+  (propagated-init
+   (list-of-elisp-or-gexps '())
+   "A list of Elisp expressions or G-expressions that should be evaluated by
+all Emacsen during initialization, including servers.  These expressions are
+serialized to the beginning of the Emacs user initialization file.")
+  (default-init
+    (emacs-configuration (emacs-configuration))
+    "General configuration used to create Emacs initialization files.  Emacsen
+will use this configuration by default, in addition to any package-specific
+configuration specified in the @code{configured-packages} field and any
+appropriate configuration for specific servers.")
+  (configured-packages
+   (list-of-emacs-packages '())
+   "A list of Emacs-related packages to install and associated configuration
+for the Emacs user initialization file.  @code{emacs-package} objects
+encapsulate lists of packages to install along with relevant configuration.")
+  (servers
+   (list-of-emacs-servers '())
+   "A list of configurations for Emacs servers."))
+
+(define-configuration/no-serialization home-emacs-extension
+  (extra-packages
+   (list-of-file-likes '())
+   "A list of additional Emacs-related packages or file-like objects to
+install.  If a package is specified in @code{configured-packages}, it does not
+need to be specified here.")
+  (indent-forms
+   (alist '())
+   "An association list of symbols and indentation rules.  Each entry is of
+the form (SYMBOL . INDENT), where SYMBOL is a symbol and INDENT is an integer
+specifying the number of argument expressions for SYMBOL."
+   (sanitizer (alist-sanitizer indent-forms
+                               ((? symbol?) . (? integer?)))))
+  (servers
+   (list-of-emacs-servers '())
+   "A list of configurations for Emacs servers.  It is an error to specify
+multiple @code{emacs-server} objects with equivalent @code{name} fields.")
+  (default-init
+    (emacs-configuration (emacs-configuration))
+    "General configuration used to create the Emacs initialization files.
+Emacsen will use this configuration by default, in addition to any
+package-specific configuration specified in the @code{configured-packages}
+field and any relevant configuration for specific servers.")
+  (configured-packages
+   (list-of-emacs-packages '())
+   "A list of Emacs-related packages and associated configuration for the
+Emacs user initialization file.  Configuration for multiple
+@code{emacs-package} objects with equivalent @code{name} fields is merged when
+possible; an error is signaled otherwise."))
+
+(define (extend-emacs-configuration original extensions)
+  "Extend an `emacs-configuration' record ORIGINAL with list of records
+EXTENSIONS."
+  (extend-record
+   emacs-configuration
+   original extensions
+   (extend-list early-init early-init)
+   (extend-alist-merge extra-init-files extra-init-files)
+   (extend-alist-merge extra-files extra-files)
+   (extend-list variables variables)
+   (extend-list modes modes)
+   (extend-list keys keys)
+   (extend-list keys-override keys-override)
+   (extend-list extra-init extra-init)))
+
+(define %default-emacs-package-configuration (emacs-package))
+(define %default-emacs-keymap-configuration (emacs-keymap))
+
+(define (extend-emacs-package original extensions)
+  "Extend an `emacs-package' record ORIGINAL with list of records
+EXTENSIONS."
+  (define extend-package-field
+    (cut extend-record-field-default <> <>
+         %default-emacs-package-configuration 'package))
+
+  (define extend-install?-field
+    (cut extend-record-field-default <> <>
+         %default-emacs-package-configuration 'install?))
+
+  (define extend-load-force?-field
+    (cut extend-record-field-default <> <>
+         %default-emacs-package-configuration 'load-force?))
+
+  (define extend-package-name-field
+    (cut extend-record-field-default <> <>
+         %default-emacs-keymap-configuration 'package-name))
+
+  (define extend-repeat?-field
+    (cut extend-record-field-default <> <>
+         %default-emacs-keymap-configuration 'repeat?))
+
+  (define (extend-emacs-keymap original extensions)
+    (extend-record
+     emacs-keymap
+     original extensions
+     (extend-package-name-field package-name package-name)
+     (extend-repeat?-field repeat? repeat?)
+     (extend-list-merge repeat-exit repeat-exit)
+     (extend-list-merge repeat-enter repeat-enter)
+     (extend-list disabled-commands disabled-commands)
+     (extend-list keys keys)))
+
+  (define (extend-keys-local-field original extensions)
+    (extend-record-list-merge original extensions
+                              'name extend-emacs-keymap))
+
+  (extend-record
+   emacs-package
+   original extensions
+   (extend-package-field package package)
+   (extend-list-merge extra-packages extra-packages)
+   (extend-alist-merge extra-files extra-files)
+   (extend-install?-field install? install?)
+   (extend-load-force?-field load-force? load-force?)
+   (extend-list load-predicates load-predicates)
+   (extend-list-merge load-after-packages load-after-packages)
+   (extend-list-merge load-paths load-paths)
+   (extend-list-merge autoloads autoloads)
+   (extend-list-merge autoloads-interactive autoloads-interactive)
+   (extend-list keys-global keys-global)
+   (extend-list keys-global-keymaps keys-global-keymaps)
+   (extend-list keys-override keys-override)
+   (extend-keys-local-field keys-local keys-local)
+   (extend-list options options)
+   (extend-list faces faces)
+   (extend-list hooks hooks)
+   (extend-list auto-modes auto-modes)
+   (extend-list magic-modes magic-modes)
+   (extend-list extra-after-load extra-after-load)
+   (extend-list extra-init extra-init)
+   (extend-alist-merge extra-keywords extra-keywords)))
+
+(define (extend-emacs-package-list original extensions)
+  "Extend a list of `emacs-package' records ORIGINAL with list of lists
+EXTENSIONS by merging records with equivalent `name' fields.  Records with a
+non-null `load-predicates' field will not be merged."
+  (define (emacs-package-no-predicates? config)
+    (match-record config <emacs-package>
+                  (load-predicates)
+      (null? load-predicates)))
+
+  (extend-record-list-merge original extensions
+                            'name extend-emacs-package
+                            #:type? emacs-package-no-predicates?))
+
+(define (server-name->file-name name)
+  "Return the full name for server NAME as a filename."
+  (string-append "emacs-" (string-delete (char-set #\/ #\nul)
+                                         name)))
+
+(define (emacs-server->provision config)
+  "Return the provision symbol for the Shepherd service created for
+@code{emacs-server} object CONFIG."
+  (match-record config <emacs-server>
+                (name)
+    (string->symbol (server-name->file-name name))))
+
+(define (server-user-directory name user-emacs-directory inherit-directory?)
+  "Return the location of the Emacs user directory for server NAME based on
+INHERIT-DIRECTORY? and the USER-EMACS-DIRECTORY from the Emacs home service."
+  (if inherit-directory?
+      user-emacs-directory
+      (string-append (file-name-concat
+                      user-emacs-directory
+                      (server-name->file-name name))
+                     "/")))
+
+(define (home-emacs-packages config)
+  "Return a list of file-like objects to install from CONFIG."
+
+  (define (rewrite-for-native-compile emacs)
+    (package-input-rewriting
+     `((,emacs-minimal . ,emacs))))
+
+  (define (package-serializer-dependencies config emacs)
+    (match-record config <emacs-package-serializer>
+                  (dependencies)
+      (filter-map (match-lambda
+                    ((dep . pred)
+                     (and (apply pred (list emacs))
+                          dep)))
+                  dependencies)))
+
+  (define (emacs-package->installable-packages config)
+    (match-record config <emacs-package>
+                  (package extra-packages install?)
+      (if install?
+          (append (if (null? package)
+                      '()
+                      (list package))
+                  extra-packages)
+          '())))
+
+  (define (server->installable-packages config)
+    (match-record config <emacs-server>
+                  (configured-packages extra-packages)
+      (append
+       (append-map emacs-package->installable-packages configured-packages)
+       extra-packages)))
+
+  (match-record config <home-emacs-configuration>
+                (emacs
+                 native-compile?
+                 configured-packages
+                 extra-packages
+                 package-serializer
+                 servers)
+    (let ((packages (delete-duplicates
+                     (append
+                      (package-serializer-dependencies package-serializer
+                                                       emacs)
+                      (append-map emacs-package->installable-packages
+                                  configured-packages)
+                      extra-packages
+                      (append-map server->installable-packages servers))
+                     eq?)))
+      (append (list emacs)
+              (if native-compile?
+                  (map (rewrite-for-native-compile emacs) packages)
+                  packages)))))
+
+(define (home-emacs-shepherd-services config)
+  "Return a list of Shepherd services for CONFIG."
+  (match-record config <home-emacs-configuration>
+                (emacs user-emacs-directory servers)
+    (map
+     (lambda (server)
+       (match-record server <emacs-server>
+                     (name
+                      inherit-directory?
+                      auto-start?
+                      debug?
+                      shepherd-requirements)
+         (let ((server-init-dir (file-name-concat
+                                 %default-emacs-config-dir
+                                 (server-name->file-name name)))
+               (server-user-dir (server-user-directory name
+                                                       user-emacs-directory
+                                                       inherit-directory?)))
+           (shepherd-service
+            (provision (list (emacs-server->provision server)))
+            (requirement shepherd-requirements)
+            (start
+             #~(make-forkexec-constructor
+                (list #$(file-append emacs "/bin/emacs")
+                      #$(string-append "--init-directory=" server-init-dir)
+                      #$(string-append "--fg-daemon=" name)
+                      #$@(if debug?
+                             (list "--debug-init")
+                             '()))
+                #:log-file
+                #$(file-name-concat server-user-dir
+                                    (string-append
+                                     (server-name->file-name name) ".log"))))
+            (stop
+             #~(make-forkexec-constructor
+                (list #$(file-append emacs "/bin/emacsclient")
+                      "-s" #$name "--eval" "(kill-emacs)")))
+            (actions (list
+                      (shepherd-configuration-action
+                       (file-name-concat server-init-dir
+                                         %emacs-user-init-filename))))
+            (auto-start? auto-start?)
+            (documentation
+             (string-append "Start the Emacs server called "
+                            name "."))))))
+     servers)))
+
+(define (home-emacs-xdg-configuration-files config)
+  "Return from CONFIG an association list of filenames and file-like objects
+to create in XDG_CONFIG_HOME."
+
+  (define emacs-config-filename
+    (cut file-name-concat "emacs" <>))
+
+  (define (elisp-file-with-forms name exps)
+    (elisp-file name exps
+                #:special-forms (append
+                                 (home-emacs-configuration-indent-forms config)
+                                 (emacs-package-serializer-indent-forms
+                                  (home-emacs-configuration-package-serializer
+                                   config)))))
+
+  (define config-emacs (home-emacs-configuration-emacs config))
+
+  (define config-package-serializer-procedure
+    (emacs-package-serializer-procedure
+     (home-emacs-configuration-package-serializer config)))
+
+  (define config-user-emacs-directory
+    (home-emacs-configuration-user-emacs-directory config))
+
+  (define (set-user-emacs-directory-sexps directory)
+    (list `(setq user-emacs-directory ,directory)
+          ;; Variables set before early init file is loaded that rely upon the
+          ;; value of `user-emacs-directory':
+          ;; XXX: `native-comp-eln-load-path' is properly set in startup.el to
+          ;; reflect the new `user-emacs-directory', but this means that
+          ;; servers which use their own `user-emacs-directory' get their own
+          ;; eln cache.
+          '(custom-reevaluate-setting 'auto-save-list-file-prefix)
+          '(custom-reevaluate-setting 'package-user-dir)
+          '(custom-reevaluate-setting 'package-quickstart-file)
+          '(custom-reevaluate-setting 'abbrev-file-name)
+          '(custom-reevaluate-setting 'custom-theme-directory)))
+
+  (define (load-custom?-sexps load-custom?)
+    ;; 'locate-user-emacs-file' also ensures that `user-emacs-directory'
+    ;; exists, creating it with the proper permissions if needed.
+    (list '(setq custom-file (locate-user-emacs-file "custom.el"))
+          (if load-custom?
+              '(if (not (file-exists-p custom-file))
+                   (make-empty-file custom-file)
+                   (load custom-file))
+              '(when (not (file-exists-p custom-file))
+                 (make-empty-file custom-file)))
+          (elisp (unelisp-newline))))
+
+  (define early-init-sexps
+    (cut emacs-configuration-early-init <>))
+
+  (define (default-init-sexps config)
+    (let ((<29? (emacs-version-<29? config-emacs)))
+      (match-record config <emacs-configuration>
+                    (variables modes keys keys-override)
+        (let ((result (append
+                       (map
+                        (match-lambda
+                          ((var . val)
+                           (if <29?
+                               `(setq ,var ,(elispifiable->elisp val))
+                               `(setopt ,var ,(elispifiable->elisp val)))))
+                        variables)
+                       (map
+                        (match-lambda
+                          ((mode . #t)
+                           `(,mode 1))
+                          ((mode . #f)
+                           `(,mode -1))
+                          ((mode . arg)
+                           `(,mode ,(elispifiable->elisp arg))))
+                        modes)
+                       (map
+                        (match-lambda
+                          (((? vector? k) . s)
+                           `(global-set-key ,k ',s))
+                          (((? string? k) . s)
+                           (if <29?
+                               `(global-set-key (kbd ,k)
+                                                ,(elispifiable->elisp s))
+                               `(keymap-global-set ,k
+                                                   ,(elispifiable->elisp s)))))
+                        keys)
+                       (map
+                        (match-lambda
+                          ((k . s)
+                           `(bind-key* ,k ,(elispifiable->elisp s))))
+                        keys-override))))
+          (if (null? result)
+              '()
+              (append result
+                      (list (elisp (unelisp-newline)))))))))
+
+  (define (default-init-extra-sexps config)
+    (match-record config <emacs-configuration>
+                  (extra-init)
+      (interpose extra-init
+                 (elisp (unelisp-newline))
+                 'suffix)))
+
+  (define (configured-packages-sexps configs)
+    (append-map (lambda (config)
+                  (append (apply config-package-serializer-procedure
+                                 (list config config-emacs))
+                          (list (elisp (unelisp-newline)))))
+                configs))
+
+  (define* (extra-init-files-sexps config #:optional subdirectory)
+    (let* ((directory (if subdirectory
+                          (file-name-concat %default-emacs-config-dir
+                                            "emacs"
+                                            subdirectory)
+                          (file-name-concat %default-emacs-config-dir
+                                            "emacs")))
+           (result (map
+                    (match-lambda
+                      ((name . _)
+                       `(load ,(file-name-concat directory name)
+                              #f #f #t)))
+                    (emacs-configuration-extra-init-files config))))
+      (if (null? result)
+          '()
+          (append result
+                  (list (elisp (unelisp-newline)))))))
+
+  (define* (extra-init-file->config-file-entry entry
+                                               #:optional subdirectory)
+    (match entry
+      ((name . files)
+       (list (emacs-config-filename (if subdirectory
+                                        (file-name-concat subdirectory name)
+                                        name))
+             (apply composite-file (basename name) (ensure-list files))))))
+
+  (define (server->config-file-entries server
+                                       propagated-init
+                                       inherit-extra-init-files
+                                       inherit-default-init
+                                       inherit-configured-packages)
+    (match-record server <emacs-server>
+                  (name
+                   inherit-init?
+                   inherit-directory?
+                   inherit-configured-packages?
+                   load-custom?
+                   configured-packages
+                   default-init)
+      (let* ((server-dir (server-name->file-name name))
+             (extra-init-files* (extra-init-files-sexps default-init
+                                                        server-dir))
+             (default-init* (if inherit-init?
+                                (extend-emacs-configuration
+                                 inherit-default-init
+                                 (list default-init))
+                                default-init))
+             (configured-packages* (if inherit-configured-packages?
+                                       (extend-emacs-package-list
+                                        inherit-configured-packages
+                                        (list configured-packages))
+                                       configured-packages)))
+        (append
+         (list
+          (list (emacs-config-filename
+                 (file-name-concat server-dir
+                                   %emacs-early-init-filename))
+                (elisp-file-with-forms %emacs-early-init-filename
+                                       (append
+                                        (set-user-emacs-directory-sexps
+                                         (server-user-directory
+                                          name
+                                          config-user-emacs-directory
+                                          inherit-directory?))
+                                        (early-init-sexps default-init))))
+          (list (emacs-config-filename
+                 (file-name-concat server-dir
+                                   %emacs-user-init-filename))
+                (elisp-file-with-forms %emacs-user-init-filename
+                                       (append
+                                        propagated-init
+                                        (load-custom?-sexps load-custom?)
+                                        (if inherit-init?
+                                            inherit-extra-init-files
+                                            '())
+                                        extra-init-files*
+                                        (default-init-sexps default-init*)
+                                        (configured-packages-sexps
+                                         configured-packages*)
+                                        (default-init-extra-sexps
+                                          default-init*)))))
+         (map (cut extra-init-file->config-file-entry <> server-dir)
+              (emacs-configuration-extra-init-files default-init))))))
+
+  (match-record config <home-emacs-configuration>
+                (user-emacs-directory
+                 load-custom?
+                 configured-packages
+                 propagated-init
+                 servers
+                 default-init)
+    (let ((propagated-init* (if (null? propagated-init)
+                                '()
+                                (append propagated-init
+                                        (list (elisp (unelisp-newline))))))
+          (extra-init-files* (extra-init-files-sexps default-init))
+          (default-init* (default-init-sexps default-init))
+          (configured-packages* (configured-packages-sexps
+                                 configured-packages))
+          (extra-init* (default-init-extra-sexps default-init)))
+      (append
+       (list (list (emacs-config-filename %emacs-early-init-filename)
+                   (elisp-file-with-forms %emacs-early-init-filename
+                                          (append
+                                           (set-user-emacs-directory-sexps
+                                            user-emacs-directory)
+                                           (early-init-sexps default-init))))
+             (list (emacs-config-filename %emacs-user-init-filename)
+                   (elisp-file-with-forms %emacs-user-init-filename
+                                          (append
+                                           propagated-init*
+                                           (load-custom?-sexps load-custom?)
+                                           extra-init-files*
+                                           default-init*
+                                           configured-packages*
+                                           extra-init*))))
+       (append-map (cut server->config-file-entries <>
+                        propagated-init*
+                        extra-init-files*
+                        default-init
+                        configured-packages)
+                   servers)
+       (map extra-init-file->config-file-entry
+            (emacs-configuration-extra-init-files default-init))))))
+
+(define (home-emacs-files config)
+  "Return from CONFIG an association list of filenames and file-like objects
+to create in the Emacs user directory."
+
+  (define file-name-with-home (make-regexp "^(~/|/home/[^/]+/)(.+)$"))
+
+  (define (file-name->home-file-name filename)
+    (or (and=> (regexp-exec file-name-with-home
+                            filename)
+               (cut match:substring <> 2))
+        filename))
+
+  (define (extra-file->home-files-entry entry directory)
+    (match entry
+      ((name . files)
+       (list (file-name-concat directory name)
+             (apply composite-file (basename name) (ensure-list files))))))
+
+  (define (package->home-files-entries package directory)
+    (match-record package <emacs-package>
+                  (extra-files)
+      (map (cut extra-file->home-files-entry <> directory)
+           extra-files)))
+
+  (define (server->home-files-entries server directory)
+    (match-record server <emacs-server>
+                  (name
+                   inherit-directory?
+                   inherit-init?
+                   inherit-configured-packages?
+                   default-init
+                   configured-packages)
+      (let ((server-dir (server-user-directory name
+                                               directory
+                                               inherit-directory?)))
+        (append (map (cut extra-file->home-files-entry <> server-dir)
+                     (append (emacs-configuration-extra-files default-init)
+                             (if (and inherit-init?
+                                      (not inherit-directory?))
+                                 (emacs-configuration-extra-files
+                                  (home-emacs-configuration-default-init
+                                   config))
+                                 '())))
+                (append-map (cut package->home-files-entries <> server-dir)
+                            (append
+                             configured-packages
+                             (if (and inherit-configured-packages?
+                                      (not inherit-directory?))
+                                 (home-emacs-configuration-configured-packages
+                                  config)
+                                 '())))))))
+
+  (match-record config <home-emacs-configuration>
+                (user-emacs-directory
+                 servers
+                 default-init
+                 configured-packages)
+    (let ((user-emacs-directory* (file-name->home-file-name
+                                  user-emacs-directory)))
+      (append
+       (map (cut extra-file->home-files-entry <> user-emacs-directory*)
+            (emacs-configuration-extra-files default-init))
+       (append-map (cut package->home-files-entries <> user-emacs-directory*)
+                   configured-packages)
+       (append-map (cut server->home-files-entries <> user-emacs-directory*)
+                   servers)))))
+
+(define (home-emacs-extensions original-config extension-configs)
+  "Extend the Emacs home service configuration ORIGINAL-CONFIG with list of
+configurations EXTENSION-CONFIGS."
+
+  (define (extend-configured-packages-field original extensions)
+    (extend-emacs-package-list original extensions))
+
+  (define (extend-servers-field original extensions)
+    ;; Extend `emacs-servers', signaling an error if any two servers have the
+    ;; same name.
+    (fold-right (lambda (elem ret)
+                  (if (find (lambda (e)
+                              (equal? (record-value e 'name)
+                                      (record-value elem 'name)))
+                            ret)
+                      (configuration-field-error #f 'name elem)
+                      (cons elem ret)))
+                '()
+                (apply append original extensions)))
+
+  (extend-record
+   home-emacs-configuration
+   original-config extension-configs
+   (extend-list-merge extra-packages extra-packages)
+   (extend-list-merge indent-forms indent-forms)
+   (extend-servers-field servers servers)
+   (extend-emacs-configuration default-init default-init)
+   (extend-configured-packages-field configured-packages configured-packages)))
+
+(define home-emacs-service-type
+  (service-type (name 'home-emacs-service)
+                (extensions
+                 (list (service-extension
+                        home-profile-service-type
+                        home-emacs-packages)
+                       (service-extension
+                        home-shepherd-service-type
+                        home-emacs-shepherd-services)
+                       (service-extension
+                        home-xdg-configuration-files-service-type
+                        home-emacs-xdg-configuration-files)
+                       (service-extension
+                        home-files-service-type
+                        home-emacs-files)))
+                (default-value (home-emacs-configuration))
+                (compose identity)
+                (extend home-emacs-extensions)
+                (description
+                 "Configure and run the GNU Emacs extensible text editor.")))
+
+
+;;;
+;;; Utility functions.
+;;;
+
+(define (schemified-elisp->home-emacs-configuration lst)
+  "Convert LST, a list of s-expressions, into a `home-emacs-configuration'
+record."
+
+  (define elisp->scheme
+    (match-lambda
+      ('t
+       #t)
+      ('nil
+       #f)
+      ((? constant? obj)
+       obj)
+      (('quote obj)
+       obj)
+      (obj
+       (elisp (unelisp obj)))))
+
+  (define (variable-specs->alist specs)
+    (let lp ((specs specs)
+             (acc '()))
+      (match specs
+        (()
+         (reverse! acc))
+        (((? blank?) . rest)
+         (lp rest acc))
+        ((var val . rest)
+         (lp rest (cons (cons var (elisp->scheme val))
+                        acc)))
+        (_
+         (raise (formatted-message (G_ "invalid `setq'/`setopt' in file")))))))
+
+  (define mode-toggle-function?
+    (match-lambda
+      ((? symbol? obj)
+       (string-suffix? "-mode" (symbol->string obj)))
+      (_ #f)))
+
+  (define (use-package->emacs-package name body)
+
+    (define elisp-keyword?
+      (match-lambda
+        ((? symbol? obj)
+         (string-prefix? ":" (symbol->string obj)))
+        (_ #f)))
+
+    (define dotted-pair?
+      (match-lambda
+        ((head . (not (? pair?)))
+         #t)
+        (_ #f)))
+
+    (define list-of-dotted-pairs?
+      (list-of dotted-pair?))
+
+    (let lp ((lst body)
+             (package (emacs-package (name name))))
+      (match lst
+        (()
+         package)
+        (((? elisp-keyword? kw) . rest)
+         (receive (args rest)
+             (break elisp-keyword? rest)
+           (match kw
+             (':demand
+              (lp rest
+                  (emacs-package
+                   (inherit package)
+                   (load-force? (match (remove blank? args)
+                                  ('nil #f)
+                                  (_ #t))))))
+             ((or ':if ':when)
+              (lp rest
+                  (emacs-package
+                   (inherit package)
+                   (load-predicates (append (emacs-package-load-predicates
+                                             package)
+                                            (match (remove blank? args)
+                                              ((exp)
+                                               (list (elisp (unelisp exp))))
+                                              (_ '())))))))
+             (':unless
+              (lp rest
+                  (emacs-package
+                   (inherit package)
+                   (load-predicates (append (emacs-package-load-predicates
+                                             package)
+                                            (match (remove blank? args)
+                                              ((exp)
+                                               (list (elisp (not
+                                                             (unelisp exp)))))
+                                              (_ '())))))))
+             (':after
+              (lp rest
+                  (emacs-package
+                   (inherit package)
+                   (load-after-packages (append
+                                         (emacs-package-load-after-packages
+                                          package)
+                                         (match (remove blank? args)
+                                           (((':all
+                                              . (? list-of-symbols? lst)))
+                                            lst)
+                                           (((':any . rest))
+                                            ;; Ignore, because we can't
+                                            ;; guarantee equivalent behavior.
+                                            '())
+                                           ((? list-of-symbols? lst)
+                                            args)
+                                           (((? list-of-symbols? lst))
+                                            lst)
+                                           (_ '())))))))
+             (':load-path
+              (lp rest
+                  (emacs-package
+                   (inherit package)
+                   (load-paths (append (emacs-package-load-paths package)
+                                       (filter string?
+                                               (match args
+                                                 (((? list? lst))
+                                                  lst)
+                                                 ((? list? lst)
+                                                  lst)
+                                                 (_ '()))))))))
+             (':autoload
+              (lp rest
+                  (emacs-package
+                   (inherit package)
+                   (autoloads (append (emacs-package-autoloads package)
+                                      (match (remove blank? args)
+                                        ((? list-of-symbols? lst)
+                                         lst)
+                                        (((? list-of-symbols? lst))
+                                         lst)
+                                        (_ '())))))))
+             (':commands
+              (lp rest
+                  (emacs-package
+                   (inherit package)
+                   (autoloads-interactive (append
+                                           (emacs-package-autoloads-interactive
+                                            package)
+                                           (match (remove blank? args)
+                                             ((? list-of-symbols? lst)
+                                              lst)
+                                             (((? list-of-symbols? lst))
+                                              lst)
+                                             (_ '())))))))
+             (':bind*
+              (lp rest
+                  (emacs-package
+                   (inherit package)
+                   (keys-override (append (emacs-package-keys-override package)
+                                          (filter dotted-pair?
+                                                  (match args
+                                                    (((? list? lst))
+                                                     lst)
+                                                    (_ args))))))))
+             (':bind
+              (receive (global local)
+                  (break elisp-keyword? (match args
+                                          (((? list? lst))
+                                           lst)
+                                          (_ args)))
+                (lp rest
+                    (emacs-package
+                     (inherit package)
+                     (keys-global (append (emacs-package-keys-global package)
+                                          (filter dotted-pair? global)))
+                     (keys-local
+                      (append
+                       (emacs-package-keys-local package)
+                       (let lp/inner ((lst (remove blank? local))
+                                      (keymaps '()))
+                         (match lst
+                           ((':map (? symbol? kmap) . rest)
+                            (receive (kspecs rest)
+                                (break (cut memq <> '(:map :repeat-map))
+                                       rest)
+                              (lp/inner rest
+                                        (append
+                                         keymaps
+                                         (list (emacs-keymap
+                                                (name kmap)
+                                                (keys (filter dotted-pair?
+                                                              kspecs))))))))
+                           ((':repeat-map (? symbol? kmap) . rest)
+                            (receive (kspecs rest)
+                                (break (cut memq <> '(:map :repeat-map))
+                                       rest)
+                              (lp/inner rest
+                                        (append
+                                         keymaps
+                                         (list
+                                          (emacs-keymap
+                                           (name kmap)
+                                           (repeat? #t)
+                                           (repeat-exit
+                                            (filter-map
+                                             (match-lambda
+                                               (((? string-or-vector?)
+                                                 . (? symbol? sym))
+                                                sym)
+                                               (_ #f))
+                                             (take-while
+                                              (negate (cut eq? <>
+                                                           ':continue))
+                                              (drop-while
+                                               (negate (cut eq? <>
+                                                            ':exit))
+                                               kspecs))))
+                                           (keys (filter dotted-pair?
+                                                         kspecs))))))))
+                           (_ keymaps)))))))))
+             (':bind-keymap
+              (lp rest
+                  (emacs-package
+                   (inherit package)
+                   (keys-global-keymaps (append
+                                         (emacs-package-keys-global-keymaps
+                                          package)
+                                         (filter dotted-pair?
+                                                 (match args
+                                                   (((? list? lst))
+                                                    lst)
+                                                   (_ args))))))))
+             (':custom
+              (lp rest
+                  (emacs-package
+                   (inherit package)
+                   (options (append (emacs-package-options package)
+                                    (filter-map
+                                     (match-lambda
+                                       ((var val . rest)
+                                        `(,var . ,(elisp->scheme val)))
+                                       (_ #f))
+                                     (match args
+                                       (((and ((? list?) . rest) lst))
+                                        ;; :custom ((foo bar))
+                                        lst)
+                                       (_ args))))))))
+             (':custom-face
+              (lp rest
+                  (emacs-package
+                   (inherit package)
+                   (faces (append (emacs-package-faces package)
+                                  (filter-map (match-lambda
+                                                (((? symbol? face)
+                                                  ((? pair? spec) ..1))
+                                                 `(,face . ,spec))
+                                                (_ #f))
+                                              args))))))
+             (':hook
+              (lp rest
+                  (emacs-package
+                   (inherit package)
+                   (hooks (append (emacs-package-hooks package)
+                                  (match args
+                                    ((((? list-of-symbols? hooks)
+                                       . (? symbol? func)))
+                                     (map (cut cons <> func)
+                                          hooks))
+                                    (((? list-of-dotted-pairs? lst))
+                                     (filter-map (match-lambda
+                                                   (((? symbol? hook)
+                                                     . (? symbol? func))
+                                                    (cons hook func))
+                                                   (_ #f))
+                                                 lst))
+                                    ((or (? list-of-symbols? hooks)
+                                         ((? list-of-symbols? hooks)))
+                                     (map
+                                      (cute cons <>
+                                            (symbol-append name '-mode))
+                                      hooks))
+                                    (_ '())))))))
+             (':mode
+              (lp rest
+                  (emacs-package
+                   (inherit package)
+                   (auto-modes (append (emacs-package-auto-modes package)
+                                       (match args
+                                         ((or ((? string? strings) ..1)
+                                              (((? string? strings) ..1)))
+                                          (map (cut cons <> name)
+                                               strings))
+                                         ((or ((? list-of-dotted-pairs? lst))
+                                              (? list-of-dotted-pairs? lst))
+                                          (filter
+                                           (match-lambda
+                                             (((? string?) . (? symbol?))
+                                              #t)
+                                             (_ #f))
+                                           lst))
+                                         (_ '())))))))
+             (':magic
+              (lp rest
+                  (emacs-package
+                   (inherit package)
+                   (magic-modes (append (emacs-package-magic-modes package)
+                                        (match args
+                                          ((or ((? string? strings) ..1)
+                                               (((? string? strings) ..1)))
+                                           (map (cut cons <> name)
+                                                strings))
+                                          ((or ((? list-of-dotted-pairs? lst))
+                                               (? list-of-dotted-pairs? lst))
+                                           (filter
+                                            (match-lambda
+                                              (((? string?) . (? symbol?))
+                                               #t)
+                                              (_ #f))
+                                            lst))
+                                          (_ '())))))))
+             (':config
+              (lp rest
+                  (emacs-package
+                   (inherit package)
+                   (extra-after-load (append (emacs-package-extra-after-load
+                                              package)
+                                             (map sexp->elisp
+                                                  args))))))
+             (':init
+              (lp rest
+                  (emacs-package
+                   (inherit package)
+                   (extra-init (append (emacs-package-extra-init package)
+                                       (map sexp->elisp
+                                            args))))))
+             (kw
+              (lp rest
+                  (emacs-package
+                   (inherit package)
+                   (extra-keywords (append (emacs-package-extra-keywords
+                                            package)
+                                           (list
+                                            `(,kw . ,(map sexp->elisp
+                                                          args)))))))))))
+        (((? blank?) . rest)
+         (lp rest package))
+        (_ (raise (formatted-message
+                   (G_ "invalid `use-package' form in file")))))))
+
+  (let loop ((lst lst)
+             (init (emacs-configuration))
+             (packages '()))
+    (match lst
+      (()
+       (home-emacs-configuration
+        (configured-packages packages)
+        (default-init init)))
+      ((((or 'setq 'setopt) . specs) . rest)
+       (loop rest
+             (emacs-configuration
+              (inherit init)
+              (variables (append (emacs-configuration-variables init)
+                                 (variable-specs->alist specs))))
+             packages))
+      ((((? mode-toggle-function? mode) . arg) . rest)
+       (loop rest
+             (emacs-configuration
+              (inherit init)
+              (modes (append (emacs-configuration-modes init)
+                             (list (cons mode
+                                         (match arg
+                                           ((1)
+                                            #t)
+                                           (()
+                                            #t)
+                                           ((-1)
+                                            #f)
+                                           (((? blank?))
+                                            #t)
+                                           ((obj)
+                                            (elisp->scheme obj))))))))
+             packages))
+      ((`(bind-key* ,(? string-or-vector? key)
+                    (,(or 'quote 'function) ,(? symbol? def)) . ,_)
+        . rest)
+       (loop rest
+             (emacs-configuration
+              (inherit init)
+              (keys-override (append (emacs-configuration-keys-override init)
+                                     (list (cons key def)))))
+             packages))
+      (((or `(global-set-key ,(? vector? key)
+                             (,(or 'quote 'function) ,(? symbol? def))
+                             . ,_)
+            `(global-set-key (kbd ,(? string? key))
+                             (,(or 'quote 'function) ,(? symbol? def))
+                             . ,_)
+            `(keymap-global-set ,(? string? key)
+                                (,(or 'quote 'function) ,(? symbol? def)))
+            `(bind-key ,(? string-or-vector? key)
+                       (,(or 'quote 'function) ,(? symbol? def)))
+            `(bind-key ,(? string-or-vector? key)
+                       (,(or 'quote 'function) ,(? symbol? def))
+                       ,(or `(quote global-map)
+                            'global-map)
+                       . ,_)) . rest)
+       (loop rest
+             (emacs-configuration
+              (inherit init)
+              (keys (append (emacs-configuration-keys init)
+                            (list (cons key def)))))
+             packages))
+      ((`(use-package ,(? symbol? package) . ,body) . rest)
+       (loop rest
+             init
+             (append packages
+                     (list (use-package->emacs-package package body)))))
+      (((? blank?) . rest)
+       (loop rest
+             init
+             packages))
+      ((exp . rest)
+       (loop rest
+             (emacs-configuration
+              (inherit init)
+              (extra-init (append (emacs-configuration-extra-init init)
+                                  (list (elisp (unelisp exp))))))
+             packages)))))
+
+(define (home-emacs-configuration->code config)
+  "Return a Scheme s-expression creating a `home-emacs-configuration' record
+equivalent to CONFIG."
+
+  (define-syntax unless-null
+    (syntax-rules ()
+      ((_ var exp)
+       (if (null? var)
+           '()
+           (list (list 'var exp))))))
+
+  (define (elisp->code exp)
+    ;; Simple serialization for Elisp expressions containing no G-expressions
+    ;; or file-likes.
+    `(elisp ,(fold-right/elisp (lambda (t s)
+                                 (match t
+                                   ((? vertical-space?)
+                                    '(unelisp-newline))
+                                   ((? page-break?)
+                                    '(unelisp-page-break))
+                                   ((? comment?)
+                                    `(unelisp-comment
+                                      ,(comment->string t)))
+                                   (_ t)))
+                               (lambda (t s)
+                                 (if (not t)
+                                     (list->dotted-list s)
+                                     s))
+                               cons
+                               '()
+                               exp)))
+
+  (define (alist->code lst)
+    (list (if (any (match-lambda
+                     ((var . (? elisp? val))
+                      #t)
+                     (_ #f))
+                   lst)
+              'quasiquote
+              'quote)
+          (map (match-lambda
+                 ((var . (? elisp? val))
+                  ;; Works because `quasiquote' expands `unquote' forms like
+                  ;; `(a . ,C) correctly into (a . C), and
+                  ;; `pretty-print-with-comments' prints them nicely.
+                  (cons var (list 'unquote
+                                  (elisp->code val))))
+                 ((var . val)
+                  (cons var val)))
+               lst)))
+
+  (define (emacs-configuration->code config)
+    (match-record config <emacs-configuration>
+                  (early-init
+                   extra-init-files
+                   extra-files
+                   variables
+                   modes
+                   keys
+                   keys-override
+                   extra-init)
+      (let ((body `(,@(unless-null early-init
+                                   `(list ,@(map elisp->code
+                                                 early-init)))
+                    ,@(unless-null extra-init-files
+                                   `(quote ,extra-init-files))
+                    ,@(unless-null extra-files
+                                   `(quote ,extra-files))
+                    ,@(unless-null variables
+                                   (alist->code variables))
+                    ,@(unless-null modes
+                                   (alist->code modes))
+                    ,@(unless-null keys
+                                   `(quote ,keys))
+                    ,@(unless-null keys-override
+                                   `(quote ,keys-override))
+                    ,@(unless-null extra-init
+                                   `(list ,@(map elisp->code
+                                                 extra-init))))))
+        (if (null? body)
+            body
+            `(emacs-configuration
+              ,@body)))))
+
+  (define (emacs-keymap->code config)
+    (match-record config <emacs-keymap>
+                  (name
+                   repeat?
+                   repeat-exit
+                   repeat-enter
+                   disabled-commands
+                   keys)
+      `(emacs-keymap
+        (name (quote ,name))
+        ,@(if (not repeat?)
+              '()
+              (list `(repeat? ,repeat?)))
+        ,@(unless-null repeat-exit
+                       `(quote ,repeat-exit))
+        ,@(unless-null repeat-enter
+                       `(quote ,repeat-enter))
+        ,@(unless-null disabled-commands
+                       `(quote ,disabled-commands))
+        ,@(unless-null keys
+                       `(quote ,keys)))))
+
+  (define (emacs-package->code config)
+    (match-record config <emacs-package>
+                  (name
+                   load-force?
+                   load-predicates
+                   load-after-packages
+                   load-paths
+                   autoloads
+                   autoloads-interactive
+                   keys-global
+                   keys-global-keymaps
+                   keys-override
+                   keys-local
+                   options
+                   faces
+                   hooks
+                   auto-modes
+                   magic-modes
+                   extra-after-load
+                   extra-init
+                   extra-keywords)
+      `(emacs-package
+        (name (quote ,name))
+        ,@(if (not load-force?)
+              '()
+              (list `(load-force? ,load-force?)))
+        ,@(unless-null load-predicates
+                       `(list ,@(map elisp->code
+                                     load-predicates)))
+        ,@(unless-null load-after-packages
+                       `(quote ,load-after-packages))
+        ,@(unless-null load-paths
+                       `(quote ,(filter string?
+                                        load-paths)))
+        ,@(unless-null autoloads
+                       `(quote ,autoloads))
+        ,@(unless-null autoloads-interactive
+                       `(quote ,autoloads-interactive))
+        ,@(unless-null keys-global
+                       `(quote ,keys-global))
+        ,@(unless-null keys-global-keymaps
+                       `(quote ,keys-global-keymaps))
+        ,@(unless-null keys-override
+                       `(quote ,keys-override))
+        ,@(unless-null keys-local
+                       `(list ,@(map emacs-keymap->code
+                                     keys-local)))
+        ,@(unless-null options
+                       (alist->code options))
+        ,@(unless-null faces
+                       `(quote ,faces))
+        ,@(unless-null hooks
+                       `(quote ,hooks))
+        ,@(unless-null auto-modes
+                       `(quote ,auto-modes))
+        ,@(unless-null magic-modes
+                       `(quote ,magic-modes))
+        ,@(unless-null extra-after-load
+                       `(list ,@(map elisp->code
+                                     extra-after-load)))
+        ,@(unless-null extra-init
+                       `(list ,@(map elisp->code
+                                     extra-init)))
+        ,@(unless-null extra-keywords
+                       (list 'quasiquote
+                             (map (match-lambda
+                                    ((head . tail)
+                                     `(,head
+                                       ,@(map (lambda (e)
+                                                (list 'unquote
+                                                      (elisp->code e)))
+                                              tail))))
+                                  extra-keywords))))))
+
+  (match-record config <home-emacs-configuration>
+                (default-init configured-packages)
+    (let ((default-init* (emacs-configuration->code default-init)))
+      `(home-emacs-configuration
+        ,@(if (null? default-init*)
+              '()
+              (list `(default-init
+                       ,default-init*)))
+        (configured-packages
+         ,(if (null? configured-packages)
+              '(quote ())
+              `(list ,@(map emacs-package->code
+                            configured-packages))))))))
+
+(define (input->home-emacs-configuration port)
+  "Return a `home-emacs-configuration' record from Elisp read from PORT."
+  (schemified-elisp->home-emacs-configuration
+    (read-with-comments/sequence port
+                                 #:elisp? #t)))
+
+(define (elisp-file->home-emacs-configuration port file)
+  "Write to PORT a Scheme snippet creating a `home-emacs-configuration' from
+the Elisp file named FILE."
+  (pretty-print-with-comments port
+                              (home-emacs-configuration->code
+                               (call-with-input-file file
+                                 input->home-emacs-configuration))
+                              #:special-forms '((emacs-configuration . 0)
+                                                (emacs-package . 0)
+                                                (emacs-keymap . 0)
+                                                (default-init . 0)
+                                                (configured-packages . 0)
+                                                (extra-after-load . 0)
+                                                (extra-init . 0)
+                                                (extra-keywords . 0))))
+
+
+;;;
+;;; Elisp reader extension.
+;;;
+
+(eval-when (expand load eval)
+
+  (define (read-elisp-extended port)
+    (read-with-comments port
+                        #:blank-line? #f
+                        #:elisp? #t
+                        #:unelisp-extensions? #t))
+
+  (define (read-elisp-expression chr port)
+    `(elisp ,(read-elisp-extended port)))
+
+  (read-hash-extend #\% read-elisp-expression))
+
+;;; emacs.scm ends here
diff --git a/gnu/local.mk b/gnu/local.mk
index 06a376a99a..e8c976327a 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -59,6 +59,7 @@
 # Copyright © 2023 Zheng Junjie <873216071@HIDDEN>
 # Copyright © 2023 Ivana Drazovic <iv.dra@HIDDEN>
 # Copyright © 2023 Andy Tai <atai@HIDDEN>
+# Copyright © 2023 Kierin Bell <fernseed@HIDDEN>
 #
 # This file is part of GNU Guix.
 #
@@ -91,6 +92,7 @@ GNU_SYSTEM_MODULES =				\
   %D%/home.scm					\
   %D%/home/services.scm			\
   %D%/home/services/desktop.scm			\
+  %D%/home/services/emacs.scm			\
   %D%/home/services/symlink-manager.scm		\
   %D%/home/services/fontutils.scm		\
   %D%/home/services/gnupg.scm			\
diff --git a/guix/read-print.scm b/guix/read-print.scm
index 25be289d60..1749179338 100644
--- a/guix/read-print.scm
+++ b/guix/read-print.scm
@@ -1,5 +1,6 @@
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2021-2023 Ludovic Courtès <ludo@HIDDEN>
+;;; Copyright © 2023 Kierin Bell <fernseed@HIDDEN>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -21,6 +22,7 @@ (define-module (guix read-print)
   #:use-module (ice-9 match)
   #:use-module (ice-9 rdelim)
   #:use-module (ice-9 vlist)
+  #:use-module (ice-9 format)
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-26)
   #:use-module (srfi srfi-34)
@@ -30,6 +32,7 @@ (define-module (guix read-print)
                 #:select (formatted-message
                           &fix-hint &error-location
                           location))
+  #:use-module (language elisp parser)
   #:export (pretty-print-with-comments
             pretty-print-with-comments/splice
             read-with-comments
@@ -150,9 +153,17 @@ (define (read-until-end-of-line port)
       ((? space?) (loop))
       (chr (unread-char chr port)))))
 
-(define* (read-with-comments port #:key (blank-line? #t))
+(define* (read-with-comments port
+                             #:key
+                             (blank-line? #t)
+                             (elisp? #f)
+                             (unelisp-extensions? #f))
   "Like 'read', but include <blank> objects when they're encountered.  When
-BLANK-LINE? is true, assume PORT is at the beginning of a new line."
+BLANK-LINE? is true, assume PORT is at the beginning of a new line.
+
+When ELISP? is true, read Elisp, and when UNELISP-EXTENSIONS? is true, convert
+<blank> objects into lists suitable for use with the `elisp' macro in the `(gnu
+home services emacs)' module."
   ;; Note: Instead of implementing this functionality in 'read' proper, which
   ;; is the best approach long-term, this code is a layer on top of 'read',
   ;; such that we don't have to rely on a specific Guile version.
@@ -172,18 +183,38 @@ (define* (read-with-comments port #:key (blank-line? #t))
              (&fix-hint
               (hint (G_ "Did you forget a closing parenthesis?")))))))
 
-  (define (reverse/dot lst)
+  (define (invalid-array-error)
+    (raise (make-compound-condition
+            (formatted-message (G_ "invalid array syntax"))
+            (condition
+             (&error-location
+              (location (match (port-filename port)
+                          (#f #f)
+                          (file (location file
+                                          (port-line port)
+                                          (port-column port))))))
+             (&fix-hint
+              (hint (G_ "Did you mean to write a dotted list?")))))))
+
+  (define (reverse/dot lst array?)
     ;; Reverse LST and make it an improper list if it contains DOT.
     (let loop ((result '())
                (lst lst))
       (match lst
-        (() result)
+        (()
+         (if array?
+             (list->array 1 result)
+             result))
         (((? dot?) . rest)
-         (if (pair? rest)
-             (let ((dotted (reverse rest)))
-               (set-cdr! (last-pair dotted) (car result))
-               dotted)
-             (car result)))
+         (if array?
+             (invalid-array-error)
+             (if (pair? rest)
+                 (let ((dotted (reverse rest)))
+                   (set-cdr! (last-pair dotted) (car result))
+                   dotted)
+                 (car result))))
+        ((('%set-lexical-binding-mode . _) . rest)
+         (loop result rest))
         ((x . rest) (loop (cons x result) rest)))))
 
   (let loop ((blank-line? blank-line?)
@@ -194,27 +225,29 @@ (define* (read-with-comments port #:key (blank-line? #t))
       (chr
        (cond ((eqv? chr #\newline)
               (if blank-line?
-                  (read-vertical-space port)
+                  (unless (and elisp? unelisp-extensions?)
+                    (read-vertical-space port))
                   (loop #t return)))
              ((eqv? chr #\page)
               ;; Assume that a page break is on a line of its own and read
               ;; subsequent white space and newline.
               (read-until-end-of-line port)
-              (page-break))
+              (unless (and elisp? unelisp-extensions?) (page-break)))
              ((char-set-contains? char-set:whitespace chr)
               (loop blank-line? return))
              ((memv chr '(#\( #\[))
               (let/ec return
-                (let liip ((lst '()))
+                (let liip ((lst '())
+                           (arr? (and elisp? (eqv? chr #\[))))
                   (define item
                     (loop (match lst
                             (((? blank?) . _) #t)
                             (_ #f))
                           (lambda ()
-                            (return (reverse/dot lst)))))
+                            (return (reverse/dot lst arr?)))))
                   (if (eof-object? item)
                       (missing-closing-paren-error)
-                      (liip (cons item lst))))))
+                      (liip (cons item lst) arr?)))))
              ((memv chr '(#\) #\]))
               (return))
              ((eq? chr #\')
@@ -222,26 +255,62 @@ (define* (read-with-comments port #:key (blank-line? #t))
              ((eq? chr #\`)
               (list 'quasiquote (loop #f return)))
              ((eq? chr #\#)
-              (match (read-char port)
-                (#\~ (list 'gexp (loop #f return)))
-                (#\$ (list (match (peek-char port)
-                             (#\@
-                              (read-char port)    ;consume
-                              'ungexp-splicing)
-                             (_
-                              'ungexp))
-                           (loop #f return)))
-                (#\+ (list (match (peek-char port)
-                             (#\@
-                              (read-char port)    ;consume
-                              'ungexp-native-splicing)
-                             (_
-                              'ungexp-native))
-                           (loop #f return)))
-                (chr
-                 (unread-char chr port)
-                 (unread-char #\# port)
-                 (read port))))
+              (cond
+               ((and elisp? unelisp-extensions?)
+                ;; Return list for `elisp' macro in `(gnu home services emacs)'
+                (match (read-char port)
+                  (#\$
+                   (match (read-char port)
+                     (#\@
+                      (list 'unelisp-splicing (read port)))
+                     (chr
+                      (unread-char chr port)
+                      (list 'unelisp (read port)))))
+                  (#\;
+                   (unread-char #\; port)
+                   (list 'unelisp-comment (read-line port 'concat)))
+                  (#\>
+                   (list 'unelisp-newline))
+                  (#\^
+                   (match (read-char port)
+                     (#\L
+                      (list 'unelisp-page-break))
+                     (chr
+                      (unread-char chr port)
+                      (unread-char #\^ port)
+                      (unread-char #\# port)
+                      (read-elisp port))))
+                  (chr
+                   (unread-char chr port)
+                   (unread-char #\# port)
+                   (read-elisp port))))
+               (elisp?
+                ;; Read normal Elisp
+                (unread-char #\# port)
+                (read-elisp port))
+               (else
+                ;; Read Scheme
+                (match (read-char port)
+                  (#\~
+                   (list 'gexp (loop #f return)))
+                  (#\$ (list (match (peek-char port)
+                               (#\@
+                                (read-char port) ;consume
+                                'ungexp-splicing)
+                               (_
+                                'ungexp))
+                             (loop #f return)))
+                  (#\+ (list (match (peek-char port)
+                               (#\@
+                                (read-char port) ;consume
+                                'ungexp-native-splicing)
+                               (_
+                                'ungexp-native))
+                             (loop #f return)))
+                  (chr
+                   (unread-char chr port)
+                   (unread-char #\# port)
+                   (read port))))))
              ((eq? chr #\,)
               (list (match (peek-char port)
                       (#\@
@@ -251,22 +320,62 @@ (define* (read-with-comments port #:key (blank-line? #t))
                        'unquote))
                     (loop #f return)))
              ((eqv? chr #\;)
-              (unread-char chr port)
-              (string->comment (read-line port 'concat)
-                               (not blank-line?)))
+              (if (and elisp? unelisp-extensions?)
+                  (begin
+                    (read-line port 'concat) ;consume
+                    (loop blank-line? return))
+                  (begin
+                    (unread-char chr port)
+                    (string->comment (read-line port 'concat)
+                                     (not blank-line?)))))
+             ((eqv? chr #\?)
+              (if elisp?
+                  (begin
+                    ;; Elisp character; improve upon `read-elisp' by returning
+                    ;; Scheme characters instead of integers.
+
+                    ;; Character read syntax support by `read-elisp':
+
+                    ;; ?X (supported), ?\uXXXX (supported), ?\uXXXXXXXX
+                    ;; (supported), ?\X (supported), ?\XXX (octal, supported),
+                    ;; ?\N{NAME} (returns same as ?\N), ?\N{U+X} (returns same
+                    ;; as ?\N), or \xXX (unsupported, signals error)
+
+                    ;; `integer->char' will signal error if integer is not in
+                    ;; range 0-#xD7FF or #xE000-#x10FFFF.
+                    (unread-char #\? port)
+                    (integer->char (read-elisp port)))
+                  (begin
+                    ;; Scheme symbol
+                    (unread-char #\? port)
+                    (read port))))
              (else
-              (unread-char chr port)
-              (match (read port)
-                ((and token '#{.}#)
-                 (if (eq? chr #\.) dot token))
-                (token token))))))))
-
-(define (read-with-comments/sequence port)
+              (cond
+               ;; Unlike for Scheme `read', `.' is an invalid read syntax for
+               ;; `read-elisp'.
+               ((and elisp?
+                     (eqv? chr #\.)
+                     (char-set-contains? char-set:whitespace ;redundant
+                                         (peek-char port)))
+                dot)
+               (elisp?
+                (unread-char chr port)
+                (read-elisp port))
+               (else
+                (unread-char chr port)
+                (match (read port)
+                  ((and token '#{.}#)
+                   (if (eq? chr #\.) dot token))
+                  (token token))))))))))
+
+(define* (read-with-comments/sequence port #:key elisp?)
   "Read from PORT until the end-of-file is reached and return the list of
 expressions and blanks that were read."
   (let loop ((lst '())
              (blank-line? #t))
-    (match (read-with-comments port #:blank-line? blank-line?)
+    (match (read-with-comments port
+                               #:blank-line? blank-line?
+                               #:elisp? elisp?)
       ((? eof-object?)
        (reverse! lst))
       ((? blank? blank)
@@ -371,8 +480,220 @@ (define %newline-forms
    ('set-xorg-configuration '())
    ('services '(home-environment))
    ('home-bash-configuration '(service))
+   ('home-emacs-configuration '())
    ('introduction '(channel))))
 
+(define %elisp-special-forms
+  ;; Forms that should be indented specially in Elisp, adapted from the
+  ;; `lisp-indent-function' property for each symbol by adding 1 to each
+  ;; integer value and substituting 3 for `defun', for compatibility with
+  ;; `%special-forms'.  This is a non-exhaustive list, generated by mapping
+  ;; over the obarray of a minimal Emacs environment, and then removing
+  ;; symbols that are obsolete or unlikely to ever appear in an Emacs package
+  ;; or configuration file.
+  (vhashq
+   ('and-let* 2)
+   ('atomic-change-group 1)
+   ('autoload 3)
+   ('benchmark-progn 1)
+   ('benchmark-run 2)
+   ('benchmark-run-compiled 2)
+   ('byte-compile-maybe-guarded 2)
+   ('catch 2)
+   ('cl-block 2)
+   ('cl-callf 3)
+   ('cl-callf2 4)
+   ('cl-case 2)
+   ('cl-defgeneric 3)
+   ('cl-define-compiler-macro 3)
+   ('cl-defmacro 3)
+   ('cl-defmethod 4)
+   ('cl-defstruct 2)
+   ('cl-defsubst 3)
+   ('cl-deftype 3)
+   ('cl-defun 3)
+   ('cl-destructuring-bind 3)
+   ('cl-do 3)
+   ('cl-do* 3)
+   ('cl-do-all-symbols 2)
+   ('cl-do-symbols 2)
+   ('cl-dolist 2)
+   ('cl-dotimes 2)
+   ('cl-ecase 2)
+   ('cl-etypecase 2)
+   ('cl-eval-when 2)
+   ('cl-flet 2)
+   ('cl-flet* 2)
+   ('cl-generic-define-context-rewriter 4)
+   ('cl-generic-define-generalizer 2)
+   ('cl-iter-defun 3)
+   ('cl-labels 2)
+   ('cl-letf 2)
+   ('cl-letf* 2)
+   ('cl-macrolet 2)
+   ('cl-multiple-value-bind 3)
+   ('cl-multiple-value-setq 2)
+   ('cl-once-only 2)
+   ('cl-progv 3)
+   ('cl-return-from 2)
+   ('cl-symbol-macrolet 2)
+   ('cl-the 2)
+   ('cl-typecase 2)
+   ('cl-with-gensyms 2)
+   ('combine-after-change-calls 1)
+   ('combine-change-calls 3)
+   ('condition-case 3)
+   ('condition-case-unless-debug 3)
+   ('def-edebug-elem-spec 2)
+   ('def-edebug-spec 2)
+   ('defadvice 3)
+   ('defalias 3)
+   ('defclass 3)
+   ('defconst 3)
+   ('defcustom 3)
+   ('defface 3)
+   ('defgroup 3)
+   ('defimage 3)
+   ('define-abbrev 3)
+   ('define-abbrev-table 3)
+   ('define-advice 3)
+   ('define-alternatives 3)
+   ('define-auto-insert 3)
+   ('define-button-type 3)
+   ('define-category 3)
+   ('define-char-code-property 3)
+   ('define-derived-mode 3)
+   ('define-fringe-bitmap 3)
+   ('define-generic-mode 2)
+   ('define-globalized-minor-mode 3)
+   ('define-inline 3)
+   ('define-keymap 3)
+   ('define-mail-user-agent 3)
+   ('define-minor-mode 3)
+   ('define-multisession-variable 3)
+   ('define-obsolete-function-alias 3)
+   ('define-obsolete-variable-alias 3)
+   ('define-short-documentation-group 3)
+   ('define-skeleton 3)
+   ('define-widget 3)
+   ('define-widget-keywords 3)
+   ('defmacro 3)
+   ('defmath 3)
+   ('defsubst 3)
+   ('deftheme 2)
+   ('defun 3)
+   ('defvar 3)
+   ('defvar-keymap 2)
+   ('defvar-local 3)
+   ('defvaralias 3)
+   ('delay-mode-hooks 1)
+   ('dlet 2)
+   ('dolist 2)
+   ('dolist-with-progress-reporter 3)
+   ('dotimes 2)
+   ('dotimes-with-progress-reporter 3)
+   ('easy-menu-define 3)
+   ('easy-mmode-defmap 2)
+   ('easy-mmode-defsyntax 2)
+   ('ert-deftest 3)
+   ('eval-after-load 2)
+   ('eval-and-compile 1)
+   ('eval-when-compile 1)
+   ('gv-define-expander 2)
+   ('gv-define-setter 3)
+   ('gv-letplace 3)
+   ('if 2)                              ; Changed from 3
+   ('if-let 2)                          ; Changed from 3
+   ('if-let* 2)                         ; Changed from 3
+   ('ignore-error 2)
+   ('ignore-errors 1)
+   ('isearch-define-mode-toggle 4)
+   ('keymap-set-after 4)
+   ('lambda 2)                          ; Changed from 3
+   ('let 2)
+   ('let* 2)
+   ('let-alist 2)
+   ('let-when-compile 2)
+   ('letrec 2)
+   ('macroexp-let2 4)
+   ('macroexp-let2* 3)
+   ('minibuffer-with-setup-hook 2)
+   ('named-let 3)
+   ('oclosure-define 2)
+   ('oclosure-lambda 3)
+   ('pcase 2)
+   ('pcase-defmacro 3)
+   ('pcase-dolist 2)
+   ('pcase-exhaustive 2)
+   ('pcase-lambda 4)
+   ('pcase-let 2)
+   ('pcase-let* 2)
+   ('prog1 2)
+   ('prog2 3)
+   ('progn 1)
+   ('rx-define 3)
+   ('rx-let 2)
+   ('rx-let-eval 2)
+   ('save-current-buffer 1)
+   ('save-excursion 1)
+   ('save-mark-and-excursion 1)
+   ('save-match-data 1)
+   ('save-restriction 1)
+   ('save-selected-window 1)
+   ('save-window-excursion 1)
+   ('seq-doseq 2)
+   ('seq-let 3)
+   ('thread-first 1)
+   ('thread-last 1)
+   ('track-mouse 1)
+   ('unless 2)
+   ('unwind-protect 2)
+   ('use-package 2)                     ; Changed from 3
+   ('when 2)
+   ('when-let 2)
+   ('when-let* 2)
+   ('while 2)
+   ('while-let 2)
+   ('while-no-input 1)
+   ('with-auto-compression-mode 1)
+   ('with-buffer-unmodified-if-unchanged 1)
+   ('with-case-table 2)
+   ('with-category-table 2)
+   ('with-coding-priority 2)
+   ('with-current-buffer 2)
+   ('with-current-buffer-window 4)
+   ('with-decoded-time-value 2)
+   ('with-delayed-message 2)
+   ('with-demoted-errors 2)
+   ('with-displayed-buffer-window 4)
+   ('with-environment-variables 2)
+   ('with-eval-after-load 2)
+   ('with-existing-directory 1)
+   ('with-file-modes 2)
+   ('with-help-window 2)
+   ('with-local-quit 1)
+   ('with-locale-environment 2)
+   ('with-memoization 2)
+   ('with-minibuffer-completions-window 1)
+   ('with-minibuffer-selected-window 1)
+   ('with-mutex 2)
+   ('with-no-warnings 1)
+   ('with-output-to-string 1)
+   ('with-output-to-temp-buffer 2)
+   ('with-selected-frame 2)
+   ('with-selected-window 2)
+   ('with-silent-modifications 1)
+   ('with-slots 3)
+   ('with-suppressed-warnings 2)
+   ('with-syntax-table 2)
+   ('with-temp-buffer 1)
+   ('with-temp-buffer-window 4)
+   ('with-temp-file 2)
+   ('with-temp-message 2)
+   ('with-timeout 2)
+   ('with-undo-amalgamate 1)
+   ('with-window-non-dedicated 2)))
+
 (define (prefix? candidate lst)
   "Return true if CANDIDATE is a prefix of LST."
   (let loop ((candidate candidate)
@@ -386,17 +707,29 @@ (define (prefix? candidate lst)
           (and (equal? head1 head2)
                (loop rest1 rest2))))))))
 
-(define (special-form-lead symbol context)
+(define* (special-form-lead symbol context
+                            #:key
+                            elisp?
+                            (special-forms '()))
   "If SYMBOL is a special form in the given CONTEXT, return its number of
 arguments; otherwise return #f.  CONTEXT is a stack of symbols lexically
-surrounding SYMBOL."
-  (match (vhash-assq symbol %special-forms)
-    (#f #f)
-    ((_ . alist)
-     (any (match-lambda
-            ((prefix . level)
-             (and (prefix? prefix context) (- level 1))))
-          alist))))
+surrounding SYMBOL.  If ELISP? is true, return the number of arguments for the
+Emacs Lisp form matching SYMBOL.  If SYMBOL is a key in the alist
+SPECIAL-FORMS, return the value of the first matching alist entry instead."
+  ;; XXX: A value N in SPECIAL-FORMS is equivalent to a value of N+1 in the
+  ;; `%special-forms' or `%elisp-special-forms' vhashes; this makes
+  ;; SPECIAL-FORMS similar to the `lisp-indent-function' symbol property in
+  ;; Emacs and probably less confusing.
+  (or (assq-ref special-forms symbol)
+      (match (vhash-assq symbol (if elisp?
+                                    %elisp-special-forms
+                                    %special-forms))
+        (#f #f)
+        ((_ . alist)
+         (any (match-lambda
+                ((prefix . level)
+                 (and (prefix? prefix context) (- level 1))))
+              alist)))))
 
 (define (newline-form? symbol context)
   "Return true if parenthesized expressions starting with SYMBOL must be
@@ -424,8 +757,11 @@ (define %natural-whitespace-string-forms
   ;; and backslashes are escaped; newlines, tabs, etc. are left as-is.
   '(synopsis description G_ N_))
 
-(define (printed-string str context)
-  "Return the read syntax for STR depending on CONTEXT."
+(define %elisp-natural-whitespace-string-forms
+  '(defun))
+
+(define* (printed-string str context #:key elisp?)
+  "Return the read syntax for STR depending on CONTEXT and ELISP?."
   (define (preserve-newlines? str)
     (and (> (string-length str) 40)
          (string-index str #\newline)))
@@ -436,7 +772,9 @@ (define (printed-string str context)
          (escaped-string str)
          (object->string str)))
     ((head . _)
-     (if (or (memq head %natural-whitespace-string-forms)
+     (if (or (memq head (if elisp?
+                            %elisp-natural-whitespace-string-forms
+                            %natural-whitespace-string-forms))
              (preserve-newlines? str))
          (escaped-string str)
          (object->string str)))))
@@ -529,13 +867,126 @@ (define %special-non-extended-symbols
   ;; extended symbols: 1+, 1-, 123/, etc.
   (make-regexp "^[0-9]+[[:graph:]]+$" regexp/icase))
 
-(define (symbol->display-string symbol context)
+(define %elisp-special-symbol-chars
+  ;; Characters that need to be backslash-escaped within an Elisp symbol (see
+  ;; (elisp) Symbol Type).
+  (char-set-complement (char-set-union char-set:letter+digit
+                                       (char-set #\- #\+ #\= #\( #\/
+                                                 #\_ #\~ #\! #\@ #\$
+                                                 #\% #\^ #\& #\: #\<
+                                                 #\> #\{ #\} #\? #\*))))
+
+(define %elisp-confusable-number-symbols
+  ;; Symbols that must begin with a backslash in order to prevent them from
+  ;; being read as Elisp numbers.
+  (make-regexp (string-append
+                "(^[+-]?[0-9]+(\\.[0-9]*[eE]?(\\+NaN|\\+INF|[0-9]+)?)?$)"
+                "|(^[0-9]+[eE][0-9]+$)")))
+
+(define* (symbol->display-string symbol context #:key elisp?)
   "Return the most appropriate representation of SYMBOL, resorting to extended
 symbol notation only when strictly necessary."
   (let ((str (symbol->string symbol)))
-    (if (regexp-exec %special-non-extended-symbols str)
-        str                                  ;no need for the #{...}# notation
-        (object->string symbol))))
+    (if elisp?
+        (let ((str* (list->string
+                     (string-fold-right (lambda (chr lst)
+                                          (if (char-set-contains?
+                                               %elisp-special-symbol-chars
+                                               chr)
+                                              (cons* #\\ chr lst)
+                                              (cons chr lst)))
+                                        '()
+                                        str))))
+          (if (regexp-exec %elisp-confusable-number-symbols str*)
+              (string-append "\\" str*)
+              str*))
+        (if (regexp-exec %special-non-extended-symbols str)
+            str                         ;no need for the #{...}# notation
+            (object->string symbol)))))
+
+(define %elisp-basic-chars
+  ;; Characters that can safely be specified using the Elisp character read
+  ;; syntax without backslash-escapes.
+  (char-set-union char-set:letter+digit
+                  (char-set #\~ #\! #\@ #\$ #\% #\^
+                            #\& #\* #\- #\_ #\= #\+
+                            #\{ #\} #\/ #\? #\< #\>)))
+
+(define %elisp-simple-escape-chars
+  ;; Whitespace, control, and other special characters that can be specified
+  ;; using the `?\X' Elisp read syntax, where X is a single character that has
+  ;; a special meaning.
+  (char-set #\alarm #\backspace #\tab #\newline #\vtab #\page #\return
+            #\esc #\space #\\ #\delete))
+
+(define (atom->elisp-string obj)
+  "Return a string representation of atom OBJ that is suitable for the Emacs
+Lisp reader.  Pairs and arrays should be serialized with
+`pretty-print-with-comments' instead."
+  (match obj
+    (#t
+     "t")
+    (() "()")
+    ((? nil?)
+     "nil")
+    ((? char?)
+     (cond
+      ((char-set-contains? %elisp-basic-chars obj)
+       (list->string (list #\? obj)))
+      ((char-set-contains? %elisp-simple-escape-chars obj)
+       (list->string (list #\? #\\ (case obj
+                                     ((#\alarm) #\a)
+                                     ((#\backspace) #\b)
+                                     ((#\tab) #\t)
+                                     ((#\newline) #\n)
+                                     ((#\vtab) #\v)
+                                     ((#\page) #\f)
+                                     ((#\return) #\r)
+                                     ((#\esc) #\e)
+                                     ((#\space) #\s)
+                                     ((#\\) #\\)
+                                     ((#\delete) #\d)))))
+      (else
+       (let ((num (char->integer obj)))
+         (if (<= num 65535)
+             (format #f "?\\u~4,'0x" num)
+             (format #f "?\\U~:@(~8,'0x~)" num))))))
+    ((? string?)
+     (printed-string obj '() #:elisp? #t))
+    ((? symbol?)
+     (symbol->display-string obj '() #:elisp? #t))
+    ((? keyword?)
+     (string-append ":" (symbol->display-string (keyword->symbol obj)
+                                                '() #:elisp? #t)))
+    ((? number? num)
+     (match num
+       ((? exact-integer?)
+        ;; E.g., 123
+        (object->string num))
+       ((? exact?)
+        ;; E.g., 1/2
+        (object->string (exact->inexact num)))
+       ((? rational?)
+        ;; E.g., 1.5
+        (object->string num))
+       ((? nan?)
+        ;; Not implemented by `read-elisp'.
+        "0.0e+NaN")
+       ((? inf?)
+        ;; Not implemented by `read-elisp'.
+        (if (negative? num)
+            "-1.0e+INF"
+            "1.0e+INF"))
+       (_
+        ;; Complex numbers
+        (raise
+         (formatted-message (G_ "cannot serialize complex number to Elisp: ~a")
+                            num)))))
+    (_
+     ;; Not an atom.
+     (raise
+      (formatted-message (G_ "Error serializing object to Elisp: ~a")
+                         obj)))))
 
 (define* (pretty-print-with-comments port obj
                                      #:key
@@ -544,7 +995,9 @@ (define* (pretty-print-with-comments port obj
                                      (format-vertical-space identity)
                                      (indent 0)
                                      (max-width 78)
-                                     (long-list 5))
+                                     (long-list 5)
+                                     (elisp? #f)
+                                     (special-forms '()))
   "Pretty-print OBJ to PORT, attempting to at most MAX-WIDTH character columns
 and assuming the current column is INDENT.  Comments present in OBJ are
 included in the output.
@@ -552,15 +1005,54 @@ (define* (pretty-print-with-comments port obj
 Lists longer than LONG-LIST are written as one element per line.  Comments are
 passed through FORMAT-COMMENT before being emitted; a useful value for
 FORMAT-COMMENT is 'canonicalize-comment'.  Vertical space is passed through
-FORMAT-VERTICAL-SPACE; a useful value of 'canonicalize-vertical-space'."
+FORMAT-VERTICAL-SPACE; a useful value of 'canonicalize-vertical-space'.
+
+If ELISP? is true, OBJ is printed as Emacs Lisp, simulating the indentation
+used by Emacs for many common forms.
+
+To specify additional rules for special indentation, use SPECIAL-FORMS, an
+association list where each entry is a pair of the form (SYMBOL . INDENT).
+When SYMBOL occurs at the beginning of a list in OBJ, the first INDENT
+expressions after SYMBOL will be indented as arguments and the rest will be
+indented as body expressions.  When ELISP? is true, arguments that cannot be
+printed on the same line as SYMBOL will be indented 4 columns beyond the base
+indentation of the enclosing list, and body expressions will be indented 2
+columns beyond the base indentation."
+
+  (define gexp-syntax?
+    (if (not elisp?)
+        (cut memq <> '(gexp ungexp ungexp-native ungexp-splicing
+                            ungexp-native-splicing))
+        (const #f)))
+
+  (define elisp-syntax?
+    (if elisp?
+        (cut eq? <> 'function)
+        (const #f)))
+
+  (define (read-syntax? obj)
+    (or (memq obj '(quote
+                    unquote
+                    unquote-splicing))
+        (gexp-syntax? obj)
+        (elisp-syntax? obj)))
+
   (define (list-of-lists? head tail)
     ;; Return true if HEAD and TAIL denote a list of lists--e.g., a list of
-    ;; 'let' bindings.
+    ;; 'let' bindings or an alist.
     (match head
-      ((thing _ ...)                              ;proper list
-       (and (not (memq thing
-                       '(quote quasiquote unquote unquote-splicing)))
-            (pair? tail)))
+      ((thing . _)
+       (and (not (read-syntax? thing))
+            (match tail
+              (((? pair?) . _)
+               #t)
+              (_ #f))))
+      (_ #f)))
+
+  (define list?*
+    (match-lambda
+      (((not (? read-syntax?)) . _)
+       #t)
       (_ #f)))
 
   (define (starts-with-line-comment? lst)
@@ -569,44 +1061,165 @@ (define* (pretty-print-with-comments port obj
       ((x . _) (and (comment? x) (not (comment-margin? x))))
       (_ #f)))
 
+  (define (array?* obj)
+    (and (array? obj)
+         (not (string? obj))))
+
+  (define (symbol->display-string* symbol context)
+    (symbol->display-string symbol context #:elisp? elisp?))
+
+  (define (printed-string* str context)
+    (printed-string str context #:elisp? elisp?))
+
+  (define (length* x)
+    ;; Return the length of list or dotted list X.
+    (let lp ((lst x)
+	     (len 0))
+      (match lst
+        (()
+         len)
+	((not (? pair?))
+	 (+ len 1))
+	((head . tail)
+	 (lp tail (+ len 1))))))
+
+  (define (dotted-list->list exp)
+    (let lp ((lst exp)
+	     (acc '()))
+      (match lst
+        (()
+         (reverse acc))
+	((not (? pair?))
+         (lp '() (cons lst acc)))
+	((head . tail)
+	 (lp tail (cons head acc))))))
+
   (let loop ((indent indent)
              (column indent)
              (delimited? #t)                  ;true if comes after a delimiter
              (context '())                    ;list of "parent" symbols
              (obj obj))
-    (define (print-sequence context indent column lst delimited?)
+    (define (print-sequence context indent column lst delimited?
+                            force-newline?)
+      (define dotted? (dotted-list? lst))
       (define long?
-        (> (length lst) long-list))
+        ;; For lists that are function calls, omit heads from long list count,
+        ;; but include them for lists that aren't function calls.
+        (> (+ (length* lst)
+              (if (or dotted?
+                      (match context
+                        (((not (? symbol?)) . _)
+                         #t)
+                        ((_ 'quote . _)
+                         #t)
+                        (_ #f)))
+                  1 0))
+           long-list))
 
       (let print ((lst lst)
                   (first? #t)
                   (delimited? delimited?)
-                  (column column))
-        (match lst
-          (()
-           column)
-          ((item . tail)
-           (define newline?
-             ;; Insert a newline if ITEM is itself a list, or if TAIL is long,
-             ;; but only if ITEM is not the first item.  Also insert a newline
-             ;; before a keyword.
-             (and (or (pair? item) long?
-                      (and (keyword? item)
-                           (not (eq? item #:allow-other-keys))))
-                  (not first?) (not delimited?)
-                  (not (blank? item))))
-
-           (when newline?
-             (newline port)
-             (display (make-string indent #\space) port))
-           (let ((column (if newline? indent column)))
-             (print tail
-                    (keyword? item)      ;keep #:key value next to one another
-                    (blank? item)
-                    (loop indent column
-                          (or newline? delimited?)
-                          context
-                          item)))))))
+                  (column column)
+                  (unquote? #f)         ;end of list when, e.g., `(a b . ,c)
+                  (kw? #f))             ;previous item was a keyword
+        (cond
+         ((null? lst)
+          column)
+         ((blank? lst)
+          ;; Comments or whitespace cannot occur at the end of a dotted list.
+          column)
+         ((or unquote? (not (pair? lst)))
+          ;; End of improper list.
+          (let ((newline? (or long?
+                              (sequence-would-protrude?
+                               (+ column 2 (if unquote? 1 0))
+                               lst)
+                              (read-syntax-would-protrude?
+                               (+ column 2 (if unquote? 1 0))
+                               lst))))
+            (if newline?
+                (begin
+                  (newline port)
+                  (display (make-string indent #\space) port))
+                (display " " port))
+            (display ". " port)
+            (when unquote? (display "," port))
+            (let ((column (+ (if newline?
+                                 (+ indent 2)
+                                 (+ column 3))
+                             (if unquote? 1 0))))
+              (loop indent column
+                    #t
+                    context
+                    lst))))
+         (else
+          (match lst
+            (('unquote obj)
+             ;; A form like `(a b . ,OBJ) was expanded into (quasiquote (a b
+             ;; unquote OBJ)), which will still be properly expanded by
+             ;; `quasiquote' into (a b . OBJ).
+             (print obj #f #f column #t kw?))
+            ((item . tail)
+             (define kw-item?*
+               (if elisp?
+                   (cond
+                    ((keyword? item) #t)
+                    ((symbol? item) (string-prefix? ":" (symbol->string item)))
+                    (else #f))
+                   (and (keyword? item)
+                        (not (eq? item #:allow-other-keys)))))
+             (define newline?
+               ;; Insert a newline if ITEM is itself a list, or if TAIL is
+               ;; long, but only if ITEM is not the first item.  Also insert a
+               ;; newline before a keyword, and before a read syntax (e.g.,
+               ;; `'', `#~', '#'') that would protrude.  We need to test
+               ;; before invocation of `print-sequence' whether the first ITEM
+               ;; would protrude, since INDENT must then be less than usual.
+               ;; We thread the results of that test to here with
+               ;; FORCE-NEWLINE?.
+               (or (and first? force-newline?)
+                   (and (or (list?* item)
+                            long?
+                            (read-syntax-would-protrude?
+                             (+ column 1) item)
+                            kw-item?*)
+                        (or dotted?     ;newline after head of improper list
+                            (not first?)
+                            (and first?
+                                 (match context
+                                   (((and (not (? symbol?))
+                                          (not (? keyword?))) . _)
+                                    ;; Allow newline before first item when
+                                    ;; head of list is not a symbol.
+                                    ;; E.g.:
+                                    ;; (use-package foo
+                                    ;;   :bind (("C-c f f" . foo) ;\n
+                                    ;;          :map foo-map
+                                    ;;          ("C-c f g" . foo-status)))
+                                    #t)
+                                   ((_ 'quote _ ...)
+                                    ;; E.g.:
+                                    ;; '(a ;\n
+                                    ;;   b)
+                                    #t)
+                                   (_ #f))))
+                        (not kw?)       ;previous ITEM not a keyword
+                        (not delimited?)
+                        (not (blank? item)))))
+
+             (when newline?
+               (newline port)
+               (display (make-string indent #\space) port))
+             (let ((column (if newline? indent column)))
+               (print tail
+                      #f
+                      (blank? item)
+                      (loop indent column
+                            (or newline? delimited?)
+                            context
+                            item)
+                      #f
+                      kw-item?*))))))))
 
     (define (sequence-would-protrude? indent lst)
       ;; Return true if elements of LST written at INDENT would protrude
@@ -614,21 +1227,41 @@ (define* (pretty-print-with-comments port obj
       ;; negatives to avoid actually rendering all of LST.
       (find (match-lambda
               ((? string? str)
-               (>= (+ (string-width str) 2 indent) max-width))
+               (>= (+ (string-width (printed-string* str '()))
+                      2 indent)
+                   max-width))
               ((? symbol? symbol)
-               (>= (+ (string-width (symbol->display-string symbol context))
+               (>= (+ (string-width (symbol->display-string* symbol context))
                       indent)
                    max-width))
               ((? boolean?)
                (>= (+ 2 indent) max-width))
               (()
                (>= (+ 2 indent) max-width))
-              (_                                  ;don't know
+              (_                        ;don't know
                #f))
-            lst))
+            (if (dotted-list? lst) (dotted-list->list lst) lst)))
+
+    (define (read-syntax-would-protrude? indent lst)
+      (match lst
+        ((or ((? read-syntax? syntax) exp)
+             (((? read-syntax? syntax) exp) . _))
+         (sequence-would-protrude? (+ indent (case syntax
+                                               ((quote) 1)
+                                               ((unquote) 1)
+                                               ((ungexp-splicing) 3)
+                                               ((ungexp-native-splicing) 3)
+                                               (else 2)))
+                                   exp))
+        (_ #f)))
+
+    (define (special-form-lead* head)
+      (special-form-lead head context
+                         #:elisp? elisp?
+                         #:special-forms special-forms))
 
     (define (special-form? head)
-      (special-form-lead head context))
+      (special-form-lead* head))
 
     (match obj
       ((? comment? comment)
@@ -665,7 +1298,8 @@ (define* (pretty-print-with-comments port obj
       (('quote lst)
        (unless delimited? (display " " port))
        (display "'" port)
-       (loop indent (+ column (if delimited? 1 2)) #t context lst))
+       (loop indent (+ column (if delimited? 1 2))
+             #t (cons 'quote context) lst))
       (('quasiquote lst)
        (unless delimited? (display " " port))
        (display "`" port)
@@ -678,33 +1312,36 @@ (define* (pretty-print-with-comments port obj
        (unless delimited? (display " " port))
        (display ",@" port)
        (loop indent (+ column (if delimited? 2 3)) #t context lst))
-      (('gexp lst)
+      (((? gexp-syntax? head) obj)
        (unless delimited? (display " " port))
-       (display "#~" port)
-       (loop indent (+ column (if delimited? 2 3)) #t context lst))
-      (('ungexp obj)
+       (match head
+         ('gexp
+          (display "#~" port)
+          (loop indent (+ column (if delimited? 2 3)) #t context obj))
+         ('ungexp
+          (display "#$" port)
+          (loop indent (+ column (if delimited? 2 3)) #t context obj))
+         ('ungexp-native
+          (display "#+" port)
+          (loop indent (+ column (if delimited? 2 3)) #t context obj))
+         ('ungexp-splicing
+          (display "#$@" port)
+          (loop indent (+ column (if delimited? 3 4)) #t context obj))
+         ('ungexp-native-splicing
+          (display "#+@" port)
+          (loop indent (+ column (if delimited? 3 4)) #t context obj))))
+      (((? elisp-syntax? head) obj)
        (unless delimited? (display " " port))
-       (display "#$" port)
+       (display "#'" port)
        (loop indent (+ column (if delimited? 2 3)) #t context obj))
-      (('ungexp-native obj)
-       (unless delimited? (display " " port))
-       (display "#+" port)
-       (loop indent (+ column (if delimited? 2 3)) #t context obj))
-      (('ungexp-splicing lst)
-       (unless delimited? (display " " port))
-       (display "#$@" port)
-       (loop indent (+ column (if delimited? 3 4)) #t context lst))
-      (('ungexp-native-splicing lst)
-       (unless delimited? (display " " port))
-       (display "#+@" port)
-       (loop indent (+ column (if delimited? 3 4)) #t context lst))
       (((? special-form? head) arguments ...)
        ;; Special-case 'let', 'lambda', 'modify-inputs', etc. so the second
        ;; and following arguments are less indented.
-       (let* ((lead    (special-form-lead head context))
+       (let* ((lead    (special-form-lead* head))
               (context (cons head context))
-              (head    (symbol->display-string head (cdr context)))
-              (total   (length arguments)))
+              (head    (symbol->display-string* head (cdr context)))
+              (total   (length arguments))
+              (body    (drop arguments (min lead total))))
          (unless delimited? (display " " port))
          (display "(" port)
          (display head port)
@@ -714,43 +1351,93 @@ (define* (pretty-print-with-comments port obj
          ;; Print the first LEAD arguments.
          (let* ((indent (+ column 2
                                   (if delimited? 0 1)))
+                (old-column column)
                 (column (+ column 1
                                   (if (zero? lead) 0 1)
                                   (if delimited? 0 1)
                                   (string-length head)))
-                (initial-indent column))
+                (initial-indent (if elisp?
+                                    ;; Indent arguments 4 columns, like Emacs
+                                    (+ old-column 4 (if delimited? 0 1))
+                                    column)))
            (define new-column
              (let inner ((n lead)
                          (arguments (take arguments (min lead total)))
-                         (column column))
+                         (column column)
+                         (newline? #f))
                (if (zero? n)
-                   (begin
-                     (newline port)
-                     (display (make-string indent #\space) port)
-                     indent)
+                   (if (null? body)     ;no newline when body is empty
+                       column
+                       (begin
+                         (newline port)
+                         (display (make-string indent #\space) port)
+                         indent))
                    (match arguments
                      (() column)
                      ((head . tail)
+                      (when newline?
+                        ;; Print a newline when previous argument was a list.
+                        (newline port)
+                        (display (make-string initial-indent #\space) port))
                       (inner (- n 1) tail
-                             (loop initial-indent column
-                                   (= n lead)
+                             (loop initial-indent
+                                   (if newline? initial-indent column)
+                                   (or newline? (= n lead))
                                    context
-                                   head)))))))
+                                   head)
+                             (list?* head)))))))
 
            ;; Print the remaining arguments.
            (let ((column (print-sequence
                           context indent new-column
-                          (drop arguments (min lead total))
-                          #t)))
+                          body
+                          #t #f)))
              (display ")" port)
              (+ column 1)))))
-      ((head tail ...)
+      ((? array?* obj)
+       ;; Vectors, arrays, bytevectors, bitvectors.
+       (if elisp?
+           (let* ((lst (array->list obj))
+                  (overflow? (>= column max-width))
+                  (column    (if overflow?
+                                 (+ indent 1)
+                                 (+ column (if delimited? 1 2)))))
+             (if overflow?
+                 (begin
+                   (newline port)
+                   (display (make-string indent #\space) port))
+                 (unless delimited? (display " " port)))
+
+             (display "[" port)
+
+             (let ((column (print-sequence context column column lst #t #f)))
+               (display "]" port)
+               (+ column 1)))
+           ;; For Scheme, `object->string' prints the proper Guile syntax for
+           ;; the specific type of array, but with long arrays on one line.
+           (let* ((str (object->string obj))
+                  (len (string-width str)))
+             (if (and (> (+ column 1 len) max-width)
+                      (not delimited?))
+                 (begin
+                   (newline port)
+                   (display (make-string indent #\space) port)
+                   (display str port)
+                   (+ indent len))
+                 (begin
+                   (unless delimited? (display " " port))
+                   (display str port)
+                   (+ column (if delimited? 0 1) len))))))
+      ((head . tail)
+       ;; Lists and improper lists.
        (let* ((overflow? (>= column max-width))
               (column    (if overflow?
                              (+ indent 1)
                              (+ column (if delimited? 1 2))))
-              (newline?  (or (newline-form? head context)
-                             (list-of-lists? head tail) ;'let' bindings
+              ;; Newline for `let' bindings, alists, long lists of constants.
+              (newline?  (or (and (not (null? tail))
+                                  (or (newline-form? head context)
+                                      (list-of-lists? head tail)))
                              (starts-with-line-comment? tail)))
               (context   (cons head context)))
          (if overflow?
@@ -761,11 +1448,29 @@ (define* (pretty-print-with-comments port obj
          (display "(" port)
 
          (let* ((new-column (loop column column #t context head))
+                (force-newline? (and (not newline?)
+                                     (or (read-syntax-would-protrude?
+                                          (+ new-column 1) tail)
+                                         (match tail
+                                           (((and lst
+                                                  ((not (? read-syntax?)) . _))
+                                             . _)
+                                            ;; Newline before initial list
+                                            ;; argument with long element(s).
+                                            (sequence-would-protrude?
+                                             (+ new-column 1) lst))
+                                           (_ #f)))))
                 (indent (if (or (>= new-column max-width)
+                                force-newline?
+                                newline?
                                 (not (symbol? head))
+                                (match context
+                                  ((_ 'quote _ ...)
+                                   #t)
+                                  (_ #f))
+                                (dotted-list? (cons head tail))
                                 (sequence-would-protrude?
-                                 (+ new-column 1) tail)
-                                newline?)
+                                 (+ new-column 1) tail))
                             column
                             (+ new-column 1))))
            (when newline?
@@ -776,18 +1481,22 @@ (define* (pretty-print-with-comments port obj
            (let ((column
                   (print-sequence context indent
                                   (if newline? indent new-column)
-                                  tail newline?)))
+                                  tail newline? force-newline?)))
              (display ")" port)
              (+ column 1)))))
       (_
        (let* ((str (cond ((string? obj)
-                          (printed-string obj context))
+                          (printed-string* obj context))
                          ((integer? obj)
-                          (integer->string obj context))
+                          (if elisp?
+                              (atom->elisp-string obj)
+                              (integer->string obj context)))
                          ((symbol? obj)
-                          (symbol->display-string obj context))
+                          (symbol->display-string* obj context))
                          (else
-                          (object->string obj))))
+                          (if elisp?
+                              (atom->elisp-string obj)
+                              (object->string obj)))))
               (len (string-width str)))
          (if (and (> (+ column 1 len) max-width)
                   (not delimited?))
diff --git a/tests/home/services/emacs.scm b/tests/home/services/emacs.scm
new file mode 100644
index 0000000000..caa70ef6fd
--- /dev/null
+++ b/tests/home/services/emacs.scm
@@ -0,0 +1,345 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2023 Kierin Bell <fernseed@HIDDEN>
+;;;
+;;; 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 (tests home services emacs)
+  #:use-module (gnu home services emacs)
+  #:use-module (guix store)
+  #:use-module (guix monads)
+  #:use-module (guix gexp)
+  #:use-module (guix derivations)
+  #:use-module (guix packages)
+  #:use-module (guix tests)
+  #:use-module (guix read-print)
+  #:use-module (gnu packages guile)
+  #:use-module (ice-9 textual-ports)
+  #:use-module (srfi srfi-64))
+
+(test-begin "emacs-home-services")
+
+;;; Test `elisp' syntax
+
+(test-equal "test `elisp' syntax, symbol"
+  't
+  (elisp->sexp (elisp t)))
+
+(test-equal "test `elisp' syntax, basic list"
+  '(a b c)
+  (elisp->sexp (elisp (a b c))))
+
+(test-equal "test `elisp' syntax, substitute symbol"
+  'a
+  (let ((foo 'a))
+    (elisp->sexp (elisp (unelisp foo)))))
+
+(test-equal "test `elisp' syntax, substitute splicing"
+  '(a b c)
+  (let ((foo '(a b c)))
+    (elisp->sexp (elisp ((unelisp-splicing foo))))))
+
+(test-equal "test `elisp' syntax, comment"
+  (comment ";comment\n")
+  (elisp->sexp (elisp (unelisp-comment ";comment\n"))))
+
+(test-equal "test `elisp' syntax, nested comment"
+  `(a ,(comment ";comment\n") b)
+  (elisp->sexp (elisp (a (unelisp-comment ";comment\n") b))))
+
+(test-equal "test `elisp' syntax, newline"
+  (vertical-space 0)
+  (elisp->sexp (elisp (unelisp-newline))))
+
+(test-equal "test `elisp' syntax, page break"
+  (page-break)
+  (elisp->sexp (elisp (unelisp-page-break))))
+
+(test-equal "elisp->sexp, nested <elisp> objects"
+  '(a (b c))
+  (elisp->sexp (elisp (a (unelisp (elisp (b (unelisp (elisp c)))))))))
+
+;;; Test #% reader extension
+
+(test-equal "test hash extension, symbol"
+  't
+  (elisp->sexp #%t))
+
+(test-equal "test hash extension, basic list"
+  '(a b c)
+  (elisp->sexp #%(a b c)))
+
+(test-equal "test hash extension, dotted list"
+  '(a . b)
+  (elisp->sexp #%(a . b)))
+
+(test-equal "test hash extension, substitute symbol"
+  'a
+  (let ((foo 'a))
+    (elisp->sexp #%#$foo)))
+
+(test-equal "test hash extension, substitute splicing"
+  '(a b c)
+  (let ((foo '(a b c)))
+    (elisp->sexp #%(#$@foo))))
+
+(test-equal "test hash extension, basic vector"
+  #(a b c)
+  (elisp->sexp #%[a b c]))
+
+(test-equal "test hash extension, basic character"
+  #\a
+  (elisp->sexp #%?a))
+
+(test-equal "test hash extension, comment"
+  (elisp->sexp (elisp (unelisp-comment ";comment\n")))
+  (elisp->sexp #%#;comment
+               ))
+
+(test-equal "test hash extension, nested comment"
+  (elisp->sexp (elisp (a (unelisp-comment ";comment\n") b)))
+  (elisp->sexp #%(a #;comment
+                  b)))
+
+(test-equal "test hash extension, page break"
+  (elisp->sexp (elisp (unelisp-newline)))
+  (elisp->sexp #%#>))
+
+(test-equal "test hash extension, page break"
+  (elisp->sexp (elisp (unelisp-page-break)))
+  (elisp->sexp #%#^L))
+
+(test-equal "test hash extension, nested"
+  '(a (b c))
+  (elisp->sexp #%(a #$#%(b #$#%c))))
+
+;;; Test home Emacs service configuration
+
+(define (input->home-emacs-configuration . x)
+  (apply (@@ (gnu home services emacs) input->home-emacs-configuration) x))
+
+(define (home-emacs-configuration->code . x)
+  (apply (@@ (gnu home services emacs) home-emacs-configuration->code) x))
+
+(define-syntax-rule (test-import-emacs-configuration str config)
+  "Test equality of `home-emacs-configuration' generated from Elisp string STR
+with record CONFIG"
+  (test-equal "test Emacs home configuration import "
+    (home-emacs-configuration->code (call-with-input-string str
+                                      input->home-emacs-configuration))
+    (home-emacs-configuration->code config)))
+
+(test-import-emacs-configuration
+ "(setq my--foo 1)
+(setq my--bar 'symbol)
+(setq my--baz (list 'a ;comment
+		    'b 'c))
+(setq my--quux '(a b . c))
+(setq my--quuux #'my--fun)
+(setopt foo-var my--foo)
+
+(foo-mode -1)
+(bar-mode 1)
+(baz-mode)
+(quux-mode my--foo)
+
+
+(bind-key* \"M-<up>\" 'scroll-down-line)
+(bind-key* \"M-<down>\" 'scroll-up-line t)
+;;; Top-level comment
+(global-set-key (kbd \"C-c b\") 'bar)
+(global-set-key [remap bar] 'baz)
+(keymap-global-set \"C-c v\" 'quux)
+(bind-key \"C-c c\" 'quuux)
+(bind-key [t] #'quuuux 'global-map t)
+
+(use-package foo
+  :demand t
+  ;; Inconvenient comment
+  :hook prog-mode
+  :custom
+  (foo-bar 'baz)
+  (foo-baz baz)
+  :init
+  ;; Ding
+  (ding)
+
+  (message \"Ding\"))
+
+(use-package bar
+  :if (eq system-type 'gnu/linx)
+  :after foo
+  :load-path \"~/src/bar\"
+  :autoload bar-internal
+  :commands bar-status bar
+  :bind* ((\"C-x n\" . bar-status))
+  :bind ((\"C-c n\" . bar)
+	 :map bar-mode
+	 (\"C-@\" . bar-bar)
+	 :map bar-status-mode
+	 (\"C-n\" . bar-next)
+	 (\"C-c C-c\" . bar-do)
+	 :repeat-map bar-repeat-map
+	 (\"n\" . bar-next)
+	 (\"c\" . bar-do))
+  :bind-keymap (\"C-c b\" . bar-mode-map)
+  :custom
+  (bar-bool t)
+  (bar-string \"bar\")
+  (bar-list '(bar-1 bar-2 bar-3))
+  (bar-list-2 `(,@bar-list bar-4))
+  (bar-var my--foo)
+  :custom-face
+  (bar-face ((t (:slant italic))))
+  (bar-highlight-face ((((class color) (background light))
+			:background \"goldenrod1\")
+		       (((class color) (background dark))
+			:background \"DarkGoldenrod4\")
+		       (t :inverse-video t)))
+  :hook ((prog-mode foo-mode) . bar-mode)
+  :mode \"\\\\.bar\\\\'\"
+  :magic \">>BAR<<\"
+  :magic-fallback \"<<BAR>>\"
+  :config
+  ;; Extra configuration
+  (add-to-list 'bar-extensions 'foo-bar)
+  :catch (lambda (_ _)
+	   (message \"Error package initialization\")))
+
+(use-package baz
+  :unless (eq system-name \"bar\")
+  :after (foo bar)
+  :load-path (\"~/src/my/baz\" \"~/src/baz\")
+  :autoload (baz-1 baz-2)
+  :commands (baz)
+  :custom
+  ((baz-option t)
+   (bar-list '((baz-1 . baz-2)))
+   (baz-var my--foo))
+  :hook ((prog-mode . baz-mode)
+	 (bar-mode . baz-mode))
+  :mode (\"\\\\.baz\\\\'\" . baz-mode)
+  :magic (\">>BAZ<<\" \"!XXBAZXX\"))
+
+(defun my--fun-1 (arg)
+  arg)
+
+(defun my--fun ()
+  (prog1 (my--fun-1 'foo)
+    (ding)))
+"
+ (home-emacs-configuration
+ (default-init
+   (emacs-configuration
+     (variables `((my--foo . 1)
+                  (my--bar . symbol)
+                  (my--baz . ,(elisp (list 'a
+                                           (unelisp-comment ";comment\n") 'b
+                                           'c)))
+                  (my--quux a b . c)
+                  (my--quuux . ,(elisp (function my--fun)))
+                  (foo-var . ,(elisp my--foo))))
+     (modes `((foo-mode . #f)
+              (bar-mode . #t)
+              (baz-mode . #t)
+              (quux-mode . ,(elisp my--foo))))
+     (keys '(("C-c b" . bar)
+             (#(remap bar) . baz)
+             ("C-c v" . quux)
+             ("C-c c" . quuux)
+             (#(t) . quuuux)))
+     (keys-override '(("M-<up>" . scroll-down-line)
+                      ("M-<down>" . scroll-up-line)))
+     (extra-init
+       (list (elisp (defun my--fun-1
+                           (arg) arg))
+             (elisp (defun my--fun ()
+                           (prog1 (my--fun-1 'foo)
+                                  (ding))))))))
+ (configured-packages
+   (list (emacs-package
+           (name 'foo)
+           (load-force? #t)
+           (options `((foo-bar . baz)
+                      (foo-baz . ,(elisp baz))))
+           (hooks '((prog-mode . foo-mode)))
+           (extra-init
+             (list (elisp (unelisp-comment ";; Ding\n"))
+                   (elisp (ding))
+                   (elisp (unelisp-newline))
+                   (elisp (message "Ding")))))
+         (emacs-package
+           (name 'bar)
+           (load-predicates (list (elisp (eq system-type 'gnu/linx))))
+           (load-after-packages '(foo))
+           (load-paths '("~/src/bar"))
+           (autoloads '(bar-internal))
+           (autoloads-interactive '(bar-status bar))
+           (keys-global '(("C-c n" . bar)))
+           (keys-global-keymaps '(("C-c b" . bar-mode-map)))
+           (keys-override '(("C-x n" . bar-status)))
+           (keys-local (list (emacs-keymap
+                               (name 'bar-mode)
+                               (keys '(("C-@" . bar-bar))))
+                             (emacs-keymap
+                               (name 'bar-status-mode)
+                               (keys '(("C-n" . bar-next)
+                                       ("C-c C-c" . bar-do))))
+                             (emacs-keymap
+                               (name 'bar-repeat-map)
+                               (repeat? #t)
+                               (keys '(("n" . bar-next)
+                                       ("c" . bar-do))))))
+           (options `((bar-bool . #t)
+                      (bar-string . "bar")
+                      (bar-list bar-1 bar-2 bar-3)
+                      (bar-list-2 . ,(elisp `(,@bar-list bar-4)))
+                      (bar-var . ,(elisp my--foo))))
+           (faces '((bar-face (t (:slant italic)))
+                    (bar-highlight-face (((class color)
+                                          (background light))
+                                         :background "goldenrod1")
+                                        (((class color)
+                                          (background dark))
+                                         :background "DarkGoldenrod4")
+                                        (t :inverse-video t))))
+           (hooks '((prog-mode . bar-mode)
+                    (foo-mode . bar-mode)))
+           (auto-modes '(("\\.bar\\'" . bar)))
+           (magic-modes '((">>BAR<<" . bar)))
+           (extra-after-load
+             (list (elisp (unelisp-comment ";; Extra configuration\n"))
+                   (elisp (add-to-list 'bar-extensions 'foo-bar))))
+           (extra-keywords
+             `((:magic-fallback ,(elisp "<<BAR>>"))
+               (:catch ,(elisp (lambda (_ _)
+                                 (message "Error package initialization")))))))
+         (emacs-package
+           (name 'baz)
+           (load-predicates (list (elisp (not (eq system-name "bar")))))
+           (load-after-packages '(foo bar))
+           (load-paths '("~/src/my/baz" "~/src/baz"))
+           (autoloads '(baz-1 baz-2))
+           (autoloads-interactive '(baz))
+           (options `((baz-option . #t)
+                      (bar-list (baz-1 . baz-2))
+                      (baz-var . ,(elisp my--foo))))
+           (hooks '((prog-mode . baz-mode)
+                    (bar-mode . baz-mode)))
+           (auto-modes '(("\\.baz\\'" . baz-mode)))
+           (magic-modes '((">>BAZ<<" . baz)
+                          ("!XXBAZXX" . baz))))))))
+
+(test-end "emacs-home-services")
diff --git a/tests/read-print.scm b/tests/read-print.scm
index 9e1d8038f1..479569e50d 100644
--- a/tests/read-print.scm
+++ b/tests/read-print.scm
@@ -1,5 +1,6 @@
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2021-2023 Ludovic Courtès <ludo@HIDDEN>
+;;; Copyright © 2023 Kierin Bell <fernseed@HIDDEN>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -33,7 +34,7 @@ (define-syntax-rule (test-pretty-print str args ...)
       (lambda (port)
         (let ((exp (call-with-input-string str
                      read-with-comments)))
-         (pretty-print-with-comments port exp args ...))))))
+          (pretty-print-with-comments port exp args ...))))))
 
 (define-syntax-rule (test-pretty-print/sequence str args ...)
   "Likewise, but read and print entire sequences rather than individual
@@ -45,6 +46,20 @@ (define-syntax-rule (test-pretty-print/sequence str args ...)
                      read-with-comments/sequence)))
          (pretty-print-with-comments/splice port lst args ...))))))
 
+(define (read-with-comments-elisp port)
+  (read-with-comments port #:elisp? #t))
+
+(define-syntax-rule (test-pretty-print-elisp str args ...)
+  "Test equality after a round-trip as with `test-pretty-print', but read and write Elisp."
+  (test-equal str
+    (call-with-output-string
+      (lambda (port)
+        (let ((exp (call-with-input-string str
+                    read-with-comments-elisp)))
+          (pretty-print-with-comments port exp
+                                      #:elisp? #t
+                                      args ...))))))
+
 
 (test-begin "read-print")
 
@@ -108,14 +123,71 @@ (define-syntax-rule (test-pretty-print/sequence str args ...)
             (read-with-comments port)))))
 
 (test-pretty-print "(list 1 2 3 4)")
-(test-pretty-print "((a . 1) (b . 2))")
+(test-pretty-print "\
+((a b)
+ (c d))")
+(test-pretty-print "\
+((a . 1)
+ (b . 2))")
+(test-pretty-print "((a b) c d)")
 (test-pretty-print "(a b c . boom)")
+(test-pretty-print "`(a b . ,c)")
+(test-pretty-print "`(a . ,(list a b c))")
+(test-pretty-print "#(a b c)")
+
+(test-pretty-print "\
+(long-regexp-var-with-backlashes
+ \"[!?;.]\\\\|--\\\\|\\\\w\\\\{3,\\\\}\\\\.\\\\|:[[:blank:]]+\")"
+                   #:max-width 78)
+
+(test-pretty-print "\
+((alist-key . alist-val)
+ (long-regexp-entry-with-backlashes
+  . \"[!?;.]\\\\|--\\\\|\\\\w\\\\{3,\\\\}\\\\.\\\\|:[[:blank:]]+\"))"
+                   #:max-width 78)
+
+(test-pretty-print "\
+(long-variable-name-with-function-value
+ #~long-gexp-with-hash-read-syntax)"
+                         #:max-width 50)
+
 (test-pretty-print "(list 1
                           2
                           3
                           4)"
                    #:long-list 3
                    #:indent 20)
+(test-pretty-print "(1 2 3 4 5)"
+                   #:long-list 5)
+(test-pretty-print "\
+(1
+ 2
+ 3
+ 4
+ 5
+ 6)"
+                   #:long-list 5)
+(test-pretty-print "(single constant)")
+(test-pretty-print "\
+'(list
+  2
+  3
+  4
+  5
+  6)"
+                   #:long-list 5)
+(test-pretty-print "\
+(1
+ 2
+ 3
+ 4
+ 5
+ . 6)"
+                   #:long-list 5)
+(test-pretty-print "\
+(list
+ (initial-list-argument-with-long-element))"
+                   #:max-width 40)
 (test-pretty-print "\
 (list abc
       def)"
@@ -305,8 +377,7 @@ (define-syntax-rule (test-pretty-print/sequence str args ...)
         (lambda _
           #t))))
   ((#:configure-flags flags)
-   `(cons \"--without-any-problem\"
-          ,flags)))")
+   `(cons \"--without-any-problem\" ,flags)))")
 
 (test-pretty-print "\
 (vertical-space one:
@@ -432,4 +503,164 @@ (define-module (foo bar)
 ;; two lines.\n")
                                          def)))))
 
+(test-equal "read-with-comments, Elisp: integer"
+  1
+  (call-with-input-string "1"
+    read-with-comments-elisp))
+
+(test-equal "read-with-comments, Elisp: float"
+  1.0
+  (call-with-input-string "1.0"
+    read-with-comments-elisp))
+
+(test-equal "read-with-comments, Elisp: basic character"
+  #\a
+  (call-with-input-string "?a"
+    read-with-comments-elisp))
+
+(test-equal "read-with-comments, Elisp: control character"
+  #\alarm
+  (call-with-input-string "?\\a"
+    read-with-comments-elisp))
+
+(test-equal "read-with-comments, Elisp: codepoint character"
+  #\x2014
+  (call-with-input-string "?\\u2014"
+    read-with-comments-elisp))
+
+(test-equal "read-with-comments, Elisp: dot notation"
+  (cons 'a 'b)
+  (call-with-input-string "(a . b)"
+    read-with-comments-elisp))
+
+(test-equal "read-with-comments, Elisp: vector"
+  #(a b c)
+  (call-with-input-string "[a b c]"
+    read-with-comments-elisp))
+
+(test-equal "read-with-comments, Elisp: vector with comment"
+  (list->array 1 `(a ,(comment ";comment\n" #t) b c))
+  (call-with-input-string "\
+[a ;comment
+b c]"
+    read-with-comments-elisp))
+
+(test-pretty-print-elisp "?a")
+(test-pretty-print-elisp "?\\a")
+(test-pretty-print-elisp "?à")
+(test-pretty-print-elisp "?\\u2014")
+(test-pretty-print-elisp "224")
+(test-pretty-print-elisp "224.5")
+(test-pretty-print-elisp "-224.5")
+(test-pretty-print-elisp "\"string\"")
+(test-pretty-print-elisp "symbol")
+(test-pretty-print-elisp "'quoted-symbol")
+(test-pretty-print-elisp "symbol\\.with\\,escapes")
+(test-pretty-print-elisp "123non-confusable-symbol")
+(test-pretty-print-elisp "\\123e0")
+(test-pretty-print-elisp ":keyword*")
+
+(test-pretty-print-elisp "(a b c)")
+(test-pretty-print-elisp "(a . b)")
+(test-pretty-print-elisp "(a b . c)")
+(test-pretty-print-elisp "`(a b ,c)")
+(test-pretty-print-elisp "`(a b . ,c)")
+(test-pretty-print-elisp "(a b 'c)")
+
+(test-pretty-print-elisp "\
+(foo arg1
+     #'longer-than
+     arg3 arg4)"
+                         #:max-width 15)
+(test-pretty-print-elisp "\
+(foo
+ (list
+  longer)
+ b c)"
+                         #:max-width 10)
+(test-pretty-print-elisp "\
+(foo
+ #'longer-than
+ arg1 arg2)"
+                         #:max-width 10)
+(test-pretty-print-elisp "\
+(a
+ #'longer-than
+ b . c)"
+                         #:max-width 10)
+
+(test-pretty-print-elisp "\
+(defun foo (x y)
+  ;; Comment
+  (let ((z (+ x y)))
+    (* z z)))")
+
+(test-pretty-print-elisp "[a b c]")
+(test-pretty-print-elisp "\
+[long-symbol
+ b c d]"
+                         #:max-width 10)
+(test-pretty-print-elisp "\
+[(long
+  list xx)
+ b c d]"
+                         #:max-width 10)
+
+(test-pretty-print-elisp "\
+(defun foo ()
+  (dlet ((x '((a b . \"c\"))))
+    x))")
+
+(test-pretty-print-elisp "\
+(defvar foo value)")
+
+(test-pretty-print-elisp "\
+(defvar foo #'foo-function
+  \"Foo function.\")")
+
+(test-pretty-print-elisp "\
+(if (fboundp 'foo-function)
+  (ding)
+  (autoload #'foo-function \"foo\"
+    \"Return foo.\"))")
+
+(test-pretty-print-elisp "\
+(long-variable-name-with-function-value
+ #'long-function-name-with-hash-read-syntax)"
+                         #:max-width 78)
+(test-pretty-print-elisp "\
+(a b ; Comment
+   c
+   d
+
+   e
+   f)"
+                         #:long-list 5)
+
+(test-pretty-print-elisp "\
+[a
+ b ; Comment
+ c
+ d
+
+ e
+ f]"
+                         #:long-list 5)
+
+(test-pretty-print-elisp "\
+(use-package foo
+  :bind ((\"C-c n\" . foo))
+  :custom (foo-bar 'bar)
+  (foo-baz my--baz)
+  :init (ding))"
+                         #:special-forms '((use-package . 1)))
+
+;; Newline after list arguments for special forms
+(test-pretty-print-elisp "\
+(with-current-buffer-window (setq buf
+                                  (get-buffer-create buf-name))
+    (cd-absolute directory)
+    (call-process-shell-command \"ls -l | sort -t _ -k 2\" nil t)
+  (dired-virtual directory))")
+
 (test-end)

base-commit: a33a335c89ce3766e2bd662bffc897bd0da2b9cd
-- 
2.40.1





Acknowledgement sent to fernseed@HIDDEN:
New bug report received and forwarded. Copy sent to , guix-patches@HIDDEN. Full text available.
Report forwarded to , guix-patches@HIDDEN:
bug#64620; Package guix-patches. Full text available.
Please note: This is a static page, with minimal formatting, updated once a day.
Click here to see this page with the latest information and nicer formatting.
Last modified: Mon, 1 Apr 2024 08:30:02 UTC

GNU bug tracking system
Copyright (C) 1999 Darren O. Benham, 1997 nCipher Corporation Ltd, 1994-97 Ian Jackson.