#+TITLE: My Emacs Bootstrap #+AUTHOR: Howard X. Abrams #+DATE: 2021-10-08 A literate programming file for bootstraping my Emacs Configuration. #+begin_src emacs-lisp :exports none ;;; bootstrap.el --- file for bootstraping my Emacs Configuration ;; ;; © 2021-2023 Howard X. Abrams ;; Licensed under a Creative Commons Attribution 4.0 International License. ;; See http://creativecommons.org/licenses/by/4.0/ ;; ;; Author: Howard X. Abrams ;; Maintainer: Howard X. Abrams ;; Created: October 8, 2021 ;; ;; This file is not part of GNU Emacs. ;; ;; *NB:* Do not edit this file. Instead, edit the original literate file at: ;; ~/other/hamacs/bootstrap.org ;; And tangle the file to recreate this one. ;; ;;; Code: #+end_src * Introduction This file contains all the variable definitions and library loading for the other files in my project. I'm installing everything using the [[https://github.com/raxod502/straight.el#getting-started][straight.el]] for package installation and management. This is initialization code configured in [[file:initialize][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 [[https://dev.to/jkreeftmeijer/emacs-package-management-with-straight-el-and-use-package-3oc8][this essay]]. ** OS Path and Native Compilation Helper functions to allow code for specific operating systems: #+begin_src emacs-lisp (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)) #+end_src 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 [[https://www.emacswiki.org/emacs/ExecPath][from here]]): #+begin_src emacs-lisp (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)))) #+end_src Clear up a Mac-specific issue that sometimes arises since I'm switching to [[http://akrl.sdf.org/gccemacs.html][native compilation project]], as the =Emacs.app= that I use doesn't have its =bin= directory, e.g. =Emacs.app/Contents/MacOS/bin=: #+begin_src emacs-lisp (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)) #+end_src Getting tired off all the packages that I load spewing a bunch of warnings that I can't do anything about: #+begin_src emacs-lisp (when (and (fboundp 'native-comp-available-p) (native-comp-available-p)) (setq native-comp-async-report-warnings-errors nil native-comp-deferred-compilation t)) #+end_src ** GNU Pretty Good Privacy On Linux, GPG is pretty straight-forward, but on the Mac, I often have troubles doing: #+begin_src sh brew install gpg #+end_src Next, on every reboot, start the agent: #+begin_src sh /opt/homebrew/bin/gpg-agent --daemon #+end_src Since =brew link gpg= doesn’t always work, this helper function may find the executable: #+begin_src emacs-lisp (defun executable (path) "Return PATH if executable, see `file-executable-p'." (let ((epath (first (file-expand-wildcards path)))) (when (and epath (file-executable-p epath)) epath))) (use-package epa-file :straight (:type built-in) :custom (epg-gpg-program (or (executable "/opt/homebrew/bin/gpg") (executable "/usr/local/bin/gpg") (executable "/usr/local/opt/gpg") (executable "/usr/bin/pgp"))) :config (epa-file-enable)) #+end_src ** Basic Libraries The following packages come with Emacs, but seems like they still need loading: #+begin_src emacs-lisp (use-package cl-lib :straight (:type built-in) :init (defun first (elt) (car elt)) :commands (first)) (require 'subr-x) #+end_src 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, [[https://github.com/magnars/.emacs.d/][Magnar Sveen]]'s Clojure-inspired [[https://github.com/magnars/dash.el][dash.el]] project: #+begin_src emacs-lisp (use-package dash) #+end_src 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 [[https://github.com/magnars/s.el][s.el]] project is a simpler string manipulation library that I (and other projects) use: #+begin_src emacs-lisp (use-package s) #+end_src Manipulate file paths with the [[https://github.com/rejeep/f.el][f.el]] project: #+begin_src emacs-lisp (use-package f) #+end_src The [[help:shell-command][shell-command]] function is useful, but having it split the output into a list is a helpful abstraction: #+begin_src emacs-lisp (defun shell-command-to-list (command) "Return list of lines from running COMMAND in shell." (thread-last command shell-command-to-string s-lines (-map 's-trim) (-remove 's-blank-str?))) #+end_src And let’s see the results: #+begin_src emacs-lisp :tangle no (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"))))) #+end_src ** My Code Location Much of my more complicated code comes from my website essays and other projects. The destination shows up here: #+begin_src emacs-lisp (add-to-list 'load-path (f-expand "~/.emacs.d/elisp")) #+end_src Hopefully, this will tie me over while I transition. ** 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. #+begin_src emacs-lisp (defun ha-emacs-for-work? () "Return non-nil when the Emacs application's location matches as one for work. Based on initially running the app with a `FOR_WORK' environment variable." (and (f-dir? "~/work") (getenv "FOR_WORK"))) #+end_src And now start the server with an appropriate tag name: #+begin_src emacs-lisp (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) #+end_src * Load the Rest The following loads the rest of my org-mode literate files. I add new filesas they are /ready/: #+begin_src emacs-lisp (defvar ha-hamacs-files (flatten-list `("ha-private.org" "ha-config.org" ,(when (display-graphic-p) "ha-display.org") "ha-org.org" ,(when (display-graphic-p) "ha-org-word-processor.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-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.") #+end_src 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. #+begin_src emacs-lisp (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 "~/other/hamacs" nil) (append ha-hamacs-files) (--filter (not (string-match (rx "README") it))) (-uniq)))) #+end_src With this function, we can test/debug/reload any individual file, via: #+begin_src emacs-lisp (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 (f-join hamacs-source-dir file))) (when (f-exists? full-file) (ignore-errors (org-babel-load-file full-file))))) #+end_src 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=. #+begin_src emacs-lisp (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 (f-join hamacs-source-dir file)) (target (file-name-concat "~/emacs.d/elisp" (concat (file-name-sans-extension file) ".el")))) (when (f-exists? 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")))))))) #+end_src And we can now reload /all/ startup files: #+begin_src emacs-lisp (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)))) #+end_src And we can tangle /all/ the files: #+begin_src emacs-lisp (defun ha-hamacs-tangle-all () "Tangle all my Org initialization/configuration files." (interactive) (dolist (file (ha-hamacs-files)) (unless (equal file "bootstrap.org") (ha-hamacs-tangle file)))) #+end_src And do it: #+begin_src emacs-lisp (ha-hamacs-reload-all) #+end_src * Technical Artifacts :noexport: Let's provide a name so we can =require= this file: #+begin_src emacs-lisp :exports none (provide 'bootstrap) ;;; bootstrap.el ends here #+end_src Before you can build this on a new system, make sure that you put the cursor over any of these properties, and hit: ~C-c C-c~ #+DESCRIPTION: A literate programming file for bootstrapping my environment. #+PROPERTY: header-args:sh :tangle no #+PROPERTY: header-args:emacs-lisp :tangle yes #+PROPERTY: header-args :results none :eval no-export :comments no mkdirp yes #+OPTIONS: num:nil toc:nil todo:nil tasks:nil tags:nil date:nil #+OPTIONS: skip:nil author:nil email:nil creator:nil timestamp:nil #+INFOJS_OPT: view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js