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.
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))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)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")))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*"))))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))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))))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)))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))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"))
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)))))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))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 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))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))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
)(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 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))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)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)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)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
n³ 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))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))))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))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))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)(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))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))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")))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))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))