nixos/home/emacs/config.org
2023-11-06 12:09:47 +00:00

37 KiB

Emacs Config

Common defaults

  (setq user-full-name "Evie Litherland-Smith"
        user-mail-address "evie@xenia.me.uk")
  (setq custom-file (locate-user-emacs-file "custom.el"))
  (set-default-coding-systems 'utf-8)
  (set-terminal-coding-system 'utf-8)
  (set-keyboard-coding-system 'utf-8)

package-archive with priorities

  (when (require 'package nil :noerror)
    (add-to-list 'package-archives '("stable" . "https://stable.melpa.org/packages/"))
    (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/"))

    (setq package-archive-priorities '(("gnu" . 99)
                                       ("nongnu" . 80)
                                       ("stable" . 70)
                                       ("melpa" . 0))))

Org-mode

For reference information, see Org-mode website

  (setq org-directory "~/Org"
        org-default-notes-file (expand-file-name "notes.org" org-directory)
        org-pretty-entities-include-sub-superscripts t
        org-pretty-entities-include-sub-superscripts t
        org-tags-column 0
        org-modern-tag nil
        org-outline-path-complete-in-steps nil
        org-refile-use-outline-path t
        org-refile-allow-creating-parent-nodes t
        org-refile-use-outline-path 'file
        org-refile-targets '((nil :maxlevel . 3)
                             (org-agenda-files :maxlevel . 3)))

Keymaps

  (keymap-global-set "C-c o a" #'org-agenda)
  (keymap-global-set "C-c o n" #'org-capture)
  (keymap-global-set "C-c o l" #'org-capture-goto-last-stored)
  (keymap-global-set "C-c o j j" #'org-journal-new-entry)
  (keymap-global-set "C-c o j n" #'org-journal-new-date-entry)
  (keymap-global-set "C-c o j s" #'org-journal-new-scheduled-entry)

  (add-hook 'org-mode-hook #'org-modern-mode)
  (add-hook 'org-agenda-finalize-hook #'org-modern-agenda)

org-agenda

  (setq org-agenda-files (list (expand-file-name org-directory)
                               (expand-file-name "journal" org-directory)
                               (expand-file-name "projects" org-directory))
        org-agenda-sticky nil
        org-agenda-window-setup 'current-window
        org-agenda-prefix-format '((agenda . " %-12:c%?-12t% s")
                                   (todo . " %-12:c")
                                   (tags . " %-12:c")
                                   (search . " %-12:c")))

org-journal

  (setq org-journal-dir (expand-file-name "journal" org-directory)
        org-journal-file-type 'monthly
        org-journal-file-format "%Y-%m.org")

Capture templates

  (setq org-capture-templates
        '(("n" "Note" entry
           (file+headline "inbox.org" "Note")
           "* %?"
           :prepend t
           :empty-lines 1)
          ("t" "Task" entry
           (file+headline "inbox.org" "Task")
           "* TODO %?"
           :prepend t
           :empty-lines 1)
          ))

Citar

  (setq citar-bibliography '("~/.references/main.bib")
        citar-library-paths '("~/.references/library")
        citar-notes-paths '("~/.references/notes")
        citar-symbols '((file "F" . "󰂺")
                        (note "N" . "󰎞")
                        (link "L" . "󰌹")))
  (when (require 'citar nil :noerror)
    (keymap-global-set "C-c o c o" #'citar-open))

Khalel

  (setq khalel-import-org-file (expand-file-name "calendar.org" org-directory)
        khalel-import-org-file-read-only nil
        khalel-import-org-file-confirm-overwrite nil
        khalel-import-start-date "-30d"
        khalel-import-end-date "+30d")
  (when (require 'khalel nil :noerror)
    (add-hook 'org-agenda-mode-hook #'khalel-import-events)
    (khalel-add-capture-template "e"))

Copy (to sort)

For now I'll just copy all config into this file, to confirm that it works properly. Will reorganise into separate sections later

TODO Defaults

  ;;; Code:
  (setq load-prefer-newer t
        indent-tabs-mode nil
        global-auto-revert-non-file-buffers t
        dired-auto-revert-buffer t
        dired-dwim-target t
        tab-always-indent 'complete
        completion-cycle-threshold 3
        completions-detailed t
        xref-show-definitions-function #'xref-show-definitions-completing-read
        kill-do-not-save-duplicates t
        auto-window-vscroll nil
        fast-but-imprecise-scrolling t
        scroll-conservatively 101
        scroll-margin 0
        scroll-preserve-screen-position 1)

  (global-auto-revert-mode +1)
  (delete-selection-mode)

  ;; Misc useful keymaps
  (keymap-global-set "M-#" #'dictionary-lookup-definition)
  (keymap-global-set "C-c r" #'recentf)
  (keymap-global-set "C-c b" #'ibuffer)
  (keymap-global-set "C-c p l" #'list-packages)
  (keymap-global-set "C-c p r" #'package-refresh-contents)
  (keymap-global-set "C-c p i" #'package-install)
  (keymap-global-set "C-c p d" #'package-delete)

  ;; turn on spell checking, if available.
  (when (and (require 'ispell nil :noerror) (executable-find ispell-program-name))
    (add-hook 'text-mode-hook #'flyspell-mode)
    (add-hook 'prog-mode-hook #'flyspell-prog-mode))

  ;; Make shebang (#!) file executable when saved
  (add-hook 'after-save-hook #'executable-make-buffer-file-executable-if-script-p)

TODO UI

  (setq use-dialog-box nil
        fill-column 80
        truncate-lines nil
        truncate-partial-width-windows nil)

  (menu-bar-mode -1)
  (tab-bar-mode -1)
  (tool-bar-mode -1)
  (scroll-bar-mode -1)
  (line-number-mode)
  (global-display-line-numbers-mode -1)
  (global-prettify-symbols-mode +1)
  (global-visual-line-mode +1)
  ;; (set-frame-font "FiraCode Nerd Font-12")
  ;; (set-frame-parameter nil 'alpha-background 80)

  ;; Nerd-Icons modes
  (when (require 'nerd-icons)
    (nerd-icons-set-font "Symbols Nerd Font Mono")
    (when (require 'nerd-icons-dired nil :noerror)
      (add-hook 'dired-mode-hook #'nerd-icons-dired-mode))

    (when (require 'nerd-icons-ibuffer nil :noerror)
      (add-hook 'ibuffer-mode-hook #'nerd-icons-ibuffer-mode))

    (when (require 'nerd-icons-completion nil :noerror)
      (nerd-icons-completion-mode)
      (when (require 'marginalia nil :noerror)
        (add-hook 'marginalia-mode-hook #'nerd-icons-completion-marginalia-setup))))

  ;; Doom-Modeline
  (setq doom-modeline-icon t
        doom-modeline-mu4e nil ;; Use mu4e own formatting
        doom-modeline-modal nil
        doom-modeline-modal-icon nil
        doom-modeline-persp-name nil
        doom-modeline-persp-icon nil)
  (when (require 'doom-modeline nil :noerror)
    (doom-modeline-mode +1))

  ;; Dashboard
  (setq initial-buffer-choice 'dashboard-open
        dashboard-icon-type 'nerd-icons
        dashboard-set-heading-icons t
        dashboard-set-file-icons t
        dashboard-set-navigator t
        dashboard-set-init-info t
        dashboard-startup-banner 'ascii
        dashboard-projects-backend 'project-el
        dashboard-projects-switch-function 'project-switch-project
        dashboard-projects-show-base t
        dashboard-recentf-show-base 'align
        dashboard-items '((recents . 5) 
                          (agenda . 15))
        dashboard-heading-icons '((recents . "nf-oct-history")
                                  (agenda . "nf-oct-calendar"))
        dashboard-banner-ascii (concat "          .000000.          \n"
                                       "         .0.    .0.         \n"
                                       "       .00.      .00.       \n"
                                       " .000cl.            .lc000. \n"
                                       ".0                        0.\n"
                                       "0.        .o0000o.        .0\n"
                                       " 00     .0'      '0.     00 \n"
                                       "  00   .0          0.   00  \n"
                                       "  HHHHH HHHHHHHHHHHH HHHHH  \n"
                                       "HHHH HHH HHHHHHHHHHHHHH HHHH\n"
                                       " HHHHHH HHHHHHHHH HHHHHHHH  \n"
                                       "  HHH HHHH HHHHHHHHH HHHH   \n"
                                       "         HHH HHHHHH         \n"
                                       "          HHHHH HH          \n"))
  (when (require 'dashboard nil :noerror)
    (keymap-global-set "C-c d" #'dashboard-open)
    (add-hook 'dashboard-mode-hook #'khalel-import-events))

  ;; Extra minor-modes
  (when (require 'git-gutter nil :noerror)
    (global-git-gutter-mode +1))

  (when (require 'which-key nil :noerror)
    (which-key-mode +1))

  (when (require 'page-break-lines nil :noerror)
    (global-page-break-lines-mode +1))

  ;;;; Help Buffers

  ;; Make `describe-*' screens more helpful
  (when (require 'helpful nil :noerror)
    (keymap-set helpful-mode-map "<remap> <revert-buffer>" #'helpful-update)
    (keymap-global-set "<remap> <describe-command>"        #'helpful-command)
    (keymap-global-set "<remap> <describe-function>"       #'helpful-callable)
    (keymap-global-set "<remap> <describe-key>"            #'helpful-key)
    (keymap-global-set "<remap> <describe-symbol>"         #'helpful-symbol)
    (keymap-global-set "<remap> <describe-variable>"       #'helpful-variable)
    (keymap-global-set "C-h F"                             #'helpful-function))

  ;; Bind extra `describe-*' commands
  (keymap-global-set "C-h K" #'describe-keymap)

  ;; add visual pulse when changing focus, like beacon but built-in
  ;; from from https://karthinks.com/software/batteries-included-with-emacs/
  (defun pulse-line (&rest _)
    "Pulse the current line."
    (pulse-momentary-highlight-one-line (point)))

  (dolist (command '(scroll-up-command
                     scroll-down-command
                     recenter-top-bottom
                     other-window))
    (advice-add command :after #'pulse-line))


  ;;; custom-ui-config.el ends here

TODO Email

  (setq sendmail-program (executable-find "msmtp")
        send-mail-function #'smtpmail-send-it
        message-sendmail-f-is-evil t
        message-sendmail-extra-arguments '("--read-envelope-from")
        message-send-mail-function #'message-send-mail-with-sendmail
        message-kill-buffer-on-exit t
        mail-user-agent 'mu4e-user-agent
        read-mail-command 'mu4e
        mu4e-maildir "~/Mail"
        mu4e-attachment-dir "~/Downloads"
        mu4e-get-mail-command "mbsync -a"
        mu4e-update-interval (* 5 60) ; Every 5 minutes
        mu4e-sent-messages-behavior 'sent
        mu4e-change-filenames-when-moving t
        mu4e-context-policy 'pick-first
        mu4e-use-fancy-chars t
        mu4e-headers-thread-single-orphan-prefix '("─>" . "─▶")
        mu4e-headers-thread-orphan-prefix        '("┬>" . "┬▶")
        mu4e-headers-thread-connection-prefix    '("│ " . "│ ")
        mu4e-headers-thread-first-child-prefix   '("├>" . "├▶")
        mu4e-headers-thread-child-prefix         '("├>" . "├▶")
        mu4e-headers-thread-last-child-prefix    '("└>" . "╰▶")
        mu4e-modeline-all-read '("R:" . "󰑇 ")
        mu4e-modeline-all-clear '("C:" . "󰚭 ")
        mu4e-modeline-new-items '("N:" . "󰎔 ")
        mu4e-modeline-unread-items '("U:" . "󰶊 ")
        mu4e-search-full-label '("F" . "󱊖 ")
        mu4e-search-hide-label '("H" . "󰘓 ")
        mu4e-search-related-label '("R" . "󰌹 ")
        mu4e-search-skip-duplicates-label '("D" . "󰆑 ")
        mu4e-search-threaded-label'("T" . "󱇫 ")
        mu4e-alert-modeline-formatter 'mu4e-alert-default-mode-line-formatter
        mu4e-headers-fields '((:human-date . 12)
                              (:flags . 6)
                              (:from-or-to . 25)
                              (:subject))
        mu4e-headers-actions '(("org capture message" . mu4e-org-store-and-capture)
                               ("capture message" . mu4e-action-capture-message)
                               ("show this thread" . mu4e-action-show-thread)) 
        mu4e-maildir-shortcuts '((:maildir "/Proton/Inbox/" :key ?p)
                                 (:maildir "/iCloud/Inbox/" :key ?i)
                                 (:maildir "/Outlook/Inbox/" :key ?w)))

  (when (require 'mu4e nil :noerror)
    (keymap-global-set "C-c m m" #'mu4e)
    (keymap-global-set "C-c m u" #'mu4e-update-index)
    (keymap-global-set "C-c m c" #'mu4e-compose-new)

    (when (require 'mu4e-alert nil :noerror)
      (mu4e-alert-set-default-style 'libnotify)
      (mu4e-alert-enable-notifications)
      (mu4e-alert-enable-mode-line-display))

    (setq mu4e-contexts (list
                         (make-mu4e-context
                          :name "Xenia"
                          :vars '((user-mail-address . "evie@xenia.me.uk")
                                  (mu4e-sent-folder . "/Proton/Sent")
                                  (mu4e-drafts-folder . "/Proton/Drafts")
                                  (mu4e-trash-folder . "/Proton/Trash")
                                  (mu4e-refile-folder . "/Proton/Archive")))
                         (make-mu4e-context
                          :name "Proton"
                          :match-func (lambda (msg) (when msg (string-prefix-p "/Proton" (mu4e-message-field msg :maildir))))
                          :vars '((user-mail-address . "e.litherlandsmith@proton.me")
                                  (mu4e-sent-folder . "/Proton/Sent")
                                  (mu4e-drafts-folder . "/Proton/Drafts")
                                  (mu4e-trash-folder . "/Proton/Trash")
                                  (mu4e-refile-folder . "/Proton/Archive")))
                         (make-mu4e-context
                          :name "iCloud"
                          :match-func (lambda (msg) (when msg (string-prefix-p "/iCloud" (mu4e-message-field msg :maildir))))
                          :vars '((user-mail-address . "e.litherlandsmith@icloud.com")
                                  (mu4e-sent-folder . "/iCloud/Sent Messages")
                                  (mu4e-drafts-folder . "/iCloud/Drafts")
                                  (mu4e-trash-folder . "/iCloud/Deleted Messages")
                                  (mu4e-refile-folder . "/iCloud/Archive")))
                         (make-mu4e-context
                          :name "Work"
                          :match-func (lambda (msg) (when msg (string-prefix-p "/Outlook" (mu4e-message-field msg :maildir))))
                          :vars '((user-mail-address . "evie.litherland-smith@ukaea.uk")
                                  (mu4e-sent-folder . "/Outlook/Sent")
                                  (mu4e-drafts-folder . "/Outlook/Drafts")
                                  (mu4e-trash-folder . "/Outlook/Trash")
                                  (mu4e-refile-folder . "/Outlook/Archive"))))))

TODO Feeds

  (let ((elfeed-base-directory "~/.elfeed"))
    (setq elfeed-db-directory (expand-file-name "db" elfeed-base-directory)
          elfeed-enclosure-default-dir (expand-file-name "enclosures" elfeed-base-directory)
          rmh-elfeed-org-files (list (expand-file-name "feeds.org" elfeed-base-directory))))

  (when (require 'elfeed nil :noerror)
    (keymap-global-set "C-c f f" #'elfeed)
    (add-hook 'elfeed-search-mode-hook #'elfeed-update)
    (when (require 'elfeed-org nil :noerror)
      (elfeed-org))
    (when (require 'elfeed-tube nil :noerror) 
      (elfeed-tube-setup)))

TODO IDE

  (when (require 'rainbow-delimiters nil :noerror)
    (add-hook 'prog-mode-hook #'rainbow-delimiters-mode))

  (when (require 'treesit-aut nil :noerror)
    (global-treesit-auto-mode +1))

  (setq apheleia-remote-algorithm 'local)
  (when (require 'apheleia nil :noerror)
    (keymap-global-set "C-c c f" #'apheleia-format-buffer)
    (apheleia-global-mode +1))

  (when (require 'eglot nil :noerror)
    (eglot-ensure))

  (setq direnv-always-show-summary nil)
  (when (require 'direnv nil :noerror)
    (direnv-mode +1))

  (when (require 'yasnippet nil :noerror)
    (require 'yasnippet-snippets nil :noerror)
    (yas-reload-all)
    (add-hook 'prog-mode-hook #'yas-minor-mode))

TODO Ligatures

  (defun fira-code-mode--make-alist (list)
    "Generate prettify-symbols alist from LIST."
    (let ((idx -1))
      (mapcar
       (lambda (s)
         (setq idx (1+ idx))
         (let* ((code (+ #Xe100 idx))
                (width (string-width s))
                (prefix ())
                (suffix '(?\s (Br . Br)))
                (n 1))
           (while (< n width)
             (setq prefix (append prefix '(?\s (Br . Bl))))
             (setq n (1+ n)))
           (cons s (append prefix suffix (list (decode-char 'ucs code))))))
       list)))

  (defconst fira-code-mode--ligatures
    '("www" "**" "***" "**/" "*>" "*/" "\\\\" "\\\\\\"
      "{-" "[]" "::" ":::" ":=" "!!" "!=" "!==" "-}"
      "--" "---" "-->" "->" "->>" "-<" "-<<" "-~"
      "#{" "#[" "##" "###" "####" "#(" "#?" "#_" "#_("
      ".-" ".=" ".." "..<" "..." "?=" "??" ";;" "/*"
      "/**" "/=" "/==" "/>" "//" "///" "&&" "||" "||="
      "|=" "|>" "^=" "$>" "++" "+++" "+>" "=:=" "=="
      "===" "==>" "=>" "=>>" "<=" "=<<" "=/=" ">-" ">="
      ">=>" ">>" ">>-" ">>=" ">>>" "<*" "<*>" "<|" "<|>"
      "<$" "<$>" "<!--" "<-" "<--" "<->" "<+" "<+>" "<="
      "<==" "<=>" "<=<" "<>" "<<" "<<-" "<<=" "<<<" "<~"
      "<~~" "</" "</>" "~@" "~-" "~=" "~>" "~~" "~~>" "%%"
      "x" ":" "+" "+" "*"))

  (defvar fira-code-mode--old-prettify-alist)

  (defun fira-code-mode--enable ()
    "Enable Fira Code ligatures in current buffer."
    (setq-local fira-code-mode--old-prettify-alist prettify-symbols-alist)
    (setq-local prettify-symbols-alist (append (fira-code-mode--make-alist fira-code-mode--ligatures) fira-code-mode--old-prettify-alist))
    (prettify-symbols-mode t))

  (defun fira-code-mode--disable ()
    "Disable Fira Code ligatures in current buffer."
    (setq-local prettify-symbols-alist fira-code-mode--old-prettify-alist)
    (prettify-symbols-mode -1))

  (define-minor-mode fira-code-mode
    "Fira Code ligatures minor mode"
    :lighter " Fira Code"
    (setq-local prettify-symbols-unprettify-at-point 'right-edge)
    (if fira-code-mode
        (fira-code-mode--enable)
      (fira-code-mode--disable)))

  (defun fira-code-mode--setup ()
    "Setup Fira Code Symbols"
    (set-fontset-font t '(#Xe100 . #Xe16f) "Fira Code Symbol"))

  (fira-code-mode)

TODO Media

  (when (require 'emms-setup nil :noerror)
    (setq emms-player-list '(emms-player-mpv)
          emms-info-functions '(emms-info-native)
          emms-source-file-default-directory "~/Music"
          emms-lyrics-dir "~/Music/lyrics"
          emms-mode-line-icon-color "white")

    (emms-all)
    (add-hook 'emms-player-started-hook #'emms-show)
    (add-hook 'emms-player-paused-hook #'emms-show)

    (when (require 'hydra nil :noerror)
      (defhydra emms (global-map "C-c e")
        "emms"
        ("b" emms-smart-browse)
        ("d" emms-show)
        ("s" emms-start)
        ("S" emms-stop)
        ("n" emms-next)
        ("p" emms-previous)
        ("P" emms-pause))))

TODO Project

  (setq project-switch-use-entire-map t
        project-switch-commands
        '((project-dired "Browse directory")
          (project-find-file "Find file")
          (project-find-regexp "Find regexp")
          (project-find-dir "Find directory")
          (project-eshell "Eshell")))

  (setq magit-clone-default-directory "~/Projects/")
  (require 'magit nil :noerror)

TODO Social

  (setq mastodon-instance-url "https://tech.lgbt"
        mastodon-active-user "Tux922")
  (require 'mastodon nil :noerror)

TODO Completion (Crafted)

  ;;; Vertico
  (when (require 'vertico nil :noerror)
    (require 'vertico-directory)
    ;; Cycle back to top/bottom result when the edge is reached
    (customize-set-variable 'vertico-cycle t)

    ;; Start Vertico
    (vertico-mode 1)

    ;; Turn off the built-in fido-vertical-mode and icomplete-vertical-mode, if
    ;; they have been turned on by crafted-defaults-config, because they interfere
    ;; with this module.
    (with-eval-after-load 'crafted-defaults-config
      (fido-mode -1)
      (fido-vertical-mode -1)
      (icomplete-mode -1)
      (icomplete-vertical-mode -1)))

  ;;; Marginalia
  (when (require 'marginalia nil :noerror)
    ;; Configure Marginalia
    (customize-set-variable 'marginalia-annotators
                            '(marginalia-annotators-heavy
                              marginalia-annotators-light
                              nil))
    (marginalia-mode 1))

  ;;; Consult
  ;; Since Consult doesn't need to be required, we assume the user wants these
  ;; setting if it is installed (regardless of the installation method).
  (when (locate-library "consult")
    ;; Set some consult bindings
    (keymap-global-set "C-s" 'consult-line)
    (keymap-set minibuffer-local-map "C-r" 'consult-history)

    (setq completion-in-region-function #'consult-completion-in-region))

  ;;; Orderless
  (when (require 'orderless nil :noerror)
    ;; Set up Orderless for better fuzzy matching
    (customize-set-variable 'completion-styles '(orderless basic))
    (customize-set-variable 'completion-category-overrides
                            '((file (styles . (partial-completion))))))

  ;;; Embark
  (when (require 'embark nil :noerror)

    (keymap-global-set "<remap> <describe-bindings>" #'embark-bindings)
    (keymap-global-set "C-." 'embark-act)

    ;; Use Embark to show bindings in a key prefix with `C-h`
    (setq prefix-help-command #'embark-prefix-help-command)

    (when (require 'embark-consult nil :noerror)
      (with-eval-after-load 'embark-consult
        (add-hook 'embark-collect-mode-hook #'consult-preview-at-point-mode))))

  ;;; Corfu
  (when (require 'corfu nil :noerror)

    (unless (display-graphic-p)
      (when (require 'corfu-terminal nil :noerror)
        (corfu-terminal-mode +1)))

    ;; Setup corfu for popup like completion
    (customize-set-variable 'corfu-cycle t)        ; Allows cycling through candidates
    (customize-set-variable 'corfu-auto t)         ; Enable auto completion
    (customize-set-variable 'corfu-auto-prefix 2)  ; Complete with less prefix keys

    (global-corfu-mode 1)
    (when (require 'corfu-popupinfo nil :noerror)

      (corfu-popupinfo-mode 1)
      (eldoc-add-command #'corfu-insert)
      (keymap-set corfu-map "M-p" #'corfu-popupinfo-scroll-down)
      (keymap-set corfu-map "M-n" #'corfu-popupinfo-scroll-up)
      (keymap-set corfu-map "M-d" #'corfu-popupinfo-toggle)))

  ;;; Cape

  (when (require 'cape nil :noerror)
    ;; Setup Cape for better completion-at-point support and more

    ;; Add useful defaults completion sources from cape
    (add-to-list 'completion-at-point-functions #'cape-file)
    (add-to-list 'completion-at-point-functions #'cape-dabbrev)

    ;; Silence the pcomplete capf, no errors or messages!
    ;; Important for corfu
    (advice-add 'pcomplete-completions-at-point :around #'cape-wrap-silent)

    ;; Ensure that pcomplete does not write to the buffer
    ;; and behaves as a pure `completion-at-point-function'.
    (advice-add 'pcomplete-completions-at-point :around #'cape-wrap-purify)

    ;; No auto-completion or completion-on-quit in eshell
    (defun crafted-completion-corfu-eshell ()
      "Special settings for when using corfu with eshell."
      (setq-local corfu-quit-at-boundary t
                  corfu-quit-no-match t
                  corfu-auto nil)
      (corfu-mode))
    (add-hook 'eshell-mode-hook #'crafted-completion-corfu-eshell))

TODO IDE (Crafted)

  ;;; Eglot
  (defun crafted-ide--add-eglot-hooks (mode-list)
    "Add `eglot-ensure' to modes in MODE-LIST.

  The mode must be loaded, i.e. found with `fboundp'.  A mode which
  is not loaded will not have a hook added, in which case add it
  manually with something like this:

  `(add-hook 'some-mode-hook #'eglot-ensure)'"
    (dolist (mode-def mode-list)
      (let ((mode (if (listp mode-def) (car mode-def) mode-def)))
        (cond
         ((listp mode) (crafted-ide--add-eglot-hooks mode))
         (t
          (when (and (fboundp mode)
                     (not (eq 'clojure-mode mode))  ; prefer cider
                     (not (eq 'lisp-mode mode))     ; prefer sly/slime
                     (not (eq 'scheme-mode mode))   ; prefer geiser
                     )
            (let ((hook-name (format "%s-hook" (symbol-name mode))))
              (message "adding eglot to %s" hook-name)
              (add-hook (intern hook-name) #'eglot-ensure))))))))

  ;; add eglot to existing programming modes when eglot is loaded.
  (with-eval-after-load "eglot"
    (crafted-ide--add-eglot-hooks eglot-server-programs))

  ;; Shutdown server when last managed buffer is killed
  (customize-set-variable 'eglot-autoshutdown t)

  ;;; tree-sitter
  (defun crafted-ide--configure-tree-sitter-pre-29 ()
    "Configure tree-sitter for Emacs 28 or earlier."

    (defun crafted-tree-sitter-load (lang-symbol)
      "Setup tree-sitter for a language.

  This must be called in the user's configuration to configure
  tree-sitter for LANG-SYMBOL.

  Example: `(crafted-tree-sitter-load 'python)'"
      (tree-sitter-require lang-symbol)
      (let ((mode-hook-name
             (intern (format "%s-mode-hook" (symbol-name lang-symbol)))))
        (add-hook mode-hook-name #'tree-sitter-mode))))

  (defun crafted-ide--configure-tree-sitter (opt-out)
    "Configure tree-sitter for Emacs 29 or later.
  OPT-OUT is a list of symbols of language grammars to opt out before auto-install."
    ;; only attempt to use tree-sitter when Emacs was built with it.
    (when (member "TREE_SITTER" (split-string system-configuration-features))
      (when (require 'treesit-auto nil :noerror)
        ;; add all items of opt-out to the `treesit-auto-opt-out-list'.
        (when opt-out
          (mapc (lambda (e) (add-to-list 'treesit-auto-opt-out-list e)) opt-out))
        ;; prefer tree-sitter modes
        (global-treesit-auto-mode)
        ;; install all the tree-sitter grammars
        (treesit-auto-install-all)
        ;; configure `auto-mode-alist' for tree-sitter modes relying on
        ;; `fundamental-mode'
        (treesit-auto-add-to-auto-mode-alist))
      (when (locate-library "combobulate")
        ;; perhaps too gross of an application, but the *-ts-modes
        ;; eventually derive from this mode.
        (add-hook 'prog-mode-hook #'combobulate-mode))))

  (defun crafted-ide-configure-tree-sitter (&optional opt-out)
    "Configure tree-sitter.
  Requires a C compiler (gcc, cc, c99) installed on the system.
  Note that OPT-OUT only affects setups with Emacs 29 or later.

  For Emacs 29 or later:
  Requires Emacs to be built using \"--with-tree-sitter\".
  All language grammars are auto-installed unless they are a member of OPT-OUT."
    (if (version< emacs-version "29")
        (crafted-ide--configure-tree-sitter-pre-29)
      (crafted-ide--configure-tree-sitter opt-out)))

  ;; turn on editorconfig if it is available
  (when (require 'editorconfig nil :noerror)
    (add-hook 'prog-mode-hook #'editorconfig-mode))

  ;; enhance ibuffer with ibuffer-project if it is available.
  (when (require 'ibuffer-project nil :noerror)
    (defun crafted-ide-enhance-ibuffer-with-ibuffer-project ()
      "Set up integration for `ibuffer' with `ibuffer-project'."
      (setq ibuffer-filter-groups (ibuffer-project-generate-filter-groups))
      (unless (eq ibuffer-sorting-mode 'project-file-relative)
        (ibuffer-do-sort-by-project-file-relative)))
    (add-hook 'ibuffer-hook #'crafted-ide-enhance-ibuffer-with-ibuffer-project))

TODO Org (Crafted)

  ;; Return or left-click with mouse follows link
  (customize-set-variable 'org-return-follows-link t)
  (customize-set-variable 'org-mouse-1-follows-link t)

  ;; Display links as the description provided
  (customize-set-variable 'org-link-descriptive t)

  ;; Visually indent org-mode files to a given header level
  (add-hook 'org-mode-hook #'org-indent-mode)

  ;; Hide markup markers
  (customize-set-variable 'org-hide-emphasis-markers t)
  (when (locate-library "org-appear")
    (add-hook 'org-mode-hook 'org-appear-mode))

  ;; Disable auto-pairing of "<" in org-mode with electric-pair-mode
  (defun crafted-org-enhance-electric-pair-inhibit-predicate ()
    "Disable auto-pairing of \"<\" in `org-mode' when using `electric-pair-mode'."
    (when (and electric-pair-mode (eql major-mode #'org-mode))
      (setq-local electric-pair-inhibit-predicate
                  `(lambda (c)
                     (if (char-equal c ?<)
                         t
                       (,electric-pair-inhibit-predicate c))))))

  ;; Add hook to both electric-pair-mode-hook and org-mode-hook
  ;; This ensures org-mode buffers don't behave weirdly,
  ;; no matter when electric-pair-mode is activated.
  (add-hook 'electric-pair-mode-hook #'crafted-org-enhance-electric-pair-inhibit-predicate)
  (add-hook 'org-mode-hook #'crafted-org-enhance-electric-pair-inhibit-predicate)

TODO Workspaces (Crafted)

  (with-eval-after-load 'tabspaces
    (customize-set-variable 'tabspaces-use-filtered-buffers-as-default t)
    (customize-set-variable 'tabspaces-remove-to-default t)
    (customize-set-variable 'tabspaces-include-buffers '("*scratch*")))

  ;; Activate it
  (customize-set-variable 'tabspaces-mode t)

  ;; Make sure project is initialized
  (project--ensure-read-project-list)

TODO Writing (Crafted)

  ;;; Whitespace
  (defun crafted-writing-configure-whitespace (use-tabs &optional use-globally &rest enabled-modes)
    "Helper function to configure `whitespace' mode.

  Enable using TAB characters if USE-TABS is non-nil.  If
  USE-GLOBALLY is non-nil, turn on `global-whitespace-mode'.  If
  ENABLED-MODES is non-nil, it will be a list of modes to activate
  whitespace mode using hooks.  The hooks will be the name of the
  mode in the list with `-hook' appended.  If USE-GLOBALLY is
  non-nil, ENABLED-MODES is ignored.

  Configuring whitespace mode is not buffer local.  So calling this
  function twice with different settings will not do what you
  think.  For example, if you wanted to use spaces instead of tabs
  globally except for in Makefiles, doing the following won't work:

  ;; turns on global-whitespace-mode to use spaces instead of tabs
  (crafted-writing-configure-whitespace nil t)

  ;; overwrites the above to turn to use tabs instead of spaces,
  ;; does not turn off global-whitespace-mode, adds a hook to
  ;; makefile-mode-hook
  (crafted-writing-configure-whitespace t nil 'makefile-mode)

  Instead, use a configuration like this:
  ;; turns on global-whitespace-mode to use spaces instead of tabs
  (crafted-writing-configure-whitespace nil t)

  ;; turn on the buffer-local mode for using tabs instead of spaces.
  (add-hook 'makefile-mode-hook #'indent-tabs-mode)

  For more information on `indent-tabs-mode', See the info
  node `(emacs)Just Spaces'

  Example usage:

  ;; Configuring whitespace mode does not turn on whitespace mode
  ;; since we don't know which modes to turn it on for.
  ;; You will need to do that in your configuration by adding
  ;; whitespace mode to the appropriate mode hooks.
  (crafted-writing-configure-whitespace nil)

  ;; Configure whitespace mode, but turn it on globally.
  (crafted-writing-configure-whitespace nil t)

  ;; Configure whitespace mode and turn it on only for prog-mode
  ;; and derived modes.
  (crafted-writing-configure-whitespace nil nil 'prog-mode)"
    (if use-tabs
        (customize-set-variable 'whitespace-style
                                '(face empty trailing indentation::tab
                                       space-after-tab::tab
                                       space-before-tab::tab))
      ;; use spaces instead of tabs
      (customize-set-variable 'whitespace-style
                              '(face empty trailing tab-mark
                                     indentation::space)))

    (if use-globally
        (global-whitespace-mode 1)
      (when enabled-modes
        (dolist (mode enabled-modes)
          (add-hook (intern (format "%s-hook" mode)) #'whitespace-mode))))

    ;; cleanup whitespace
    (customize-set-variable 'whitespace-action '(cleanup auto-cleanup)))

  ;;; parentheses
  (electric-pair-mode 1) ; auto-insert matching bracket
  (show-paren-mode 1)    ; turn on paren match highlighting

  ;;; LaTeX configuration
  (with-eval-after-load 'latex
    (customize-set-variable 'TeX-auto-save t)
    (customize-set-variable 'TeX-parse-self t)
    (setq-default TeX-master nil)

    ;; compile to pdf
    (tex-pdf-mode)

    ;; correlate the source and the output
    (TeX-source-correlate-mode)

    ;; set a correct indentation in a few additional environments
    (add-to-list 'LaTeX-indent-environment-list '("lstlisting" current-indentation))
    (add-to-list 'LaTeX-indent-environment-list '("tikzcd" LaTeX-indent-tabular))
    (add-to-list 'LaTeX-indent-environment-list '("tikzpicture" current-indentation))

    ;; add a few macros and environment as verbatim
    (add-to-list 'LaTeX-verbatim-environments "lstlisting")
    (add-to-list 'LaTeX-verbatim-environments "Verbatim")
    (add-to-list 'LaTeX-verbatim-macros-with-braces "lstinline")
    (add-to-list 'LaTeX-verbatim-macros-with-delims "lstinline")

    ;; electric pairs in auctex
    (customize-set-variable 'TeX-electric-sub-and-superscript t)
    (customize-set-variable 'LaTeX-electric-left-right-brace t)
    (customize-set-variable 'TeX-electric-math (cons "$" "$"))

    ;; open all buffers with the math mode and auto-fill mode
    (add-hook 'LaTeX-mode-hook #'auto-fill-mode)
    (add-hook 'LaTeX-mode-hook #'LaTeX-math-mode)

    ;; add support for references
    (add-hook 'LaTeX-mode-hook #'turn-on-reftex)
    (customize-set-variable 'reftex-plug-into-AUCTeX t)

    ;; to have the buffer refresh after compilation
    (add-hook 'TeX-after-compilation-finished-functions #'TeX-revert-document-buffer))

  (defun crafted-latex-use-pdf-tools ()
    "Use PDF Tools instead of docview, requires a build environment
  to compile PDF Tools.

  Depends on having `pdf-tools'."

    (with-eval-after-load 'latex
      (customize-set-variable 'TeX-view-program-selection '((output-pdf "PDF Tools")))
      (customize-set-variable 'TeX-view-program-list '(("PDF Tools" TeX-pdf-tools-sync-view)))
      (customize-set-variable 'TeX-source-correlate-start-server t)))

  ;; message the user if the latex executable is not found
  (defun crafted-writing-tex-warning-if-no-latex-executable ()
    "Print a message to the minibuffer if the \"latex\" executable cannot be found."
    (unless (executable-find "latex")
      (message "latex executable not found")))
  (add-hook 'tex-mode-hook #'crafted-writing-tex-warning-if-no-latex-executable)

  (when (and (executable-find "latex")
             (executable-find "latexmk"))
    (with-eval-after-load 'latex
      (when (require 'auctex-latexmk nil 'noerror)
        (with-eval-after-load 'auctex-latexmk
          (auctex-latexmk-setup)
          (customize-set-variable 'auctex-latexmk-inherit-TeX-PDF-mode t))

        (defun crafted-writing-tex-make-latexmk-default-command ()
          "Set `TeX-command-default' to \"LatexMk\"."
          (setq TeX-command-default "LatexMk"))
        (add-hook 'TeX-mode-hook #'crafted-writing-tex-make-latexmk-default-command))))

  ;;; Markdown
  (when (fboundp 'markdown-mode)
    ;; because the markdown-command variable may not be loaded (yet),
    ;; check manually for the other markdown processors.  If it is
    ;; loaded, the others are superfluous but `or' fails fast, so they
    ;; are not checked if `markdown-command' is set and the command is
    ;; indeed found.
    (unless (or (and (boundp 'markdown-command)
                     (executable-find markdown-command))
                (executable-find "markdown")
                (executable-find "pandoc"))
      (message "No markdown processor found, preview may not possible."))

    (with-eval-after-load 'markdown-mode
      (customize-set-variable 'markdown-enable-math t)
      (customize-set-variable 'markdown-enable-html t)
      (add-hook 'markdown-mode-hook #'conditionally-turn-on-pandoc)))

  ;;; PDF Support when using pdf-tools
  (when (locate-library "pdf-tools")
    ;; load pdf-tools when going into doc-view-mode
    (defun crafted-writing-load-pdf-tools ()
      "Attempts to require pdf-tools, but for attaching to hooks."
      (require 'pdf-tools nil :noerror))
    (add-hook 'doc-view-mode-hook #'crafted-writing-load-pdf-tools)

    ;; when pdf-tools is loaded, apply settings.
    (with-eval-after-load 'pdf-tools
      (setq-default pdf-view-display-size 'fit-width)))