Skip to content

Repository files navigation

Kudu

Banner

Welcome to Kudu Emacs

What is Kudu?

The complexity and extensibility of GNU Emacs, paired with its lack of integration with contemporary technical standards, has driven the development of Emacs distributions that contain packages and functionality not included by the GNU project. Kudu is a project meant to expand the scope of such distributions to every user-facing part of the operating system using dialects of the lisp programming language. This allows the user to easily and seamlessly “live in Emacs”, using tools integrated directly into the program, such as the Emacs X Window Manager (EXWM), guix.el, and mu4e.

Earlier distributions have focused on integrating Emacs within an otherwise alien system, like DOOM’s and Spacemacs’ focus on keybinds derived from the Vi editor, to maximize the number of workflows that the distribution could be incorporated into. Kudu does not take this approach, but rather empowers the user to construct their own system within a completely configurable system. All tools are written in lisp, the simple syntax of which allows for a seamless experience and self-sufficient system capable of performing all the daily tasks of modern life. It is hoped that this declarative and atomic system offered by GNU Guix will allow more secure and maintainable infrastructure.

The origin for the name is the kudu, an antelope similar to that of the Gnu, the namesake of the GNU Project. Kudu is not part of the GNU Project, and its developers are not members of GNU or the FSF. However we share a positive opinion of free software and therefore want to contribute to its mainstream adoption.

Configuration

Use-package

Probably one of the most useful packages, even if not very prominent when using Emacs, is use-package. It allows you to declaratively write your configuration and have the included Emacs package manager download them for you, and also have configurations for packages only run when packages are loaded, similarly to (with-eval-after-load ...). The variables set here simply enable this behaviour. If the version of Emacs is older than Emacs 29, use-package won’t be available by default. It is therefore installed here as well.

The diminish package hides certain minor modes from being shown in the mode-line and is not installed by default. For this reason its used to check if Kudu has been run before, and therefore if it needs to update its package repositories. Feel free to perform this check on any other package, or remove it entirely, but beware that (package-refresh-contents) must be run before the other use-package declarations for package.el to install all the other packages needed.

(setq use-package-always-defer t
      use-package-always-ensure t)

