Spacemacs tangled user configuration

Table of Contents

1. Introduction

This is a org file where its code snippets will be read by spacemacs for user-init and user-config. It is inspired by spacemacs.org.

2. user-init

All code snippets under this section will be added to dotspacemacs//user-init function, which is

Initialization function for user code.

It is called immediately after dotspacemacs/init, before layer configuration executes. This function is mostly useful for variables that need to be set before packages are loaded. If you are unsure, you should try in setting them in `dotspacemacs/user-config' first.

2.1. Custom variables

;; Keep Emacs Custom output out of tracked init.el. Spacemacs otherwise copies
;; its internal Custom cache into `dotspacemacs/emacs-custom-settings`.
(setq custom-file (expand-file-name "custom.el" dotspacemacs-directory))
(load custom-file 'noerror 'nomessage)

2.2. Locale

(setq system-time-locale "C")

2.3. Theme

(setq-default dotspacemacs-themes '(
                                    doom-one
                                    doom-monokai-pro
                                    spacemacs-dark
                                    doom-zenburn))

2.4. ROS

(defun spacemacs/update-ros-envs ()
  "Update all environment variables in `spacemacs-ignored-environment-variables'
from their values currently sourced in the shell environment (e.g. .bashrc)"
  (interactive)
  (setq exec-path-from-shell-check-startup-files nil)
  (exec-path-from-shell-copy-envs spacemacs-ignored-environment-variables)
  (message "ROS environment copied successfully from shell"))


;; Ignore any ROS environment variables since they might change depending
;; on which catkin workspace is used. When a new catkin workspace is chosen
;; call `spacemacs/update-ros-envs' to update theses envs accordingly
(setq-default spacemacs-ignored-environment-variables '("ROS_IP"
                                                        "PYTHONPATH"
                                                        "CMAKE_PREFIX_PATH"
                                                        "ROS_MASTER_URI"
                                                        "ROS_PACKAGE_PATH"
                                                        "ROSLISP_PACKAGE_DIRECTORIES"
                                                        "PKG_CONFIG_PATH"
                                                        "LD_LIBRARY_PATH"))

2.5. Shell

Set shell to be bash explicitly because my default shell fish does not work along with spacemacs.

(setq-default shell-file-name "/bin/bash")

2.6. Layers

2.6.1. groovy

(setq default-groovy-lsp-jar-path "~/.spacemacs.d/groovy-language-server-all.jar")
(if (file-exists-p default-groovy-lsp-jar-path)
    (setq groovy-lsp-jar-path default-groovy-lsp-jar-path)
  (message (concat default-groovy-lsp-jar-path " does not exist")))

2.7. Workarounds

2.7.1. Prefer MELPA Stable for shared packages

MELPA occasionally updates archive-contents before all referenced rolling build tarballs are available. Giving MELPA Stable a higher priority lets package.el use stable releases for packages that exist in both archives while still falling back to rolling MELPA for packages that are not released on MELPA Stable.

(setq configuration-layer-elpa-archives
      '(("melpa-stable" . "stable.melpa.org/packages/")
        ("melpa"        . "melpa.org/packages/")
        ("gnu"          . "elpa.gnu.org/packages/")
        ("nongnu"       . "elpa.nongnu.org/nongnu/")))
(setq package-archive-priorities
      '(("melpa-stable" . 20)
        ("melpa"        . 4)
        ("nongnu"       . 2)
        ("gnu"          . 1)))

2.7.2. Isolate Python LSP servers by project

Pyright and Ruff copy their multi-root variables when their lsp-mode clients register. Set them during user-init so each Python project gets its own process and selects its project-local interpreter when present.

(setq lsp-pyright-multi-root nil
      lsp-ruff-multi-root nil)

(defconst my/lsp-generated-directory-watch-ignores
  '("[/\\\\]\\.pi-subagents\\'"
    "[/\\\\]\\.pi\\'"
    "[/\\\\]\\.ros2_ws\\'"
    "[/\\\\]\\.superpowers\\'"
    "[/\\\\]\\.ruff_cache\\'")
  "Generated directory regexps excluded from recursive LSP watching.")

(with-eval-after-load 'lsp-mode
  (dolist (regexp my/lsp-generated-directory-watch-ignores)
    (unless (member regexp (default-value 'lsp-file-watch-ignored-directories))
      (set-default 'lsp-file-watch-ignored-directories
                   (cons regexp
                         (default-value 'lsp-file-watch-ignored-directories))))))

(defun my/python-pet-finalize-buffer-tools ()
  "Finalize order-sensitive Python tool variables after Pet setup."
  (setq-local python-shell-interpreter (pet-executable-find "python"))
  (setq-local lsp-pyright-langserver-command "pyright")
  (when (bound-and-true-p flycheck-mode)
    (pet-flycheck-toggle-local-vars)))

(with-eval-after-load 'pet
  (add-hook 'pet-after-buffer-local-vars-setup
            #'my/python-pet-finalize-buffer-tools))

2.7.3. Prevent lsp-mode org-element detection from calling Org mode

lsp-mode 10.0.0 probes the org-element API by opening a temporary org-mode buffer during package load. If that org-mode setup errors before lsp-mode finishes loading, Emacs 30 can corrupt cl-generic dispatch: later seq-empty-p calls can lose their list method and reloading lsp-mode can fail with lsp-execute-command is already defined as something else than a generic function.

This config uses Org 9.7+ (currently 9.8.x), so answer the API-version question directly and avoid the temporary org-mode buffer entirely. The advice is installed in user-init before lsp-mode loads; advising after lsp-mode is too late when org-element is already loaded.

Github issue: https://github.com/emacs-lsp/lsp-mode/issues/4898

(defun my/lsp-org-element-new-api-p ()
  "Return non-nil when Org uses the 9.7+ property-based element API."
  (let ((version (and (fboundp 'org-version)
                      (ignore-errors (org-version)))))
    (if version
        (version<= "9.7" version)
      ;; This Spacemacs config installs a modern Org.  Defaulting to the new
      ;; API keeps lsp-mode from opening an org-mode buffer during startup.
      t)))

(defun my/lsp-detect-org-element-api-without-org-mode (_orig-fn &rest _args)
  "Detect lsp-mode's Org element API support without calling `org-mode'."
  (my/lsp-org-element-new-api-p))

