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. isort, compatible with black
- 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. ccls
- 3.2.10. devcontainer
- 3.2.11. cmake-ide
- 3.2.12. company
- 3.2.13. minuet-ai
- 3.2.14. codex-proxy
- 3.2.15. pi-coding-agent
- 3.2.16. lsp-ui
- 3.2.17. popup
- 3.2.18. dap
- 3.2.19. flycheck
- 3.2.20. Indentation for web development
- 3.2.21. groovy (Jenkinsfile)
- 3.2.22. plantuml
- 3.2.23. nvm
- 3.2.24. conf-mode
- 3.2.25. Rust
- 3.2.26. scad-mode
- 3.2.27. Style for linux kernel development
- 3.2.28. tramp + clang-format
- 3.2.29. 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-ai
- 3.4.5. org-pomodora
- 3.4.6. org-agenda
- 3.4.7. org-cv
- 3.4.8. org-babel
- 3.4.9. org-latex
- 3.4.10. org-journal
- 3.4.11. org-table
- 3.4.12. org-todo
- 3.4.13. org-hugo
- 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. Workaround for unsigned packages
(setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3")
2.7.2. 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. isort, compatible with black
(setq py-isort-options '("--profile" "black"))
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. 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.10. 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.11. 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.12. 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.13. 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.4-mini, 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:8080/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.4-mini"
: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, you MUST produce a Conventional Commit:
- Use the staged diff as the source of truth when it is present in the prompt.
- Use `type(scope): summary` for the subject, e.g. `feat(minuet): add model selector`.
- 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 commit message text, without commentary or markdown fences."
"Additional Minuet guidance for `git-commit-mode'.")
(defun my/minuet-git-commit-buffer-p ()
"Return non-nil when the current buffer is a Git commit message buffer."
(or (bound-and-true-p git-commit-mode)
(derived-mode-p 'git-commit-elisp-text-mode)))
(defvar my/minuet-git-staged-diff-max-chars 12000
"Maximum number of staged diff characters to include in Minuet prompts.")
(defun my/minuet-git-root ()
"Return the current Git repository root, or nil outside a Git repo."
(locate-dominating-file default-directory ".git"))
(defun my/minuet-git-command-output (&rest args)
"Return output from git ARGS in the current repository, or nil on failure."
(require 'subr-x)
(when-let* ((root (my/minuet-git-root)))
(with-temp-buffer
(let ((default-directory root))
(when (zerop (apply #'process-file "git" nil t nil args))
(string-trim-right (buffer-string)))))))
(defun my/minuet-truncate-for-prompt (text max-chars)
"Return TEXT truncated to MAX-CHARS for prompt inclusion."
(if (and text (> (length text) max-chars))
(concat (substring text 0 max-chars)
"\n\n[staged diff truncated for prompt]")
text))
(defun my/minuet-git-staged-diff-for-prompt (_context)
"Return staged Git changes formatted for Minuet commit prompts."
(when (my/minuet-git-commit-buffer-p)
(let* ((stat (my/minuet-git-command-output "diff" "--cached" "--stat"))
(diff (my/minuet-git-command-output "diff" "--cached" "--"))
(diff (my/minuet-truncate-for-prompt
diff
my/minuet-git-staged-diff-max-chars)))
(mapconcat #'identity
(delq nil
(list (unless (string= (or stat "") "")
(concat "Staged diffstat:\n" stat))
(unless (string= (or diff "") "")
(concat "Staged diff:\n" diff))))
"\n\n"))))
(defun my/minuet-openai-compatible-chat-template ()
"Return a mode-aware chat input template for Minuet."
(if (my/minuet-git-commit-buffer-p)
"{{{:language-and-tab}}}
<contextBeforeCursor>
{{{:context-before-cursor}}}<cursorPosition>
<contextAfterCursor>
{{{:context-after-cursor}}}
<stagedChanges>
{{{:staged-diff}}}
</stagedChanges>"
minuet-default-chat-input-template-prefix-first))
(defun my/minuet-openai-compatible-guidelines ()
"Return mode-aware generation guidelines for Minuet."
(if (my/minuet-git-commit-buffer-p)
"Guidelines:
1. Complete text after the `<cursorPosition>` marker.
2. Produce one complete Conventional Commit message.
3. The subject line is required and must be `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 commit message text. 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-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))
(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)
(plist-put minuet-openai-compatible-options
:optional
'(:max_tokens 128
:temperature 0.2
:top_p 0.9
:chat_template_kwargs (:enable_thinking :false))))
3.2.14. codex-proxy
Manage the local IceBear codex-proxy Docker Compose service used by Minuet's Codex profile. The service lives in ~/.local/share/codex-proxy and exposes the dashboard/API at http://127.0.0.1:8787.
(defvar my/codex-proxy-directory "~/.local/share/codex-proxy"
"Directory containing the local codex-proxy Docker Compose setup.")
(defvar my/codex-proxy-dashboard-url "http://127.0.0.1:8787"
"Dashboard URL for the local codex-proxy service.")
(defun my/codex-proxy--default-directory ()
"Return the codex-proxy Compose directory or signal a user error."
(let ((directory (file-name-as-directory
(expand-file-name my/codex-proxy-directory))))
(unless (file-directory-p directory)
(user-error "codex-proxy directory does not exist: %s" directory))
directory))
(defun my/codex-proxy--run-docker-compose (arguments)
"Run docker compose with ARGUMENTS in `my/codex-proxy-directory'."
(let ((default-directory (my/codex-proxy--default-directory)))
(async-shell-command (concat "docker compose " arguments) "*codex-proxy*")))
(defun my/codex-proxy-docker-up ()
"Start the local codex-proxy Docker Compose service."
(interactive)
(my/codex-proxy--run-docker-compose "up -d"))
(defun my/codex-proxy-docker-down ()
"Stop and remove the local codex-proxy Docker Compose service."
(interactive)
(my/codex-proxy--run-docker-compose "down"))
(defun my/codex-proxy-docker-restart ()
"Restart the local codex-proxy container."
(interactive)
(my/codex-proxy--run-docker-compose "restart codex-proxy"))
(defun my/codex-proxy-docker-status ()
"Show the local codex-proxy Docker Compose status."
(interactive)
(my/codex-proxy--run-docker-compose "ps"))
(defun my/codex-proxy-docker-logs ()
"Follow recent logs for the local codex-proxy container."
(interactive)
(my/codex-proxy--run-docker-compose "logs --tail=200 -f codex-proxy"))
(defun my/codex-proxy-open-dashboard ()
"Open the local codex-proxy dashboard."
(interactive)
(browse-url my/codex-proxy-dashboard-url))
(spacemacs/declare-prefix "aid" "codex-proxy")
(spacemacs/set-leader-keys
"aidu" 'my/codex-proxy-docker-up
"aidd" 'my/codex-proxy-docker-down
"aidr" 'my/codex-proxy-docker-restart
"aids" 'my/codex-proxy-docker-status
"aidl" 'my/codex-proxy-docker-logs
"aido" 'my/codex-proxy-open-dashboard)
3.2.15. 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.16. 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.17. 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.18. 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.19. flycheck
(with-eval-after-load 'flycheck
(setq flycheck-check-syntax-automatically '(save
idle-buffer-switch
mode-enabled)))
3.2.20. 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.21. groovy (Jenkinsfile)
(setq groovy-indent-offset 2)
3.2.22. 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.23. 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.24. conf-mode
(add-to-list 'auto-mode-alist '("\\.eds\\'" . conf-mode))
(add-to-list 'auto-mode-alist '("\\.dcf\\'" . conf-mode))
3.2.25. Rust
3.2.25.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.26. 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.27. 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.28. 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.29. 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-ai
(use-package org-ai
:ensure t
:commands (org-ai-mode
org-ai-global-mode)
:custom (org-ai-openai-api-token (auth-source-pick-first-password :host "api.openai.com"))
:init
(add-hook 'org-mode-hook #'org-ai-mode) ; enable org-ai in org-mode
:config
(org-ai-global-mode) ; installs global keybindings on C-c M-a
(setq org-ai-default-chat-model "gpt-4o")
(org-ai-install-yasnippets)) ; if you are using yasnippet and want `ai` snippets
3.4.5. 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.6. org-agenda
(defun scan-new-agenda-files ()
(interactive)
(let ((org-dir (expand-file-name "~/org/")))
(if (file-directory-p org-dir)
(progn
(message "Scanning new agenda files...")
(setq org-agenda-files (directory-files-recursively org-dir "\\.org\\'" nil nil t)))
(message "Warning: ~/org/ directory does not exist. Skipping agenda files scan."))))
(with-eval-after-load 'org-agenda
(scan-new-agenda-files)
(define-key org-agenda-mode-map "m" 'org-agenda-month-view)
(define-key org-agenda-mode-map "y" 'org-agenda-year-view))
(spacemacs/set-leader-keys "aou" 'scan-new-agenda-files)
;; Add category into the ~org-todo-list~ view
(setq org-agenda-prefix-format
'((agenda . " %i %-16:c%?-12t% s")
(todo . " %i %-16:c ") ;; Added a space here
(tags . " %i %-16:c")
(search . " %i %-16:c")))
;; Add custom agenda view for my dashboard
(setq org-agenda-custom-commands
'(("d" "My Dashboard"
((agenda ""
((org-agenda-span 7)
;; This line fixes the column width (%-16c reserves 16 chars)
(org-agenda-prefix-format " %-16:c%?-12t% s")))
(todo "UNDER_REVIEW" ;; Show what's under review
((org-agenda-overriding-header "Under Review")))
(todo "IN_PROGRESS" ;; Show what I'm working on NOW
((org-agenda-overriding-header "Current Focus")))
(todo "READY" ;; Show what's ready for development (e.g. groomed and prioritized)
((org-agenda-overriding-header "Selected for Development")))
(todo "TODO" ;; Show the rest of the list
((org-agenda-overriding-header "Inbox / Backlog")))))))
;; Customize the colors of TODO keywords for better visual distinction
(setq org-todo-keyword-faces
'(;; Standard Flow
("TODO" . (:foreground "goldenrod" :weight bold))
("IN_PROGRESS" . (:foreground "deep sky blue" :weight bold))
("UNDER_REVIEW" . (:foreground "medium purple" :weight bold))
;; Bug Tracking Flow
("REPORT" . (:foreground "indian red" :weight bold))
("BUG" . (:foreground "red" :weight bold))
("KNOWNCAUSE" . (:foreground "orange" :weight bold))
("FIXED" . (:foreground "green" :weight bold))
;; Terminal / Inactive States (Muted)
("CANCELED" . (:foreground "dark gray" :strike-through t))
("REPORTED" . (:foreground "cadet blue"))))
3.4.7. org-cv
(use-package ox-awesomecv
:load-path "~/.spacemacs.d/private/org-cv"
:init (require 'ox-awesomecv))
3.4.8. org-babel
(with-eval-after-load 'org
(org-babel-do-load-languages
'org-babel-load-languages
'((C . t)
(python . t)
(shell . t))))
3.4.9. 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.10. 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.11. org-table
(with-eval-after-load 'org
(define-key org-mode-map (kbd "C-<tab>") 'org-table-previous-field))
3.4.12. 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.13. org-hugo
(spacemacs/set-leader-keys-for-major-mode 'org-mode "Th" 'org-hugo-auto-export-mode)
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)))
;; Use loopback pinentry so Emacs prompts for the passphrase in the minibuffer.
;; Requires allow-loopback-pinentry in ~/.gnupg/gpg-agent.conf.
;; NOTE: loopback needs an interactive minibuffer; the periodic idle-timer
;; sync (see org-gcal--prime-gpg-cache below) cannot prompt, so we proactively
;; prime the gpg-agent cache once at startup. After that, the agent's TTL
;; (configured for 1 week in gpg-agent.conf) covers subsequent timer syncs.
(setq epg-pinentry-mode 'loopback)
:config
;; Map each calendar ID to its dedicated org file.
;; Both files live in ~/org/home/ and are auto-discovered by scan-new-agenda-files.
(setq org-gcal-fetch-file-alist
`((,(auth-source-pick-first-password :host "org-gcal-calendar-id-personal")
. "~/org/home/gcal/gcal-personal.org")
(,(auth-source-pick-first-password :host "org-gcal-calendar-id-work")
. "~/org/home/gcal/gcal-work.org")))
;; Fetch window: 7 days back, 30 days forward.
(setq org-gcal-up-days 7
org-gcal-down-days 30)
;; Do not auto-archive old events outside the fetch window.
(setq org-gcal-auto-archive nil)
;; Ask before removing events that appear cancelled/missing on Google's side.
;; Avoids silent archiving of items that are simply outside the fetch window.
(setq org-gcal-update-cancelled-events-with-todo t
org-gcal-remove-api-cancelled-events 'ask)
;; Collect recurring event instances under their parent headline.
(setq org-gcal-recurring-events-mode 'nested)
;; Suppress popup notifications.
(setq org-gcal-notify-p nil))
;; Safe fetch wrapper: clears stale sync lock before fetching.
;; Uses org-gcal-fetch (read-only — never pushes local changes back to Google).
(defun my/org-gcal-fetch-safe ()
"Fetch Google Calendar events, clearing any stale sync lock first."
(interactive)
(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))
(defun my/org-gcal--prime-gpg-cache ()
"Decrypt the org-gcal plstore once to prime the gpg-agent passphrase cache.
Called interactively at Emacs startup so the loopback pinentry prompt has a
minibuffer available. After the agent caches the passphrase (TTL configured in
~/.gnupg/gpg-agent.conf), subsequent timer-driven org-gcal syncs decrypt without
needing a prompt, avoiding the misleading
epg-error \"Decryption failed\" \"No secret key: <subkey>\"
which gpg returns when no pinentry is reachable in a non-interactive context."
(let ((plstore-file (expand-file-name "oauth2-auto.plist" user-emacs-directory))
store)
(when (file-exists-p plstore-file)
(condition-case err
(unwind-protect
(progn
(setq store (plstore-open plstore-file))
;; `plstore-find' consults/decrypts secret entries through the
;; public plstore API, which is stable across Emacs versions.
(dolist (entry (plstore-find store nil))
(plstore-get store (car entry))))
(when store
(plstore-close store)))
(error
(message "org-gcal: failed to prime gpg-agent cache: %s"
(error-message-string err)))))))
;; Prime once after startup so the first decrypt happens with a minibuffer
;; available (loopback pinentry needs it). Run slightly deferred so other
;; startup activity doesn't compete for the minibuffer.
(add-hook 'emacs-startup-hook
(lambda ()
(run-with-idle-timer 2 nil #'my/org-gcal--prime-gpg-cache)))
;; Auto-fetch every 30 minutes. Adjust the interval as needed.
(run-at-time "30 min" 1800 #'my/org-gcal-fetch-safe)
;; Manual sync keybinding: SPC a o s
(spacemacs/set-leader-keys "aos" 'my/org-gcal-fetch-safe)
3.4.16. org-clock
3.4.16.1. Save the clock history across Emacs sessions
(setq org-clock-persist 'history)
(org-clock-persistence-insinuate)
3.4.17. Replace selected markdown region to org format
(defun replace-markdown-region-with-org (beginning end)
"Replace the selected markdown region with its corresponding org-mode format."
(interactive "r")
(if (use-region-p)
(let ((tmp-buffer "Markdown To Org Tmp"))
(pcase (shell-command-on-region (region-beginning) (region-end)
"pandoc -f markdown -t org" tmp-buffer t)
(0 (message "Successfully converted to org-mode format."))
(127 (message "pandoc not found. Install it with 'sudo apt install pandoc'."))
(_ (message "Failed to convert the selected region to org-mode format."))
)
)
(message "No active region is found.")))
(spacemacs/set-leader-keys-for-major-mode 'org-mode "RR" 'replace-markdown-region-with-org)
3.4.18. ob-lean4
(use-package ob-lean4
:load-path "~/.spacemacs.d/private/ob-lean4")
(add-to-list 'org-babel-load-languages '(lean4 . t))
3.4.19. ob-cmake
(use-package ob-cmake
:load-path "~/.spacemacs.d/private/ob-cmake")
(add-to-list 'org-babel-load-languages '(cmake . t))
3.4.20. ob-rust
(require 'ob-rust)
(add-to-list 'org-babel-load-languages '(rust . t))
3.5. Utility
3.5.1. beacon mode
(beacon-mode 1)
3.5.2. Toggle clang-format on save
(defun c-c++-toggle-clang-format-on-save ()
(interactive)
(cond
(c-c++-enable-clang-format-on-save
(message "[c-c++] disable clang-format on save")
(setq c-c++-enable-clang-format-on-save nil))
((not c-c++-enable-clang-format-on-save)
(message "[c-c++] enable clang-format on save")
(setq c-c++-enable-clang-format-on-save t))
))
(spacemacs/set-leader-keys-for-major-mode 'c-mode "Tf" 'c-c++-toggle-clang-format-on-save)
(spacemacs/set-leader-keys-for-major-mode 'c++-mode "Tf" 'c-c++-toggle-clang-format-on-save)
(spacemacs/declare-prefix-for-mode 'c-mode "Tf" "toggle-clang-format-on-save")
(spacemacs/declare-prefix-for-mode 'c++-mode "Tf" "toggle-clang-format-on-save")
3.5.3. auto-indent
;; I want to disable pasting with formatting on C/C++ buffers
(add-to-list 'spacemacs-indent-sensitive-modes 'c-mode)
(add-to-list 'spacemacs-indent-sensitive-modes 'c++-mode)
3.5.4. format-all
(add-hook 'sh-mode-hook #'format-all-mode)
(add-hook 'fish-mode-hook #'format-all-mode)
(add-hook 'cmake-mode-hook #'format-all-mode)
3.5.5. glow, the markdown viewer
;; Configure glow viewer
(defun start-glow-viewer ()
(interactive)
(start-process "glow-markdown-viewer" nil
"/usr/bin/x-terminal-emulator"
(file-truename "~/.spacemacs.d/scripts/glow_mk_viewer.sh")
(buffer-file-name nil)))
3.5.6. google-search
;; Set google as default search engine and open links with xdg-open or open depending on the OS
(spacemacs/set-leader-keys "ag" 'engine/search-google)
(setq browse-url-browser-function 'browse-url-generic
engine/browser-function 'browse-url-generic
browse-url-generic-program (cond ((string-equal system-type "darwin") "open")
((string-equal system-type "gnu/linux") "xdg-open")))
3.5.7. Kill all buffers
(defun nuke-all-buffers ()
(interactive)
(mapcar 'kill-buffer (buffer-list))
(delete-other-windows))
(global-set-key (kbd "C-x K") 'nuke-all-buffers)
3.5.8. ranger
(with-eval-after-load 'ranger
(require 'hydra)
(define-key ranger-mode-map (kbd "M-h") 'ranger-prev-tab)
(define-key ranger-mode-map (kbd "M-l") 'ranger-next-tab)
(define-key ranger-mode-map (kbd "M-n") 'ranger-new-tab))
(spacemacs/set-leader-keys "ar" 'ranger)
3.5.9. cheat.sh
The one and only one cheatsheet.
(spacemacs/declare-prefix "ac" "cheat-sh")
;; Prompt to select a topic to show its cheatsheet
(spacemacs/set-leader-keys "acl" 'cheat-sh)
;; Show the help page of cheat.sh
(spacemacs/set-leader-keys "ach" 'cheat-sh-help)
;; Get the cheatsheet for the marked region
(spacemacs/set-leader-keys "acr" 'cheat-sh-region)
;; Get a random page of cheatsheet
(spacemacs/set-leader-keys "acR"
(lambda ()
(interactive)
(cheat-sh ":random")))
(spacemacs/declare-prefix "acR" "cheat.sh/:random")
3.5.10. chatgpt-shell
(use-package chatgpt-shell
:load-path "~/.spacemacs.d/private/chatgpt-shell")
(setq chatgpt-shell-openai-key
(auth-source-pick-first-password :host "api.openai.com"))
3.6. mu4e
Email client using mu4e (Spacemacs layer) + mbsync (isync) for IMAP sync.
Two Gmail accounts: personal and work. Credentials stored in ~/.authinfo (not committed).
Requires: apt install isync maildir-utils mu4e w3m
3.6.1. Setup instructions (one-time)
Run these commands once after installing the system:
# Create maildir structure
mkdir -p ~/.mail/personal/{cur,new,tmp}
mkdir -p ~/.mail/work/{cur,new,tmp}
# Initial mail sync (after ~/.mbsyncrc is configured)
mbsync -a
# Index mail with mu (after initial sync)
mu init --maildir=~/.mail \
--my-address=$(pass show or cat ~/.authinfo | grep mu4e-email-personal ...) \
--my-address=$(...)
# Simpler: let mu4e run mu init on first launch via mu4e-update-mail-and-index
3.6.2. Configuration
(with-eval-after-load 'mu4e
;; Basic settings
(setq mu4e-maildir "~/.mail"
mu4e-get-mail-command "mbsync -a"
mu4e-update-interval (* 5 60) ; sync every 5 minutes
mu4e-compose-signature-auto-include nil
mu4e-view-show-images t
mu4e-view-image-max-width 800
mu4e-headers-date-format "%Y-%m-%d"
mu4e-headers-time-format "%H:%M"
mu4e-change-filenames-when-moving t ; avoid duplicate UID issues
mu4e-confirm-quit nil)
;; Maildir shortcuts shown in the main view
(setq mu4e-maildir-shortcuts
'((:maildir "/personal/INBOX" :key ?i :name "Personal Inbox")
(:maildir "/personal/[Gmail]/Sent Mail" :key ?s :name "Personal Sent")
(:maildir "/work/INBOX" :key ?I :name "Work Inbox")
(:maildir "/work/[Gmail]/Sent Mail" :key ?S :name "Work Sent")))
;; HTML rendering with w3m
(setq mu4e-html2text-command "w3m -T text/html")
;; Read account identities from ~/.authinfo
(let* ((personal-email (auth-source-pick-first-password :host "mu4e-email-personal"))
(personal-name (auth-source-pick-first-password :host "mu4e-name-personal"))
(work-email (auth-source-pick-first-password :host "mu4e-email-work"))
(work-name (auth-source-pick-first-password :host "mu4e-name-work")))
;; Contexts: one per account
(setq mu4e-contexts
(list
(make-mu4e-context
:name "Personal"
:match-func
(lambda (msg)
(when msg
(string-prefix-p "/personal" (mu4e-message-field msg :maildir))))
:vars `((user-mail-address . ,(or personal-email ""))
(user-full-name . ,(or personal-name ""))
(mu4e-sent-folder . "/personal/[Gmail]/Sent Mail")
(mu4e-drafts-folder . "/personal/[Gmail]/Drafts")
(mu4e-trash-folder . "/personal/[Gmail]/Bin")
(mu4e-refile-folder . "/personal/Archive")
(smtpmail-smtp-user . ,personal-email)
(smtpmail-smtp-server . "smtp.gmail.com")
(smtpmail-smtp-service . 587)
(smtpmail-stream-type . starttls)))
(make-mu4e-context
:name "Work"
:match-func
(lambda (msg)
(when msg
(string-prefix-p "/work" (mu4e-message-field msg :maildir))))
:vars `((user-mail-address . ,(or work-email ""))
(user-full-name . ,(or work-name ""))
(mu4e-sent-folder . "/work/[Gmail]/Sent Mail")
(mu4e-drafts-folder . "/work/[Gmail]/Drafts")
(mu4e-trash-folder . "/work/[Gmail]/Bin")
(mu4e-refile-folder . "/work/Archive")
(smtpmail-smtp-user . ,work-email)
(smtpmail-smtp-server . "smtp.gmail.com")
(smtpmail-smtp-service . 587)
(smtpmail-stream-type . starttls)))))
;; Policy: ask which context on ambiguous messages; default to Personal
(setq mu4e-context-policy 'pick-first
mu4e-compose-context-policy 'ask-if-none))
;; SMTP via smtpmail — passwords come from ~/.authinfo automatically
(setq send-mail-function 'smtpmail-send-it
message-send-mail-function 'smtpmail-send-it
smtpmail-auth-credentials "~/.authinfo")
;; org-mu4e integration: store links to emails (mu4e-org replaces org-mu4e since mu 1.8)
(require 'mu4e-org)
(setq org-mu4e-link-query-in-headers-mode nil)
;; Use 'Q' to truly quit mu4e
(define-key mu4e-main-mode-map (kbd "Q") 'mu4e-quit)
(define-key mu4e-headers-mode-map (kbd "Q") 'mu4e-quit)
(define-key mu4e-view-mode-map (kbd "Q") 'mu4e-quit)
;; Use 'q' to just bury the buffer (background)
(define-key mu4e-main-mode-map (kbd "q") 'mu4e-view-quit)
(define-key mu4e-headers-mode-map (kbd "q") 'mu4e-view-quit)
(define-key mu4e-view-mode-map (kbd "q") 'mu4e-view-quit)
)
;; Keybinding: open mu4e with SPC a m
(spacemacs/set-leader-keys "am" 'mu4e)
(spacemacs/declare-prefix "am" "mu4e-mail")
(mu4e t)
3.7. Workarounds
3.7.1. Workaround for the bug where company-mode and evil-mode are conflicting
(evil-declare-change-repeat 'company-complete)
3.7.2. doom-modeline icons in GUI vs terminal
;; Enable doom-modeline icons in GUI, disable in terminal
(with-eval-after-load 'doom-modeline
(setq doom-modeline-icon (display-graphic-p))
;; When creating a new frame (e.g. emacsclient), re-check
(add-hook 'after-make-frame-functions
(lambda (frame)
(with-selected-frame frame
(setq doom-modeline-icon (display-graphic-p))))))
3.7.3. Workaround for spammed false positive warning messages
Ticket: syl20bnr/spacemacs#16575 ‘org-element-at-point’ cannot be used in non-Org buffer
(add-to-list 'warning-suppress-types '(org-element org-element-parser))