(unless (package-installed-p 'diminish)
  (package-refresh-contents)
  (package-install 'use-package)
  (package-install 'diminish))

Auto-compile

Compiles elisp files to improve the speed and responsiveness of Emacs at the cost of first-time startup time. The settings in init.el makes sure that updated elisp files take priority over older, compiled files.

(use-package auto-compile
  :after dashboard
  :ensure t
  :config
  (auto-compile-on-load-mode 1)
  (auto-compile-on-save-mode 1))

(setq native-comp-async-report-warnings-errors nil)

Backups

Emacs usually stores backups in the same directory as the files themselves, cluttering up your nice and tidy system. This moves them to a dedicated directory within .emacs.d.

(setq backup-directory-alist '((".*" . "~/.emacs.d/backups")))

EXWM

The Emacs X Window Manager allows you to use your entire desktop within Emacs. Other windows are managed like traditional Emacs buffers, and different workspace are implemented using separate Emacs frames. This is arguably the largest change to using traditional window managers and desktop environments, and it transforms Emacs from simply a program that can do everything to the way to interact with one’s computer.

However, Emacs can still be used without constituting the entire system. Therefore EXWM should only be loaded if no other window manager is running. That way startup time isn’t wasted whenever the user wants to run Emacs in the terminal, on a computer using a desktop environment, or another window manager.

(if (display-graphic-p)
    (use-package exwm
      :defer 0.1
      :config
   ;; EXWM related functions

   (defun xrandr-find-monitor-names ()
     "Returns a list of connected monitors"
     (let ((xrandr-contents nil) (monitor-names nil))
       (shell-command "xrandr" "*xrandr-output*")
       (switch-to-buffer "*xrandr-output*")
       (setq xrandr-contents (buffer-string))
       (kill-buffer "*xrandr-output*")
       (setq xrandr-contents (replace-regexp-in-string "\\(.* connected\\).*\n\\|.*\n" "\\1" xrandr-contents))
       ;; Find the string only of the monitors that are connected according to xrandr and remove all other text.
       (remove "" (split-string xrandr-contents " connected"))))

   (defun exwm-monitors-format ()
     "Formats the list from xrandr-find-monitor-names to apply EXWM workspaces"
     (let ((monitors (xrandr-find-monitor-names)) (counter 0) (return-value nil))
       (while monitors
         (push counter return-value)
         (push (car monitors) return-value)
         (setq counter (+ counter 1))
         (setq monitors (cdr monitors)))
       (nreverse return-value)))

   (setq switch-to-buffer-obey-display-actions t)
   (defvar exwm-is-running nil)
   (call-process "sh" nil "*window-manager*" nil "-c" "wmctrl -m; echo $status")

   (when (and
          (get-buffer "*window-manager-error*") ;; The shell command has to both encounter an error and a running in an X environment.
          (eq window-system 'x))
     (setq exwm-is-running t)


     (setq battery-mode-line-format "⟨%b%p%%⟩ ")
     (unless (equal "Power N/A, battery unknown (N/A% load, remaining time N/A)" (battery))
       (display-battery-mode 1))
     (setq display-time-day-and-date t)

     ;; Changes the name of EXWM-buffers to the corresponding window-name rather than *EXWM*<N>.
     (add-hook 'exwm-update-class-hook
               (lambda ()
                 (exwm-workspace-rename-buffer exwm-class-name)))

     ;; Configure monitors
     (require 'exwm-randr)
     (setq exwm-randr-workspace-monitor-plist (exwm-monitors-format))
     (setq exwm-workspace-number (length (xrandr-find-monitor-names)))
     (shell-command "bash ~/.screenlayout/desktop.sh")
     (setq exwm-workspace-number (/ (length (exwm-monitors-format)) 2))      
     (exwm-randr-enable)

     ;; These  keys will always be sent to EXWM rather than to the X window.
     (setq exwm-input-prefix-keys
           '(?\C-x
             ?\C-g
             ?\M-x
             ?\M-z))

     ;; Sends the key after C-q directly to the X window.
     ;; (define-key exwm-mode-map [?\C-q] 'exwm-input-send-next-key)

     (setq exwm-input-global-keys
           `(
             ([?\s-r] . exwm-reset)
             ([s-left] . windmove-left)
             ([s-right] . windmove-right)
             ([s-up] . windmove-up)
             ([s-down] . windmove-down)
             ([?\s-w] . exwm-workspace-switch)
             ([?\C-q] . exwm-input-send-next-key)
             ([?\s-a] . (lambda (command)
                          (interactive (list (read-shell-command " λ ")))
                          (start-process-shell-command command nil command)))
             ([?\s-w] . exwm-workspace-switch)
             ([?\s-u] . (lambda ()
                          (interactive)
                          (shell-command "brightnessctl --quiet --min-value set +10")))
             ([?\s-d] . (lambda ()
                          (interactive)
                          (shell-command "brightnessctl --quiet --min-value set 10-")))
             ))
     ;; Actually starts EXWM
     (exwm-enable))

   (when (get-buffer "*window-manager*")
     (kill-buffer "*window-manager*"))
   (when (get-buffer "*window-manager-error*")
     (kill-buffer "*window-manager-error*"))))

General visual elements

Visible bell changes the otherwise quite jarring bell sound into a visual flash on it top and bottom of the Emacs frame. prettify-symbols-mode allows certain major modes to change the appearance of strings, the classic example being the Greek letter lambda in lisp-modes for lambda calculus. pixel-scroll-precision-mode allows you to scroll past things like images without buffers jumping around all the time.

(setq visible-bell t)
(add-hook 'after-init-hook
          (lambda () (setq global-prettify-symbols-mode 1
                           pixel-scroll-precision-mode 1)))

(add-hook 'prog-mode-hook 'display-line-numbers-mode)
(add-hook 'prog-mode-hook '(lambda () (toggle-truncate-lines 1)))  
(setq pixel-scroll-precision-mode t)
(setq scroll-margin 2)

(setq comint-scroll-to-bottom-on-input t
      comint-scroll-to-bottom-on-output nil
      redisplay-skip-fontification-on-input t)

Formats tabs to Linux-kernel standards and keeps them so using the aggressive-indent package.

(setq-default tab-width 8)
(setq-default standard-indent 8)
(setq-default indent-tabs-mode nil)

(use-package aggressive-indent
  :hook prog-mode
  :diminish aggressive-indent-mode)

Creates ample spacing around elements such as between buffers, around the mode line, and the edges of the screen.

(use-package spacious-padding
  :defer t
  :diminish spacious-padding-mode
  :init
  (spacious-padding-mode)
  :config
  (setq spacious-padding-widths
        '(:internal-border-width
          15
          :header-line-width
          5
          :mode-line-width
          10
          :right-divider-width
          5
          :fringe-width
          10))
  (if (display-graphic-p)
      (setq spacious-padding-subtle-frame-lines t)
    (setq spacious-padding-subtle-frame-lines
          '(:mode-line-active org-hide
                              :mode-line-inactive org-hide))))

(setq modus-themes-italic-constructs t)
(setq modus-themes-bold-constructs t)

Without this setting Emacs sometimes asks for confirmation via a “Yes or no” prompt, and sometimes “y or n”. This is generally difficult to predict, and so this setting forces the message to always send “y or n” forms; like most programs run in a terminal.

(defalias 'yes-or-no-p 'y-or-n-p)

The default Emacs mode-line is a bit busy and certain elements of it are difficult to intuitively understand. This simplifies it considerably to make it more readable and also adds a header line.

(defun mode-line-padding ()
  "Sets the spacing between left and right aligned things in the mode line."
  (let ((r-length (length (format-mode-line mode-line-end-spaces))))
    (propertize " "
                'display `(space :align-to (- right ,r-length)))))

(setq-default
 mode-line-format
 '("%e"
   (:eval (unless (string-match-p "\\*.*\\*" (buffer-name))
            (let* ((read-only (and buffer-read-only (buffer-file-name)))
                   (modified (buffer-modified-p)))
              (propertize
               (if (or buffer-read-only
                       (not (buffer-file-name))) ""
                 (if modified ""
                   ""))))))
   mode-name
   " "
   mode-line-buffer-identification
   mode-line-format-right-align
   (:eval (propertize (or vc-mode "") 'face 'org-tag))
   " "
   (:eval (setq mode-line-end-spaces mode-line-misc-info))))

Adds as nicely formatted clock in all cases, even when not running in EXWM.

(setq display-time-default-load-average nil)
(setq display-time-24hr-format t)

(display-time-mode)

When prose for publication the traditional centred appearance of WYSISWYG is quite comfortable. The olivetti package does this by centreing the contents of the windows where this type of content is displayed.

(use-package olivetti
  :hook
  (org-mode markdown-mode eww-mode message-mode elfeed-show-mode)
  :config
  (setq-default olivetti-body-width (+ fill-column 7))
  (add-hook 'olivetti-mode-hook
            (lambda () (set-face-attribute
                        'olivetti-fringe nil
                        :inherit 'default
                        :background 'unspecified
                        :foreground 'unspecified))))

rainbow-delimiters differentiates layers of parentheses using different colours so that they can be identified at a glance.

(use-package rainbow-delimiters
  :hook prog-mode)

smartparens is intended to help in a similar way by highlighting the current s-expression.

(use-package smartparens
  :bind (("M-DEL" . sp-backward-delete-word))
  :hook (prog-mode text-mode markdown-mode org-mode)
  :config
  (require 'smartparens-config))

Adds little icons for completion frameworks.

(use-package svg-lib)
(use-package kind-icon
  :after corfu
  :custom (kind-icon-default-face 'corfu-default)
  :config
  (add-to-list 'corfu-margin-formatters #'kind-icon-margin-formatter)
  (unless (display-graphic-p)
    (setq kind-icon-use-icons nil)))

Emacs is a wonderful alternative to a terminal, encompassing many of the features seen in modern terminals. For a cleaner look, this hides the mode-line in windows used to interact with shells.

(use-package hide-mode-line
  :hook
  (eat-mode . hide-mode-line-mode)
  (term-mode . hide-mode-line-mode)
  (eshell-mode . hide-mode-line-mode)
  (ghostel-mode . hide-mode-line-mode)
  (dashboard-mode . hide-mode-line-mode)
  (pdf-view-mode . hide-mode-line-mode))

Rainbow-mode makes colour codes into the colours that they represent.

(use-package rainbow-mode
  :config
  (setq rainbow-html-colors 'auto)
  (setq rainbow-latex-colors 'auto)
  (setq rainbow-ansi-colors 'auto)
  (setq rainbow-r-colors 'auto)
  :hook (prog-mode . rainbow-mode))

Dashboard

Configures the all-important Emacs dashboard that shows up on startup.

(use-package dashboard
  :init
  (dashboard-setup-startup-hook)
  :config
  (setq dashboard-icon-type 'all-the-icons)
  (setq dashboard-banner-logo-title "Welcome to Kudu Emacs!")
  (setq dashboard-center-content 'middle)
  (setq dashboard-startup-banner
        (if (window-system)
            Kudu-gui-logo
          "~/.emacs.d/Logos/KuduLogo_text.txt"))
  (setq compilation-ask-about-save nil)
  (setq dashboard-show-shortcuts nil)
  (setq dashboard-set-navigator nil)
  (setq dashboard-set-init-info t)
  (setq dashboard-set-footer nil)

  (add-hook  'dashboard-mode-hook (lambda ()
                                    (display-line-numbers-mode -1)
                                    (setq cursor-type nil)))
  
  (defvar kudu/dashboard-tools
    '(("Email"    "m" mu4e)
      ("Elfeed"   "e" elfeed)
      ("Dired"    "f" dired)
      ("Scratch"  "s" scratch-buffer)
      ("Notes"    "n" vulpea-find)
      ("Agent"    "p" agent-shell)))

  (defun kudu/dashboard-button-action (button)
    (let ((command (button-get button 'kudu/dashboard-command)))
      (if (commandp command)
          (call-interactively command)
        (user-error "Invalid Dashboard command: %S" command))))

  (defun kudu/dashboard-insert-tools (_list-size)
    (dolist (tool kudu/dashboard-tools)
      (pcase-let ((`(,label ,key ,command) tool))
        (define-key dashboard-mode-map (kbd key) command)
        (insert (format "%-18s " label))
        (insert-button
         (format "[%s]" key)
         'action #'kudu/dashboard-button-action
         'kudu/dashboard-command command
         'follow-link t)
        (insert "\n"))))

  (add-to-list
   'dashboard-item-generators
   '(tools . kudu/dashboard-insert-tools))

  (setq dashboard-items
        '((tools . 1))))

Completion

Corfu

In-buffer code completion using corfu. By default corfu only works in a GUI environment, but the corfu-terminal package allows for use when run using the -nw flag.

(use-package corfu
  :hook (prog-mode text-mode)
  :custom
  (setq corfu-auto t)
  :config
  (global-corfu-mode)
  (corfu-history-mode)
  (setq corfu-popupinfo-delay 0.5)
  (corfu-popupinfo-mode +1))

(use-package corfu-terminal
  :after corfu
  :config
  (unless (display-graphic-p)
    (corfu-terminal-mode +1)))

Cape

corfu does not provide candidates for completion, but this is provided by cape, or the Completion At Point Extensions package.

(use-package cape
  :defer 0.1
  :config
  (setq cape-dict-file (list "/home/jovo/Documents/Papers/english-dict" "/home/jovo/Documents/Papers/swedish-dict")
        cape-dabbrev-buffer-function #'cape-text-buffers)
  (add-hook 'completion-at-point-functions #'cape-history)
  (add-hook 'completion-at-point-functions #'cape-file)
  (add-hook 'completion-at-point-functions #'cape-programming)
  (add-hook 'completion-at-point-functions #'cape-language)
  (add-hook 'completion-at-point-functions #'cape-abbrev)
  
  (defun cape-language (&optional interactive)
    (interactive (list t))
    (if interactive
        (cape-interactive #'cape-language)
      (cape-wrap-super #'cape-dabbrev #'cape-dict)))

  (defun cape-programming (&optional interactive)
    (interactive (list t))
    (if interactive
        (cape-interactive #'cape-programming)
      (cape-wrap-super #'cape-keyword #'cape-elisp-symbol)))

  (defun cape-all (&optional interactive)
    (interactive (list t))
    (if interactive
        (cape-interactive #'cape-language)
      (cape-wrap-super #'cape-programming #'cape-language)))

  (defun kudu/exclusive-no (orig-fun)
    "Adds :exclusive 'no to ORIG-FUN so that global
'completion-at-point-functions' can run."
    (when-let* ((res (funcall orig-fun)))
      (append res (list :exclusive 'no))))

  (advice-add 'elisp-completion-at-point :around #'kudu/exclusive-no)
  
  (setopt text-mode-ispell-word-completion nil))

Spellcheck

In a sense, spellcheck is also a form of automatic completion. For this we use the jinx package for easy multi-language spelling.

(use-package jinx
  :hook text-mode
  :bind ("C-." . jinx-correct-nearest)
  :config
  (global-jinx-mode)
  (setq jinx-languages "en_GB sv_SE"))

Minibuffer Completion

Uses vertico to show minibuffer completion, and marginalia and orderless to format it.

(use-package vertico
  :init
  (vertico-mode)
  :config
  (setq vertico-count 10)
  (vertico-indexed-mode)
  (vertico-mouse-mode))

(use-package marginalia
  :hook emacs-startup
  :config
  (setq marginalia-separator " | ")
  (setq marginalia-align 'center))

(use-package orderless
  :custom
  (completion-styles '(orderless basic prescient))
  (completion-category-overrides '((file (styles basic partial-completion)))))

Prescient

Shows those completion results that are hopefully most useful, both in the minibuffer and the main buffer.

(use-package prescient
  :after vertico
  :config
  (setq prescient-persist-mode t)
  (setq prescient-history-length 5)
  (setq prescient-sort-full-matches-first t))

(use-package corfu-prescient
  :hook global-corfu-mode)

(use-package vertico-prescient
  :hook vertico-mode
  :config
  (setq vertico-prescient-enable-filtering nil))

Etags-regen

This automatically regenerates the TAGS file containing information about the structure of programming projects for many languages.

(use-package etags-regen
  :after xref
  :config
  (setq etags-regen-ignores
        '("*.pyc" ".git" ".venv" "venv" "node_modules"))
  (etags-regen-mode 1))

Consult

consult provides various functions that integrates with the completion API.

(use-package consult
  :bind (;; C-c bindings in `mode-specific-map'
         ("C-c M-x" . consult-mode-command)
         ("C-c h" . consult-history)
         ("C-c k" . consult-kmacro)
         ("C-c m" . consult-man)
         ("C-c i" . consult-info)
         ([remap Info-search] . consult-info)
         ;; C-x bindings in `ctl-x-map'
         ("C-x M-:" . consult-complex-command)     ;; orig. repeat-complex-command
         ("C-x b" . consult-buffer)                ;; orig. switch-to-buffer
         ("C-x 4 b" . consult-buffer-other-window) ;; orig. switch-to-buffer-other-window
         ("C-x 5 b" . consult-buffer-other-frame)  ;; orig. switch-to-buffer-other-frame
         ("C-x t b" . consult-buffer-other-tab)    ;; orig. switch-to-buffer-other-tab
         ("C-x r b" . consult-bookmark)            ;; orig. bookmark-jump
         ("C-x p b" . consult-project-buffer)      ;; orig. project-switch-to-buffer
         ;; Custom M-# bindings for fast register access
         ("M-#" . consult-register-load)
         ("M-'" . consult-register-store)          ;; orig. abbrev-prefix-mark (unrelated)
         ("C-M-#" . consult-register)
         ;; Other custom bindings
         ("M-y" . consult-yank-pop)                ;; orig. yank-pop
         ;; M-g bindings in `goto-map'
         ("M-g e" . consult-compile-error)
         ("M-g f" . consult-flymake)               ;; Alternative: consult-flycheck
         ("M-g g" . consult-goto-line)             ;; orig. goto-line
         ("M-g M-g" . consult-goto-line)           ;; orig. goto-line
         ("M-g o" . consult-outline)               ;; Alternative: consult-org-heading
         ("M-g m" . consult-mark)
         ("M-g k" . consult-global-mark)
         ("M-s I" . consult-imenu)
         ("M-s i" . consult-imenu-multi)
         ;; M-s bindings in `search-map'
         ("M-s f" . consult-fd)
         ("M-s c" . consult-locate)
         ("M-s G" . consult-git-grep)
         ("M-s r" . consult-ripgrep)
         ("M-s l" . consult-line)
         ("M-s L" . consult-line-multi)
         ("M-s k" . consult-keep-lines)
         ("M-s u" . consult-focus-lines)
         ;; Isearch integration
         ("M-s e" . consult-isearch-history)
         :map isearch-mode-map
         ("C-h" . consult-isearch-history)         ;; orig. isearch-edit-string
         ("M-s e" . consult-isearch-history)       ;; orig. isearch-edit-string
         ("M-s l" . consult-line)                  ;; needed by consult-line to detect isearch
         ("M-s L" . consult-line-multi)            ;; needed by consult-line to detect isearch
         ("C-m" . consult-imenu)
         ;; Minibuffer history
         :map minibuffer-local-map
         ("M-s" . consult-history)                 ;; orig. next-matching-history-element
         ("M-r" . consult-history)
         :map vertico-map
         ("C-s" . consult-fd))
  :config
  (advice-add #'register-preview :override #'consult-register-window)
  (setq xref-show-xrefs-function #'consult-xref
        xref-show-definitions-function #'consult-xref)

  (setq consult-async-refresh-delay 0
        consult-async-input-throttle 0
        consult-async-input-debounce 0)
  (consult-customize
   consult-theme :preview-key '(:debounce 0.2 any)
   consult-ripgrep consult-git-grep consult-grep
   consult-bookmark consult-recent-file consult-xref
   ;; :preview-key "M-."
   :preview-key '(:debounce 0.4 any)))

To remember and find all these commands, we use the built-in which-key package.

(use-package which-key
  :config
  (setq which-key-mode t))

Flycheck

Tangentially related is Flycheck, providing in-buffer syntax checking.

(use-package flycheck
  :after cape
  :config
  (setq flycheck-global-modes '(not lisp-interaction-mode))
  (global-flycheck-mode)
  (global-flycheck-eglot-mode 1))

Org-mode

Configures Org-mode to make it more attractive and usable.

(setq completion-cycle-threshold 2)
(setq tab-always-indent 'complete)

(use-package org
  :defer 18
  :config
  (setq org-format-latex-options
        (plist-put org-format-latex-options
                   :scale 1.3
                   ))
  (setq org-format-latex-options
        (plist-put org-format-latex-options
                   :html-scale 3
                   ))
  (setq org-startup-indented t
        org-toggle-pretty-entities t
        org-hide-leading-stars t
        org-hide-emphasis-markers t
        org-src-preserve-indentation t
        org-export-with-broken-links t)
  (add-hook 'text-mode-hook 'turn-on-visual-line-mode)
  (add-to-list 'org-latex-packages-alist
               '("AUTO" "babel" t ("pdflatex" "xelatex" "lualatex")))

  (defun kudu/org-latex-small-caps (text backend _info)
    "Wrap runs of two or more uppercase letters in LaTeX small caps."
    (when (org-export-derived-backend-p backend 'latex)
      (let ((case-fold-search nil))
        (with-temp-buffer
          (insert text)
          (goto-char (point-min))
          (while (re-search-forward
                  "[[:upper:]][[:upper:][:digit:]]*"
                  nil t)
            (let ((match (match-string-no-properties 0)))
              (when (>= (length match) 2)
                (replace-match
                 (concat "\\textsc{" (downcase match) "}")
                 t
                 t))))
          (buffer-string)))))

  (defun kudu/org-latex-chinese (text backend _info)
    "Wrap runs of Chinese characters in \\zh{} for LaTeX export."
    (when (org-export-derived-backend-p backend 'latex)
      (with-temp-buffer
        (insert text)
        (goto-char (point-min))
        ;; Add more characters/ranges here if your text requires them.
        (let ((chinese-re "[一-龥々〆〇ヶ]+"))
          (while (re-search-forward chinese-re nil t)
            (let ((match (match-string-no-properties 0)))
              (replace-match
               (format "\\zh{%s}" match)
               t
               t))))
        (buffer-string))))

  (add-hook 'org-export-filter-plain-text-functions
            #'kudu/org-latex-chinese)
  (add-hook 'org-export-filter-plain-text-functions
            #'kudu/org-latex-small-caps))

(use-package org-superstar
  :hook (org-mode . org-superstar-mode))
(use-package org-fragtog
  :hook (org-mode . org-fragtog-mode))
(use-package toc-org
  :hook (org-mode . toc-org-mode))
(use-package org-appear
  :hook (org-mode . org-appear-mode))
(use-package yasnippet
  :diminish yas-minor-mode
  :hook (org-mode . yas-minor-mode)
  :config
  (yas-load-directory "~/.emacs.d/snippets/"))
(use-package yasnippet-snippets)

(use-package org-modern
  :hook
  (org-mode . org-modern-mode)
  (org-agenda-finalize . org-modern-agenda)
  :custom
  (org-modern-table nil)
  (org-modern-star nil)
  (org-modern-hide-stars nil)
  (org-modern--table nil))

(setq org-export-with-email t)
(setq org-export-with-smart-quotes t)
(setq org-export-with-section-numbers nil)
(setq org-export-with-statistics-cookies nil)
(setq org-export-with-broken-links t)
(setq sentence-end-double-space nil)

(unless (file-directory-p "~/.emacs.d/site-lisp/org-modern-indent")
  (async-shell-command "git clone https://github.com/jdtsmith/org-modern-indent.git ~/.emacs.d/site-lisp/org-modern-indent/"))

(use-package org-modern-indent
  :load-path "~/.emacs.d/site-lisp/org-modern-indent"
  :hook (org-mode . org-modern-indent-mode))

(defun org-title-to-buffer-name ()
  "Rename file-visiting Org buffers to their #+TITLE."
  (interactive)
  (when-let ((title (and buffer-file-name
                         (org-get-title))))
    (rename-buffer title t)))

(add-hook 'org-mode-hook 'org-title-to-buffer-name)

(use-package textui
  :vc (:url "https://github.com/yibie/textui"))

(use-package org-table-widget
  :vc (:url "https://github.com/yibie/org-table-widget")
  ;; :hook org-mode
  )

Org-roam

(use-package org-roam
  :config
  (setq org-roam-directory "~/Documents/Notes/roam/")
  (setq org-roam-node-display-template (concat "${title:*} " (propertize "${tags:10}" 'face 'org-tag))))

(use-package org-roam-ui
  :after org-roam
  :config
  (setq org-roam-ui-follow t
        org-roam-ui-update-on-save t))

(defun get-roam-node-count ()
  (interactive)
  (message "Number of nodes: %s" (length (org-roam-db-query [:select [id] :from nodes]))))

Vulpea

Vulpea is a faster (and async) alternative to org-roam that can use the same database and can coexist with org-roam. consult-vulpea allows live previews when selecting notes.

(use-package vulpea
  :bind (("M-o" . vulpea-find)
         ("C-c M-i" . vulpea-insert))
  :config
  (setq vulpea-db-sync-directories
        '("~/Documents/Notes/roam/"
          "~/Documents/blog/posts/"
          "~/Documents/blog/drafts/"))
  (setq vulpea-db-index-plain-links nil
        vulpea-db-sync-verbose nil
        vulpea-db-parse-method 'single-temp-buffer
        vulpea-db-async-extraction 'full)

  (vulpea-db-autosync-mode +1))

(use-package consult-vulpea
  :after vulpea
  :custom
  (consult-vulpea-mode +1))

(use-package vulpea-ui
  :after vulpea
  :custom
  (vulpea-ui-fast-parse t))

Lisp

Emacs is an amazing environment for writing in various lisp dialects, with wonderful support out-of-the-box. However, there are various different packages designed to improve this experience in general or in slight, specific ways. Sly is a fork of the popular SLIME package for an integrated common lisp REPL among other things. It is superior to SLIME because it has ASCII-art cats.

(use-package sly
  :config
  (setq inferior-lisp-program "sbcl --dynamic-space-size 4Gb"))

(setq show-paren-delay 0)
(show-paren-mode)

Scheme

Due to Kudu’s deep integration with the GNU Guix system, it is only natural to improve the systems used to interact with guile and scheme specifically. For this the guix.el and the wonderful geiser packages are used, where guix.el is a magit-inspired Emacs frontend and geiser is a package aiming to improve the scheme experience in Emacs, with geiser-x providing special support for working the relevant flavour of scheme.

(use-package guix)

(use-package geiser-guile)
(use-package geiser-chez)
(use-package geiser-mit)
(use-package geiser-chicken)
(use-package geiser-racket)
(use-package geiser-chibi)
(use-package geiser-kawa)
(use-package geiser-gambit)
(use-package geiser-stklos)
(use-package geiser-gauche)

Parens pairing

Most of the time when writing parentheses, brackets, and quotes we want to pair them. This significantly improves comfort since you no longer need to stretch for modifier keys to finish of the pair. And even if you do, electric-pair-mode will detect it and move the point past as if you had just entered the character. This is of course not just useful for lisp, but in any context when writing pairs of brackets or parentheses.

(setq electric-pair-pairs '((?\{ . ?\})
                            (?\( . ?\))
                            (?\[ . ?\])
                            (?\" . ?\")))
(electric-pair-mode t)

File management

Dired is Emacs’ built in text-based file manager. It’s however pretty rough around its edges, such as it opening each directory in a separate buffer making navigation a hassle. However certain tweaks can make it a formidable tool accessible directly within Emacs. Take that and midnight commander!

(use-package dired-open-with
  :preface
  (defun dired-open-with-find-file (orig-fun &optional other-window)
  "Run dired-open-with if the file is a PDF, image, or video."
  (let ((file (dired-get-filename)))
    (if (and file
             (or (string-match "\\.pdf\\'" file)
                 (string-match "\\.png\\'" file)
                 (string-match "\\.jpg\\'" file)
                 (string-match "\\.jpeg\\'" file)
                 (string-match "\\.mkv\\'" file)
                 (string-match "\\.mp4\\'" file)
                 (string-match "\\.avi\\'" file)))
        (dired-open-with)
      (funcall orig-fun))))  ;; Call the original function for other file types.
  :init
  (advice-add 'dired-find-file :around 'dired-open-with-find-file))

(setf dired-kill-when-opening-new-dired-buffer t)
(setq dired-listing-switches "-BhlD  --group-directories-first")
(defalias 'eaf-open-in-file-manager #'dired)

(add-hook 'dired-mode-hook 'toggle-truncate-lines)
(add-hook 'dired-mode-hook 'display-line-numbers-mode)

(setq delete-by-moving-to-trash t)

(use-package all-the-icons-dired
  :hook (dired-mode . all-the-icons-dired-mode)
  :config (setq all-the-icons-dired-monochrome nil))

(use-package lin
  :config
  (lin-global-mode))

PDF-tools

The default docview mode of viewing PDFs is quite bad, and is improved immensely by the pdf-tools package. For some this may not be enough, and it is possible to replace it with an external pdf viewer (like evince or zathura) using the above openwith package.

(use-package pdf-tools
  :config
  (pdf-loader-install)
  (add-hook  'pdf-view-mode-hook (lambda () (display-line-numbers-mode -1))))

Version Control

Magit is wonderful, and one of the killer apps that makes Emacs a system than other editors or IDEs. However it is not installed by default, so it is defined here.

(use-package magit
  :config
  (setq magit-process-find-password-functions
        '(magit-process-password-auth-source)
        magit-process-connection-type 'pty
        magit-log-section-commit-count 20)
  
  (add-hook 'git-commit-setup-hook
            (lambda ()
              (local-set-key (kbd "TAB") #'completion-at-point))))

(use-package magit-todos
  :after magit
  :config (magit-todos-mode 1))

(use-package magit-delta
  :after magit
  :config (magit-delta-mode t))

(use-package forge
  :after magit
  :hook (magit-status-mode-hook . forge-pull))

LLMs

Artificial intelligence tools based on Large Language Models (LLMs) are quite popular these days. There’s no reason to pop out to a web browser or terminal (running something like Claude code) to use LLMs in your workflow if you’re using Emacs; Emacs is naturally a perfect fit for handling the editing and generation of text.

There are a multitude of LLM packages, but one of the most polished ones is gptel. Remember to set gptel-api-key and gptel-model in $HOME/.emacs.d/secrets/secret.org if you want to use this functionality. agent-shell is a good alternative for managing not just simple conversations, but full-blown agentetic workflows (like interacting with claude-code or the free-software pi).

(use-package gptel
  :bind (("C-c C-m" . gptel-menu))
  :config
  (setq gptel-default-mode 'org-mode)
  (setq gptel-api-key 'gptel-api-key-from-auth-source)
  (setq gptel-track-media t)

  ;;; Gives the base gptel buffer the ability to search the web for
  ;;; infromation and documentation using the tools from 'gptel-agent'.

  (gptel-make-tool
   :name "WebSearch"
   :function 'gptel-agent--web-search-eww
   :description "Search the web for the first five results to a query.  The query can be an arbitrary string.  Returns the top five results from the search engine as a list of plists.  Each object has the keys `:url` and `:excerpt` for the corresponding search result.

This tool uses the Emacs web browser (eww) with its default search engine (typically DuckDuckGo) to perform searches. No API key is required.

If required, consider using the url as the input to the `Read` tool to get the contents of the url.  Note that this might not work as the `Read` tool does not handle javascript-enabled pages."
   :args '((:name "query"
                  :type string
                  :description "The natural language search query, can be multiple words.")
           (:name "count"
                  :type integer
                  :description "Number of results to return (default 5)"
                  :optional t))
   :include t
   :async t
   :category "gptel-agent")

  (gptel-make-tool
   :function #'gptel-agent--read-url
   :name "WebFetch"
   :description "Fetch and read the contents of a URL.

- Returns the text of the URL (not HTML) formatted for reading.
- Request times out after 30 seconds."
   :args '(( :name "url"
             :type "string"
             :description "The URL to read"))
   :async t
   :include t
   :category "gptel-agent"))

(use-package gptel-agent)
(use-package agent-shell
  :bind (("M-p" . agent-shell-hermes-start-agent))
  :config
  (setq agent-shell-anthropic-claude-environment
        (agent-shell-make-environment-variables :inherit-env t))

  (setq agent-shell-pi-environment
        (agent-shell-make-environment-variables "PATH" (concat "~/.npm-packages/bin" path-separator (getenv "PATH"))))

  (setq agent-shell-transcript-file-path-function nil)
  (defun shell-maker-welcome-message (config)
  "Return a welcome message to be printed using CONFIG."
  (format "")))

(use-package agent-shell-notifications
  :preface (add-to-list 'load-path "~/.emacs.d/elpa/agent-shell-notifications")
  :vc (:url "https://github.com/zackattackz/agent-shell-notifications/")
  :hook
  (agent-shell-mode . agent-shell-notifications-mode)
  (agent-shell-viewport-edit-mode . agent-shell-notifications-viewport-edit-mode)
  (agent-shell-viewport-view-mode . agent-shell-notifications-viewport-view-mode)
  :config
  (setq agent-shell-notifications-timeout -1)
  (setq agent-shell-notifications-idle-timeout 0))

Tooling

Emacs has a wonderful undo-system, but it can be hard to get an idea of how it works intuitively. vundo helps with this by creating a wonderful visualization for your branching undo tree.

(use-package vundo
  :bind (("C-x u" . vundo)))

Uses avy to allow jumping to text anywhere on the screen by writing a few short characters.

(use-package avy
  :bind (("C-<tab>" . avy-goto-char-2)))

Emacs is better than any terminal emulator, and being able to run Emacs lisp code in a terminal through eshell is incredibly powerful. But eshell has trouble running full-screen terminal programs, and so the eat (Emulate A Terminal) package is used for those programs that need it. For even more high-powered terminal needs, ghostel can run pretty much anything that your terminal emulator can.

(use-package eat
  :config
  (setq eat-kill-buffer-on-exit t)
  (keymap-set eat-mode-map "M-w" 'kill-ring-save))

(use-package ghostel
  :config (ghostel-eshell-visual-command-mode t))

(use-package fish-completion
  :hook
  (eshell-load-hook . load-fish-completion)
  :config
  (use-package bash-completion)
  (defun load-fish-completion ()
    (when (executable-find "fish")
      (add-hook 'eshell-load-hook (fish-completion-mode)))))

(use-package eshell
  :bind ("C-t" . eshell)
  :config
  ;; Create a new lambda-themed prompt.
  (setq eshell-prompt-function
        (lambda ()
          (concat (abbreviate-file-name (eshell/pwd)) " λ ")))
  (setq eshell-prompt-regexp "^[^λ]* λ")

  (setq eshell-scroll-to-bottom-on-input 'all)
  (setq-local tab-always-indent 'complete)
  (setq eshell-history-size 10000)
  (setq eshell-save-history-on-exit t)
  (setq eshell-hist-ignoredups t)

  (defalias 'eshell/clear 'eshell/clear-scrollback))

One of the things that make Emacs so extensible is the easy access to documentation of functions and variables through the C-h series of keybinds. The helpful package continues this by improving the default help buffers with more information and better formatting.

(use-package helpful
  :bind (("C-c C-d" . helpful-at-point)
         ("C-h v" . helpful-variable)
         ("C-h f" . helpful-callable)
         ("C-h x" . helpful-command)
         ("C-h k" . helpful-key)))

When using vertico and the minibuffer windows will sometimes jarringly move up to show point if it is far down. stillness-mode changes this so that the windows stay still and show the same content as the did before, except that covered by that minibuffer.

(use-package stillness-mode
  :hook vertico
  :config
  (stillness-mode))

Automatically compresses and decompresses files.

(auto-compression-mode)

Saves your completion history between sessions.

(savehist-mode)

A function for translating to-and from Chinese using gptel:

(defun zhongwen-translate ()
  (interactive)
  (let ((contents (if (use-region-p)
                      (buffer-substring-no-properties (region-beginning) (region-end))
                    (thing-at-point 'word))))
    (gptel-request contents
      :system "Write the character in pinyin and give a very short translation it into
English. If it is an English word, give a traditional Chinese
 translation instead. Give a structured output that looks like this:
  Character: X
  Pinyin:    X
  Meaning:   X"
      :callback (lambda (response info)
                  (message "%s" response)))))

(bind-key "C-x z" 'zhongwen-translate)
(bind-key "C-x C-z" 'zhongwen-translate)

Window management

(setq display-buffer-alist
      '(("\\*mu4e-update\\*"
         (display-buffer-at-bottom))
        ("magit-diff:.*"
         (display-buffer-at-bottom))
        ("\\*vundo tree\\*"
         (display-buffer-at-bottom)
         (window-height . 10))
        (".*"
         (display-buffer-same-window
          display-buffer-reuse-mode-window
          display-buffer-use-some-window
          display-buffer-use-least-recent-window
          display-buffer-reuse-window)
         nil)))

(setq display-buffer-base-action
      '((display-buffer-reuse-window
         display-buffer-use-some-window
         display-buffer-use-least-recent-window)))

(setq pop-up-windows nil)
(bind-key "C-x o" #'next-window-any-frame)

The winpulse package will quickly flash the window you are moving to, making it easy to see what window point is active in.

(use-package winpulse
  :vc (:url "https://github.com/xenodium/winpulse")
  :custom
  (winpulse-mode +1))

Web browsing

This custom function means that images rendered in Emacs’ Emacs Web Wowser (eww) will retain the same width as fill-column, and thus not extend outside the eww window.

(defun eww-limit-images-to-fill-column ()
  (let* ((window (get-buffer-window (current-buffer) t))
         (col-px (* fill-column
                    (if window
                        (window-font-width window)
                      (frame-char-width)))))
    (save-excursion
      (goto-char (point-min))
      (while-let ((match (text-property-search-forward
                          'display nil (lambda (_ v) (imagep v)))))
        (let* ((img (prop-match-value match))
               (size (image-size img t))
               (w (car size)))
          (when (and w (> w col-px))
            (setf (image-property img :scale)
                  (/ (float col-px) w))))))))

(add-hook 'eww-after-render-hook #'eww-limit-images-to-fill-column)

(setq shr-max-width fill-column)

(use-package shr-tag-pre-highlight
  :after eww
  :config
  (add-to-list 'shr-external-rendering-functions
               '(pre . shr-tag-pre-highlight)))

elfeed is a RSS-browser for emacs that integrates will with shr and eww to render posts.

(use-package elfeed
  :bind (:map elfeed-search-mode-map
         ("U" . elfeed-update))
  :config
  
  (defun elfeed-show-rename-to-title (&rest _)
  "Rename the elfeed-show buffer to the current entry's title."
  (interactive)
  (when-let ((entry (and (derived-mode-p 'elfeed-show-mode)
                         elfeed-show-entry))
             (title (elfeed-entry-title entry)))
    (rename-buffer title t)))

  (advice-add 'elfeed-show-entry :after #'elfeed-show-rename-to-title))

Functions

The sudo function raises the privilege of the current buffer to root permissions without having to close and open it again through TRAMP.

(defun sudo ()
  "Opens the current buffer at point with root privelages using TRAMP."
  (interactive)
  (let ((position (point)))
    (find-alternate-file (concat "/sudo::"
                                 (buffer-file-name (current-buffer))))
    (goto-char position)))

Magit can sometimes create a lot of buffers for different processes that are annoying to close one by one, this function closes all buffers whose name contains “magit”.

(defun kill-magit-buffers ()
  "Kills all buffers whose name begins with magit."
  (interactive)
  (mapc (lambda (buffer) 
          (if (buffer-match-p ".*magit.*" buffer) 
              (kill-buffer buffer))) 
        (buffer-list)))

Emacs does not have a nice easy to use elisp function for calculating the factorial of a value, this adds it. This works out particularly nicely since the standard notation for the factorial of a value uses prefix notation.

(defun ! (n)
  "An emacs function to calculate the factorial of n using the calc library."
  (let ((output (string-to-number (calc-eval (format "%s!" n)))))
    (kill-buffer "*Calculator*")
    output))

Function for calculation the number of possible permutations and combinations respectively.

(defun nPr (n k)
  "A function for calculating the number of permutations in combinatorics."
  (/
   (! n)
   (! (- n k))))

(defun nCr (n k)
  "A function for calculating the number of combinations in combinatorics."
  (/
   (! n)
   (* (! k) (! (- n k)))))

Allows you to save a flyspell word at point to your personal dictionary.

(defun flyspell-save-word ()
  (interactive)
  (let ((current-location (point))
        (word (flyspell-get-word)))
    (when (consp word)    
      (flyspell-do-correct 'save nil (car word) current-location (cadr word) (caddr word) current-location))))

(add-hook 'org-mode-hook (lambda ()
                           (keymap-set org-mode-map "C-c C-v"
                                       'flyspell-save-word)))
(defun load-emacs-secret-file ()
  "Load the .emacs.d/secrets/ file"
  (interactive)
  (org-babel-load-file (expand-file-name "~/.emacs.d/secrets/secret.org")))

Mail

Emacs provides many different mail utilities, but one of the most popular is mu4e (Mail Utilities for Emacs). Using this requires an IMAP server to something similar to store mail the in HOME/mail/ directory and a local SMTP server to send the mail.

(use-package mu4e
  :defer 10
  :ensure nil
  :hook
  (mu4e-compose-mode-hook . olivetti-mode)
  (mu4e-compose-mode-hook . mail-signature)
  (mu4e-view-mode-hook . olivetti-mode)
  :bind (("C-c s" . mml-secure-sign))
  :config
  
  (defalias 'email #'mu4e "Defines an email command to run mu4e.")
  (setq fill-flowed-encode-column 998)
  
  (setq mu4e-change-filenames-when-moving t ; avoid sync conflicts
        mu4e-update-interval (* 10 60) ; check mail every 10 minutes
        mu4e-compose-format-flowed t ; re-flow mail so it's not hard wrapped for clients that support it
        mu4e-get-mail-command "mbsync -a"
        mu4e-maildir "/mail/Inbox")

  (setq mail-user-agent 'mu4e-user-agent)
  (setq message-kill-buffer-on-exit t)

  (defun kudu/extract-email-username (email)
    "Extract the 'user' part of 'user@domain.tld' from EMAIL."
    (and (string-match "^\\([^@]+\\)@" email)
         (match-string 1 email)))
  
  (setq mu4e-drafts-folder
        (concat "/"
                (kudu/extract-email-username user-mail-address)
                "/Drafts")
        mu4e-sent-folder
        (concat "/"
                (kudu/extract-email-username user-mail-address)
                "/Sent")
        mu4e-refile-folder
        (concat "/"
                (kudu/extract-email-username user-mail-address)
                "/Archive")
        mu4e-trash-folder
        (concat "/"
                (kudu/extract-email-username user-mail-address)
                "/Trash"))

  (setq mu4e-maildir-shortcuts
        '(("/inbox"     . ?i)
          ("/Sent"      . ?s)
          ("/Trash"     . ?t)
          ("/Drafts"    . ?d)
          ("/All Mail"  . ?a)))

  (setq message-send-mail-function 'smtpmail-send-it
        smtpmail-smtp-server "127.0.0.1"
        smtpmail-smtp-service 1025
        smtpmail-stream-type  'ssl
        mml-secure-openpgp-sign-with-sender t
        mml-secure-openpgp-encrypt-to-self t
        mail-signature-file "~/Documents/Papers/signature.txt")

  (defun kudu/hide-mu4e-update-modeline (&rest _)
    "Hides the mode-line in '*mu4e-updates*'-buffers."
    (when (buffer-live-p mu4e--update-buffer)
      (with-current-buffer mu4e--update-buffer
        (unless (derived-mode-p 'mu4e--update-mail-mode)
          (mu4e--update-mail-mode))
        (hide-mode-line-mode 1))))

  (advice-add 'mu4e--update-mail-and-index-real
              :after #'kudu/hide-mu4e-update-modeline)

  (setq mu4e-split-view 'vertical)

  (setq mu4e-headers-visible-columns 100)

  (defun set-mu4e-context (context-name full-name mail-address)
    "Return a mu4e context named CONTEXT-NAME with :match-func matching
      folder name CONTEXT-NAME in Maildir. The context's
      `user-mail-address', `user-full-name' and
      `mu4e-compose-signature'`smtpmail-smpt-server' is set to
      MAIL-ADDRESS FULL-NAME SIGNATURE and SERVER respectively.  Special
      folders are set to context specific folders."
    (let ((dir-name (concat "/" context-name)))
      (make-mu4e-context
       :name context-name
       ;; we match based on the maildir of the message
       :match-func
       `(lambda (msg)
          (when msg
            (string-match-p
             ,(concat "^" dir-name)
             (mu4e-message-field msg :maildir))))
       :vars
       `((user-mail-address    . ,mail-address)
         (user-full-name       . ,full-name)
         (mu4e-sent-folder     . ,(concat "/"
                                          (kudu/extract-email-username user-mail-address)
                                          "/Sent"))
         (mu4e-drafts-folder   . ,(concat "/"
                                          (kudu/extract-email-username user-mail-address)
                                          "/Drafts"))
         (mu4e-trash-folder    . ,(concat "/"
                                          (kudu/extract-email-username user-mail-address)
                                          "/Trash"))
         (mu4e-refile-folder   . ,(concat "/"
                                          (kudu/extract-email-username user-mail-address)
                                          "/Archive"))))))

  ;;Fixing duplicate UID errors when using mbsync and mu4e
  (setf mu4e-change-filenames-when-moving t)

  (setf mu4e-maildir-shortcuts
        `((,(concat "/" (kudu/extract-email-username user-mail-address)
                    "/Inbox")
           . ?f)
          (,(concat "/" (kudu/extract-email-username user-mail-address)
                    "/Drafts")
           . ?d)
          (,(concat "/" (kudu/extract-email-username user-mail-address)
                    "/Sent")
           . ?s)))

  ;; To set your email, enter the values of these
  
  ;; (setf mu4e-contexts
  ;;       (list
  ;;        (set-mu4e-context "context name"
  ;;                          "First-name Last-name"
  ;;                          "username1@email-provider.tld")
  ;;        (set-mu4e-context "context name"
  ;;                          "First-name Last-name"
  ;;                          "username2@other-provider.tld")))

  (setf message-send-mail-function 'smtpmail-send-it
        mu4e-compose-signature ""
        mu4e-compose-context-policy 'ask)


  (defun kudu/mu4e-main-clean-version ()
    "Remove the '- mu for emacs version' text from the mu4e main view."
    (when-let ((buf (get-buffer mu4e-main-buffer-name)))
      (with-current-buffer buf
        (setq buffer-read-only nil)
        (save-excursion
          (goto-char (point-min))
          (when (re-search-forward
                 "mu4e - mu for emacs version \\(.*\\)" nil t)
            (delete-region
             (line-beginning-position)
             (line-end-position))))
        (setq buffer-read-only t))))

  (advice-add 'mu4e--main-redraw :after #'kudu/mu4e-main-clean-version))

Allows notify-send to inform you of new emails when not using Emacs, and adds a nice mode-line decoration to remind you.

(use-package mu4e-alert
  :after 'mu4e
  :config
  (mu4e-alert-set-default-style 'libnotify)
  (mu4e-alert-enable-notifications)
  (setq mu4e-alert-enable-mode-line-display 1))

Allows you to write nicely formatted HTML emails using org-mode to fit in with the rest of society.

(use-package org-msg
  :after mu4e
  :config
  (setq mail-user-agent 'mu4e-user-agent)
  (setq org-msg-options "html-postamble:nil H:5 num:nil ^:{} toc:nil author:nil email:nil \\n:t"
        org-msg-startup "hidestars indent inlineimages"
        org-msg-greeting-fmt
        "\n#+begin_greeting\nDear%s,\n#+end_greeting\n\n"
        org-msg-default-alternatives
        '((new		. (text))
          (reply-to-html	. (text html))
          (reply-to-text	. (text)))
        org-msg-convert-citation t)
  (if (file-exists-p "~/Documents/email.css")
      (setq org-msg-enforce-css
            (org-msg-css-file-to-list "~/Documents/email.css")))
  (org-msg-mode t))

Encryption

Emacs comes with a suite of tools to handle encrypted files and text through the Easy Privacy Guard (EasyPG, or simply EPG) package. We can use this to handle various secrets and to encrypt email messages.

By default Emacs uses the systems own GUI pinentry program for entering passwords when encrypting, decrypting, or singing things. This does not work when interacting with Emacs over SSH or in a TTY, but in those cases we can use Emacs itself to enter passwords.

(unless window-system
  (setq epg-pinentry-mode 'loopback))

This provides functionality to easily manage encrypted files using age in a way similar to how the built-in epg works. Change the identity (private) and recipient (public) keys to whatever location you want to use.

(use-package age
  :defer 5
  :custom
  (age-program "rage")
  (age-default-identity "~/.config/age/key.txt")
  (age-default-recipient
   '("~/.ssh/age_yubikey.pub"
     "~/.ssh/age_backup.pub"))
  :config
  (age-file-enable))

Emacs uses the first file found in this list to store various passwords. For examples, see the earlier function magit-process-password-auth-source or the value 'gptel-api-key-from-auth-source where magit and gptel respectively searches the auth-sources file for passwords and authentication information. Other uses might be email, SSH, or interacting with Emacs’ TRAMP package. If you use the encrypted .authinfo.gpg or -~.age~ files Emacs will automatically ask for you to decrypt it when trying to get its contents.

(setq auth-sources '("~/.authinfo.age" "~/.authinfo.gpg" "~/.authinfo" "~/.netrc"))

This series of functions interact with the mu4e package configured earlier to automatically detect when messages can be encrypted, and otherwise signs them. It also checks if correspondents’ keys have been uploaded via Web Key Directory, and if so imports them.

(defun kudu/message-recipients ()
  "Return a list of all recipients in the message, looking at TO, CC and BCC.
Each recipient is in the format of `mail-extract-address-components'."
  (mapcan (lambda (header)
            (let ((header-value (message-fetch-field header)))
              (and
               header-value
               (mail-extract-address-components header-value t))))
          '("To" "Cc" "Bcc")))

(defun kudu/message-locate-keys ()
  "Tries to find the public keys of 'kudu/message-recipients' via WKD through --locate-keys."
  (interactive)
  (dolist (recipient (string-to-list (kudu/message-recipients)))
    (let ((recipient-email (cadr recipient))
          (proc
           (make-process
            :name "gpg-locate-keys"
            :command (list epg-gpg-program
                           "--no-tty"
                           "--locate-keys"
                           (cadr recipient))
            :connection-type 'pipe
            :filter (lambda (proc string)
                      (process-put proc
                                   'output
                                   (concat (or (process-get proc 'output)
                                               "")
                                           string)))
            :sentinel (lambda (proc event)
                        (when (eq (process-status proc) 'exit)
                          (let ((output (process-get proc 'output))
                                (email (process-get proc 'email)))
                            (cond
                             ((and output (string-match-p "imported: [1-9]" output))
                              (message "Public key imported for %s" email))
                             ((and output (string-match-p "pub" output))
                              (message "Public key available for %s" email))
                             (t (message "No public key found for %s" email)))))))))
      (process-put proc 'email recipient-email))))

(with-eval-after-load 'mu4e
  (add-hook 'mu4e-view-mode-hook 'kudu/message-locate-keys))

(defun kudu/message-all-epg-keys-available-p ()
  "Return non-nil if the pgp keyring has a public key for each recipient."
  (require 'epa)
  (let ((context (epg-make-context epa-protocol)))
    (catch 'break
      (dolist (recipient (kudu/message-recipients))
        (let ((recipient-email (cadr recipient)))
          (when (and recipient-email (not (epg-list-keys context recipient-email)))
            (throw 'break nil))))
      t)))

(defun kudu/message-sign-encrypt-if-all-keys-available ()
  "Add MML tag to encrypt message when there is a key for each recipient,
sign it otherwise."
  (if (kudu/message-all-epg-keys-available-p)
      (if (y-or-n-p "Encrypt? ")
          (mml-secure-message-sign-encrypt)
        (when (y-or-n-p "Sign? ")
          (mml-secure-message-sign)))
    (when (y-or-n-p "Sign? ")
      (mml-secure-message-sign))))

(with-eval-after-load 'mu4e
  (add-hook 'message-send-hook 'kudu/message-sign-encrypt-if-all-keys-available))

About

The GNU/Linux distribution for the modern lisp hacker

Topics

Resources

Stars

7 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages