;;; -*- lexical-binding: t -*-
I’m specifying the Emacs I’m using via nixpkgs / my home-manager setup, but for
package management I’m currently using the now-built-in use-package
with
integration for straight
. Having the packages come from nixpkgs
(marked as
external
in list-packages
) is nice, but requires a nixos-rebuild
and restarting
Emacs after making some changes.
Straight.el is bootstrapped in init.el as first step of the setup. This needs to
be done at the earliest possible time during startup, as I don’t want to use the
built-in version of org
, and org is needed to read this file and thus start the
whole process.
Now set up use-package
(which is nowadays built-in) in a way such that the
:ensure
keyword is unset by default, that is, use-package
does not install
packages on its own, as straight.el
is responsible for doing so.
(require 'use-package)
(setq straight-use-package-by-default t)
(setq use-package-always-ensure nil)
I like the systemcrafters idea of having a custom minor mode for my
org
mode configuration files which takes care of tangling (that is, transforming
org
to Emacs Lisp). I previously only tangled on startup, from my init.el
file,
which left me unable to better document this process in exchange for not having
to deal with a one-off bootstrapping “problem”.
After defining this particular minor mode, we want to enable it in all the org
files that reside in user-emacs-directory
.
(define-minor-mode +literate-config-mode
"A minor mode for interacting with org files belonging to my literate
Emacs configuration."
:init-value nil
:global nil
:lighter " +litconf")
(defun +is-literate-config-file ()
"Determine whether the current file is part of my literate Emacs
configuration."
(when-let (file (buffer-file-name))
(and (file-in-directory-p file user-emacs-directory)
(string= "org" (file-name-extension file)))))
(add-hook
'org-mode-hook
(defun +possibly-enable-+literate-config-mode ()
"Enable `+literate-config-mode' if this org file belongs to my literate
Emacs configuration."
(when (+is-literate-config-file)
(+literate-config-mode 1))))
This might not be the best way to tangle when saving (systemcrafters does it by
installing some after-save-hook
only when their respective minor mode is
enabled), but here goes:
(add-hook
'after-save-hook
(defun +literate-config-after-save-hook ()
"If `+literate-config-mode' is enabled, tangle the currently visited org
file."
(when +literate-config-mode
(+tangle-literate-config-file (buffer-file-name)))))
For no real reason whatsoever I like it when my Emacs starts up quickly. The
benchmark-init
package nicely profiles where the time during startup went, and
if a culprit is found, the “fix” is usually to add :defer
to a use-package
call.
Note that this block is not tangled; I only run this when I encounter weird behavior.
(use-package benchmark-init
:demand
:config (benchmark-init/activate)
:hook (after-init . benchmark-init/deactivate))
Sometimes having non-POSIX shells (like fish
that I’m using most of the time) as
$SHELL
leads to hard-to-debug problems and subtle failures. To circumvent this
we can point Emacs to any POSIX shell:
(setq shell-file-name (executable-find "bash"))
This isn’t so nice overall as now bash
is used in term
and vterm
by default, but
for the latter we can just set a different shell again, as vterm
is only used
interactively.
By default, Emacs creates lots of backup files while editing (which is nice and
saved me a couple of times). Yet I can’t stand pressing C-x C-j
and seeing all
those greyed-out names seemingly cluttering my directory, getting copied when
using scp
or similar, etc. Yes, they’re globally ignored in my git
configuration, but wouldn’t it be cool if they were to just live somewhere else?
(setq +backup-directory (file-name-concat user-emacs-directory ".autosave/"))
(setq backup-by-copying t)
(setq backup-directory-alist `(("." . ,+backup-directory)))
(setq auto-save-file-name-transforms `((".*" ,+backup-directory t)))
(setq lock-file-name-transforms auto-save-file-name-transforms)
I don’t know what lisp-interaction-mode
is, but usually I just want to execute
or try out some Emacs Lisp in a *scratch*
buffer anyway.
(setq initial-scratch-message "")
(setq initial-major-mode #'emacs-lisp-mode)
I use the general package to define keybindings in a more straightforward and
declarative way. This is especially useful for Vim emulation with evil-mode
,
where keys have to be bound according to the current (Vim) mode.
(use-package general)
The concept of a prefix key (or leader key) is quite useful in my opinion:
decide on a key that should hold all custom or built-in functionality (not
specific to a major mode, where C-c <letter>
would be the appropriate place to
bind things to). With this mechanism you basically create your own custom
mode-agnostic keymap, accessible almost everywhere.
I used to use C-t
as my prefix key when not using evil
. With Vim emulation SPC
becomes is more attractive as leader key. (That’s why Spacemacs, Doom Emacs,
Helix etc. are also using it by default.)
(general-create-definer leader
:keymaps 'override
:states '(normal insert emacs visual motion)
:prefix "SPC"
:non-normal-prefix "C-t")
(use-package emacs
:bind
("C-h F" . describe-face)
("C-h M" . describe-keymap)
("C-h V" . set-variable)
("C-x C-o" . other-window)
:config
(leader
"SPC" 'execute-extended-command
"b b" 'consult-buffer
"b k" 'kill-current-buffer
"b i" 'ibuffer
"b p" 'previous-buffer
"b n" 'next-buffer
"b r" 'revert-buffer-quick
"b s" 'scratch-buffer
"q k" 'save-buffers-kill-emacs
"q r" 'restart-emacs))
The command to narrow the view to the current region is disabled by default, as it might actually confuse people new to Emacs who invoke it by accident.
Narrowing is a concept used in many areas of Emacs, but I now mostly use it when
writing org
files like this one right here. Narrowing means restricting what can
be seen of the current buffer to a smaller area; marking the whole buffer with
C-x h
or similar commands then only affect that restricted area. In org-mode
there are built-in commands to narrow to a subtree/section, or blocks, for
instance; but narrowing is also used by Emacs itself. An example would be
capturing todo items (with org-capture
), where you on see the item(s) you wish
to add, but are actually editing your whole inbox .org
file within a narrowed
view.
(put 'narrow-to-region 'disabled nil)
It’s awkward to have to do C-S-<backspace>
or C-k C-k
, followed by yanking, to
“just” copy the line point is on to the kill ring. Note that when using evil
,
this command is obsolete, as Vim bindings make this quite easy.
(defun +copy-line-at-point ()
"Copy the current line without killing it first."
(interactive)
(save-excursion
(beginning-of-line)
(let ((start (point)))
(forward-line 1)
(kill-ring-save start (point)))))
I do not want customizations done via customize
to end up in this file. Use a
separate file instead and load that one on startup.
(let ((my-custom-file (locate-user-emacs-file "custom.el")))
(setq custom-file my-custom-file)
(load custom-file 'no-error))
A quick way to jump to this file, and an accompanying keybinding, as I do this very often.
(defun +open-init-file ()
"Open my config.org file."
(interactive)
(find-file (file-truename (locate-user-emacs-file "config.org"))))
(leader "e" #'+open-init-file)
TODO
(use-package hydra
:defer)
Commands like next-error
provide navigation for more than just errors in the
strict sense of the word. For instance, they also work with occur
and grep
type
buffers (this holds for the variants of those that I use as well: embark
and
ripgrep
, with or without consult
).
(defhydra hydra-navigate-errors (:hint nil)
"\n
_f_: first _n_: next _p_: previous\n
"
("n" next-error)
("p" previous-error)
("f" first-error)
("q" nil "exit"))
(leader "n e" 'hydra-navigate-errors/body)
Load themes and other improvements over the default Emacs look.
One thing to pay attention to is that nearly all those operations on “visuals”
work slightly differently when starting Emacs as emacsclient
, for instance via
emacsclient -a ''
Setting some things at this point often results in the changes not being applied
correctly. It’s due to them happening in a technical, invisible Emacs frame. So
you’ll often see (daemonp)
being called, checking whether this instance of Emacs
is using the daemon, then adding some initializer function to
server-after-make-frame-hook
if it is.
The default cursor is black, which interferes with mostly using a dark theme. Brighten it up a bit.
(set-mouse-color "white")
Enable a forever-blinking cursor. I used to disable this mode but I found myself searching for the cursor a couple of times lately.
(setq blink-cursor-delay 0.5)
(setq blink-cursor-blinks -1)
(blink-cursor-mode 1)
Don’t show a cursor in inactive windows.
(setq-default cursor-in-non-selected-windows nil)
An alist of my preferred font families, together with a plist of certain attributes that need to be applied when switching to the respective font.
(setq +font-alist
'((pragmata-pro
. (:family
"Pragmata Pro Mono"))
(ibm-vga-8x14
. (:family
"MxPlus IBM VGA 8x14"
:default-height
200))
(ibm-vga-9x16
. (:family
"MxPlus IBM VGA 9x16"
:default-height
200))
(dos-16
. (:family
"Mx437 DOS/V TWN16"
:default-height
200))
(iosevka
. (:family
"Iosevka Term"))
(iosevka-comfy
. (:family
"Iosevka Comfy"))
(dejavu
. (:family
"DejaVu Sans Mono"))
(ibm-plex
. (:family
"IBM Plex Mono"))
(cascadia
. (:family
"Cascadia Code"))
(source-code-pro
. (:family
"Source Code Pro"))
(consolas
. (:family
"Consolas"
:default-height
180))
(fira-code
. (:family
"Fira Code"
:org-height
0.8))
(jetbrains-mono
. (:family
"JetBrains Mono"
:org-height
0.8))
(julia-mono
. (:family
"JuliaMono"
:org-height
0.9))
(courier-prime
. (:family
"Courier Prime"
:org-height
0.95))
(fantasque-sans-mono
. (:family
"Fantasque Sans Mono"))
(lucida-console
. (:family
"Lucida Console"
:default-height
166
:org-height
0.8))
(inconsolata
. (:family
"Inconsolata"
:default-height
170
:org-height
0.9))
(termingus
. (:family
"Termingus"
:default-height
200))
(unifont
. (:family
"Unifont"
:default-height
200))
(geist
. (:family
"Geist Mono"))
(berkeley-mono
. (:family
"Berkeley Mono"))
(pt-mono
. (:family
"PT Mono"))))
+current-font
contains a symbol pointing to one of the fonts specified in
+font-alist
. Since I can now globally “set” and change a font I like for my
system, Emacs should try to adapt to this (at startup) as well. The function
+load-font-from-env
does just that, as the global font – if it exists – can be
read from an environment variable. If a setting for this font is found in Emacs,
that is taken, unless there’s a +default-font
set, which always “wins”.
If neither default font nor environment variable are set/found, I fall back to DejaVu Sans Mono.
(setq +default-font 'berkeley-mono)
(defun +load-font-from-env ()
"See whether an environment variable specifying a 'system font' is
set, and translate that to one of the font symbols."
(when-let ((f (getenv "KENRAN_DEFAULT_FONT"))
(font (seq-find
(lambda (x)
(equal (plist-get (cdr x) :family)
f))
+font-alist)))
(car font)))
(setq +current-font
(or +default-font
(+load-font-from-env)
'dejavu))
For org-mode
I (sometimes) like using a non-monospace font. This is it.
(defconst +variable-pitch-font "Cantarell")
The function I use to switch between the various fonts I like. It applies the
attributes of its value in +font-alist
.
(defun +switch-font (arg font)
"Apply the attributes stored for FONT in `+font-alist'. When
called with non-nil prefix argument ARG the current height is
reset to the default height for the selected font."
(interactive
(list current-prefix-arg
(intern
(completing-read
"Font: "
(mapcar #'car
(assoc-delete-all +current-font
(copy-alist +font-alist)))))))
(let* ((attrs (alist-get font +font-alist))
(family (plist-get attrs :family))
(default-height (or (plist-get attrs :default-height) 170))
(height (or (and arg default-height)
(face-attribute 'default :height)))
;; `buffer-face-mode' is enabled when calling
;; `variable-pitch-mode'
(org-height (if (bound-and-true-p buffer-face-mode)
(or (plist-get attrs :org-height) 0.9)
1.0))
(weight (or (plist-get attrs :weight) 'regular)))
(setq +current-font font)
(setq +default-font-height default-height)
(set-face-attribute
'default nil
:font family
:weight weight
:height height)
(set-face-attribute
'fixed-pitch nil
:font family
:height org-height)
(set-face-attribute
'variable-pitch nil
:font +variable-pitch-font
:height 1.0)
(set-face-attribute
'fixed-pitch-serif nil
:font family
:inherit 'fixed-pitch
:height 1.0)
(message "Switched to font: %s" family)))
Finally, set all the face attributes synchronously, or register a hook that
makes sure that these also work when using the Emacs daemon together with
emacsclient
.
(if (daemonp)
(add-hook 'server-after-make-frame-hook
(defun +switch-to-current-font ()
(+switch-font t +current-font)
(remove-hook 'server-after-make-frame-hook
#'+switch-to-current-font)))
(+switch-font t +current-font))
I find myself switching fonts all the time; I just seem to need that kind of visual refreshment. So let’s bind it to a “leader” key.
(leader "t f" #'+switch-font)
A command to switch themes interactively. Emacs’s load-theme
applies all the
loaded themes on top of each other; I like to only have one theme active at all
times, so I use disable-theme
on all themes in custom-enabled-themes
before
enabling the target theme.
(defun +available-themes ()
"Get a list of the names of all available themes, excluding the
currently enabled one(s)."
(mapcar #'symbol-name
(seq-difference (custom-available-themes)
custom-enabled-themes)))
(defun +switch-theme (name)
"Switch themes interactively. Similar to `load-theme' but also
disables all other enabled themes."
(interactive
(list (intern
(completing-read
"Theme: "
(+available-themes)))))
(progn
(mapc #'disable-theme
custom-enabled-themes)
(princ name)
(load-theme name t)))
(defun +enable-random-theme ()
"Randomly choose and enable a theme."
(interactive)
(+switch-theme
(intern
(seq-random-elt (+available-themes)))))
(defhydra hydra-random-theme (:hint nil)
"\n
Choose a random theme
[_n_]: next [_q_]: exit\n
"
("n" +enable-random-theme)
("q" nil))
When developing a theme, like I did with naga
, it’s handy to be able to reload
it on the fly.
(defun +reload-theme ()
"Reload the currently active theme."
(interactive)
(let ((active-theme (car custom-enabled-themes)))
(+switch-theme active-theme)))
As with fonts, I like changing visuals regularly, as in, multiple times a day usually. So keybindings for this come in useful:
(leader
"t t" #'+switch-theme
"t r" #'+reload-theme
"t R" 'hydra-random-theme/body)
Since I cannot ever decide which theme I like best, there are a few themes, or theme collections, loaded here.
I usually use my own naga theme. It can be found on MELPA nowadays, though it’s still only “finished” for the parts I really use. Should there be enough interest, I could style some more parts, but it’s not anything I plan on doing for now.
I’m using a “mutable” path to the theme repository, assuming I’ve cloned my
project repository to a fixed location. This allows for quick iteration by
changing stuff in the theme, followed by +reload-theme
.
(add-to-list
'custom-theme-load-path
(file-name-as-directory (expand-file-name "~/projects/emacs-naga-theme")))
Configure naga
and naga-dimmed
:
(setq naga-theme-use-lighter-org-block-background nil)
(setq naga-theme-modeline-style 'green-box)
(setq naga-theme-use-red-cursor t)
(setq naga-theme-surround-org-blocks t)
(setq naga-theme-use-lighter-org-block-background t)
This package by Protesilaos Stavrou is my first choice of “external” themes. I
find myself going back to modus-vivendi
in the evening, even though I keep
saying that I don’t like that high of a contrast.
These two themes are very customizable and come with the most comprehensive and extensive documentation (same as with basically anything that Prot makes available).
(use-package modus-themes
:straight (:type built-in)
:defer
:config
(setq modus-themes-subtle-line-numbers t)
(setq modus-themes-bold-constructs t)
(setq modus-themes-italic-constructs nil)
(setq modus-themes-syntax '(green-strings alt-syntax))
(setq modus-themes-prompts '(background bold))
(setq modus-themes-mode-line nil)
(setq modus-themes-completions
'((matches . (intense background))
(selection . (intense accented))
(popup . (intense accented))))
(setq modus-themes-fringes nil)
(setq modus-themes-paren-match '(bold intense))
(setq modus-themes-region '(accented bg-only))
;; TODO: org agenda, mail citations
(setq modus-themes-org-blocks nil))
Whenever you want or need to channel your inner Tsoding, switch to Iosevka and turn on:
(use-package gruber-darker-theme
:disabled
:defer)
This package used to be my go-to source of different themes. It’s a megapack, started by the creator of Doom Emacs, Henrik Lissner, but over time it grew into an extensive collection of different themes.
It also comes with a DSL to create custom “doom themes”, that is, one specifies a relatively small number of faces / colors and the results are propagated to most faces of all the common packages. Without using (something like) this, it’s quite a bit of work to style lots of packages, as one might imagine. I’ll leave this here for posterity and also to from time to time enable it and check out newly added doom themes.
(use-package doom-themes
:disabled
:defer)
I discovered this package by accident, while randomly selecting themes to try
out via straight-use-package
.
(use-package srcery-theme
:disabled
:defer)
For nostalgic reasons I like to pretend I’m using Spacemacs from time to time.
(use-package spacemacs-theme
:defer)
(use-package base16-theme
:defer)
(+switch-theme 'naga-dimmed)
Rainbow-mode
does what the (org) title says: whenever you see a color in text in
Emacs, say, the nice orange #ff9000, then it will be rendered with the
respective background color. The package will even pick a fitting light or dark
foreground for you.
Note that I load this deferred, and it won’t be automatically started when a
color is encountered; I’ll activate it with M-x rainbow-mode RET
whenever I see
fit.
(use-package rainbow-mode
:defer)
I could (and can) live with the default mode line just fine, but I sometimes (usually when sharing my screen) stupidly click on the modes and something annoying happens. So let’s try to fiddle with it to make it work the way I like:
- No context/mouse menus
- major mode separate from the list of minor modes I want to see
- Render the major mode with its “real” (but shortened through stripping the
always-present
-mode
ending) name, i.e., in this file it should just showorg
- Strip stuff away that I don’t look at anyway
(defmacro +with-active-face (face)
"Return FACE if we're in the mode line of the active window, and
the `mode-line-inactive' face otherwise."
`(if (mode-line-window-selected-p)
,face
'mode-line-inactive))
(defcustom +evil-state-mode-line-format
'(:eval
(let ((fg (face-attribute 'default :foreground))
(bg (face-attribute 'default :background))
(error-fg (face-attribute 'error :foreground)))
(cond
((eq evil-state 'insert)
(propertize
" INSERT "
'face
(+with-active-face `(:foreground ,bg :background ,error-fg))))
((eq evil-state 'normal)
(propertize
" NORMAL "
'face
(+with-active-face `(:foreground ,bg :background ,fg))))
((eq evil-state 'motion)
(propertize
" MOTION "
'face
(+with-active-face `(:foreground ,bg :background ,fg))))
((eq evil-state 'visual)
(propertize
" VISUAL "
'face
(+with-active-face `(:foreground ,bg :background ,(face-attribute 'font-lock-function-name-face :foreground)))))
((eq evil-state 'emacs)
(propertize
" EMACS "
'face
(+with-active-face `(:foreground ,bg :background ,(face-attribute 'font-lock-keyword-face :foreground)))))
(t " "))))
"Specifies how to display the current `evil-state' in the mode
line."
:risky t)
(defcustom +mode-line-compilation-format
'(compilation-in-progress
(:eval (propertize
" [Compiling...]"
'face
(+with-active-face compilation-mode-line-run))))
"How to display the indicator for a running compilation process in
the mode line."
:risky t)
(defcustom +mode-line-flymake-format
'(flymake-mode
(:eval (when-let ((counters (format-mode-line 'flymake-mode-line-counters)))
`(" " ,counters))))
"How to display the name of the current buffer in the mode line."
:risky t)
(defcustom +major-mode-mode-line-format
'("" (:eval (string-replace "-mode" "" (symbol-name major-mode))))
"How to display the active major mode in the mode line."
:risky t)
(defun +visible-minor-modes ()
"Return `minor-mode-alist', but with certain modes I don't want to
see filtered out."
(let ((hidden-modes '(gcmh-mode
yas-minor-mode
buffer-face-mode
eldoc-mode
evil-org-mode
evil-commentary-mode
company-mode
company-box-mode
global-company-mode
lsp-lens-mode
org-indent-mode
auto-revert-mode
auto-fill-function
dot-mode
editorconfig-mode
flymake-mode
evil-collection-unimpaired-mode
abbrev-mode)))
(seq-difference minor-mode-alist
hidden-modes
(lambda (hidden cell)
(eq (car cell)
hidden)))))
(defcustom +minor-modes-mode-line-format
'(:eval
(let ((s (format-mode-line (+visible-minor-modes))))
(if (string-empty-p s) ""
(concat "(" (substring s 1) ")"))))
"How to display the active minor modes in the mode line."
:risky t)
(setq-default
mode-line-format
'(""
+evil-state-mode-line-format
+mode-line-flymake-format
+mode-line-compilation-format
(:propertize " %b" face mode-line-buffer-id)
;; Always show current line and column, without checking `column-number-mode'
;; and `line-number-mode'
(" L%l C%c")
(" " +major-mode-mode-line-format)
(" " +minor-modes-mode-line-format)))
Disable the help display in the minibuffer when hovering over the mode line.
(setq-default mode-line-default-help-echo nil)
By default, Emacs has a couple of keybindings defined for interaction with the
mode line (usually mouse bindings). These are tagged with the special
<mode-line>
“key”. Let’s remove all of them.
(keymap-global-unset "<mode-line>" t)
(let ((alist '((33 . ".\\(?:\\(?:==\\|!!\\)\\|[!=]\\)")
(35 . ".\\(?:###\\|##\\|_(\\|[#(?[_{]\\)")
(36 . ".\\(?:>\\)")
(37 . ".\\(?:\\(?:%%\\)\\|%\\)")
(38 . ".\\(?:\\(?:&&\\)\\|&\\)")
(42 . ".\\(?:\\(?:\\*\\*/\\)\\|\\(?:\\*[*/]\\)\\|[*/>]\\)")
(43 . ".\\(?:\\(?:\\+\\+\\)\\|[+>]\\)")
(45 . ".\\(?:\\(?:-[>-]\\|<<\\|>>\\)\\|[<>}~-]\\)")
(46 . ".\\(?:\\(?:\\.[.<]\\)\\|[.=-]\\)")
(47 . ".\\(?:\\(?:\\*\\*\\|//\\|==\\)\\|[*/=>]\\)")
(48 . ".\\(?:x[a-zA-Z]\\)")
(58 . ".\\(?:::\\|[:=]\\)")
(59 . ".\\(?:;;\\|;\\)")
(60 . ".\\(?:\\(?:!--\\)\\|\\(?:~~\\|->\\|\\$>\\|\\*>\\|\\+>\\|--\\|<[<=-]\\|=[<=>]\\||>\\)\\|[*$+~/<=>|-]\\)")
(61 . ".\\(?:\\(?:/=\\|:=\\|<<\\|=[=>]\\|>>\\)\\|[<=>~]\\)")
(62 . ".\\(?:\\(?:=>\\|>[=>-]\\)\\|[=>-]\\)")
(63 . ".\\(?:\\(\\?\\?\\)\\|[:=?]\\)")
(91 . ".\\(?:]\\)")
(92 . ".\\(?:\\(?:\\\\\\\\\\)\\|\\\\\\)")
(94 . ".\\(?:=\\)")
(119 . ".\\(?:ww\\)")
(123 . ".\\(?:-\\)")
(124 . ".\\(?:\\(?:|[=|]\\)\\|[=>|]\\)")
(126 . ".\\(?:~>\\|~~\\|[>=@~-]\\)"))))
(dolist (char-regexp alist)
(set-char-table-range composition-function-table (car char-regexp)
`([,(cdr char-regexp) 0 font-shape-gstring]))))
FIXME: Move some of the following to early-init.el
instead. See Prot’s
configuration for inspiration and give credit.
I wish to know how fast my Emacs is starting. I’m not sure how to make use of
all that use-package
has to offer in that regard yet, but I want to at least
know when I’ve made things worse.
(add-hook
'emacs-startup-hook
(lambda ()
(message
"Emacs startup took %.2f seconds with %d garbage collection%s"
(float-time (time-subtract after-init-time before-init-time))
gcs-done
(if (> gcs-done 1) "s" ""))))
Disable the graphical UI things like the tool and menu bars, the splash screen, and others.
(tool-bar-mode -1)
(menu-bar-mode -1)
(when (boundp 'scroll-bar-mode)
(scroll-bar-mode -1))
(tooltip-mode -1)
(setq inhibit-splash-screen nil)
(setq inhibit-x-resources nil)
(setq window-combination-resize t)
The following setting enables answering those yes/no questions with just y
or n
.
(fset 'yes-or-no-p 'y-or-n-p)
If ring-bell-function
is nil
, Emacs will still make a sound on, for instance,
C-g
. Since this annoys me to no end, I disable this by customizing the function
to essentially “do nothing”.
(setq ring-bell-function #'ignore)
Make commands shown with M-x depend on the active major mode. Note: this doesn’t
work correctly yet, as (command-modes 'some-command)
seems to return the modes
in an unexpected format.
(setq read-extended-command-predicate
#'command-completion-default-include-p)
To display line numbers, the aptly named display-line-numbers
package is used. I
prefer a hybrid mode for displaying line numbers. That is, line numbers are
shown in a relative way, but the current line displays its absolute line number.
In insert mode, line numbers should be disabled altogether. That’s what these
two functions are used for.
(defun +switch-to-absolute-line-numbers ()
"Enable absolute line numbers."
(when (bound-and-true-p display-line-numbers-mode)
(setq display-line-numbers t)))
(defun +switch-to-hybrid-line-numbers ()
"Enable relative line numbers, but with the current line
showing its absolute line number."
(when (bound-and-true-p display-line-numbers-mode)
(setq display-line-numbers 'relative)
(setq display-line-numbers-current-absolute t)))
(defun +toggle-line-numbers ()
"Toggle `display-line-numbers-mode'. Meant to be used in a
keybinding."
(interactive)
(display-line-numbers-mode 'toggle))
(use-package display-line-numbers
:defer
:hook ((evil-insert-state-entry . +switch-to-absolute-line-numbers)
(evil-insert-state-exit . +switch-to-hybrid-line-numbers))
:config
(setq display-line-numbers-type 'relative)
(setq display-line-numbers-current-absolute t))
(leader "t l" #'+toggle-line-numbers)
(setq require-final-newline t)
(setq mode-require-final-newline t)
When using Emacs HEAD
(with the merged native-comp
branch) a lot of warnings
show up during startup and when changing modes. We could increase the minimum
severity for logs to be shown by setting warning-minimum-level
to :error
, or
just disable the warnings for native compilation entirely like this:
(setq native-comp-async-report-warnings-errors 'silent)
(setq-default indent-tabs-mode nil)
Looking at you, Go.
(setq-default tab-width 4)
(setq read-file-name-completion-ignore-case t)
The default Emacs behavior when yanking (in the Emacs sense of the word) things from the clipboard by clicking the middle mouse button is to insert those at the mouse cursor position. I wish to be able to carelessly click anywhere and have it insert at point, similar to how it’s done in most terminal emulators.
Of course there’s an existing Emacs options for this:
(setq mouse-yank-at-point t)
When writing prose I often use auto-fill-mode
to automatically break long lines.
Emacs uses the fill-column
variable to determine when to break. Its default of
70 is a little low for my taste, though.
(setq-default fill-column 80)
Controversial, I know, but I’ve gotten used to it in Doom and actually like not having to change my typing flow depending on the context anymore.
(setq sentence-end-double-space nil)
Emacs’ M-x compile
command (and M-x project-compile
, which I use much more
often) create a new buffer that contains the compilation output. This buffer
does not automatically follow the output if it reaches the bottom of the first
page, so let’s change that.
(setq compilation-scroll-output t)
The evil
package offers a very complete Vim experience inside of Emacs. I’ve
borrowed some pieces of configuration from wasamasa, specifically the part where
I default to emacs
mode. The reason is that (sometimes due to evil
, other times
evil-collection
) some buffers, like popups in special-mode
, don’t behave the way
I’d expect them to.
(use-package evil
:init
(setq evil-want-integration t)
(setq evil-want-keybinding nil)
(evil-mode 1)
:config
(setq evil-insert-state-cursor '(hbar . 6))
(general-define-key
:states 'normal
"U" 'evil-redo)
(general-define-key
:keymaps 'special-mode-map
:states '(normal motion)
"q" #'quit-window)
(add-to-list 'evil-emacs-state-modes 'sieve-manage-mode)
:custom
((evil-want-C-u-scroll t)
(evil-want-C-u-delete nil)
(evil-want-C-w-delete t)
(evil-want-Y-yank-to-eol t)
(evil-undo-system 'undo-redo)
(evil-symbol-word-search t)
(evil-jumps-cross-buffers nil)
(evil-mode-line-format nil))
:bind
(:map evil-window-map
("C-h" . evil-window-left)
("C-k" . evil-window-up)
("C-j" . evil-window-down)
("C-l" . evil-window-right)
("C-d" . evil-window-delete)))
In order for scrolling with C-u
, C-d
, C-f
, C-b
, and especially with z t
and z b
,
to not leave point on the first or last line of the visible page, we can use the
built-in scroll-margin
variable.
(setq scroll-margin 2)
The analogue of Tim Pope’s vim-surround
plugin in Emacs. Now I can use things
like ysiw
) to surround an inner word with non-padded normal parentheses, ds]
to
delete surrounding brackets, or cs[{
to change surrounding brackets to curly
braces with whitespace padding. Selected regions can be surround with e.g. S`
.
(use-package evil-surround
:after evil
:config
(global-evil-surround-mode))
By default. Emacs distinguishes between commenting a single line and commenting
a region. Its built-in commands are C-x C-;
and comment-or-uncomment-region
.
Using these with evil
is in my opinion a little clunky. The evil-commentary
packages aims to make this easier and comes with a couple more useful functions,
like commenting out a selection while also copying it into a register. Let’s try
it out and see whether it’s more useful than, say, just writing some ELisp to
call the correct Emacs command depending on the visual selection.
(use-package evil-commentary
:after evil
:config
(evil-commentary-mode))
A local leader key is something that can be used to bind situational commands to
usually mode-specific ones. I used ,
for this in Vim; same here now.
(general-create-definer local-leader
:states '(normal visual motion)
:prefix ",")
This is a package I have a love/hate relationship with. evil-collection
in
principle is a great idea, but I’ve found it to be “slightly buggy” at times,
and I also don’t need or like evil
to be integrated everywhere. The most
prominent example for this might be terminal-like things, but I might be coming
around to that.
In the past, whenever I had any misbehavior after a package update, it felt like
a 50:50 chance of evil-collection
being the reason behind it. This is not meant
to be a stab in their direction, as I think that this just lies in the nature of
all things evil
: the community will usually follow up with a solution, but there
will be a period of time between underlying package changes and that solution
where it just does not really work.
For these reasons I have (twice now) tried to live without this package, but
that doesn’t seem to satisfy me either; the context switching between
traditional C-n
or C-p
bindings (or n
and p
, which are often used in special
modes) starts to be frustrating after a month or so. So here goes another try,
this time selectively enabling packages instead of evilify everything.
(use-package evil-collection
:after evil
:config
(evil-collection-init
'(dired
docker
eldoc
evil-mc
git-timemachine
grep
help
helpful
ibuffer
imenu
magit
markdown-mode
mu4e
mu4e-conversation
(package-menu package)
pass
proced
vterm ; let's try this once more
xref
)))
I like evaluating the top-level form I’m currently on by pressing C-c C-c
,
similar to how one compiles in SLY/SLIME.
(use-package emacs
:bind
(:map emacs-lisp-mode-map
("C-c C-c" . eval-defun)))
Make whitespace symbols visible.
(use-package whitespace
:defer
:config
(setq whitespace-line-column 100)
(setq whitespace-global-modes
'(not magit-status-mode
org-mode))
(setq whitespace-style
'(face newline newline-mark missing-newline-at-eof
trailing empty tabs tab-mark))
(setq whitespace-display-mappings
'((newline-mark 10
[9166 10])
(tab-mark 9
[187 9]
[92 9]))))
When the manpage to be opened has finished loading, I’d like it to be shown in a
separate, selected window. One way to accomplish this is by configuring the
notification method via Man-notify-method
.
(use-package man
:defer
:config
(setq Man-notify-method 'aggressive))
As with a lot of built-in popup-like functionality in Emacs, there’s a lot of
different ways to configure them. I want the *Apropos*
and *Help*
buffers to be
selected (i.e., focused) automatically, like the rest of the popups out there.
(defun +pop-to-current-buffer ()
"Pop to the current buffer. This is supposed to be used in hooks
for modes/commands that spawn unfocused windows, like `apropos'."
(pop-to-buffer (current-buffer)))
(use-package emacs
:straight (:type built-in)
:config
(setq help-window-select t)
(add-hook 'apropos-mode-hook #'+pop-to-current-buffer)
(add-hook 'compilation-mode-hook #'+pop-to-current-buffer))
For many things I use avy
now, but can’t get around the de-facto standard
isearch
. I haven’t gotten around to configuring it a lot, but this will probably
grow in the coming weeks or months.
(use-package isearch
:straight (:type built-in)
:config
(setq-default isearch-lazy-count t))
(use-package simple
:straight (:type built-in)
:config
(setq eval-expression-print-length nil)
(setq eval-expression-print-level nil))
(use-package eldoc
:config
(advice-add 'eldoc-doc-buffer
:after
(defun +focus-eldoc-buffer ()
(pop-to-buffer eldoc--doc-buffer))))
(use-package dired
:straight (:type built-in)
:defer
:config
(setq dired-kill-when-opening-new-dired-buffer t)
(setq dired-create-destination-dirs 'ask)
:custom
;; Sort directories to the top
(dired-listing-switches "-la --group-directories-first"))
Beautify dired
a bit.
(use-package diredfl
:defer
:after dired
:hook (dired-mode . diredfl-mode))
Dired-narrow
is a package containing functionality to enter a filter to narrow
down the contents of a dired
buffer interactively. The filter could be either
some fixed string, with normal or fuzzy matching, or a regural expression. Bind
those three functions to the local leader key to have easier access, as dired
already has lots of keys bound.
(use-package dired-narrow
:defer
:after dired)
Ediff
is a great way to diff and/or merge files or buffers. By default it
creates a new frame containing a “control buffer” used to navigate the diff and
manipulate the output. Unfortunately for the longest time this behaved weirdly
for me: whenever I’d tab to the frame containing the diff, do something, then
tab back, the next navigational command from the control frame would work but
drop me back in the diff frame. It’s possible to use ediff-setup-windows-plain
as setup function, which makes ediff
single-frame, circumventing the problem.
(use-package ediff-wind
:defer
:straight (:type built-in)
:config
(setq ediff-window-setup-function #'ediff-setup-windows-plain))
I used to use smartparens
to automatically insert closing parentheses and other
pairs in non-lispy modes. One thing I was missing from Neovim, though, was the
newlines and indentation that it inserted automatically when pressing RET
with
point between braces.
The built-in electric-pair-mode
does just that (by default). I just realized
that I don’t really need it after all (neither with evil
nor without it).
(use-package emacs
:straight (:type built-in)
:init
(electric-pair-mode -1) ; disabled
:config
(setq electric-pair-open-newline-between-pairs t))
Sometimes I accidentally mess up my window layout. Winner-mode
comes with the
winner-undo
command (bound by default to C-<left>
) that reverts such changes.
(use-package emacs
:straight (:type built-in)
:init (winner-mode))
Abbrev-mode
is a nice built-in minor mode that silently replaces some things I
type with other things. It is mostly used for correcting typos, though I haven’t
really “trained” my self-made list of abbrevs – I’ve just started using it.
Since it doesn’t come with a global mode itself, I use setq-default
to enable it
everywhere.
(use-package emacs
:straight (:type built-in)
:init
(setq-default abbrev-mode t)
:config
(setq save-abbrevs nil)
(setq abbrev-file-name
(locate-user-emacs-file "abbrev_defs")))
I started with helm
in spacemacs, then later switched to Doom Emacs where after
a while I tried out ivy
and loved it. Configuring Emacs from scratch was when I
decided to try out some of the newer, more lightweight Emacs packages like
selectrum and vertico. Those integrate very well with default Emacs
functionality, so a lot of things can utilize them “implicitly”. I’ve stuck with
vertico
and I’ve been happy with it ever since.
(use-package vertico
:straight (vertico :files (:defaults "extensions/*.el"))
:init
(vertico-mode)
:custom
(vertico-cycle t)
(vertico-resize t)
:bind
(:map vertico-map
("C-;" . +vertico-select-randomly)))
Directory navigation in C-x d
or C-x C-f
is something else that I liked in Doom
Emacs, as Doom had a notion of “directory name”, that is, DEL
would delete one
level in the directory hierarchy, including the slash symbol. The following
extension to vertico
does just that.
(use-package vertico-directory
:straight nil
:after vertico
:bind (:map vertico-map
("DEL" . vertico-directory-delete-char)
("C-w" . vertico-directory-delete-word)
("RET" . vertico-directory-enter)))
Let’s give vertico-posframe
another try. This makes Emacs look a little similar
to Neovim with something like Telescope, though I like my centered frame to be a
little smaller.
(use-package vertico-posframe
:after vertico
:config
(setq vertico-posframe-height 13)
(setq vertico-posframe-border-width 3)
(setq vertico-posframe-poshandler #'posframe-poshandler-frame-center)
(vertico-posframe-mode))
savehist-mode
keeps a history of commands and inputs I’ve done in a
context-sensitive way, and then shows those at the top when presented with
possible results from vertico
.
(use-package savehist
:init
(savehist-mode))
orderless is a completion style that fits in very well with vertico
(or
selectrum
, for that matter). Parts of a search string may match according to
several matching styles. We want to be able to specify which matching style to
use by appending a suffix so a search string. Therefore we define style
dispatchers and use them to customize orderless-style-dispatchers
.
Prepending an equals sign to a search term will search for literal matches of the preceding string.
(defun +literal-if-= (pattern _index _total)
(when (string-prefix-p "=" pattern)
`(orderless-literal . ,(substring pattern 1))))
A prepended bang discards everything that matches the preceding literal string.
(defun +without-if-! (pattern _index _total)
(when (string-prefix-p "!" pattern)
`(orderless-without-literal . ,(substring pattern 1))))
The tilde sign gives me a way to have “fuzzy” search, if needed.
(defun +flex-if-~ (pattern _index _total)
(when (string-prefix-p "~" pattern)
`(orderless-flex . ,(substring pattern 1))))
(use-package orderless
:custom (completion-styles '(orderless))
(orderless-style-dispatchers
'(+literal-if-=
+without-if-!
+flex-if-~)))
The consult package is the analogue of counsel
, which I used for quite some
time, though not in any extent close to full. This defines some basic bindings
mostly taken from an example in its readme.
(use-package consult
:bind (("C-x b" . consult-buffer)
("C-x C-b" . consult-buffer)
("C-x 4 b" . consult-buffer-other-window)
("C-x 5 b" . consult-buffer-other-frame)
("M-g e" . consult-compile-error)
("M-g g" . consult-goto-line)
("M-g M-g" . consult-goto-line)
("M-g o" . consult-outline)
("M-g m" . consult-mark)
("M-g k" . consult-global-mark)
("M-g i" . consult-imenu)
("M-s f" . consult-find)
("M-s L" . consult-locate)
("M-s g" . consult-grep)
("M-s G" . consult-git-grep)
("M-s r" . consult-ripgrep)
("M-s l" . consult-line)
("M-s k" . consult-keep-lines)
("M-s u" . consult-focus-lines))
:config
(setq consult-project-root-function
(lambda ()
(when-let (project (project-current))
(project-root project))))
(setq consult-ripgrep-args
(concat consult-ripgrep-args
" --hidden"
" -g \"!.git\"")))
;; TODO other isearch integration?
;; TODO :init narrowing, preview delay
I haven’t really grokked Embark yet. It seems to be amazing, though! What I
mostly use it for at the moment is its embark-act
command in conjunction with
embark-export
. With this I often pull the results of some grep
command into a
separate buffer, where I can then utilize wgrep
to bulk-modify the original
buffers.
(use-package embark
:bind (("C-," . embark-act)
("C-h B" . embark-bindings))
:init
(setq prefix-help-command #'embark-prefix-help-command))
Integrate embark
with consult
.
(use-package embark-consult
:after (embark consult)
:demand
:hook (embark-collect-mode . embark-consult-preview-minor-mode))
I switch themes frequently, usually often in one single day, depending on
lighting and mood. But I can never quite decide, and sometimes
+enable-random-theme
hits too many “negatives”. One thing I now like to do is
the following:
- Call
+switch-theme
- Narrow it down with
vertico
/orderless
according to current taste, likebase16 !light !metal
- From the remaining entries, choose a random candidate
This is the function used to do so; it can be used in many contexts.
(defun +vertico-select-randomly ()
"Select a random thing from the current (possibly narrowed) list of
candidates."
(interactive)
(unless (= vertico--total 0)
(let ((index (random vertico--total)))
(vertico--goto index)
(vertico-exit))))
Try out avy
to quickly jump to specific locations in the currently visible area
of the buffer. This is similar to evil-snipe
in Emacs, or (neo)vim plugins like
vim-snipe
, easymotion
, leap.nvim
, hop.nvim
, lightspeed.nvim
, etc.
One cool thing about avy
is that it is well-integrated with evil
, meaning that
it’s possible to use avy
operations in conjunction with Vim commands. For
instance, deleting up until the next avy-goto-char-timer
match can be done with
ds
(where I bind s
to the avy
operation below) and then sniping the correct
result.
(use-package avy
:defer
:config
(setq avy-timeout-seconds 0.3)
(setq avy-all-windows nil)
(general-define-key
:states '(normal motion)
"S" 'evil-avy-goto-char-timer)
(general-define-key
:states '(normal motion)
"s" 'evil-avy-goto-char-in-line-timer)
:bind
("C-'" . avy-goto-char-timer))
The command avy-goto-char-timer
is the perfect solution for my jumping needs in
almost every case. I still find myself trying to navigate to multi-character
sequences in the current line quite often, and would like to have the same
behavior there; that is:
- Incrementally narrowing and highlighting the possible results
- Immediately jumping to unique matches, so I can type until it’s unique and “be there”
The following snippet was handed to me on reddit.
(defun avy-goto-char-in-line-timer ()
(interactive)
(let ((avy-all-windows nil))
(cl-letf (((symbol-function 'avy--find-visible-regions)
(lambda (&rest args)
`((,(point-at-bol) . ,(point-at-eol))))))
(call-interactively 'avy-goto-char-timer))))
(evil-define-avy-motion avy-goto-char-in-line-timer inclusive)
I sometimes like using variable-pitch-mode
, which makes it so only code,
verbatim, and some other things are written with my current monospace /
fixed-width font, and the rest uses a serif font more suitable for longer texts.
but customizing these faces with set-face-attribute
has the usual problems with
the initial daemon startup, and doesn’t hold up when switching fonts or themes.
that’s why i’ve put those changes into the following functions which i can call
whenever these sorts of changes happen, either through hooks or manual trigger.
Note that some themes, like modus-{vivendi,operandi}
, might set the :inherit
attribute on a face, in which case a naive (set-face-attribute face nil :inherit
'fixed-pitch)
overrides the theme settings. To circumvent this I’ve written the
following function that appends a single new value to the current :inherit
attribute value of a face.
(defun +inherit-fixed-pitch (face)
"Append `fixed-pitch' to the `:inherit' attribute of FACE."
(let* ((current (face-attribute face :inherit))
(new (cond
((eq 'unspecified current)
'fixed-pitch)
((listp current)
(if (member 'fixed-pitch current)
current
(cons 'fixed-pitch current)))
((not (eq 'fixed-pitch current))
(list 'fixed-pitch current)))))
(set-face-attribute face nil :inherit new)))
(defun +org-font-setup ()
"Set the face attributes for code, verbatim, and other markup
elements."
(interactive)
(+inherit-fixed-pitch 'org-block)
(+inherit-fixed-pitch 'org-block-begin-line)
(+inherit-fixed-pitch 'org-block-end-line)
(+inherit-fixed-pitch 'org-document-info-keyword)
(+inherit-fixed-pitch 'org-document-info)
(+inherit-fixed-pitch 'org-code)
(+inherit-fixed-pitch 'org-table)
(+inherit-fixed-pitch 'org-verbatim)
(+inherit-fixed-pitch 'org-checkbox)
(+inherit-fixed-pitch 'org-meta-line)
(+inherit-fixed-pitch 'org-special-keyword)
(+inherit-fixed-pitch 'org-link)
(+inherit-fixed-pitch 'org-todo)
(+inherit-fixed-pitch 'org-done)
(+inherit-fixed-pitch 'org-drawer)
(+inherit-fixed-pitch 'org-property-value)
(+inherit-fixed-pitch 'org-document-title))
(add-hook '+switch-theme-hook #'+org-font-setup)
I capture mostly TODO
items, so it’s convenient to have a special shortcut for
that.
(defun +capture-todo ()
"Capture a TODO item with `org-capture'."
(interactive)
(org-capture nil "t"))
For navigation and other org
-specific stuff I’m going to try out another hydra
.
(defhydra hydra-org (:hint nil)
"\n
navigational commands
^^----------------------^^----------------------------
visible header: [_n_] / [_p_]
sibling header: [_N_] / [_P_]
parent header: [_k_]
block: [_b_] / [_B_]\n
"
("n" org-next-visible-heading)
("p" org-previous-visible-heading)
("b" org-next-block)
("B" org-previous-block)
("N" org-forward-heading-same-level)
("P" org-backward-heading-same-level)
("k" org-up-element)
("q" nil "exit"))
When writing text with org
, auto-fill-mode
should be enabled to automatically
break overly long lines into smaller pieces when typing. One may still use M-q
to re-fill paragraphs when editing text. After loading org
, a custom font setup
might run to adjust the headers.
(use-package org
;; Use the built-in version of org (which is quite up-to-date as I'm
;; always using emacs HEAD). This circumvents problems with
;; 'org-compat of the older version having been loaded.
:straight (:type built-in)
:hook
((org-mode . auto-fill-mode)
(org-mode . +org-font-setup)
;; (org-mode . variable-pitch-mode)
(org-trigger . save-buffer)
;; Inheriting fixed-pitch in +org-font-setup doesn't work; the
;; face is not yet known there, so use a hook.
(org-indent-mode . (lambda ()
(+inherit-fixed-pitch 'org-indent)
(+inherit-fixed-pitch 'org-hide)))
(org-capture-mode . evil-insert-state))
:custom
((org-startup-indented t)
(org-startup-folded 'content)
(org-directory "~/org")
(org-log-done t)
(org-special-ctrl-a/e t)
;; If this has a value greater than 0, every RET press
;; keeps indenting the source block further and further.
(org-edit-src-content-indentation 0)
(org-default-notes-file "~/org/notes.org")
(org-agenda-files '("~/org/inbox.org"
"~/org/gtd.org"))
(org-agenda-restore-windows-after-quit t)
(org-refile-targets `(("~/org/gtd.org" :maxlevel . 3)
("~/org/someday.org" :level . 1)))
(org-capture-templates '(("t" "Todo" entry
(file+headline "~/org/inbox.org" "Tasks")
"* TODO %i%?")
("n" "Note" entry
(file+headline "~/org/notes.org" "Notes")
"* %?\n%a\nNote taken on %U")))
(org-capture-bookmark nil)
(org-bookmark-names-plist nil)
(org-todo-keywords '((sequence
"TODO(t)"
"WAITING(w)"
"|"
"DONE(d)"
"CANCELLED(c)")))
(org-html-htmlize-output-type 'css))
:config
(setq org-use-fast-todo-selection 'expert)
(setq-default org-hide-emphasis-markers t)
(advice-add 'org-refile
:after (lambda (&rest _) (org-save-all-org-buffers)))
(local-leader
:keymaps 'org-mode-map
"," 'hydra-org/body)
(leader
"o a" #'org-agenda
"o t" #'+capture-todo
"o c" #'org-capture
"o l" #'org-store-link
"o f" #'org-cycle-agenda-files)
:bind
(:map org-mode-map
("C-'" . nil)))
Some things don’t quite work when evil
is enabled, like the header cycling.
Evil-org
fixes these small issue, and also adds some bonus functionality like o
and O
being slightly “smart”, for instance, adding new bullet points when inside
lists. Additionally, it configures the org-agenda
view to be more compatible
with evil
as well.
(use-package evil-org
:defer t
:hook (org-mode . evil-org-mode)
:config
(require 'evil-org-agenda)
(evil-org-agenda-set-keys))
Minad’s org-modern package looks very promising, so let’s try it out.
(use-package org-modern
:hook
(org-mode . org-modern-mode)
:config
(setq org-modern-star '("◉" "○" "✸" "✿" "✤" "✜" "◆" "▶")
org-modern-block-name '((t . t)
("src" "»" "«")
("example" "»–" "–«")
("quote" "❝" "❞")
("export" "⏩" "⏪"))))
In my org
configuration I’m setting org-hide-emphasis-markers
to t
, thus hiding
certain markup elements around text. Unfortunately it seem to be currently
impossible to switch this interactively, or I just don’t know how, which
prevents me from simply adding a keybinding to toggle it.
Thankfully a new package has appeared recently: org-appear. It reacts to the position of point to automatically show surrounding markup.
(use-package org-appear
:hook ((org-mode . org-appear-mode))
:config
(setq org-appear-autolinks t)
(setq org-appear-autosubmarkers t)
(setq org-appear-autoentities t)
(setq org-appear-autokeywords t)
(setq org-appear-trigger 'always))
(use-package htmlize
:defer
:after ox)
Sometimes I need to showcase some stuff, often code, in a quick and textual
manner. This is where org-present
comes in handy, as it can present Org mode
files/headers with a huge font size, looking a bit like slides.
(use-package org-present
:hook
((org-present-mode
. (lambda ()
(org-present-big)
(org-display-inline-images)
(org-present-read-only)))
(org-present-mode-quit
. (lambda ()
(org-present-small)
(org-remove-inline-images)
(org-present-read-write))))
:config
(local-leader
:keymaps 'org-present-mode-keymap
"n" #'org-present-next
"p" #'org-present-prev
"q" #'org-present-quit))
(use-package flymake
:straight (:type built-in)
:defer
:config
(setq flymake-suppress-zero-counters nil)
(setq flymake-fringe-indicator-position 'left-fringe)
(setq flymake-no-changes-timeout 1.0)
(setq flymake-mode-line-lighter ""))
(defun +flymake-next-error ()
"Jump to the next flymake diagnostic that's at least of severity
`:error'."
(interactive)
(flymake-goto-next-error 1 '(:error) t))
(defun +flymake-prev-error ()
"Jump to the previous flymake diagnostic that's at least of severity
`:error'."
(interactive)
(flymake-goto-prev-error 1 '(:error) t))
ShellCheck is a great little program providing feedback when writing shell
scripts. The Emacs package flymake-shellcheck integrates ShellCheck with
Flymake. We have to trigger flymake-shellcheck-load
when loading shell scripts,
and also enable Flymake itself, both done via hooks to sh-mode
.
(use-package flymake-shellcheck
:commands (flymake-shellcheck-load)
:hook ((sh-mode . flymake-shellcheck-load)
(sh-mode . flymake-mode)))
This is another one of Daniel Mendler’s (aka minad
’s) absolutely great Emacs
packages! I’ve replaced company
with corfu
in the past, but back then it did not
have the automatic mode (corfu-auto
) yet. Without automatic completion it was a
little more tedious to use in modes where TAB
changes the level of indentation,
like in haskell-mode
for instance.
Now that this feature exists it’s time to give the package another try. The
first impression was very positive, as corfu
is using a child frame for the
completion popup and thus does not clash with whitespace-mode
the way company
does.
(use-package corfu
:disabled
:straight (corfu :files (:defaults "extensions/*.el"))
:init (global-corfu-mode)
:hook (evil-insert-state-exit . corfu-quit)
:config
(setq corfu-cycle t)
(setq corfu-auto t)
(setq corfu-auto-delay 0.0)
(setq corfu-exclude-modes
'(erc-mode
haskell-interactive-mode)))
I’ll have to figure out whether I like this or not. At the moment it seems great.
(use-package corfu-popupinfo
:disabled
:straight nil
:after corfu
:config
(corfu-popupinfo-mode)
(setq corfu-popupinfo-delay 0.5))
(defun +ignore-elisp-keywords (cand)
"Do not show Emacs Lisp keywords in completions in
'emacs-lisp-mode'."
(or (not (keywordp cand))
(eq (char-after (car completion-in-region--data)) ?:)))
(defun +setup-elisp-capfs ()
"Uses 'cape-capf-super' to work around the problem that dabbrev
completions don't show up in 'emacs-lisp-mode' by default."
(setq-local completion-at-point-functions
`(,(cape-capf-super
(cape-capf-predicate
#'elisp-completion-at-point
#'+ignore-elisp-keywords)
#'cape-dabbrev)
cape-file))
(setq-local cape-dabbrev-min-length 4))
(defun +register-default-capfs ()
"I use 'cape-dabbrev' and 'cape-file' everywhere as they are
generally useful. This function needs to be called in certain
mode hooks, as some modes fill the buffer-local capfs with
exclusive completion functions, so that the global ones don't get
called at all."
(add-to-list 'completion-at-point-functions #'cape-dabbrev)
(add-to-list 'completion-at-point-functions #'cape-file))
(use-package cape
:hook ((emacs-lisp-mode . +setup-elisp-capfs)
(haskell-mode . +register-default-capfs))
:init
(+register-default-capfs))
(use-package company
:init
(global-company-mode)
:hook
(evil-insert-state-exit . company-abort))
(use-package company-box
:hook (company-mode . company-box-mode))
There are different ways to “do e-mail in Emacs”. Over the last two years I’ve
tried out notmuch
, gnus
, and mu4e
. Some thoughts on each of those:
The Emacs integration for notmuch
is great; it has the most intuitive and
appealing UI from each of the options. Notmuch
works by referencing incoming
e-mail in a separate database only, not ever touching or modifying it. I really
like this idea, and in practice it also felt great due to the quick und
customizable searches. The usual approach is to use a tag-based system of
categorizing your e-mail, but simply having lots of stored queries is a little
bit more flexible.
But notmuch
only handles this single aspect; this means that one needs to find
solutions to the following:
- Getting mail
- Sending mail
- Initial tagging + synchronization of tags between machines
- Alternatively to tagging, using saved queries to “separate” mails
Due to the declarative e-mail account configuration from home-manager
getting
and sending mail are very simple, and I could also easily switch between
different tools like isync
or offlineimap
. For sending mail I use msmtp
.
The initial tagging can be done with a shell script using the well-documented
notmuch
CLI, or via afew
, but I’ve since done away with tagging at all due to
synchronization problems I’ll describe in the following section.
Using muchsync
looks great on paper but is very finicky with sent mail, which
I’d also like to sync back via IMAP to my accounts. The client machine sends
this and puts it into respective sent
directories; muchsync
synchronizes these
directories as well, but I’ve had problems with mails appearing twice, or not
appearing at all on the respective “other” machine, at least in the past. It
looks or feels like my usage of muchsync --nonew
on the clients was a potential
problem: I’ve verified that after sending a mail and it having landed in the
correct sent
directory, a simple muchsync my-server
didn’t lead to the mail
appearing on my servers. It worked after executing notmuch new
once, though, so
I guess muchsync
only synchronizes those mails that are part of the current
notmuch
database state.
One solution would be to make sure that whenever I’m polling from within Emacs,
both muchsync my-server
and notmuch new
are executed. Since notmuch
has
deprecated the notmuch-poll-script
variable in their Emacs client, I have to use
the hooks it provides to make sure muchsync
is executed. Putting muchsync
--nonew
into the preNew
hook while having an unsynchronized sent mail on the
client sounds correct on paper in order to not execute notmuch new
twice, but it
means that in the case of an unsynchronized sent mail, this mail won’t have been
pushed to the server after the first call, if I am correct. So I’ll have to
experiment and probably live with notmuch new
being called twice (which is fine
as it’s blazingly fast).
I’ve never managed to get it quite right, and debugging misbehavior has been a
nightmare as I cannot reliably reproduce it. So when trying out notmuch
once
more, I’ll do so without any tagging at all, utilizing saved queries only.
Now pull in and configure the actual notmuch
package. Note that some of these
options customize built-in functionality, but thematically they do belong here.
(use-package notmuch
:defer
:init
(setq user-mail-address "johannes.maier@mailbox.org")
(leader "m" 'notmuch)
:custom
;; msmtp is registered as sendmail
(message-send-mail-function 'message-send-mail-with-sendmail)
(message-kill-buffer-on-exit t)
;; When replying to mail, choose the account to use
;; based on the recipient address
(message-sendmail-envelope-from 'header)
(mail-envelope-from 'header)
(mail-user-agent 'message-user-agent)
;; Settings for notmuch itself
(notmuch-show-all-multipart/alternative-parts nil)
(notmuch-hello-sections
'(notmuch-hello-insert-header
notmuch-hello-insert-saved-searches
notmuch-hello-insert-footer))
(notmuch-show-empty-saved-searches t)
(notmuch-always-prompt-for-sender t)
(notmuch-search-oldest-first nil)
(notmuch-maildir-use-notmuch-insert t)
(notmuch-archive-tags nil)
(notmuch-fcc-dirs
'(("johannes.maier@mailbox.org" . "mailbox/Sent")
("johannes.maier@active-group.de" . "ag/Sent")
(".*" . "sent")))
(notmuch-saved-searches
'((:name "work inbox"
:query "folder:ag/Inbox"
:key "w"
:search-type tree)
(:name "sent"
:query "folder:ag/Sent or folder:mailbox/Sent"
:key "s"
:search-type tree)
(:name "private inbox"
:query "folder:mailbox/Inbox"
:key "p"
:search-type tree)
(:name "work archive"
:query "path:ag/Archives/**"
:search-type tree)
(:name "private archive"
:query "path:mailbox/Archive/**"
:search-type tree)))
:bind
(:map notmuch-show-mode-map
("a" . +notmuch-archive)
("d" . +notmuch-delete)
:map notmuch-tree-mode-map
("a" . +notmuch-archive)
("d" . +notmuch-delete)
:map notmuch-hello-mode-map
("s" . notmuch-tree)))
In order to be able to use notmuch
again, I need to rely on saved searches only
in a way that I get the same state from a clean maildir sync on each machine. So
let’s circumvent the whole idea of notmuch
and actually touch our mail to
archive, delete, etc. We don’t actually delete things, just move them from
maildir to maildir, which requires some small hacks to refresh the notmuch
buffers.
(defun +notmuch-get-source-file ()
"Get the source file for the currently hovered email."
(car
(cond
((equal major-mode #'notmuch-tree-mode)
(notmuch-tree-get-prop :filename))
((equal major-mode #'notmuch-show-mode)
(notmuch-show-get-prop :filename))
((equal major-mode #'notmuch-search-mode)
(warn "FIXME: Not implemented for `notmuch-search-mode'!"))
(warn "cannot find source file for mail"))))
(defun +notmuch-new-without-hooks ()
"Execute 'notmuch new --no-hooks', circumventing the automatic polling
notmuch does in its preNew hook, yielding quicker refreshes."
(interactive)
(if (equal major-mode #'notmuch-tree-mode)
(notmuch-call-notmuch-process "new" "--no-hooks")))
(defun +notmuch-move-into-maildir (email maildir)
"Move EMAIL (that is, the corresponding file) into MAILDIR."
(let* ((parts (split-string (file-truename email) ":"))
(target-file (concat
maildir
"/cur/"
(org-id-uuid)
(when-let (rest (cadr parts))
(format ":%s" rest)))))
(message "[+email] moving %s to %s" email target-file)
(rename-file email target-file)
(let ((line (line-number-at-pos)))
(+notmuch-new-without-hooks)
(add-hook 'notmuch-tree-process-exit-functions
(defun +notmuch-restore-point (proc)
(goto-line line)
(remove-hook 'notmuch-tree-process-exit-functions #'+notmuch-restore-point)))
(notmuch-refresh-this-buffer))))
(defun +is-work-email (email)
"Determine whether a given EMAIL belongs to my work account."
(string-match "/ag/" (file-name-directory email)))
(defun +notmuch-archive ()
"Archive the current email."
(interactive)
(let* ((email (+notmuch-get-source-file))
(archive-year (caddr (calendar-current-date)))
;; TODO: get maildir location from system configuration
(archive-dir (if (+is-work-email email)
(format "~/.mail/ag/Archives/%s" archive-year)
(format "~/.mail/mailbox/Archive/%s" archive-year))))
(+notmuch-move-into-maildir email archive-dir)))
(defun +notmuch-unarchive ()
"Unarchive the current email."
(interactive)
(let* ((email (+notmuch-get-source-file))
(maildir (if (+is-work-email email)
"~/.mail/ag/Inbox"
"~/.mail/mailbox/Inbox")))
(+notmuch-move-into-maildir email maildir)))
(defun +notmuch-delete ()
"Delete the current email (by moving it into the trash)."
(interactive)
(let ((email (+notmuch-get-source-file)))
(+notmuch-move-into-maildir
email
(if (+is-work-email email)
"~/.mail/ag/Junk"
"~/.mail/mailbox/Trash"))))
Gnus-alias
makes it possible to use different identities when composing mail. I
mostly use it to make sure that replies to a mail are sent from the address I’ve
received it at.
(use-package gnus-alias
:defer t
:config
(setq gnus-alias-identity-alist
`(("mailbox"
nil
"Johannes Maier <johannes.maier@mailbox.org>"
nil
nil
nil
nil)
("ag"
nil
"Johannes Maier <johannes.maier@active-group.de>"
"Active Group GmbH"
nil
nil
,(concat
"Johannes Maier\n"
"johannes.maier@active-group.de\n\n"
"+49 (7071) 70896-67\n\n"
"Active Group GmbH\n"
"Hechinger Str. 12/1\n"
"72072 Tübingen\n"
"Registergericht: Amtsgericht Stuttgart, HRB 224404\n"
"Geschäftsführer: Dr. Michael Sperber"))))
(setq gnus-alias-default-identity "mailbox")
(setq gnus-alias-identity-rules
'(("ag" ("any" "@active-group.de" both) "ag")))
:hook
(message-setup . gnus-alias-determine-identity))
As I’ve written before, I’ve never given the mighty gnus
the trial it deserves.
Getting into this package is really quite scary, for lack of a better word. The
reason is that gnus
defines abstractions over “news”, where the word nowadays
can incorporate everything from feeds, reddit, usenet, email, etc. The result is
that one has to learn lots of specialized and often confusing terminology before
being able to use gnus
(especially for email). Due to the length and
comprehensiveness of the manual the learning curve is quite steep.
Plus, I feel like you cannot “just start using gnus
” and get used to it, whereas
that is an actual path to succees in something like mu4e
, for instance. With
gnus
there’s a lot of configuration to be done before even being able to do
anything.
I’m not sure yet what I will have to sync between machines; the automatically
created .newsrc.eld
file is the most likely candidate. It seems like that the
path to this file can (only?) be configured by setting the path to the startup
file, meaning the newsreader-agnostic .newsrc
file – that I’m not actually
using, as I will only be using gnus
.
(use-package gnus
:disabled
:init
(setq gnus-directory "~/.gnus/")
(setq gnus-home-directory "~/.gnus/")
(setq gnus-startup-file "~/org/newsrc")
(setq gnus-init-file (locate-user-emacs-file "gnus.el"))
:config
(setq user-full-name "Johannes Maier")
(setq user-mail-address "johannes.maier@mailbox.org")
(setq message-directory "~/.gnus")
(setq message-send-mail-function 'message-send-mail-with-sendmail)
(setq send-mail-function 'message-send-mail-with-sendmail)
(setq message-sendmail-envelope-from 'header)
(setq mail-envelope-from 'header)
(setq mail-specify-envelope-from 'header)
(setq gnus-check-new-newsgroups t)
(setq gnus-gcc-mark-as-read t)
(setq nnml-directory "~/.gnus")
(setq gnus-interactive-exit t)
(setq gnus-asynchronous t)
(setq gnus-use-article-prefetch 15)
(setq gnus-select-method '(nnnil ""))
(setq gnus-secondary-select-methods
'((nntp "news.gwene.org")
(nnimap "ag"
(nnimap-address "imap.active-group.de")
(nnimap-server-port 993)
(nnimap-stream ssl)
(nnimap-inbox "INBOX"))
(nnimap "mailbox"
(nnimap-address "imap.mailbox.org")
(nnimap-server-port 993)
(nnimap-stream ssl)
(nnimap-inbox "INBOX")))))
Mu is what I was using for the longest period of time, with mu4e
being its Emacs
frontend. It’s not as customizable as notmuch
, but part of its charm is that I
don’t need to sync anything between my machines, at the cost of mu
touching my
e-mail (adding custom headers I believe). I don’t mind this at all, and I can
use isync
and msmtp
to receive and send mail on any host.
For writing e-mails mu4e
uses message-mode
like the other tools. This checks the
user-full-name
variable to fill in my name.
(setq user-full-name "Johannes Maier")
The actual mu4e
configuration is one huge use-package
block, but most of it is
due to its concept of contexts. Usually there’s one context for each of my
e-mail addresses, and switching between them I may set some context-specific
variables, or even change the mu4e
UI accordingly.
(use-package mu4e
:disabled
:straight
(:local-repo "~/.nix-profile/share/emacs/site-lisp/mu4e"
:type built-in)
:defer
:commands (mu4e)
:config
(setq mail-user-agent 'mu4e-user-agent)
(setq mu4e-completing-read-function #'completing-read)
;; I don't sync drafts to either of the accounts
(setq mu4e-confirm-quit nil)
(setq mu4e-change-filenames-when-moving t)
(setq mu4e-drafts-folder "/drafts")
(setq mu4e-attachment-dir "~/Downloads/")
(setq mu4e-contexts
`(,(make-mu4e-context
:name "mailbox"
:match-func (lambda (msg)
(when msg
(string-prefix-p "/mailbox"
(mu4e-message-field msg :maildir)
t)))
:vars '((user-mail-address . "johannes.maier@mailbox.org")
(mu4e-compose-signature . nil)
(mu4e-sent-folder . "/mailbox/Sent")
(mu4e-trash-folder . "/mailbox/Trash")
(mu4e-refile-folder . (lambda (msg)
(let* ((date (mu4e-message-field-at-point :date))
(year (decoded-time-year (decode-time date))))
(concat "/mailbox/Archive/"
(number-to-string year)))))))
,(make-mu4e-context
:name "ag"
:match-func (lambda (msg)
(when msg
(string-prefix-p "/ag"
(mu4e-message-field msg :maildir)
t)))
:vars `((user-mail-address . "johannes.maier@active-group.de")
;; FIXME: Signature in a file?
(mu4e-compose-signature . ,(concat
"Johannes Maier\n"
"johannes.maier@active-group.de\n\n"
"+49 (7071) 70896-67\n\n"
"Active Group GmbH\n"
"Hechinger Str. 12/1\n"
"72072 Tübingen\n"
"Registergericht: Amtsgericht Stuttgart, HRB 224404\n"
"Geschäftsführer: Dr. Michael Sperber"))
(mu4e-sent-folder . "/ag/Sent")
(mu4e-refile-folder . (lambda (msg)
(let* ((date (mu4e-message-field-at-point :date))
(year (decoded-time-year (decode-time date))))
(concat "/ag/Archives/"
(number-to-string year)))))
(mu4e-trash-folder . "/ag/Trash")))))
(setq mu4e-bookmarks '((:name "Active-Group inbox" :query "maildir:/ag/Inbox" :key ?a)
(:name "Mailbox inbox" :query "maildir:/mailbox/Inbox" :key ?m)
(:name "Unread messages" :query "flag:unread AND NOT flag:trashed" :key ?u)
(:name "Sent" :query "maildir:/ag/Sent OR maildir:/mailbox/Sent" :key ?s)))
(setf (alist-get 'trash mu4e-marks)
(list :char '("d" . "▼")
:prompt "dtrash"
:dyn-target (lambda (target msg)
(mu4e-get-trash-folder msg))
:action (lambda (docid msg target)
(mu4e~proc-move docid (mu4e~mark-check-target target)) "-N")))
(setq mu4e-headers-fields '((:human-date . 12)
(:flags . 6)
(:maildir . 15)
(:mailing-list . 10)
(:from . 22)
(:subject)))
(setq mu4e-context-policy 'pick-first)
(setq mu4e-compose-policy 'ask)
;; No search limit
(setq mu4e-search-results-limit -1)
(setq mu4e-headers-results-limit -1)
;; Always show duplicates (so I can clean them up)
(setq mu4e-search-skip-duplicates nil)
(setq mu4e-headers-skip-duplicates nil)
;; Getting mail via mbsync
(setq mu4e-get-mail-command "mbsync -a")
;; Composing emails
(setq message-send-mail-function #'message-send-mail-with-sendmail)
(setq send-mail-function #'message-send-mail-with-sendmail)
(setq message-sendmail-envelope-from 'header)
(setq mail-envelope-from 'header)
(setq mail-specify-envelope-from 'header)
(setq message-kill-buffer-on-exit t)
;; Visuals
(setq mu4e-headers-thread-single-orphan-prefix '("─> " . "─▶"))
(setq mu4e-headers-thread-orphan-prefix '("┬> " . "┬▶ "))
(setq mu4e-headers-thread-child-prefix '("├> " . "├▶"))
(setq mu4e-headers-thread-connection-prefix '("│ " . "│ "))
(setq mu4e-headers-thread-duplicate-prefix '("= " . "≡ "))
(setq mu4e-headers-thread-first-child-prefix '("├> " . "├▶"))
(setq mu4e-headers-thread-last-child-prefix '("└> " . "╰▶"))
(general-define-key
:keymaps '(mu4e-view-mode-map mu4e-headers-mode-map)
:states '(normal motion)
"R" #'mu4e-compose-wide-reply))
mu4e
uses the built-in message-mode
for composing mail. In order to receive a
warning or yes/no question whenever I try sending without having specified a
subject header, I have to hook into this.
(defun +confirm-empty-mail-subject ()
"Check whether the subject header of the current message is empty,
and abort in this case (https://emacs.stackexchange.com/a/41176)."
(or (message-field-value "Subject")
(y-or-n-p "Really send without subject? ")
(keyboard-quit)))
(add-hook 'message-send-mail-hook #'+confirm-empty-mail-subject)
One thing I’m missing from Doom Emacs is the way it handled all sorts of popup-like buffers. When using vanilla Emacs with packages, there are some different behaviors w.r.t. popups:
- Window splits, new buffer is focused
- Window splits, but new buffer is not focused
- Popup opens over current buffer
- The popup may be closed by pressing
q
- The popup needs to be closed by killing the window
- The popup needs to be closed by killing the buffer
- … and probably others
Doom makes it so there is a unified way of dealing with these, and they all open
and behave the same way. In theory much of this should boil down to good
customization of display-buffer-alist
, but that’s pretty arcane. Shackle.el
seems to make this easier. There’s also popper.el
(it goes well together with
shackle.el
), which can designate windows/buffers meeting certain criteria as
popups, which can then be hidden/shown on a whim. I might want that at a later
point, but first I have to take the following hurdle:
Some buffers (think helpful
or help
) should have a “designated” window, that is,
create “their” window if it doesn’t already exist, and open subsequent buffers
in that one. This is (of course) possible with shackle
, yet I’m pretty sure I’d
need to understand the inner workings of display-buffer
to customize it that
way. It seems prudent then to tackle those, and I might end up not needing
shackle
that way.
(setq display-buffer-alist
'(("\\*\\(helpful\\|Help\\|Apropos\\)"
(display-buffer-reuse-mode-window display-buffer-at-bottom)
(mode . (helpful-mode help-mode apropos-mode))
(window-height . 0.4))
("\\*mu4e-main\\*"
(display-buffer-same-window))
("\\*Native-compile-Log\\*"
(display-buffer-no-window)
(allow-no-window . t))
("\\*Async-native-compile-log\\*"
(display-buffer-no-window)
(allow-no-window . t))
("\\*sly-compilation\\*"
(display-buffer-no-window)
(allow-no-window . t))))
- [ ] REPLs? vterm?
- [ ]
*Backtrace*
- [ ] Shell command results
- [ ] LSP /
eglot
- [ ] lispy evaluation results?
Resizing windows is one of those things that still make me use a mouse, as I
find the default bindings awkward to use and especially chain. A hydra
might
just remedy that:
(defhydra hydra-window-size (:hint nil)
"\n
action: [+]^^ [+]^^
----------^^--------^^----
height: [_g_] [_l_]
width: [_w_] [_n_]\n
"
("b" balance-windows "balance windows" :color blue)
("g" enlarge-window)
("l" shrink-window)
("w" (lambda ()
(interactive)
(enlarge-window-horizontally 2)))
("n" (lambda ()
(interactive)
(shrink-window-horizontally 2)))
("q" nil "exit"))
(leader "w" 'hydra-window-size/body)
I’ve been using weechat for IRC communication in the past. And while my usage of IRC has decreased quite a bit due to a lot of things moving over to Discord, there are some channels and communities that have their sole online presence in IRC. As with anything, it’s worth trying whether just using Emacs might be preferable. So far my experience with ERC has been quite smooth and I don’t regret it yet, so I’ve fully switched over.
(use-package erc
:defer
:config
(setq erc-autojoin-channels-alist
'((libera "#emacs"
"#nyxt"
"#systemcrafters"
"#org-mode"
"#haskell"
"#nim"
"#notmuch"
"#zig"
"#crawl"
"#guix"
"#commonlisp"
"#lisp"
"#herrhotzenplotz"
"#gcli"
"#stumpwm"
"#voidlinux")))
(setq erc-track-exclude
'("#org-mode" "#crawl" "#nim" "#zig"))
(setq erc-track-exclude-types '("333" "353"))
(setq erc-hide-list '("NICK" "MODE" "AWAY" "JOIN" "PART" "QUIT" "AWAY"))
(setq erc-track-exclude-server-buffer t)
(setq erc-kill-server-buffer-on-quit t)
(setq erc-kill-buffer-on-part t)
(setq erc-fill-column 130)
(setq erc-fill-static-center 20)
(setq erc-fill-function #'erc-fill-static))
(use-package erc-hl-nicks
:after erc
(add-to-list 'erc-modules 'hl-nicks))
(use-package erc-image
:after erc
:config
(setq erc-image-inline-rescale 200)
(add-to-list 'erc-modules 'image))
(defun start-irc ()
"Connect to some IRC servers."
(interactive)
(erc-tls :id 'libera
:server "irc.libera.chat"
:port 6697
:nick "kenran"
:full-name "kenran"
:client-certificate (let ((cert-dir (getenv "KENRAN_IRC_CERTS")))
`(,(concat cert-dir "/kenran.key")
,(concat cert-dir "/kenran.crt")))))
I’m still new to this, and have only scratched the surface of when to successfully use them. In particular I’m not sure about what my most-used commands will be, and if and where to bind those.
(use-package multiple-cursors
:defer
:config
(keymap-unset mc/keymap "<return>")
:init
(add-hook 'multiple-cursors-mode-hook
(defun +work-around-multiple-cursors-issue ()
"Loads the file multiple-cursors-core.el (probably for the second
time), which makes the cursors work again. See
https://www.reddit.com/r/emacs/comments/121swxh/multiplecursors_error_on_emacs_29060/."
(load "multiple-cursors-core.el")
(remove-hook 'multiple-cursors-mode-hook #'+work-around-multiple-cursors-issue))))
Due to the nature of mc/mark-next-like-this
and consorts, a hydra should lend
itself very well to this package. It can then be bound to a top-level keybinding
for the best of both worlds.
Credit: I found ejmr’s archived Emacs configuration on GitHub; it contains lots of hydras, so I took heavy inspiration there.
(defhydra hydra-multiple-cursors (:hint nil)
"\n
^^forward ^^backward region-based
^^--------------^^------------- ^^-------------------^^---------------------
[_n_] next [_p_] next [_l_] lines [_C-a_] beg. of lines
[_N_] skip [_P_] skip [_a_] all [_C-e_] end. of lines
[_M-n_] unmark [_M-p_] unmark [_M-w_] all words [_M-w_] words in defun
[_w_] word [_W_] word [_r_] regexp
"
("n" mc/mark-next-like-this)
("N" mc/skip-to-next-like-this)
("M-n" mc/unmark-next-like-this)
("p" mc/mark-previous-like-this)
("P" mc/skip-to-previous-like-this)
("M-p" mc/unmark-previous-like-this)
("w" mc/mark-next-like-this-word)
("W" mc/mark-previous-like-this-word)
("r" mc/mark-all-in-region-regexp :exit t)
("l" mc/edit-lines :exit t)
("a" mc/mark-all-like-this :exit t)
("M-w" mc/mark-all-words-like-this :exit t)
("C-w" mc/mark-all-words-like-this-in-defun :exit t)
("C-a" mc/edit-beginnings-of-lines :exit t)
("C-e" mc/edit-ends-of-lines :exit t)
("q" nil "exit"))
(keymap-global-set "C-z" #'hydra-multiple-cursors/body)
(use-package yasnippet
:init
(yas-global-mode))
Another generally useful package by Steve Purcell is reformatter.el. It enables easy definition of commands to format buffers, as well as minor modes that, when active, automatically apply these commands on save.
(use-package reformatter
:defer)
vterm
is a terminal emulator for Emacs, more feature-rich than the built-in
term
. This is very useful for quickly spawning a terminal, for instance in the
top-level directory of a project.
(use-package vterm
:commands (vterm)
:defer
:config
(setq vterm-shell "fish"))
(setq c-default-style '((awk-mode . "awk")
(other . "stroustrup")))
Zig is a relatively new systems programming language that I could see me learning more in-depth in the near future. It’s a smaller language than, say, Rust, and less safe; but I like its explicit nature and great defaults. The community is very welcoming so far, as well!
(use-package zig-mode
:defer)
tuareg is the standard mode for OCaml editing, providing syntax highlighting,
REPL support, etc., similar to what haskell-mode
does for Haskell.
(use-package tuareg
:defer
:hook (tuareg-mode . (lambda () (setq mode-name "🐫")))
:config
(setq tuareg-indent-align-with-first-arg nil)
(setq tuareg-match-patterns-aligned t)
(local-leader
:keymaps 'tuareg-mode-map
"f" #'+ocamlformat-format-buffer))
To get some IDE features for OCaml in Emacs I use merlin.
;; (use-package merlin
;; :hook ((tuareg-mode . merlin-mode)))
merlin-eldoc integrates merlin
with eldoc-mode
, automatically documenting things
at point.
(use-package merlin-eldoc
:after merlin
:hook (tuareg-mode . merlin-eldoc-setup)
:config
(setq merlin-eldoc-max-lines 8)
(setq merlin-eldoc-type-verbosity 'min)
(setq merlin-eldoc-function-arguments t)
(setq merlin-eldoc-doc t))
I use ocamlformat
to automatically format files on save (using .dir-locals.el
to
eval
the reformatter-created mode.
(reformatter-define +ocamlformat-format
:program "ocamlformat"
:args (list "--name" (buffer-file-name) "-")
:lighter " +ocamlformat")
The OCaml build tool dune
has configuration files written with s-expression
syntax. So in the special dune-mode
let’s add lispy=/=lispyville
as well.
(use-package dune
:hook (dune-mode . lispy-mode))
I’m often using the fish shell. It comes with its own, POSIX-incompatible
language, but I mainly use it for fish
’s configuration (though most of that is
done via nix
, anyway). It’s nice to have syntax highlighting, though.
(use-package fish-mode
:defer)
I want to be able to simply clone and work in projects and adapt to their respective styles of indentation, newlines at the end of files, and the like. EditorConfig comes with a specified file format to describe these things, possible even on per-file basis; all one needs to use these is support of one’s editor. Many editors have out-of-the-box EditorConfig support nowadays. For Emacs, there’s the official editorconfig-emacs package.
(use-package editorconfig
:config
(setq editorconfig-mode-lighter " EC")
(editorconfig-mode 1))
I’m not 100 percent happy with this package, as paragraphs seem to be acting
strange. Deleting a paragraph via dap
for instance often deletes the following
one, too, plus sometimes the previous section header.
(use-package ini-mode
:defer)
(use-package js
:defer
:config
(setq js-indent-level 2))
(use-package psc-ide
:disabled
:hook (purescript-mode . psc-ide-mode)
:config
(setq psc-ide-rebuild-on-save t))
(use-package purescript-mode
:disabled
:hook (purescript-mode . turn-on-purescript-indentation))
I use Nix for tons of things (like the repository you found this Emacs
configuration in). As of now there are a couple of widely used formatters, of
which I personally like nixfmt
the most.
But flake.nix
files can nowadays also “declare” which formatter should be used
for any .nix
files in their respective project; that formatter is then
integrated with the Flake CLI, by calling nix fmt <some-file-or-dir>
. The “user
experience” is still lacking in my opinion, as it doesn’t accept input from
stdin
(yet?), and is thus harder to integrate with text editors. This is why I
still reformatter-define
functions for more convenient to use formatters as well
as nix fmt
here.
(reformatter-define +nix-format
:program "nix"
:args (list "fmt" input-file)
:stdin nil
:stdout nil
:lighter " +nix-fmt")
(reformatter-define +nixfmt-format
:program "nixfmt"
:lighter " +nixfmt")
(reformatter-define +alejandra-format
:program "alejandra"
:lighter " +alejandra")
(use-package nix-mode
:mode "\\.nix\\'"
:config
(local-leader
:keymaps 'nix-mode-map
"f" #'+nixfmt-format-buffer))
(use-package markdown-mode
:mode (("README\\.md\\'" . gfm-mode)
("\\.md\\'" . markdown-mode)
("\\.markdown\\'" . markdown-mode))
:init (setq markdown-command "pandoc")
:config
(mapc #'evil-declare-ignore-repeat
'(markdown-forward-paragraph
markdown-backward-paragraph))
:hook ((markdown-mode gfm-mode) . auto-fill-mode))
Provide an interactive mode for writing Haskell. I can work with a REPL, get feedback and compilation errors shown in the code, and so on. I’ve also added a bunch of utility functions and want to load everything lazily, so I’ve created a custom local Emacs package that contains everything.
(use-package +haskell
:straight nil
:demand
:load-path +custom-package-dir)
(use-package dhall-mode
:mode "\\.dhall\\'"
:config
(setq dhall-type-check-inactivity-timeout 2))
I still have to semi-regularly write Dockerfiles. This package comes with syntax highlighting for those.
(use-package dockerfile-mode
:defer
:config
(add-to-list 'evil-emacs-state-modes 'docker-image-mode)
(add-to-list 'evil-emacs-state-modes 'docker-container-mode)
(add-to-list 'evil-emacs-state-modes 'docker-volume-mode)
(add-to-list 'evil-emacs-state-modes 'docker-network-mode)
(add-to-list 'evil-emacs-state-modes 'docker-context-mode))
I’m trying out this package, as it provides a magit
-like UI to control images,
containers, volumes, networks, etc. from inside Emacs.
(use-package docker
:defer)
(use-package yaml-mode
:defer)
The key to using Clojure effectively with Emacs seems to be CIDER.
(use-package clojure-mode
:defer)
(use-package cider
:after clojure-mode
:defer)
Some of our work projects use cljfmt
to enforce a certain code style. It’s a
little hard to “get right”, though, as the source files to be formatted need to
be inside the project directory.
(reformatter-define +cljfmt
:program "cljfmt"
:args (list "fix" input-file)
:stdin nil
:stdout nil
:input-file (reformatter-temp-file-in-current-directory)
:lighter " +cljfmt")
(use-package csv-mode
:defer)
(use-package plantuml-mode
:defer
:init
(add-to-list 'auto-mode-alist
'("\\.\\(plantuml\\|puml\\)\\'" . plantuml-mode))
:config
(setq plantuml-default-exec-mode 'executable))
SLY seems to be a bit more actively developed and modern than SLIME.
(use-package sly
:defer
:config
(setq inferior-lisp-program "sbcl")
(add-hook 'sly-macroexpansion-minor-mode-hook #'turn-off-evil-mode)
(add-to-list 'evil-emacs-state-modes 'sly-mrepl-mode)
(add-to-list 'evil-emacs-state-modes 'sly-db-mode)
(add-to-list 'evil-emacs-state-modes 'sly-inspector-mode)
(add-to-list 'evil-emacs-state-modes 'sly-xref-mode)
(local-leader
:keymaps 'lisp-mode-map
"q" #'sly-quit-lisp))
sly-asdf
gives integration with Common Lisp’s package manager, ASDF.
(use-package sly-asdf
:defer)
I don’t like that sly
immediately jumps inside its REPL buffer (and window) it
creates. The following works well, at least in the situation where you don’t
want to connect to an existing sly
session/REPL.
(defun +sly (&optional command coding-system interactive callback)
(interactive (list nil nil t nil))
(let ((buf (buffer-name (current-buffer))))
(add-hook 'sly-mrepl-hook
(defun +sly-jump-back ()
(pop-to-buffer buf)
(remove-hook 'sly-mrepl-hook #'+sly-jump-back)
(when callback (funcall callback))))
(sly command coding-system interactive)))
When developing one of my Common Lisp projects, the normal “startup” workflow would be as follows:
- Open the ASDF file
M-x sly
- Use
sly-compile-and-load-file
to load the file, making the system definitions known to, for instance, quicklisp - Load the system with
sly-asdf-load-system
- Inside the REPL, set the current package via
sly-mrepl-set-package
The following utility function +sly-load-project
simplifies the above process by
only needing to be inside the ASDF file when invoking it. The rest is done
automatically.
(defun +sly-set-repl-package (successp notes buffer loadp)
"Pop to the current MREPL buffer and call `sly-mrepl-set-package'."
(if successp
(progn
(pop-to-buffer (sly-mrepl--find-buffer))
(sly-mrepl-set-package)
(remove-hook 'sly-compilation-finished-hook #'+sly-set-repl-package))
(warn "Compilation has failed. Can't set REPL package.")))
(defun +sly-load-project ()
"From within an ASDF file, load it as well as the contained package(s),
then switch the current REPL package."
(interactive)
(+sly nil nil nil
(lambda ()
(sly-load-file (buffer-file-name))
(let ((system (sly-asdf-find-current-system)))
(add-hook 'sly-compilation-finished-hook #'+sly-set-repl-package)
(sly-asdf-load-system system)))))
To have easier access to these functions when inside ASDF buffers, let’s create a specific minor mode (and, implicitly, keymap, to hold bindings for our functions).
(define-minor-mode +asdf-mode
"A minor mode to signify ASDF's files."
:init-value nil
:global nil
:lighter " +asdf")
To enable +asdf-mode
in .asd
files, we hook into lisp-mode
; note that this is
easier for major modes, which can make use of auto-mode-alist
.
(defun +possibly-enable-+asdf-mode ()
"Enable `+asdf-mode' if the current file has the 'asd' extension, and the
buffer's major mode is `lisp-mode'."
(when (and buffer-file-name
(string= "asd"
(file-name-extension buffer-file-name)))
(+asdf-mode 1)))
(add-hook 'lisp-mode-hook #'+possibly-enable-+asdf-mode)
To finally set the keybinding in +asdf-mode
, we need to circumvent a known evil
problem. Note that the keymap below isn’t really a keymap (actually, we don’t
even create one for +asdf-mode
), but rather the mode’s symbol itself. In
conjunction with :definer 'minor-mode
this creates a binding that immediately
works.
(local-leader
:definer 'minor-mode
:keymaps '+asdf-mode
"l" #'+sly-load-project)
(use-package racket-mode
:defer
:hook ((racket-mode . racket-xp-mode)
(racket-mode . racket-unicode-input-method-enable)
(racket-repl-mode . racket-unicode-input-method-enable)))
(use-package rust-mode
:defer
:config
(setq rust-format-on-save t))
I don’t use Java, but Bob Nystrom’s excellent and free book Crafting
Interpreters uses it for the first part. It’s actually quite OK to write Java
with meghanada,
but it takes a long time to download all its dependencies.
(use-package meghanada
:disabled
:defer
:init
(add-hook 'java-mode-hook
(lambda ()
(meghanada-mode t)
(flycheck-mode +1)
(setq c-basic-offset 2)
(add-hook 'before-save-hook 'meghanada-code-beautify-before-save))))
From time to time I need to write some Lua code, like for DCSS RC files. I
haven’t used much from lua-mode
yet, but having syntax highlighting at least is
nice.
(use-package lua-mode
:defer t)
Sometimes I wish to format Lua files. Stylua seems nice.
(reformatter-define +stylua-format
:program "stylua"
:args
(list
"--search-parent-directories"
"--stdin-filepath"
(buffer-file-name)
"-")
:lighter " +stylua")
(local-leader
:keymaps 'lua-mode-map
"f" #'+stylua-format-buffer)
(use-package anaconda-mode
:defer
:hook (python-mode . anaconda-mode))
(use-package pyimport
:defer)
Formatting with black
:
(reformatter-define +black-format
:program "black"
:args (list input-file)
:stdin nil
:stdout nil
:input-file (reformatter-temp-file-in-current-directory)
:lighter " +black")
Sorting imports with isort
:
(reformatter-define +isort
:program "isort"
:args (list input-file)
:stdin nil
:stdout nil
:input-file (reformatter-temp-file-in-current-directory)
:lighter " +isort")
(use-package nim-mode
:disabled
:defer)
(use-package fsharp-mode
:defer
:config
(setq fsharp-indent-offset 2)
(setq fsharp-continuation-offset 2)
(setq inferior-fsharp-program "dotnet fsi --readline-"))
I neither like nor normally use Go, but it’s nice to have some syntax highlighting at least for when I do have to read it.
(use-package go-mode
:defer)
Crystal is the language I learn/use to write my Twitch bot with, at least for now. So far I’m liking it and it will surely also transfer to my Ruby reading skills.
(use-package crystal-mode
:defer)
Sometimes one needs to edit/write WASM text files (.wat
) manually. Sadly this
package is not on MELPA yet, but that’s no problem with straight
.
(use-package wat-mode
:straight (:type git :host github :repo "devonsparks/wat-mode")
:mode "\\.wat\\'"
:defer)
TODO: document
(use-package eglot
:defer)
(use-package lsp-mode
:init
(setq lsp-keymap-prefix "C-;")
:config
(setq lsp-ui-doc-enable nil)
(setq lsp-diagnostics-provider :flycheck)
:defer
:commands lsp)
(use-package lsp-ui
:commands lsp-ui-mode)
(use-package consult-lsp
:defer
:after lsp-mode)
(use-package flycheck
:after lsp-mode
:config
(mapc #'evil-declare-ignore-repeat
'(flycheck-next-error
flycheck-previous-error
flycheck-first-error))
:defer)
This gives us better and more readable help pages. We also replace some built-in
C-h
keybings with helpful-*
functions.
(use-package helpful
:defer
:config
(setq helpful-max-buffers 2)
(setq helpful-switch-buffer-function #'+helpful-switch-to-buffer)
:bind (("C-h f" . helpful-callable)
("C-h v" . helpful-variable)
("C-h k" . helpful-key)))
By default, helpful
annoys me with its way of continuously spawning new windows
in new locations.
(defun +helpful-switch-to-buffer (buffer-or-name)
(if (eq major-mode 'helpful-mode)
(switch-to-buffer buffer-or-name)
(pop-to-buffer buffer-or-name)))
I’ve used projectile for a while. It’s great, but I found myself not using most
of its features. Now that the built-in project.el
has been coming along great,
I’m giving it a try. I’m very happy with it so far.
The following are utility functions that mostly rely on being in the top-level
directory of a known project. project.el
is making this possible in a
straightforward way.
(defun +add-nix-flakes-envrc-file ()
"If it doesn't already exist create a .envrc file containing 'use
flake in the current directory."
(interactive)
(let ((envrc (expand-file-name ".envrc")))
(if (file-exists-p envrc)
(message "%s" "Envrc file already exists")
(write-region "use flake" nil envrc))))
(defun +project-vterm ()
"Open a `vterm' session in the project root of the current
project."
(interactive)
(let ((default-directory
(if-let (project (project-current))
(project-root project)
default-directory)))
(vterm)))
(defun +project-edit-dir-local-variable (mode variable value)
"Edit directory-local variables in the root directory of the
current project."
(interactive
;; Taken from `add-dir-local-variable', as I don't know of a better
;; way to simply wrap that command.
(let (variable)
(require 'files-x)
(list
(read-file-local-variable-mode)
(setq variable (read-file-local-variable "Add or edit directory-local variable"))
(read-file-local-variable-value variable))))
(let ((default-directory (project-root (project-current t))))
(modify-dir-local-variable mode variable value 'add-or-replace)))
Another thing I sometimes need is quickly navigate into my project directory, so
why not write something to open dired
there and then add this function to
project-prefix-map
?
(defun +navigate-to-projects ()
"Open a `dired' buffer in my personal project directory."
(interactive)
(dired "~/projects"))
One tricky thing was making it possible to bind the keymap project-prefix-map
to
a key. One needs to make it callable via fset
.
(use-package project
:config
(fset 'project-prefix-map project-prefix-map)
(setq project-switch-commands
'((project-find-file "find file")
(consult-ripgrep "search/grep" ?s)
(magit-project-status "git status" ?g)
(project-dired "dired")
(+project-vterm "vterm" ?t)
(project-switch-to-buffer "find buffer" ?b)
(project-vc-dir "vc")))
(leader "p" project-prefix-map)
:bind
(:map project-prefix-map
("t" . +project-vterm)
("g" . magit-project-status)
("d" . project-dired)
("s" . consult-ripgrep)
("D" . +project-edit-dir-local-variable)
("n" . +navigate-to-projects)))
Not much to say here: magit
is awesome and in my top 3 reasons why I can’t ever
switch to anything that doesn’t have this. I’ve tried vim-fugitive
and neogit
for (neo)vim, and while they’re great, I still missed magit
. I’m in the process
of getting used to the non-=evil= keybindings (again); once that is finished, I
might add some custom bindings here. magit-status-here
is surely the one I use
the most, but I have a hunch it’s not used nearly often enough to warrant a
custom key chord; I can just use M-x
, where I don’t have to enter much due to
savehist-mode
.
(use-package magit
:defer
:hook ((git-commit-mode . evil-insert-state)
(git-commit-mode . (lambda () (set-fill-column 70))))
:config
;; No autosave for open buffers, as that might trigger hooks and
;; such.
(setq magit-save-repository-buffers nil)
(setq magit-diff-refine-hunk t)
(setq magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)
(setq magit-bury-buffer-function #'magit-restore-window-configuration)
(setq magit-section-initial-visibility-alist
'((stashes . hide)
(untracked . show)))
;; I frequently pull with the autostash option, so add that to the
;; transient command list.
(transient-append-suffix 'magit-pull "-r"
'("-a" "Autostash" "--autostash")))
I used to have way more magit
keybindings when I used evil
, but as mentioned
above M-x
seems like the way to go. I was missing magit-status-here
a lot,
though, so add it in an accessible spot.
(leader "g" 'magit-status-here)
(use-package git-timemachine
:defer)
It’s useful to have a little bit of syntax highlighting in files like .gitignore
or .gitattributes
. The git-modes
package provides just that, and autoloads the
specific modes for the respective file types. Like its readme proposes it’s also
possible to reuse the gitignore-mode
for other things, in this case
.dockerignore
files.
(use-package git-modes
:defer
:init
(add-to-list 'auto-mode-alist
(cons "/.dockerignore\\'" 'gitignore-mode)))
I’ve grown very fond of the way the lispy package works, especially without
evil
. The basic idea is that there are certain special positions in Lisp code
where it’s very uncommon to insert any letter. When point is in such a special
position – most commonly on an opening parenthesis or directly behind a closing
parenthesis – letters that you enter execute special Lisp editing commands: >
slurps s-expressions, j
and k
move down and up on the same level, r
raises and
lots and lots more. After getting used to it for a bit, it feels absolutely
great and I continue to discover new bindings gradually. There’s also an awesome
video by abo-abo, linked in the readme, where they showcase many things lispy
has to offer.
(use-package lispy
:defer
:config
(setq lispy-colon-p nil)
:hook
((emacs-lisp-mode
lisp-mode
clojure-mode
clojurec-mode
clojurescript-mode
common-lisp-mode
racket-mode
racket-repl-mode
sly-repl-mode
slime-repl-mode)
. lispy-mode))
When I was not using evil
for a couple of months, I learned how to use lispy
. I
really loved that, but going fully “holy” now feels wrong when writing Lisp
code. So I’ll try reverting to lispyville
, which changes some lispy
keybindings
(which I don’t like) but also integrates lispy
with evil
, making it so that
keybindings like C
, deletions and pastes don’t interfere with the balanced
parentheses.
(use-package lispyville
:hook
((lispy-mode . lispyville-mode))
:init
(setq lispyville-key-theme
'(operators
c-w
(prettify insert)
additional-movement
(atom-movement t)
slurp/barf-lispy
additional
additional-insert)))
Annotate minibuffer completions, like showing the bound keys and docstrings for
commands in M-x
, variable values in C-h v
, file sizes and permissions in C-x
C-f
, and much more.
(use-package marginalia
:init
(marginalia-mode)
(advice-add #'marginalia-cycle :after
(lambda () (when (bound-and-true-p selectrum-mode)
(selectrum-exhibit 'keep-selected))))
:config
(setq marginalia-annotators
'(marginalia-annotators-heavy marginalia-annotators-light nil))
:bind
(:map minibuffer-local-map
("M-A" . marginalia-cycle)))
Steve Purcell’s envrc package is an alternative to emacs-direnv. The latter has
a long-standing issue where it sometimes loads too late, that is, after packages
like lsp-mode
would need it. envrc
has worked flawlessly so far. Note: this
should probably be one of the last modes to load, as the hook function is then
placed before the other modes to ensure direnv
integration is working as
expected.
(use-package envrc
:defer
:init (envrc-global-mode))
I use ripgrep on the command line a lot. This package makes it comfortably usable from within Emacs.
(use-package ripgrep
:defer)
(use-package ace-window
:defer
:init
(setq aw-keys '(?i ?n ?e ?a ?h ?t ?s ?r))
(general-define-key
:keymaps 'override
:states '(normal insert emacs visual motion)
"C-l" 'ace-window)
:config
(set-face-attribute 'aw-leading-char-face nil :height 2.5))
The default-text-scale
package (again by Steve Purcell) makes it a breeze to
“globally” scale text in Emacs. That is, it won’t only increase the font size in
the current buffer as the text-scale-increase
command does, but rather do it
everywhere.
(use-package default-text-scale
:defer
:after hydra
:config
(setq default-text-scale-amount 10))
Now I like increasing/decreasing the font, for instance when presenting or pair
programming, untit it “fits”. Let’s create a hydra
for this, so that I can press
single keys until it quits – either after q
or a timeout.
(defhydra hydra-global-zoom (:hint nil :timeout 3)
"\n
[_g_]: [+] [_s_]: set
[_l_]: [-] [_r_]: reset\n
"
("g" default-text-scale-increase)
("l" default-text-scale-decrease)
("r" (lambda ()
(interactive)
(setq default-text-scale--complement 0)
(face-spec-set 'default `((t (:height ,+default-font-height))))
(set-face-attribute 'default nil
:height +default-font-height)))
("s" (lambda (height)
(interactive "nFont size: ")
(set-face-attribute 'default nil
:height height))
:color blue)
("q" nil "exit"))
(leader "z" 'hydra-global-zoom/body)
(use-package wgrep
:defer
:custom
((wgrep-auto-save-buffer t)
(wgrep-change-readonly-file nil)
(wgrep-too-many-file-length 15)))
(use-package gcmh
:init
(gcmh-mode 1))