Spacemacs tangled user configuration
Table of Contents
- 1. Introduction
- 2. user-init
- 3. user-config
- 3.1. Performance
- 3.2. IDE config
- 3.2.1. Highlight LaTeX fragments in org-mode
- 3.2.2. Set fill column to 100
- 3.2.3. Sort Python imports with Ruff
- 3.2.4. Add new line when a file is about to be saved
- 3.2.5. Apply ANSI Color codes for buffer and region
- 3.2.6. Disable auto-indent in C/C++ mode after typing
:: - 3.2.7. MacOS
- 3.2.8. Prevent using UI dialogs for prompts
- 3.2.9. Prune deleted LSP workspace folders
- 3.2.10. ccls
- 3.2.11. devcontainer
- 3.2.12. cmake-ide
- 3.2.13. company
- 3.2.14. minuet-ai
- 3.2.15. codex-proxy
- 3.2.16. gptel
- 3.2.17. pi-coding-agent
- 3.2.18. lsp-ui
- 3.2.19. popup
- 3.2.20. dap
- 3.2.21. flycheck
- 3.2.22. Indentation for web development
- 3.2.23. groovy (Jenkinsfile)
- 3.2.24. plantuml
- 3.2.25. nvm
- 3.2.26. conf-mode
- 3.2.27. Rust
- 3.2.28. scad-mode
- 3.2.29. Style for linux kernel development
- 3.2.30. tramp + clang-format
- 3.2.31. Get current branch name in magit
- 3.3. Miscellaneous
- 3.3.1. popper
- 3.3.2. which-key-posframe
- 3.3.3. ultra-scroll
- 3.3.4. spacious-padding
- 3.3.5. Window Divider Visibility
- 3.3.6. No title bar
- 3.3.7. cursor
- 3.3.8.
C-afor increasing number,C-xfor descreasing number - 3.3.9. Default python interpreter
- 3.3.10. Disable spacemacs buffer warnings
- 3.3.11. Find this file
- 3.3.12.
-for going to the first non-blank position of the previous line - 3.3.13. Keybinding for Zoom in / out
- 3.3.14. Smart quit when pressing
SPC q q - 3.3.15. Make
win vim mode move to end of the word (not stopped by_) - 3.3.16. Smooth scrolling
- 3.3.17. Transparency settings
- 3.3.18. Turn on xclip-mode
- 3.3.19. Use windows key as meta key
- 3.3.20. Visiting a file uses its truename as the visited-file name
- 3.3.21. Do not autosave undo history
- 3.3.22. Native compilation
- 3.4. org-mode
- 3.4.1. Do not hide the macro markers
- 3.4.2. org-modern
- 3.4.3. Export code blocks with stylesheet CSS
- 3.4.4. org-pomodora
- 3.4.5. org-agenda
- 3.4.6. org-cv
- 3.4.7. org-babel
- 3.4.8. org-latex
- 3.4.9. org-journal
- 3.4.10. org-table
- 3.4.11. org-todo
- 3.4.12. org-hugo
- 3.4.13. org-pandoc PDF export
- 3.4.14. org-roam
- 3.4.15. org-gcal
- 3.4.16. org-clock
- 3.4.17. Replace selected markdown region to org format
- 3.4.18. ob-lean4
- 3.4.19. ob-cmake
- 3.4.20. ob-rust
- 3.5. Utility
- 3.6. mu4e
- 3.7. Workarounds
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 Git's worktree root for the current directory, or nil."
(require 'subr-x)
(with-temp-buffer
(when (zerop (process-file "git" nil t nil
"rev-parse" "--show-toplevel"))
(file-name-as-directory
(string-trim-right (buffer-string))))))
(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. Run SPC a i d i once to explicitly install a missing setup from the official upstream files; it never overwrites an existing or partial setup and does not start Docker. Run SPC a i d u afterwards to start the installed service.
(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.")
(require 'cl-lib)
(defconst my/codex-proxy--compose-url
"https://raw.githubusercontent.com/icebear0828/codex-proxy/master/docker-compose.yml"
"Official upstream Docker Compose URL for codex-proxy.")
(defconst my/codex-proxy--environment-url
"https://raw.githubusercontent.com/icebear0828/codex-proxy/master/.env.example"
"Official upstream environment-template URL for codex-proxy.")
(defconst my/codex-proxy--installation-files
'("docker-compose.yml" ".env.example" ".env")
"Files managed by `my/codex-proxy-install'.")
(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))
(unless (catch 'compose-file-found
(dolist (name '("compose.yaml" "compose.yml"
"docker-compose.yaml" "docker-compose.yml"))
(when (file-readable-p (expand-file-name name directory))
(throw 'compose-file-found t))))
(user-error "codex-proxy Compose file is missing in %s (expected compose.yaml, compose.yml, docker-compose.yaml, or docker-compose.yml)"
directory))
directory))
(defun my/codex-proxy--installation-paths (directory)
"Return absolute managed installation paths within DIRECTORY."
(mapcar (lambda (name) (expand-file-name name directory))
my/codex-proxy--installation-files))
(defun my/codex-proxy--file-exists-or-symlink-p (path)
"Return non-nil when PATH exists, including a dangling symbolic link."
(or (file-exists-p path) (file-symlink-p path)))
(defun my/codex-proxy--download-file (url destination)
"Download URL into DESTINATION, rejecting a non-200 response."
(require 'url-http)
(let ((buffer (url-retrieve-synchronously url t t 30)))
(unless buffer
(error "Unable to download codex-proxy setup from %s" url))
(unwind-protect
(with-current-buffer buffer
(unless (= url-http-response-status 200)
(error "codex-proxy setup download failed for %s (HTTP %s)"
url url-http-response-status))
(write-region url-http-end-of-headers (point-max) destination nil 'silent))
(kill-buffer buffer))))
(defun my/codex-proxy--replace-required-seam (content regexp replacement label)
"Replace exactly one REGEXP in CONTENT or signal LABEL's upstream drift."
(let ((start 0)
(count 0))
(while (string-match regexp content start)
(setq count (1+ count)
start (match-end 0)))
(unless (= count 1)
(error "codex-proxy upstream %s seam expected once, found %d" label count))
(replace-regexp-in-string regexp replacement content t t)))
(defun my/codex-proxy--prepare-installation (temporary-directory)
"Download and transform codex-proxy files into TEMPORARY-DIRECTORY."
(let* ((compose-file (expand-file-name "docker-compose.yml" temporary-directory))
(environment-example-file (expand-file-name ".env.example" temporary-directory))
(environment-file (expand-file-name ".env" temporary-directory)))
(my/codex-proxy--download-file my/codex-proxy--compose-url compose-file)
(my/codex-proxy--download-file my/codex-proxy--environment-url
environment-example-file)
(with-temp-buffer
(insert-file-contents compose-file)
(let ((content (my/codex-proxy--replace-required-seam
(buffer-string)
"^[[:space:]]*- \"127\\.0\\.0\\.1:\\${PORT:-8080}:8080\"$"
" - \"127.0.0.1:${PORT:-8080}:8080\""
"Compose loopback API port")))
(write-region
(my/codex-proxy--replace-required-seam
content "^[[:space:]]*- \"1455:1455\"$"
" - \"127.0.0.1:1455:1455\"" "Compose port 1455")
nil compose-file nil 'silent)))
(with-temp-buffer
(insert-file-contents environment-example-file)
(write-region
(my/codex-proxy--replace-required-seam
(buffer-string) "^PORT=8080$" "PORT=8787" "environment PORT")
nil environment-file nil 'silent))))
(defun my/codex-proxy-install ()
"Install a new local codex-proxy Compose setup without starting Docker."
(interactive)
(let* ((directory (file-name-as-directory
(expand-file-name my/codex-proxy-directory)))
(target-files (my/codex-proxy--installation-paths directory))
(directory-existed (file-directory-p directory)))
(when (cl-some #'my/codex-proxy--file-exists-or-symlink-p target-files)
(user-error "codex-proxy setup already exists in %s; refusing to overwrite it"
directory))
(let ((temporary-directory (make-temp-file "codex-proxy-install-" t))
installed-files
completed)
(unwind-protect
(progn
(my/codex-proxy--prepare-installation temporary-directory)
(unless directory-existed
(make-directory directory t))
(when (cl-some #'my/codex-proxy--file-exists-or-symlink-p target-files)
(user-error "codex-proxy setup appeared in %s; refusing to overwrite it"
directory))
(dolist (name my/codex-proxy--installation-files)
(let ((source (expand-file-name name temporary-directory))
(target (expand-file-name name directory)))
(rename-file source target)
(when (string= name ".env")
(set-file-modes target #o600))
(push target installed-files)))
(setq completed t)
(message "Installed codex-proxy setup in %s; run SPC a i d u to start it."
directory))
(unless completed
(dolist (file installed-files)
(when (my/codex-proxy--file-exists-or-symlink-p file)
(delete-file file)))
(when (and (not directory-existed)
(file-directory-p directory)
(null (directory-files directory nil directory-files-no-dot-files-regexp)))
(delete-directory directory)))
(when (file-directory-p temporary-directory)
(delete-directory temporary-directory t))))))
(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
"aidi" 'my/codex-proxy-install
"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.28.1. STL review
(require 'cl-lib)
(defvar-local my/stl-review-source-buffer nil
"Visited STL source buffer associated with an adapter or preview buffer.")
(defvar-local my/stl-review-source-path nil
"Validated STL path associated with an adapter or preview buffer.")
(defvar-local my/stl-review-adapter-buffer nil
"Hidden SCAD buffer that imports the STL being reviewed.")
(defvar-local my/stl-review-preview-buffer nil
"SCAD preview buffer for the STL being reviewed.")
(defvar-local my/stl-review-preview-state nil
"Persistent STL preview state: source buffer, adapter buffer, and executable.")
(put 'my/stl-review-source-path 'permanent-local t)
(put 'my/stl-review-adapter-buffer 'permanent-local t)
(put 'my/stl-review-preview-buffer 'permanent-local t)
(put 'my/stl-review-preview-state 'permanent-local t)
(defun my/stl-review--validate-source-file (source)
"Return local regular SOURCE or signal an actionable error."
(when (file-remote-p source)
(user-error "STL review supports local .stl files only: %s" source))
(setq source (expand-file-name source))
(unless (file-regular-p source)
(user-error "STL review source is missing or is not a regular file: %s"
source))
source)
(defun my/stl-review--source-file ()
"Return the current local STL file or signal an actionable error."
(let ((source (buffer-file-name)))
(unless source
(user-error "STL review requires a visited local .stl file"))
(my/stl-review--validate-source-file source)))
(defun my/stl-review--openscad ()
"Return the OpenSCAD executable resolved from PATH."
(let ((openscad (executable-find "openscad")))
(unless (and openscad (file-executable-p openscad))
(user-error
"OpenSCAD is not executable. Install OpenSCAD and ensure `openscad` is on PATH"))
openscad))
(defun my/stl-review--scad-string (string)
"Return STRING quoted and escaped for an OpenSCAD string literal."
(concat
"\""
(mapconcat
(lambda (character)
(pcase character
(?\\ "\\\\")
(?\" "\\\"")
(?\n "\\n")
(?\r "\\r")
(?\t "\\t")
(_ (char-to-string character))))
string
"")
"\""))
(defun my/stl-review--write-adapter (source)
"Replace the current hidden buffer with an import of SOURCE."
(let ((inhibit-read-only t))
(erase-buffer)
(insert (format "import(%s);\n" (my/stl-review--scad-string source)))
(set-buffer-modified-p nil)))
(defun my/stl-review--configure-preview ()
"Restore STL-specific preview state after `scad-preview-mode' resets."
(when my/stl-review-preview-state
(let ((source-path (plist-get my/stl-review-preview-state :source-path))
(source-buffer (plist-get my/stl-review-preview-state :source-buffer))
(adapter (plist-get my/stl-review-preview-state :adapter))
(openscad (plist-get my/stl-review-preview-state :openscad)))
(setq-local my/stl-review-source-path source-path
my/stl-review-source-buffer source-buffer
my/stl-review-adapter-buffer adapter
scad-command (shell-quote-argument openscad))
(use-local-map (copy-keymap scad-preview-mode-map))
(local-set-key (kbd "g") #'my/stl-review-rerender)
(when (fboundp 'evil-local-set-key)
(evil-local-set-key 'normal (kbd "g") #'my/stl-review-rerender))
(add-hook 'kill-buffer-hook #'my/stl-review--preview-killed nil t))))
(add-hook 'scad-preview-mode-hook #'my/stl-review--configure-preview)
(defun my/stl-review--preview-killed ()
"Clean the hidden adapter and clear the owning STL workflow buffer."
(let* ((state my/stl-review-preview-state)
(owner (plist-get state :owner))
(adapter my/stl-review-adapter-buffer)
(preview (current-buffer)))
(when (buffer-live-p adapter)
(kill-buffer adapter))
(when (buffer-live-p owner)
(with-current-buffer owner
(when (eq my/stl-review-preview-buffer preview)
(setq-local my/stl-review-source-path nil
my/stl-review-preview-buffer nil
my/stl-review-adapter-buffer nil)
(remove-hook 'post-command-hook
#'my/ranger-stl-preview--watch-entry-change t))))))
(defun my/stl-review--cleanup ()
"Clean the preview and hidden adapter associated with the current STL buffer."
(remove-hook 'post-command-hook #'my/stl-review--select-preview t)
(remove-hook 'post-command-hook
#'my/ranger-stl-preview--watch-entry-change t)
(let ((preview my/stl-review-preview-buffer)
(adapter my/stl-review-adapter-buffer))
(setq-local my/stl-review-source-path nil
my/stl-review-preview-buffer nil
my/stl-review-adapter-buffer nil)
(when (buffer-live-p preview)
(kill-buffer preview))
(when (buffer-live-p adapter)
(kill-buffer adapter))))
(defun my/stl-review--owner-killed ()
"Clean STL review buffers when their owning buffer is killed."
(my/stl-review--cleanup))
(defun my/stl-review--select-preview ()
"Select the STL preview after an interactive file visit completes."
(remove-hook 'post-command-hook #'my/stl-review--select-preview t)
(when (buffer-live-p my/stl-review-preview-buffer)
(pop-to-buffer my/stl-review-preview-buffer)))
(defun my/stl-review-rerender ()
"Rerender the current STL preview from its source file on disk."
(interactive)
(unless (derived-mode-p 'scad-preview-mode)
(user-error "STL review rerender is only available in an STL preview"))
(let ((source-path my/stl-review-source-path)
(adapter my/stl-review-adapter-buffer))
(unless (buffer-live-p adapter)
(user-error "The STL review adapter is no longer available"))
(let ((source (my/stl-review--validate-source-file source-path))
(openscad (my/stl-review--openscad)))
(with-current-buffer adapter
(my/stl-review--write-adapter source))
(setq-local my/stl-review-source-path source
my/stl-review-preview-state
(plist-put (copy-sequence my/stl-review-preview-state)
:source-path source))
(setq-local my/stl-review-preview-state
(plist-put my/stl-review-preview-state :openscad openscad))
(my/stl-review--configure-preview)
(scad--preview-render))))
(defun my/stl-review--create-preview (source &optional source-buffer)
"Create a preview for validated SOURCE, optionally linked to SOURCE-BUFFER.
When SOURCE-BUFFER is nil, the current buffer owns a passive preview and no
STL file buffer is visited."
(require 'scad-mode)
(let* ((owner (current-buffer))
(openscad (my/stl-review--openscad))
(adapter (generate-new-buffer
(format " *stl review adapter: %s*"
(file-name-nondirectory source))))
preview)
(condition-case err
(progn
(with-current-buffer adapter
(setq-local default-directory (file-name-directory source))
(my/stl-review--write-adapter source)
(scad-mode)
(when (bound-and-true-p flymake-mode)
(flymake-mode -1))
(let ((scad-command (shell-quote-argument openscad)))
(if source-buffer
(scad-preview)
(cl-letf (((symbol-function 'display-buffer)
(lambda (&rest _arguments) nil)))
(scad-preview))))
(setq preview scad--preview-buffer))
(unless (buffer-live-p preview)
(error "scad-preview-mode did not create a preview buffer"))
(with-current-buffer owner
(setq-local my/stl-review-source-path source
my/stl-review-adapter-buffer adapter
my/stl-review-preview-buffer preview))
(with-current-buffer adapter
(setq-local my/stl-review-source-buffer source-buffer
my/stl-review-preview-buffer preview))
(with-current-buffer preview
(setq-local my/stl-review-preview-state
(list :source-path source :source-buffer source-buffer
:owner owner :adapter adapter :openscad openscad))
(my/stl-review--configure-preview))
preview)
(error
(let ((created-preview
(and (buffer-live-p adapter)
(buffer-local-value 'scad--preview-buffer adapter))))
(when (buffer-live-p created-preview)
(kill-buffer created-preview)))
(when (buffer-live-p adapter)
(kill-buffer adapter))
(signal (car err) (cdr err))))))
(defun my/stl-review--start ()
"Create and select a standalone preview for the current visited STL."
(my/stl-review--cleanup)
(let ((preview (my/stl-review--create-preview
(my/stl-review--source-file) (current-buffer))))
(add-hook 'kill-buffer-hook #'my/stl-review--owner-killed nil t)
(add-hook 'post-command-hook #'my/stl-review--select-preview nil t)
preview))
(defun my/stl-review--show-error (message)
"Replace binary content with MESSAGE in the read-only STL review buffer."
(let ((inhibit-read-only t))
(erase-buffer)
(insert "STL review unavailable: " message "\n\n"
"Install OpenSCAD and ensure `openscad` is on PATH, then revert this buffer.")
(goto-char (point-min))
(set-buffer-modified-p nil))
(setq-local buffer-read-only t))
(define-derived-mode my/stl-review-mode special-mode "STL-Review"
"Read-only OpenSCAD preview workflow for a visited STL file."
(condition-case err
(my/stl-review--start)
(error
(let ((message (error-message-string err)))
(my/stl-review--show-error message)
(user-error "%s" message)))))
(add-to-list 'auto-mode-alist '("\\.stl\\'" . my/stl-review-mode))
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-regionas 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.
- If yes, we assume it as the input file name for
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))
(defun my/org-agenda-refresh-and-open ()
"Refresh agenda files, then open the agenda dispatcher."
(interactive)
(scan-new-agenda-files)
(call-interactively #'org-agenda))
(spacemacs/set-leader-keys
"aoo" 'my/org-agenda-refresh-and-open
"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 ((export-info (org-export-get-environment 'pandoc-eisvogel))
(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
(append
(list "--from" "org"
"--output" output-file
"--pdf-engine=xelatex"
"--template" "eisvogel"
"--listings")
(when (plist-get export-info :with-toc)
(list "--toc"))
(when (plist-get export-info :section-numbers)
(list "--number-sections"))
(list "--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):
- Create a Google Cloud project, enable Calendar API, create a Desktop OAuth 2.0 client.
- In Google Calendar settings, share any secondary account's calendar with your primary Gmail address.
- Generate a dedicated GPG key:
gpg --full-generate-key(RSA 4096, no expiry). - Note the key ID:
gpg --list-secret-keys --keyid-format=long - Add to
~/.authinfo:machine org-gcal-client-id password <client-id>.apps.googleusercontent.commachine 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>
- 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)))
;; Manual fetches use loopback pinentry so Emacs prompts for GPG passphrases
;; in the minibuffer. Background OAuth store access enforces
;; `epg-pinentry-mode' `error', so an expired gpg-agent cache fails promptly
;; instead of blocking Emacs on a passphrase request. Loopback requires
;; allow-loopback-pinentry in ~/.gnupg/gpg-agent.conf.
(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))
;; A policy token remains active until the Deferred returned by org-gcal settles.
;; This is required because OAuth token reads happen in later Deferred callbacks,
;; after a normal dynamic binding of `epg-pinentry-mode' would have unwound.
(defvar my/org-gcal--active-gpg-policies nil)
(defvar my/org-gcal--operation-depth 0)
(defvar my/org-gcal--requested-gpg-policy nil)
(defun my/org-gcal--pinentry-mode ()
"Return the active org-gcal GPG policy, defaulting to non-prompting mode.
An overlapping background operation takes precedence over a manual operation so
it can never inherit a loopback prompt."
(if (catch 'background
(dolist (entry my/org-gcal--active-gpg-policies)
(when (eq (cdr entry) 'error)
(throw 'background t))))
'error
(if my/org-gcal--active-gpg-policies 'loopback 'error)))
(defun my/org-gcal--remove-gpg-policy (token &rest _)
"Remove TOKEN after its org-gcal Deferred settles or is cancelled."
(setq my/org-gcal--active-gpg-policies
(assq-delete-all token my/org-gcal--active-gpg-policies)))
(defun my/org-gcal--cancel-gpg-policy (token original-cancel deferred)
"Remove TOKEN, then restore and invoke DEFERRED's ORIGINAL-CANCEL function."
(my/org-gcal--remove-gpg-policy token)
;; A second cancellation uses the original function directly, so the token
;; cleanup wrapper runs exactly once while preserving Deferred cancellation.
(setf (deferred-cancel deferred) original-cancel)
(funcall original-cancel deferred))
(defun my/org-gcal--background-gpg-error (err)
"Report ERR from a non-prompting org-gcal OAuth store operation."
(message "org-gcal: background fetch will not prompt for GPG; run SPC a o s manually to unlock the gpg-agent cache (%s)"
(error-message-string err))
(signal (car err) (cdr err)))
(defun my/org-gcal--prime-gpg-cache (policy &optional noerror)
"Decrypt the org-gcal plstore once under POLICY without retaining plaintext.
POLICY is `loopback' for a manual command and `error' for all background work.
If NOERROR is non-nil, report failures and return nil; otherwise re-signal them."
(let ((plstore-file (expand-file-name "oauth2-auto.plist" user-emacs-directory))
(epg-pinentry-mode policy)
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. The plaintext values are discarded.
(dolist (entry (plstore-find store nil))
(plstore-get store (car entry)))
t)
(when store
(plstore-close store)))
(error
(cond
(noerror
(message "org-gcal: failed to prime gpg-agent cache: %s"
(error-message-string err))
nil)
((eq policy 'error)
(my/org-gcal--background-gpg-error err))
(t
(signal (car err) (cdr err)))))))))
(defun my/org-gcal--with-gpg-policy (original &rest args)
"Prime once and retain a GPG policy around a top-level org-gcal operation."
(if (> my/org-gcal--operation-depth 0)
(apply original args)
(let* ((policy (or my/org-gcal--requested-gpg-policy
(if (called-interactively-p 'interactive) 'loopback 'error)))
(token (make-symbol "org-gcal-gpg-policy"))
(my/org-gcal--operation-depth (1+ my/org-gcal--operation-depth)))
(push (cons token policy) my/org-gcal--active-gpg-policies)
(condition-case err
(progn
(my/org-gcal--prime-gpg-cache policy)
(let ((result (apply original args)))
(if (deferred-p result)
;; `deferred:watch' runs on success and error, preserves the
;; value/error, and re-signals errors into the original chain.
(let ((watcher
(deferred:watch result
(apply-partially #'my/org-gcal--remove-gpg-policy token))))
;; `deferred:cancel' bypasses watch callbacks, so wrap the
;; returned watcher's existing cancel function as well.
(setf (deferred-cancel watcher)
(apply-partially #'my/org-gcal--cancel-gpg-policy
token (deferred-cancel watcher)))
watcher)
(my/org-gcal--remove-gpg-policy token)
result)))
(error
(my/org-gcal--remove-gpg-policy token)
(signal (car err) (cdr err)))))))
(defun my/org-gcal--with-oauth2-auto-gpg-policy (original username provider &rest args)
"Apply the active org-gcal GPG policy to OAuth plstore access only.
This advises oauth2-auto's private read/write seams in the installed package;
keep the `fboundp' guards because that package may change those internals."
(if (not (eq provider 'org-gcal))
(apply original username provider args)
(let ((policy (my/org-gcal--pinentry-mode)))
(condition-case err
(let ((epg-pinentry-mode policy))
(apply original username provider args))
(error
(if (eq policy 'error)
(my/org-gcal--background-gpg-error err)
(signal (car err) (cdr err))))))))
;; Safe fetch wrapper: read-only fetch plus stale-lock handling. The dynamic
;; request makes this interactive command loopback-capable before fetch calls
;; its nested sync operation.
(defun my/org-gcal-fetch-safe ()
"Fetch Google Calendar events without pushing local changes to Google."
(interactive)
(let ((my/org-gcal--requested-gpg-policy
(if (called-interactively-p 'interactive) 'loopback 'error)))
(condition-case err
(progn
(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 (called-interactively-p 'interactive)
(signal (car err) (cdr err))
(message "org-gcal: fetch skipped: %s"
(error-message-string err)))))))
(with-eval-after-load 'oauth2-auto
(dolist (function '(oauth2-auto--plstore-read oauth2-auto--plstore-write))
(when (fboundp function)
(unless (advice-member-p #'my/org-gcal--with-oauth2-auto-gpg-policy function)
(advice-add function :around #'my/org-gcal--with-oauth2-auto-gpg-policy)))))
(with-eval-after-load 'org-gcal
(dolist (function '(org-gcal-fetch org-gcal-sync))
(unless (advice-member-p #'my/org-gcal--with-gpg-policy function)
(advice-add function :around #'my/org-gcal--with-gpg-policy))))
;; Prime once after startup without prompting. If the cache is locked, use
;; SPC a o s manually before the next scheduled fetch.
(add-hook 'emacs-startup-hook
(lambda ()
(run-with-idle-timer 2 nil #'my/org-gcal--prime-gpg-cache 'error :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.8.1. Ranger STL preview
(defun my/ranger-stl-preview-file-p (entry-name)
"Return non-nil when ENTRY-NAME names an STL file."
(string-equal (downcase (or (file-name-extension entry-name) "")) "stl"))
(defun my/ranger-stl-preview-error-buffer (source message owner)
"Return an OWNER-linked passive Ranger error buffer for SOURCE and MESSAGE."
(let ((buffer (generate-new-buffer
(format " *ranger STL error: %s*"
(file-name-nondirectory source)))))
(with-current-buffer buffer
(special-mode)
;; Ranger adds a mouse binding with `local-set-key'; isolate that
;; mutation from the shared `special-mode-map'.
(use-local-map (copy-keymap special-mode-map))
(setq-local my/stl-review-preview-state
(list :source-path source :source-buffer nil
:owner owner :adapter nil))
(add-hook 'kill-buffer-hook #'my/stl-review--preview-killed nil t)
(my/stl-review--show-error message))
buffer))
(defun my/ranger-stl-preview--owns-state-p ()
"Return non-nil when the current Ranger buffer owns STL state."
(or (buffer-live-p my/stl-review-preview-buffer)
(buffer-live-p my/stl-review-adapter-buffer)))
(defun my/ranger-stl-preview--owns-live-source-p (source)
"Return non-nil when the current Ranger buffer owns live SOURCE preview state."
(and (equal source my/stl-review-source-path)
(buffer-live-p my/stl-review-preview-buffer)
(buffer-live-p my/stl-review-adapter-buffer)))
(defun my/ranger-stl-preview--entry-path ()
"Return the current Ranger entry path, or nil when there is no entry."
(condition-case nil
(let ((entry-name (dired-get-filename nil t)))
(and entry-name (expand-file-name entry-name)))
(error nil)))
(defun my/ranger-stl-preview-cleanup-stale-owner ()
"Clean STL state only after this Ranger owner's entry path changes."
(let ((entry-path (my/ranger-stl-preview--entry-path)))
(when (and (my/ranger-stl-preview--owns-state-p)
(not (equal entry-path my/stl-review-source-path)))
(my/stl-review--cleanup))))
(defun my/ranger-stl-preview--watch-entry-change ()
"Clean this Ranger owner's STL state as soon as its entry changes."
(my/ranger-stl-preview-cleanup-stale-owner))
(defun my/ranger-stl-preview--watch-owner ()
"Install buffer-local cleanup hooks while this Ranger owns STL state."
(when (my/ranger-stl-preview--owns-state-p)
(add-hook 'kill-buffer-hook #'my/stl-review--owner-killed nil t)
(add-hook 'post-command-hook
#'my/ranger-stl-preview--watch-entry-change nil t)))
(defun my/ranger-stl-preview-setup-preview (original-function &rest arguments)
"Apply Ranger STL ownership cleanup before ORIGINAL-FUNCTION runs."
(when (derived-mode-p 'ranger-mode)
(my/ranger-stl-preview-cleanup-stale-owner))
(apply original-function arguments))
(defun my/ranger-stl-preview-buffer (original-function entry-name)
"Return a passive STL preview for Ranger, else delegate ORIGINAL-FUNCTION."
(if (and (derived-mode-p 'ranger-mode)
(my/ranger-stl-preview-file-p entry-name))
(let ((source (expand-file-name entry-name)))
(condition-case err
(progn
(setq source (my/stl-review--validate-source-file source))
(let ((preview
(if (my/ranger-stl-preview--owns-live-source-p source)
my/stl-review-preview-buffer
(my/stl-review--cleanup)
(my/stl-review--create-preview source))))
(my/ranger-stl-preview--watch-owner)
preview))
(error
(my/stl-review--cleanup)
(let ((error-buffer
(my/ranger-stl-preview-error-buffer
source (error-message-string err) (current-buffer))))
(setq-local my/stl-review-source-path source
my/stl-review-adapter-buffer nil
my/stl-review-preview-buffer error-buffer)
(my/ranger-stl-preview--watch-owner)
error-buffer))))
(when (derived-mode-p 'ranger-mode)
(my/stl-review--cleanup))
(funcall original-function entry-name)))
(defun my/ranger-stl-preview-install-advice ()
"Install the Ranger STL advice once, replacing any prior copy."
(advice-remove 'ranger-preview-buffer #'my/ranger-stl-preview-buffer)
(advice-add 'ranger-preview-buffer :around #'my/ranger-stl-preview-buffer)
(advice-remove 'ranger-setup-preview #'my/ranger-stl-preview-setup-preview)
(advice-add 'ranger-setup-preview :around
#'my/ranger-stl-preview-setup-preview))
(with-eval-after-load 'ranger
(my/ranger-stl-preview-install-advice))
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))