#+title:  My Emacs Bootstrap
#+author: Howard X. Abrams
#+date:   2021-10-08
#+tags: emacs

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 <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:
  ;;            ~/src/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]].

* Initial Settings
** 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
* 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
       (seq-map 's-trim)
       (seq-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 (expand-file-name "elisp" user-emacs-directory))
#+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 instance is for work.
  Matches based on a `FOR_WORK' environment variable."
    (and (file-directory-p "~/work")
         (getenv "FOR_WORK")))
#+end_src

And now start the server with an appropriate tag name:

#+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))
#+end_src

*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:
#+begin_src emacs-lisp
  (defvar ha-hamacs-files (flatten-list
                           `("ha-private.org"
                             "ha-config.org"
                             ;; "ha-leader.org"
                             "ha-evil.org"
                             ;; "ha-meow.org"
                             "ha-applications.org"
                             ,(when (display-graphic-p)
                                "ha-display.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.")
#+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 hamacs-source-dir 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 (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))))))
#+end_src

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=.

#+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 (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"))))))))
#+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))
      (ha-hamacs-tangle file)))
#+end_src
** 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))))
    (let ((full-file (file-name-concat hamacs-source-dir file)))
      (find-file full-file)))
#+end_src

Whew … and do it all:
#+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: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
