[bug#77352] home: services: define hyprland home service

Message ID 20250328232510.32282-2-email@cmargiotta.net
State New
Headers
Series [bug#77352] home: services: define hyprland home service |

Commit Message

Carmine Margiotta March 28, 2025, 11:25 p.m. UTC
  ---
 gnu/home/services/hyprland.scm | 490 +++++++++++++++++++++++++++++++++
 1 file changed, 490 insertions(+)
 create mode 100644 gnu/home/services/hyprland.scm


base-commit: b282b5ecd90da5dc3a034e7064ac41808f7f6c24
  

Comments

Andrew Wong March 29, 2025, 3:37 p.m. UTC | #1
Very cool patch! Would it be possible to add functionality for loading 
plugins on startup? I have a pending patch series[1] that packages 
several Hyprland plugins, and it would be nice to be able to enter a 
list of plugin packages to be loaded directly in the config so they 
could be "dependencies" of the service.

Also, how does this cooperate with session managers like the default 
GDM, which launches wayland from its .desktop entry in 
/run/current-system/profile/share/wayland-sessions[2]? If it only 
generates a configuration (i.e. doesn't start the Hyprland process), 
would it be possible to define services that start and run only while 
Hyprland runs? (this would be distinct from what "exec" lines do in that 
the packages are then pulled into the profile by the service, letting 
Guix "understand" the dependency)

The default settings should be the same as Hyprland's auto-generated 
config[3], unless there's some guix-specific requirement. This is 
because users will want to be able to easily transfer the config they 
already use, and to be able to use the documentation and advice that 
already exists that assumes new users have the autogenerated config. 
Users should not have to do extra configuration to get a consistent 
default experience.

To name a few issues, having `extra-config` defined by default would 
require users to go out of their way to use what would otherwise be a 
"last-resort" feature in order to get the default experience. Also, I 
know from experience that settings vrr to anything but 0 can cause some 
monitors to go black unless the vrr matches their (fixed) refresh rate; 
vrr is not a universal feature, the ability for the computer to control 
the monitor's vrr setting even less so.

Perhaps the service could straight-up copy or parse-in the file (which 
would make transitioning from a "traditional" config file easy, too!) 
from the Hyprland package itself to reduce the need for Guix to maintain 
it every time it changes. Off the top of my head, the cheapest solution 
to the problem would be to hardcode the default config's checksum, and 
print an alert asking for maintenance when it changes.

Finally, you should check over your patch with regards to Guix' coding 
style conventions[4]. Most conspicuously, lines should be 80 characters 
wide at maximum. Also, when updating a patch, you should send a v2 
instead of creating a new bug number.

Your patch shows a lot of promise, and it's great to see someone get the 
ball rolling on something I've only been daydreaming about for a while 
now. If you'd like help on any specific part, LMK!

- Andrew Wong

[1] bug#76910
[2] 
https://guix.gnu.org/manual/devel/en/html_node/X-Window.html#index-gdm_002dservice_002dtype
[3] less $(guix build hyprland)/share/hypr/hyprland.conf
[4] https://guix.gnu.org/manual/en/html_node/Formatting-Code.html
  
Carmine Margiotta March 29, 2025, 5:39 p.m. UTC | #2
First of all, I apologize in advance for my mistakes, but this workflow
is completely new to me!

On 3/29/25 4:37 PM, Andrew Wong wrote:
 > Very cool patch! Would it be possible to add functionality for loading
 > plugins on startup? I have a pending patch series[1] that packages
 > several Hyprland plugins, and it would be nice to be able to enter a
 > list of plugin packages to be loaded directly in the config so they
 > could be "dependencies" of the service.

This is a really interesting idea! I am not  very familiar with Hyprland
plugins - is there currently a declarative way to manage them? The only
method I know is using hyprctl.

 > Also, how does this cooperate with session managers like the default
 > GDM, which launches wayland from its .desktop entry in
 > /run/current-system/profile/share/wayland-sessions[2]? If it only
 > generates a configuration (i.e. doesn't start the Hyprland process),
 > would it be possible to define services that start and run only while
 > Hyprland runs? (this would be distinct from what "exec" lines do in that
 > the packages are then pulled into the profile by the service, letting
 > Guix "understand" the dependency)

This does not interact with session managers at all, it only generates a
configuration without starting the process itself.
For services that should run only while Hyprland is active, would this
require managing Hyprland itself via shepherd? (I am still new to Guix,
I might be mistaken here!)

 > The default settings should be the same as Hyprland's auto-generated
 > config[3], unless there's some guix-specific requirement. This is
 > because users will want to be able to easily transfer the config they
 > already use, and to be able to use the documentation and advice that
 > already exists that assumes new users have the autogenerated config.
 > Users should not have to do extra configuration to get a consistent
 > default experience.

That makes perfect sense, I will fix that asap.

 > To name a few issues, having `extra-config` defined by default would
 > require users to go out of their way to use what would otherwise be a
 > "last-resort" feature in order to get the default experience.

You raise a valid point, I will try to redesign this to implement
submaps in a more coherent way.

 > Perhaps the service could straight-up copy or parse-in the file (which
 > would make transitioning from a "traditional" config file easy, too!)
 > from the Hyprland package itself to reduce the need for Guix to maintain
 > it every time it changes. Off the top of my head, the cheapest solution
 > to the problem would be to hardcode the default config's checksum, and
 > print an alert asking for maintenance when it changes.

Your cheapest solution is good!

 > Finally, you should check over your patch with regards to Guix' coding
 > style conventions[4]. Most conspicuously, lines should be 80 characters
 > wide at maximum.

Thank you for pointing this out. I did run guix style hyprland.scm
--whole-file before submitting, but I see now that it doesn't catch line
length violations. Could you recommend any additional linting tools that
could help verify style compliance more thoroughly? I will be more
careful next time, thank you!

 > Your patch shows a lot of promise, and it's great to see someone get the
 > ball rolling on something I've only been daydreaming about for a while
 > now. If you'd like help on any specific part, LMK!

Thank you so much for your kind words!

- Carmine Margiotta
  
Andrew Wong March 30, 2025, 2:06 a.m. UTC | #3
On 3/29/25 1:39p, Carmine Margiotta wrote:
> First of all, I apologize in advance for my mistakes, but this 
> workflow is completely new to me!

No worries! Everyone starts somewhere.

> This is a really interesting idea! I am not  very familiar with 
> Hyprland plugins - is there currently a declarative way to manage 
> them? The only method I know is using hyprctl.

I've been managing them declaratively by making `exec-once` calls to
`hyprctl plugin load`, so maybe the service could use those. Another 
possiblity is fabricating hyprpm's state.[1]

> This does not interact with session managers at all, it only 
> generates a configuration without starting the process itself. For 
> services that should run only while Hyprland is active, would this 
> require managing Hyprland itself via shepherd? (I am still new to 
> Guix, I might be mistaken here!)

That's what I was wondering myself. Maybe it's possible to modify or
wrap hyprland's .desktop entry to encapsulate the whole thing in a
service? Like a hyprland.desktop that actually starts a service that
runs the REAL hyprland.desktop session. That way you could construct
your DE out of services configured in guix itself.

> Could you recommend any additional linting tools that could help 
> verify style compliance more thoroughly? I will be more careful next 
> time, thank you!

Are you editing using emacs? If so, you can use display-fill-column-mode
to see the column limit. Otherwise, just skim through the style guide
linked by the manual. A motion I do often after some heavy editing is
typing C-M-u a few times to move a few scopes up, C-M-space to select
the while s-expression, and then C-M-q to re-indent the region.

  [1] https://wiki.hyprland.org/Plugins/Using-Plugins/#hyprpm
  

Patch

diff --git a/gnu/home/services/hyprland.scm b/gnu/home/services/hyprland.scm
new file mode 100644
index 0000000000..f024f272d7
--- /dev/null
+++ b/gnu/home/services/hyprland.scm
@@ -0,0 +1,490 @@ 
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2025 Carmine Margiotta <email@cmargiotta.net>
+;;;
+;;; 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 hyprland)
+  #:use-module (gnu packages wm)
+  #:use-module (guix gexp)
+  #:use-module (gnu services configuration)
+  #:use-module (gnu home services)
+  #:use-module (guix packages)
+  #:use-module (ice-9 match)
+  #:use-module (srfi srfi-1)
+
+  #:export (hyprland-extension hyprland-configuration binding monitor
+                               home-hyprland-service-type))
+
+;;; Commentary:
+;;;
+;;; A Guix Home service to configure Hyprland, an highly customizabile dynamic tiling Wayland compositor
+;;;
+;;; Code:
+
+;;;
+;;; Helper functions.
+;;;
+
+;; Repeat v n times
+(define (repeat v n)
+  (if (eq? n 0)
+      '()
+      `(,v ,@(repeat v
+                     (- n 1)))))
+
+;; Generate an indenter string of n tabs
+(define (indent tabs)
+  (if (<= tabs 0) ""
+      (apply string-append
+             (repeat "\t" tabs))))
+
+(define (flatten lst)
+  (let loop
+    ((lst lst)
+     (acc '()))
+    (cond
+      ((null? lst)
+       acc)
+      ((pair? lst)
+       (loop (car lst)
+             (loop (cdr lst) acc)))
+      (else (cons lst acc)))))
+
+;;;
+;;; Definition of configurations.
+;;;
+
+;; Entry inside a 'block' configuration
+;; allowed formats: (symbol string) (symbol number) (symbol boolean) (symbol block-entries)
+;; A block entry can contain a list of block entries, effectively allowing nested blocks
+(define (block-entry? data)
+  (or (null? data)
+      (match data
+        (((? symbol?)
+          (or (? string?)
+              (? number?)
+              (? boolean?)
+              (? block-entries?)))
+         #t))))
+
+;; List of block entries
+(define (block-entries? data)
+  (every block-entry? data))
+
+;; An executable (a target for the exec action) can be a string or a gexp
+(define (executable? value)
+  (or (string? value)
+      (gexp? value)))
+
+;; A list of valid executables
+(define (executable-list? values)
+  (every executable? values))
+
+;; Block sub-configuration (a container of block entries)
+(define-configuration block
+  (entries (block-entries '()) "Block entries"
+           (serializer (lambda (name value)
+                         (serialize-block-entries name value 1)))))
+
+;; Monitor sub-configuration
+(define-configuration monitor
+  (name (string "") "Monitor's name"
+        (serializer (lambda (_ n)
+                      (string-append "monitor = " n ", "))))
+  (resolution (string "preferred") "Monitor's resolution"
+              (serializer (lambda (_ n)
+                            (string-append n ", "))))
+  (position (string "auto") "Monitor's position"
+            (serializer (lambda (_ n)
+                          (string-append n ", "))))
+  (scale (string "1") "Monitor's scale"
+         (serializer (lambda (_ n)
+                       n)))
+  (transform (string "") "Monitor's scale"
+             (serializer (lambda (_ n)
+                           (if (string-null? n) "\n"
+                               (string-append ", transform, " n "\n"))))))
+
+;; List of monitors definition
+(define (monitors? arg)
+  (every monitor? arg))
+
+;; List of strings
+(define (string-list? arg)
+  (every string? arg))
+
+;; Binding sub-configuration
+(define-configuration binding
+  (flags (string "")
+         "Bind flags (https://wiki.hyprland.org/Configuring/Binds/)"
+         (serializer (lambda (_ n)
+                       (string-append "bind" n " = "))))
+  (mod (string "$mod") "Mod key"
+       (serializer (lambda (_ n)
+                     n)))
+  (shift? (boolean #f) "If mod is shifted"
+          (serializer (lambda (_ n)
+                        (string-append (if n " SHIFT" "") ", "))))
+  (key (string) "Binding main key"
+       (serializer (lambda (_ n)
+                     (string-append n ", "))))
+  (action (string "exec") "Binding action"
+          (serializer (lambda (_ n)
+                        n)))
+  (args (executable "") "Binding action's args"
+        (serializer (lambda (name value)
+                      (if (string? value)
+                          (if (string-null? value) "\n"
+                              (string-append ", " value "\n"))
+                          #~(string-append ", "
+                                           #$(serialize-executable name value)
+                                           "\n"))))))
+
+;; List of bindings
+(define (binding-list? value)
+  (every binding? value))
+
+;; Optional string
+(define-maybe/no-serialization string)
+
+;; Binding block sub-configuration
+(define-configuration bindings
+  (main-mod (maybe-string "") "Main mod bound to $mod"
+            (serializer (lambda (_ n)
+                          (string-append "\n$mod = " n "\n\n"))))
+  (binds (binding-list '()) "Bindings"
+         (serializer (lambda (_ n)
+                       #~(string-append #$@(map (lambda (b)
+                                                  (serialize-configuration b
+                                                   binding-fields)) n))))))
+
+;;;
+;;; Serialization functions.
+;;;
+
+(define (serialize-block name block)
+  #~(string-append #$(symbol->string name) " {\n"
+                   #$(if (null? block) ""
+                         (serialize-configuration block block-fields)) "\n}\n"))
+
+;; A block entry will be serialized as an indented hyprlang
+;; statement, nested blocks are allowed
+(define (serialize-block-entry value tabs)
+  (string-append (or (match value
+                       (() "")
+                       (((? symbol? name)
+                         value)
+                        (string-append (indent tabs)
+                                       (symbol->string name)
+                                       (match value
+                                         ((? string? v)
+                                          (string-append " = " v))
+                                         ((? number? v)
+                                          (string-append " = "
+                                                         (number->string v)))
+                                         ((? boolean? v)
+                                          (if v " = true" " = false"))
+                                         ((? block-entries? v)
+                                          (string-append " {\n"
+                                                         (serialize-block-entries
+                                                          #f v
+                                                          (+ tabs 1))
+                                                         (indent tabs) "}")))
+                                       "\n"))
+                       ((_)
+                        #f)) "\n")))
+
+;; String lists will be serialized as name = value\n
+(define (serialize-string-list name values)
+  (apply string-append
+         (map (lambda (w)
+                (string-append (symbol->string name) " = " w "\n")) values)))
+
+;; Gexp executables will be serialized on a program-file
+(define (serialize-executable name value)
+  (if (string? value) value
+      (program-file (symbol->string name) value
+                    #:module-path %load-path)))
+
+;; Lists serializers
+(define (serialize-block-entries _ entries level)
+  (apply string-append
+         (map (lambda (e)
+                (serialize-block-entry e level)) entries)))
+
+(define (serialize-monitors _ monitors)
+  #~(string-append #$@(map (lambda (m)
+                             (serialize-configuration m monitor-fields))
+                           monitors)))
+
+(define (serialize-executable-list name values)
+  #~(apply string-append
+           (map (lambda (w)
+                  (string-append #$(symbol->string name) " = " w "\n"))
+                '#$(map (lambda (v)
+                          (serialize-executable name v)) values))))
+
+;; Hyprland full configuration
+(define-configuration hyprland-configuration
+  (package
+    (package
+      hyprland) "Hyprland package to use"
+    (serializer (λ (_ n) "")))
+  (monitors (monitors (list (monitor))) "Monitors definition")
+  (exec-once (executable-list '()) "Command to exec once")
+  (exec (executable-list '()) "Command to automatically exec")
+  (general (block (block)) "General configuration variables")
+  (decoration (block (block)) "Decoration configuration variables")
+  (animations (block (block)) "Animation configuration variables")
+  (workspace (string-list '()) "Workspaces settings")
+  (windowrule (string-list '()) "Window rules (v2)")
+  (dwindle (block (block)) "Dwindle layout settings")
+  (master (block (block)) "Master layout settings")
+  (misc (block (block)) "Misc settings")
+  (input (block (block)) "Input settings")
+  (gestures (block (block)) "Gestures settings")
+  (bindings (bindings (bindings)) "Bindings configuration"
+            (serializer (λ (_ n)
+                           (serialize-configuration n bindings-fields))))
+  (extra-config (string "") "Extra config"
+                (serializer (λ (_ n)
+                               (string-append n "\n")))))
+
+;; Hyprland configuration extension for other services
+;; External services can add new exec entries or new bindings
+(define-configuration hyprland-extension
+  (exec-once (executable-list '())
+             "Commands to be executed with hyprland once")
+  (exec (executable-list '()) "Commands to be executed with hyprland")
+  (bindings (binding-list '()) "Extra binds")
+  (no-serialization))
+
+;;;
+;;; Default settings and useful constants.
+;;;
+(define-public %default-hyprland-general
+  (block (entries '((gaps_in 5)
+                    (gaps_out 20)
+                    (border_size 2)
+                    (col.active_border "rgba(33ccffee) rgba(00ff99ee) 45deg")
+                    (col.inactive_border "rgba(595959aa)")
+                    (resize_on_border #f)
+                    (allow_tearing #f)
+                    (layout "dwindle")))))
+
+(define-public %default-hyprland-decoration
+  (block (entries '((rounding 10)
+                    (rounding_power 2)
+                    (active_opacity 1.0)
+                    (inactive_opacity 0.9)
+                    (dim_inactive #t)
+                    (dim_strength 0.05)
+
+                    (shadow ((enabled #t)
+                             (range 4)
+                             (render_power 3)
+                             (color "rgba(1a1a1aee)")))
+                    (blur ((enabled #t)
+                           (size 3)
+                           (passes 1)
+                           (vibrancy 0.1696)))))))
+
+(define-public %default-hyprland-animations
+  (block (entries '((enabled #t)
+                    (bezier "easeOutQuint,0.23,1,0.32,1")
+                    (bezier "easeInOutCubic,0.65,0.05,0.36,1")
+                    (bezier "linear,0,0,1,1")
+                    (bezier "almostLinear,0.5,0.5,0.75,1.0")
+                    (bezier "quick,0.15,0,0.1,1")
+
+                    (animation "global, 1, 10, default")
+                    (animation "border, 1, 5.39, easeOutQuint")
+                    (animation "windows, 1, 4.79, easeOutQuint")
+                    (animation "windowsIn, 1, 4.1, easeOutQuint, popin 87%")
+                    (animation "windowsOut, 1, 1.49, linear, popin 87%")
+                    (animation "fadeIn, 1, 1.73, almostLinear")
+                    (animation "fadeOut, 1, 1.46, almostLinear")
+                    (animation "fade, 1, 3.03, quick")
+                    (animation "layers, 1, 3.81, easeOutQuint")
+                    (animation "layersIn, 1, 4, easeOutQuint, fade")
+                    (animation "layersOut, 1, 1.5, linear, fade")
+                    (animation "fadeLayersIn, 1, 1.79, almostLinear")
+                    (animation "fadeLayersOut, 1, 1.39, almostLinear")
+                    (animation "workspaces, 1, 1.94, almostLinear, fade")
+                    (animation "workspacesIn, 1, 1.21, almostLinear, fade")
+                    (animation "workspacesOut, 1, 1.94, almostLinear, fade")))))
+
+(define-public %default-hyprland-workspace
+  '("w[tv1], gapsout:0, gapsin:0" "f[1], gapsout:0, gapsin:0"))
+
+(define-public %default-hyprland-windowrule
+  '("bordersize 0, floating:0, onworkspace:w[tv1]"
+    "rounding 0, floating:0, onworkspace:w[tv1]"
+    "bordersize 0, floating:0, onworkspace:f[1]"
+    "rounding 0, floating:0, onworkspace:f[1]"))
+
+(define-public %default-hyprland-misc
+  (block (entries '((force_default_wallpaper -1)
+                    (disable_hyprland_logo #f)
+                    (enable_swallow #t)
+                    (vrr 2)))))
+
+(define-public %default-hyprland-gestures
+  (block (entries '((workspace_swipe #t)))))
+
+(define-public %default-hyprland-bindings
+  (bindings (main-mod "SUPER")
+            (binds `(,(binding (key "Q")
+                               (action "killactive")) ,(binding (shift? #t)
+                                                                (key "F")
+                                                                (action
+                                                                 "togglefloating"))
+                     ,(binding (key "F")
+                               (action "fullscreen"))
+                     ,(binding (shift? #t)
+                               (key "R")
+                               (action "exec")
+                               (args "hyprctl reload"))
+                     ;; Dwindle layout
+                     ,(binding (key "P")
+                               (action "pseudo"))
+                     ,(binding (key "J")
+                               (action "togglesplit"))
+                     ;; Move focus with arrow keys
+                     ,(binding (key "left")
+                               (action "movefocus")
+                               (args "l"))
+                     ,(binding (key "right")
+                               (action "movefocus")
+                               (args "r"))
+                     ,(binding (key "up")
+                               (action "movefocus")
+                               (args "u"))
+                     ,(binding (key "down")
+                               (action "movefocus")
+                               (args "d"))
+                     ;; Switch workspaces
+                     ,@(map (lambda (index)
+                              (binding (key (number->string index))
+                                       (action "workspace")
+                                       (args (number->string index))))
+                            (iota 10))
+                     ;; Move active window to workspace
+                     ,@(map (lambda (index)
+                              (binding (shift? #t)
+                                       (key (number->string index))
+                                       (action "movetoworkspace")
+                                       (args (number->string index))))
+                            (iota 10))
+                     ;; Move/resize with mouse
+                     ,(binding (flags "m")
+                               (key "mouse:272")
+                               (action "movewindow"))
+                     ,(binding (flags "m")
+                               (key "mouse:273")
+                               (action "resizewindow"))
+                     ,(binding (key "R")
+                               (action "submap")
+                               (args "resize"))))))
+
+(define-public %hyprland-resize-submap
+  "submap = resize
+binde  = ,right,  resizeactive,  10  0
+binde  = ,left,   resizeactive, -10  0
+binde  = ,up,     resizeactive,  0  -10
+binde  = ,down,   resizeactive,  0   10
+bind   = ,escape, submap, reset
+submap = reset
+")
+
+(define-public %default-hyprland-configuration
+  (hyprland-configuration (general %default-hyprland-general)
+                          (decoration %default-hyprland-decoration)
+                          (animations %default-hyprland-animations)
+                          (workspace %default-hyprland-workspace)
+                          (windowrule %default-hyprland-windowrule)
+                          (misc %default-hyprland-misc)
+                          (gestures %default-hyprland-gestures)
+                          (bindings %default-hyprland-bindings)
+                          (extra-config %hyprland-resize-submap)))
+
+;;;
+;;; Useful scripts
+;;;
+
+;; Reload the first instance of hyprland, to
+;; automatically load the new configuration
+(define (hyprland-reload config)
+  #~(begin
+      (display "Reloading hyprland configuration...")
+      (system* #$(file-append (hyprland-configuration-package config)
+                              "/bin/hyprctl") "--instance" "0" "reload")))
+
+;;;
+;;; Definition of the Home Service.
+;;;
+
+(define-public home-hyprland-service-type
+  (service-type (name 'home-hyprland-config)
+                (description "Configure Sway by providing a file
+@file{~/.config/hypr/hyprland.conf}.")
+                (compose (λ (extensions)
+                            (hyprland-extension (exec-once (flatten (map
+                                                                     hyprland-extension-exec-once
+                                                                     extensions)))
+                                                (exec (flatten (map
+                                                                hyprland-extension-exec
+                                                                extensions)))
+                                                (bindings (flatten (map
+                                                                    hyprland-extension-bindings
+                                                                    extensions))))))
+                (extend (λ (config rules)
+                           (hyprland-configuration (inherit config)
+                                                   (exec-once (append (hyprland-configuration-exec-once
+                                                                       config)
+                                                                      (hyprland-extension-exec-once
+                                                                       rules)))
+                                                   (exec (append (hyprland-configuration-exec
+                                                                  config)
+                                                                 (hyprland-extension-exec
+                                                                  rules)))
+                                                   (bindings (bindings (inherit
+                                                                        (hyprland-configuration-bindings
+                                                                         config))
+                                                                       (binds (append
+                                                                               (bindings-binds
+                                                                                (hyprland-configuration-bindings
+                                                                                 config))
+                                                                               
+                                                                               (hyprland-extension-bindings
+                                                                                rules))))))))
+                (extensions (list (service-extension
+                                   home-activation-service-type
+                                   ;; Trigger hyprctl reload after a new config has been applied
+                                   hyprland-reload)
+                                  (service-extension home-profile-service-type
+                                   (λ (config)
+                                      `(,(hyprland-configuration-package
+                                          config))))
+                                  (service-extension
+                                   home-xdg-configuration-files-service-type
+                                   (λ (c)
+                                      `(("hypr/hyprland.conf" ,(mixed-text-file
+                                                                "hyprland-cfg"
+                                                                (serialize-configuration
+                                                                 c
+                                                                 hyprland-configuration-fields))))))))
+                (default-value %default-hyprland-configuration)))