doom

My literate doom emacs config
git clone https://github.com/michaelneuper/doom.git
Files | Log | Refs

doom/config.org (79264B)


#+title: Doom Emacs Config
#+author: Michael Neuper
#+startup: content noinlineimages nolatexpreview

This is my literate doom emacs config.
[[#basic][Basic]] sets up some basic emacs and doom configurations.
[[#doom-modules][Doom Modules]] contains the configuration all packages and tries to mirror the structure
of doom's =init.el=.

- Each top-level heading under [[#doom-modules][doom modules]] corresponds to a Doom module category
  (e.g., [[#lang][:lang]], [[#ui][:ui]]), as declared in the =doom!= macro in =init.el=.
- Each second-level heading  corresponds to an individual module within that category
  (e.g., [[#org][org]], [[#modeline][modeline]]).
- Each third-level heading represents any flags applied to that module
  (e.g., [[#+pretty][+pretty]], [[#+roam2][+roam]]), which alter the module’s behavior or dependencies.
- Subsequent headings:
  + Core packages enabled by those flags (e.g., [[#org-modern][org-modern]])
  + Manually installed packages related to that module (e.g., org-modern-indent)
  + Configuration snippets or tweaks logically grouped by package or functionality

* Contents :toc:
- [[#basic][Basic]]
  - [[#defaults][Defaults]]
  - [[#visual-enchancements][Visual Enchancements]]
- [[#doom-modules][Doom Modules]]
  - [[#completion][:completion]]
  - [[#ui][:ui]]
  - [[#editor][:editor]]
  - [[#emacs][:emacs]]
  - [[#term][:term]]
  - [[#tools][:tools]]
  - [[#lang][:lang]]

* Basic
** Defaults
*** Header
Make this file run slightly faster (see [[https://nullprogram.com/blog/2016/12/22/][this blog post]]).
#+begin_src emacs-lisp
;;; $DOOMDIR/config.el -*- lexical-binding: t; -*-
#+end_src

#+begin_src emacs-lisp :tangle packages.el
;; -*- no-byte-compile: t; -*-
;;; $DOOMDIR/packages.el
#+end_src

#+begin_src emacs-lisp :tangle doctor.el
;; -*- no-byte-compile: t; -*-
;;; $DOOMDIR/doctor.el
#+end_src

*** Basic
#+begin_src emacs-lisp
(setq! user-full-name "Michael Neuper"
       user-mail-address "michael@michaelneuper.com"
       auto-save-default t
       make-backup-files t
       confirm-kill-emacs nil
       auto-revert-avoid-polling t ;; refresh buffers when files change on disk
       doom-localleader-key "SPC l" ;; remap <localleader> from SPC m to SPC l
       doom-localleader-alt-key "M-SPC l")

#+end_src

Some useful keybindings for the minbuffer.
#+begin_src emacs-lisp
(map! :map minibuffer-local-map
      "C-k" #'delete-minibuffer-contents
      "C-p" #'yank)
#+end_src

Some =which-key= adjustments.
#+begin_src emacs-lisp
(use-package! which-key
  :init
  (setq which-key-idle-delay 0.5
        which-key-allow-multiple-replacements t)
  :config
  ;; modified from https://tecosaur.github.io/emacs-config/config.html#which-key
  (pushnew!
   which-key-replacement-alist
   '(("" . "\\`+?evil[-:]?\\(?:a-\\)?\\(.*\\)") . (nil . "◂\\1"))
   '(("\\`g s" . "\\`evilem--?motion-\\(.*\\)") . (nil . "◃\\1"))
   '(("" . "\\`+?magit[-:]?\\(?:a-\\)?\\(.*\\)") . (nil . "\\1"))))
#+end_src
*** macOS
Some macOS specific settings.

- For thinner fonts on macOS run ~defaults write org.gnu.Emacs AppleFontSmoothing -int 0~
- To revert run ~defaults delete org.gnu.Emacs AppleFontSmoothing~
#+begin_src emacs-lisp
(when (featurep :system 'macos)
  (setq mac-command-modifier 'meta
        mac-option-modifier 'none
        mac-right-option-modifier 'super
        ns-use-proxy-icon nil ; disable file icon in titlebar
        ns-use-native-fullscreen t)

  (display-battery-mode 1))
#+end_src

Hide those pesky =.DS_Store= files.
#+begin_src emacs-lisp
(add-to-list 'completion-ignored-extensions ".DS_Store")
#+end_src
** Visual Enchancements
*** Theme & Fonts
Use the gruvbox theme and set some fonts.
The fonts need to be installed on your machine and make sure to run ~M-x nerd-icons-install-fonts~.
#+ATTR_ORG: :width 500
[[https://raw.githubusercontent.com/michaelneuper/doom/main/images/gruvbox-colorscheme.png]]

#+begin_src emacs-lisp
(setq doom-theme 'doom-gruvbox
      doom-gruvbox-dark-variant "medium"
      doom-gruvbox-padded-modeline t)

(if (featurep :system 'macos)
    (progn
      (defconst my/serif-font "New York")
      (defconst my/sans-serif-font "SF Pro")
      (defconst my/mono-font "SF Mono"))
  (progn
    (defconst my/serif-font "Latin Modern Roman")
    (defconst my/sans-serif-font "Inter")
    (defconst my/mono-font "JetBrainsMono Nerd Font")))

(setq doom-font (font-spec :family my/mono-font :size 14.5)
      doom-variable-pitch-font (font-spec :family my/serif-font)
      doom-emoji-font (font-spec :family "Apple Color Emoji"))
#+end_src

Add some missing file icons to =nerd-fonts=.
#+begin_src emacs-lisp
(after! nerd-icons
  (dolist (entry
           '(("s"     nerd-icons-sucicon "nf-seti-asm"         :face nerd-icons-blue)
             ("o"     nerd-icons-codicon "nf-cod-file_binary"  :face nerd-icons-orange)
             ("class" nerd-icons-codicon "nf-cod-file_binary"  :face nerd-icons-orange)
             ("vim"   nerd-icons-devicon "nf-dev-vim"          :face default)
             ("ms"    nerd-icons-mdicon  "nf-md-file_document" :face nerd-icons-silver)
             ("1"     nerd-icons-mdicon  "nf-md-file_document" :face nerd-icons-silver)
             ("ps"    nerd-icons-mdicon  "nf-md-vector_curve"  :face nerd-icons-dred)
             ("typ"   nerd-icons-flicon  "nf-linux-typst"      :face nerd-icons-dblue)
             ("c3"    nerd-icons-codicon "nf-cod-file_code"    :face nerd-icons-blue)))
    (add-to-list 'nerd-icons-extension-icon-alist entry)))
#+end_src

Make escape characters like =\n= orange like in neovim.
#+begin_src emacs-lisp
(custom-theme-set-faces! 'doom-gruvbox
  `(font-lock-escape-face :foreground ,(doom-color 'orange)))

(custom-set-faces!
  `(font-lock-constant-face :weight bold)
  `(font-lock-doc-face
    :foreground ,(face-attribute 'font-lock-comment-face :foreground)))

(after! hexl-mode
  (custom-set-faces!
    `(hexl-address-region :font ,my/mono-font)
    `(hexl-ascii-region
      :background ,(doom-color 'bg-alt)
      :foreground ,(doom-color 'fg-alt))))
#+end_src
*** Lines
Enable relative line numbers and make their slant normal (not italic).
#+begin_src emacs-lisp
(setopt display-line-numbers-type 'visual
        display-line-numbers-width 4 ;; fixes weird horizontal jumping in long org files
        display-line-numbers-width-start t
        display-line-numbers-grow-only t)

(custom-set-faces!
  '((line-number line-number-current-line)
    :slant normal)
  `(line-number-current-line
    :background ,(face-attribute 'line-number :background)
    :foreground ,(doom-color 'fg)))
#+end_src

Only highlight the current line in programming modes and =dired=.
#+begin_src emacs-lisp
(setq global-hl-line-modes '(prog-mode dired-mode)) ;; FIXME not showing in dired
#+end_src

Give highlighted lines a slightly blue background.
#+begin_src emacs-lisp
(custom-set-faces!
  `(region
    :background ,(doom-blend (doom-color 'blue) (doom-color 'bg) 0.2)))
#+end_src
*** Window
Start emacs in a 100x40 window.
#+begin_src emacs-lisp
(add-to-list 'default-frame-alist '(width . 100))
(add-to-list 'default-frame-alist '(height . 40))
#+end_src

Format the title.
#+begin_src emacs-lisp
;; copied from https://hieuphay.com/doom-emacs-config/#some-good-defaults
;; (setq frame-title-format
;;       '(""
;;         (:eval
;;          (if (s-contains-p org-roam-directory (or buffer-file-name ""))
;;              (replace-regexp-in-string
;;               ".*/[0-9]*-?" "☰ "
;;               (subst-char-in-string ?_ ?  buffer-file-name))
;;            "%b"))
;;         (:eval
;;          (let ((project-name (projectile-project-name)))
;;            (unless (string= "-" project-name)
;;              (format (if (buffer-modified-p)  " ◉ %s" " ● %s") project-name))))))

;; (setq frame-title-format '("" (:eval buffer-name) "%b"))
(setq frame-title-format "GNU Emacs")
#+end_src

* Doom Modules
** :completion
*** corfu
Disable corfu's auto-popups and preselect the first candidate.
#+begin_src emacs-lisp
(after! corfu
  (setq corfu-auto nil
        corfu-preselect 'first
        corfu-preview-current nil
        +corfu-want-tab-prefer-expand-snippets t))
#+end_src
** :ui
*** deft
Quickly search and filter through [[+roam]] notes.
#+begin_src emacs-lisp
(use-package! deft
  :commands deft
  :config
  (setq deft-recursive t
        deft-use-filter-string-for-filename t
        deft-default-extension "org"
        deft-directory org-roam-directory)

  (map! :leader :desc "Search with deft" "r s" #'deft))
#+end_src
*** dashboard
#+ATTR_ORG: :width 350
[[https://raw.githubusercontent.com/michaelneuper/doom/main/images/dashboard.png]]

Remove the shortmenu from the dashboard and loaded text.
#+begin_src emacs-lisp
(remove-hook! '+dashboard-functions
  #'+dashboard-widget-shortmenu
  #'+dashboard-widget-loaded) ;; "Doom loaded x packages" text
#+end_src

Add a random custom message along with the emacs icon in the footer like the [[https://github.com/emacs-dashboard/emacs-dashboard][dashboard]] package.
#+begin_src emacs-lisp
;; copied from https://github.com/emacs-dashboard/emacs-dashboard/blob/master/dashboard-widgets.el
(defcustom my/dashboard-footer-messages
  '("The one true editor, Emacs!"
    "Who the hell uses VIM anyway? Go Evil!"
    "Free as free speech, free as free Beer"
    "Happy coding!"
    "Vi Vi Vi, the editor of the beast"
    "Welcome to the church of Emacs"
    "While any text editor can save your files, only Emacs can save your soul"
    "I showed you my source code, pls respond")
  "A list of messages, one of which dashboard chooses to display.")

(defun my/dashboard-widget-footer ()
  "Draw a random custom dashboard footer message."
  (let* ((icon (propertize
                #(" " 0 1 (display (height 1.5)))
                'face `(:foreground ,(doom-color 'magenta))))
         (msg  (nth (random (length my/dashboard-footer-messages))
                    my/dashboard-footer-messages))
         (line (concat icon msg)))
    (+dashboard-insert
     (propertize " " 'display '(space . (:relative-height 2.0)))
     line
     (propertize " " 'display '(space . (:relative-height 0.75))))))

(setq +dashboard-functions
      (let (result inserted)
        (dolist (fn +dashboard-functions)
          (unless (memq fn '(+dashboard-widget-shortmenu
                             +dashboard-widget-footer
                             my/dashboard-widget-footer))
            (when (and (not inserted)
                       (eq fn #'+dashboard-widget-loaded))
              (push #'my/dashboard-widget-footer result)
              (setq inserted t))
            (push fn result)))
        (unless inserted
          (push #'my/dashboard-widget-footer result))
        (nreverse result)))
#+end_src

Disable [[modeline]] and cursor on the dashboard.
#+begin_src emacs-lisp
(add-hook! '+dashboard-mode-hook
  (when (fboundp 'doom-modeline-auto-set-modeline)
    (doom-modeline-auto-set-modeline))
  (mode-line-invisible-mode +1)
  (setq-local evil-normal-state-cursor '(nil)))

(after! doom-modeline
  (setq doom-modeline-mode-alist
        (assq-delete-all 'dashboard-mode
          (assq-delete-all '+dashboard-mode doom-modeline-mode-alist))))
#+end_src

Set a custom ascii banner for when emacs is used in the terminal, and a splash image for GUI mode.
#+begin_src emacs-lisp
;; modified from https://discourse.doomemacs.org/t/how-to-change-your-splash-screen/57
(defun my-weebery-is-always-greater ()
  (let* ((banner '("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀"
                   "⢸⠉⣹⠋⠉⢉⡟⢩⢋⠋⣽⡻⠭⢽⢉⠯⠭⠭⠭⢽⡍⢹⡍⠙⣯⠉⠉⠉⠉⠉⣿⢫⠉⠉⠉⢉⡟⠉⢿⢹⠉⢉⣉⢿⡝⡉⢩⢿⣻⢍⠉⠉⠩⢹⣟⡏⠉⠹⡉⢻⡍⡇"
                   "⢸⢠⢹⠀⠀⢸⠁⣼⠀⣼⡝⠀⠀⢸⠘⠀⠀⠀⠀⠈⢿⠀⡟⡄⠹⣣⠀⠀⠐⠀⢸⡘⡄⣤⠀⡼⠁⠀⢺⡘⠉⠀⠀⠀⠫⣪⣌⡌⢳⡻⣦⠀⠀⢃⡽⡼⡀⠀⢣⢸⠸⡇"
                   "⢸⡸⢸⠀⠀⣿⠀⣇⢠⡿⠀⠀⠀⠸⡇⠀⠀⠀⠀⠀⠘⢇⠸⠘⡀⠻⣇⠀⠀⠄⠀⡇⢣⢛⠀⡇⠀⠀⣸⠇⠀⠀⠀⠀⠀⠘⠄⢻⡀⠻⣻⣧⠀⠀⠃⢧⡇⠀⢸⢸⡇⡇"
                   "⢸⡇⢸⣠⠀⣿⢠⣿⡾⠁⠀⢀⡀⠤⢇⣀⣐⣀⠀⠤⢀⠈⠢⡡⡈⢦⡙⣷⡀⠀⠀⢿⠈⢻⣡⠁⠀⢀⠏⠀⠀⠀⢀⠀⠄⣀⣐⣀⣙⠢⡌⣻⣷⡀⢹⢸⡅⠀⢸⠸⡇⡇"
                   "⢸⡇⢸⣟⠀⢿⢸⡿⠀⣀⣶⣷⣾⡿⠿⣿⣿⣿⣿⣿⣶⣬⡀⠐⠰⣄⠙⠪⣻⣦⡀⠘⣧⠀⠙⠄⠀⠀⠀⠀⠀⣨⣴⣾⣿⠿⣿⣿⣿⣿⣿⣶⣯⣿⣼⢼⡇⠀⢸⡇⡇⠇"
                   "⢸⢧⠀⣿⡅⢸⣼⡷⣾⣿⡟⠋⣿⠓⢲⣿⣿⣿⡟⠙⣿⠛⢯⡳⡀⠈⠓⠄⡈⠚⠿⣧⣌⢧⠀⠀⠀⠀⠀⣠⣺⠟⢫⡿⠓⢺⣿⣿⣿⠏⠙⣏⠛⣿⣿⣾⡇⢀⡿⢠⠀⡇"
                   "⢸⢸⠀⢹⣷⡀⢿⡁⠀⠻⣇⠀⣇⠀⠘⣿⣿⡿⠁⠐⣉⡀⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠉⠓⠳⠄⠀⠀⠀⠀⠋⠀⠘⡇⠀⠸⣿⣿⠟⠀⢈⣉⢠⡿⠁⣼⠁⣼⠃⣼⠀⡇"
                   "⢸⠸⣀⠈⣯⢳⡘⣇⠀⠀⠈⡂⣜⣆⡀⠀⠀⢀⣀⡴⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢽⣆⣀⠀⠀⠀⣀⣜⠕⡊⠀⣸⠇⣼⡟⢠⠏⠀⡇"
                   "⢸⠀⡟⠀⢸⡆⢹⡜⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠋⣾⡏⡇⡎⡇⠀⡇"
                   "⢸⠀⢃⡆⠀⢿⡄⠑⢽⣄⠀⠀⠀⢀⠂⠠⢁⠈⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠄⡐⢀⠂⠀⠀⣠⣮⡟⢹⣯⣸⣱⠁⠀⡇"
                   "⠈⠉⠉⠉⠉⠉⠉⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠉⠉⠉⠉⠉⠁")))
    (propertize (mapconcat #'identity banner "\n")
                'face '+dashboard-banner)))

(setq +dashboard-ascii-banner-fn #'my-weebery-is-always-greater
      fancy-splash-image (file-name-concat doom-private-dir "images/logo.svg"))
#+end_src
*** indent-guides
Disable =indent-bars= by default.
#+begin_src emacs-lisp
(add-hook '+indent-guides-inhibit-functions
          (lambda () t))
#+end_src
*** modeline
Make the modeline look a bit nicer.
[[https://raw.githubusercontent.com/michaelneuper/doom/main/images/modeline.png]]

#+begin_src emacs-lisp
(remove-hook 'doom-modeline-mode-hook #'size-indication-mode)

(after! doom-modeline
  ;; see https://github.com/seagle0128/doom-modeline?tab=readme-ov-file#customize
  (setq doom-modeline-buffer-file-name-style 'auto
        doom-modeline-always-show-macro-register t
        doom-modeline-enable-word-count nil
        doom-modeline-buffer-encoding t
        doom-modeline-major-mode-icon t
        doom-modeline-buffer-modification-icon nil
        doom-modeline-bar-width 0
        doom-modeline-height 30
        doom-modeline-modal nil
        doom-modeline-spc-face-overrides `(:family ,my/mono-font))

  ;; see https://discourse.doomemacs.org/t/how-to-switch-customize-or-write-themes/37#tweak-the-current-theme-3
  (custom-set-faces!
    `(doom-modeline-buffer-file
      :foreground ,(doom-color 'fg-alt)
      :family ,my/sans-serif-font)
    `(mode-line
      :background ,(doom-color 'bg)
      :foreground ,(doom-color 'fg-alt)
      :overline ,(doom-color 'base4)
      :family ,my/sans-serif-font)
    `(mode-line-inactive
      :overline ,(doom-color 'base4)
      :family ,my/sans-serif-font
      :box nil)
    `(mode-line-active
      :foreground ,(doom-color 'fg-alt)
      :box nil)
    '(header-line
      :overline nil))

  ;; make modeline look like vim in terminal
  (unless (display-graphic-p)
    (setq doom-modeline-icon nil)

    (custom-set-faces!
    `(mode-line
      :background ,(doom-color 'bg-alt2)
      :foreground ,(doom-color 'fg))
    '(doom-modeline-buffer-file
      :weight normal)))

  (map! :leader :desc "Mode line" "t m" #'mode-line-invisible-mode))
#+end_src

We expect the file encoding to be =LF UTF-8= so, only display it in modeline when it's something else.
#+begin_src emacs-lisp
;; copied from https://tecosaur.github.io/emacs-config/config.html#file-encoding
(defun doom-modeline-conditional-buffer-encoding ()
  "We expect the encoding to be LF UTF-8, so only show the modeline when this is not the case"
  (setq-local doom-modeline-buffer-encoding
              (unless (and (memq (plist-get (coding-system-plist buffer-file-coding-system) :category)
                                 '(coding-category-undecided coding-category-utf-8))
                           (not (memq (coding-system-eol-type buffer-file-coding-system) '(1 2))))
                t)))

(add-hook 'after-change-major-mode-hook #'doom-modeline-conditional-buffer-encoding)
#+end_src

Change the filename showed in the modline in an =org-roam= buffer to be =(<date>) <name>.org=.
Only relevant if you have [[+roam]].
#+begin_src emacs-lisp :tangle no
;; TODO show the name of the note instead of the name of the file
;; modified from https://tecosaur.github.io/emacs-config/config.html#modeline-file-name
;; (defadvice! doom-modeline--buffer-file-name-roam-aware-a (orig-fun)
;;   :around #'doom-modeline-buffer-file-name ; takes no args
;;   (if (string-match-p (regexp-quote org-roam-directory) (or buffer-file-name ""))
;;       (replace-regexp-in-string
;;        "\\(?:^\\|.*/\\)\\([0-9]\\{4\\}\\)\\([0-9]\\{2\\}\\)\\([0-9]\\{2\\}\\)[0-9]*-"
;;        "(\\1-\\2-\\3) "
;;        (subst-char-in-string ?_ ?  buffer-file-name))
;;     (funcall orig-fun)))
#+end_src

*** popup
#+begin_src emacs-lisp
(set-popup-rules!
  '(("^\\*compilation" :size 0.35 :select t)))
#+end_src
*** tabs
Disable tabs by default and make them toggleable.
#+begin_src emacs-lisp
(use-package! centaur-tabs
  :commands (centaur-tabs-mode
             centaur-tabs-local-mode
             centaur-tabs-forward
             centaur-tabs-backward)
  :init
  (remove-hook 'doom-first-file-hook #'centaur-tabs-mode)
  (when (daemonp)
    (remove-hook 'server-after-make-frame-hook #'centaur-tabs-mode))

  (map! :leader
        :prefix "t"
        :desc "Tabs"       "t" #'centaur-tabs-mode
        :desc "Local tabs" "T" #'centaur-tabs-local-mode)
  :config
  (centaur-tabs-change-fonts my/sans-serif-font 140)
  :bind
  (:map evil-normal-state-map
        ("g t" . centaur-tabs-forward)
        ("g T" . centaur-tabs-backward)))
#+end_src
*** unicode
#+begin_src emacs-lisp
(after! unicode
  (if (featurep :system 'macos)
      (setq doom-symbol-font (font-spec :family "Symbola"))))
#+end_src
*** zen
Use =doom-variable-pitch-font= in the following modes:
- =org-mode=
- =org-roam-mode= (only relevant if you have [[+roam]])
- =doom-docs-org-mode=
#+begin_src emacs-lisp
;; modified from https://discourse.doomemacs.org/t/cant-size-doom-variable-pitch-font/4572/2
(use-package! mixed-pitch
  :hook ((org-mode           . mixed-pitch-mode)
         (org-roam-mode      . mixed-pitch-mode)
         (doom-docs-org-mode . mixed-pitch-mode))
  :config
  (setq mixed-pitch-set-height t
        variable-pitch-serif-font doom-variable-pitch-font)

  (pushnew! mixed-pitch-fixed-pitch-faces
            'warning
            'org-drawer 'org-cite-key 'org-list-dt 'org-hide
            'corfu-default 'font-latex-math-face)

  (set-face-attribute 'variable-pitch nil :height 1.1)

  (map! :leader :desc "Mixed pitch" "t M" #'mixed-pitch-mode))
#+end_src

Reduce text indent in =org= buffers caused by =mixed-pitch-mode=.
#+begin_src emacs-lisp
;; copied from https://tecosaur.github.io/emacs-config/config.html#reduced-text-indent
(defadvice! +org-indent--reduced-text-prefixes ()
  :after #'org-indent--compute-prefixes
  (setq org-indent--text-line-prefixes
        (make-vector org-indent--deepest-level nil))
  (when (> org-indent-indentation-per-level 0)
    (dotimes (n org-indent--deepest-level)
      (aset org-indent--text-line-prefixes
            n
            (org-add-props
                (concat (make-string (* n (1- org-indent-indentation-per-level))
                                     ?\s)
                        (if (> n 0)
                             (char-to-string org-indent-boundary-char)
                          "\u200b"))
                nil 'face 'org-indent)))))
#+end_src
*** workspaces
#+begin_src emacs-lisp
;; TODO use unicode symbols for [1], [2], ...
(after! persp-mode
  (custom-set-faces!
    `(+workspace-tab-face
      :family ,my/sans-serif-font)
    `(+workspace-tab-selected-face
      :box (:color ,(doom-color 'fg-alt)
            :line-width 1
            :style nil)
      :background ,(doom-color 'fg-alt)
      :foreground ,(doom-color 'bg-alt)
      :family ,my/sans-serif-font
      :weight bold)))
#+end_src
*** other
**** solaire-mode
This is a package built in to doom to distinguish between code buffers and popup buffers
by giving popups a darker background.
I'm not a fan, so I disable it.
#+begin_src emacs-lisp :tangle packages.el
(package! solaire-mode :disable t)
#+end_src
**** spacious-padding
Add padding to emacs frames and windows with [[https://github.com/protesilaos/spacious-padding][spacious-padding]].
#+begin_src emacs-lisp :tangle packages.el
(package! spacious-padding)
#+end_src

#+begin_src emacs-lisp
;; https://protesilaos.com/emacs/spacious-padding
(use-package! spacious-padding
  :ensure t
  :config
  (setq spacious-padding-widths
        '(:internal-border-width 15
          :header-line-width 5
          :mode-line-width 10
          :tab-width 5 ;; ?
          :right-divider-width 5
          :fringe-width 5))

  (when (display-graphic-p)
    (spacious-padding-mode 1)))
#+end_src
**** rainbow-delimiters
#+begin_src emacs-lisp :tangle packages.el
(package! rainbow-delimiters)
#+end_src

Make rainbow delimiters toggleable.
#+begin_src emacs-lisp
(add-hook! (prog-mode conf-mode)
           #'rainbow-delimiters-mode)

;; https://github.com/orgs/doomemacs/projects/5/views/1?pane=issue&itemId=117610828
(map! :leader :desc "Rainbow delimiters" "t (" #'rainbow-delimiters-mode)
#+end_src
**** breadcrumb
Enable breadcrumbs.
#+begin_src emacs-lisp :tangle packages.el
(package! breadcrumb)
#+end_src

#+begin_src emacs-lisp
(use-package! breadcrumb
  :commands (breadcrumb-local-mode breadcrumb-mode)
  :init
  (map! :leader :desc "Breadcrumbs" "t ." #'breadcrumb-local-mode)
  :config
  (setq breadcrumb-imenu-crumb-separator " › ")

  (custom-set-faces!
    `((breadcrumb-face breadcrumb-project-leaf-face)
      :family ,my/mono-font)
    `((breadcrumb-project-base-face
       breadcrumb-project-leaf-face
       breadcrumb-project-crumbs-face)
      :foreground ,(face-attribute 'font-lock-comment-face :foreground)
      :family ,my/mono-font)))
#+end_src
#+begin_src emacs-lisp :tangle packages.el
#+end_src
** :editor
*** evil
Set some =evil= variables and define some keybindings.
#+begin_src emacs-lisp
(after! evil
  (setq evil-move-cursor-back nil     ; don't move the block cursor when toggling insert mode
        evil-kill-on-visual-paste nil ; don't put overwritten text in the kill ring
        evil-respect-visual-line-mode t
        evil-want-fine-undo t
        evil-move-beyond-eol t
        +evil-want-o/O-to-continue-comments nil)

  (map! :nv "~" #'repeat
        :prefix "g"
        :nv "y" #'evilnc-copy-and-comment-operator
        :nv "Y" #'+evil:yank-unindented))
#+end_src

Make the cursor blue, like in zed.
#+begin_src emacs-lisp
(defun my/set-evil-cursors-h ()
  (let ((curs-color (doom-color 'blue)))
    (setq evil-normal-state-cursor   `(box ,curs-color)
          evil-insert-state-cursor   `(bar ,curs-color)
          evil-visual-state-cursor   `(hollow ,curs-color)
          evil-replace-state-cursor  `(hbar ,curs-color)
          evil-operator-state-cursor `(evil-half-cursor ,curs-color)))

  (when (fboundp 'evil-refresh-cursor)
    (evil-refresh-cursor)))

(add-hook! '(doom-load-theme-hook doom-after-modules-config-hook)
           #'my/set-evil-cursors-h)
#+end_src

*** format
#+begin_src emacs-lisp
(map! :map apheleia-mode-map
      :leader
      "fs" #'+format/save-buffer
      "bs" #'+format/save-buffer-no-reformat)
#+end_src
*** lispy
Add a toggle for =lispy-mode=.
#+begin_src emacs-lisp
(defun my/toggle-lispy ()
  "Toggle lispy-mode: enable if off, disable lispy & lispyville if on. Echo the new state in the minibuffer."
  (interactive)
  (if lispy-mode
      (progn
        (lispy-mode -1)
        (when (bound-and-true-p lispyville-mode)
          (lispyville-mode -1))
        (message "Lispy mode disabled"))
    (lispy-mode 1)
    (message "Lispy mode enabled")))

(map! :leader :desc "Lispy" "t L" #'my/toggle-lispy)
#+end_src
*** whitespace
#+begin_src emacs-lisp
(setq +whitespace-guess-in-projects t)
#+end_src

This package shows whitespace characters like tabs and spaces in the selected region.
[[https://raw.githubusercontent.com/michaelneuper/doom/main/images/whitespace.png]]

#+begin_src emacs-lisp :tangle packages.el
(package! whitespace4r :recipe
  (:host github
   :repo "twlz0ne/whitespace4r.el"
   :files ("whitespace4r.el")))
#+end_src

#+begin_src emacs-lisp
(use-package! whitespace4r
  :hook (prog-mode . whitespace4r-mode)
  :config
  (custom-set-faces!
    `((whitespace4r-tab whitespace4r-space whitespace4r-hspace)
      :background ,(face-attribute 'region :background)
      :foreground ,(face-attribute 'font-lock-comment-face :foreground))))
#+end_src
** :emacs
*** dired
Define a custom function to quickly find files in favourite directories.
#+begin_src emacs-lisp
(defvar my/favorite-dirs
  '(("h" "~/" "Home")
    ("c" "~/Developer/" "Code")
    ("d" "~/Downloads/" "Downloads")
    ("g" "~/GitHub/" "GitHub")
    ("t" "~/.Trash/" "Trash")
    ("o" "~/OneDrive - Stellenbosch University/" "OneDrive")
    ("u" "/scpx:ubuntu-vm:" "Ubuntu VM")
    ("n" "/scpx:narga-general:" "NARGA"))
  "List of favorite directories in the form (KEY PATH LABEL).")

(defun my/open-favorite-dir ()
  "Prompt with a single key to jump to a favourite directory."
  (interactive)
  (let* ((max-label-width
          (apply #'max
                 (mapcar (lambda (entry)
                           (string-width (nth 2 entry)))
                         my/favorite-dirs)))
         (prompt
          (concat
           (propertize "Select directory:\n" 'face 'bold)
           (mapconcat
            (lambda (entry)
              (let* ((key (car entry))
                     (label (nth 2 entry))
                     (path (nth 1 entry))
                     ;; pad the label to max width
                     (padded-label (format (format "%%-%ds" max-label-width) label)))
                (format
                 "%s %s  %s"
                 (propertize key 'face 'success)
                 (propertize padded-label 'face 'default)
                 (propertize path 'face 'shadow))))
            my/favorite-dirs
            "\n")
           "\n> "))
         (key (char-to-string
               (read-char-choice
                prompt
                (mapcar #'string-to-char (mapcar #'car my/favorite-dirs)))))
         (dir (cadr (assoc key my/favorite-dirs))))
    (doom-project-browse dir)))

(map! :leader
      :prefix "f"
      :desc "Quick Access" "q" #'my/open-favorite-dir
                           "d" #'consult-dir)
#+end_src

Set =quick-access-entries= for =dirvish=.
Ensure [[https://github.com/alexluigit/dirvish/blob/main/docs/CUSTOMIZING.org#install-dependencies-for-an-enhanced-preview-experience][these]] programs are installed to enable file previews.
#+begin_src emacs-lisp
(setq dired-mouse-drag-files t
      mouse-drag-and-drop-region-cross-program t)

(after! dired-x
  ;; hide "dotfiles"
  (setq dired-omit-files
        (concat dired-omit-files "\\|^\\..*$")))

(after! dirvish
  (setq dirvish-attributes
        (append
         '(vc-state subtree-state nerd-icons collapse)
         '(git-msg file-size)))

  (setopt dirvish-quick-access-entries my/favorite-dirs))

(map! :leader :desc "Dirvish Quick Access" "o q" #'dirvish-quick-access)
#+end_src
*** vc
Configure =blamer= for inline git blame annotations.
Toggle annotations with =SPC t B=, and show detailed commit info in a posframe with =SPC g i=.
Clicking an annotation opens the full diff in Magit.
#+begin_src emacs-lisp :tangle packages.el
(package! blamer)
#+end_src

#+begin_src emacs-lisp
(use-package! blamer
  :commands (blamer-mode
             blamer-show-posframe-commit-info
             blamer-show-commit-info)
  :init
  (map! :leader
        :desc "Git blame"   "t B" #'blamer-mode
        :desc "Commit info" "g i" (lambda () (interactive)
                                    (funcall (if (display-graphic-p)
                                                #'blamer-show-posframe-commit-info
                                              #'blamer-show-commit-info))))
  :config
  (defun blamer-callback-show-commit-diff (commit-info)
    (interactive)
    (let ((commit-hash (plist-get commit-info :commit-hash)))
      (when commit-hash
        (magit-show-commit commit-hash))))

  (setq blamer-author-formatter "  %s"
        blamer-datetime-formatter ", %s"
        blamer-commit-formatter ""
        blamer-avatar-folder (file-name-concat doom-cache-dir "blamer/avatars/")
        blamer-bindings '(("<mouse-1>" . blamer-callback-show-commit-diff))
        blamer-min-offset 10)

  (custom-set-faces!
    `(blamer-face
      :height 0.85
      :foreground ,(face-attribute 'font-lock-comment-face :foreground)
      :slant normal)))
#+end_src
** :term
*** eshell
Customise eshell:
- Set custom aliases
- Set custom prompt
- Add icons to =eshell/ls= and make links clickable
- Add an =fzf=-like =C-r= keybinding
#+begin_src emacs-lisp
(after! eshell
  ;; ALIASES
  (set-eshell-alias!
   "ll" "ls -lAh $*"
   "cd" "z $*"
   "home" "cd ~"
   "v" "emacs $*"

   ;; git
   "gs" "git-st"
   "ga" "git add $1"
   "gb" "git branch $1"
   "gbd" "git branch -D $1"
   "gcb" "git checkout -b $1"
   "gch" "git checkout $1"
   "gc" "git commit -m $1"
   "gp" "git pull"
   "gpu" "git push"

   ;; homebrew
   "bs" "brew search $1"
   "bi" "brew install $*"
   "bic" "brew install --cask $*"
   "bu" "brew update && brew upgrade")

  ;; PROMPT
  (custom-set-faces!
    `(+eshell-prompt-pwd
      :foreground ,(doom-color 'teal)
      :weight bold)
    `(+eshell-prompt-git-branch
      :foreground ,(doom-color 'violet)))

  ;; modified from https://github.com/doomemacs/doomemacs/blob/master/modules/term/eshell/autoload/prompts.el
  (defun my/+eshell--current-git-branch ()
    "Return current Git branch prefixed by remote icon, or \"\" if none.
Formats as: propertized \" at <icon> on  <branch-name>\""
    (let* (;; probe remote URL
           (remote-res    (doom-call-process "git" "ls-remote" "--get-url"))
           (remote-status (car remote-res))
           (remote-url    (cdr remote-res))

           ;; pick remote icon
           (icon
            (cond
             ((and (zerop remote-status)
                   (string-match-p "github"    remote-url)) "")
             ((and (zerop remote-status)
                   (string-match-p "gitlab"    remote-url)) "")
             ((and (zerop remote-status)
                   (string-match-p "bitbucket" remote-url)) "")
             ((and (zerop remote-status)
                   (string-match-p "git"       remote-url)) "")
             (t                                               "")))

           ;; get branch
           (branch
            (cl-destructuring-bind (status . output)
                (doom-call-process "git" "symbolic-ref" "-q" "--short" "HEAD")
              (if (zerop status)
                  output
                (cl-destructuring-bind (s . o)
                    (doom-call-process "git" "describe" "--all" "--always" "HEAD")
                  (if (zerop s) o "")))))

           ;; final formatted output
           (p-branch
            (when (and branch (not (string-empty-p branch)))
              (concat
               (propertize (format " at %s on " icon)
                           'face 'default)
               (propertize (format " %s" branch)
                           'face '+eshell-prompt-git-branch)))))
      (or p-branch "")))

  (defun my/eshell-default-prompt-fn ()
    (concat (if (bobp) "" "\n")
            (let ((pwd (eshell/pwd)))
              (propertize (if (equal pwd "~")
                              pwd (abbreviate-file-name (shrink-path-file pwd)))
                          'face '+eshell-prompt-pwd))
            (my/+eshell--current-git-branch)
            (propertize "\nλ" 'face (if (zerop eshell-last-command-status) 'success 'error))
            " "))

  (setq eshell-prompt-function #'my/eshell-default-prompt-fn)

  ;; ICONS
  ;; modified from https://www.reddit.com/r/emacs/comments/xboh0y/how_to_put_icons_into_eshell_ls/
  (defun lem-eshell-prettify (file)
    "Add features to listings in `eshell/ls' output.
The features are:
1. Add decoration like 'ls -F':
   - Mark directories with a `/'
   - Mark executables with a `*'
2. Make each listing into a clickable link to open the
   corresponding file or directory.
3. Add icons (requires `nerd-icons')
This function is meant to be used as advice around
`eshell-ls-annotate', where FILE is the cons describing the file."
    (let* ((name (car file))
           (icon (if (eq (cadr file) t)
                     (propertize (nerd-icons-icon-for-dir name) 'face 'eshell-ls-directory)
                   (nerd-icons-icon-for-file name)))
           (suffix
            (cond
             ;; Directory
             ((eq (cadr file) t) "/")
             ;; Executable
             ((and (/= (user-uid) 0) ; root can execute anything
                   (eshell-ls-applicable (cdr file) 3 #'file-executable-p (car file)))
              "*"))))
      (cons
       (concat icon " "
               (propertize name
                           'keymap eshell-ls-file-keymap
                           'mouse-face 'highlight
                           'file-name (expand-file-name (substring-no-properties (car file)) default-directory))
               (when (and suffix (not (string-suffix-p suffix name)))
                 (propertize suffix 'face 'shadow)))
       (cdr file))))

  (defun eshell-ls-file-at-point ()
    "Get the full path of the Eshell listing at point."
    (get-text-property (point) 'file-name))

  (defun eshell-ls-find-file ()
    "Open the Eshell listing at point."
    (interactive)
    (find-file (eshell-ls-file-at-point)))

  (defun eshell-ls-delete-file ()
    "Delete the Eshell listing at point."
    (interactive)
    (let ((file (eshell-ls-file-at-point)))
      (when (yes-or-no-p (format "Delete file %s?" file))
        (delete-file file 'trash))))

  (defvar eshell-ls-file-keymap
    (let ((map (make-sparse-keymap)))
      (define-key map (kbd "RET") #'eshell-ls-find-file)
      (define-key map (kbd "<return>") #'eshell-ls-find-file)
      (define-key map [mouse-1] #'eshell-ls-find-file)
      (define-key map (kbd "D") #'eshell-ls-delete-file)
      map)
    "Keys in effect when point is over a file from `eshell/ls'.")

  (advice-add #'eshell-ls-annotate :filter-return #'lem-eshell-prettify)

  (map! :map eshell-mode-map
        :ni "C-r" #'+eshell/search-history))
#+end_src
*** vterm
Some settings in my =.zshrc= cause issues in vterm,
so I use this environment variable to disable them when running inside vterm.
#+begin_src emacs-lisp
(after! vterm
  (setq vterm-environment '("VTERM=1"))

  (map! :map vterm-mode-map
        :ni "C-j" #'vterm-send-down
        :ni "C-k" #'vterm-send-up
        :i  "C-b" #'backward-char
        :i  "C-f" #'forward-char))

(map! :leader
      :prefix "o"
      :desc "Toggle vterm popup" "v" #'+vterm/toggle
      :desc "Open vterm here" "V" #'+vterm/here)
#+end_src
** :tools
*** debugger
#+begin_src emacs-lisp
(after! dape
  (setq dape-breakpoint-margin-string "●") ;; or use •

  (custom-set-faces!
    `(dape-inlay-hint-face :inherit dape-inlay-hint-highlight-face
      :background ,(face-attribute 'hl-line :background))))
#+end_src
*** eval
#+begin_src emacs-lisp
(setq eros-eval-result-prefix "⇒ ") ;; or use ⟹
#+end_src
*** lookup
Prevent those annoying popups from resizing the echo area.
#+begin_src emacs-lisp
(after! eldoc
  (setq eldoc-echo-area-use-multiline-p nil))
#+end_src

*** llm
#+begin_src emacs-lisp
(defvar my/llm-sidebar-width 0.4
  "Width fraction for LLM sidebars.")
#+end_src

**** gptel
Set up =gptel= to use copilot.
#+begin_src emacs-lisp
(after! gptel
  (setq gptel-model 'gpt-4.1
        gptel-backend (gptel-make-gh-copilot "Copilot")
        gptel-default-mode 'org-mode)

  (custom-set-faces!
    `(gptel-context-highlight-face
      :background ,(doom-color 'bg-alt))
    `(gptel-context-deletion-face
      :inherit diff-removed))

  (map! :map gptel-mode-map
        :leader
        :prefix "o l"
        :desc "Remove all context" "R" #'gptel-context-remove-all))
#+end_src

Install some tools from the =llm-tool-collection= package.
#+begin_src emacs-lisp :tangle packages.el
(package! llm-tool-collection
  :recipe (:host github :repo "skissue/llm-tool-collection"))
#+end_src

#+begin_src emacs-lisp
(after! gptel
  ;; see https://github.com/karthink/gptel/wiki/Tools-collection
  (gptel-make-tool
   :function (lambda (command &optional working_dir)
               (with-temp-message (format "Executing command: `%s`" command)
                 (let ((default-directory (if (and working_dir (not (string= working_dir "")))
                                              (expand-file-name working_dir)
                                            default-directory)))
                   (shell-command-to-string command))))
   :name "run_command"
   :description "Executes a shell command and returns the output as a string. IMPORTANT: This tool allows execution of arbitrary code; user confirmation will be required before any command is run."
   :args (list
          '(:name "command"
            :type string
            :description "The complete shell command to execute.")
          '(:name "working_dir"
            :type string
            :description "Optional: The directory in which to run the command. Defaults to the current directory if not specified."))
   :category "command"
   :confirm t
   :include t)

  (mapcar (apply-partially #'apply #'gptel-make-tool)
          (llm-tool-collection-get-all)))
#+end_src
**** agent-shell
#+begin_src emacs-lisp :tangle packages.el
(package! shell-maker)
(package! acp)
(package! agent-shell)
#+end_src

#+begin_src emacs-lisp
(use-package! shell-maker :defer t)

(use-package! agent-shell
  :commands agent-shell
  :after shell-maker
  :config
  (require 'shell-maker)
  (require 'agent-shell)
  :bind (:map agent-shell-mode-map
              ("C-RET" . newline)))
#+end_src

Configure sidebar.
#+begin_src emacs-lisp :tangle packages.el
(package! agent-shell-sidebar :recipe
  (:host github
   :repo "cmacrae/agent-shell-sidebar"))
#+end_src

#+begin_src emacs-lisp
(use-package! agent-shell-sidebar
  :commands agent-shell-sidebar-toggle
  :after agent-shell
  :custom
  (agent-shell-sidebar-width (format "%d%%" (* 100 my/llm-sidebar-width)))
  (agent-shell-sidebar-default-config
   (agent-shell-openai-make-codex-config)))

;; (map! :leader "t a" #'agent-shell-sidebar-toggle)
#+end_src

Add a cute pet.
#+begin_src emacs-lisp :tangle packages.el
(package! agent-shell-pet :recipe
  (:host github
   :repo "lgmoneda/agent-shell-pet"
   :files ("*.el" "pets" "renderers")))
#+end_src

#+begin_src emacs-lisp
(use-package! agent-shell-pet
  :after agent-shell
  :custom
  (agent-shell-pet-id "rick")
  (agent-shell-pet-size 'small)
  :config
  (global-agent-shell-pet-mode 1)

  (if (featurep :system 'macos)
      (setq agent-shell-pet-renderer 'macos-native)))
#+end_src

Some enhancements on mac.
#+begin_src emacs-lisp :tangle packages.el
(package! agent-shell-macext :recipe
  (:host github
   :repo "cxa/agent-shell-macext")
  :ignore (not (featurep :system 'macos)))
#+end_src

#+begin_src emacs-lisp
(use-package! agent-shell-macext
  :if (featurep :system 'macos)
  :hook (agent-shell-mode . agent-shell-macext-setup)
  :custom
    (agent-shell-macext-notifications t))
#+end_src

**** codex-ide
#+begin_src emacs-lisp :tangle packages.el
(package! codex-ide :recipe
  (:host github
   :repo "dgillis/emacs-codex-ide"))
#+end_src

Codex package setup and keybindings.
#+begin_src emacs-lisp
(use-package! codex-ide
  :commands codex-ide-menu
  :hook (codex-ide-session-mode . (lambda () (display-line-numbers-mode -1)))
  :custom
  (codex-ide-enable-emacs-tool-bridge t)
  :init
  (map! :leader
        :prefix "o"
        :desc "Codex menu" "c" #'codex-ide-menu))
#+end_src

Submit Codex prompts with =RET=.
#+begin_src emacs-lisp
(after! codex-ide-session-mode
  (dolist (map (list codex-ide-session-mode-map
                     codex-ide-session-prompt-minor-mode-map))
    (define-key map (kbd "RET") #'codex-ide-submit)
    (define-key map (kbd "<return>") #'codex-ide-submit)))
#+end_src

Jump to the active prompt before inserting text.
#+begin_src emacs-lisp
(defun my/codex--goto-active-prompt ()
  "Move point to the active Codex prompt, if one exists."
  (when (derived-mode-p 'codex-ide-session-mode)
    (when-let* ((session (or (bound-and-true-p codex-ide--session)
                             (and (fboundp 'codex-ide--session-for-current-buffer)
                                  (codex-ide--session-for-current-buffer))))
                (input-end (and (fboundp 'codex-ide-session-mode--input-end-position)
                                (codex-ide-session-mode--input-end-position session))))
      (goto-char input-end))))

(defun my/codex-self-insert (n)
  "Insert text at the active Codex prompt, moving there first if needed."
  (interactive "p")
  (when (and (derived-mode-p 'codex-ide-session-mode)
             (not (codex-ide--point-in-active-prompt-p)))
    (my/codex--goto-active-prompt))
  (self-insert-command n))

(with-eval-after-load 'codex-ide-session-mode
  (define-key codex-ide-session-mode-map [remap self-insert-command]
              #'my/codex-self-insert))
#+end_src

Toggle a workspace-local Codex sidebar.
#+begin_src emacs-lisp
(defun my/codex--current-workspace-key ()
  "Return a workspace-local key for Codex sidebar state."
  (cond
   ((fboundp '+workspace-current-name)
    (+workspace-current-name))
   ((fboundp 'safe-persp-name)
    (safe-persp-name (get-current-persp)))
   ((fboundp 'projectile-project-root)
    (projectile-project-root))
   ((fboundp 'codex-ide--get-working-directory)
    (codex-ide--get-working-directory))
   (t
    default-directory)))

(defun my/codex--sidebar-buffer-name ()
  "Return the Codex sidebar buffer name for the current workspace."
  (format "*doom:codex-popup:%s*" (my/codex--current-workspace-key)))

(defun my/codex--sidebar-session-buffer ()
  "Return a Codex session buffer for the current sidebar workspace."
  (or (get-buffer (my/codex--sidebar-buffer-name))
      (let* ((choice (completing-read
                      "No Codex buffer. Action: "
                      '("continue most recent" "search sessions" "start new")
                      nil t nil nil "continue most recent"))
             (session
              (pcase choice
                ("continue most recent"
                 (codex-ide--start-session 'continue))
                ("search sessions"
                 (let* ((directory (codex-ide--get-working-directory))
                        (query-session
                         (codex-ide--ensure-query-session-for-thread-selection
                          directory))
                        (thread (codex-ide--pick-thread query-session)))
                   (codex-ide--show-or-resume-thread
                    (alist-get 'id thread)
                    directory)))
                (_
                 (codex-ide--start-session 'new))))
             (buffer (codex-ide-session-buffer session)))
        (when (buffer-live-p buffer)
          (with-current-buffer buffer
            (rename-buffer (my/codex--sidebar-buffer-name) t)))
        buffer)))

(defun my/codex--restore-window-buffer (window buffer)
  "Restore WINDOW to BUFFER when Codex session creation reused it."
  (when (and (window-live-p window)
             (buffer-live-p buffer)
             (not (eq (window-buffer window) buffer)))
    (set-window-buffer window buffer)))

(defun my/codex-toggle-sidebar ()
  "Toggle a workspace-local Codex session in a side window."
  (interactive)
  (require 'codex-ide-core)
  (require 'codex-ide-session)
  (let* ((buffer-name (my/codex--sidebar-buffer-name))
         (window (get-buffer-window buffer-name t)))
    (if (window-live-p window)
        (delete-window window)
      (let ((origin-window (selected-window))
            (origin-buffer (current-buffer))
            (codex-ide-select-window-on-open nil)
            (display-buffer-overriding-action
             `((display-buffer-reuse-window
                display-buffer-in-side-window)
               (side . right)
               (slot . 0)
               (window-width . ,my/llm-sidebar-width))))
        (select-window
         (display-buffer-in-side-window
          (my/codex--sidebar-session-buffer)
          `((side . right)
            (slot . 0)
            (window-width . ,my/llm-sidebar-width))))
        (my/codex--restore-window-buffer origin-window origin-buffer)))))

(map! :leader
      :desc "Toggle Codex sidebar" "t a" #'my/codex-toggle-sidebar)
#+end_src

*** lsp +eglot
By default =eglot-code-action-indicator= is set to a lightbulb emoji (💡) which
annoyingly causes the line height to increase when hovering over certain things on macOS,
let's change this to a nerd font light bulb instead.

Also disable inlay hints.
#+begin_src emacs-lisp
(after! eglot
  (setq eglot-code-action-indicator ""
        eglot-ignored-server-capabilities ;; disable some intrusive features
        '(:documentHighlightProvider
          :documentOnTypeFormattingProvider))

  (defadvice! +eglot--no-auto-inlay-hints-a (fn)
    "Prevent eglot from auto-enabling inlay hints on activation."
    :around #'eglot--maybe-activate-editing-mode
    (let ((orig (symbol-function 'eglot-inlay-hints-mode)))
      (cl-letf (((symbol-function 'eglot-inlay-hints-mode)
                 (lambda (n) (when (< n 1) (funcall orig n)))))
        (funcall fn))))

  (custom-set-faces!
    `(eglot-inlay-hint-face ;; FIXME background does not change when selected
      :foreground ,(doom-color 'base5)
      :height 0.9)
    '((eglot-semantic-definition
       eglot-semantic-declaration
       eglot-semantic-static
       eglot-semantic-defaultLibrary)
      :foreground unspecified)
    `(eglot-semantic-operator :foreground ,(doom-color 'cyan))
    `(eglot-semantic-namespace :foreground ,(doom-color 'violet))
    '(eglot-semantic-comment :slant italic)
    '(eglot-semantic-documentation :slant normal)
    '(eglot-semantic-defaultLibrary :weight bold)
    '(eglot-semantic-macro :weight normal))

  (map! :map eglot-mode-map
        :leader
        :desc "Toggle inlay hints" "t h" #'eglot-inlay-hints-mode))
#+end_src
*** magit
**** magit
Add icons to =magit= and make the commit summary bold.
#+begin_src emacs-lisp
;; REVIEW https://github.com/magit/magit/issues/2942
(after! magit
  (setopt magit-format-file-function #'magit-format-file-nerd-icons)

  (setq auth-sources '("~/.authinfo")) ;; FIXME not using apple keychain

  (with-eval-after-load 'magit
    (setq magit-revision-show-gravatars '("^Author:     " . "^Commit:     ")))

  (custom-set-faces!
    '(git-commit-summary :weight bold)))
#+end_src

Highlight conventional commit prefixes in magit log and revision buffers,
with separate faces for types, scopes, and breaking-change markers.
#+begin_src emacs-lisp
;; REVIEW: remove when https://github.com/magit/magit/issues/4027 is in doom's version of magit
(defface my/magit-conventional-commit-default-face
  '((t (:inherit font-lock-comment-face :weight bold)))
  "Default face for conventional commit keywords.")

(defface my/magit-conventional-commit-scope-face
  '((t (:inherit (default my/magit-conventional-commit-default-face)
        :weight bold)))
  "Face for conventional commit scopes.")

(defface my/magit-conventional-commit-feat-face
  '((t (:inherit (success my/magit-conventional-commit-default-face))))
  "Face for `feat' conventional commits.")

(defface my/magit-conventional-commit-fix-face
  '((t (:inherit (error my/magit-conventional-commit-default-face))))
  "Face for `fix' and breaking conventional commits.")

(defface my/magit-conventional-commit-revert-face
  '((t (:inherit (error my/magit-conventional-commit-default-face))))
  "Face for `revert' conventional commits.")

(defface my/magit-conventional-commit-docs-face
  '((t (:inherit my/magit-conventional-commit-default-face)))
  "Face for `docs' conventional commits.")

(defface my/magit-conventional-commit-style-face
  '((t (:inherit my/magit-conventional-commit-default-face)))
  "Face for `style' conventional commits.")

(defface my/magit-conventional-commit-refactor-face
  '((t (:inherit my/magit-conventional-commit-default-face)))
  "Face for `refactor' conventional commits.")

(defface my/magit-conventional-commit-perf-face
  '((t (:inherit my/magit-conventional-commit-default-face)))
  "Face for `perf' conventional commits.")

(defface my/magit-conventional-commit-test-face
  '((t (:inherit my/magit-conventional-commit-default-face)))
  "Face for `test' conventional commits.")

(defface my/magit-conventional-commit-build-face
  '((t (:inherit my/magit-conventional-commit-default-face)))
  "Face for `build' conventional commits.")

(defface my/magit-conventional-commit-ci-face
  '((t (:inherit my/magit-conventional-commit-default-face)))
  "Face for `ci' conventional commits.")

(defface my/magit-conventional-commit-chore-face
  '((t (:inherit my/magit-conventional-commit-default-face)))
  "Face for `chore' conventional commits.")

(defconst my/magit-conventional-commit-keywords
  '(("feat"     . my/magit-conventional-commit-feat-face)
    ("fix"      . my/magit-conventional-commit-fix-face)
    ("docs"     . my/magit-conventional-commit-docs-face)
    ("style"    . my/magit-conventional-commit-style-face)
    ("refactor" . my/magit-conventional-commit-refactor-face)
    ("perf"     . my/magit-conventional-commit-perf-face)
    ("test"     . my/magit-conventional-commit-test-face)
    ("build"    . my/magit-conventional-commit-build-face)
    ("ci"       . my/magit-conventional-commit-ci-face)
    ("chore"    . my/magit-conventional-commit-chore-face)
    ("revert"   . my/magit-conventional-commit-revert-face))
  "Faces for conventional commit types.")

(defun my/magit-conventional-commit-face (type breaking-p)
  "Return the face for TYPE, forcing red when BREAKING-P is non-nil."
  (if breaking-p
      'my/magit-conventional-commit-fix-face
    (or (cdr (assoc-string type my/magit-conventional-commit-keywords t))
        'my/magit-conventional-commit-default-face)))

(defun my/magit-highlight-conventional-commit-keywords ()
  "Highlight conventional commit keywords in magit summary buffers."
  (save-excursion
    (goto-char (point-min))
    (let ((limit (line-end-position)))
      (while (re-search-forward
              "^\\([[:space:]]*\\)\\([[:alpha:]][[:alnum:]-]*\\)\\(?:\\(!\\)\\)?\\(?:\\(([^)]*)\\)\\)?\\(?:\\(!\\)\\)?\\(:\\)"
              limit t)
        (let* ((type-group 2)
               (bang-before-scope-group 3)
               (scope-group 4)
               (bang-after-scope-group 5)
               (colon-group 6)
               (type (match-string-no-properties type-group))
               (breaking-p (or (match-beginning bang-before-scope-group)
                               (match-beginning bang-after-scope-group)))
               (face (my/magit-conventional-commit-face type breaking-p)))
          (put-text-property (match-beginning type-group) (match-end type-group)
                             'font-lock-face face)
          (when (match-beginning bang-before-scope-group)
            (put-text-property (match-beginning bang-before-scope-group)
                               (match-end bang-before-scope-group)
                               'font-lock-face face))
          (when (match-beginning scope-group)
            (put-text-property (match-beginning scope-group) (match-end scope-group)
                               'font-lock-face (if breaking-p
                                                   face
                                                 'my/magit-conventional-commit-scope-face)))
          (when (match-beginning bang-after-scope-group)
            (put-text-property (match-beginning bang-after-scope-group)
                               (match-end bang-after-scope-group)
                               'font-lock-face face))
          (put-text-property (match-beginning colon-group) (match-end colon-group)
                             'font-lock-face 'default))))))

(add-hook! '(magit-log-wash-summary-hook
             magit-revision-wash-message-hook)
           :append #'my/magit-highlight-conventional-commit-keywords)
#+end_src

**** magit-prime
Speed up magit.
#+begin_src emacs-lisp :tangle packages.el
(when (modulep! :tools magit)
  (package! magit-prime))
#+end_src

#+begin_src emacs-lisp
(use-package! magit-prime
  :when (modulep! :tools magit)
  :hook (magit-mode . magit-prime-mode))
#+end_src

**** magit-todos
Display TODOs in magit buffer.
#+begin_src emacs-lisp :tangle packages.el
(when (modulep! :tools magit)
  (package! magit-todos))
#+end_src

Change the suffix regex to make the =:= and the end of the keyword optional.
So this regex should match:
- =TODO bar=
- =TODO: bar=
- =TODO(foo): bar=
- =TODO[foo]: bar=
#+begin_src emacs-lisp
(use-package! magit-todos
  :when (modulep! :tools magit)
  :hook (magit-status-mode . magit-todos-mode)
  :config
  (setq magit-todos-keyword-suffix "\\(?::\\|\\(?:[([][^])]+[])]\\)?\\|:\\)?"))
#+end_src

*** pdf
Automatically enable dark mode in pdfs.
#+begin_src emacs-lisp
;; TODO add continuous scroll
(add-hook 'pdf-view-mode-hook #'pdf-view-midnight-minor-mode)
#+end_src

The default resize factor of =1.25= is  a bit too much.
#+begin_src emacs-lisp
(after! pdf-view
  (setq pdf-view-resize-factor 1.1))
#+end_src

Open pdf links in =org= with =pdf-tools=.
#+begin_src emacs-lisp
(after! org
  (setq org-file-apps
        (append (remove '("\\.pdf\\'" . default) org-file-apps)
                '(("\\.pdf\\'" . emacs)))))
#+end_src

Create some keybindings for commonly used commands.
#+begin_src emacs-lisp
(map! :map pdf-view-mode-map
      :localleader
      (:prefix ("h" . "history")
       :desc "Go forward in history"  "f" #'pdf-history-forward
       :desc "Go backward in history" "b" #'pdf-history-backward)

      (:prefix ("a" . "annotations")
       :desc "Text"      "t" #'pdf-annot-add-text-annotation
       :desc "Markup"    "m" #'pdf-annot-add-markup-annotation
       :desc "Highlight" "h" #'pdf-annot-add-highlight-markup-annotation
       :desc "Strikeout" "s" #'pdf-annot-add-strikeout-markup-annotation))
#+end_src
*** tree-sitter
#+begin_src emacs-lisp
(after! treesit
  (setq treesit-font-lock-level 3
        treesit-auto-install-grammar 'ask))
#+end_src
** :lang
*** cc +lsp +tree-sitter
Configure =cc-mode=.
#+begin_src emacs-lisp
(after! (cc-mode c-ts-mode)
  (setq c-basic-offset 4
        c-doc-comment-style '((c-mode    . doxygen)
                              (c-ts-mode . doxygen))
        whitespace-style '(face trailing lines-tail empty)))

(add-hook! '(c-mode-hook c-ts-mode-hook)
  (unless (or (file-exists-p "makefile")
              (file-exists-p "Makefile"))
    (setq-local compile-command "cc -Wall -o main main.c")))
#+end_src

Fix issue where two new lines are inserted after ={=.
#+begin_src emacs-lisp
(after! eglot
  (add-hook! 'eglot-managed-mode-hook :append
    (defun +eglot--rm-eglot--post-self-insert-hook ()
      ;; remove eglot’s post-self-insert-hook in current buffer
      (remove-hook 'post-self-insert-hook #'eglot--post-self-insert-hook t))))
#+end_src

Enable =yaml-mode= in =.clang-format= files.
#+begin_src emacs-lisp
(add-to-list 'auto-mode-alist (cons "\\.clang-format\\'" 'yaml-mode))
#+end_src

*** java
Set indentation level for =java-mode= to 2.
#+begin_src emacs-lisp
(after! java-mode
  (setq c-basic-offset 2))
#+end_src
*** python +lsp
Requires [[lsp +eglot]] and requires =pyright= to be installed on your machine.
Install with ~npm install -g pyright~.
#+begin_src emacs-lisp
(after! eglot
  (add-to-list 'eglot-server-programs '(python-mode . ("pyright-langserver" "--stdio"))))

(add-hook! 'python-mode-hook
  (setq python-shell-interpreter "python3.11"
        doom-modeline-env-python-executable "python3.11"))
#+end_src
*** rust +lsp
Eglot receives Rust-specific semantic tokens like ~lifetime~, but it only
defines faces for standard LSP token types, so add the missing Rust ones.
#+begin_src emacs-lisp
(after! eglot
  (defface eglot-semantic-lifetime
    `((t (:foreground ,(doom-color 'fg-alt) :slant italic)))
    "Face for Rust lifetime semantic tokens."
    :group 'eglot-semantic-faces)

  (dolist (token '("lifetime" "macro"))
    (add-to-list 'eglot-semantic-token-types token)))
#+end_src
*** latex +cdlatex
Enable auomatic insertion of braces after sub/superscript in =LaTeX-math-mode=.
This used to the default behaviour in doom but was changed in commit [[https://github.com/doomemacs/doomemacs/commit/57fcd95e7dd61fcfc235293d7909e7b008466113][57fcd95]].
#+begin_src emacs-lisp
(setq TeX-electric-sub-and-superscript t)
#+end_src

My CDLaTeX templates.
#+begin_src emacs-lisp
(after! cdlatex
  (setq cdlatex-math-symbol-alist ;; expand when prefixed with `
        ;; TODO change prefix key to something like ;
        '((?' ("`"))
          (?e ("\\varepsilon" "\\epsilon"))
          (?f ("\\varphi" "\\phi"))
          (?0 ("\\varnothing" "\\emptyset"))
          (?> ("\\to" "\\implies"))
          (?= ("\\iff" "\\equiv"))
          (?| ("\\mid" "\\vert"))
          (?: ("\\coloneqq")))
        cdlatex-math-modify-alist ;; modify text with '
        '((?b "\\mathbb" nil t nil nil)
          (?c "\\mathcal" nil t nil nil)
          (?f "\\mathbf" nil t nil nil)
          (?m "\\mathrm" nil t nil nil)
          (?r "\\mathrel" nil t nil nil)
          (?s "\\mathsf" nil t nil nil)
          (?o "\\operatorname" nil t nil nil))
        cdlatex-command-alist ;; expand with <TAB>
        '(("eqn" "Insert an EQUATION* environment template" "" cdlatex-environment ("equation*") t nil)
          ("aln" "Insert an ALIGN* environment template" "" cdlatex-environment ("align*") t nil)
          ("sum" "Insert \\sum\\limits_{}^{}" "\\sum\\limits_{?}^{}" cdlatex-position-cursor nil nil t)
          ("prod" "Insert \\prod\\limits_{}^{}" "\\prod\\limits_{?}^{}" cdlatex-position-cursor nil nil t)
          ("bun" "Insert \\bigcup\\limits_{}^{}" "\\bigcup\\limits_{?}^{}" cdlatex-position-cursor nil nil t)
          ("bin" "Insert \\bigcap\\limits_{}^{}" "\\bigcap\\limits_{?}^{}" cdlatex-position-cursor nil nil t)
          ("lim" "Insert \\lim_\\limits{{} \\to {}}" "\\lim_\\limits{{?} \\to {}}" cdlatex-position-cursor nil nil t)
          ("sr" "Insert {}^2" "{?}^2" cdlatex-position-cursor nil nil t)
          ("cb" "Insert {}^3" "{?}^3" cdlatex-position-cursor nil nil t)
          ("op" "Insert \\operatorname{}()" "\\operatorname{?}()" cdlatex-position-cursor nil nil t)))

  (map! :map cdlatex-mode-map
        :i "TAB" #'cdlatex-tab))
#+end_src
*** org
Use tecosaur's fork of =org-mode=, this is necessary for [[#org-latex][org-latex]].
#+begin_src emacs-lisp :tangle packages.el
(package! org :recipe
  (:host nil
   :repo "https://git.tecosaur.net/mirrors/org-mode.git"
   :remote "mirror"
   :fork (:host nil
          :repo "https://git.tecosaur.net/tec/org-mode.git"
          :branch "dev"
          :remote "tecosaur")
   :files (:defaults "etc")
   :build t
   :pre-build
   (with-temp-file "org-version.el"
     (require 'lisp-mnt)
     (let ((version (with-temp-buffer
                      (insert-file-contents "lisp/org.el")
                      (lm-header "version")))
           (git-version
            (string-trim
             (with-temp-buffer
               (call-process "git" nil t nil "rev-parse" "--short" "HEAD")
               (buffer-string)))))
       (insert
        (format "(defun org-release () \"The release version of Org.\" %S)\n" version)
        (format "(defun org-git-version () \"The truncate git commit hash of Org mode.\" %S)\n" git-version)
        "(provide 'org-version)\n"))))
  :pin nil)

(unpin! org)
#+end_src
**** org-mode
Set some =org= variables.
#+begin_src emacs-lisp
(setq org-directory "~/Documents/Org"
      org-use-property-inheritance t ; fix weird issue with src blocks
      org-startup-with-inline-images t
      ;; org-display-remote-inline-images 'cache
      org-edit-src-content-indentation 0)

;; FIXME blurry previews when using #+attr_org: :width
;; this only seems to be an issue when using emacs-plus (as opposed to emacs-mac)
;; (setq org-image-actual-width (/ (display-pixel-width) 3)) ; set images to a third of the width of the screen
      ;; org-image-actual-width (min (/ (display-pixel-width) 3) 800)
#+end_src

Scale headings in =org-mode=.
#+begin_src emacs-lisp
(after! org
  (custom-set-faces!
    `(org-document-title :foreground ,(doom-color 'fg) :height 1.3 :weight bold)
    `(org-level-1 :inherit 'outline-1 :weight medium   :height 1.1)
    `(org-level-2 :inherit 'outline-2 :weight medium)
    `(org-level-3 :inherit 'outline-3 :weight medium)
    `(org-level-4 :inherit 'outline-4 :weight medium)
    `(org-level-5 :inherit 'outline-5 :weight medium)))
#+end_src

By default, all =org= links are the same colour, lets change that.
#+begin_src emacs-lisp
(defconst my/org-link-colors
  '(("http"  . blue)
    ("https" . blue)
    ("file"  . orange)
    ("pdf"   . orange)
    ("id"    . violet))
  "Alist of Org link prefixes and their Doom theme colours.")

(defun my/org-apply-link-colors ()
  "Register custom faces for Org link schemes based on `my/org-link-colors'."
  (dolist (entry my/org-link-colors)
    (let ((scheme (car entry))
          (color  (cdr entry)))
      (org-link-set-parameters
       scheme
       :face       `(:foreground ,(doom-color color))
       :mouse-face `(:foreground ,(doom-color 'bg)
                     :background ,(doom-color color))))))

(after! org
  (my/org-apply-link-colors)

  (custom-set-faces!
    `((org-link)
      :weight normal
      :underline nil)))
#+end_src

Open plain URLs inside source blocks instead of executing the block.
#+begin_src emacs-lisp
(defvar my/org-src-url-click-opened-until nil
  "Time until which source block execution is suppressed after opening a URL.")

(defun my/url-at-point-or-position (&optional pos)
  "Return a plain URL at POS or point.
This scans the current line explicitly, which is more reliable inside
fontified Org source blocks than `thing-at-point'."
  (let ((pos (or pos (point)))
        found)
    (save-excursion
      (goto-char pos)
      (save-restriction
        (narrow-to-region (line-beginning-position) (line-end-position))
        (goto-char (point-min))
        (while (and (not found)
                    (re-search-forward "\\_<https?://[^][(){}<>'\"[:space:]]+" nil t))
          (when (<= (match-beginning 0) pos (match-end 0))
            (setq found (match-string-no-properties 0))))))
    found))

(defadvice! my/org-babel-open-url-at-point-a (fn &rest args)
  "Open plain URLs in Org source blocks instead of executing the block."
  :around #'org-babel-execute-src-block
  (cond
   ((and my/org-src-url-click-opened-until
         (time-less-p nil my/org-src-url-click-opened-until))
    nil)
   ((if-let* ((url (my/url-at-point-or-position))
              ((memq (org-element-type (org-element-context))
                     '(src-block inline-src-block))))
        (progn
          (browse-url url)
          t)))
   (t
    (apply fn args))))

(defun my/org-mouse-open-url-in-src-block-maybe (event)
  "Open a clicked plain URL in an Org source block.
Return non-nil when EVENT was handled."
  (when (derived-mode-p 'org-mode)
    (let ((pos (posn-point (event-start event))))
      (when (integer-or-marker-p pos)
        (save-excursion
          (goto-char pos)
          (when-let* ((url (my/url-at-point-or-position pos))
                      ((memq (org-element-type (org-element-context))
                             '(src-block inline-src-block))))
            (browse-url url)
            (setq my/org-src-url-click-opened-until
                  (time-add nil 0.5))
            t))))))

(defadvice! my/org-mouse-drag-region-open-url-a (fn event &rest args)
  "Open plain URLs in Org source blocks before mouse drag selection."
  :around #'mouse-drag-region
  (unless (my/org-mouse-open-url-in-src-block-maybe event)
    (apply fn event args)))

(after! evil
  (defadvice! my/org-evil-mouse-drag-region-open-url-a (fn event &rest args)
    "Open plain URLs in Org source blocks before Evil mouse drag selection."
    :around #'evil-mouse-drag-region
    (unless (my/org-mouse-open-url-in-src-block-maybe event)
      (apply fn event args))))
#+end_src

I incorrectly tagged a bunch of org files, so this function fixes that.
#+begin_src emacs-lisp
(defun my/org-fix-filetags ()
  "Rewrite every `#+filetags:' directive in the current buffer so it uses the
canonical colon syntax, e.g.
  #+filetags: tag1 tag2  ->  #+filetags: :tag1:tag2:"
  (interactive)
  (save-excursion
    (goto-char (point-min))
    (while (re-search-forward "^#\\+filetags:\\s-*\\(.*?\\)\\s-*$" nil t)
      (let* ((line-beg (match-beginning 0))
             (line-end (match-end 0))
             (raw      (match-string-no-properties 1))
             (tags     (split-string raw "[: \t]+" t))
             (clean    (concat "#+filetags: :"
                               (string-join tags ":")
                               ":"
                               "\n")))
        (unless (equal (buffer-substring-no-properties line-beg line-end) clean)
          (goto-char line-beg)
          (delete-region line-beg line-end)
          (insert clean))
        (forward-line 1)))))
#+end_src
**** org-latex
Use native highlighting for =LaTeX=  related syntax in =org= buffers.
By using native highlighting the =org-face= gets added which we want to avoid.
#+begin_src emacs-lisp
(setq org-highlight-latex-and-related '(native script))

(after! org-src
  (add-to-list 'org-src-block-faces
               `("latex" (:background ,(doom-color 'bg)
                          :extend t))))
#+end_src

Enable =evil-tex-mode= in LaTeX source blocks.
Requires [[latex]] and [[evil]] to be enabled.
#+begin_src emacs-lisp
(add-hook! 'org-src-mode-hook
  (when (string= major-mode "latex-mode")
    (evil-tex-mode 1)))
#+end_src

Set up =LaTeX= previews in =org= buffers.
[[https://raw.githubusercontent.com/michaelneuper/doom/main/images/latex-preview.png]]

#+begin_src emacs-lisp
;; TODO enable latex previews in org-roam and latex buffers (use xenops)

;; modified from https://abode.karthinks.com/org-latex-preview/
(use-package! org-latex-preview
  :after org
  :hook (org-mode . org-latex-preview-mode)
  :init
  (setq org-startup-with-latex-preview t)

  (after! org
    (dolist (pkg '("amsmath" "amssymb" "mathtools" "mathrsfs"))
      (add-to-list 'org-latex-packages-alist `("" ,pkg t))))

  :config
  (plist-put org-latex-preview-appearance-options
             ;; :page-width 0.8)
             :page-width 1.0)

  (setq org-latex-preview-mode-ignored-commands
        '(next-line previous-line mwheel-scroll ultra-scroll
          scroll-up-command scroll-down-command
          evil-scroll-up evil-scroll-down evil-scroll-line-up evil-scroll-line-down)
        org-latex-preview-numbered t
        org-latex-preview-mode-display-live t
        org-latex-preview-mode-update-delay 0.25)
        ;; org-latex-preview-cache 'temp) ;; HACK fix `org-persist' issue: https://discord.com/channels/406534637242810369/1056621127188881439/1392466785168785540

  ;; code for centering LaTeX previews -- a terrible idea
  (defun my/org-latex-preview-uncenter (ov)
    (overlay-put ov 'before-string nil))
  (defun my/org-latex-preview-recenter (ov)
    (overlay-put ov 'before-string (overlay-get ov 'justify)))
  (defun my/org-latex-preview-center (ov)
    (save-excursion
      (goto-char (overlay-start ov))
      (when-let* ((elem (org-element-context))
                  ((or (eq (org-element-type elem) 'latex-environment)
                       (string-match-p "^\\\\\\[" (org-element-property :value elem))))
                  (img (overlay-get ov 'display))
                  (prop `(space :align-to (- center (0.55 . ,img))))
                  (justify (propertize " " 'display prop 'face 'default)))
        (overlay-put ov 'justify justify)
        (overlay-put ov 'before-string (overlay-get ov 'justify)))))
  (define-minor-mode org-latex-preview-center-mode
    "Center equations previewed with `org-latex-preview'."
    :global nil
    (if org-latex-preview-center-mode
        (progn
          (add-hook 'org-latex-preview-overlay-open-functions
                    #'my/org-latex-preview-uncenter nil :local)
          (add-hook 'org-latex-preview-overlay-close-functions
                    #'my/org-latex-preview-recenter nil :local)
          (add-hook 'org-latex-preview-overlay-update-functions
                    #'my/org-latex-preview-center nil :local))
      (remove-hook 'org-latex-preview-overlay-close-functions
                   #'my/org-latex-preview-recenter)
      (remove-hook 'org-latex-preview-overlay-update-functions
                   #'my/org-latex-preview-center)
      (remove-hook 'org-latex-preview-overlay-open-functions
                   #'my/org-latex-preview-uncenter))))
#+end_src
**** +pretty
***** org-modern
Some eye candy in =org= buffers provided.
#+begin_src emacs-lisp
;; modified from https://sophiebos.io/posts/beautifying-emacs-org-mode/
(after! org-modern
  (setq org-auto-align-tags t
        org-tags-column 0
        org-fold-catch-invisible-edits 'show-and-error
        org-special-ctrl-a/e t
        org-insert-heading-respect-content t

        org-modern-table nil ;; use a different package in `other' for pretty tables
        org-modern-todo nil
        org-modern-priority nil
        org-modern-progress 6

        ;; agenda
        org-agenda-tags-column 0
        org-agenda-block-separator ?─
        org-agenda-time-grid
        '((daily today require-timed)
          (800 1000 1200 1400 1600 1800 2000)
          " ┄┄┄┄┄ " "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄")
        org-agenda-current-time-string
        "⭠ now ─────────────────────────────────────────────────"

        ;; text replacements
        org-modern-fold-stars
        `(("◉" . "○" )
          ("◈" . "◇" )
          ("◉" . "○" )
          ("◈" . "◇" )
          ("◉" . "○" ))
        org-modern-checkbox
        ;; requires nerd font
        '((88 . "󰄵")
          (45 . "󰡖")
          (32 . "󰄱"))
        org-modern-list
        '((43 . "•")
          (45 . "–")
          (42 . "↪")))

  (custom-set-faces!
    `(org-modern-tag
      :background ,(doom-color 'fg-alt)
      :foreground ,(doom-color 'bg-alt)
      :family ,my/sans-serif-font
      :height 0.7)))
#+end_src
***** org-appear
#+begin_src emacs-lisp
(after! org-appear
  (setq org-hide-emphasis-markers t
        org-pretty-entities nil
        ;; org-appear-autoentities t
        org-appear-autosubmarkers t
        org-appear-inside-latex t
        org-appear-autolinks 'just-brackets))
#+end_src
***** other
Modern block styling from [[https://github.com/jdtsmith/org-modern-indent][org-modern-indent]].
#+begin_src emacs-lisp :tangle packages.el
(package! org-modern-indent :recipe
  (:host github
   :repo "jdtsmith/org-modern-indent"))
#+end_src

#+begin_src emacs-lisp :tangle no
;; (use-package! org-modern-indent
;;   :hook (org-mode . org-modern-indent-mode)
;;   :config
;;   (custom-set-faces!
;;     `((org-block-begin-line)
;;       :inherit default
;;       :background nil)
;;     `((org-block-end-line)
;;       :inherit org-block-begin-line
;;       :background nil))

;;   (setq org-modern-block-name
;;         '((t . t)
;;           ("src" "──»" "──«")
;;           ("example" "─»»" "─««")
;;           ("quote" "─ ❝" "─ ❞")
;;           ("export" "⏩" "⏪"))))
#+end_src

Align tables with images and hidden markup correctly with [[https://github.com/casouri/valign][valign]].
*NOTE*: Can be slow, especially with large =org= files.
#+begin_src emacs-lisp :tangle packages.el
(package! valign)
#+end_src

#+begin_src emacs-lisp
(defun my/valign-mode-maybe ()
  (when (< (buffer-size) 50000)
    (valign-mode 1)))

(use-package! valign
  :hook (org-mode . my/valign-mode-maybe)
  :config (setq valign-fancy-bar t))
#+end_src
**** +dragndrop
Customise drag-and-drop support for images and files, insert them into the =images/= directory.
#+begin_src emacs-lisp
(use-package! org-download
  :defer t
  :init
  (setq-default org-download-image-dir "images")
  (map! :map org-mode-map
        :localleader
        :desc "Rename image at point" "a C" #'org-download-rename-at-point)
  :config
  (setq org-download-method 'directory
        org-download-link-format "[[file:images/%s]]\n"
        org-download-heading-lvl nil))

#+end_src
**** +roam
Set up =org-roam= along with keybindings that have the [[kbd:][<leader> r]] prefix.
#+begin_src emacs-lisp
(remove-hook 'org-load-hook #'+org-init-roam-h)

(use-package! org-roam
  :defer t
  :init
  (setq org-roam-directory (file-truename "~/Notes")
        org-roam-db-location (file-truename "~/Notes/org-roam.db")
        org-attach-id-dir "assets/")

  ;; copied from https://hieuphay.com/doom-emacs-config/#customizing-main-interface
  (defun my/org-roam-node-find-by-mtime ()
    "Find a node by last modified date."
    (interactive)
    (require 'org-roam)
    (find-file
     (org-roam-node-file
      (org-roam-node-read nil nil #'org-roam-node-read-sort-by-file-mtime))))

  (map! :leader
        (:prefix ("r" . "roam")
         :desc "Find node" "f" #'my/org-roam-node-find-by-mtime))
  :config
  (org-roam-db-autosync-enable)

  ;; TODO use built-in func https://github.com/doomemacs/doomemacs/commit/849b1e4aa8a5089943c0254e68db4a0bc40371d9
  ;; custom `org-roam' functions
  (defun my/org-roam--after-point ()
    "If in Evil normal state and not at EOL, move one char forward."
    (when (and (bound-and-true-p evil-mode)
               (evil-normal-state-p)
               (not (eolp)))
      (forward-char)))

  (defun my/current-buffer-has-module-tag ()
    "Return non-nil when this buffer’s #+filetags line contains :module:."
    (save-excursion
      (goto-char (point-min))
      (re-search-forward "^#\\+filetags:.*:module:" nil t)))

  (defun my/org-roam-insert-lowercase ()
    "Insert an org-roam link after point.
   Lower-case the link text unless the current buffer is tagged :module:."
    (interactive)
    (let* ((module-context-p (my/current-buffer-has-module-tag))
           (org-roam-node-formatter
            (lambda (node)
              (let ((title (org-roam-node-title node)))
                (if module-context-p title (downcase title))))))
      (my/org-roam--after-point)
      (call-interactively #'org-roam-node-insert)))

  (defun my/org-roam-insert-custom-title ()
    "Pick a node, then prompt for a verbatim link description."
    (interactive)
    (my/org-roam--after-point)
    (let* ((node (org-roam-node-read))
           (desc (read-string "Description: "))
           (link (org-link-make-string
                  (concat "id:" (org-roam-node-id node))
                  desc)))
      (insert link)
      (org-roam-link-replace-at-point link)
      (run-hooks 'org-roam-insert-node-hook)
      (forward-char)))

  ;; keybindings
  (map! :map org-roam-mode-map
        :leader
        (:prefix ("r" . "roam")
         :desc "Add alias"          "a" #'org-roam-alias-add
         :desc "Remove alias"       "A" #'org-roam-alias-remove
         :desc "Toggle roam buffer" "b" #'org-roam-buffer-toggle
         :desc "Insert node"        "i" #'my/org-roam-insert-lowercase
         :desc "Insert title"       "I" #'my/org-roam-insert-custom-title
         :desc "Add tag"            "t" #'org-roam-tag-add
         :desc "Remove tag"         "T" #'org-roam-tag-remove
         :desc "Visit node"         "v" #'org-roam-node-visit)))
#+end_src

=org-roam-ui= provides a pretty graph.
#+begin_src emacs-lisp :tangle packages.el
(package! org-roam-ui)
#+end_src

#+begin_src emacs-lisp
(use-package! websocket :after org-roam)

(use-package! org-roam-ui
  :after org-roam
  :config
  (setq org-roam-ui-sync-theme t
        org-roam-ui-follow t
        org-roam-ui-update-on-save t
        org-roam-ui-open-on-start t
        org-roam-mode-sections
        (list #'org-roam-backlinks-section
              #'org-roam-reflinks-section
              #'org-roam-unlinked-references-section))

  (map! :map org-roam-mode-map
        :leader
        (:prefix ("r" . "roam")
         :desc "Open ORUI" "u" #'org-roam-ui-open)))
#+end_src
*** other
**** c3
#+begin_src emacs-lisp :tangle packages.el
(package! c3-ts-mode :recipe
  (:host github
   :repo "c3lang/c3-ts-mode"))
#+end_src

#+begin_src emacs-lisp
(use-package! c3-ts-mode
  :mode ("\\.c3\\'" . c3-ts-mode)
  :init
  (set-tree-sitter! 'c3-mode 'c3-ts-mode
    '((c3 :url "https://github.com/c3lang/tree-sitter-c3")))

  :config
  (setq c3-ts-mode-indent-offset 4)
  (set-eglot-client! '(c3-mode c3-ts-mode)
                     (list (expand-file-name "~/Downloads/server/bin/release/c3lsp"))))
#+end_src
**** arduino
#+begin_src emacs-lisp :tangle packages.el
(package! arduino-mode)
#+end_src

#+begin_src emacs-lisp
;; TODO add arduino support https://www.reddit.com/r/emacs/comments/17n4ti6/how_to_configure_arduino_lsp/
;; (after! arduino-mode
;;   (add-to-list 'eglot-server-programs
;;                '(arduino-mode . ("arduino-language-server"
;;                                  "-clangd" "/usr/bin/clangd"
;;                                  "-cli" "/opt/homebrew/bin/arduino-cli"
;;                                  "-cli-config" "~/Library/Arduino15/arduino-cli.yaml"
;;                                  "-fqbn" "arduino:avr:uno"))))
#+end_src
**** asm
#+begin_src emacs-lisp :tangle packages.el
(package! nasm-mode)
#+end_src

#+begin_src emacs-lisp
(use-package! nasm-mode
  :hook (asm-mode . nasm-mode)
  :config
  (setq nasm-after-mnemonic-whitespace :space)
  (custom-set-faces!
    `(nasm-instructions :foreground ,(doom-color 'cyan))
    `(nasm-registers    :foreground ,(doom-color 'violet))
    `(nasm-local-labels :foreground ,(doom-color 'yellow))))
#+end_src