2023-12-03 18:57:36 +00:00
#+title : My Emacs Bootstrap
#+author : Howard X. Abrams
#+date : 2021-10-08
#+tags : emacs
2021-11-02 00:27:14 +00:00
A literate programming file for bootstraping my Emacs Configuration.
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :exports none
2022-03-09 18:45:37 +00:00
;;; bootstrap.el --- file for bootstraping my Emacs Configuration
;;
2023-02-23 17:35:36 +00:00
;; © 2021-2023 Howard X. Abrams
2022-06-18 00:25:47 +00:00
;; Licensed under a Creative Commons Attribution 4.0 International License.
2022-03-09 18:45:37 +00:00
;; See http://creativecommons.org/licenses/by/4.0/
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams >
;; 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:
2024-10-19 20:34:01 +00:00
;; ~/src/hamacs/bootstrap.org
2022-03-09 18:45:37 +00:00
;; And tangle the file to recreate this one.
;;
;;; Code:
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-02 00:27:14 +00:00
* Introduction
This file contains all the variable definitions and library loading for the other files in my project.
2023-02-23 17:28:00 +00:00
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.
2022-03-03 23:16:50 +00:00
2021-11-02 00:27:14 +00:00
See the details in [[https://dev.to/jkreeftmeijer/emacs-package-management-with-straight-el-and-use-package-3oc8 ][this essay ]].
2021-12-08 21:56:30 +00:00
2023-12-22 04:26:51 +00:00
* Initial Settings
2025-01-28 21:18:08 +00:00
** Garbage Collection Settings
GC has always been a contentious subject in Emacs. Lot less of an issue now, but let’ s not slow the startup (any more than I already do by checking all the packages to see if anything new needs to be downloaded).
Limit garbage collection before startup and then go back to the default value (8 MiB) after startup.
#+BEGIN_SRC emacs-lisp
(setq gc-cons-threshold most-positive-fixnum)
(add-hook 'emacs-startup-hook
(lambda ()
(setq gc-cons-threshold (* 100 1024 1024))))
#+END_SRC
2021-12-08 21:56:30 +00:00
** OS Path and Native Compilation
2021-12-13 18:45:32 +00:00
Helper functions to allow code for specific operating systems:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2021-12-13 18:45:32 +00:00
(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))
2022-06-18 00:25:47 +00:00
#+end_src
2021-12-13 18:45:32 +00:00
2022-06-18 00:25:47 +00:00
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 ]]):
2021-12-08 21:56:30 +00:00
2024-02-23 00:55:56 +00:00
#+begin_src emacs-lisp
2021-12-08 21:56:30 +00:00
(defun set-exec-path-from-shell ()
"Set up Emacs' `exec-path' and PATH environment variable to match
that used by the user's shell.
2022-06-18 00:25:47 +00:00
The MacOS, where GUI apps are not started from a shell, requires this."
2021-12-08 21:56:30 +00:00
(interactive)
2022-01-03 06:42:22 +00:00
(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))))
2022-06-18 00:25:47 +00:00
#+end_src
2021-12-08 21:56:30 +00:00
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= :
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2021-12-13 18:45:32 +00:00
(when (ha-running-on-macos?)
2022-01-03 06:42:22 +00:00
(add-to-list 'exec-path "/usr/local/bin")
2023-11-06 17:36:06 +00:00
(add-to-list 'exec-path "/opt/homebrew/bin")
2021-12-08 21:56:30 +00:00
(add-to-list 'exec-path (concat invocation-directory "bin") t))
2022-06-18 00:25:47 +00:00
#+end_src
2021-12-08 21:56:30 +00:00
Getting tired off all the packages that I load spewing a bunch of warnings that I can't do anything about:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2021-12-13 18:45:32 +00:00
(when (and (fboundp 'native-comp-available-p)
(native-comp-available-p))
(setq native-comp-async-report-warnings-errors nil
native-comp-deferred-compilation t))
2022-06-18 00:25:47 +00:00
#+end_src
2023-12-22 04:26:51 +00:00
* Basic Libraries
2021-11-02 00:27:14 +00:00
The following packages come with Emacs, but seems like they still need loading:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2022-01-06 23:36:39 +00:00
(use-package cl-lib
:straight (:type built-in)
:init (defun first (elt) (car elt))
:commands (first))
(require 'subr-x)
2022-06-18 00:25:47 +00:00
#+end_src
2022-01-06 23:36:39 +00:00
Ugh. Why am I getting a missing =first= function error? I define a simple implementation, that the CL library will overwrite ... at some point.
2022-06-18 00:25:47 +00:00
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
2024-02-23 00:55:56 +00:00
(use-package dash)
2022-06-18 00:25:47 +00:00
#+end_src
2022-01-06 23:36:39 +00:00
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.
2021-11-02 00:27:14 +00:00
The [[https://github.com/magnars/s.el ][s.el ]] project is a simpler string manipulation library that I (and other projects) use:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2024-02-23 00:55:56 +00:00
(use-package s)
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-02 00:27:14 +00:00
Manipulate file paths with the [[https://github.com/rejeep/f.el ][f.el ]] project:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2022-10-18 03:49:00 +00:00
(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
2024-08-11 04:58:05 +00:00
(seq-map 's-trim)
(seq-remove 's-blank-str?)))
2022-10-18 03:49:00 +00:00
#+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")))))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-02 00:27:14 +00:00
** My Code Location
2022-06-18 00:25:47 +00:00
Much of my more complicated code comes from my website essays and other projects. The destination shows up here:
#+begin_src emacs-lisp
2024-08-08 05:15:53 +00:00
(add-to-list 'load-path (expand-file-name "elisp" user-emacs-directory))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-02 00:27:14 +00:00
2023-12-22 04:26:51 +00:00
* Emacs Server Control
2022-10-03 17:10:38 +00:00
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.
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2022-01-03 06:42:22 +00:00
(defun ha-emacs-for-work? ()
2024-02-24 06:20:25 +00:00
"Return non-nil when the Emacs instance is for work.
Matches based on a `FOR_WORK' environment variable."
2024-07-06 03:43:28 +00:00
(and (file-directory-p "~/work")
2022-10-03 17:10:38 +00:00
(getenv "FOR_WORK")))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-06 00:06:55 +00:00
2022-04-01 18:33:24 +00:00
And now start the server with an appropriate tag name:
2021-11-06 00:06:55 +00:00
2024-08-11 04:58:05 +00:00
#+begin_src emacs-lisp
(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))
2022-06-18 00:25:47 +00:00
#+end_src
2024-08-11 04:58:05 +00:00
*Note:* When starting Emacs as a terminal program (only happens when I am attempting to evaluate code), we don’ t start the server.
2021-11-02 00:27:14 +00:00
* Load the Rest
2024-02-09 20:05:13 +00:00
The following /defines/ the rest of my org-mode literate files, that I load later with the =ha-hamacs-load= function:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
(defvar ha-hamacs-files (flatten-list
`("ha-private.org"
2024-11-22 20:56:39 +00:00
,(when (display-graphic-p)
"ha-theme.org"
"ha-display.org")
2022-06-18 00:25:47 +00:00
"ha-config.org"
2024-02-09 20:05:13 +00:00
;; "ha-leader.org"
"ha-evil.org"
;; "ha-meow.org"
2023-12-22 05:22:16 +00:00
"ha-applications.org"
2022-06-18 00:25:47 +00:00
"ha-org.org"
2022-09-21 06:09:42 +00:00
,(when (display-graphic-p)
"ha-org-word-processor.org")
2024-07-07 18:29:08 +00:00
"ha-org-literate.org"
2022-09-21 06:09:42 +00:00
"ha-org-clipboard.org"
"ha-capturing-notes.org"
"ha-agendas.org"
2022-10-18 03:49:00 +00:00
"ha-data.org"
2022-09-21 06:09:42 +00:00
"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?)
2023-04-01 23:29:40 +00:00
'("ha-org-sprint.org"
2024-07-01 23:43:52 +00:00
"ha-programming-ansible.org"
2023-04-01 23:29:40 +00:00
;; "ha-programming-ruby.org"
"ha-work.org")
2022-09-21 06:09:42 +00:00
;; Personal Editor
'("ha-org-journaling.org"
2024-03-22 20:43:12 +00:00
;; "ha-irc.org"
2022-09-21 06:09:42 +00:00
"ha-org-publishing.org"
"ha-email.org"
"ha-aux-apps.org"
2022-11-03 03:59:00 +00:00
"ha-feed-reader.org"))
"ha-dashboard.org"))
2021-11-10 01:18:52 +00:00
"List of org files that complete the hamacs project.")
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-10 01:18:52 +00:00
2022-09-02 23:05:23 +00:00
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)
2024-02-22 00:28:57 +00:00
(directory-files hamacs-source-dir nil)
2022-09-02 23:05:23 +00:00
(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:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2021-11-10 01:18:52 +00:00
(defun ha-hamacs-load (file)
2024-02-24 06:20:25 +00:00
"Load or reload an org-mode FILE containing literate
Emacs configuration code."
(interactive (list (completing-read "Org file: "
2024-08-11 04:58:05 +00:00
(ha-hamacs-files :all))))
(let ((full-file (expand-file-name file hamacs-source-dir)))
2024-07-06 03:43:28 +00:00
(when (file-exists-p full-file)
2024-08-11 04:58:05 +00:00
(message ">>> %s" full-file)
(if (called-interactively-p)
(org-babel-load-file full-file)
(ignore-errors (org-babel-load-file full-file))))))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-10 01:18:52 +00:00
2024-08-11 04:58:05 +00:00
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.
2024-06-17 16:50:53 +00:00
** Tangling the Hamacs
2023-07-05 20:20:55 +00:00
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
2024-02-24 06:20:25 +00:00
(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))))
2024-07-06 03:43:28 +00:00
(let ((full-file (file-name-concat hamacs-source-dir file))
2024-02-24 06:20:25 +00:00
(target (file-name-concat "~/emacs.d/elisp"
(concat (file-name-sans-extension file)
".el"))))
2024-07-06 03:43:28 +00:00
(when (file-exists-p full-file)
2024-02-24 06:20:25 +00:00
(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"))))))))
2023-07-05 20:20:55 +00:00
#+end_src
2022-09-02 23:05:23 +00:00
And we can now reload /all/ startup files:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2021-11-24 00:29:52 +00:00
(defun ha-hamacs-reload-all ()
"Reload our entire ecosystem of configuration files."
(interactive)
2022-09-02 23:05:23 +00:00
(dolist (file (ha-hamacs-files))
2022-05-11 21:35:48 +00:00
(unless (equal file "bootstrap.org")
(ha-hamacs-load file))))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-24 00:29:52 +00:00
2023-07-05 20:20:55 +00:00
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))
2024-08-11 04:58:05 +00:00
(ha-hamacs-tangle file)))
2023-07-05 20:20:55 +00:00
#+end_src
2024-06-17 16:50:53 +00:00
** 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 [[file:ha-config.org::*Projects ][project-find-file ]]) is sufficient but quicker if I supply a /focused list/ of just the files in my project:
#+begin_src emacs-lisp
(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))))
2024-07-06 03:43:28 +00:00
(let ((full-file (file-name-concat hamacs-source-dir file)))
2024-06-17 16:50:53 +00:00
(find-file full-file)))
#+end_src
2024-07-02 18:38:32 +00:00
Whew … and do it all:
#+begin_src emacs-lisp
(ha-hamacs-reload-all)
#+end_src
2021-11-02 00:27:14 +00:00
* Technical Artifacts :noexport:
2022-06-18 00:25:47 +00:00
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
2021-11-02 00:27:14 +00:00
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~
2024-03-07 04:02:25 +00:00
#+description : A literate programming file for bootstrapping my environment.
2021-11-02 00:27:14 +00:00
2024-03-07 04:02:25 +00:00
#+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
2021-11-02 00:27:14 +00:00
2024-03-07 04:02:25 +00:00
#+options : num:nil toc:t todo:nil tasks:nil tags:nil date:nil
#+options : skip:nil author:nil email:nil creator:nil timestamp:nil
#+infojs_opt : view:nil toc:t ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js