(advice-add 'lsp--detect-org-element-api
            :around #'my/lsp-detect-org-element-api-without-org-mode)

(with-eval-after-load 'lsp-mode
  (setq lsp--org-element-use-new-api (my/lsp-org-element-new-api-p)))

2.7.4. Workaround for unsigned packages

(setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3")

2.7.5. Acknowledge org-roam-v2

(setq org-roam-v2-ack t)

3. user-config

All code snippets under this section will be added to dotspacemacs//user-config function, which is

Configuration function for user code.

This function is called at the very end of Spacemacs initialization after layers configuration. You are free to put any user code.

3.1. Performance

3.1.1. gcmh (Garbage Collection Magic Hack)

Use idle-time garbage collection instead of a static threshold. Collects garbage when Emacs is idle, keeping GC threshold high during active use.

(use-package gcmh
  :config
  (setq gcmh-idle-delay 'auto       ; default: auto (based on `gcmh-auto-idle-delay-factor')
        gcmh-auto-idle-delay-factor 10
        gcmh-high-cons-threshold (* 128 1024 1024))  ; 128MB during active use
  (gcmh-mode 1))

3.2. IDE config

3.2.1. Highlight LaTeX fragments in org-mode

(setq org-highlight-latex-and-related '(native))

3.2.2. Set fill column to 100

(setq-default fill-column 100)

3.2.3. Sort Python imports with Ruff

(defvar my/python-ruff-sort-imports-on-save t
  "If non-nil, sort Python imports on save via Ruff's isort rules.")
(put 'my/python-ruff-sort-imports-on-save 'safe-local-variable #'booleanp)

(defun my/python-ruff-executable ()
  "Return the Ruff executable configured for the current buffer, or nil."
  (let ((command
         (if (and (boundp 'ruff-format-command)
                  (stringp ruff-format-command)
                  (not (string= ruff-format-command "")))
             ruff-format-command
           "ruff")))
    (executable-find command)))

(defun my/python-ruff-sort-imports ()
  "Sort imports in the current Python buffer using Ruff's `I' rules."
  (interactive)
  (when (and my/python-ruff-sort-imports-on-save
             (derived-mode-p 'python-mode 'python-ts-mode))
    (let ((ruff-executable (my/python-ruff-executable)))
      (unless ruff-executable
        (user-error "Cannot sort imports: ruff executable not found"))
      (let ((stdin-filename (or buffer-file-name
                                (expand-file-name "stdin.py" default-directory)))
            (output-buffer (generate-new-buffer " *ruff-import-sort*")))
        (unwind-protect
            (let ((exit-code
                   (call-process-region
                    (point-min) (point-max)
                    ruff-executable nil output-buffer nil
                    "check" "--fix-only" "--select" "I"
                    "--stdin-filename" stdin-filename "--silent" "-")))
              (unless (zerop exit-code)
                (user-error "Ruff import sorting failed with exit code %s" exit-code))
              (unless (string= (buffer-string)
                               (with-current-buffer output-buffer (buffer-string)))
                (replace-buffer-contents output-buffer)))
          (kill-buffer output-buffer))))))

(add-hook 'before-save-hook #'my/python-ruff-sort-imports)

3.2.4. Add new line when a file is about to be saved

(setq require-final-newline t)

3.2.5. Apply ANSI Color codes for buffer and region

(defun apply-ansi-color-codes-to-region (beginning end)
  "Apply ANSI color codes to the selected region."
  (interactive "r")
  (ansi-color-apply-on-region beginning end))

(defun apply-ansi-color-codes-to-buffer ()
  "Apply ANSI color codes to the whole buffer."
  (interactive)
  (apply-ansi-color-codes-to-region (point-min) (point-max)))

3.2.6. Disable auto-indent in C/C++ mode after typing ::

;; Disable electric indentation in C and C++ modes
(defun my-disable-electric-indent-mode ()
  (setq-local c-electric-flag nil))

;; Hook this function to C mode and C++ mode
(add-hook 'c-mode-hook 'my-disable-electric-indent-mode)
(add-hook 'c++-mode-hook 'my-disable-electric-indent-mode)

3.2.7. MacOS

; Use Command key as meta in emacs
(setq mac-option-key-is-meta nil
      mac-command-key-is-meta t
      mac-command-modifier 'meta
      mac-option-modifier 'none)

3.2.8. Prevent using UI dialogs for prompts

(setq use-dialog-box nil)

3.2.9. Prune deleted LSP workspace folders

  (require 'cl-lib)

  (defconst my/lsp-single-root-server-ids
    '(pyright pyright-remote ruff)
    "Server IDs whose obsolete persisted multi-root maps must be removed.")

  (defun my/lsp-existing-local-or-remote-directory-p (directory)
    "Return non-nil when DIRECTORY should remain in the LSP workspace cache."
    (and (stringp directory)
         (or (file-remote-p directory)
             (file-directory-p directory))))

  (defvar my/lsp--session-for-folder-update nil)
  (defvar my/lsp--folders-for-session-update nil)

  (defun my/lsp-set-session-folders (session folders)
    "Set SESSION's folder list after lsp-mode has defined its struct setter.
Keep the `setf' form quoted so byte-compiling this config before lsp-mode loads
cannot compile against an unknown `lsp-session-folders' setter."
    (let ((my/lsp--session-for-folder-update session)
          (my/lsp--folders-for-session-update folders))
      (eval '(setf (lsp-session-folders my/lsp--session-for-folder-update)
                   my/lsp--folders-for-session-update))))

  ;; Safety: this only removes missing folders and obsolete single-root server
  ;; maps from `lsp-mode' session metadata, then rewrites `.lsp-session-v1'. It
  ;; never deletes workspace directories or files.
  (defun my/lsp-prune-missing-workspace-folders (&rest _)
    "Remove missing folders and obsolete multi-root maps from the LSP session."
    (interactive)
    (when (and (fboundp 'lsp-session)
               (fboundp 'lsp--persist-session))
      (let* ((session (lsp-session))
             (changed nil))
        (cl-labels
            ((keep-directory-p
              (directory)
              (if (my/lsp-existing-local-or-remote-directory-p directory)
                  t
                (setq changed t)
                (message "Pruned missing LSP workspace folder: %s" directory)
                nil))
             (prune-folders
              (folders)
              (let (kept)
                (dolist (directory folders (nreverse kept))
                  (cond
                   ((not (keep-directory-p directory)))
                   ((member directory kept)
                    (setq changed t))
                   (t
                    (push directory kept)))))))
          (my/lsp-set-session-folders
           session (prune-folders (lsp-session-folders session)))
          (let (server-ids-to-remove)
            (maphash
             (lambda (server-id folders)
               (if (memq server-id my/lsp-single-root-server-ids)
                   (progn
                     (setq changed t)
                     (push server-id server-ids-to-remove))
                 (let ((pruned-folders (prune-folders folders)))
                   (if pruned-folders
                       (puthash server-id pruned-folders
                                (lsp-session-server-id->folders session))
                     (push server-id server-ids-to-remove)))))
             (lsp-session-server-id->folders session))
            (dolist (server-id server-ids-to-remove)
              (remhash server-id (lsp-session-server-id->folders session))))
          (let (dead-folder-keys)
            (maphash
             (lambda (directory _workspaces)
               (unless (keep-directory-p directory)
                 (push directory dead-folder-keys)))
             (lsp-session-folder->servers session))
            (dolist (directory dead-folder-keys)
              (remhash directory (lsp-session-folder->servers session))))
          (when changed
            (lsp--persist-session session))))))

  (defun my/lsp-prune-missing-workspace-folders-safely (&rest args)
    "Sanitize stale LSP session metadata without aborting lsp-mode loading."
    (condition-case err
        (apply #'my/lsp-prune-missing-workspace-folders args)
      (error
       (message "Skipping LSP workspace folder pruning after error: %S" err))))

  (with-eval-after-load 'lsp-mode
    (when (advice-member-p #'my/lsp-prune-missing-workspace-folders 'lsp)
      (advice-remove 'lsp #'my/lsp-prune-missing-workspace-folders))
    (unless (advice-member-p #'my/lsp-prune-missing-workspace-folders-safely 'lsp)
      (advice-add 'lsp :before #'my/lsp-prune-missing-workspace-folders-safely))
    (my/lsp-prune-missing-workspace-folders-safely))

3.2.10. ccls

(with-eval-after-load 'ccls
  (setq ccls-root-files (add-to-list 'ccls-root-files "build/compile_commands.json" t))
  (setq ccls-sem-highlight-method 'font-lock)
  (setq ccls-initialization-options
        (list :cache (list :directory (file-truename (concat (file-name-as-directory spacemacs-cache-directory) ".ccls-cache")))
              :compilationDatabaseDirectory "build"))
  ;; Only set the specific ccls path if it exists
  (when (file-exists-p "~/.spacemacs.d/ccls/Release/ccls")
    (setq ccls-executable (file-truename "~/.spacemacs.d/ccls/Release/ccls"))))

3.2.11. devcontainer

Enable devcontainer-mode globally so that M-x compile (SPC c c) runs inside the devcontainer when the project has a .devcontainer/devcontainer.json. The devcontainer CLI is installed outside the default PATH, so we add its directory to exec-path.

  ;; Make the devcontainer CLI discoverable
  (let ((devcontainer-bin-dir (expand-file-name "~/.devcontainers/bin")))
    (when (file-directory-p devcontainer-bin-dir)
      (add-to-list 'exec-path devcontainer-bin-dir)))

  ;; Enable devcontainer-mode globally — advises `compilation-start'
  ;; to run inside the container when a devcontainer.json is present
  (devcontainer-mode 1)

  ;; Use vterm for devcontainer-term
  (with-eval-after-load 'devcontainer
    (defun devcontainer-vterm (command)
      "Open a vterm buffer running COMMAND inside the devcontainer.
Creates a new terminal each time; buffers are named with incrementing numbers."
      (require 'vterm)
      (let* ((base-name (format "*devcontainer-term: %s*"
                                (project-name (project-current))))
             (buf (generate-new-buffer base-name)))
        (with-current-buffer buf
          (let ((vterm-shell command))
            (vterm-mode)))
        (pop-to-buffer buf)))
    (setq devcontainer-term-function #'devcontainer-vterm)

    ;; Fix: devcontainer.el does not implement ${localEnv:VAR}
    ;; interpolation, which causes errors when remoteEnv references
    ;; host environment variables (e.g. DISPLAY).
    (advice-add 'devcontainer--lookup-variable :around
                (lambda (orig-fn match)
                  "Add ${localEnv:VAR} support to devcontainer variable interpolation."
                  (or (funcall orig-fn match)
                      (save-match-data
                        (when (string-match "\\${localEnv:\\([^}]+\\)}" match)
                          (or (getenv (match-string 1 match)) "")))))))

  ;; Keybindings under SPC d
  (spacemacs/declare-prefix "DD" "devcontainer")
  (spacemacs/set-leader-keys "DDu" 'devcontainer-up)
  (spacemacs/set-leader-keys "DDr" 'devcontainer-restart)
  (spacemacs/set-leader-keys "DDR" 'devcontainer-rebuild-and-restart)
  (spacemacs/set-leader-keys "DDt" 'devcontainer-term)
  (spacemacs/set-leader-keys "DDx" 'devcontainer-execute-command)
  (spacemacs/set-leader-keys "DDk" 'devcontainer-kill-container)

3.2.12. cmake-ide

;; C++ build dir setting
(put 'cmake-ide-dir 'safe-local-variable 'stringp)
(put 'cmake-ide-make-command 'safe-local-variable 'stringp)
(put 'cmake-ide-cmake-args 'safe-local-variable 'stringp)

;; Configure neocmakelsp
(with-eval-after-load 'lsp-mode
  ;; Note: 'stdio' should not have dashes
  (lsp-register-client
   (make-lsp-client :new-connection (lsp-stdio-connection '("neocmakelsp" "stdio"))
                    :activation-fn (lsp-activate-on "cmake")
                    :server-id 'neocmakelsp)))

(add-hook 'cmake-mode-hook #'lsp)

3.2.13. company

(with-eval-after-load 'company
  (define-key company-active-map (kbd "M-n") nil)
  (define-key company-active-map (kbd "M-p") nil)
  (define-key company-active-map (kbd "C-j") 'company-select-next)
  (define-key company-active-map (kbd "C-k") 'company-select-previous))

3.2.14. minuet-ai

Use Minuet with an OpenAI-compatible completion endpoint. The default profile is IceBear codex-proxy at http://127.0.0.1:8787/v1/chat/completions with model gpt-5.5 and low reasoning effort, using Codex Pro/ChatGPT quota through the local proxy. Run the proxy locally, log in through its dashboard, and export CODEX_PROXY_API_KEY with the dashboard API key.

Switch profiles at runtime with SPC a i C for Codex proxy, SPC a i Q for local Qwen, or SPC a i R to choose a named profile. Override startup defaults with MINUET_OPENAI_COMPATIBLE_PROFILE (codex-proxy or local-qwen), MINUET_OPENAI_COMPATIBLE_ENDPOINT, MINUET_OPENAI_COMPATIBLE_MODEL, or MINUET_OPENAI_COMPATIBLE_API_KEY_SOURCE. At runtime, use SPC a i M to select another endpoint/model; models are fetched automatically from the endpoint's /models API when available.

  (use-package minuet
    :commands (minuet-complete-with-minibuffer
               minuet-show-suggestion
               minuet-configure-provider
               minuet-auto-suggestion-mode)
    :bind (("M-i" . minuet-show-suggestion)
           ("C-c m" . minuet-configure-provider)
           :map minuet-active-mode-map
           ("M-p" . minuet-previous-suggestion)
           ("M-n" . minuet-next-suggestion)
           ("TAB" . minuet-accept-suggestion)
           ("<tab>" . minuet-accept-suggestion)
           ("M-A" . minuet-accept-suggestion)
           ("M-a" . minuet-accept-suggestion-line)
           ("M-e" . minuet-dismiss-suggestion))
    :init
    (defconst my/minuet-openai-compatible-default-profile "codex-proxy"
      "Default named OpenAI-compatible profile for Minuet.")

    (defun my/minuet-openai-compatible-profile-names ()
      "Return known Minuet OpenAI-compatible profile names."
      '("codex-proxy" "local-qwen"))

    (defvar my/minuet-openai-compatible-profile
      (or (getenv "MINUET_OPENAI_COMPATIBLE_PROFILE")
          my/minuet-openai-compatible-default-profile)
      "Current named OpenAI-compatible profile for Minuet.")

    (defun my/minuet-openai-compatible-profile-defaults (profile)
      "Return default settings for Minuet OpenAI-compatible PROFILE."
      (pcase profile
        ("local-qwen"
         (list :end-point (or (getenv "MINUET_QWEN_ENDPOINT")
                              "http://127.0.0.1:13636/v1/chat/completions")
               :model (or (getenv "MINUET_QWEN_MODEL")
                          "qwen3.6-27b")
               :api-key-source (or (getenv "MINUET_QWEN_API_KEY_SOURCE")
                                   "local")))
        ("codex-proxy"
         (list :end-point "http://127.0.0.1:8787/v1/chat/completions"
               :model "gpt-5.5"
               :api-key-source "CODEX_PROXY_API_KEY"))
        (_
         (my/minuet-openai-compatible-profile-defaults
          my/minuet-openai-compatible-default-profile))))

    (defvar my/minuet-openai-compatible-endpoint
      (or (getenv "MINUET_OPENAI_COMPATIBLE_ENDPOINT")
          (plist-get (my/minuet-openai-compatible-profile-defaults
                      my/minuet-openai-compatible-profile)
                     :end-point))
      "OpenAI-compatible chat completion endpoint used by Minuet.")

    (defvar my/minuet-openai-compatible-model
      (or (getenv "MINUET_OPENAI_COMPATIBLE_MODEL")
          (plist-get (my/minuet-openai-compatible-profile-defaults
                      my/minuet-openai-compatible-profile)
                     :model))
      "OpenAI-compatible model used by Minuet.")

    (defvar my/minuet-openai-compatible-api-key-source
      (or (getenv "MINUET_OPENAI_COMPATIBLE_API_KEY_SOURCE")
          (plist-get (my/minuet-openai-compatible-profile-defaults
                      my/minuet-openai-compatible-profile)
                     :api-key-source))
      "Environment variable name, placeholder, or runtime API key for Minuet.")

    (defun my/minuet-codex-proxy-api-key-from-config ()
      "Return the local codex-proxy API key from its generated config file."
      (let ((config-file (expand-file-name "~/.local/share/codex-proxy/data/local.yaml")))
        (when (file-readable-p config-file)
          (with-temp-buffer
            (insert-file-contents config-file)
            (goto-char (point-min))
            (when (re-search-forward
                   "^[[:space:]]*proxy_api_key:[[:space:]]*\\(?:\"\\([^\"]+\\)\"\\|'\\([^']+\\)'\\|\\([^[:space:]#]+\\)\\)"
                   nil
                   t)
              (or (match-string 1)
                  (match-string 2)
                  (match-string 3)))))))

    (defun my/minuet-openai-compatible-api-key ()
      "Return the API key for Minuet's OpenAI-compatible provider."
      (let ((source my/minuet-openai-compatible-api-key-source))
        (or (and (stringp source) (getenv source))
            (and (stringp source)
                 (string= source "CODEX_PROXY_API_KEY")
                 (my/minuet-codex-proxy-api-key-from-config))
            (and (stringp source) (not (string= source "")) source)
            "local")))

    (defvar my/minuet-conventional-commit-prompt
      "When completing a Git commit message:
- If non-comment commit text already appears before `<cursorPosition>`, treat it as fixed user text and continue after the cursor only.
- Do not rewrite, repeat, or prepend a Conventional Commit prefix before already typed commit text.
- If the subject is empty, produce a Conventional Commit using `type(scope): summary`, e.g. `feat(minuet): add model selector`.
- Use the staged diff as the source of truth when it is present in the prompt.
- Choose a lowercase type such as `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `build`, `ci`, `chore`, `perf`, or `revert`.
- Keep the subject concise, imperative, and ideally under 72 characters.
- For non-trivial staged changes, include a body after one blank line.
- Body lines should explain why and summarize important areas changed; prefer 1-3 concise bullet points.
- Return only the text to insert after the cursor, without commentary or markdown fences."
      "Additional Minuet guidance for `git-commit-mode'.")

    (defun my/minuet-git-commit-buffer-p ()
      "Return non-nil when the current buffer is a Git commit message buffer."
      (or (bound-and-true-p git-commit-mode)
          (derived-mode-p 'git-commit-elisp-text-mode)))

    (defvar my/minuet-git-staged-diff-max-chars 12000
      "Maximum number of staged diff characters to include in Minuet prompts.")

    (defun my/minuet-git-root ()
      "Return the current Git repository root, or nil outside a Git repo."
      (locate-dominating-file default-directory ".git"))

    (defun my/minuet-git-command-output (&rest args)
      "Return output from git ARGS in the current repository, or nil on failure."
      (require 'subr-x)
      (when-let* ((root (my/minuet-git-root)))
        (with-temp-buffer
          (let ((default-directory root))
            (when (zerop (apply #'process-file "git" nil t nil args))
              (string-trim-right (buffer-string)))))))

    (defun my/minuet-truncate-for-prompt (text max-chars)
      "Return TEXT truncated to MAX-CHARS for prompt inclusion."
      (if (and text (> (length text) max-chars))
          (concat (substring text 0 max-chars)
                  "\n\n[staged diff truncated for prompt]")
        text))

    (defun my/minuet-git-staged-diff-for-prompt (_context)
      "Return staged Git changes formatted for Minuet commit prompts."
      (when (my/minuet-git-commit-buffer-p)
        (let* ((stat (my/minuet-git-command-output "diff" "--cached" "--stat"))
               (diff (my/minuet-git-command-output "diff" "--cached" "--"))
               (diff (my/minuet-truncate-for-prompt
                      diff
                      my/minuet-git-staged-diff-max-chars)))
          (mapconcat #'identity
                     (delq nil
                           (list (unless (string= (or stat "") "")
                                   (concat "Staged diffstat:\n" stat))
                                 (unless (string= (or diff "") "")
                                   (concat "Staged diff:\n" diff))))
                     "\n\n"))))

    (defun my/minuet-openai-compatible-chat-template ()
      "Return a mode-aware chat input template for Minuet."
      (if (my/minuet-git-commit-buffer-p)
          "{{{:language-and-tab}}}
<contextBeforeCursor>
{{{:context-before-cursor}}}<cursorPosition>
<contextAfterCursor>
{{{:context-after-cursor}}}
<stagedChanges>
{{{:staged-diff}}}
</stagedChanges>"
        minuet-default-chat-input-template-prefix-first))

    (defun my/minuet-openai-compatible-guidelines ()
      "Return mode-aware generation guidelines for Minuet."
      (if (my/minuet-git-commit-buffer-p)
          "Guidelines:
1. Complete text after the `<cursorPosition>` marker; preserve all user-authored text before it.
2. If non-comment commit text already exists before the cursor, continue that text only. Do not start a new subject, repeat the typed words, or prepend `type(scope):`.
3. If the subject is empty, produce one complete Conventional Commit message with `type(scope): summary` or `type: summary`.
4. For non-trivial staged changes, include a blank line followed by a body.
5. Body lines should be concise bullets or short wrapped paragraphs; explain why, not every mechanical detail.
6. Use the staged diff as source of truth. Do not invent changes.
7. Return only the text to insert after the cursor. If multiple alternatives are provided, separate them with <endCompletion>."
        minuet-default-guidelines))

    (defun my/minuet-openai-compatible-system-prompt ()
      "Return a mode-aware system prompt for Minuet's OpenAI-compatible provider."
      (if (my/minuet-git-commit-buffer-p)
          (concat minuet-default-prompt-prefix-first
                  "\n"
                  my/minuet-conventional-commit-prompt)
        minuet-default-prompt-prefix-first))

    (defun my/minuet-openai-compatible-fewshots ()
      "Return mode-aware few-shot examples for Minuet's OpenAI-compatible provider."
      (unless (my/minuet-git-commit-buffer-p)
        minuet-default-fewshots-prefix-first))

    (defun my/minuet-openai-compatible-optional-params ()
      "Return optional request params for Minuet's current profile."
      (append
       '(:max_tokens 128
         :temperature 0.2
         :top_p 0.9)
       (pcase my/minuet-openai-compatible-profile
         ("codex-proxy" '(:reasoning_effort "low"))
         ("local-qwen" '(:chat_template_kwargs (:enable_thinking :false))))))

    (defun my/minuet-openai-compatible-models-endpoint (endpoint)
      "Return the OpenAI-compatible models endpoint for chat ENDPOINT."
      (cond
       ((string-match-p "/models\\(?:[?#].*\\)?\\'" endpoint) endpoint)
       ((string-match "/chat/completions\\(?:[?#].*\\)?\\'" endpoint)
        (replace-match "/models" t t endpoint))
       ((string-match "/completions\\(?:[?#].*\\)?\\'" endpoint)
        (replace-match "/models" t t endpoint))
       ((string-match "/responses\\(?:[?#].*\\)?\\'" endpoint)
        (replace-match "/models" t t endpoint))
       ((string-match-p "/\\'" endpoint) (concat endpoint "models"))
       (t (concat endpoint "/models"))))

    (defun my/minuet-openai-compatible--model-name (entry)
      "Return a model name from an OpenAI-compatible model ENTRY."
      (cond
       ((stringp entry) entry)
       ((listp entry)
        (or (plist-get entry :id)
            (plist-get entry :model)
            (plist-get entry :name)))))

    (defun my/minuet-openai-compatible-fetch-models (endpoint api-key-source)
      "Fetch model names from ENDPOINT using API-KEY-SOURCE."
      (require 'json)
      (require 'url)
      (let* ((models-endpoint (my/minuet-openai-compatible-models-endpoint endpoint))
             (api-key (let ((my/minuet-openai-compatible-api-key-source api-key-source))
                        (my/minuet-openai-compatible-api-key)))
             (url-request-extra-headers `(("Accept" . "application/json")
                                          ("Authorization" . ,(concat "Bearer " api-key))))
             (url-show-status nil)
             (buffer (condition-case err
                         (url-retrieve-synchronously models-endpoint t t 5)
                       (error
                        (message "Could not fetch Minuet models from %s: %s"
                                 models-endpoint
                                 (error-message-string err))
                        nil))))
        (if (not buffer)
            (progn
              (message "Could not fetch Minuet models from %s" models-endpoint)
              nil)
          (unwind-protect
              (with-current-buffer buffer
                (goto-char (point-min))
                (if (not (re-search-forward "\r?\n\r?\n" nil t))
                    (progn
                      (message "Could not parse Minuet models response from %s" models-endpoint)
                      nil)
                  (condition-case err
                      (let* ((json-object (json-parse-string
                                           (buffer-substring-no-properties (point) (point-max))
                                           :object-type 'plist
                                           :array-type 'list
                                           :false-object nil))
                             (entries (or (plist-get json-object :data)
                                          (plist-get json-object :models)))
                             (models (delq nil
                                           (mapcar #'my/minuet-openai-compatible--model-name
                                                   entries))))
                        (sort (delete-dups models) #'string-lessp))
                    (error
                     (message "Could not parse Minuet models from %s: %s"
                              models-endpoint
                              (error-message-string err))
                     nil))))
            (kill-buffer buffer)))))

    (defun my/minuet-apply-openai-compatible-settings ()
      "Apply OpenAI-compatible settings to Minuet."
      (setq minuet-provider 'openai-compatible)
      (plist-put minuet-openai-compatible-options
                 :end-point
                 my/minuet-openai-compatible-endpoint)
      (plist-put minuet-openai-compatible-options
                 :api-key
                 #'my/minuet-openai-compatible-api-key)
      (plist-put minuet-openai-compatible-options
                 :model
                 my/minuet-openai-compatible-model)
      (setf (plist-get (plist-get minuet-openai-compatible-options :system) :prompt)
            #'my/minuet-openai-compatible-system-prompt)
      (setf (plist-get (plist-get minuet-openai-compatible-options :system) :guidelines)
            #'my/minuet-openai-compatible-guidelines)
      (setf (plist-get (plist-get minuet-openai-compatible-options :chat-input) :template)
            #'my/minuet-openai-compatible-chat-template)
      (setf (plist-get (plist-get minuet-openai-compatible-options :chat-input) :staged-diff)
            #'my/minuet-git-staged-diff-for-prompt)
      (plist-put minuet-openai-compatible-options
                 :fewshots
                 #'my/minuet-openai-compatible-fewshots)
      (plist-put minuet-openai-compatible-options
                 :optional
                 (my/minuet-openai-compatible-optional-params)))

    (defun my/minuet-set-openai-compatible-model (endpoint api-key-source model)
      "Interactively set Minuet ENDPOINT, API-KEY-SOURCE, and MODEL."
      (interactive
       (progn
         (require 'minuet)
         (let* ((endpoint (read-string "Minuet endpoint: "
                                       my/minuet-openai-compatible-endpoint))
                (api-key-source (read-string
                                 "API key env var or placeholder: "
                                 my/minuet-openai-compatible-api-key-source))
                (models (my/minuet-openai-compatible-fetch-models endpoint api-key-source))
                (default-model (or (and (member my/minuet-openai-compatible-model models)
                                        my/minuet-openai-compatible-model)
                                   (car models)
                                   my/minuet-openai-compatible-model))
                (model (completing-read "Minuet model: " models nil nil nil nil default-model)))
           (list endpoint api-key-source model))))
      (require 'minuet)
      (setq my/minuet-openai-compatible-endpoint endpoint
            my/minuet-openai-compatible-api-key-source api-key-source
            my/minuet-openai-compatible-model model)
      (my/minuet-apply-openai-compatible-settings)
      (message "Minuet model set to %s at %s" model endpoint))

    (defun my/minuet-set-openai-compatible-profile (profile)
      "Switch Minuet to named OpenAI-compatible PROFILE."
      (interactive
       (list (completing-read "Minuet profile: "
                              (my/minuet-openai-compatible-profile-names)
                              nil
                              t
                              nil
                              nil
                              my/minuet-openai-compatible-profile)))
      (let* ((profile (if (member profile (my/minuet-openai-compatible-profile-names))
                          profile
                        my/minuet-openai-compatible-default-profile))
             (defaults (my/minuet-openai-compatible-profile-defaults profile))
             (endpoint (plist-get defaults :end-point))
             (model (plist-get defaults :model))
             (api-key-source (plist-get defaults :api-key-source)))
        (require 'minuet)
        (setq my/minuet-openai-compatible-profile profile
              my/minuet-openai-compatible-endpoint endpoint
              my/minuet-openai-compatible-api-key-source api-key-source
              my/minuet-openai-compatible-model model)
        (my/minuet-apply-openai-compatible-settings)
        (message "Minuet profile set to %s (%s at %s)" profile model endpoint)))

    (defun my/minuet-use-codex-proxy ()
      "Switch Minuet to the Codex proxy profile."
      (interactive)
      (my/minuet-set-openai-compatible-profile "codex-proxy"))

    (defun my/minuet-use-local-qwen ()
      "Switch Minuet to the local Qwen profile."
      (interactive)
      (my/minuet-set-openai-compatible-profile "local-qwen"))

    (add-hook 'prog-mode-hook #'minuet-auto-suggestion-mode)
    (add-hook 'git-commit-mode-hook #'minuet-auto-suggestion-mode)
    (add-hook 'markdown-mode-hook #'minuet-auto-suggestion-mode)
    (add-hook 'org-mode-hook #'minuet-auto-suggestion-mode)

    (spacemacs/declare-prefix "ai" "AI tools")
    (spacemacs/set-leader-keys
      "ais" 'minuet-show-suggestion
      "aim" 'minuet-complete-with-minibuffer
      "aiM" 'my/minuet-set-openai-compatible-model
      "aiR" 'my/minuet-set-openai-compatible-profile
      "aiC" 'my/minuet-use-codex-proxy
      "aiQ" 'my/minuet-use-local-qwen
      "aic" 'minuet-configure-provider
      "ait" 'minuet-auto-suggestion-mode)
    :config
    (setq minuet-n-completions 1
          minuet-context-window 4096
          minuet-request-timeout 12
          minuet-auto-suggestion-debounce-delay 0.8
          minuet-auto-suggestion-throttle-delay 1.5)

    (my/minuet-apply-openai-compatible-settings))

3.2.15. codex-proxy

Manage the local IceBear codex-proxy Docker Compose service used by Minuet's Codex profile. The service lives in ~/.local/share/codex-proxy and exposes the dashboard/API at http://127.0.0.1:8787.

(defvar my/codex-proxy-directory "~/.local/share/codex-proxy"
  "Directory containing the local codex-proxy Docker Compose setup.")

(defvar my/codex-proxy-dashboard-url "http://127.0.0.1:8787"
  "Dashboard URL for the local codex-proxy service.")

(defun my/codex-proxy--default-directory ()
  "Return the codex-proxy Compose directory or signal a user error."
  (let ((directory (file-name-as-directory
                    (expand-file-name my/codex-proxy-directory))))
    (unless (file-directory-p directory)
      (user-error "codex-proxy directory does not exist: %s" directory))
    directory))

(defun my/codex-proxy--run-docker-compose (arguments)
  "Run docker compose with ARGUMENTS in `my/codex-proxy-directory'."
  (let ((default-directory (my/codex-proxy--default-directory)))
    (async-shell-command (concat "docker compose " arguments) "*codex-proxy*")))

(defun my/codex-proxy-docker-up ()
  "Start the local codex-proxy Docker Compose service."
  (interactive)
  (my/codex-proxy--run-docker-compose "up -d"))

(defun my/codex-proxy-docker-down ()
  "Stop and remove the local codex-proxy Docker Compose service."
  (interactive)
  (my/codex-proxy--run-docker-compose "down"))

(defun my/codex-proxy-docker-restart ()
  "Restart the local codex-proxy container."
  (interactive)
  (my/codex-proxy--run-docker-compose "restart codex-proxy"))

(defun my/codex-proxy-docker-status ()
  "Show the local codex-proxy Docker Compose status."
  (interactive)
  (my/codex-proxy--run-docker-compose "ps"))

(defun my/codex-proxy-docker-logs ()
  "Follow recent logs for the local codex-proxy container."
  (interactive)
  (my/codex-proxy--run-docker-compose "logs --tail=200 -f codex-proxy"))

(defun my/codex-proxy-open-dashboard ()
  "Open the local codex-proxy dashboard."
  (interactive)
  (browse-url my/codex-proxy-dashboard-url))

(spacemacs/declare-prefix "aid" "codex-proxy")
(spacemacs/set-leader-keys
  "aidu" 'my/codex-proxy-docker-up
  "aidd" 'my/codex-proxy-docker-down
  "aidr" 'my/codex-proxy-docker-restart
  "aids" 'my/codex-proxy-docker-status
  "aidl" 'my/codex-proxy-docker-logs
  "aido" 'my/codex-proxy-open-dashboard)

3.2.16. gptel

Use gptel's direct OpenAI OAuth backend for the ChatGPT Plus/Pro Codex subscription.

(spacemacs/declare-prefix "aig" "gptel")
(spacemacs/set-leader-keys
  "aigg" 'gptel
  "aigs" 'spacemacs//gptel-send-wrapper
  "aigq" 'spacemacs//gptel-abort-wrapper
  "aigm" 'gptel-menu
  "aigc" 'gptel-add
  "aigf" 'gptel-add-file
  "aigr" 'gptel-rewrite)

(with-eval-after-load 'gptel
  (require 'gptel-openai-oauth)
  (setq gptel-default-mode 'org-mode
        gptel-model 'gpt-5.5
        gptel-backend (gptel-make-openai-oauth
                        "OpenAI-sub"
                        :request-params '(:reasoning (:effort "xhigh")))))

3.2.17. pi-coding-agent

Emacs frontend for the pi coding agent. Requires the pi CLI to be installed, available in PATH, and authenticated with pi --login (or provider API keys configured).

(use-package pi-coding-agent
  :commands (pi-coding-agent
             pi-coding-agent-toggle)
  :init
  (defalias 'pi 'pi-coding-agent)

  (spacemacs/declare-prefix "ai" "AI tools")
  (spacemacs/set-leader-keys
    "aip" 'pi-coding-agent
    "aiP" 'pi-coding-agent-toggle))

3.2.18. lsp-ui

(with-eval-after-load 'lsp-ui
  (define-key lsp-ui-peek-mode-map (kbd "C-j") 'lsp-ui-peek--select-next)
  (define-key lsp-ui-peek-mode-map (kbd "j") 'lsp-ui-peek--select-next)
  (define-key lsp-ui-peek-mode-map (kbd "C-k") 'lsp-ui-peek--select-prev)
  (define-key lsp-ui-peek-mode-map (kbd "k") 'lsp-ui-peek--select-prev))

3.2.19. popup

Use evil keybindings for selecting items in popup menus.

(with-eval-after-load 'popup
  (define-key popup-menu-keymap (kbd "C-j") 'popup-next)
  (define-key popup-menu-keymap (kbd "C-k") 'popup-previous))

3.2.20. dap

(defun my/show-dap-hydra (_arg)
  "Show `dap-hydra' when DAP stops at a breakpoint."
  (call-interactively #'dap-hydra))

;; Remove previous anonymous hook variants when reloading this config.
(require 'cl-lib)
(when (boundp 'dap-stopped-hook)
  (setq dap-stopped-hook
        (cl-remove-if
         (lambda (hook)
           (and (not (eq hook #'my/show-dap-hydra))
                (not (symbolp hook))
                (string-match-p "\\bdap-hydra\\b" (prin1-to-string hook))))
         dap-stopped-hook)))
(add-hook 'dap-stopped-hook #'my/show-dap-hydra)

3.2.21. flycheck

(with-eval-after-load 'flycheck
  (setq flycheck-check-syntax-automatically '(save
                                              idle-buffer-switch
                                              mode-enabled)))

3.2.22. Indentation for web development

(defun setup-web-dev-indent (n)
  ;; web development
  (setq coffee-tab-width n) ; coffeescript
  (setq javascript-indent-level n) ; javascript-mode
  (setq js-indent-level n) ; js-mode
  (setq js2-basic-offset n) ; js2-mode, in latest js2-mode, it's alias of js-indent-level
  (setq web-mode-markup-indent-offset n) ; web-mode, html tag in html file
  (setq web-mode-css-indent-offset n) ; web-mode, css in html file
  (setq web-mode-code-indent-offset n) ; web-mode, js code in html file
  (setq css-indent-offset n) ; css-mode
  )
(setup-web-dev-indent 2)

3.2.23. groovy (Jenkinsfile)

(setq groovy-indent-offset 2)

3.2.24. plantuml

(when (fboundp 'plantuml-mode)
  (require 'org-src)
  ;; Enable plantuml-mode for all *.pu files by default
  (add-to-list 'auto-mode-alist '("\\.pu\\'" . plantuml-mode))
  (setq org-plantuml-jar-path plantuml-jar-path)
  (add-to-list 'org-src-lang-modes '("plantuml" . plantuml))
  (org-babel-do-load-languages 'org-babel-load-languages '((plantuml . t)))
  (setq plantuml-svg-background "white")
  (setq plantuml-indent-level 2)
  )

3.2.25. nvm

;; TODO: use the default version instead of hard-coding the specific version
(condition-case err
    (nvm-use "22")
  (error (message "Could not initialize nvm for emacs. %s" (error-message-string err))))

3.2.26. conf-mode

(add-to-list 'auto-mode-alist '("\\.eds\\'" . conf-mode))
(add-to-list 'auto-mode-alist '("\\.dcf\\'" . conf-mode))

3.2.27. Rust

3.2.27.1. Allow user input in the rust-run command
(spacemacs/set-leader-keys-for-major-mode 'rustic-mode "cx" 'rustic-cargo-run-with-user-input)
(defun rustic-cargo-run-with-user-input ()
  "Build and run Rust code."
  (interactive)
  (rustic-cargo-run)
  (let (
        (orig-win (selected-window))
        (run-win (display-buffer (get-buffer "*cargo-run*") nil 'visible))
        )
    (select-window run-win)
    (comint-mode)
    (read-only-mode 0)
    (select-window orig-win)
    )
  )

3.2.28. scad-mode

(use-package scad-mode
  :load-path "~/.spacemacs.d/private/scad-mode")
(add-hook 'scad-mode-hook 'flymake-mode-on)
(with-eval-after-load 'scad-mode
  (define-key scad-preview-mode-map (kbd "C-h") 'scad-preview-rotate-z-)
  (define-key scad-preview-mode-map (kbd "C-l") 'scad-preview-rotate-z+)
  (define-key scad-preview-mode-map (kbd "C-k") 'scad-preview-rotate-x-)
  (define-key scad-preview-mode-map (kbd "C-j") 'scad-preview-rotate-x+)
  (define-key scad-preview-mode-map (kbd "M-h") 'scad-preview-distance-)
  (define-key scad-preview-mode-map (kbd "M-l") 'scad-preview-distance+)
  (define-key scad-preview-mode-map (kbd "M-k") 'scad-preview-translate-z+)
  (define-key scad-preview-mode-map (kbd "M-j") 'scad-preview-translate-z-))

3.2.29. Style for linux kernel development

;; Linux kernel development
(defun c-lineup-arglist-tabs-only (ignored)
  "Line up argument lists by tabs, not spaces"
  (let* ((anchor (c-langelem-pos c-syntactic-element))
         (column (c-langelem-2nd-pos c-syntactic-element))
         (offset (- (1+ column) anchor))
         (steps (floor offset c-basic-offset)))
    (* (max steps 1)
       c-basic-offset)))

(add-hook 'c-mode-common-hook
          (lambda ()
            ;; Add kernel style
            (c-add-style
             "linux-tabs-only"
             '("linux" (c-offsets-alist
                        (arglist-cont-nonempty
                         c-lineup-gcc-asm-reg
                         c-lineup-arglist-tabs-only))))))
(add-hook 'c-mode-hook
          (lambda ()
            (let ((filename (buffer-file-name)))
              ;; Enable kernel mode for the appropriate files
              (when (and filename
                         ;; TODO: avoid the harded coded path
                         (string-match (expand-file-name "~/Dev/kernels")
                                       filename))
                (setq indent-tabs-mode t)
                (setq show-trailing-whitespace t)
                (c-set-style "linux-tabs-only")))))

3.2.30. tramp + clang-format

clang-format does not work properly when editing a file on a remote host or in a docker container with tramp. See the issue here: https://github.com/kljohann/clang-format.el/issues/5

Here upon clang-format-region call, we first check if the file is on a remote host or in a docker container.

  • If not, we call clang-format-region as usual.
  • If yes, we first check if a file with the same path exists on the local disk.
    • If yes, we assume it as the input file name for clang-format-region.
    • If not, we assume the input file is under the $HOME directory.

Depending on where the input file is assumed to be, clang-format will find the .clang-format file for the formatting in a dominant parent directory of the assumed input file path.

(defun tramp-aware-clang-format (orig-fun start end &optional style assume-file-name)
  (unless assume-file-name
    (setq assume-file-name
          (if (file-remote-p buffer-file-name)
              (let ((maybe-existing-local-buffer-file-name (replace-regexp-in-string "/docker:[^:]+:" "" buffer-file-name)))
                ;; If file `maybe-existing-local-buffer-file-name' exists on local disk, use it.
                (if (file-exists-p maybe-existing-local-buffer-file-name)
                    maybe-existing-local-buffer-file-name
                  ;; Otherwise, use `buffer-file-name' as if it is under the $HOME directory.
                  (concat (getenv "HOME") "/" (file-name-nondirectory buffer-file-name))))
            buffer-file-name)))
  (message "assume-file-name: %s" assume-file-name)
  (apply orig-fun (list start end style assume-file-name)))

(advice-add 'clang-format-region :around #'tramp-aware-clang-format)

3.2.31. Get current branch name in magit

(defun magit-add-current-branch-name-to-kill-ring ()
  "Show the current branch in the echo-area and add it to the `kill-ring'."
  (interactive)
  (let ((branch (magit-get-current-branch)))
    (if branch
        (progn (kill-new branch)
               (message "%s" branch))
      (user-error "There is not current branch"))))

; TODO: Move this keybinding from "Checkout" to "Do" section
(with-eval-after-load 'magit
  (transient-insert-suffix 'magit-branch "b"
    '("k" "copy branch name" magit-add-current-branch-name-to-kill-ring)))

3.3. Miscellaneous

3.3.1. popper

Tame ephemeral popup buffers (help, compilation, shells, REPLs). Toggle with C-`, cycle with M-`, promote/demote with C-M-`.

(use-package popper
  :bind (("C-`"   . popper-toggle)
         ("M-`"   . popper-cycle)
         ("C-M-`" . popper-toggle-type))
  :init
  (setq popper-reference-buffers
        '("\\*Messages\\*"
          "\\*Warnings\\*"
          "\\*Backtrace\\*"
          "\\*Compile-Log\\*"
          "Output\\*$"
          "\\*Async Shell Command\\*"
          help-mode
          helpful-mode
          compilation-mode
          flycheck-error-list-mode
          grep-mode
          occur-mode
          "^\\*eshell.*\\*$" eshell-mode
          "^\\*shell.*\\*$"  shell-mode
          "^\\*term.*\\*$"   term-mode
          "^\\*vterm.*\\*$"  vterm-mode))
  (setq popper-group-function #'popper-group-by-projectile)
  (popper-mode +1)
  (popper-echo-mode +1))

3.3.2. which-key-posframe

Show which-key popup as a floating frame centered on screen (GUI only).

(use-package which-key-posframe
  :after which-key
  :config
  (setq which-key-posframe-poshandler 'posframe-poshandler-frame-center
        which-key-posframe-border-width 2)
  (when (display-graphic-p)
    (which-key-posframe-mode 1))
  (add-hook 'after-make-frame-functions
            (lambda (frame)
              (with-selected-frame frame
                (if (display-graphic-p)
                    (which-key-posframe-mode 1)
                  (which-key-posframe-mode -1))))))

3.3.3. ultra-scroll

(use-package ultra-scroll
:init
(setq scroll-conservatively 101 ; important!
      scroll-margin 0)
:config
(ultra-scroll-mode 1))

3.3.4. spacious-padding

Add breathing room around windows, mode lines, and frame borders.

Note: The :right-divider-width 20 setting works together with window-divider-mode. See Window Divider Visibility section for divider color customization.

(use-package spacious-padding
  :config
  (setq spacious-padding-widths
        '( :internal-border-width 10
           :header-line-width 4
           :mode-line-width 4
           :right-divider-width 20
           :scroll-bar-width 8
           :fringe-width 8))
  ;; Only enable in GUI frames
  (when (display-graphic-p)
    (spacious-padding-mode 1))
  ;; Re-check when creating new frames (e.g. emacsclient)
  (add-hook 'after-make-frame-functions
            (lambda (frame)
              (with-selected-frame frame
                (if (display-graphic-p)
                    (spacious-padding-mode 1)
                  (spacious-padding-mode -1))))))

3.3.5. Window Divider Visibility

Window dividers are enabled via window-divider-mode to show pixel-based separators between split windows. By default, spacious-padding makes these invisible (matching background color), but we override this to make them subtly visible.

;; Configure window-divider widths to match spacious-padding
(setq window-divider-default-right-width 20
      window-divider-default-bottom-width 1
      window-divider-default-places 'right-only)

;; Enable window-divider-mode in GUI frames
(when (display-graphic-p)
  (window-divider-mode 1))

;; Make window dividers subtly visible (override spacious-padding's invisible style)
;; Using #3f444a - a subtle gray approximately 10% lighter than doom-one background
(defun my/set-visible-window-dividers ()
  "Set window divider faces to be subtly visible."
  (let ((divider-color "#3f444a"))
    (set-face-foreground 'window-divider divider-color)
    (set-face-foreground 'window-divider-first-pixel divider-color)
    (set-face-foreground 'window-divider-last-pixel divider-color)))

;; Apply after theme loads
(add-hook 'spacemacs-post-theme-change-hook #'my/set-visible-window-dividers)

;; Apply when idle (runs after theme loads at startup)
(run-with-idle-timer 0 nil #'my/set-visible-window-dividers)

;; Re-apply for new frames (emacsclient)
(add-hook 'after-make-frame-functions
          (lambda (frame)
            (with-selected-frame frame
              (when (display-graphic-p)
                (window-divider-mode 1)
                (my/set-visible-window-dividers)))))

3.3.6. No title bar

(add-to-list 'default-frame-alist '(undecorated-round . t))

3.3.7. cursor

; Display Emacs cursor in terminal as it would be in GUI
;; (global-term-cursor-mode)

3.3.8. C-a for increasing number, C-x for descreasing number

(evil-define-key 'normal global-map (kbd "C-a") 'evil-numbers/inc-at-pt)
(evil-define-key 'normal global-map (kbd "C-x") 'evil-numbers/dec-at-pt)

3.3.9. Default python interpreter

(setq python-shell-interpreter (executable-find "python3"))

3.3.10. Disable spacemacs buffer warnings

(setq spacemacs-buffer--warnings nil)

3.3.11. Find this file

Create binding to spacemacs.org file

(defun spacemacs/find-config-file ()
  (interactive)
  (find-file (concat dotspacemacs-directory "/spacemacs.org")))

(spacemacs/set-leader-keys "fec" 'spacemacs/find-config-file)

3.3.12. - for going to the first non-blank position of the previous line

(evil-define-key 'normal global-map (kbd "-") 'evil-previous-line-first-non-blank)

3.3.13. Keybinding for Zoom in / out

(define-key (current-global-map) (kbd "C-+") 'spacemacs/zoom-frm-in)
(define-key (current-global-map) (kbd "C--") 'spacemacs/zoom-frm-out)

3.3.14. Smart quit when pressing SPC q q

If running as a daemon, kill the current frame. Otherwise, prompt to kill Emacs.

(defun my/smart-quit ()
  "Quit Emacs smartly.
If running as a daemon, kill the current frame.
Otherwise, prompt to kill Emacs."
  (interactive)
  (if (daemonp)
      (spacemacs/frame-killer)
    (spacemacs/prompt-kill-emacs)))

(spacemacs/set-leader-keys "qq" 'my/smart-quit)

3.3.15. Make w in vim mode move to end of the word (not stopped by _)

(with-eval-after-load 'evil
  (defalias #'forward-evil-word #'forward-evil-symbol))

3.3.16. Smooth scrolling

;; Scroll one line at a time (less "jumpy" than defaults)
(when (display-graphic-p)
  (setq mouse-wheel-scroll-amount '(1 ((shift) . 1))
        mouse-wheel-progressive-speed nil))
(setq scroll-step 1
      scroll-margin 0
      scroll-conservatively 100000)

3.3.17. Transparency settings

(defun my/enable-transparency (&optional frame)
  "Apply configured Spacemacs transparency to FRAME."
  (let* ((frame (or frame (selected-frame)))
         (active (max frame-alpha-lower-limit
                      (min 100
                           (or (bound-and-true-p dotspacemacs-active-transparency)
                               90))))
         (inactive (max frame-alpha-lower-limit
                        (min 100
                             (or (bound-and-true-p dotspacemacs-inactive-transparency)
                                 active)))))
    (if (fboundp 'spacemacs//frame-alpha-set-pair)
        (spacemacs//frame-alpha-set-pair frame active inactive)
      (set-frame-parameter frame 'alpha (cons active inactive)))))

(spacemacs/set-leader-keys "tt" 'spacemacs/toggle-transparency)
;; Remove the old undefined hook if this config is reloaded in a live session.
(remove-hook 'after-make-frame-functions 'spacemacs/enable-transparency)
(add-hook 'after-make-frame-functions #'my/enable-transparency)

3.3.18. Turn on xclip-mode

(use-package xclip
  :config (xclip-mode t))

3.3.19. Use windows key as meta key

It is meant to avoid conflicts with i3wm, where I use alt as the meta key.

(setq x-super-keysym 'meta)

3.3.20. Visiting a file uses its truename as the visited-file name

E.g. when visiting a soft/hard link.

(setq find-file-visit-truename t)

3.3.21. Do not autosave undo history

(with-eval-after-load 'undo-tree
  (setq undo-tree-auto-save-history nil))

3.3.22. Native compilation

(when (and (fboundp 'native-comp-available-p)
           (native-comp-available-p))
  (message "Native compilation is available")
  (setq native-comp-async-report-warnings-errors nil))

3.4. org-mode

3.4.1. Do not hide the macro markers

(with-eval-after-load 'org
  (setq org-hide-macro-markers nil))

3.4.2. org-modern

Modern visual styling for org-mode: clean TODO keywords, tables, timestamps, tags, priorities, and heading bullets.

(use-package org-modern
  :hook (org-mode . org-modern-mode)
  :hook (org-agenda-finalize . org-modern-agenda)
  :custom
  ;; Disable org-modern TODO styling to preserve our custom keyword faces
  (org-modern-todo nil)
  (org-modern-done nil)
  ;; Clean table styling
  (org-modern-table t)
  ;; Modern timestamps
  (org-modern-timestamp t)
  ;; Modern tag styling
  (org-modern-tag t)
  ;; Modern priority styling
  (org-modern-priority t)
  ;; Use pretty star bullets for headings
  (org-modern-star '("◉" "○" "◈" "◇" "▸"))
  ;; Hide leading stars (org-modern handles the display)
  (org-modern-hide-stars t)
  ;; Block styling
  (org-modern-block-fringe nil)
  ;; Horizontal rule
  (org-modern-horizontal-rule t))

3.4.3. Export code blocks with stylesheet CSS

  ;; Keep Org HTML exports independent of the active Emacs theme.
  ;; Theme-provided inline colors can clash with document themes such as
  ;; ReadTheOrg, which already includes a matching htmlize.css stylesheet.
  (defconst my/org-html-src-code-css
    "<style type=\"text/css\">
pre.src > code {
  background: transparent;
  border: 0;
  color: inherit;
  display: block;
  font-size: inherit;
  max-width: none;
  overflow: visible;
  padding: 0;
  white-space: inherit;
  word-wrap: normal;
}
</style>")

  (with-eval-after-load 'ox-html
    (setq org-html-htmlize-output-type 'css)
    (unless (string-match-p (regexp-quote my/org-html-src-code-css)
                            (or org-html-head-extra ""))
      (setq org-html-head-extra
            (concat org-html-head-extra my/org-html-src-code-css))))

3.4.4. org-pomodora

;; Lower the volume of the sounds
(setq org-pomodoro-audio-player "play")
(setq org-pomodoro-finished-sound-args "-v 0.01")
(setq org-pomodoro-long-break-sound-args "-v 0.01")
(setq org-pomodoro-short-break-sound-args "-v 0.01")

3.4.5. org-agenda

(defun scan-new-agenda-files ()
  (interactive)
  (let ((org-dir (expand-file-name "~/org/")))
    (if (file-directory-p org-dir)
        (progn
          (message "Scanning new agenda files...")
          (setq org-agenda-files (directory-files-recursively org-dir "\\.org\\'" nil nil t)))
      (message "Warning: ~/org/ directory does not exist. Skipping agenda files scan."))))

(with-eval-after-load 'org-agenda
  (scan-new-agenda-files)
  (define-key org-agenda-mode-map "m" 'org-agenda-month-view)
  (define-key org-agenda-mode-map "y" 'org-agenda-year-view))

(spacemacs/set-leader-keys "aou" 'scan-new-agenda-files)

;; Add category into the ~org-todo-list~ view
(setq org-agenda-prefix-format
      '((agenda . " %i %-16:c%?-12t% s")
        (todo   . " %i %-16:c ") ;; Added a space here
        (tags   . " %i %-16:c")
        (search . " %i %-16:c")))

;; Add custom agenda view for my dashboard
(setq org-agenda-custom-commands
      '(("d" "My Dashboard"
         ((agenda ""
                  ((org-agenda-span 7)
                   ;; This line fixes the column width (%-16c reserves 16 chars)
                   (org-agenda-prefix-format "  %-16:c%?-12t% s")))
          (todo "UNDER_REVIEW"                   ;; Show what's under review
                ((org-agenda-overriding-header "Under Review")))
          (todo "IN_PROGRESS"                    ;; Show what I'm working on NOW
                ((org-agenda-overriding-header "Current Focus")))
          (todo "READY"                          ;; Show what's ready for development (e.g. groomed and prioritized)
                ((org-agenda-overriding-header "Selected for Development")))
          (todo "TODO"                           ;; Show the rest of the list
                ((org-agenda-overriding-header "Inbox / Backlog")))))))


;; Customize the colors of TODO keywords for better visual distinction
(setq org-todo-keyword-faces
      '(;; Standard Flow
        ("TODO"         . (:foreground "goldenrod" :weight bold))
        ("IN_PROGRESS"  . (:foreground "deep sky blue" :weight bold))
        ("UNDER_REVIEW"  . (:foreground "medium purple" :weight bold))

        ;; Bug Tracking Flow
        ("REPORT"       . (:foreground "indian red" :weight bold))
        ("BUG"          . (:foreground "red" :weight bold))
        ("KNOWNCAUSE"   . (:foreground "orange" :weight bold))
        ("FIXED"        . (:foreground "green" :weight bold))

        ;; Terminal / Inactive States (Muted)
        ("CANCELED"     . (:foreground "dark gray" :strike-through t))
        ("REPORTED"     . (:foreground "cadet blue"))))

3.4.6. org-cv

(use-package ox-awesomecv
  :load-path "~/.spacemacs.d/private/org-cv"
  :init (require 'ox-awesomecv))

3.4.7. org-babel

(with-eval-after-load 'org
  (org-babel-do-load-languages
   'org-babel-load-languages
   '((C . t)
     (python . t)
     (shell . t))))

3.4.8. org-latex

(with-eval-after-load 'ox-latex
  (add-to-list 'org-latex-classes
               '("scrartcl"
                 "\\documentclass[a4paper,11pt]{scrartcl}"
                 ("\\section{%s}" . "\\section*{%s}")
                 ("\\subsection{%s}" . "\\subsection*{%s}")
                 ("\\subsubsection{%s}" . "\\subsubsection*{%s}")
                 ("\\paragraph{%s}" . "\\paragraph*{%s}")
                 ("\\subparagraph{%s}" . "\\subparagraph*{%s}"))))

3.4.9. org-journal

(with-eval-after-load 'org-journal
  (setq org-journal-dir "~/org/home/roam/journal/")
  (setq org-journal-date-format "%A, %m/%d/%Y")
  (setq org-journal-file-type 'monthly)
  (setq org-journal-file-format "%Y%m%d.org"))

(spacemacs/set-leader-keys
  "aojj" (lambda () (interactive)
           (org-journal-new-entry nil)))
(spacemacs/declare-prefix "aojj" "journal-home")

3.4.10. org-table

(with-eval-after-load 'org
  (define-key org-mode-map (kbd "C-<tab>") 'org-table-previous-field))

3.4.11. org-todo

(with-eval-after-load 'org
  (let ((capture-template  "* TODO %?\n%U\n%i\n%a"))
    (progn
      (setq org-todo-keywords
            '((sequence "TODO(t)" "READY(r)" "IN_PROGRESS" "UNDER_REVIEW" "|" "DONE(d)")
              (sequence "REPORT(p)" "BUG(b)" "KNOWNCAUSE(k)" "|" "FIXED(f)")
              (sequence "|" "INACTIVE(i)" "INCOMPLETE(n)" "CANCELED(c)" "REPORTED(R)")))
      (setq org-capture-templates '(
                                    ("t" "Task" entry (file+headline "~/org/home/tasks.org" "Tasks")
                                     "* TODO %?\n%U\n%i\n%a")
                                    ("e" "Email" entry (file+headline "~/org/home/tasks.org" "Tasks")
                                     "* TODO %? [[mu4e:msgid:%l][Email]]\n%U\n%i")
                                    ))
      (setq org-project-capture-capture-template capture-template))))
(spacemacs/set-leader-keys
  "aoh" (lambda () (interactive) (find-file "~/org/home/tasks.org"))
  "aoc" (lambda () (interactive) (org-capture nil "t"))
  )
(spacemacs/declare-prefix "aoh" "org-capture-show-task")
(spacemacs/declare-prefix "aoc" "org-capture-task")

3.4.12. org-hugo

(spacemacs/set-leader-keys-for-major-mode 'org-mode "Th" 'org-hugo-auto-export-mode)

3.4.13. org-pandoc PDF export

  (defconst my/org-pandoc-eisvogel-unicode-header
    "\\usepackage{newunicodechar}
\\newunicodechar{→}{$\\to$}
\\newunicodechar{×}{$\\times$}
\\newunicodechar{≈}{$\\approx$}
\\newunicodechar{ε}{$\\epsilon$}
\\lstset{literate={→}{{$\\to$}}1 {×}{{$\\times$}}1 {≈}{{$\\approx$}}1 {ε}{{$\\epsilon$}}1}")

  (defun my/org-pandoc-eisvogel--trim-whitespace (value)
    "Trim whitespace from VALUE."
    (replace-regexp-in-string "^[[:space:]]+\\|[[:space:]]+$" "" value))

  (defun my/org-pandoc-eisvogel--font-available-p (font)
    "Return non-nil when FONT is available via `fc-match`."
    (when (executable-find "fc-match")
      (with-temp-buffer
        (when (zerop (call-process "fc-match" nil (current-buffer) nil
                                   "-f" "%{family}\n" font))
          (let* ((families (split-string
                            (replace-regexp-in-string "[\n\r]+" "," (buffer-string))
                            "," t)))
            (member font (mapcar #'my/org-pandoc-eisvogel--trim-whitespace families)))))))

  (defun my/org-pandoc-eisvogel--choose-font (candidates)
    "Return the first available font in CANDIDATES, or the first entry."
    (let ((selected nil)
          (remaining candidates))
      (while (and remaining (not selected))
        (when (my/org-pandoc-eisvogel--font-available-p (car remaining))
          (setq selected (car remaining)))
        (setq remaining (cdr remaining)))
      (or selected (car candidates))))

  (defun my/org-pandoc-eisvogel--ensure-pdf-extension (path)
    "Ensure PATH ends with `.pdf`."
    (if (string-match-p "\\.pdf\\'" path)
        path
      (concat path ".pdf")))

  (defun my/org-pandoc-eisvogel--default-output-path ()
    "Return `<basename>.pdf` for the current Org file."
    (unless (buffer-file-name)
      (user-error "Current Org buffer is not visiting a file"))
    (concat (file-name-sans-extension (buffer-file-name)) ".pdf"))

  (defun my/org-pandoc-eisvogel-export (&optional output-file)
    "Export current Org file to a Pandoc + Eisvogel PDF.

If OUTPUT-FILE is nil, export to `<basename>.pdf` in the current file's
directory."
    (interactive)
    (unless (derived-mode-p 'org-mode)
      (user-error "Not in an Org buffer"))
    (unless (executable-find "pandoc")
      (user-error "Pandoc not found in PATH"))
    (let ((input-file (or (buffer-file-name)
                          (user-error "Current Org buffer is not visiting a file")))
          (output-file (my/org-pandoc-eisvogel--ensure-pdf-extension
                        (or output-file
                            (my/org-pandoc-eisvogel--default-output-path))))
          (main-font (or (getenv "PDF_MAINFONT")
                         (my/org-pandoc-eisvogel--choose-font
                          '("TeX Gyre Pagella" "Noto Serif" "DejaVu Serif"))))
          (sans-font (or (getenv "PDF_SANSFONT")
                         (my/org-pandoc-eisvogel--choose-font
                          '("Noto Sans" "DejaVu Sans"))))
          (mono-font (or (getenv "PDF_MONOFONT")
                         (my/org-pandoc-eisvogel--choose-font
                          '("Fira Code" "Noto Sans Mono" "DejaVu Sans Mono"))))
          (math-font (or (getenv "PDF_MATHFONT")
                         (my/org-pandoc-eisvogel--choose-font
                          '("TeX Gyre Pagella Math" "Latin Modern Math"))))
          (mono-font-options (or (getenv "PDF_MONOFONT_OPTIONS") "Scale=0.88"))
          (header-file (make-temp-file "org-pandoc-eisvogel" nil ".tex"))
          (output-buffer (get-buffer-create " *org-pandoc-eisvogel*")))
      (with-current-buffer output-buffer (erase-buffer))
      (with-temp-file header-file
        (insert my/org-pandoc-eisvogel-unicode-header))
      (unwind-protect
          (let ((exit-code
                 (apply #'call-process
                        "pandoc"
                        input-file
                        output-buffer
                        nil
                        (list "--from" "org"
                              "--output" output-file
                              "--pdf-engine=xelatex"
                              "--template" "eisvogel"
                              "--listings"
                              "--toc"
                              "--number-sections"
                              "--include-in-header" header-file
                              "--variable" "geometry:margin=0.8in"
                              "--variable" (format "mainfont=%s" main-font)
                              "--variable" (format "sansfont=%s" sans-font)
                              "--variable" (format "monofont=%s" mono-font)
                              "--variable" (format "mathfont=%s" math-font)
                              "--variable" "mainfontoptions=Ligatures=TeX"
                              "--variable" (format "monofontoptions=%s" mono-font-options)
                              "--variable" "code-block-font-size=\\small"))))
            (if (zerop exit-code)
                (progn
                  (message "Exported Org to PDF: %s" output-file)
                  output-file)
              (with-current-buffer output-buffer
                (user-error "Pandoc export failed (exit %s):
%s"
                            exit-code
                            (buffer-string)))))
        (delete-file header-file))
      output-file))

  (defun my/org-pandoc-eisvogel-export-and-open ()
    "Export current Org buffer to `<basename>.pdf` and open it."
    (interactive)
    (let ((output (my/org-pandoc-eisvogel-export)))
      (browse-url-of-file output)
      output))

  (defun my/org-pandoc-eisvogel-export-to-file (output-file)
    "Export current Org buffer to a chosen PDF output path."
    (interactive
     (list
      (let ((default (my/org-pandoc-eisvogel--default-output-path)))
        (read-file-name "Export PDF to: "
                       (file-name-directory default)
                       default
                       nil
                       (file-name-nondirectory default)))))
    (my/org-pandoc-eisvogel-export
     (my/org-pandoc-eisvogel--ensure-pdf-extension output-file)))

  (defun my/org-pandoc-eisvogel--reject-dispatch-options
      (async subtreep visible-only body-only)
    "Reject Org export dispatcher options not supported by this Pandoc path."
    (when (or async subtreep visible-only body-only)
      (user-error "Pandoc/Eisvogel export only supports synchronous whole-file PDF export")))

  (defun my/org-pandoc-eisvogel-export-dispatch
      (async subtreep visible-only body-only)
    "Org export-dispatch entry for `my/org-pandoc-eisvogel-export`."
    (my/org-pandoc-eisvogel--reject-dispatch-options
     async subtreep visible-only body-only)
    (my/org-pandoc-eisvogel-export))

  (defun my/org-pandoc-eisvogel-export-and-open-dispatch
      (async subtreep visible-only body-only)
    "Org export-dispatch entry for `my/org-pandoc-eisvogel-export-and-open`."
    (my/org-pandoc-eisvogel--reject-dispatch-options
     async subtreep visible-only body-only)
    (my/org-pandoc-eisvogel-export-and-open))

  (defun my/org-pandoc-eisvogel-export-to-file-dispatch
      (async subtreep visible-only body-only)
    "Org export-dispatch entry for `my/org-pandoc-eisvogel-export-to-file`."
    (my/org-pandoc-eisvogel--reject-dispatch-options
     async subtreep visible-only body-only)
    (call-interactively #'my/org-pandoc-eisvogel-export-to-file))

  (with-eval-after-load 'ox
    (org-export-define-backend 'pandoc-eisvogel nil
      :menu-entry
      '(?E "Export to Pandoc/Eisvogel PDF"
           ((?p "As PDF file" my/org-pandoc-eisvogel-export-dispatch)
            (?o "As PDF file and open" my/org-pandoc-eisvogel-export-and-open-dispatch)
            (?f "To chosen PDF file" my/org-pandoc-eisvogel-export-to-file-dispatch)))))

3.4.14. org-roam

3.4.14.1. Automatically sync the database
(org-roam-db-autosync-mode)
3.4.14.2. Migrate the current buffer from org-roam v1 to v2
(defun org-roam-migrate-current-buffer-v1-to-v2 ()
  (interactive)
  (org-roam-migrate-v1-to-v2))

3.4.15. org-gcal

Sync Google Calendar events into Org Agenda (read-only fetch). Credentials are stored in ~/.authinfo and never committed to the repo. OAuth tokens are GPG-encrypted via plstore.

Setup instructions (one-time, manual):

  1. Create a Google Cloud project, enable Calendar API, create a Desktop OAuth 2.0 client.
  2. In Google Calendar settings, share any secondary account's calendar with your primary Gmail address.
  3. Generate a dedicated GPG key: gpg --full-generate-key (RSA 4096, no expiry).
  4. Note the key ID: gpg --list-secret-keys --keyid-format=long
  5. Add to ~/.authinfo:
    • machine org-gcal-client-id password <client-id>.apps.googleusercontent.com
    • machine org-gcal-client-secret password <client-secret>
    • machine org-gcal-calendar-id-personal password <personal@gmail.com>
    • machine org-gcal-calendar-id-work password <shared-work-calendar-id>
    • machine org-gcal-gpg-key-id password <GPG-long-key-id>
  6. On first fetch, approve OAuth access in the browser. Tokens are saved and reused automatically.
  (use-package org-gcal
    :defer t
    :init
    ;; Credentials must be set BEFORE org-gcal.el loads, because it calls
    ;; org-gcal-reload-client-id-secret at load time to register the OAuth client.
    (setq org-gcal-client-id     (auth-source-pick-first-password :host "org-gcal-client-id")
          org-gcal-client-secret (auth-source-pick-first-password :host "org-gcal-client-secret"))
    ;; Use asymmetric GPG key for encrypting the OAuth token store.
    ;; Key ID is read from authinfo so it is never hardcoded in this repo.
    (require 'plstore)
    (let ((gpg-key-id (auth-source-pick-first-password :host "org-gcal-gpg-key-id")))
      (when gpg-key-id
        (add-to-list 'plstore-encrypt-to gpg-key-id)))
    ;; Use loopback pinentry so Emacs prompts for GPG passphrases in the minibuffer.
    ;; Requires allow-loopback-pinentry in ~/.gnupg/gpg-agent.conf.
    ;; NOTE: loopback needs an interactive minibuffer; timer-driven syncs cannot
    ;; prompt reliably, so every fetch primes the gpg-agent cache first and aborts
    ;; before org-gcal starts if the OAuth token store cannot be decrypted.
    (setq epg-pinentry-mode 'loopback)
    :config
    ;; Map each calendar ID to its dedicated org file.
    ;; Both files live in ~/org/home/ and are auto-discovered by scan-new-agenda-files.
    (setq org-gcal-fetch-file-alist
          `((,(auth-source-pick-first-password :host "org-gcal-calendar-id-personal")
             . "~/org/home/gcal/gcal-personal.org")
            (,(auth-source-pick-first-password :host "org-gcal-calendar-id-work")
             . "~/org/home/gcal/gcal-work.org")))

     ;; Fetch window: 7 days back, 30 days forward.
    (setq org-gcal-up-days   7
          org-gcal-down-days 30)

    ;; Do not auto-archive old events outside the fetch window.
    (setq org-gcal-auto-archive nil)

    ;; Ask before removing events that appear cancelled/missing on Google's side.
    ;; Avoids silent archiving of items that are simply outside the fetch window.
    (setq org-gcal-update-cancelled-events-with-todo t
          org-gcal-remove-api-cancelled-events       'ask)

    ;; Collect recurring event instances under their parent headline.
    (setq org-gcal-recurring-events-mode 'nested)

    ;; Suppress popup notifications.
    (setq org-gcal-notify-p nil))

  ;; Safe fetch wrapper: primes GPG and clears stale sync lock before fetching.
  ;; Uses org-gcal-fetch (read-only — never pushes local changes back to Google).
  (defun my/org-gcal-fetch-safe ()
    "Fetch Google Calendar events after verifying the OAuth token store decrypts."
    (interactive)
    (let ((interactive-call (called-interactively-p 'interactive)))
      (condition-case err
          (progn
            (my/org-gcal--prime-gpg-cache)
            (require 'org-gcal)
            (when (bound-and-true-p org-gcal--sync-lock)
              (warn "org-gcal: stale sync lock detected, clearing it.")
              (org-gcal--sync-unlock))
            (org-gcal-fetch))
        (error
         (if interactive-call
             (signal (car err) (cdr err))
           (message "org-gcal: fetch skipped: %s"
                    (error-message-string err)))))))

  (defun my/org-gcal--prime-gpg-cache (&optional noerror)
    "Decrypt the org-gcal plstore once to prime the gpg-agent passphrase cache.

Return non-nil when there is no token store yet or decryption succeeds. If
NOERROR is non-nil, report failures as messages and return nil. Otherwise signal
`user-error' so manual and timer-driven fetches stop before org-gcal enters its
deferred OAuth path and reports a later, less useful GPG error."
    (let ((plstore-file (expand-file-name "oauth2-auto.plist" user-emacs-directory))
          store)
      (if (not (file-exists-p plstore-file))
          t
        (condition-case err
            (unwind-protect
                (progn
                  (setq store (plstore-open plstore-file))
                  ;; `plstore-find' consults/decrypts secret entries through the
                  ;; public plstore API, which is stable across Emacs versions.
                  ;; The plaintext values are intentionally discarded.
                  (dolist (entry (plstore-find store nil))
                    (plstore-get store (car entry)))
                  t)
              (when store
                (plstore-close store)))
          (error
           (if noerror
               (progn
                 (message "org-gcal: failed to prime gpg-agent cache: %s"
                          (error-message-string err))
                 nil)
             (user-error
              "org-gcal: cannot decrypt %s; fetch aborted before reading OAuth tokens (%s)"
              (file-name-nondirectory plstore-file)
              (error-message-string err))))))))

  (defun my/org-gcal--prime-gpg-cache-before-sync (&rest _)
    "Prime GPG before any direct `org-gcal-sync' or `org-gcal-fetch' call."
    (my/org-gcal--prime-gpg-cache))

  (with-eval-after-load 'org-gcal
    (unless (advice-member-p #'my/org-gcal--prime-gpg-cache-before-sync
                             'org-gcal-sync)
      (advice-add 'org-gcal-sync :before
                  #'my/org-gcal--prime-gpg-cache-before-sync)))

  ;; Prime once after startup so the first decrypt happens with a minibuffer
  ;; available (loopback pinentry needs it). Run slightly deferred so other
  ;; startup activity doesn't compete for the minibuffer. Fetches still prime
  ;; again and abort safely if this startup attempt fails.
  (add-hook 'emacs-startup-hook
            (lambda ()
              (run-with-idle-timer 2 nil #'my/org-gcal--prime-gpg-cache :noerror)))

  ;; Auto-fetch every 30 minutes. Adjust the interval as needed.
  (run-at-time "30 min" 1800 #'my/org-gcal-fetch-safe)

  ;; Manual sync keybinding: SPC a o s
  (spacemacs/set-leader-keys "aos" 'my/org-gcal-fetch-safe)

3.4.16. org-clock

3.4.16.1. Save the clock history across Emacs sessions
(setq org-clock-persist 'history)
(org-clock-persistence-insinuate)

3.4.17. Replace selected markdown region to org format

(defun replace-markdown-region-with-org (beginning end)
  "Replace the selected markdown region with its corresponding org-mode format."
  (interactive "r")
  (if (use-region-p)
      (let ((tmp-buffer "Markdown To Org Tmp"))
        (pcase (shell-command-on-region (region-beginning) (region-end)
                                        "pandoc -f markdown -t org" tmp-buffer t)
          (0 (message "Successfully converted to org-mode format."))
          (127 (message "pandoc not found. Install it with 'sudo apt install pandoc'."))
          (_ (message "Failed to convert the selected region to org-mode format."))
          )
        )
    (message "No active region is found.")))
(spacemacs/set-leader-keys-for-major-mode 'org-mode "RR" 'replace-markdown-region-with-org)

3.4.18. ob-lean4

(use-package ob-lean4
  :load-path "~/.spacemacs.d/private/ob-lean4")
(add-to-list 'org-babel-load-languages '(lean4 . t))

3.4.19. ob-cmake

(use-package ob-cmake
  :load-path "~/.spacemacs.d/private/ob-cmake")
(add-to-list 'org-babel-load-languages '(cmake . t))

3.4.20. ob-rust

(require 'ob-rust)
(add-to-list 'org-babel-load-languages '(rust . t))

3.5. Utility

3.5.1. beacon mode

(beacon-mode 1)

3.5.2. Toggle clang-format on save

(defun c-c++-toggle-clang-format-on-save ()
  (interactive)
  (cond
   (c-c++-enable-clang-format-on-save
    (message "[c-c++] disable clang-format on save")
    (setq c-c++-enable-clang-format-on-save nil))
   ((not c-c++-enable-clang-format-on-save)
    (message "[c-c++] enable clang-format on save")
    (setq c-c++-enable-clang-format-on-save t))
   ))

(spacemacs/set-leader-keys-for-major-mode 'c-mode "Tf" 'c-c++-toggle-clang-format-on-save)
(spacemacs/set-leader-keys-for-major-mode 'c++-mode "Tf" 'c-c++-toggle-clang-format-on-save)
(spacemacs/declare-prefix-for-mode 'c-mode "Tf" "toggle-clang-format-on-save")
(spacemacs/declare-prefix-for-mode 'c++-mode "Tf" "toggle-clang-format-on-save")

3.5.3. auto-indent

;; I want to disable pasting with formatting on C/C++ buffers
(add-to-list 'spacemacs-indent-sensitive-modes 'c-mode)
(add-to-list 'spacemacs-indent-sensitive-modes 'c++-mode)

3.5.4. format-all

(add-hook 'sh-mode-hook #'format-all-mode)
(add-hook 'fish-mode-hook #'format-all-mode)
(add-hook 'cmake-mode-hook #'format-all-mode)

3.5.5. glow, the markdown viewer

;; Configure glow viewer
(defun start-glow-viewer ()
  (interactive)
  (start-process "glow-markdown-viewer" nil
                 "/usr/bin/x-terminal-emulator"
                 (file-truename "~/.spacemacs.d/scripts/glow_mk_viewer.sh")
                 (buffer-file-name nil)))

3.5.6. google-search

;; Set google as default search engine and open links with xdg-open or open depending on the OS
(spacemacs/set-leader-keys "ag" 'engine/search-google)
(setq browse-url-browser-function 'browse-url-generic
      engine/browser-function 'browse-url-generic
      browse-url-generic-program (cond ((string-equal system-type "darwin") "open")
                                        ((string-equal system-type "gnu/linux") "xdg-open")))

3.5.7. Kill all buffers

(defun nuke-all-buffers ()
  (interactive)
  (mapcar 'kill-buffer (buffer-list))
  (delete-other-windows))
(global-set-key (kbd "C-x K") 'nuke-all-buffers)

3.5.8. ranger

(with-eval-after-load 'ranger
  (require 'hydra)
  (define-key ranger-mode-map (kbd "M-h") 'ranger-prev-tab)
  (define-key ranger-mode-map (kbd "M-l") 'ranger-next-tab)
  (define-key ranger-mode-map (kbd "M-n") 'ranger-new-tab))
(spacemacs/set-leader-keys "ar" 'ranger)

3.5.9. cheat.sh

The one and only one cheatsheet.

(spacemacs/declare-prefix "ac" "cheat-sh")

;; Prompt to select a topic to show its cheatsheet
(spacemacs/set-leader-keys "acl" 'cheat-sh)

;; Show the help page of cheat.sh
(spacemacs/set-leader-keys "ach" 'cheat-sh-help)

;; Get the cheatsheet for the marked region
(spacemacs/set-leader-keys "acr" 'cheat-sh-region)

;; Get a random page of cheatsheet
(spacemacs/set-leader-keys "acR"
  (lambda ()
    (interactive)
    (cheat-sh ":random")))
(spacemacs/declare-prefix "acR" "cheat.sh/:random")

3.5.10. chatgpt-shell

(use-package chatgpt-shell
  :load-path "~/.spacemacs.d/private/chatgpt-shell")
(setq chatgpt-shell-openai-key
      (auth-source-pick-first-password :host "api.openai.com"))

3.6. mu4e

Email client using mu4e (Spacemacs layer) + mbsync (isync) for IMAP sync. Two Gmail accounts: personal and work. Credentials stored in ~/.authinfo (not committed). Requires: apt install isync maildir-utils mu4e w3m

3.6.1. Setup instructions (one-time)

Run these commands once after installing the system:

# Create maildir structure
mkdir -p ~/.mail/personal/{cur,new,tmp}
mkdir -p ~/.mail/work/{cur,new,tmp}

# Initial mail sync (after ~/.mbsyncrc is configured)
mbsync -a

# Index mail with mu (after initial sync)
mu init --maildir=~/.mail \
    --my-address=$(pass show or cat ~/.authinfo | grep mu4e-email-personal ...) \
    --my-address=$(...)
# Simpler: let mu4e run mu init on first launch via mu4e-update-mail-and-index

3.6.2. Configuration

(with-eval-after-load 'mu4e
  ;; Basic settings
  (setq mu4e-maildir "~/.mail"
        mu4e-get-mail-command "mbsync -a"
        mu4e-update-interval (* 5 60)       ; sync every 5 minutes
        mu4e-compose-signature-auto-include nil
        mu4e-view-show-images t
        mu4e-view-image-max-width 800
        mu4e-headers-date-format "%Y-%m-%d"
        mu4e-headers-time-format "%H:%M"
        mu4e-change-filenames-when-moving t  ; avoid duplicate UID issues
        mu4e-confirm-quit nil)

  ;; Maildir shortcuts shown in the main view
  (setq mu4e-maildir-shortcuts
        '((:maildir "/personal/INBOX"             :key ?i :name "Personal Inbox")
          (:maildir "/personal/[Gmail]/Sent Mail" :key ?s :name "Personal Sent")
          (:maildir "/work/INBOX"                 :key ?I :name "Work Inbox")
          (:maildir "/work/[Gmail]/Sent Mail"     :key ?S :name "Work Sent")))

  ;; HTML rendering with w3m
  (setq mu4e-html2text-command "w3m -T text/html")

  ;; Read account identities from ~/.authinfo
  (let* ((personal-email (auth-source-pick-first-password :host "mu4e-email-personal"))
         (personal-name  (auth-source-pick-first-password :host "mu4e-name-personal"))
         (work-email     (auth-source-pick-first-password :host "mu4e-email-work"))
         (work-name      (auth-source-pick-first-password :host "mu4e-name-work")))

    ;; Contexts: one per account
    (setq mu4e-contexts
          (list
           (make-mu4e-context
            :name "Personal"
            :match-func
            (lambda (msg)
              (when msg
                (string-prefix-p "/personal" (mu4e-message-field msg :maildir))))
            :vars `((user-mail-address       . ,(or personal-email ""))
                    (user-full-name          . ,(or personal-name ""))
                    (mu4e-sent-folder        . "/personal/[Gmail]/Sent Mail")
                    (mu4e-drafts-folder      . "/personal/[Gmail]/Drafts")
                    (mu4e-trash-folder       . "/personal/[Gmail]/Bin")
                    (mu4e-refile-folder      . "/personal/Archive")
                    (smtpmail-smtp-user      . ,personal-email)
                    (smtpmail-smtp-server    . "smtp.gmail.com")
                    (smtpmail-smtp-service   . 587)
                    (smtpmail-stream-type    . starttls)))
           (make-mu4e-context
            :name "Work"
            :match-func
            (lambda (msg)
              (when msg
                (string-prefix-p "/work" (mu4e-message-field msg :maildir))))
            :vars `((user-mail-address       . ,(or work-email ""))
                    (user-full-name          . ,(or work-name ""))
                    (mu4e-sent-folder        . "/work/[Gmail]/Sent Mail")
                    (mu4e-drafts-folder      . "/work/[Gmail]/Drafts")
                    (mu4e-trash-folder       . "/work/[Gmail]/Bin")
                    (mu4e-refile-folder      . "/work/Archive")
                    (smtpmail-smtp-user      . ,work-email)
                    (smtpmail-smtp-server    . "smtp.gmail.com")
                    (smtpmail-smtp-service   . 587)
                    (smtpmail-stream-type    . starttls)))))

    ;; Policy: ask which context on ambiguous messages; default to Personal
    (setq mu4e-context-policy 'pick-first
          mu4e-compose-context-policy 'ask-if-none))

  ;; SMTP via smtpmail — passwords come from ~/.authinfo automatically
  (setq send-mail-function         'smtpmail-send-it
        message-send-mail-function 'smtpmail-send-it
        smtpmail-auth-credentials  "~/.authinfo")

  ;; org-mu4e integration: store links to emails (mu4e-org replaces org-mu4e since mu 1.8)
  (require 'mu4e-org)
  (setq org-mu4e-link-query-in-headers-mode nil)

  ;; Use 'Q' to truly quit mu4e
  (define-key mu4e-main-mode-map (kbd "Q") 'mu4e-quit)
  (define-key mu4e-headers-mode-map (kbd "Q") 'mu4e-quit)
  (define-key mu4e-view-mode-map (kbd "Q") 'mu4e-quit)

  ;; Use 'q' to just bury the buffer (background)
  (define-key mu4e-main-mode-map (kbd "q") 'mu4e-view-quit)
  (define-key mu4e-headers-mode-map (kbd "q") 'mu4e-view-quit)
  (define-key mu4e-view-mode-map (kbd "q") 'mu4e-view-quit)
  )

;; Keybinding: open mu4e with SPC a m
(spacemacs/set-leader-keys "am" 'mu4e)
(spacemacs/declare-prefix "am" "mu4e-mail")
(mu4e t)

3.7. Workarounds

3.7.1. Workaround for the bug where company-mode and evil-mode are conflicting

(evil-declare-change-repeat 'company-complete)

3.7.2. doom-modeline icons in GUI vs terminal

;; Enable doom-modeline icons in GUI, disable in terminal
(with-eval-after-load 'doom-modeline
  (setq doom-modeline-icon (display-graphic-p))
  ;; When creating a new frame (e.g. emacsclient), re-check
  (add-hook 'after-make-frame-functions
            (lambda (frame)
              (with-selected-frame frame
                (setq doom-modeline-icon (display-graphic-p))))))

3.7.3. Workaround for spammed false positive warning messages

Ticket: syl20bnr/spacemacs#16575 ‘org-element-at-point’ cannot be used in non-Org buffer

(add-to-list 'warning-suppress-types '(org-element org-element-parser))

Created: 2026-07-15 Wed 11:50

Validate