From 34ecc09def78408024bcfff2a78339a7009da5a4 Mon Sep 17 00:00:00 2001 From: Kat Huang Date: Sat, 18 Apr 2026 19:03:11 -0700 Subject: [PATCH] Fix Emacs Elpaca bootstrap and startup --- dotfiles/emacs.d/README.org | 18 ++++- dotfiles/emacs.d/early-init.el | 2 + dotfiles/emacs.d/elpaca-installer.el | 102 ++++++++++++++++-------- dotfiles/emacs.d/init.el | 114 +++++++++++++++++++++++---- dotfiles/emacs.d/kat-mode.org | 41 ++++------ dotfiles/emacs.d/org-config.org | 3 +- 6 files changed, 201 insertions(+), 79 deletions(-) diff --git a/dotfiles/emacs.d/README.org b/dotfiles/emacs.d/README.org index c5343171..a99dee7a 100644 --- a/dotfiles/emacs.d/README.org +++ b/dotfiles/emacs.d/README.org @@ -1347,6 +1347,7 @@ Paradox is a package.el extension. I have no use for it now that I use straight. ** load-dir #+BEGIN_SRC emacs-lisp (use-package load-dir + :ensure (:host github :repo "emacs-straight/load-dir") :demand t :config (progn @@ -2670,7 +2671,7 @@ The following is taken from [[https://github.com/syl20bnr/spacemacs/blob/a650877 **** clj-refactor #+BEGIN_SRC emacs-lisp ;;@WORKAROUND clj-refactor dependency inflections Version metadata -(use-package inflections :ensure (:depth nil :version elpaca-latest-tag :version-regexp "[[:digit:]]\\.[[:digit:]]")) +(use-package inflections) (use-package clj-refactor :commands clj-refactor-mode) #+END_SRC @@ -4108,8 +4109,19 @@ load-theme hook (See the heading below). #+END_SRC ** Set Font #+BEGIN_SRC emacs-lisp -(add-to-list 'default-frame-alist - '(font . "JetBrainsMono Nerd Font-10:weight=medium")) +(defvar imalison:preferred-font-families + '("JetBrainsMono Nerd Font" + "JetBrains Mono" + "JetBrainsMono Nerd Font Mono")) + +(defun imalison:default-font-parameter () + (catch 'font + (dolist (family imalison:preferred-font-families) + (when (find-font (font-spec :family family)) + (throw 'font (format "%s-10" family)))))) + +(when-let ((font (imalison:default-font-parameter))) + (add-to-list 'default-frame-alist `(font . ,font))) #+END_SRC ** imalison:appearance #+BEGIN_SRC emacs-lisp diff --git a/dotfiles/emacs.d/early-init.el b/dotfiles/emacs.d/early-init.el index 512068a5..9e0c264d 100644 --- a/dotfiles/emacs.d/early-init.el +++ b/dotfiles/emacs.d/early-init.el @@ -1 +1,3 @@ +;; -*- lexical-binding: t; -*- + (setq package-enable-at-startup nil) diff --git a/dotfiles/emacs.d/elpaca-installer.el b/dotfiles/emacs.d/elpaca-installer.el index d497e8b2..dc337051 100644 --- a/dotfiles/emacs.d/elpaca-installer.el +++ b/dotfiles/emacs.d/elpaca-installer.el @@ -1,6 +1,23 @@ ;; Elpaca Installer -*- lexical-binding: t; -*- (defvar elpaca-installer-version 0.12) -(defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory)) + +(defun elpaca-installer--state-root () + "Return a writable root for Elpaca state." + (let* ((preferred user-emacs-directory) + (fallback (expand-file-name + "emacs/" + (or (getenv "XDG_STATE_HOME") + (expand-file-name "~/.local/state/"))))) + (condition-case nil + (progn + (make-directory preferred t) + preferred) + (file-error + (make-directory fallback t) + fallback)))) + +(defvar elpaca-directory + (expand-file-name "elpaca/" (elpaca-installer--state-root))) (defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory)) (defvar elpaca-sources-directory (expand-file-name "sources/" elpaca-directory)) (defvar elpaca-legacy-repos-directory (expand-file-name "repos/" elpaca-directory)) @@ -9,6 +26,26 @@ :files (:defaults "elpaca-test.el" (:exclude "extensions")) :build (:not elpaca-activate))) +(defun elpaca-installer--ensure-symlink (target alias) + "Create symlink from ALIAS to TARGET, ignoring pre-existing paths." + (condition-case nil + (make-symbolic-link target alias) + (file-already-exists nil))) + +(defun elpaca-installer--build-stale-p (build) + "Return non-nil when BUILD contains older compiled artifacts than its sources." + (when (file-directory-p build) + (catch 'stale + (dolist (entry (directory-files build t "\\.elc?\\'")) + (when (string-suffix-p ".elc" entry) + (let* ((source (substring entry 0 -1)) + (source-truename (and (file-exists-p source) + (ignore-errors (file-truename source))))) + (when (and source-truename + (file-newer-than-file-p source-truename entry)) + (throw 'stale t))))) + nil))) + (defun elpaca-installer--repo-installer-version (repo) "Return the installer version expected by elpaca checkout at REPO." (let ((elpaca-el (expand-file-name "elpaca.el" repo))) @@ -34,9 +71,18 @@ (dolist (entry (directory-files build t directory-files-no-dot-files-regexp)) (when-let* ((target (file-symlink-p entry)) (truename (ignore-errors (file-truename entry))) - (source-root (and truename - (directory-file-name - (file-name-directory truename))))) + (source-root + (catch 'source-root + (dolist (root roots) + (when (string-prefix-p root truename) + (let* ((relative (file-relative-name truename root)) + (repo-name (car (split-string relative "/" t))) + (repo-root (and repo-name + (expand-file-name repo-name root)))) + (when (file-directory-p repo-root) + (throw 'source-root + (directory-file-name repo-root)))))) + nil))) (dolist (root roots) (when (string-prefix-p root truename) (when (file-directory-p source-root) @@ -68,39 +114,21 @@ ((file-symlink-p source) (delete-file source) (if desired-source - (make-symbolic-link desired-source - (directory-file-name source)) + (elpaca-installer--ensure-symlink + desired-source + (directory-file-name source)) (delete-directory build 'recursive))) ((file-exists-p source) nil) (desired-source - (make-symbolic-link desired-source - (directory-file-name source))) + (elpaca-installer--ensure-symlink + desired-source + (directory-file-name source))) (t (delete-directory build 'recursive)))))))) (defun elpaca-installer--repair-source-dir-aliases () - "Create compatibility symlinks for legacy repos ending in `.el'." - (when (file-directory-p elpaca-sources-directory) - (dolist (entry (directory-files elpaca-sources-directory t directory-files-no-dot-files-regexp)) - (when-let* (((file-directory-p entry)) - (name (file-name-nondirectory (directory-file-name entry))) - ((string-suffix-p ".el" name)) - (alias-name (substring name 0 (- (length name) 3))) - (alias (expand-file-name alias-name elpaca-sources-directory)) - (target (ignore-errors - (directory-file-name (file-truename entry))))) - (cond - ((and (file-symlink-p alias) - (equal (ignore-errors (directory-file-name (file-truename alias))) - target)) - nil) - ((file-symlink-p alias) - (delete-file alias) - (make-symbolic-link target alias)) - ((file-exists-p alias) - nil) - (t - (make-symbolic-link target alias))))))) + "Compatibility hook retained for older configs." + nil) ;; Elpaca now expects package sources under `sources/`. Preserve older local ;; installs that still use `repos/` so startup can recover without recloning. (when (and (file-directory-p elpaca-legacy-repos-directory) @@ -109,8 +137,9 @@ (directory-file-name elpaca-sources-directory))) (when (and (file-directory-p elpaca-sources-directory) (not (file-exists-p elpaca-legacy-repos-directory))) - (make-symbolic-link (directory-file-name elpaca-sources-directory) - (directory-file-name elpaca-legacy-repos-directory))) + (elpaca-installer--ensure-symlink + (directory-file-name elpaca-sources-directory) + (directory-file-name elpaca-legacy-repos-directory))) (elpaca-installer--repair-source-dir-aliases) (elpaca-installer--repair-build-source-layout) (let* ((repo (expand-file-name "elpaca/" elpaca-sources-directory)) @@ -124,13 +153,14 @@ ((not (equal repo-version (format "%s" elpaca-installer-version))))) (when (file-directory-p build) (delete-directory build 'recursive)) - (when (file-directory-p elpaca-cache-directory) + (when (and (boundp 'elpaca-cache-directory) + (file-directory-p elpaca-cache-directory)) (delete-directory elpaca-cache-directory 'recursive)) (when (file-directory-p repo) (delete-directory repo 'recursive))) + (when (elpaca-installer--build-stale-p build) + (delete-directory build 'recursive)) (add-to-list 'load-path repo) - (when (file-exists-p build) - (add-to-list 'load-path build)) (unless (file-exists-p repo) (make-directory repo t) (when (<= emacs-major-version 28) (require 'subr-x)) @@ -154,5 +184,7 @@ (require 'elpaca) (elpaca-generate-autoloads "elpaca" repo) (let ((load-source-file-function nil)) (load autoloads)))) +(require 'elpaca) +(setq elpaca-log-functions '(elpaca-log-command-query)) (add-hook 'after-init-hook #'elpaca-process-queues) (elpaca `(,@elpaca-order)) diff --git a/dotfiles/emacs.d/init.el b/dotfiles/emacs.d/init.el index 4ee71c60..eb71fc01 100644 --- a/dotfiles/emacs.d/init.el +++ b/dotfiles/emacs.d/init.el @@ -8,11 +8,76 @@ (defvar imalison:do-benchmark nil) (defun emacs-directory-filepath (filename) - (concat (file-name-directory load-file-name) filename)) + (expand-file-name filename user-emacs-directory)) (load-file (expand-file-name "elpaca-installer.el" user-emacs-directory)) + +;; Elpaca's initial queue logger can fire during self-bootstrap before its +;; helper is callable under --debug-init. Keep command logging, but skip the +;; fragile initial-queue logger so startup diagnostics reach the real config. +(setq elpaca-log-functions '(elpaca-log-command-query)) ;; Default hosted git clones to SSH (e.g., git@github.com:owner/repo.git). (setq elpaca-order-defaults (plist-put elpaca-order-defaults :protocol 'ssh)) + +(defun imalison:existing-executable (&rest candidates) + (seq-find #'file-executable-p (delq nil candidates))) + +(defun imalison:emacs-bin-directory () + (let ((emacsclient (or (executable-find "emacsclient") + (when invocation-directory + (expand-file-name "emacsclient" invocation-directory)) + (when invocation-directory + (expand-file-name "../../../../bin/emacsclient" + invocation-directory))))) + (when (and emacsclient (file-executable-p emacsclient)) + (directory-file-name (file-name-directory emacsclient))))) + +(defun imalison:emacsclient-executable () + (imalison:existing-executable + (executable-find "emacsclient") + (when invocation-directory + (expand-file-name "emacsclient" invocation-directory)) + (when invocation-directory + (expand-file-name "../../../../bin/emacsclient" invocation-directory)))) + +;; GUI Emacs launched from the app bundle may not inherit a PATH that contains +;; the matching emacsclient binary. +(when-let ((emacs-bin (imalison:emacs-bin-directory))) + (add-to-list 'exec-path emacs-bin) + (setenv "PATH" (concat emacs-bin path-separator (or (getenv "PATH") "")))) +(setq emacsclient-program-name + (imalison:emacsclient-executable)) +(setq with-editor-emacsclient-executable + (imalison:emacsclient-executable)) + +(defun imalison:elpaca-menu-local-repos (request &optional item) + (when (eq request 'index) + (let ((root elpaca-sources-directory)) + (cl-labels + ((item-info + (pkg) + (let* ((name (symbol-name pkg)) + (dir (expand-file-name name root)) + (git-dir (expand-file-name ".git" dir))) + (when (file-directory-p git-dir) + (with-temp-buffer + (when (zerop (call-process "git" nil t nil "-C" dir "config" "--get" "remote.origin.url")) + (let ((remote (string-trim (buffer-string)))) + (when (not (string-empty-p remote)) + (list :source "Local repos" + :recipe (list :package name + :repo remote + :local-repo name)))))))))) + (if item + (item-info item) + (cl-loop for path in (directory-files root nil "^[^.]" t) + for pkg = (intern path) + for info = (item-info pkg) + when info + collect (cons pkg info))))))) + +(add-to-list 'elpaca-menu-functions #'imalison:elpaca-menu-local-repos t) + (elpaca elpaca-use-package (elpaca-use-package-mode)) (elpaca-wait) (setq use-package-enable-imenu-support t) @@ -40,6 +105,15 @@ :config (progn (dash-enable-font-lock))) +;; Some split packages fall through the active menus in this config. Give +;; Elpaca an explicit source so startup doesn't get stuck on recipe lookup or +;; stale branch-mapped clones. +(elpaca `(queue :host github :repo "emacs-straight/queue")) +(elpaca `(git-commit :host github :repo "magit/magit" + :files ("lisp/git-commit.el" "lisp/git-commit-pkg.el"))) +(elpaca `(magit-section :host github :repo "magit/magit" + :files ("lisp/magit-section.el" "lisp/magit-section-pkg.el"))) + (use-package gh :defer t :ensure (:host github :repo "IvanMalison/gh.el")) @@ -85,6 +159,22 @@ (elpaca-wait) +(require 'org) + +(defun imalison:load-literate-file (org-file) + (let ((el-file (concat (file-name-sans-extension org-file) ".el"))) + ;; Prefer the tangled file on normal startup and only re-tangle when the + ;; Org source changed. + (if (and (file-exists-p el-file) + (not (file-newer-than-file-p org-file el-file))) + (load-file el-file) + (org-babel-load-file org-file)))) + +(defun imalison:load-kat-mode () + (let ((debug-on-error t)) + (imalison:load-literate-file + (emacs-directory-filepath "kat-mode.org")))) + ;; Install transient early to prevent built-in version from loading ;; Workaround: overriding-text-conversion-style is void on pgtk builds (no ;; HAVE_TEXT_CONVERSION) but transient's .elc compiled on X11 has static-if @@ -99,29 +189,25 @@ ;; Magit's split packages are compiled separately; make them available before ;; the larger config queue reaches magit itself. (use-package git-commit - :ensure (:host github :repo "magit/magit" - :files ("lisp/git-commit.el" "lisp/git-commit-pkg.el") - :wait t) + :ensure nil :defer t) (use-package magit-section - :ensure (:host github :repo "magit/magit" - :files ("lisp/magit-section.el" "lisp/magit-section-pkg.el") - :wait t) + :ensure nil :defer t) (elpaca-wait) -(when (or (equal (s-trim (shell-command-to-string "whoami")) "kat") - imalison:kat-mode) - (let ((debug-on-error t)) - (org-babel-load-file - (concat (file-name-directory load-file-name) "kat-mode.org")))) - (let ((debug-on-error t)) - (org-babel-load-file + (imalison:load-literate-file (expand-file-name "README.org" user-emacs-directory))) +(when (or (equal (s-trim (shell-command-to-string "whoami")) "kat") + imalison:kat-mode) + ;; Machine-specific overrides can reuse packages declared in README once + ;; Elpaca has activated the main init queue. + (add-hook 'elpaca-after-init-hook #'imalison:load-kat-mode)) + ;; (when imalison:do-benchmark (benchmark-init/deactivate)) ;; Local Variables: diff --git a/dotfiles/emacs.d/kat-mode.org b/dotfiles/emacs.d/kat-mode.org index ee0b3889..c19224f4 100644 --- a/dotfiles/emacs.d/kat-mode.org +++ b/dotfiles/emacs.d/kat-mode.org @@ -1,6 +1,7 @@ * evil #+begin_src emacs-lisp (use-package evil + :ensure nil :demand t :config (progn @@ -22,6 +23,7 @@ This makes evil-mode play nice with org-fc #+begin_src emacs-lisp (use-package org-fc + :ensure (:host github :repo "l3kn/org-fc") :demand t :config (progn @@ -49,6 +51,7 @@ This makes evil-mode play nice with org-fc #+begin_src emacs-lisp (setq imalison:org-whoami "Kat Huang") (setq org-directory "~/org/") ; This is the directory where you want to save your Org files. Change as necessary. +(defvar org-capture-templates nil) (add-to-list 'org-capture-templates '("j" "Journal" entry (file+datetree "~/org/daily-journal.org") "* %?\nEntered on %U\n %i\n %a")) @@ -61,7 +64,7 @@ This makes evil-mode play nice with org-fc ** Journal #+begin_src emacs-lisp (setq imalison:journal-template-filepath - (imalison:join-paths org-directory "templates" "daily-journal-template.org")) + (expand-file-name "templates/daily-journal-template.org" org-directory)) #+end_src ** Insert a link to a task selected from agenda #+begin_src emacs-lisp @@ -119,6 +122,7 @@ This makes evil-mode play nice with org-fc * Disable autoflake #+begin_src emacs-lisp (use-package apheleia + :ensure nil :demand t :config (progn @@ -131,11 +135,6 @@ This makes evil-mode play nice with org-fc * Packages #+begin_src emacs-lisp (use-package org-drill) -(require 'package) -(add-to-list 'package-archives - '("melpa" . "https://melpa.org/packages/") t) -(package-initialize) - #+end_src * Disable wild notifactions @@ -145,26 +144,17 @@ This makes evil-mode play nice with org-fc * Swift #+begin_src emacs-lisp -(require 'package) -(add-to-list 'package-archives - '("melpa" . "https://melpa.org/packages/") t) -(package-initialize) +(use-package swift-mode + :ensure nil + :demand t + :config + (add-to-list 'auto-mode-alist '("\\.swift\\'" . swift-mode))) -(unless (package-installed-p 'swift-mode) - (package-refresh-contents) - (package-install 'swift-mode)) - -(unless (package-installed-p 'lsp-mode) - (package-refresh-contents) - (package-install 'lsp-mode)) - -;; Swift Mode -(require 'swift-mode) -(add-to-list 'auto-mode-alist '("\\.swift\\'" . swift-mode)) - -;; LSP Mode -(require 'lsp-mode) -(add-hook 'swift-mode-hook #'lsp) +(use-package lsp-mode + :ensure nil + :commands lsp + :config + (add-hook 'swift-mode-hook #'lsp)) ;; Set the path to SourceKit-LSP if it's not in your PATH (setq lsp-sourcekit-executable "/path/to/sourcekit-lsp") @@ -173,4 +163,3 @@ This makes evil-mode play nice with org-fc (setq lsp-sourcekit-executable-args '("-toolchain" "/path/to/swift-toolchain")) #+end_src - diff --git a/dotfiles/emacs.d/org-config.org b/dotfiles/emacs.d/org-config.org index d3d9f562..948a38c7 100644 --- a/dotfiles/emacs.d/org-config.org +++ b/dotfiles/emacs.d/org-config.org @@ -1011,6 +1011,7 @@ alphanumeric characters only." ** org-roam #+begin_src emacs-lisp (use-package org-roam + :if (file-directory-p (expand-file-name "~/org/roam/")) :after org :defer 1 :preface @@ -1060,7 +1061,7 @@ alphanumeric characters only." org-roam-buffer-visibility-fn 'imalison:org-roam-frame-based-buffer-visibility-fn) (emit-make-mode-dependent imalison:org-roam-set-frame-visibility-mode frame-mode))) :custom - (org-roam-directory (file-truename "~/org/roam/"))) + (org-roam-directory (expand-file-name "~/org/roam/"))) #+end_src ***** ui #+begin_src emacs-lisp