Guix Ricing Epicness

23 April 2024 | programming guix guile

I was ricing my guix, as one does naturally, and I stumbled upon 733T hacks leading to a flash of inspiration. What follows is a blow-by-blow account of this adventure.

Hold on tight!

Bash, My Old Friend

First, I found out that there is a home-bash-service-type in the guix manual. It occurred to me that I could rid of zsh, coz bash purism. So, I configured it like so:

(service home-bash-service-type
         (home-bash-configuration
          (guix-defaults? #t)
          (aliases '(("feed" . "nohup mpv --playlist=~/playlist.m3u &")
                     ("gparted" . "sudo -EH gparted")
                     ("rm" . "rm -i")
                     ("cp" . "cp -i")
                     ("mv" . "mv -i")
                     (".." . "cd ..")
                     ("du" . "du -kh")
                     ("df" . "df -kTh")
                     ))
          (bashrc (list
                   (local-file "./.bashrc" "bashrc")
                   (mixed-text-file "blesh-init"
                                    "[[ $- == *i* ]] && source "
                                    blesh
                                    "/share/blesh/ble.sh --noattach
   ")
                         ;;;  blesh with init config file
                   ;;                       (mixed-text-file "blesh-init"
                   ;;                                        "[[ $- == *i* ]] && source "
                   ;;                                        blesh
                   ;;                                        "/share/blesh/ble.sh --noattach --rcfile /home/kaizer/.config/blesh/init.sh
                   ;; ")
                   (mixed-text-file "liquidprompt"
                                    "[[ $- = *i* ]] && source "
                                    liquidprompt
                                    "/share/liquidprompt/liquidprompt
   ")
                   (mixed-text-file "powerline-theme"
                                    "source "
                                    liquidprompt "/share/liquidprompt/themes/powerline/powerline.theme
   ")
                   (mixed-text-file "blesh-exit"
                                    "[[ ${BLE_VERSION-} ]] && ble-attach")))))

There are a few things to note:

  1. The aliases go to the top of the generated ~/.bashrc as well as the defaults.
    • This might be important if you anything is being initialized before your aliases or before bash is configured and runs.
  2. This argument here: provides the rest of the ~/.bashrc:

    (local-file "./.bashrc" "bashrc")
    
    
  3. Here we load some plugins and we can see the power of scheme!
    • Guix stores packages in the store so the path to the package looks sth like /gnu/store/<hash>/<pkg>.
    • If we try to load the program directly, using the path in our ~/.bashrc then we will run into 2 problems:
      • The path changes every time we re-install package and this can happen when removing old package, system or home generations and clearing the cache.
      • The path is inconvenient to type and find.
    • Thus, we use the package symbol (the package name) to refer to it while in guile land.
(mixed-text-file "blesh-init"
                 "[[ $- == *i* ]] && source "
                 blesh
                 "/share/blesh/ble.sh --noattach
   ")

;;  blesh with init config file
;; (mixed-text-file "blesh-init"
;;                  "[[ $- == *i* ]] && source "
;;                  blesh
;;                  "/share/blesh/ble.sh --noattach --rcfile /home/kaizer/.config/blesh/init.sh
;; ")

(mixed-text-file "liquidprompt"
                 "[[ $- = *i* ]] && source "
                 liquidprompt
                 "/share/liquidprompt/liquidprompt
   ")

(mixed-text-file "powerline-theme"
                 "source "
                 liquidprompt "/share/liquidprompt/themes/powerline/powerline.theme
   ")

(mixed-text-file "blesh-exit"
                 "[[ ${BLE_VERSION-} ]] && ble-attach")

Config Filez, Simply

Guix home also provides a home-xdg-configuration-files-service-type such that you can provide files to be stored in $XDG_CONFIG_HOME.

Why would you want to do this? It seems pretty basic.

There are 2 main reasons:

So, simply, the configuration is as follows:

(service home-xdg-configuration-files-service-type
         `(("alacritty/alacritty.toml" ,(local-file "./alacritty.toml"))
           ("tmux/tmux.conf" ,(local-file "./tmux.conf"))
           ("beets/config.yaml" ,(local-file "./beets-config.yaml"))
           ("mako/config" ,(local-file "./mako-config"))
           ("waybar/config" ,(local-file "./waybar-config"))
           ("waybar/style.css" ,(local-file "./waybar-style.css"))
           ("sway/config" ,(local-file "./sway-greetd.conf"))
           ;; ("blesh/init.sh" ,(local-file "./blesh-init.sh"))
           ;; ("liquidpromptrc" ,(local-file "./liquidpromptrc")
           ("pantalaimon/pantalaimon.conf" ,(local-file "./pantalaimon.conf"))))

Epicness 🔥

There is a guix system service that allows you to reload your home configuration such that you do not have to every time you make a change to the system.

Services, at Your Behest, Majesty

We can use services to further define specific system behavior either by running certain commands on startup, daemonizing (wrapping them up in shepherd) them or through mcron.

For example, if we want a certain command to run on the system-level on a schedule, we can use the mcron-service-type to create a simple-service that we will provide to our os-level config like so:

(simple-service 'my-cron-jobs
                mcron-service-type
                (list
                 ;; garbage-collector-job ; FIXME: messes with guix home so constant reinstalls
                 ;; updatedb-job
                 idutils-job))

We can then define the services like so:

;; superceded by file-database-service-type
;; (define updatedb-job
;;   ;; Run 'updatedb' at 3AM every day.  Here we write the
;;   ;; job's action as a Scheme procedure.
;;   #~(job '(next-hour '(3))
;;          (lambda ()
;;            (system* (string-append #$findutils "/bin/updatedb")
;;                     "--prunepaths=/tmp /var/tmp /gnu/store"))
;;          "updatedb"))

(define garbage-collector-job
  ;; Collect garbage 90 minutes after midnight every day.
  ;; The job's action is a shell command.
  #~(job "30 0 * * 1"                    ;Vixie cron syntax
         "guix gc -F 1G"))

(define idutils-job
  ;; Update the index database as user "kaizer" at 12:15PM
  ;; and 19:15PM.  This runs from the user's home directory.
  #~(job '(next-minute-from (next-hour '(12 19)) '(15))
         (string-append #$idutils "/bin/mkid src")
         #:user "kaizer"))

This uses the job command to create this service which you can read about here. Suffice to say what is most important is grokking the Vixie cron syntax.

On a user level, we have two options; either we use shepherd or mcron.

If our command does not need to be scheduled then shepherd is the best option because it will allow us to start, stop, enable and disable the service on demand.

A simple shepherd configuration, located at $XDG_CONFIG_HOME/shepherd/init.scm, can look like this:

;; Shepherd User Services
(load "/home/kaizer/.config/shepherd/services.scm")

(register-services
 emacs
 ;; wallpaper ; NOTE: launched with mcron atm
 mcron
 ;; mpd
 ;; gpg-agent
 ;; jackd
 ;; ibus-daemon
 ;; workrave
 )

;; Send shepherd into the background.
(action 'shepherd 'daemonize)

;; Services to start when shepherd starts:
(for-each start '(emacs
                  ;; wallpaper ; NOTE: launched with mcron atm
                  mcron
                  ;; mpd
                  ;; gpg-agent
                  ;; ibus-daemon
                  ;; workrave
                  ))

The accompanying services file:

(define emacs
  (make <service>
    #:provides '(emacs)
    ;; #:requires '()
    #:start (make-system-constructor "emacs --daemon")
    #:stop (make-system-destructor "emacsclient --eval \"(kill-emacs)\"")))

(define mpd
  (make <service>
    #:provides '(mpd)
    #:respawn? #t
    #:start (make-forkexec-constructor '("mpd" "--no-daemon"))
    #:stop (make-kill-destructor)))

(define wallpaper
  (make <service>
    #:provides '(wallpaper)
    #:respawn? #t
    #:start (make-forkexec-constructor '("random-sway-wallpaper.sh >/dev/null 2>&1"))
    #:stop (make-kill-destructor)))

(define mcron
  (service
   '(mcron)
   ;; Run /usr/bin/mcron without any command-line arguments.
   #:start (make-forkexec-constructor '("mcron"))
   #:stop (make-kill-destructor)
   #:respawn? #t))

Finally, we can specify the jobs to be run by mcron like so:

(job '(next-minute-from (current-time) (range 0 60 15))
 "random-sway-wallpaper.sh &> /dev/null")

The file is located in $XDG_CONFIG_HOME/cron/<name>.guile, where <name> is the name of the job. One job per file.

Recapitulation

We have seen how to use Guix to manage .config files as well as how to specify services to run certain programs either on startup or on timed loops with mcron.

The beauty is that we used scheme for everything and in a deterministic manner.

All we need to do is check the config into version control and boom, we are done. No more ricing from scratch.

Respect the Guix.

Addendum

I figured I might as well share the random-sway-wallpaper.sh script. Here you go:

#!/bin/sh

# I am running sway so I set the bg with swaybg there for the startup bg
# We get that PID here so we can kill it and launch swaybg again
PID=$(pidof swaybg)

kill "$PID"

# we launch swaybg with a random .jpg wallpaper from our collection
swaybg -i "$(find "$HOME"/Pictures/Wallpapers/*.jpg -type f | shuf -n1)" -m fill &

Another bonus, say we wanted to get dad jokes every hour or so, we can create a job like so in $XDG_CONFIG_HOME/cron/jokes.guile:

(job '(next-hour-from (current-time) (range 0 24 1))
 "jokes.sh")

And the script could be:

#!/bin/sh

# we use the api from icanhazdadjoke.com to get a random dad joke on demand!
joke=$(curl -H "Accept: text/plain" https://icanhazdadjoke.com/)

# send it as a notification
notify-send "$joke"

Did you hear about the bread factory burning down? They say the business is toast.

Did you hear about the submarine industry? It really took a dive…