My Emacs Bootstrap

Table of Contents

A literate programming file for bootstraping my Emacs Configuration.

Introduction

This file contains all the variable definitions and library loading for the other files in my project.

I’m installing everything using the straight.el for package installation and management. This is initialization code configured in initialize, and calls to use-package now accepts a :straight parameter that allows me to retrieve special versions of some packages.

See the details in this essay.

Initial Settings

OS Path and Native Compilation

Helper functions to allow code for specific operating systems:

(defun ha-running-on-macos? ()
  "Return non-nil if running on Mac OS systems."
  (equal system-type 'darwin))

(defun ha-running-on-linux? ()
  "Return non-nil if running on Linux systems."
  (equal system-type 'gnu/linux))

With the way I start Emacs, I may not have the PATH I actually use (from the shell) available, so we’ll force it (code taken from here):

(defun set-exec-path-from-shell ()
  "Set up Emacs' `exec-path' and PATH environment variable to match
that used by the user's shell.

The MacOS, where GUI apps are not started from a shell, requires this."
  (interactive)
  (let* ((path-from-shell (shell-command-to-string "echo $PATH"))
         (trimmed-path    (replace-regexp-in-string (rx (zero-or-more space) eol)
                                                    "" path-from-shell))
         (in-fish?        (string-match (rx "fish" eol)
                                        (shell-command-to-string "echo $SHELL")))
         (separator       (if in-fish? " " ":"))
         (env-path        (if in-fish? (replace-regexp-in-string " " ":" trimmed-path) trimmed-path)))
    (message "PATH=%s" path-from-shell)
    (setenv "PATH" env-path)
    (setq exec-path (split-string trimmed-path separator))))

Clear up a Mac-specific issue that sometimes arises since I’m switching to native compilation project, as the Emacs.app that I use doesn’t have its bin directory, e.g. Emacs.app/Contents/MacOS/bin:

(when (ha-running-on-macos?)
    (add-to-list 'exec-path "/usr/local/bin")
    (add-to-list 'exec-path "/opt/homebrew/bin")
    (add-to-list 'exec-path (concat invocation-directory "bin") t))

Getting tired off all the packages that I load spewing a bunch of warnings that I can’t do anything about:

(when (and (fboundp 'native-comp-available-p)
           (native-comp-available-p))
  (setq native-comp-async-report-warnings-errors nil
        native-comp-deferred-compilation t))

Basic Libraries

The following packages come with Emacs, but seems like they still need loading:

(use-package cl-lib
  :straight (:type built-in)
  :init (defun first (elt) (car elt))
  :commands (first))

(require 'subr-x)

Ugh. Why am I getting a missing first function error? I define a simple implementation, that the CL library will overwrite … at some point.

While most libraries will take care of their dependencies, I want to install my dependent libraries, e.g, Magnar Sveen’s Clojure-inspired dash.el project:

(use-package dash)

Sure this package is essentially syntactic sugar, and to help share my configuration, I attempt to use thread-last instead of ->>, but, I still like it.

The s.el project is a simpler string manipulation library that I (and other projects) use:

(use-package s)

Manipulate file paths with the f.el project:

(use-package f)

The shell-command function is useful, but having it split the output into a list is a helpful abstraction:

(defun shell-command-to-list (command)
  "Return list of lines from running COMMAND in shell."
  (thread-last command
     shell-command-to-string
     s-lines
     (seq-map 's-trim)
     (seq-remove 's-blank-str?)))

And let’s see the results:

(ert-deftest shell-command-to-list-test ()
  (should (equal '("hello world")
                 (shell-command-to-list "echo hello world")))

  ;; We don't need blank lines:
  (should (equal '("hello world" "goodbye for now")
                 (shell-command-to-list "echo '\n\nhello world\n\ngoodbye for now\n\n'"))

  ;; No output? Return null:
  (should (null (shell-command-to-list "echo")))

  ;; No line should contain carriage returns:
  (should (null (seq-filter
                 (lambda (line) (s-contains? "\n" line))
                 (shell-command-to-list "ls")))))

My Code Location

Much of my more complicated code comes from my website essays and other projects. The destination shows up here:

(add-to-list 'load-path (expand-file-name "elisp" user-emacs-directory))

Emacs Server Control

I actually run two instances of Emacs on some systems, where one instance has all my work-related projects, perspectives, and packages installed (like LSP), and my personal instance has other packages running (like IRC and Mail). I need a function that can make that distinction, and based on that, it will set server-start appropriately, so that emacsclient can call into the correct one.

(defun ha-emacs-for-work? ()
  "Return non-nil when the Emacs instance is for work.
Matches based on a `FOR_WORK' environment variable."
  (and (file-directory-p "~/work")
       (getenv "FOR_WORK")))

And now start the server with an appropriate tag name:

(when (display-graphic-p)
  (if (not (ha-emacs-for-work?))
      (setq server-name "personal")
    (setq server-name "work")
    (when (ha-running-on-macos?)
      (set-exec-path-from-shell)))

  (server-start))

Note: When starting Emacs as a terminal program (only happens when I am attempting to evaluate code), we don’t start the server.

Load the Rest

The following defines the rest of my org-mode literate files, that I load later with the ha-hamacs-load function:

(defvar ha-hamacs-files (flatten-list
                         `("ha-private.org"
                           ,(when (display-graphic-p)
                              "ha-theme.org"
                              "ha-display.org")
                           "ha-config.org"
                           ;; "ha-leader.org"
                           "ha-evil.org"
                           ;; "ha-meow.org"
                           "ha-applications.org"
                           "ha-org.org"
                           ,(when (display-graphic-p)
                              "ha-org-word-processor.org")
                           "ha-org-literate.org"
                           "ha-org-clipboard.org"
                           "ha-capturing-notes.org"
                           "ha-agendas.org"
                           "ha-data.org"
                           "ha-passwords.org"
                           "ha-eshell.org"
                           "ha-remoting.org"
                           "ha-programming.org"
                           "ha-programming-elisp.org"
                           "ha-programming-python.org"
                           ,(if (ha-emacs-for-work?)
                                '("ha-org-sprint.org"
                                  "ha-programming-ansible.org"
                                  ;; "ha-programming-ruby.org"
                                  "ha-work.org")
                              ;; Personal Editor
                              '("ha-org-journaling.org"
                                ;; "ha-irc.org"
                                "ha-org-publishing.org"
                                "ha-email.org"
                                "ha-aux-apps.org"
                                "ha-feed-reader.org"))
                           "ha-dashboard.org"))
  "List of org files that complete the hamacs project.")

The list of hamacs org-formatted files stored in ha-hamacs-files is selectively short, and doesn’t include all files, for instance, certain languages that I’m learning aren’t automatically included. The function, ha-hamacs-files will return the list loaded at startup, as well as with an optional parameter, return them all.

(defun ha-hamacs-files (&optional all)
  "Return a list of my org files in my `hamacs' directory."
  (if (not all)
      ha-hamacs-files

    (thread-last (rx ".org" string-end)
                 (directory-files hamacs-source-dir nil)
                 (append ha-hamacs-files)
                 (--filter (not (string-match (rx "README") it)))
                 (-uniq))))

With this function, we can test/debug/reload any individual file, via:

(defun ha-hamacs-load (file)
  "Load or reload an org-mode FILE containing literate
Emacs configuration code."
  (interactive (list (completing-read "Org file: "
                                      (ha-hamacs-files :all))))
  (let ((full-file (expand-file-name file hamacs-source-dir)))
    (when (file-exists-p full-file)
      (message ">>> %s" full-file)
      (if (called-interactively-p)
          (org-babel-load-file full-file)
        (ignore-errors (org-babel-load-file full-file))))))

Notice that when we call this function non-interactively (e.g. from the Lisp function, ha-hamacs-reload-all), we suppress any errors. Obviously, I want to see the errors when calling interactively.

Tangling the Hamacs

And this similar function, will tangle one of my files. Notice that in order to increase the speed of the tangling process (and not wanting to pollute a project perspective), I use a temporary buffer instead of find-file.

(defun ha-hamacs-tangle (file)
  "Tangle an org-mode FILE containing literate Emacs
configuration code."
  (interactive (list (completing-read "Org file: "
                             (ha-hamacs-files :all))))
  (let ((full-file (file-name-concat hamacs-source-dir file))
        (target (file-name-concat "~/emacs.d/elisp"
                      (concat (file-name-sans-extension file)
                              ".el"))))
    (when (file-exists-p full-file)
      (ignore-errors
        (with-temp-buffer
          (insert-file-contents full-file)
          (with-current-buffer (concat temporary-file-directory file)
            (org-babel-tangle nil target (rx "emacs-lisp"))))))))

And we can now reload all startup files:

(defun ha-hamacs-reload-all ()
  "Reload our entire ecosystem of configuration files."
  (interactive)
  (dolist (file (ha-hamacs-files))
    (unless (equal file "bootstrap.org")
      (ha-hamacs-load file))))

And we can tangle all the files:

(defun ha-hamacs-tangle-all ()
  "Tangle all my Org initialization/configuration files."
  (interactive)
  (dolist (file (ha-hamacs-files))
    (ha-hamacs-tangle file)))

Edit my Files

Changing my Emacs configuration is as simple as editing an Org file containing the code, and evaluating that block or expression. Or even re-loading the entire file as described above. Calling find-file (or more often project-find-file) is sufficient but quicker if I supply a focused list of just the files in my project:

(defun ha-hamacs-find-file (file)
  "Call `find-file' FILE.
When called interactively, present org files containing
my literate Emacs configuration code."
  (interactive (list (completing-read "Org file: "
                                      (ha-hamacs-files :all))))
  (let ((full-file (file-name-concat hamacs-source-dir file)))
    (find-file full-file)))

Whew … and do it all:

(ha-hamacs-reload-all)