Building a Modern Lisp Machine
Table of Contents
Attention Conservation Notice
A quick overview of a series of programs that I use and how they fit together. Each of these programs will take quite a long time to master, and this is from the point of view of an amateur explorer. If you find any of this interesting, your time would be better spent installing and trying these yourself instead of following my personal setup.
“We're like licorice. Not everybody likes licorice, but the people who like licorice really like licorice.”
- Jerry Garcia
In the above quote, one can easily swap licorice for emacs and lisp. Once you get going, it's often hard to stop trying to turn everything into lisp or trying to integrate every program on your computer into emacs.
In this post, I document the components that make up my current computing system. Think of this as a narrowly focused uses this
Emacs (The Cornerstone)
The most important piece of the system is Emacs. The goal is to have emacs be the controller that puts everything else in motion. I would like to be able to write emacs lisp functions that launch, configure, and use the processes that run the rest of the system.
There is so much content on the web about emacs. If you haven't tried it out, I recommend System Crafter's Emacs from Scratch Series
Emacs is almost an operating system, but since it there isn't a package for that yet, we can defer to GuixSD.
Here I will use Guix (the package manager) and GuixSD (the operating system) to mean GuixSD.
Guix takes a fascinating approach to package management.
- Everything is transactional, immutable, and declarative.
- If you break your system, you simply reboot and load into a previous iteration.
- If you want to try out some software without committing to the installation, you can easily write up a configuration file containing the software you're interested in and load it on demand with guix-shell. Then when you're done, removing the software is as easy as closing the shell.
Guix is named in homage to Guile (the scheme dialect that is used across the OS) and Nix (a different functional package manager with its own distribution (NixOS)). A normal way of interacting with Guix is to use Guile scheme (the language that GuixSD uses for everything) to write and edit configurations that declaratively say how you would like your system to be. Once you have everything written down, you invoke a command from the package manager and it makes your wishes come true.
As a high-level example, here is the outline of my operating system:
Here you can declaratively set users, the bootloader, devices, package servers, and more.
(use-modules (gnu) (gnu packages audio) (gnu packages pulseaudio) (gnu packages version-control) (gnu packages package-management) (nongnu packages linux)) (use-service-modules cups desktop networking ssh xorg) (operating-system (kernel linux) (firmware (list linux-firmware)) (locale "en_US.utf8") (timezone "America/New_York") (keyboard-layout (keyboard-layout "us")) (host-name "neptune") (users (cons* (user-account (name "neptune") (comment "Neptune") (group "users") (home-directory "/home/neptune") (supplementary-groups '("wheel" "netdev" "audio" "video"))) %base-user-accounts)) (packages (append (list (specification->package "nss-certs") (specification->package "git") (specification->package "bluez") (specification->package "bluez-alsa") (specification->package "pulseaudio")) %base-packages)) (services (append (list (service gnome-desktop-service-type) (service openssh-service-type) (bluetooth-service #:auto-enable? #t) (set-xorg-configuration (xorg-configuration (keyboard-layout keyboard-layout)))) (modify-services %desktop-services (guix-service-type config => (guix-configuration (inherit config) (substitute-urls (append (list "https://substitutes.nonguix.org") %default-substitute-urls)) (authorized-keys (append (list (local-file "signing-key.pub")) %default-authorized-guix-keys))))))) (bootloader (bootloader-configuration (bootloader grub-efi-bootloader) (targets (list "/boot/efi")) (keyboard-layout keyboard-layout))) (swap-devices (list (swap-space (target (uuid "uuid-here"))))) (file-systems (cons* (file-system (mount-point "/") (device (uuid "uuid-here" 'ext4)) (type "ext4")) (file-system (mount-point "/boot/efi") (device (uuid "uuid-here" 'fat32)) (type "vfat")) %base-file-systems)))
If I were to change the code here, all I would have to do is run a command and my system would be updated to the exact specification.
Emacs also has a way to interact with Guix. There is a wonderful guix.el emacs package that provides an emacs interface for interacting with packages, services, profiles, and others.
Now we have 2 pieces working together: Emacs (the user-level operating system) and GuixSD (the package manager and operating system).
Things to Try
Here are some things I would like to dig deeper into with Guix:
- having a separate configuration file for each programming language based on what directory I'm in. Something like direnv, but using guix shell
- building a custom channel
- try out Guix Home
- building a base image and then using it to derive specific images for different hardware, akin to inheritance in OOP
- Deploying a cluster of guix boxes
By default, Guix allows the user to select from a range of normal GNU/Linux desktop environments (gnome, xfce, exwm). On this system, I opted to use StumpWM.
StumpWM is a manual tiling window manager that is written in Common Lisp. Like our other two wonderful programs (emacs and guix), it also has a declarative configuration file that allows the user to customize to their heart's content.
As an example, here is my current (sparse) configuration:
(in-package :stumpwm) (init-load-path #p"~/.config/stumpwm/modules/") (stumpwm:add-to-load-path "~/.guix-profile/share/common-lisp/sbcl/stumpwm-stumptray") (stumpwm:add-to-load-path "~/.guix-profile/share/common-lisp/sbcl/stumpwm-swm-gaps") (stumpwm:add-to-load-path "~/.guix-profile/share/common-lisp/sbcl/stumpwm-ttf-fonts") (set-prefix-key (kbd "C-v")) (which-key-mode) ;; key maps (defvar *my-applications-keymap* (let ((m (make-sparse-keymap))) (define-key m (kbd "b") "exec nyxt") (define-key m (kbd "t") "exec alacritty") (define-key m (kbd "g") "exec chromium") (define-key m (kbd "c") "exec gnome-calculator") (define-key m (kbd "f") "exec nautilus") m)) (define-key *root-map* (kbd "a") '*my-applications-keymap*) ;; notifications (setf *mouse-focus-policy* :click) (setq *message-window-gravity* :center) ;; gaps (load-module "swm-gaps") (setf swm-gaps:*inner-gaps-size* 10 swm-gaps:*outer-gaps-size* 5 swm-gaps:*head-gaps-size* 5) (when *initializing* (swm-gaps:toggle-gaps)) ;; desktop wallpaper (run-shell-command "feh --bg-scale ~/Pictures/mountains.jpg")
It also has an emacs package stumpwm-mode that allows the user to run the configuration commands and immediately see the impact on the surrounding window system.
I'm sure there is a huge amount of customization available here, as everything is run on Common Lisp. I haven't been using it long enough to wield those powers (yet). If you know of any good resources for advanced StumpWM usage, please send them my way! (e-mail is in the about page)
Things to Try
Here are some ideas I would like to try with StumpWM:
- set up standard window layout configurations that can be launched with a single keybinding (i.e. a development layout, a study layout, etc)
- create emacs lisp functions that create the window configurations mentioned above, i.e. call stumpwm commands from emacs
- dig deeper into a frame-based workflow with emacs to get the most out of buffer management with stumpwm
- find a way to never use a mouse. Ratpoison is the predecessor of StumpWM, which has the goal of "saying goodbye to the rodent"
Why not EXWM?
EXWM is great! I used it for a while with NixOS as the underlying operating system. Since EXWM is just running a window manager on top of emacs, it is arguably even closer to the goals of this system (to have everything talking and working together). I've chosen StumpWM because:
- there seems to be more of a community around providing contributor modules specifically targeting the window management aspect
- it runs in a separate process (whereas EXWM runs a single process for both window management and emacs)
- I hadn't tried it before and it looked neat
One of the big perks for EXWM is that window management is as simple as buffer management. If you want to run a browser in a buffer, you can do it. With StumpWM, you can also replicate emacs' basic buffer behavior, but it takes some effort. The way I found to do that is to switch to a frame-based workflow, in which each new buffer spawns a new frame (a window outside of emacs). Then you can handle all those windows with stumpwm.
I tried this out but didn't go the extra mile to get used to it. These days I usually just have 2 windows open: emacs and a browser. In emacs I use the standard buffer management tools to split my windows and have different key bindings for traversing buffers than what I use in StumpWM. When I want to jump to the browser, I use stump's key bindings to move back and forth. This will likely change in the future as I slowly begin tweaking my workflow to incorporate more StumpWM.
For a browser, I use Nyxt. Like StumpWM, Nyxt also:
- is written in Common Lisp
- provides access to the process for customization while it is running.
- has a configuration file written in Common Lisp
- allows the user to write functions and have complete control over the running components
I haven't updated my configuration file yet, but I plan to once I am more familiar. Here is a snippet from the Nyxt docs in which a custom keymap is created:
(defvar *my-keymap* (make-keymap "my-map")) (define-key *my-keymap* "C-f" 'Nyxt/web-mode:history-forwards "C-b" 'Nyxt/web-mode:history-backwards) (define-mode my-mode () "Dummy mode for the custom key bindings in `*my-keymap*'." ((keymap-scheme (keymap:make-scheme scheme:cua *my-keymap* scheme:emacs *my-keymap* scheme:vi-normal *my-keymap*)))) (define-configuration (buffer web-buffer) ((default-modes (append '(my-mode) %slot-default%))))
Nyxt provides some nice emacs keybindings which can be turned on in the settings area, making the learning curve small if you are used to emacs. Overall, the browser feels exactly like what you would expect an emacs browser to feel like:
- It has buffers and a modeline that allows you to switch between them (C-x b). There are no visual tabs!
- This is neat, but it leads to having a lot of windows open at a time without intending to. If you're like me and don't close tabs often, you might find yourself having several hundred tabs open without meaning to.
- You can find windows by fuzzy matching text in the buffer! I find this way more efficient and effective than graphically looking at a bunch of tabs.
- It has a lot of functions that can be accessed at any time (C-SPC in Nyxt is very similar to M-x).
- It allows the user to jump around a web page using only the keyboard (much like Avy in emacs, C-j in Nyxt)
I haven't explored controlling Nyxt with emacs yet, but it looks like there is some fruitful exploration going on already
Things to Try
Here are some things I want to explore:
- it has a hook system, which should allow me to launch events when certain things happen
- it has a handful of automation utilities. I would love to be able to do things like web scraping without writing a small script to do so (similar to emacs recording macros)
- it has an interface for password managers
- it has a community that adds extensions. I'll have to dig around here sometime
What does everything look like together?
Open it in a new window in order to view the full-size