303 KiB
Emacs Configuration
- Introduction
- A Warning Before You Proceed
- Basic Configuration
- Custom Elisp
- Package Management
- Keybinding Management
- Packages Configuration
- Autocompletion
- Applications
- Editing
- Emacs built-ins
- EXWM
- Making my life easier
- LaTeX
- Org-mode
- Programming
- Visual Configuration
- Misc
- Keybindings
Introduction
After a couple of years using Spacemacs and a failed attempt at
switching to DoomEmacs, I’m finally switching back to a vanilla
configuration! Why? Because I got tired of the framework getting in my
way when I wanted to do stuff. I’m sure this is more applicable to
Spacemacs than DoomEmacs since the latter has nice macros written to
easily add new packages and configure them, such as package!
, after!
,
and others. But ultimately, I wanted to have a system I designed
entirely, with the keybinds I want, the packages I want.
Aso, why Emacs? You know this famous quote:
Emacs is a great operating system, it just lacks a good text editor.
It’s actually pretty true in my opinion. Emacs is basically a Lisp machine with a default text editor, programmed with EmacsLisp, a general-purpose programming language. Therefore, if you want to do something in Emacs, with enough Elisp you can do it — if it’s not in Emacs already, that is.
![Dammit Emacs…](/phundrak/config.phundrak.com/media/commit/b81099061193bb9480d89d4695ba3a95ccec5c85/org/config/img/real_programmers.png)
A Warning Before You Proceed
This configuration makes heavy use of the noweb syntax. This means if
you encounter some code that looks <<like-this>>
, org-mode will
replace this snippet with another code snippet declared elsewhere in
my configuration. If you see some code that looks <<like-this()>>
,
some generating code will run and replace this piece of text with the
text generated. A quick example:
(defun hello ()
<<generate-docstring()>>
<<print-hello>>)
Will instead appear as
(defun hello ()
<<generate-docstring()>>
<<print-hello>>)
This is because I have the block of code below named
generate-docstring
which generates an output, which replaces its noweb
tag. You can recognize noweb snippets generating code with the
parenthesis. Often, such blocks aren’t visible in my HTML exports, but
you can still see them if you open the actual org source file.
(concat "\""
"Print \\\"Hello World!\\\" in the minibuffer."
"\"")
On the other hand, noweb snippets without parenthesis simply replace
the snippet with the equivalent named code block. For instance the one
below is named print-hello
and is placed as-is in the target source
block.
(message "Hello World!")
Basic Configuration
Early Init
The early init file is the file loaded before anything else in Emacs. This is where I put some options in order to disable as quickly as possible some built-in features of Emacs before they can be even loaded, speeding Emacs up a bit.
(setq package-enable-at-startup nil
inhibit-startup-message t
frame-resize-pixelwise t ; fine resize
package-native-compile t) ; native compile packages
(scroll-bar-mode -1) ; disable scrollbar
(tool-bar-mode -1) ; disable toolbar
(tooltip-mode -1) ; disable tooltips
(set-fringe-mode 10) ; give some breathing room
(menu-bar-mode -1) ; disable menubar
(blink-cursor-mode 0) ; disable blinking cursor
(setq gc-cons-threshold (* 1024 1024 1024))
Emacs Behavior
Editing Text in Emacs
I never want to keep trailing spaces in my files, which is why I’m doing this:
(add-hook 'before-save-hook #'whitespace-cleanup)
I don’t understand why some people add two spaces behind a full stop, I sure don’t. Let’s tell Emacs.
(setq-default sentence-end-double-space nil)
There is a minor mode in Emacs which allows to have a finer way of
jumping from word to word: global-subword-mode
. It detects if what
Emacs usually considers a word can be understood as several words, as
in camelCase words, and allows us to jump words on this finer level.
(global-subword-mode 1)
Changing half my screen each time my cursor goes too high or too low
is not exactly ideal. Fortunately, if we set scroll-conservatively
high enough we can have the cursor stay on top or at the bottom of the
screen while the text scrolls progressively.
(setq scroll-conservatively 1000)
Lastly, I want the default mode for Emacs to be Emacs Lisp.
(setq-default initial-major-mode 'emacs-lisp-mode)
Indentation
I don’t like tabs. They rarely look good, and if I need it I can
almost always tell Emacs to use them through a .dir-locals.el
file or
through the config file of my code formatter. So by default, let’s
disable them:
(setq-default indent-tabs-mode nil)
(add-hook 'prog-mode-hook (lambda () (setq indent-tabs-mode nil)))
Just to go on a little tangent here: I don’t exactly hate tabs, but I find them really annoying when your text editor knows only them. Sure, for indentation they work great, and they allow different people getting different settings in their text editor depending on their preferred tastes —some may prefer 2 spaces tabs, some may prefer 4 spaces tabs, some deranged people prefer 8 spaces tabs, and some monsters prefer 3!
But the thing is, once you indented your code and then you need alignment, tabs don’t work anymore! Or they may on your text editor but not on your coworker’s! (He’s the one using 3 spaces tabs by the way).
So, is the answer to use spaces instead of tabs, and screw peoples’ preferences in terms of tabs width? No, I say the answer is more moderate than that, and it might frighten or anger some of you at first: use both spaces and tabs. Now, before you lynch me on the main avenue in front of everyone, let me tell you absolutely no one should ever be mixing spaces and tabs for indentation, that would be absolutely terrible and would bring the worst of both worlds. What’s the best of both worlds then?
Tabs for indentation
Spaces for alignment
I haven’t found a way to automate that in Emacs yet aside from formatters’ config file, and tabs look bat in EmacsLisp anyway, so I’ll stick with spaces by default and change it where needed.
Programming Modes
First off, my definition of what makes a “programming mode” doesn’t exactly
fit mine, so on top of prog-mode
, let’s add a few other modes.
Modes |
---|
prog-mode |
latex-mode |
(mapconcat (lambda (mode) (format "%s-hook" (car mode)))
modes
" ")
Line Number
Since version 26, Emacs has a built-in capacity of displaying line numbers on the left-side of the buffer. This is a fantastic feature that should actually be the default for all programming modes.
(dolist (mode '(<<prog-modes-gen()>>))
(add-hook mode #'display-line-numbers-mode))
Folding code
Most programming languages can usually have their code folded, be it
code between curly braces, chunks of comments or code on another level
of indentation (Python, why…?). The minor-mode that enables that is
hs-minor-mode
, let’s enable it for all of these programming modes:
(dolist (mode '(<<prog-modes-gen()>>))
(add-hook mode #'hs-minor-mode))
Stay Clean, Emacs!
As nice as Emacs is, it isn’t very polite or clean by default: open a
file, and it will create backup files in the same directory. But then,
when you open your directory with your favorite file manager and see
almost all of your files duplicated with a ~
appended to the filename,
it looks really uncomfortable! This is why I prefer to tell Emacs to
keep its backup files to itself in a directory it only will access.
(setq backup-directory-alist `(("." . ,(expand-file-name ".tmp/backups/"
user-emacs-directory))))
It also loves to litter its init.el
with custom variables here and
there, but the thing is: I regenerate my init.el
each time I tangle
this file! How can I keep Emacs from adding stuff that will be almost
immediately lost? Did someone say custom file?
(setq-default custom-file (expand-file-name ".custom.el" user-emacs-directory))
(when (file-exists-p custom-file) ; Don’t forget to load it, we still need it
(load custom-file))
If we delete a file, we want it moved to the trash, not simply deleted.
(setq delete-by-moving-to-trash t)
Finally, the scatch buffer always has some message at its beginning, I don’t want it!
(setq-default initial-scratch-message nil)
Stay Polite, Emacs!
When asking for our opinion on something, Emacs loves asking us to answer by “yes” or “no”, but in full! That’s very rude! Fortunately, we can fix this.
(defalias 'yes-or-no-p 'y-or-n-p)
This will make Emacs ask us for either hitting the y
key for “yes”, or
the n
key for “no”. Much more polite!
It is also very impolite to keep a certain version of a file in its buffer when said file has changed on disk. Let’s change this behavior:
(global-auto-revert-mode 1)
Much more polite! Note that if the buffer is modified and its changes haven’t been saved, it will not automatically revert the buffer and your unsaved changes won’t be lost. Very polite!
Misc
Let’s raise Emacs undo memory to 10MB, and make Emacs auto-save our files by default.
(setq undo-limit 100000000
auto-save-default t)
(setq window-combination-resize t) ; take new window space from all other windows
Personal Information
Emacs needs to know its master! For various reasons by the way, some
packages rely of these variables to know who it is talking to or
dealing with, such as mu4e
which will guess who you are if you haven’t
set it up correctly.
(setq user-full-name "Lucien Cartier-Tilet"
user-real-login-name "Lucien Cartier-Tilet"
user-login-name "phundrak"
user-mail-address "lucien@phundrak.com")
Visual Configuration
The first visual setting in this section will activate the visible bell. What it does is I get a visual feedback each time I do something Emacs doesn’t agree with, like tring to go up a line when I’m already at the top of the buffer.
(setq visible-bell t)
It is nicer to see a cursor cover the actual space of a character.
(setq x-stretch-cursor t)
When text is ellipsed, I want the ellipsis marker to be a single character of three dots. Let’s make it so:
(with-eval-after-load 'mule-util
(setq truncate-string-ellipsis "…"))
With Emacs 29.0.50 onwards, a new frame parameter exists:
alpha-background
. Unlike alpha
, this frame parameter only makes Emacs’
background transparent, excluding images and text.
(add-to-list 'default-frame-alist '(alpha-background . 0.9))
Modeline Modules
I sometimes use Emacs in fullscreen, meaning my usual taskbar will be hidden. This is why I want the current date and time to be displayed, in an ISO-8601 style, although not exactly ISO-8601 (this is the best time format, fight me).
(require 'time)
(setq display-time-format "%Y-%m-%d %H:%M")
(display-time-mode 1) ; display time in modeline
Something my taskbar doesn’t have is a battery indicator. However, I want it enabled only if I am on a laptop or if a battery is available.
(let ((battery-str (battery)))
(unless (or (equal "Battery status not available" battery-str)
(string-match-p (regexp-quote "N/A") battery-str))
(display-battery-mode 1)))
This isn’t a modeline module per se, but we have an indicator of the current line in Emacs. And although it is useful, I also often wish to know which column I’m on. This can be activated like so:
(column-number-mode)
The following code is, as will several chunks of code in this config,
borrowed from TEC’s configuration. It hides the encoding information
of the file if the file itself is a regular UTF-8 file with \n
line
ending. Be aware the doom-modeline-buffer-encoding
variable is usabel
here only because I use the Doom modeline as seen below.
(defun modeline-contitional-buffer-encoding ()
"Hide \"LF UTF-8\" in modeline.
It is expected of files to be encoded with LF UTF-8, so only show
the encoding in the modeline if the encoding is worth notifying
the user."
(setq-local doom-modeline-buffer-encoding
(unless (and (memq (plist-get (coding-system-plist buffer-file-coding-system) :category)
'(coding-category-undecided coding-category-utf-8))
(not (memq (coding-system-eol-type buffer-file-coding-system) '(1 2))))
t)))
Now, let’s automate the call to this function in order to apply the modifications to the modeline each time we open a new file.
(add-hook 'after-change-major-mode-hook #'modeline-contitional-buffer-encoding)
Fonts
I don’t like the default font I usually have on my machines, I really don’t. I prefer Cascadia Code, as it also somewhat supports the IPA.
(defvar phundrak/default-font-size 90
"Default font size.")
(defvar phundrak/default-font-name "Cascadia Code"
"Default font.")
(defun my/set-font ()
(when (find-font (font-spec :name phundrak/default-font-name))
(set-face-attribute 'default nil
:font phundrak/default-font-name
:height phundrak/default-font-size)))
(my/set-font)
(add-hook 'server-after-make-frame-hook #'my/set-font)
Frame Title
This is straight-up copied from TEC’s configuration. See their comment on the matter.
(setq frame-title-format
'(""
"%b"
(:eval
(let ((project-name (projectile-project-name)))
(unless (string= "-" project-name)
(format (if (buffer-modified-p) " ◉ %s" " ● %s - Emacs") project-name))))))
A better custom variable setter
Something people often forget about custom variables in Elisp is they
can have a custom setter that will run some code if we set the
variable properly with customize-set-variable
, so setq
shouldn’t be
the user’s choice by default. But repeatedly writing
customize-set-variable
can get tiring and boring. So why not take the
best of both world and create csetq
, a setq
that uses
customize-set-variable
under the hood while it keeps a syntax similar
to the one setq
uses?
(defmacro csetq (&rest forms)
"Bind each custom variable FORM to the value of its VAL.
FORMS is a list of pairs of values [FORM VAL].
`customize-set-variable' is called sequentially on each pairs
contained in FORMS. This means `csetq' has a similar behaviour as
`setq': each VAL expression are evaluated sequentially, i.e. the
first VAL is evaluated before the second, and so on. This means
the value of the first FORM can be used to set the second FORM.
The return value of `csetq' is the value of the last VAL.
\(fn [FORM VAL]...)"
(declare (debug (&rest sexp form))
(indent 1))
;; Check if we have an even number of arguments
(when (= (mod (length forms) 2) 1)
(signal 'wrong-number-of-arguments (list 'csetq (1+ (length forms)))))
;; Transform FORMS into a list of pairs (FORM . VALUE)
(let (sexps)
(while forms
(let ((form (pop forms))
(value (pop forms)))
(push `(customize-set-variable ',form ,value)
sexps)))
`(progn ,@(nreverse sexps))))
I first got inspired by this blog article (archived article, just in case) but it seems the code snippet no longer works properly, so not only did I have to modify it to make it work with an arbitrary amount of arguments (as long as it’s pairs of variables and their value), but I also had to make the code simply work.
Custom Elisp
Dired functions
phundrak/open-marked-files
This function allows the user to open all marked files from a dired buffer as new Emacs buffers.
(defun phundrak/open-marked-files (&optional files)
"Open all marked FILES in Dired buffer as new Emacs buffers."
(interactive)
(let* ((file-list (if files
(list files)
(if (equal major-mode "dired-mode")
(dired-get-marked-files)
(list (buffer-file-name))))))
(mapc (lambda (file-path)
(find-file file-path))
(file-list))))
Switch between buffers
Two default shortcuts I really like from Spacemacs are SPC b m
and SPC
b s
, which bring the user directly to the *Messages*
buffer and the
*scratch*
buffer respectively. These functions do exactly this.
(defun switch-to-messages-buffer ()
"Switch to Messages buffer."
(interactive)
(switch-to-buffer (messages-buffer)))
(defun switch-to-scratch-buffer ()
"Switch to Messages buffer."
(interactive)
(switch-to-buffer "*scratch*"))
Screenshots
Since Emacs27, it is possible for Emacs to take screenshots of itself
in various formats. I’m mainly interested by the SVG and PNG format,
so I’ll only write functions for these. It isn’t really redundant with
the screenshot.el
package used here since these functions take a
screenshot of Emacs as a whole rather than of a code snippet.
First, we have a general function which is a slight modification of
the function shared by Alphapapa in this Reddit comment. It has been
modified so it is possible to pass the function an argument for the
format the screenshot will be taken as, and if type
is nil
the user
can still chose it.
(defun self-screenshot (&optional type)
"Save a screenshot of type TYPE of the current Emacs frame.
As shown by the function `', type can weild the value `svg',
`png', `pdf'.
This function will output in /tmp a file beginning with \"Emacs\"
and ending with the extension of the requested TYPE."
(interactive)
(let* ((type (if type type
(intern (completing-read "Screenshot Type: "
'(png svg pdf postscript)))))
(extension (pcase type
('png ".png")
('svg ".svg")
('pdf ".pdf")
('postscript ".ps")
(otherwise (error "Cannot export screenshot of type %s" otherwise))))
(filename (make-temp-file "Emacs-" nil extension))
(data (x-export-frames nil type)))
(with-temp-file filename
(insert data))
(kill-new filename)
(message filename)))
I used this function to take the screenshots you can see in this document.
Handle new windows
The two functions below allow the user to not only create a new window to the right or below the current window (respectively), but also to focus the new window immediately.
(defun split-window-right-and-focus ()
"Spawn a new window right of the current one and focus it."
(interactive)
(split-window-right)
(windmove-right))
(defun split-window-below-and-focus ()
"Spawn a new window below the current one and focus it."
(interactive)
(split-window-below)
(windmove-down))
(defun kill-buffer-and-delete-window ()
"Kill the current buffer and delete its window."
(interactive)
(progn
(kill-this-buffer)
(delete-window)))
Extend add-to-list
One function I find missing regarding add-to-list
is add-all-to-list
so I can add multiple elements to a list at once. Instead, with
vanilla Emacs, I have to repeatedly call add-to-list
. That’s not very
clean. Let’s declare this missing function:
(defun add-all-to-list (list-var elements &optional append compare-fn)
"Add ELEMENTS to the value of LIST-VAR if it isn’t there yet.
ELEMENTS is a list of values. For documentation on the variables
APPEND and COMPARE-FN, see `add-to-list'."
(let (return)
(dolist (elt elements return)
(setq return (add-to-list list-var elt append compare-fn)))))
Package Management
Repositories
By default, only GNU’s repositories are available to the package managers of Emacs. I also want to use Melpa and org-mode’s repository, so let’s add them! Note that the stock elpa repository is renamed to gnu due to the addition of another Elpa repository, nongnu, which will hosts packages that do not conform to the FSF’s copyright assignment. Both the gnu and the nonfree repositories are Elpa repositories now, and they are renamed here in order to avoid any confusion between the two of them. Melpa is a community-maintained repository which contains an absurd amount of Emacs packages.
(setq package-archives '(("melpa" . "https://melpa.org/packages/")
("gnu" . "https://elpa.gnu.org/packages/")
("nongnu" . "https://elpa.nongnu.org/nongnu/")))
Straight
For my package management, I prefer to use straight
(Github). This is
due to its capacity of integrating nicely with use-package
, which also
supports general
which I use for my keybindings (see below), but also
because with it I can specify where to retrieve packages that are not
on MELPA or ELPA but on Github and other online Git repositories too.
First, let’s bootstrap straight.
(defvar bootstrap-version)
(defvar comp-deferred-compilation-deny-list ()) ; workaround, otherwise straight shits itself
(let ((bootstrap-file
(expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
(bootstrap-version 5))
(unless (file-exists-p bootstrap-file)
(with-current-buffer
(url-retrieve-synchronously
"https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
'silent 'inhibit-cookies)
(goto-char (point-max))
(eval-print-last-sexp)))
(load bootstrap-file nil 'nomessage))
Now, we can refresh our package list in order to be able to install stuff.
(package-initialize)
(unless package-archive-contents
(package-refresh-contents))
From time to time, I fork some packages either because I’m trying to
implement something new in said package, or because the package is
unmaintained and I want to continue developing it a bit more. Straight
provides a nice feature for using forks of a package with its :fork
option. If set to t
, then straight will attempt to retrieve the
package with the same name but with a different username on the same
host. This username is retrieved through the following variable:
(setq straight-host-usernames
'((github . "Phundrak")
(gitlab . "Phundrak")))
The huge advantage of straight is it clones through git the packages
it installs. This means development can be done directly on the
downloaded package. However, Forge (a Magit extension for interacting
with websites such as Github, Gitlab, and such) interacts by default
with the forge described by the origin
remote, which isn’t necessarily
the one I want Forge to interact with by default. Therefore, all
default remotes are named straight
so it won’t collide with my regular
development flow.
(setq straight-vc-git-default-remote-name "straight")
We finally come to the use-package
installation. This is done like so:
(straight-use-package '(use-package :build t))
(setq use-package-always-ensure t)
Keybinding Management
Which-key
Which key is, I think, one of my favorite quality of life package. When you begin a keybind, Emacs will show you all keybinds you can follow the first one with in order to form a full keychord. Very useful when you have a lot of keybinds and don’t remember exactly what is what.
(use-package which-key
:straight (:build t)
:defer t
:init (which-key-mode)
:diminish which-key-mode
:config
(setq which-key-idle-delay 1))
General
General is an awesome package for managing keybindings. Not only is it
oriented towards keychords by default (which I love), but it also
provides some integration with evil so that we can declare keybindings
for certain states only! This is a perfect replacement for define-key
,
evil-define-key
, and any other function for defining keychords. And it
is also possible to declare a prefix for my keybindings! By default,
all keybinds will be prefixed with SPC
and keybinds related to a
specific mode (often major modes) will be prefixed by a comma ,
(and
by C-SPC
and M-m
respectively when in insert-mode
or emacs-mode
). You
can still feel some influence from my Spacemacs years here.
(use-package general
:straight (:build t)
:init
(general-auto-unbind-keys)
:config
(general-create-definer phundrak/undefine
:keymaps 'override
:states '(normal emacs))
(general-create-definer phundrak/evil
:states '(normal))
(general-create-definer phundrak/leader-key
:states '(normal insert visual emacs)
:keymaps 'override
:prefix "SPC"
:global-prefix "C-SPC")
(general-create-definer phundrak/major-leader-key
:states '(normal insert visual emacs)
:keymaps 'override
:prefix ","
:global-prefix "M-m"))
(mapconcat (lambda (line)
(let* ((key (car line))
(function (cadr line))
(comment (caddr line)))
(format "\"%s%s\" %s"
prefix
key
(if (string= "" comment)
(if (or (string= "" function)
(string= "nil" function))
"nil"
(concat "#'" function))
(format "'(%s :wk %s)"
(if (or (string= "" function)
(string= "nil" function))
":ignore t"
function)
(if (or (string= "none" comment)
(string= "nil" comment))
"t"
(concat "\"" comment "\"")))))))
table
"\n")
Evil
Evil emulates most of vim’s keybinds, because let’s be honest here, they are much more comfortable than Emacs’.
(use-package evil
:straight (:build t)
:after (general)
:init
(setq evil-want-integration t
evil-want-keybinding nil
evil-want-C-u-scroll t
evil-want-C-i-jump nil)
(require 'evil-vars)
(evil-set-undo-system 'undo-tree)
:config
<<evil-undefine-keys>>
<<evil-bepo>>
(evil-mode 1)
(setq evil-want-fine-undo t) ; more granular undo with evil
(evil-set-initial-state 'messages-buffer-mode 'normal)
(evil-set-initial-state 'dashboard-mode 'normal))
I want to undefine some of the default keybinds of Evil because it
does not match my workflow. Namely, I use the space key and the comma
as leaders for my keybinds, and I’m way too used to Emacs’ C-t
, C-a
,
C-e
, and C-y
.
(evil-global-set-key 'motion "t" 'evil-next-visual-line)
(evil-global-set-key 'motion "s" 'evil-previous-visual-line)
(general-define-key
:keymaps 'evil-motion-state-map
"SPC" nil
"," nil)
(general-define-key
:keymaps 'evil-insert-state-map
"C-t" nil)
(general-define-key
:keymaps 'evil-insert-state-map
"U" nil
"C-a" nil
"C-y" nil
"C-e" nil)
Something else that really bugs me is I use the bépo layout, which is
not at all like the qwerty layout. For instance, hjkl
becomes ctsr
.
Thus, I need some bépo-specific changes.
(dolist (key '("c" "C" "t" "T" "s" "S" "r" "R" "h" "H" "j" "J" "k" "K" "l" "L"))
(general-define-key :states 'normal key nil))
(general-define-key
:states 'motion
"h" 'evil-replace
"H" 'evil-replace-state
"j" 'evil-find-char-to
"J" 'evil-find-char-to-backward
"k" 'evil-substitute
"K" 'evil-smart-doc-lookup
"l" 'evil-change
"L" 'evil-change-line
"c" 'evil-backward-char
"C" 'evil-window-top
"t" 'evil-next-visual-line
"T" 'evil-join
"s" 'evil-previous-visual-line
"S" 'evil-lookup
"r" 'evil-forward-char
"R" 'evil-window-bottom)
This package enables and integrates Evil into a lot of different modes, such as org-mode, dired, mu4e, etc. Again, I need some additional code compared to most people due to the bépo layout.
(use-package evil-collection
:after evil
:straight (:build t)
:config
;; bépo conversion
(defun my/bépo-rotate-evil-collection (_mode mode-keymaps &rest _rest)
(evil-collection-translate-key 'normal mode-keymaps
;; bépo ctsr is qwerty hjkl
"c" "h"
"t" "j"
"s" "k"
"r" "l"
;; add back ctsr
"h" "c"
"j" "t"
"k" "s"
"l" "r"))
(add-hook 'evil-collection-setup-hook #'my/bépo-rotate-evil-collection)
(evil-collection-init))
undo-tree
is my preferred way of undoing and redoing stuff. The main
reason is it doesn’t create a linear undo/redo history, but rather a
complete tree you can navigate to see your complete editing history.
One of the two obvious things to do are to tell Emacs to save all its
undo history fies in a dedicated directory, otherwise we’d risk
littering all of our directories. The second thing is to simply
globally enable its mode.
(use-package undo-tree
:defer t
:straight (:build t)
:custom
(undo-tree-history-directory-alist
`(("." . ,(expand-file-name (file-name-as-directory "undo-tree-hist")
user-emacs-directory))))
:init
(global-undo-tree-mode)
:config
<<undo-tree-ignore-text-properties>>
<<undo-tree-compress-files>>
(setq undo-tree-visualizer-diff t
undo-tree-visualizer-timestamps t
undo-tree-auto-save-history t
undo-tree-enable-undo-in-region t
undo-limit (* 800 1024)
undo-strong-limit (* 12 1024 1024)
undo-outer-limit (* 128 1024 1024)))
An interesting behavior from DoomEmacs is to compress the history
files with zstd
when it is present on the system. Not only do we enjoy
much smaller files (according to DoomEmacs, we get something like 80%
file savings), Emacs can load them much faster than the regular files.
Sure, it uses more CPU time uncompressing these files, but it’s
insignificant and it’s still faster than loading a heavier file.
(when (executable-find "zstd")
(defun my/undo-tree-append-zst-to-filename (filename)
"Append .zst to the FILENAME in order to compress it."
(concat filename ".zst"))
(advice-add 'undo-tree-make-history-save-file-name
:filter-return
#'my/undo-tree-append-zst-to-filename))
Hydra
Hydra is a simple menu creator for keybindings.
(use-package hydra
:straight (:build t)
:defer t)
Hydras
The following hydra allows me to quickly zoom in and out in the current buffer.
(defhydra hydra-zoom ()
"
^Zoom^ ^Other
^^^^^^^--------------------------
[_t_/_s_] zoom in/out [_q_] quit
[_0_]^^ reset zoom
"
("t" text-scale-increase "zoom in")
("s" text-scale-decrease "zoom out")
("0" text-scale-adjust "reset")
("q" nil "finished" :exit t))
Similarly, this one is also inspired from Spacemacs and allows the
user to interact with the width of the buffer in writeroom
.
(defhydra writeroom-buffer-width ()
"
^Width^ ^Other
^^^^^^^^-----------------------
[_t_] enlarge [_r_/_0_] adjust
[_s_] shrink [_q_]^^ quit
"
("q" nil :exit t)
("t" writeroom-increase-width "enlarge")
("s" writeroom-decrease-width "shrink")
("r" writeroom-adjust-width "adjust")
("0" writeroom-adjust-width "adjust"))
Another similar one is for mu4e-view-mode
that allows me to shrink or
grow the mu4e-headers
buffer when viewing an email.
(defhydra mu4e-headers-split-adjust-width ()
"
^Zoom^ ^Other
^^^^^^^---------------------------------
[_t_/_s_] shrink/enlarge view [_q_] quit
"
("q" nil :exit t)
("t" mu4e-headers-split-view-shrink "shrink")
("s" mu4e-headers-split-view-grow "enlarge"))
Similarly still, this one allows me to manage the size my Emacs windows.
(defhydra windows-adjust-size ()
"
^Zoom^ ^Other
^^^^^^^-----------------------------------------
[_t_/_s_] shrink/enlarge vertically [_q_] quit
[_c_/_r_] shrink/enlarge horizontally
"
("q" nil :exit t)
("c" shrink-window-horizontally)
("t" enlarge-window)
("s" shrink-window)
("r" enlarge-window-horizontally))
This one allows me to manipulate my Emacs frames’ background transparency.
(defun my/transparency-round (val)
"Round VAL to the nearest tenth of an integer."
(/ (round (* 10 val)) 10.0))
(defun my/increase-frame-alpha-background ()
"Increase current frame’s alpha background."
(interactive)
(set-frame-parameter nil
'alpha-background
(my/transparency-round
(min 1.0
(+ (frame-parameter nil 'alpha-background) 0.1))))
(message "%s" (frame-parameter nil 'alpha-background)))
(defun my/decrease-frame-alpha-background ()
"Decrease current frame’s alpha background."
(interactive)
(set-frame-parameter nil
'alpha-background
(my/transparency-round
(max 0.0
(- (frame-parameter nil 'alpha-background) 0.1))))
(message "%s" (frame-parameter nil 'alpha-background)))
(defhydra my/modify-frame-alpha-background ()
"
^Transparency^ ^Other^
^^^^^^^^^^^^^^------------------------
[_t_] decrease transparency [_q_] quit
[_s_] increase transparency
"
("q" nil :exit t)
("s" my/decrease-frame-alpha-background)
("t" my/increase-frame-alpha-background))
Packages Configuration
Autocompletion
Code Autocompletion
Company is, in my opinion, the best autocompleting engine for Emacs, and it is one of the most popular if not the most popular.
(use-package company
:straight (:build t)
:defer t
:hook (company-mode . evil-normalize-keymaps)
:init (global-company-mode)
:config
(setq company-minimum-prefix-length 2
company-toolsip-limit 14
company-tooltip-align-annotations t
company-require-match 'never
company-global-modes '(not erc-mode message-mode help-mode gud-mode)
company-frontends
'(company-pseudo-tooltip-frontend ; always show candidates in overlay tooltip
company-echo-metadata-frontend) ; show selected candidate docs in echo area
company-backends '(company-capf)
company-auto-commit nil
company-auto-complete-chars nil
company-dabbrev-other-buffers nil
company-dabbrev-ignore-case nil
company-dabbrev-downcase nil))
This package is a backend for company. It emulates
ac-source-dictionary
by proposing text related to the current
major-mode.
(use-package company-dict
:after company
:straight (:build t)
:config
(setq company-dict-dir (expand-file-name "dicts" user-emacs-directory)))
On the other hand, company-box
is a Company front-end which offers
colors, icons, documentation and so on. Very nice.
Declaring all the icons for the variable
company-box-icons-all-the-icons
is quite verbose in Elisp, so I do it
with an org-table.
Type | Icon | Color |
---|---|---|
Unknown | find_in_page | purple |
Text | text_fields | green |
Method | functions | red |
Function | functions | red |
Constructor | functions | red |
Field | functions | red |
Variable | adjust | blue |
Class | class | red |
Interface | settings_input_component | red |
Module | view_module | red |
Property | settings | red |
Unit | straighten | red |
Value | filter_1 | red |
Enum | plus_one | red |
Keyword | filter_center_focus | red |
Snippet | short_text | red |
Color | color_lens | red |
File | insert_drive_file | red |
Reference | collections_bookmark | red |
Folder | folder | red |
EnumMember | people | red |
Constant | pause_circle_filled | red |
Struct | streetview | red |
Event | event | red |
Operator | control_point | red |
TypeParameter | class | red |
Template | short_text | green |
ElispFunction | functions | red |
ElispVariable | check_circle | blue |
ElispFeature | stars | orange |
ElispFace | format_paint | pink |
(mapconcat (lambda (row)
(format "(%s . ,(all-the-icons-material \"%s\" :face 'all-the-icons-%s))"
(car row)
(cadr row)
(caddr row)))
table
"\n")
(use-package company-box
:straight (:build t)
:after (company all-the-icons)
:config
(setq company-box-show-single-candidate t
company-box-backends-colors nil
company-box-max-candidates 50
company-box-icons-alist 'company-box-icons-all-the-icons
company-box-icons-all-the-icons
(let ((all-the-icons-scale-factor 0.8))
`(
<<gen-company-box-icons()>>))))
Ivy
My main menu package is ivy
which I use as much as possible –I’ve
noticed helm
can be slow, very slow in comparison to ivy
so I’ll use
the latter as much as possible. Actually, only ivy
is installed for
now. I could have used ido
too, but I find it to be a bit too
restricted in terms of features compared to ivy
.
(use-package ivy
:straight (:build t)
:defer t
:diminish
:bind (("C-s" . swiper)
:map ivy-minibuffer-map
("TAB" . ivy-alt-done)
("C-l" . ivy-alt-done)
("C-t" . ivy-next-line)
("C-s" . ivy-previous-line)
("C-u" . ivy-scroll-up-command)
("C-d" . ivy-scroll-down-command)
:map ivy-switch-buffer-map
("C-t" . ivy-next-line)
("C-s" . ivy-previous-line)
("C-l" . ivy-done)
("C-d" . ivy-switch-buffer-kill)
:map ivy-reverse-i-search-map
("C-t" . ivy-next-line)
("C-s" . ivy-previous-line)
("C-d" . ivy-reverse-i-search-kill))
:config
(ivy-mode 1)
(setq ivy-wrap t
ivy-height 17
ivy-sort-max-size 50000
ivy-fixed-height-minibuffer t
ivy-read-action-functions #'ivy-hydra-read-action
ivy-read-action-format-function #'ivy-read-action-format-columns
projectile-completion-system 'ivy
ivy-on-del-error-function #'ignore
ivy-use-selectable-prompt t))
There is also prescient.el
that offers some nice features when
coupled with ivy
, guess what was born out of it? ivy-prescient
, of
course!
(use-package ivy-prescient
:after ivy
:straight (:build t))
I warned you I’d use too much all-the-icons
, I did!
(use-package all-the-icons-ivy
:straight (:build t)
:after (ivy all-the-icons)
:hook (after-init . all-the-icons-ivy-setup))
A buffer popping at the bottom of the screen is nice and all, but have you considered a floating buffer in the center of your frame?
(use-package ivy-posframe
:defer t
:after (:any ivy helpful)
:hook (ivy-mode . ivy-posframe-mode)
:straight (:build t)
:init
(ivy-posframe-mode 1)
:config
(setq ivy-fixed-height-minibuffer nil
ivy-posframe-border-width 10
ivy-posframe-parameters
`((min-width . 90)
(min-height . ,ivy-height))))
Something that can be missing sometimes in Ivy is the ability to
select multiple entries at once. For instance, when programming in
Java, LPS can offer you to automatically generate the methods equals
and hashCode
based on selected members, but with vanilla Ivy, you can
only select one. Not really useful. ivy-hydra
is a package that offers
a Hydra interface when in Ivy which allows you to select multiple
choices among other things.
(use-package ivy-hydra
:requires (ivy hydra)
:after ivy
:straight (:build t))
Finally, let’s make ivy
richer:
(use-package ivy-rich
:straight (:build t)
:after ivy
:init
(ivy-rich-mode 1))
Counsel
I could almost merge this chapter with the previous one since counsel
is a package that provides loads of completion functions for ivy. The
ones I find most useful are counsel-M-x
and counsel-find-file
.
(use-package counsel
:straight (:build t)
:after recentf
:after ivy
:bind (("M-x" . counsel-M-x)
("C-x b" . counsel-ibuffer)
("C-x C-f" . counsel-find-file)
:map minibuffer-local-map
("C-r" . 'counsel-minibuffer-history)))
Yasnippet
Yasnippet allows you to insert some pre-made code by just typing a few characters. It can even generate some string with Elisp expressions and ask the user for some input in some precise places.
(use-package yasnippet
:defer t
:straight (:build t)
:init
(yas-global-mode)
:hook ((prog-mode . yas-minor-mode)
(text-mode . yas-minor-mode)))
Of course, yasnippet wouldn’t be as awesome as it is without premade snippets.
(use-package yasnippet-snippets
:defer t
:after yasnippet
:straight (:build t))
Similarly, yatemplate offers premade files rather than just strings. That’s still yasnippet by the way.
(use-package yatemplate
:defer t
:after yasnippet
:straight (:build t))
And finally, with ivy you can chose your snippets from a menu if you’re not sure or if you don’t remember what your snippet is.
(use-package ivy-yasnippet
:defer t
:after (ivy yasnippet)
:straight (:build t)
:general
(phundrak/leader-key
:infix "i"
:packages 'ivy-yasnippet
"y" #'ivy-yasnippet))
Applications
Bitwarden
This package is still a very much work in progress one I’m developing in order to interact with Bitwarden in Emacs with the help of the Bitwarden CLI. Use at your own risks.
(use-package bitwarden
:defer t
:straight (bitwarden :build t
:type git
:host nil
:repo "https://labs.phundrak.com/phundrak/bitwarden.el"))
Docker
Docker is an awesome tool for reproducible development environments. Due to this, I absolutely need a mode for editing Dockerfiles.
(use-package dockerfile-mode
:defer t
:straight (:build t)
:hook (dockerfile-mode . lsp-deferred)
:init
(put 'docker-image-name 'safe-local-variable #'stringp)
:mode "Dockerfile\\'"
:general
(phundrak/major-leader-key
:keymaps 'general-mode-map
:packages 'lsp-mode
"l" '(:keymap lsp-command-map :which-key "lsp")))
The docker
package also provides interactivity with Docker and
docker-compose from Emacs.
(use-package docker
:defer t
:straight (:build t))
Elfeed
Elfeed is a nice Atom and RSS reader for Emacs. The only thing I want to change for now is the default search filter: I want to see not only unread news but read news as well, a bit like my emails; and where the database is to be stored.
(use-package elfeed
:defer t
:straight (:build t)
:config
<<elfeed-open-youtube-with-mpv>>
:custom
((elfeed-search-filter "@6-months-ago")
(elfeed-db-directory (expand-file-name ".elfeed-db"
user-emacs-directory))))
I don’t want YouTube videos to be open with my web browser when I
invoke elfeed-show-visit
, so I’ll advise this function so I can modify
the behavior of said function. Oh, and I already made a neat package
for playing YouTube videos and friends through youtube-dl or its
superior fork yt-dlp in mpv.
(defun my/elfeed-filter-youtube-videos (orig-fun &rest args)
"Open with mpv the video leading to PATH"
(let ((link (elfeed-entry-link elfeed-show-entry)))
(when link
(if (string-match-p ".*youtube\.com.*watch.*" link)
;; This is a YouTube video, open it with mpv
(progn
(require 'ytplay)
(ytplay link))
(apply orig-fun args)))))
(advice-add 'elfeed-show-visit :around #'my/elfeed-filter-youtube-videos)
A future improvement to be made is to let the user chose the resolution of the video before it is launched. I may not always have the best internet connection, and viewing 4K videos on a 1080p display is not something very useful.
Elfeed-goodies is a package which enhances the Elfeed experience. Aside from running its setup command as soon as possible, I also set in this code block all my keybinds for Elfeed here.
(use-package elfeed-goodies
:defer t
:after elfeed
:commands elfeed-goodies/setup
:straight (:build t)
:init
(elfeed-goodies/setup)
:general
(phundrak/undefine
:keymaps '(elfeed-show-mode-map elfeed-search-mode-map)
:packages 'elfeed
"DEL" nil
"s" nil)
(phundrak/evil
:keymaps 'elfeed-show-mode-map
:packages 'elfeed
<<general-keybindings-gen(table=elfeed-keybinds-show-mode)>>)
(phundrak/evil
:keymaps 'elfeed-search-mode-map
:packages 'elfeed
<<general-keybindings-gen(table=elfeed-keybinds-search-mode)>>)
(phundrak/major-leader-key
:keymaps 'elfeed-search-mode-map
:packages 'elfeed
<<general-keybindings-gen(table=elfeed-keybinds-search-mode-prefixed)>>))
Last but not least, my Elfeed configuration is stored in an org file
thanks to elfeed-org
.
(use-package elfeed-org
:defer t
:after elfeed
:straight (:build t)
:init
(elfeed-org)
:config
(setq rmh-elfeed-org-files '("~/org/elfeed.org")))
Keybinds
First, here are the keybinds for Elfeed’s elfeed-show-mode
. They
aren’t prefixed by SPC
like most of my keybinds, a direct keypress
will directly launch the function.
Key | Function | Comment |
---|---|---|
+ | elfeed-show-tag | |
- | elfeed-show-untag | |
« | elfeed-show-prev | |
» | elfeed-show-next | |
b | elfeed-show-visit | |
C | elfeed-kill-link-url-at-point | |
d | elfeed-show-save-enclosure | |
l | elfeed-show-next-link | |
o | elfeed-goodies/show-ace-link | |
q | elfeed-kill-buffer | |
S | elfeed-show-new-live-search | |
u | elfeed-show-tag–unread | |
y | elfeed-show-yank |
Same thing, different mode, here are my keybinds for
elfeed-search-mode
.
Key | Function | Comment |
---|---|---|
« | elfeed-search-first-entry | |
» | elfeed-search-last-entry | |
b | elfeed-search-browse-url | |
f | filter | |
fc | elfeed-search-clear-filter | |
fl | elfeed-search-live-filter | |
fs | elfeed-search-set-filter | |
u | update | |
us | elfeed-search-fetch | |
uS | elfeed-search-update | |
uu | elfeed-update | |
uU | elfeed-search-update–force | |
y | elfeed-search-yank |
I have some additional keybinds for elfeed-search-mode
, but these one
are prefixed with ,
(and M-m
).
Key | Function | Comment |
---|---|---|
c | elfeed-db-compact | |
t | tag | |
tt | elfeed-search-tag-all-unread | |
tu | elfeed-search-untag-all-unread | |
tT | elfeed-search-tag-all | |
tU | elfeed-search-untag-all |
Basic configuration
As seen below, I use org-msg
to compose my emails, which includes by
default my signature. Therefore, there is no need for Emacs itself to
know about it since I don’t want it to include it a second time after
org-msg
already did.
(setq message-signature nil
mail-signature nil)
Gnus
(use-package gnus
:straight (:type built-in)
:defer t
:config
(require 'gnus-topic)
(setq gnus-select-method '(nnnil))
(setq gnus-secondary-select-methods '((nntp "news.gwene.org")))
(setq gnus-asynchronous t ;; async
gnus-use-article-prefetch 15
;; article
gnus-visible-headers (mapcar (lambda (str) (concat "^" str ":"))
'("From" "To" "Cc" "Subject" "Newsgroup"
"Date" "Followup-To" "Reply-To"
"Organization" "X-Newsreader" "X-Mailer"))
gnus-sorted-header-list gnus-visible-headers
gnus-thread-sort-functions '(gnus-thread-sort-by-number
gnus-thread-sort-by-subject
(not gnus-thread-sort-by-date))
;; group
gnus-level-subscribed 6
gnus-level-unsubscribed 7
gnus-level-zombie 8
gnus-group-sort-function '((gnus-group-sort-by-unread)
(gnus-group-sort-by-alphabet)
(gnus-group-sort-by-rank))
gnus-group-line-format "%M%p%P%5y:%B%(%g%)\n"
gnus-group-mode-line-format "%%b"
gnus-topic-display-empty-topics nil
;; summary
gnus-auto-select-first nil
gnus-summary-ignore-duplicates t
gnus-suppress-duplicates t
gnus-summary-to-prefix "To:"
gnus-summary-line-format "%U%R %-18,18&user-date; %4L:%-25,25f %B%s\n"
gnus-summary-mode-line-format "[%U] %p"
gnus-sum-thread-tree-false-root ""
gnus-sum-thread-tree-indent " "
gnus-sum-thread-tree-single-indent ""
gnus-sum-thread-tree-leaf-with-other "+->"
gnus-sum-thread-tree-root ""
gnus-sum-thread-tree-single-leaf "\\->"
gnus-sum-thread-tree-vertical "|")
(add-hook 'dired-mode-hook #'gnus-dired-mode)
(add-hook 'gnus-group-mode-hook #'gnus-topic-mode)
(add-hook 'gnus-select-group-hook #'gnus-group-set-timestamp)
(dolist (mode '(gnus-group-mode-hook gnus-summary-mode-hook gnus-browse-mode-hook))
(add-hook mode #'hl-line-mode))
:general
(phundrak/evil
:keymaps 'gnus-summary-mode-map
:packages 'gnus
"«" #'gnus-summary-prev-article
"»" #'gnus-summary-next-article)
(phundrak/major-leader-key
:keymaps 'gnus-summary-mode-map
:packages 'gnus
"d" #'gnus-summary-delete-article
"f" #'gnus-summary-mail-forward
"r" '(:ignore t :wk "reply")
"rr" #'gnus-summary-reply-with-original
"rl" #'gnus-summary-reply-to-list-with-original
"rw" #'gnus-summary-wide-reply-with-original
"rW" #'gnus-summary-very-wide-reply-with-original)
(phundrak/evil
:keymaps 'gnus-group-mode-map
:packages 'gnus
"«" #'gnus-group-prev-group
"»" #'gnus-group-next-group)
(phundrak/major-leader-key
:keymaps '(gnus-group-mode-map)
:packages 'gnus
"SPC" #'gnus-topic-read-group
"c" '(gnus-topic-catchup-articles :which-key "catchup")
"f" '(gnus-fetch-group :which-key "fetch")
"j" '(:ignore t :which-key "jump")
"jg" #'gnus-group-jump-to-group
"jt" #'gnus-topic-jump-to-topic
"L" #'gnus-group-list-all-groups
"n" #'gnus-group-news
"t" '(gnus-group-topic-map :which-key "topics")
"u" #'gnus-group-unsubscribe))
Mu4e
Mu4e is a very eye-pleasing email client for Emacs, built around mu
and which works well with mbsync
(found in Arch’s isync
package). For
me, the main advantage of mu4e is it has a modern interface for
emailing, and quite straightforward. I tried a couple of other email
clients for Emacs, and I even was for some time a Gnus user, but in
the end, mu4e really works best for me. Below you’ll find my
configuration for the mu4e
package itself.
(use-package mu4e
:after all-the-icons
:straight (:build t :location site)
:commands mu4e mu4e-compose-new
:init
(csetq mu4e-completing-read-function 'completing-read
mu4e-use-fancy-chars t
message-kill-buffer-on-exit t
mu4e-org-support nil)
(let ((dir (concat (getenv "HOME") "/Downloads/mu4e")))
(when (file-directory-p dir)
(csetq mu4e-attachment-dir dir)))
(defmacro mu4e-view-mode--prepare ()
`(lambda () (visual-line-mode 1)))
:gfhook ('mu4e-view-mode-hook (mu4e-view-mode--prepare))
:general
(phundrak/evil
:keymaps 'mu4e-main-mode-map
:packages 'mu4e
"U" #'mu4e-update-index)
:config
(require 'mu4e-view)
(with-eval-after-load 'mm-decode
(add-to-list 'mm-discouraged-alternatives "text/html")
(add-to-list 'mm-discouraged-alternatives "text-richtext"))
(add-hook 'mu4e-view-mode-hook (lambda () (setq truncate-lines nil)))
(add-hook 'mu4e-headers-mode-hook (lambda () (setq truncate-lines t)))
<<mu4e-keybindings-undef>>
<<mu4e-keybindings-view>>
<<mu4e-keybindings-view-no-prefix>>
<<mu4e-keybindings-header>>
<<mu4e-keybindings-header-no-leader>>
<<mu4e-keybindings-message>>
<<mu4e-mail-service>>
<<mu4e-mail-on-machine>>
<<mu4e-no-signature>>
<<mu4e-bookmarks>>
<<mu4e-maildirs>>
(when (fboundp 'imagemagick-register-types)
(imagemagick-register-types))
(require 'gnus-dired)
(setq gnus-dired-mail-mode 'mu4e-user-agent)
(add-hook 'mu4e-compose-mode-hook (lambda () (use-hard-newlines t 'guess)))
(add-hook 'mu4e-compose-mode-hook 'mml-secure-message-sign-pgpmime)
(setq mu4e-change-filenames-when-moving t
mu4e-update-interval 60
mu4e-compose-format-flowed t
mu4e-view-show-addresses t
mu4e-sent-messages-behaviour 'sent
mu4e-hide-index-messages t
mu4e-view-show-images t ; try to show images
mu4e-view-image-max-width 600
message-send-mail-function #'smtpmail-send-it ; how to send an email
smtpmail-stream-type 'starttls
message-kill-buffer-on-exit t ; close after sending
mu4e-context-policy 'pick-first ; start with first (default) context
mu4e-compose-context-policy 'ask-if-none ; compose with current context, or ask
mu4e-completing-read-function #'ivy-completing-read ; use ivy
mu4e-confirm-quit t ; no need to ask
mu4e-header-fields '((:account . 12)
(:human-date . 12)
(:flags . 4)
(:from . 25)
(:subject)))
;; set mail user agent
(setq mail-user-agent 'mu4e-user-agent)
<<mu4e-fancy-marks>>
<<mu4e-vertical-split>>
<<mu4e-headers-mode>>
(defun mu4e-action-open-as-pdf (msg)
"Export and open MSG as pdf."
(let* ((date (mu4e-message-field msg :date))
(infile (mu4e~write-body-to-html msg))
(outfile (format-time-string "/tmp/%Y-%m-%d-%H-%M-%S.pdf" date)))
(with-temp-buffer
(shell-command
(format "wkhtmltopdf %s %s" infile outfile) t))
(find-file outfile))))
Quick sidenote: on ArchLinux, you’ll need to install either mu
or
mu-git
from the AUR in order to use mu4e. I also have a .desktop
file
so I can open mu4e directly from my program picker. It uses the shell
script emacsmail
I’ve written here.
[Desktop Entry]
Name=Mu4e
GenericName=Mu4e
Comment=Maildir Utils for Emacs
MimeType=x-scheme-handler/mailto;
Exec=/home/phundrak/.local/bin/emacsmail %U
Icon=emacs
Type=Application
Terminal=false
Categories=Network;Email;TextEditor
StartupWMClass=Gnus
Keywords=Text;Editor;
Basic configuration
First, let’s inform Emacs how it can send emails, using which service and how. In my case, I use my own mail server.
(setq smtpmail-smtp-server "mail.phundrak.com"
smtpmail-smtp-service 587
smtpmail-stream-type 'starttls
message-send-mail-function 'smtpmail-send-it)
We also need to inform it on where my emails are stored on my machine, and how to retrieve them.
(setq mu4e-get-mail-command "mbsync -a"
mu4e-root-maildir "~/Mail"
mu4e-trash-folder "/Trash"
mu4e-refile-folder "/Archive"
mu4e-sent-folder "/Sent"
mu4e-drafts-folder "/Drafts")
In the same vein of this bit of configuration, I do not want mu4e to
insert my mail signature, org-msg
already does that.
(setq mu4e-compose-signature nil)
Bookmarks
In mu4e, the main focus isn’t really mail directories such as your inbox, your sent messages and such, but instead you manipulate bookmarks which will show you emails depending on tags. This mean you can create some pretty customized bookmarks that go way beyound your simple inbox, outbox and all. Actually, four of my bookmarks have a couple of filtering:
- anything in my inbox linked to my university
- the emacs-doctor mailing list (French Emacs mailing list)
- emails related to my internship
- and my inbox for any mail not caught by any of these filters
And all of them will have the requirement not to display any trashed email. Actually, all of my bookmarks will have this requirement, except for the bookmark dedicated to them as well as my sent emails. I’ll add these latter requirements later.
Here are the requirements for my university bookmark. The regex
matches any email address which contains either up8.edu
or
univ-paris8
, which can be found in email addresses from the University
Paris 8 (my university).
(mapconcat (lambda (list)
(let ((address (string-replace (regexp-quote "~") "" (car list))))
(mapconcat (lambda (flag)
(concat flag ":" address))
'("list" "t" "f" "contact")
" OR ")))
lists
" OR ")
(let ((regex "/.*up8\\.edu|.*univ-paris8.*/"))
(concat
<<mu4e-bookmarks-from-copy-to-gen>>
" OR maildir:/Univ/Inbox OR maildir:/Univ/Junk"))
f:/.*up8\.edu|.*univ-paris8.*/ OR c:/.*up8\.edu|.*univ-paris8.*/ OR t:/.*up8\.edu|.*univ-paris8.*/ OR maildir:/Univ/Inbox OR maildir:/Univ/Junk
Next I need an inbox dedicated to the association I’m part of.
(let ((regex "/.*supran\\.fr/"))
<<mu4e-bookmarks-from-copy-to-gen>>)
f:/.*supran\.fr/ OR c:/.*supran\.fr/ OR t:/.*supran\.fr/
As for the Emacs-doctor list, I need to match both the current, modern
mailing list address but also its old address. The same applies for
the emacs-devel mailing list as well as Github emails related to my
package eshell-info-banner.el
(see here). Here are the addresses to
match:
/ateliers.*emacs.*/
/emacs-.*@gnu.org/
/.*eshell-info-banner.*/
/.*emacsfr.*/
"<<mu4e-bookmarks-mailing-lists(lists=mu4e-emacs-mailing-lists)>>"
list:/ateliers.*emacs.*/ OR t:/ateliers.*emacs.*/ OR f:/ateliers.*emacs.*/ OR contact:/ateliers.*emacs.*/ OR list:/emacs-.*@gnu.org/ OR t:/emacs-.*@gnu.org/ OR f:/emacs-.*@gnu.org/ OR contact:/emacs-.*@gnu.org/ OR list:/.*eshell-info-banner.*/ OR t:/.*eshell-info-banner.*/ OR f:/.*eshell-info-banner.*/ OR contact:/.*eshell-info-banner.*/ OR list:/.*emacsfr.*/ OR t:/.*emacsfr.*/ OR f:/.*emacsfr.*/ OR contact:/.*emacsfr.*/
Another bookmark I wish to have is one dedicated to emails related to more general development topics, including issues and PRs from Github.
/.*\\.github\\.com/
/.*\\.gitlab\\.com/
stumpwm-devel@nongnu.org
/.*sr\\.ht/
;; "<<mu4e-bookmarks-mailing-lists(lists=mu4e-github-mailing-lists)>> AND NOT ()"
(string-join '("<<mu4e-bookmarks-mailing-lists(lists=mu4e-github-mailing-lists)>>"
"AND NOT ("
<<mu4e-bookmarks-filter-emacs-list>>
")")
" ")
list:/.*\.github\.com/ OR t:/.*\.github\.com/ OR f:/.*\.github\.com/ OR contact:/.*\.github\.com/ OR list:/.*\.gitlab\.com/ OR t:/.*\.gitlab\.com/ OR f:/.*\.gitlab\.com/ OR contact:/.*\.gitlab\.com/ OR list:stumpwm-devel@nongnu.org OR t:stumpwm-devel@nongnu.org OR f:stumpwm-devel@nongnu.org OR contact:stumpwm-devel@nongnu.org OR list:/.*sr\.ht/ OR t:/.*sr\.ht/ OR f:/.*sr\.ht/ OR contact:/.*sr\.ht/ AND NOT ( list:/ateliers.*emacs.*/ OR t:/ateliers.*emacs.*/ OR f:/ateliers.*emacs.*/ OR contact:/ateliers.*emacs.*/ OR list:/emacs-.*@gnu.org/ OR t:/emacs-.*@gnu.org/ OR f:/emacs-.*@gnu.org/ OR contact:/emacs-.*@gnu.org/ OR list:/.*eshell-info-banner.*/ OR t:/.*eshell-info-banner.*/ OR f:/.*eshell-info-banner.*/ OR contact:/.*eshell-info-banner.*/ OR list:/.*emacsfr.*/ OR t:/.*emacsfr.*/ OR f:/.*emacsfr.*/ OR contact:/.*emacsfr.*/ )
When it comes to my internship, all emails will contain an address containing aubay.com
(that’s where my internship takes place).
/.*aubay.com/
(let ((regex "/.*aubay\\.com/"))
<<mu4e-bookmarks-from-copy-to-gen>>)
f:/.*aubay\.com/ OR c:/.*aubay\.com/ OR t:/.*aubay\.com/
As I said earlier, something that will often come back in my bookmarks is the emails must not be trashed to appear. I want also to display junk emails, so I end up with the following rule:
(mapconcat #'identity
`("NOT flag:trashed"
,(format "(%s)" (mapconcat (lambda (maildir) (concat "maildir:" maildir))
'("/Inbox" "/Junk")
" OR ")))
" AND ")
NOT flag:trashed AND (maildir:/Inbox OR maildir:/Junk)
And for the last string-generating code, let’s describe my main inbox:
(mapconcat #'identity
(cons "<<mu4e-bookmarks-default-filter()>>"
`(,(format "(%s)"
<<mu4e-bookmarks-filter-aubay>>)
,(format "(%s)" "<<mu4e-bookmarks-filter-asso()>>")
,(format "(%s)"
<<mu4e-bookmarks-filter-emacs-list>>)
,(format "(%s)"
<<mu4e-bookmarks-filter-github-list>>)
,(format "(%s)"
<<mu4e-bookmarks-filter-uni>>)))
" AND NOT ")
NOT flag:trashed AND (maildir:/Inbox OR maildir:/Junk) AND NOT (f:/.*aubay\.com/ OR c:/.*aubay\.com/ OR t:/.*aubay\.com/) AND NOT (f:/.*supran.fr/ OR c:/.*supran.fr/ OR t:/.*supran.fr/) AND NOT (list:/ateliers.*emacs.*/ OR t:/ateliers.*emacs.*/ OR f:/ateliers.*emacs.*/ OR contact:/ateliers.*emacs.*/ OR list:/emacs-.*@gnu.org/ OR t:/emacs-.*@gnu.org/ OR f:/emacs-.*@gnu.org/ OR contact:/emacs-.*@gnu.org/ OR list:/.*eshell-info-banner.*/ OR t:/.*eshell-info-banner.*/ OR f:/.*eshell-info-banner.*/ OR contact:/.*eshell-info-banner.*/ OR list:/.*emacsfr.*/ OR t:/.*emacsfr.*/ OR f:/.*emacsfr.*/ OR contact:/.*emacsfr.*/) AND NOT (list:/.*\.github\.com/ OR t:/.*\.github\.com/ OR f:/.*\.github\.com/ OR contact:/.*\.github\.com/ OR list:/.*\.gitlab\.com/ OR t:/.*\.gitlab\.com/ OR f:/.*\.gitlab\.com/ OR contact:/.*\.gitlab\.com/ OR list:stumpwm-devel@nongnu.org OR t:stumpwm-devel@nongnu.org OR f:stumpwm-devel@nongnu.org OR contact:stumpwm-devel@nongnu.org OR list:/.*sr\.ht/ OR t:/.*sr\.ht/ OR f:/.*sr\.ht/ OR contact:/.*sr\.ht/ AND NOT ( list:/ateliers.*emacs.*/ OR t:/ateliers.*emacs.*/ OR f:/ateliers.*emacs.*/ OR contact:/ateliers.*emacs.*/ OR list:/emacs-.*@gnu.org/ OR t:/emacs-.*@gnu.org/ OR f:/emacs-.*@gnu.org/ OR contact:/emacs-.*@gnu.org/ OR list:/.*eshell-info-banner.*/ OR t:/.*eshell-info-banner.*/ OR f:/.*eshell-info-banner.*/ OR contact:/.*eshell-info-banner.*/ OR list:/.*emacsfr.*/ OR t:/.*emacsfr.*/ OR f:/.*emacsfr.*/ OR contact:/.*emacsfr.*/ )) AND NOT (f:/.*up8\.edu|.*univ-paris8.*/ OR c:/.*up8\.edu|.*univ-paris8.*/ OR t:/.*up8\.edu|.*univ-paris8.*/ OR maildir:/Univ/Inbox OR maildir:/Univ/Junk)
We can finally define our bookmarks! The code reads as follows:
(setq mu4e-bookmarks
`((:name "Inbox"
:key ?i
:query ,(format "%s"
<<mu4e-bookmarks-inbox-filters>>))
(:name "Internship"
:key ?a
:query ,(format "(%s) AND (%s)"
"<<mu4e-bookmarks-default-filter()>>"
<<mu4e-bookmarks-filter-aubay>>))
(:name "University"
:key ?u
:query ,(format "%s AND %s"
"<<mu4e-bookmarks-default-filter()>>"
"<<mu4e-bookmarks-filter-uni()>>"))
(:name "Dev"
:key ?d
:query ,(format "%s AND (%s)"
"<<mu4e-bookmarks-default-filter()>>"
"<<mu4e-bookmarks-filter-github-list()>>"))
(:name "Emacs"
:key ?e
:query ,(format "%s AND %s"
"<<mu4e-bookmarks-default-filter()>>"
<<mu4e-bookmarks-filter-emacs-list>>))
(:name "Supran"
:key ?s
:query ,(format "%s AND %s"
"<<mu4e-bookmarks-default-filter()>>"
"<<mu4e-bookmarks-filter-asso()>>"))
(:name "Sent" :key ?S :query "maildir:/Sent OR maildir:/Univ/Sent")
(:name "All Unread" :key ?U :query "flag:unread AND NOT flag:trashed")
(:name "Today" :key ?t :query "date:today..now AND NOT flag:trashed")
(:name "This Week" :key ?w :query "date:7d..now AND NOT flag:trashed")
(:name "This Month" :key ?m :query "date:1m..now AND NOT flag:trashed")
(:name "This Year" :key ?y :query "date:1y..now AND NOT flag:trashed")))
Maildirs
Sometimes, bookmarks are a bit too restrictive when I want to search
for stuff. Simply jumping to a mail directory, or maildir, is more
than enough. In mu4e, with my current setup, I can jump to any maildir
with the shortcut Jo
in normal-mode, and I just need to chose in a
list the maildir I want to jump to. But it can be faster.
With this piece of code, I can simply jump to my inbox maildir with
Ji
, to my sent messages with Js
, and so on.
(setq mu4e-maildir-shortcuts
'((:maildir "/Inbox" :key ?i)
(:maildir "/Sent" :key ?s)
(:maildir "/Junk" :key ?j)
(:maildir "/Trash" :key ?t)))
Dealing with spammers
I’m sure you have received at least one email recently from a sketchy email address asking you something that might be completely unrelated to what you do, or at least somewhat related. Fortunately, we have a hero! Now, let me write a function that will insert their pre-written text at point so I don’t have to go back to their Twitter thread each time I want to shut spammers up.
(defun reply-to-bill ()
(interactive)
(insert "Please forward this email to bill@noprocurement.com,
and delete my email, as I’ll be changing jobs soon, and this
email address will no longer be active.
Bill Whiskoney is a senior partner at Nordic Procurement
Services, and he handles our budget and will help you further or
introduce you to someone who can."))
If you want the full story, make sure to read the whole thread, I guarantee it, it’s worth your time! And in case the Twitter thread disappear in the future, here is a backup.
Getting Fancy
I’m not a huge fan of mu4e’s default icons marking my emails, so I’ll
redefine them as follows. Be aware the name of these icons are from
faicon in the package all-the-icons
.
Mark | Flag | Icon |
---|---|---|
draft | D | pencil |
flagged | F | flag |
new | N | rss |
passed | P | check |
replied | R | reply |
seen | S | eye |
unread | u | eye-slash |
trashed | T | trash |
attach | a | paperclip |
encrypted | x | lock |
signed | s | certificate |
(mapconcat (lambda (line)
(let ((mark (car line))
(flag (cadr line))
(icon (caddr line)))
(format "mu4e-headers-%s-mark `(\"%s\" . ,(all-the-icons-faicon \"%s\" :height 0.8))"
mark
flag
icon)))
table
"\n")
mu4e-headers-draft-mark `("D" . ,(all-the-icons-faicon "pencil" :height 0.8)) mu4e-headers-flagged-mark `("F" . ,(all-the-icons-faicon "flag" :height 0.8)) mu4e-headers-new-mark `("N" . ,(all-the-icons-faicon "rss" :height 0.8)) mu4e-headers-passed-mark `("P" . ,(all-the-icons-faicon "check" :height 0.8)) mu4e-headers-replied-mark `("R" . ,(all-the-icons-faicon "reply" :height 0.8)) mu4e-headers-seen-mark `("S" . ,(all-the-icons-faicon "eye" :height 0.8)) mu4e-headers-unread-mark `("u" . ,(all-the-icons-faicon "eye-slash" :height 0.8)) mu4e-headers-trashed-mark `("T" . ,(all-the-icons-faicon "trash" :height 0.8)) mu4e-headers-attach-mark `("a" . ,(all-the-icons-faicon "paperclip" :height 0.8)) mu4e-headers-encrypted-mark `("x" . ,(all-the-icons-faicon "lock" :height 0.8)) mu4e-headers-signed-mark `("s" . ,(all-the-icons-faicon "certificate" :height 0.8))
Let’s enable them and set them:
(setq mu4e-use-fancy-chars t
<<mu4e-fancy-marks-gen()>>)
(defun my/set-mu4e-headers-width ()
(let ((width (window-body-width))
(threshold (+ 120 80)))
(setq mu4e-split-view (if (> width threshold)
'vertical
'horizontal))))
(setq mu4e-headers-visible-columns 120
mu4e-headers-visible-lines 15)
(add-hook 'mu4e-headers-mode-hook #'my/set-mu4e-headers-width)
Headers mode
(add-hook 'mu4e-headers-mode-hook (lambda () (visual-line-mode -1)))
(add-hook 'mu4e-headers-mode-hook (lambda () (toggle-truncate-lines -1)))
Keybindings
By default, Evil has some pretty annoying keybindings for users of the
bépo layout: hjkl
becomes ctsr
for us. Let’s undefine some of these:
(phundrak/undefine
:keymaps 'mu4e-view-mode-map
:packages 'mu4e
"S" nil
"r" nil
"c" nil
"gu" nil)
(phundrak/undefine
:keymaps '(mu4e-view-mode-map mu4e-headers-mode-map)
:packages 'mu4e
"s" nil)
Now, let’s define some keybindings for mu4e’s view mode, that is when
we are viewing an email. All these keybindings will reside between the
major-mode specific leader key ,
and most of these keybindings can be
described with a simple function:
Keybinding | Function | Description |
---|---|---|
& | mu4e-view-pipe | |
. | mu4e-headers-split-adjust-width/body | mu4e-headers width |
a | nil | attachments |
a& | mu4e-view-pipe-attachment | |
aa | mu4e-view-attachment-action | |
ao | mu4e-view-open-attachment | |
aO | mu4e-view-open-attachment-with | |
c | nil | compose |
cc | mu4e-compose-new | |
ce | mu4e-compose-edit | |
cf | mu4e-compose-forward | |
cr | mu4e-compose-reply | |
cR | mu4e-compose-resend | |
l | mu4e-show-log | |
m | nil | mark |
md | mu4e-view-mark-for-trash | |
mD | mu4e-view-mark-for-delete | |
mm | mu4e-view-mark-for-move | |
mr | mu4e-view-mark-for-refile | |
mR | mu4e-view-mark-for-read | |
mu | mu4e-view-mark-for-unread | |
mU | mu4e-view-mark-for-unmark | |
t | mu4e-view-mark-thread | mark thread |
T | nil | toggle |
Tc | mu4e-view-toggle-hide-cited | |
Th | mu4e-view-toggle-html | |
n | mu4e-view-headers-next | |
N | mu4e-view-headers-next-unread | |
p | mu4e-view-headers-prev | |
P | mu4e-view-headers-prev-unread | |
u | nil | url |
uf | mu4e-view-go-to-url | |
uF | mu4e-view-fetch-url |
(phundrak/major-leader-key
:keymaps 'mu4e-view-mode-map
:packages 'mu4e
<<general-keybindings-gen(table=mu4e-keybindings-view-tbl)>>)
Two other keybinds are added without a prefix, just for the sake of convenience.
(phundrak/evil
:keymaps 'mu4e-view-mode-map
:packages 'mu4e
"«" #'mu4e-view-headers-prev
"»" #'mu4e-view-headers-next)
I’ll also declare two keybinds for mu4e’s headers mode.
(phundrak/major-leader-key
:keymaps 'mu4e-headers-mode-map
:packages 'mu4e
"t" '(mu4e-view-mark-thread :which-key "mark thread")
"s" 'swiper)
I will also redefine without a leader key ctsr
in order to be able to
move freely (remember, bépo layout for me).
Key | Function | Comment |
---|---|---|
c | evil-backward-char | |
t | evil-next-visual-line | |
s | evil-previous-visual-line | |
r | evil-forward-char |
(phundrak/evil
:keymaps 'mu4e-headers-mode-map
:packages 'mu4e
<<general-keybindings-gen(table=mu4e-keybindings-header-no-leader-table)>>)
Finally, let’s declare a couple of keybindings for when we are composing a message. This time, all my keybindings are prefixed with the major-mode leader and call a simple function.
Key | Function | Description |
---|---|---|
, | message-send-and-exit | |
c | message-send-and-exit | |
a | message-kill-buffer | |
k | message-kill-buffer | |
s | message-dont-send | |
f | mml-attach-file |
(phundrak/major-leader-key
:keymaps 'message-mode-map
:packages 'mu4e
<<general-keybindings-gen(table=mu4e-keybindings-message-tbl)>>)
Composing messages
Org mime is cool and all, you can write some org-mode and then export it so you can send an HTML email. BUT, have you considered skipping the export part and write your emails directly in org-mode?
(use-package org-msg
:after (mu4e)
:straight (:build t)
:hook ((mu4e-compose-pre . org-msg-mode))
:custom-face
(mu4e-replied-face ((t (:weight normal :foreground "#b48ead"))))
:config
(defun my/org-msg-signature-convert (orig-fun &rest args)
"Tweak my signature when replying as plain/text only."
(let ((res (apply orig-fun args)))
(when (equal (cadr args) '(text))
(setf (alist-get 'signature res)
(replace-regexp-in-string "\n+" "\n" org-msg-signature)))
res))
(advice-add 'org-msg-composition-parameters
:around 'my/org-msg-signature-convert)
(add-hook 'mu4e-headers-mode (lambda () (toggle-truncate-lines -1)))
(setq org-msg-startup "inlineimages"
org-msg-default-alternatives '((new . (text))
(reply-to-html . (text))
(reply-to-text . (text)))
org-msg-greeting-name-limit 3
org-msg-signature (format "\n--\n#+begin_signature\n%s\n#+end_signature"
(with-temp-buffer
(insert-file-contents mail-signature-file)
(while (re-search-forward "\n" nil t)
(replace-match "\n\n"))
(buffer-string))))
:general
(phundrak/major-leader-key
:keymaps 'org-msg-edit-mode-map
:packages 'org-msg
<<general-keybindings-gen(table=org-msg-edit-mode-keybinds)>>))
The keybinds are relatively simple org-msg-edit-mode
:
Key | Function | Description |
---|---|---|
, | message-send-and-exit | |
c | message-send-and-exit | |
a | message-kill-buffer | |
k | message-kill-buffer | |
s | message-dont-send | |
f | org-msg-attach |
Email alerts
There is also a package for mu4e which generates desktop notifications when new emails are received. By default, I want to be notified by all messages in my inbox and junk folder. Also, I’ll use Emacs’ default notification system, and I’ll activate the modeline notification.
(use-package mu4e-alert
:straight (:build t)
:after mu4e
:defer t
:init
(add-hook 'after-init-hook #'mu4e-alert-enable-notifications)
(add-hook 'after-init-hook #'mu4e-alert-enable-mode-line-display)
(mu4e-alert-set-default-style 'notifications)
:config
(setq mu4e-alert-interesting-mail-query "flag:unread"))
EMMS and Media
EMMS, also known as the Emacs MultiMedia System, allows the user to interact through Emacs with multimedia elements such as music and videos. My main use for it will be for music with MPD (see its configuration here).
(use-package emms
:defer t
:after all-the-icons
:straight (:build t)
:init
(require 'emms-setup)
(require 'emms-mark)
(emms-all)
(add-to-list 'emms-info-functions 'emms-info-mpd)
(add-to-list 'emms-player-list 'emms-player-mpd)
(emms-player-mpd-sync-from-mpd)
(emms-player-mpd-connect)
<<emms-fd-name>>
<<emms-fd-function>>
<<emms-search-set-variable>>
<<emms-media-hydra>>
(defun emms-player-toggle-pause ()
(interactive)
(shell-command-and-echo "mpc toggle"))
:custom
((emms-source-file-default-directory (expand-file-name "~/Music"))
(emms-player-mpd-server-name "localhost")
(emms-player-mpd-server-port "6600")
(emms-player-mpd-music-directory (expand-file-name "~/Music"))
(emms-browser-thumbnail-small-size 64)
(emms-browser-thumbnail-medium-size 128)
(emms-browser-covers #'emms-browser-cache-thumbnail-async)
(emms-playlist-default-major-mode 'emms-mark-mode))
:general
(phundrak/undefine
:keymaps 'emms-browser-mode-map
:packages 'emms
"s" nil
"r" nil)
(phundrak/evil
:keymaps 'emms-browser-mode-map
:packages 'emms
"a" #'emms-browser-add-tracks
"A" #'emms-browser-add-tracks-and-play
"b" '(:ignore t :which-key "browse by")
"bA" #'emms-browse-by-album
"ba" #'emms-browse-by-artist
"bg" #'emms-browse-by-genre
"bs" #'emms-smart-browse
"by" #'emms-browse-by-year
"c" #'emms-browser-clear-playlist
"S" '(:ignore t :which-key "search")
"SA" '(emms-browser-search-by-album :which-key "by album")
"Sa" '(emms-browser-search-by-artist :which-key "by artist")
"Ss" '(emms-browser-search-by-names :which-key "by name")
"St" '(emms-browser-search-by-names :which-key "by title")
"q" #'kill-this-buffer)
(phundrak/evil
:keymaps 'emms-playlist-mode-map
:packages 'emms
"d" #'emms-playlist-mode-kill-track
"p" #'emms-playlist-mode-play-smart
"q" #'kill-this-buffer)
(phundrak/leader-key
:infix "m"
:packages 'emms
"" '(:ignore t :which-key "media")
"." #'hydra-media/body
"«" #'emms-player-mpd-previous
"»" #'emms-player-mpd-next
"c" #'emms-player-mpd-clear
"e" '(:ignore t :which-key "emms")
"eb" #'emms-browser
"ep" #'emms-playlist-mode-go
"es" #'emms-player-mpd-show
"p" '((lambda ()
(interactive)
(shell-command-and-echo "mpc toggle"))
:which-key "mpc toggle")
"s" #'emms-stop
"u" '(:ignore t :which-key "update")
"um" #'emms-player-mpd-update-all
"uc" #'emms-cache-set-from-mpd-all))
Finding files from EMMS
EMMS has two default ways of finding files: either a built-in function
relatively slow but portable, or with find
which is arguably faster
but less portable. Honestly, I don’t care about portability, I’ll
always use this Emacs config on Linux, but I don’t want to use find
either since there is something even faster: fd
.
First we’ll declare a variable that can hold the path to the fd
executable:
(defvar emms-source-file-fd (executable-find "fd"))
Then, we need to declare a new function that will use fd
to find
files. The function, as specified by the documentation of
emms-source-file-directory-tree-function
, receives two arguments dir
and regex
. We can work with that!
(defun emms-source-file-directory-tree-fd (dir regex)
"Return a list of all files under DIR that match REGEX.
This function uses the external fd utility. The name for fd may
be supplied using `emms-source-file-fd'."
(with-temp-buffer
(call-process emms-source-file-fd
nil t nil
"-L" ; follow symlinks
regex
"-t f"
(expand-file-name dir))
(delete ""
(split-string (buffer-substring (point-min)
(point-max))
"\n"))))
We can finally set this function as our search function.
(setq emms-source-file-directory-tree-function #'emms-source-file-directory-tree-fd)
Keybinds
I also want to create a small hydra for manipulating MPD:
(defun shell-command-and-echo (command &optional echo prefix)
"Run COMMAND and display the result of ECHO prefixed by PREFIX.
Run COMMAND as a shell command.
If ECHO is non nil, display the result of its execution as a
shell command to the minibuffer through `MESSAGE'.
If PREFIX is non nil, it will prefix the output of ECHO in the
minibuffer, both separated by a single space."
(progn
(with-temp-buffer
(shell-command command
(current-buffer)
(current-buffer))
(when echo
(message "%s%s"
(if prefix
(concat prefix " ")
"")
(string-trim
(shell-command-to-string "mpc volume")))))))
(defhydra hydra-media (:hint nil)
"
%s(all-the-icons-material \"volume_up\" :height 1.0 :v-adjust -0.2)
[_s_]
« [_c_] _p_ [_r_] » [_S_] %s(all-the-icons-material \"stop\")
[_t_]
%s(all-the-icons-material \"volume_down\" :height 1.0 :v-adjust -0.2)
"
("c" emms-player-mpd-previous)
("r" emms-player-mpd-next)
("t" (shell-command-and-echo "mpc volume -2" "mpc volume" "mpc"))
("s" (shell-command-and-echo "mpc volume +2" "mpc volume" "mpc"))
("p" (shell-command-and-echo "mpc toggle"))
("S" emms-player-mpd-stop)
("q" nil :exit t))
Nov
Nov is a major-mode for reading EPUB files within Emacs. Since I have it, I don’t need any other Epub reader on my computer! Plus this one is customizable and programmable, why would I use any other EPUB reader?
(use-package nov
:straight (:build t)
:defer t
:mode ("\\.epub\\'" . nov-mode)
:general
(phundrak/evil
:keymaps 'nov-mode-map
:packages 'nov
"c" #'nov-previous-document
"t" #'nov-scroll-up
"C-d" #'nov-scroll-up
"s" #'nov-scroll-down
"C-u" #'nov-scroll-down
"r" #'nov-next-document
"gm" #'nov-display-metadata
"gn" #'nov-next-document
"gp" #'nov-previous-document
"gr" #'nov-render-document
"gt" #'nov-goto-toc
"gv" #'nov-view-source
"gV" #'nov-view-content-source)
:config
(setq nov-text-width 95))
PDF Tools
pdf-tools
enables PDF support for Emacs, much better than its built-in
support with DocView. Aside from the classical settings such as
keybinds, I also enable the midnight colors by default; think of it as
an equivalent of Zathura’s recolor feature which kind of enables a
dark mode for PDFs.
(use-package pdf-tools
:defer t
:magic ("%PDF" . pdf-view-mode)
:straight (:build t)
:hook (pdf-tools-enabled . pdf-view-midnight-minor-mode)
:general
(phundrak/evil
:keymaps 'pdf-view-mode-map
:packages 'pdf-tools
"y" #'pdf-view-kill-ring-save
"t" #'evil-collection-pdf-view-next-line-or-next-page
"s" #'evil-collection-pdf-view-previous-line-or-previous-page)
(phundrak/major-leader-key
:keymaps 'pdf-view-mode-map
:packages 'pdf-tools
"a" '(:ignore t :which-key "annotations")
"aD" #'pdf-annot-delete
"at" #'pdf-annot-attachment-dired
"ah" #'pdf-annot-add-highlight-markup-annotation
"al" #'pdf-annot-list-annotations
"am" #'pdf-annot-markup-annotation
"ao" #'pdf-annot-add-strikeout-markup-annotation
"as" #'pdf-annot-add-squiggly-markup-annotation
"at" #'pdf-annot-add-text-annotation
"au" #'pdf-annot-add-underline-markup-annotation
"f" '(:ignore t :which-key "fit")
"fw" #'pdf-view-fit-width-to-window
"fh" #'pdf-view-fit-height-to-window
"fp" #'pdf-view-fit-page-to-window
"s" '(:ignore t :which-key "slice/search")
"sb" #'pdf-view-set-slice-from-bounding-box
"sm" #'pdf-view-set-slice-using-mouse
"sr" #'pdf-view-reset-slice
"ss" #'pdf-occur
"o" 'pdf-outline
"m" 'pdf-view-midnight-minor-mode)
:config
(with-eval-after-load 'pdf-view
(csetq pdf-view-midnight-colors '("#d8dee9" . "#2e3440"))))
One thing pdf-tools
doesn’t handle is restoring the PDF to the last
point it was visited — in other words, open the PDF where I last
left it.
(use-package pdf-view-restore
:after pdf-tools
:defer t
:straight (:build t)
:hook (pdf-view-mode . pdf-view-restore-mode)
:config
(setq pdf-view-restore-filename (expand-file-name ".tmp/pdf-view-restore"
user-emacs-directory)))
Project Management
Magit
Magit is an awesome wrapper around Git for Emacs! Very often, I go from disliking to really hating Git GUI clients because they often obfuscate which Git commands are used to make things happen. Such a thing doesn’t happen with Magit, it’s pretty transparent but it still provides some awesome features and visualizations of what you are doing and what Git is doing! In short, I absolutely love it!
(use-package magit
:straight (:build t)
:defer t
:init
(setq forge-add-default-bindings nil)
:config
(csetq magit-clone-default-directory "~/fromGIT/"
magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)
(with-eval-after-load 'evil-collection
(phundrak/evil
:packages '(evil-collection magit)
:keymaps '(magit-mode-map magit-log-mode-map magit-status-mode-map)
:states 'normal
"t" #'magit-tag
"s" #'magit-stage))
:general
(:keymaps '(git-rebase-mode-map)
:packages 'magit
"C-t" #'evil-next-line
"C-s" #'evil-previous-line)
(phundrak/major-leader-key
:keymaps 'git-rebase-mode-map
:packages 'magit
"," #'with-editor-finish
"k" #'with-editor-cancel
"a" #'with-editor-cancel)
(phundrak/leader-key
:infix "g"
:packages 'magit
"" '(:ignore t :wk "git")
"b" #'magit-blame
"c" #'magit-clone
"d" #'magit-dispatch
"i" #'magit-init
"s" #'magit-status
"l" #'magit-log
"y" #'my/yadm
"S" #'magit-stage-file
"U" #'magit-unstage-file
"f" '(:ignore t :wk "file")
"fd" #'magit-diff
"fc" #'magit-file-checkout
"fl" #'magit-file-dispatch
"fF" #'magit-find-file))
Alphapapa also created an awesome package for Magit: magit-todos which display in the Magit buffer a list of TODOs found in the current project to remind you of what to do next.
First, let’s setup our todo keywords with hl-todo
. A good few todo
keywords are already defined in the hl-todo-keyword-faces
variable.
Why not use them? hl-todo-mode
enables fontlock highlight of these
keywords in a buffer. Let’s enable this mode globally.
(use-package hl-todo
:defer t
:straight (:build t)
:init (global-hl-todo-mode 1)
:general
(phundrak/leader-key
:packages '(hl-todo)
:infix "c"
"" '(:ignore t :which-key "todos")
"n" #'hl-todo-next
"p" #'hl-todo-previous))
We can now configure properly magit-todos
. Notice my custom function
hooked to magit-mode-hook
. This is because this package tries to find
TODOs in all files in the current project, and my yadm repository’s
root is my $HOME
. So, yeah, no magit-todos
in yadm.
(use-package magit-todos
:straight (:build t)
:after (magit hl-todo)
:init
(with-eval-after-load 'magit
(defun my/magit-todos-if-not-yadm ()
"Deactivate magit-todos if in yadm Tramp connection.
If `magit--default-directory' points to a yadm Tramp directory,
deactivate `magit-todos-mode', otherwise enable it."
(if (string-prefix-p "/yadm:" magit--default-directory)
(magit-todos-mode -1)
(magit-todos-mode +1)))
(add-hook 'magit-mode-hook #'my/magit-todos-if-not-yadm))
:config
(csetq magit-todos-ignore-case t))
Finally, it is also possible to use Gitflow’s framework with Magit
with magit-gitflow
:
(use-package magit-gitflow
:defer t
:after magit
:straight (magit-gitflow :build t
:type git
:host github
:repo "jtatarik/magit-gitflow")
:hook (magit-mode . turn-on-magit-gitflow))
Forge
Forge acts as an interface for Github, Gitlab, and Bitbucket inside Magit. A lot of possibilities are present, you can read issues and pull requests, create them, and fork projects among other things.
NOTE: Make sure to configure a GitHub token before using this package!
(use-package forge
:after magit
:straight (:build t)
:init
(evil-collection-forge-setup)
:general
(phundrak/major-leader-key
:keymaps 'forge-topic-mode-map
"c" #'forge-create-post
"e" '(:ignore t :which-key "edit")
"ea" #'forge-edit-topic-assignees
"ed" #'forge-edit-topic-draft
"ek" #'forge-delete-comment
"el" #'forge-edit-topic-labels
"em" #'forge-edit-topic-marks
"eM" #'forge-merge
"en" #'forge-edit-topic-note
"ep" #'forge-edit-post
"er" #'forge-edit-topic-review-requests
"es" #'forge-edit-topic-state
"et" #'forge-edit-topic-title))
Projectile
First, I need to install ripgrep
, a faster reimplementation of grep
,
which will be very useful when managing projects.
(use-package ripgrep
:if (executable-find "rg")
:straight (:build t)
:defer t)
Now, I can use projectile, which is sort of the de facto standard
project manager in Emacs. I know there’s project.el
, but,… Eh…
(use-package projectile
:straight (:build t)
:diminish projectile-mode
:custom ((projectile-completion-system 'ivy))
:init
(setq projectile-switch-project-action #'projectile-dired)
:config
(projectile-mode)
(add-to-list 'projectile-ignored-projects "~/")
(add-to-list 'projectile-globally-ignored-directories "^node_modules$")
:general
(phundrak/leader-key
"p" '(:keymap projectile-command-map :which-key "projectile")))
And of course, there is a counsel package dedicated to projectile.
(use-package counsel-projectile
:straight (:build t)
:after (counsel projectile)
:config (counsel-projectile-mode))
Recentf
The built-in package recentf
keeps track of recently opened files. But
by default, it only follows the twenty most recent files, that not
nearly enough for me, so I raise it to two hundred. I also don’t want
recentf to follow the Elfeed database, so I add it to the list of
excluded files.
(use-package recentf
:straight (:build t :type built-in)
:custom ((recentf-max-saved-items 2000))
:config
(add-all-to-list 'recentf-exclude
`(,(rx (* any)
(or "elfeed-db"
"eln-cache"
"conlanging/content"
"org/config"
"/Mail/Sent"
".cache/")
(* any)
(? (or "html" "pdf" "tex" "epub")))
,(rx (* any)
".elc"
eol)
,(rx "/"
(or "rsync" "ssh" "tmp" "yadm" "sudoedit" "sudo")
(* any)))))
Screenshot
screenshot.el
is a nice utility package made by TEC. It allows the
user to take a screenshot of a specific area of a buffer and make it
look nice.
(use-package screenshot
:defer t
:straight (screenshot :build t
:type git
:host github
:repo "tecosaur/screenshot")
:config (load-file (locate-library "screenshot.el"))
:general
(phundrak/leader-key
:infix "a"
:packages '(screenshot)
"S" #'screenshot))
Shells
Shell-pop
Shell-pop allows the user to easily call for a new shell in a pop-up buffer.
(use-package shell-pop
:defer t
:straight (:build t)
:custom
(shell-pop-default-directory "/home/phundrak")
(shell-pop-shell-type (quote ("eshell" "*eshell*" (lambda () (eshell shell-pop-term-shell)))))
(shell-pop-window-size 30)
(shell-pop-full-span nil)
(shell-pop-window-position "bottom")
(shell-pop-autocd-to-working-dir t)
(shell-pop-restore-window-configuration t)
(shell-pop-cleanup-buffer-at-process-exit t))
VTerm
VTerm gives Emacs access to regular shells with an almost regular
emulator. Be aware you will most likely need to hit C-c
twice to send
an interrupt signal.
(use-package vterm
:defer t
:straight t
:config
(setq vterm-shell "/usr/bin/fish"))
One annoying think with vterm is it only can create one buffer, you
can’t have multiple vterm buffers by default. multi-vterm
fixes this
issue.
(use-package multi-vterm
:after vterm
:defer t
:straight (:build t)
:general
(phundrak/major-leader-key
:packages '(vterm multi-vterm)
:keymap 'vterm-mode-map
"c" #'multi-vterm
"n" #'multi-vterm-next
"p" #'multi-vterm-prev))
XWidgets Webkit Browser
I used to use the xwidgets webkit browser in order to view or preview HTML files from Emacs, but it seems the Cairo background transparency patch breaks it. So while this isn’t patched, I will disable Xwidgets in my Emacs build, and these keybinds will not be tangled.
(phundrak/evil
:keymaps 'xwidget-webkit-mode-map
"<mouse-4>" #'xwidget-webkit-scroll-down-line
"<mouse-5>" #'xwidget-webkit-scroll-up-line
"c" #'xwidget-webkit-scroll-backward
"t" #'xwidget-webkit-scroll-up-line
"s" #'xwidget-webkit-scroll-down-line
"r" #'xwidget-webkit-scroll-forward
"h" #'xwidget-webkit-goto-history
"C" #'xwidget-webkit-back
"R" #'xwidget-webkit-forward
"C-r" #'xwidget-webkit-reload
"j" nil
"k" nil
"l" nil
"H" nil
"L" nil
"C-d" #'xwidget-webkit-scroll-up
"C-u" #'xwidget-webkit-scroll-down)
Wttr.in
(use-package wttrin
:defer t
:straight (wttrin :build t
:local-repo "~/fromGIT/emacs-packages/emacs-wttrin"
:type git)
;; :host github
;; :repo "Phundrak/emacs-wttrin"
:config
(setq wttrin-default-cities '("Aubervilliers" "Paris" "Lyon" "Nonières" "Saint Agrève")
wttrin-use-metric t))
Editing
First, I’ll define some keybindings for easily inserting pairs when editing text.
(general-define-key
:states 'visual
"M-[" #'insert-pair
"M-{" #'insert-pair
"M-<" #'insert-pair
"M-'" #'insert-pair
"M-`" #'insert-pair
"M-\"" #'insert-pair)
Atomic Chrome
Why write in your browser when you could write with Emacs? Despite its name, this package isn’t only geared towards Chrome/Chromium-based browsers but also towards Firefox since its 2.0 version. I find it a bit unfortunate Chrome’s name stuck in the package’s name though.
(use-package atomic-chrome
:straight (:build t)
:init
(atomic-chrome-start-server)
:config
(setq atomic-chrome-default-major-mode 'markdown-mode
atomic-chrome-url-major-mode-alist `(("github\\.com" . gfm-mode)
("gitlab\\.com" . gfm-mode)
("labs\\.phundrak\\.com" . markdown-mode)
("reddit\\.com" . markdown-mode))))
Editorconfig
Editorconfig is a unified way of passing to your text editor settings
everyone working in a repo need to follow. .editorconfig
files work
for VSCode users, vim users, Atom users, Sublime users, and of course
Emacs users.
(use-package editorconfig
:defer t
:straight (:build t)
:diminish editorconfig-mode
:init
(editorconfig-mode t))
Evil Nerd Commenter
Emacs’ default commenting system is nice, but I don’t find it smart enough for me.
(use-package evil-nerd-commenter
:after evil
:straight (:build t))
Iedit
Iedit is a powerful text editing tool that can be used to refactor code through the edition of multiple regions at once, be it in a region or in a whole buffer. Since I’m using evil, I’ll also use a compatibility package that adds states for iedit.
(use-package evil-iedit-state
:defer t
:straight (:build t)
:commands (evil-iedit-state evil-iedit-state/iedit-mode)
:init
(setq iedit-curent-symbol-default t
iedit-only-at-symbol-boundaries t
iedit-toggle-key-default nil)
:general
(phundrak/leader-key
:infix "r"
:packages '(iedit evil-iedit-state)
"" '(:ignore t :which-key "refactor")
"i" #'evil-iedit-state/iedit-mode)
(general-define-key
:keymaps 'evil-iedit-state-map
"c" nil
"s" nil
"J" nil
"S" #'iedit-expand-down-a-line
"T" #'iedit-expand-up-a-line
"h" #'evil-iedit-state/evil-change
"k" #'evil-iedit-state/evil-substitute
"K" #'evil-iedit-state/substitute
"q" #'evil-iedit-state/quit-iedit-mode))
Parinfer
Don’t let the name of the package fool you! parinfer-rust-mode
is not
a parinfer
mode for rust-mode
, but a mode for parinfer-rust
. parinfer
was a project for handling parenthesis and other double markers in a
much more intuitive way when writing Lisp code. However, it is now out
of date (last commit was on January 2nd, 2019) and the repository has
since been archived. New implementations then appeared, one of them is
parinfer-rust
, obviously written in Rust, around which
parinfer-rust-mode
is built. Enabling parinfer-rust-mode
should also
automatically disable smartparens-mode
in order to avoid conflicting
behavior.
(use-package parinfer-rust-mode
:defer t
:straight (:build t)
:diminish parinfer-rust-mode
:hook emacs-lisp-mode common-lisp-mode scheme-mode
:init
(setq parinfer-rust-auto-download t
parinfer-rust-library-directory (concat user-emacs-directory
"parinfer-rust/"))
(add-hook 'parinfer-rust-mode-hook
(lambda () (smartparens-mode -1)))
:general
(phundrak/major-leader-key
:keymaps 'parinfer-rust-mode-map
"m" #'parinfer-rust-switch-mode
"M" #'parinfer-rust-toggle-disable))
Smartparens
smartparens
is a package similar to parinfer
, but while the latter is
more specialized for Lisp dialects, smartparens
works better with
other programming languages that still uses parenthesis, but not as
much as Lisp dialects; think for example C, C++, Rust, Javascript, and
so on.
(use-package smartparens
:defer t
:straight (:build t)
:hook (prog-mode . smartparens-mode))
string-edit
string-edit
is a cool package that allows the user to write naturally
a string and get it automatically escaped for you. No more manually
escaping your strings!
(use-package string-edit-at-point
:defer t
:straight (:build t))
Writeroom
On the other hand, writeroom
allows the user to enter a
distraction-free mode of Emacs, and I like that! But the default width
is a bit too small for me, and I prefer not to go fullscren.
(use-package writeroom-mode
:defer t
:straight (:build t)
:init (global-writeroom-mode 1)
:config
(setq writeroom-width 100
writeroom-fullscreen-effect nil
writeroom-maximize-window nil
writeroom-mode-line t
writeroom-major-modes '(text-mode org-mode markdown-mode nov-mode Info-mode)))
Emacs built-ins
Dired
Dired is Emacs’ built-in file manager. It’s really great, and replaces any graphical file manager for me most of the time because:
- I am not limited to x tabs or panes
- All actions can be done with keybindings
- I get a consistent behavior between Dired and Emacs, since it’s the same thing.
I used to have an extensive configuration for Dired with a couple of additional packages to make it more usable. Dirvish rendered that obsolete!
(use-package dirvish
:straight (:build t)
:defer t
:init (dirvish-override-dired-mode)
:custom
(dirvish-quick-access-entries
'(("h" "~/" "Home")
("d" "~/Downloads/" "Downloads")
("c" "~/org/config" "Config")
("C" "~/Documents/conlanging/content" "Conlanging")))
(dirvish-mode-line-format
'(:left (sort file-time "" file-size symlink) :right (omit yank index)))
(dirvish-attributes '(all-the-icons file-size collapse subtree-state vc-state git-msg))
:config
(dirvish-peek-mode)
<<dired-drag-and-drop>>
<<dired-listing-flags>>
<<dired-files-and-dirs>>
<<dirvish-exa-offload>>
(setq dired-dwim-target t
dired-recursive-copies 'always
dired-recursive-deletes 'top
delete-by-moving-to-trash t
dirvish-preview-dispatchers (cl-substitute 'pdf-preface 'pdf dirvish-preview-dispatchers))
:general
(phundrak/evil
:keymaps 'dirvish-mode-map
:packages '(dired dirvish)
"q" #'dirvish-quit
"TAB" #'dirvish-subtree-toggle)
(phundrak/major-leader-key
:keymaps 'dirvish-mode-map
:packages '(dired dirvish)
"A" #'gnus-dired-attach
"a" #'dirvish-quick-access
"d" #'dirvish-dispatch
"e" #'dirvish-emerge-menu
"f" #'dirvish-fd-jump
"F" #'dirvish-file-info-menu
"h" '(:ignore t :which-key "history")
"hp" #'dirvish-history-go-backward
"hn" #'dirvish-history-go-forward
"hj" #'dirvish-history-jump
"hl" #'dirvish-history-last
"l" '(:ignore t :which-key "layout")
"ls" #'dirvish-layout-switch
"lt" #'dirvish-layout-toggle
"m" #'dirvish-mark-menu
"s" #'dirvish-quicksort
"S" #'dirvish-setup-menu
"y" #'dirvish-yank-menu
"n" #'dirvish-narrow))
It requires some programs which can be installed like so:
pacman -S --needed --noprogressbar --noconfirm --color=never \
fd poppler ffmpegthumbnailer mediainfo imagemagick tar unzip
Since Emacs 29, it is possible to enable drag-and-drop between Emacs and other applications.
(csetq dired-mouse-drag-files t
mouse-drag-and-drop-region-cross-program t)
In Dirvish, it’s best to use the long name of flags whenever possible, otherwise some commands won’t work.
(csetq dired-listing-switches (string-join '("--all"
"--human-readable"
"--time-style=long-iso"
"--group-directories-first"
"-lv1")
" "))
However, it is possible to instead use exa
when it is available.
Instead of making Emacs’ main thread to the file listing in a
directory, we offload it to an external thread.
(dirvish-define-preview exa (file)
"Use `exa' to generate directory preview."
:require ("exa")
(when (file-directory-p file)
`(shell . ("exa" "--color=always" "-al" ,file))))
(add-to-list 'dirvish-preview-dispatchers 'exa)
Finally, some directories need to be set for Dired to store various files and images.
(let ((my/file (lambda (path &optional dir)
(expand-file-name path (or dir user-emacs-directory))))
(my/dir (lambda (path &optional dir)
(expand-file-name (file-name-as-directory path)
(or dir user-emacs-directory)))))
(csetq image-dired-thumb-size 150
image-dired-dir (funcall my/dir "dired-img")
image-dired-db-file (funcall my/file "dired-db.el")
image-dired-gallery-dir (funcall my/dir "gallery")
image-dired-temp-image-file (funcall my/file "temp-image" image-dired-dir)
image-dired-temp-rotate-image-file (funcall my/file "temp-rotate-image" image-dired-dir)))
Copying files with Dired is a blocking process. It’s usually fine when
there’s not a lot to copy, but it becomes more annoying when moving
larger files. The package dired-rsync
allows to copy files with rsync
in the background so we can carry on with our tasks while the copy is
happening.
(use-package dired-rsync
:if (executable-find "rsync")
:defer t
:straight (:build t)
:general
(phundrak/evil
:keymaps 'dired-mode-map
:packages 'dired-rsync
"C-r" #'dired-rsync))
Compilation mode
After reading about a blog article, I found out it is possible to run
quite a few things through compilation-mode
, so why not? First, let’s
redefine some keybinds for this mode. I’ll also define a general
keybind in order to re-run my programs from other buffers than the
compilation-mode
buffer. I also want to follow the output of the
compilation buffer, as well as enable some syntax highlighting.
(use-package compile
:defer t
:straight (compile :type built-in)
:hook (compilation-filter . colorize-compilation-buffer)
:init
(require 'ansi-color)
(defun colorize-compilation-buffer ()
(let ((inhibit-read-only t))
(ansi-color-apply-on-region (point-min) (point-max))))
:general
(phundrak/evil
:keymaps 'compilation-mode-map
"g" nil
"r" nil
"R" #'recompile
"h" nil)
(phundrak/leader-key
"R" #'recompile)
:config
(setq compilation-scroll-output t))
Eshell
Eshell is a built-in shell available from Emacs which I use almost as often as fish. Some adjustments are necessary to make it fit my taste though.
(use-package eshell
:defer t
:straight (:type built-in :build t)
:config
(setq eshell-prompt-function
(lambda ()
(concat (abbreviate-file-name (eshell/pwd))
(if (= (user-uid) 0) " # " " λ ")))
eshell-prompt-regexp "^[^#λ\n]* [#λ] ")
<<eshell-alias-file>>
<<eshell-concat-shell-command>>
<<eshell-alias-open>>
<<eshell-alias-clear>>
<<eshell-alias-buffers>>
<<eshell-alias-emacs>>
<<eshell-alias-mkcd>>
:general
(phundrak/evil
:keymaps 'eshell-mode-map
[remap evil-collection-eshell-evil-change] #'evil-backward-char
"c" #'evil-backward-char
"t" #'evil-next-visual-line
"s" #'evil-previous-visual-line
"r" #'evil-forward-char
"h" #'evil-collection-eshell-evil-change)
(general-define-key
:keymaps 'eshell-mode-map
:states 'insert
"C-a" #'eshell-bol
"C-e" #'end-of-line))
Aliases
First, let’s declare our list of “dumb” aliases we’ll use in Eshell. You can find them here.
(setq eshell-aliases-file (expand-file-name "eshell-alias" user-emacs-directory))
A couple of other aliases will be defined through custom Elisp functions, but first I’ll need a function for concatenating a shell command into a single string:
(defun phundrak/concatenate-shell-command (&rest command)
"Concatenate an eshell COMMAND into a single string.
All elements of COMMAND will be joined in a single
space-separated string."
(mapconcat #'identity command " "))
I’ll also declare some aliases here, such as open
and openo
that
respectively allow me to open a file in Emacs, and same but in another
window.
(defalias 'open #'find-file)
(defalias 'openo #'find-file-other-window)
The default behavior of eshell/clear
is not great at all, although it
clears the screen it also scrolls all the way down. Therefore, let’s
alias it to eshell/clear-scrollback
which has the correct behavior.
(defalias 'eshell/clear #'eshell/clear-scrollback)
As you see, these were not declared in my dedicated aliases file but
rather were declared programmatically. This is because I like to keep
my aliases file for stuff that could work too with other shells were
the syntax a bit different, and aliases related to Elisp are kept
programmatically. I’ll also declare list-buffers
an alias of ibuffer
because naming it that way kind of makes more sense to me.
(defalias 'list-buffers 'ibuffer)
I still have some stupid muscle memory telling me to open emacs
, vim
or nano
in Eshell, which is stupid: I’m already inside Emacs and I
have all its power available instantly. So, let’s open each file
passed to these commands.
(defun eshell/emacs (&rest file)
"Open each FILE and kill eshell.
Old habits die hard."
(when file
(dolist (f (reverse file))
(find-file f t))))
Finally, I’ll declare mkcd
which allows the simultaneous creation of a
directory and moving into this newly created directory. And of course,
it will also work if the directory also exists or if parent
directories don’t, similarly to the -p
option passed to mkdir
.
(defun eshell/mkcd (dir)
"Create the directory DIR and move there.
If the directory DIR doesn’t exist, create it and its parents
if needed, then move there."
(mkdir dir t)
(cd dir))
Commands
When I’m in Eshell, sometimes I wish to open multiple files at once in
Emacs. For this, when I have several arguments for find-file
, I want
to be able to open them all at once. Let’s modify find-file
like so:
(defadvice find-file (around find-files activate)
"Also find all files within a list of files. This even works recursively."
(if (listp filename)
(cl-loop for f in filename do (find-file f wildcards))
ad-do-it))
I also want to be able to have multiple instances of Eshell opened at
once. For that, I declared the function eshell-new
that does exactly
that.
(defun eshell-new ()
"Open a new instance of eshell."
(interactive)
(eshell 'N))
A very useful command I use often in fish is z
, a port from bash’s and
zsh’s command that allows to jump around directories based on how
often we go in various directories.
(use-package eshell-z
:defer t
:after eshell
:straight (:build t)
:hook (eshell-mode . (lambda () (require 'eshell-z))))
Environment Variables
Some environment variables need to be correctly set so Eshell can
correctly work. I would like to set two environment variables related
to Dart development: the DART_SDK
and ANDROID_HOME
variables.
(setenv "DART_SDK" "/opt/dart-sdk/bin")
(setenv "ANDROID_HOME" (concat (getenv "HOME") "/Android/Sdk/"))
The EDITOR
variable also needs to be set for git commands, especially the
yadm
commands.
(setenv "EDITOR" "emacsclient -c -a emacs")
Finally, for some specific situations I need SHELL
to be set to
something more standard than fish:
(setenv "SHELL" "/bin/sh")
Visual configuration
I like to have at quick glance some information about my machine when I fire up a terminal. I haven’t found anything that does that the way I like it, so I’ve written a package! It’s actually available on MELPA, but since I’m the main dev of this package, I’ll keep track of the git repository.
(use-package eshell-info-banner
:after (eshell)
:defer t
:straight (eshell-info-banner :build t
:type git
:host github
:protocol ssh
:repo "phundrak/eshell-info-banner.el")
:hook (eshell-banner-load . eshell-info-banner-update-banner)
:config
(setq eshell-info-banner-width 80
eshell-info-banner-partition-prefixes '("/dev" "zroot" "tank")))
Another feature I like is fish-like syntax highlight, which brings some more colors to Eshell.
(use-package eshell-syntax-highlighting
:after (esh-mode eshell)
:defer t
:straight (:build t)
:config
(eshell-syntax-highlighting-global-mode +1))
Powerline prompts are nice, git-aware prompts are even better!
eshell-git-prompt
is nice, but I prefer to write my own package for
that.
(use-package powerline-eshell
:if (string= (string-trim (shell-command-to-string "uname -n")) "leon")
:load-path "~/fromGIT/emacs-packages/powerline-eshell.el/"
:after eshell)
Eww
Since Emacs 29, it is possible to automatically rename eww
buffers to
a more human-readable name, see Prot’s blog post on the matter.
(use-package eww
:defer t
:straight (:type built-in)
:config
(setq eww-auto-rename-buffer 'title))
Image-mode
I won’t modify much for image-mode
(the mode used to display images)
aside from Emacs’ ability to use external converters to display some
images it wouldn’t be able to handle otherwise.
(setq image-use-external-converter t)
Info
Let’s define some more intuitive keybinds for info-mode
.
(use-package info
:defer t
:straight (info :type built-in :build t)
:general
(phundrak/evil
:keymaps 'Info-mode-map
"c" #'Info-prev
"t" #'evil-scroll-down
"s" #'evil-scroll-up
"r" #'Info-next)
(phundrak/major-leader-key
:keymaps 'Info-mode-map
"?" #'Info-toc
"b" #'Info-history-back
"f" #'Info-history-forward
"m" #'Info-menu
"t" #'Info-top-node
"u" #'Info-up))
Tab Bar
(use-package tab-bar
:defer t
:straight (:type built-in)
:custom
(tab-bar-close-button-show nil)
(tab-bar-new-button-show nil)
(tab-bar-new-tab-choice "*dashboard*")
:custom-face
(tab-bar ((t (:background "#272C36"
:foreground "#272C36"
:box (:line-width (8 . 5) :style flat-button)))))
:init
(advice-add #'tab-new
:after
(lambda (&rest _) (when (y-or-n-p "Rename tab? ")
(call-interactively #'tab-rename)))))
Tramp
Tramp is an Emacs built-in package that allows the user to connect to
various hosts using various protocols, such as ssh
and
rsync
. However, I have some use-case for Tramp which are not
supported natively. I will describe them here.
(use-package tramp
:straight (tramp :type built-in :build t)
:config
<<tramp-add-yadm>>
(csetq tramp-ssh-controlmaster-options nil
tramp-verbose 0
tramp-auto-save-directory (locate-user-emacs-file "tramp/")
tramp-chunksize 2000)
(add-to-list 'backup-directory-alist ; deactivate auto-save with TRAMP
(cons tramp-file-name-regexp nil)))
Yadm
yadm
is a git wrapper made to easily manage your dotfiles. It has
loads of features I don’t use (the main one I like but don’t use is
its Jinja-like host and OS-aware syntax), but unfortunately Magit
doesn’t play nice with it. Tramp to the rescue, and this page explains
how! Let’s just insert in my config this code snippet:
(add-to-list 'tramp-methods
'("yadm"
(tramp-login-program "yadm")
(tramp-login-args (("enter")))
(tramp-login-env (("SHELL") ("/bin/sh")))
(tramp-remote-shell "/bin/sh")
(tramp-remote-shell-args ("-c"))))
I’ll also create a fuction for connecting to this new Tramp protocol:
(defun my/yadm ()
"Manage my dotfiles through TRAMP."
(interactive)
(magit-status "/yadm::"))
EXWM
So, I’m finally slowly getting back to EXWM. I tried it a couple of years ago, but that was with the SpacemacsOS layer on Spacemacs, on a laptop which got accidentaly formatted before I could save my config and all… So it got me some time to come back. I’m still a bit worried about Emacs being single threaded, so if I get one blocking function blocking Emacs, my whole desktop will hang, but for now I haven’t had this issue.
All my EXWM config is enabled only if I launch Emacs with the argument
--with-exwm
, otherwise none of the related packages get installed, let
alone activated and made available.
First, I need to install the X protocol Emacs Lisp Bindings. It doesn’t seem to be available in any repo, so I’ll install it directly from Git.
(use-package xelb
:if (seq-contains-p command-line-args "--with-exwm")
:straight (xelb :build t
:type git
:host github
:repo "emacs-straight/xelb"
:fork "ch11ng/xelb"))
Next is a function I’ve stolen copied from Daviwil’s desktop
configuration. This allows to launch software in the background
easily.
(defun exwm/run-in-background (command &optional once)
(let ((command-parts (split-string command " +")))
(apply #'call-process `(,(car command-parts) nil 0 nil ,@(cdr command-parts)))))
In order to launch Emacs with EXWM with startx
, I need a xinit
file.
This one is exported to $HOME/.xinitrc.emacs
.
xhost +SI:localuser:$USER
# Set fallback cursor
xsetroot -cursor_name left_ptr
# If Emacs is started in server mode, `emacsclient` is a convenient
# way to edit files in place (used by e.g. `git commit`)
export VISUAL=emacsclient
export EDITOR="$VISUAL"
# in case Java applications display /nothing/
# wmname LG3D
# export _JAVA_AWT_WM_NONREPARENTING=1
autorandr -l home
exec emacs --with-exwm
EXWM itself
Now we come to the plat de résistance. Like with xelb
, I’m using its
Git source to install it to make sure I get the right version — the
version available on the GNU ELPA is from the same source, true, but I
don’t know at which rate it is updated. And more packages down the
line will depend on this Git repository, so I might as well just clone
it right now.
As you can see, in the :config
secion I added to two hooks functions
so buffers are accurately renamed. While the average X window will
simply get the name of the current X window, I want Firefox and
Qutebrowser to be prefixed with the name of the browser. Actually, all
these will be renamed this way:
- Kitty
- Qutebrowser
(format "%s\n%S"
(mapconcat (lambda (buffer)
(let ((buffer-name (car buffer)))
(format "(\"%s\" %S)"
(downcase buffer-name)
`(exwm-workspace-rename-buffer (concat ,buffer-name
" - "
exwm-title)))))
buffers
"\n")
'(_otherwise (exwm-workspace-rename-buffer exwm-title)))
("kitty" (exwm-workspace-rename-buffer (concat "EXWM: " "Kitty" " - " exwm-title))) ("qutebrowser" (exwm-workspace-rename-buffer (concat "EXWM: " "Qutebrowser" " - " exwm-title))) (_otherwise (exwm-workspace-rename-buffer exwm-title))
(add-hook 'exwm-update-class-hook
(lambda () (exwm-workspace-rename-buffer exwm-class-name)))
(add-hook 'exwm-update-title-hook
(lambda ()
(pcase exwm-class-name
<<exwm-gen-buffers-rename()>>)))
As you can see below, in the :config
section I added two advices and one
hook in order to correctly integrate evil with EXWM. When I’m in an X
window, I want to be in insert-mode so I can type however I want.
However, when I exit one, I want to default back to normal-mode.
(add-hook 'exwm-manage-finish-hook (lambda () (call-interactively #'exwm-input-release-keyboard)))
(advice-add #'exwm-input-grab-keyboard :after (lambda (&optional id) (evil-normal-state)))
(advice-add #'exwm-input-release-keyboard :after (lambda (&optional id) (evil-insert-state)))
Secondly, I add i
, C-SPC
, and M-m
as exwm prefix keys so they aren’t
sent directly to the X windows but caught by Emacs (and EXWM). I’ll
use the i
key in normal-mode to enter insert-mode
and have Emacs
release the keyboard so the X window can grab it. Initially, I had
s-<escape>
as a keybind for grabbing back the keyboard from an X
window, as if I were in insert mode and wanted to go back to normal
mode, and I had s-I
to toggle keyboard grabbing. But I found myself
more than once trying to use s-<escape>
to toggle this state, s-I
completely forgotten. So I removed s-I
and made s-<escape>
behave like
s-I
once did.
(general-define-key
:keymaps 'exwm-mode-map
:states 'normal
"i" #'exwm-input-release-keyboard)
(exwm-input-set-key (kbd "s-<escape>") #'exwm-input-toggle-keyboard)
(push ?\i exwm-input-prefix-keys)
(push (kbd "C-SPC") exwm-input-prefix-keys)
(push (kbd "M-m") exwm-input-prefix-keys)
As stated a couple of times in my different configuration files, I’m using the bépo layout, which means the default keys in the number row are laid as follow:
(defconst exwm-workspace-keys '("\"" "«" "»" "(" ")" "@" "+" "-" "/" "*"))
With this, we can create keybinds for going or sending X windows to workspaces 0 to 9.
(setq exwm-input-global-keys
`(,@exwm-input-global-keys
,@(mapcar (lambda (i)
`(,(kbd (format "s-%s" (nth i exwm-workspace-keys))) .
(lambda ()
(interactive)
(exwm-workspace-switch-create ,i))))
(number-sequence 0 9))
,@(mapcar (lambda (i)
`(,(kbd (format "s-%d" i)) .
(lambda ()
(interactive)
(exwm-workspace-move-window ,(let ((index (1- i)))
(if (< index 0)
(- 10 index)
;; FIXME: does not work with s-0
index))))))
(number-sequence 0 9))))
You can then see the list of the keybinds I have set for EXWM, which
are all prefixed with SPC x
in normal mode (and C-SPC x
in insert
mode), with the exception of s-RET
which opens an eshell terminal.
(exwm-input-set-key (kbd "s-<return>") (lambda ()
(interactive)
(eshell)))
(phundrak/leader-key
:infix "x"
"" '(:ignore t :which-key "EXWM")
"d" #'exwm-debug
"k" #'exwm-input-send-next-key
"l" '((lambda ()
(interactive)
(start-process "" nil "plock"))
:which-key "lock")
"r" '(:ignore t :wk "rofi")
"rr" '((lambda () (interactive)
(shell-command "rofi -show drun" (current-buffer) (current-buffer)))
:wk "drun")
"rw" '((lambda () (interactive)
(shell-command "rofi -show window" (current-buffer) (current-buffer)))
:wk "windows")
"R" '(:ignore t :wk "restart")
"Rr" #'exwm-reset
"RR" #'exwm-restart
"t" '(:ignore t :which-key "toggle")
"tf" #'exwm-layout-toggle-fullscreen
"tF" #'exwm-floating-toggle-floating
"tm" #'exwm-layout-toggle-mode-line
"w" '(:ignore t :which-key "workspaces")
"wa" #'exwm-workspace-add
"wd" #'exwm-workspace-delete
"ws" #'exwm-workspace-switch
"x" '((lambda ()
(interactive)
(let ((command (string-trim (read-shell-command "RUN: "))))
(start-process command nil command)))
:which-key "run")
"RET" #'eshell-new)
A couple of commands are also automatically executed through my
autostart
script written here.
(exwm/run-in-background "autostart")
Finally, let’s only initialize and start EXWM once functions from exwm-randr ran, because otherwise having multiple monitors don’t work.
(with-eval-after-load 'exwm-randr
(exwm-init))
The complete configuration for the exwm
package can be found below.
(use-package exwm
:if (seq-contains-p command-line-args "--with-exwm")
:straight (exwm :build t
:type git
:host github
:repo "ch11ng/exwm")
:custom
(use-dialog-box nil "Disable dialog boxes since they are unusable in EXWM")
(exwm-input-line-mode-passthrough t "Pass all keypresses to emacs in line mode.")
:init
(require 'exwm-config)
(setq exwm-workspace-number 6)
:config
(set-frame-parameter (selected-frame) 'alpha-background 0.7)
<<exwm-randr>>
<<exwm-buffers-name>>
<<exwm-advices-evil>>
<<exwm-prefix-keys>>
<<exwm-bepo-number-row>>
<<exwm-workspace-keybinds>>
<<exwm-keybinds>>
<<exwm-autostart>>
<<exwm-init>>)
EXWM-Evil integration
(use-package evil-exwm-state
:if (seq-contains-p command-line-args "--with-exwm")
:defer t
:after exwm
:straight (evil-exwm-state :build t
:type git
:host github
:repo "domenzain/evil-exwm-state"))
Multimonitor support
(require 'exwm-randr)
(exwm/run-in-background "xwallpaper --zoom \"${cat $HOME/.cache/wallpaper}\"")
(start-process-shell-command
"xrandr" nil "xrandr --output eDP1 --mode 1920x1080 --pos 2560x0 --rotate normal --output HDMI1 --primary --mode 2560x1080 --pos 0x0 --rotate normal --output VIRTUAL1 --off --output DP-1-0 --off --output DP-1-1 --off")
(exwm-randr-enable)
(setq exwm-randr-workspace-monitor-plist '(3 "eDP1"))
Keybinds for a desktop environment
(use-package desktop-environment
:defer t
:straight (desktop-environment :build t
:type git
:host github
:repo "DamienCassou/desktop-environment")
:after exwm
:diminish t
:config
(add-hook 'exwm-init-hook #'desktop-environment-mode)
(setq desktop-environment-update-exwm-global-keys :prefix
exwm-layout-show-al-buffers t)
(setq desktop-environment-bluetooth-command "bluetoothctl"
desktop-environment-brightness-get-command "xbacklight -get"
desktop-environment-brightness-get-regexp (rx line-start (group (+ digit)))
desktop-environment-brightness-set-command "xbacklight %s"
desktop-environment-brightness-normal-increment "-inc 5"
desktop-environment-brightness-normal-decrement "-dec 5"
desktop-environment-brightness-small-increment "-inc 2"
desktop-environment-brightness-small-decrement "-dec 2"
desktop-environment-volume-normal-decrement "-d 5"
desktop-environment-volume-normal-increment "-i 5"
desktop-environment-volume-small-decrement "-d 2"
desktop-environment-volume-small-increment "-i 2"
desktop-environment-volume-set-command "pamixer -u %s"
desktop-environment-screenshot-directory "~/Pictures/Screenshots"
desktop-environment-screenlock-command "plock"
desktop-environment-music-toggle-command "mpc toggle"
desktop-environment-music-previous-command "mpc prev"
desktop-environment-music-next-command "mpc next"
desktop-environment-music-stop-command "mpc stop")
(general-define-key
"<XF86AudioPause>" (lambda () (interactive)
(with-temp-buffer
(shell-command "mpc pause" (current-buffer) (current-buffer)))))
(desktop-environment-mode))
Bluetooth
(defvar bluetooth-command "bluetoothctl")
(defun bluetooth-turn-on ()
(interactive)
(let ((process-connection-type nil))
(start-process "" nil bluetooth-command "power" "on")))
(defun bluetooth-turn-off ()
(interactive)
(let ((process-connection-type nil))
(start-process "" nil bluetooth-command "power" "off")))
(defun create-bluetooth-device (raw-name)
"Create a bluetooth device cons from RAW NAME.
The cons will hold first the MAC address of the device, then its
human-friendly name."
(let ((split-name (split-string raw-name " " t)))
`(,(mapconcat #'identity (cddr split-name) " ") . ,(cadr split-name))))
(require 'dbus)
(defun bluetooth-get-devices ()
(let ((bus-list (dbus-introspect-get-node-names :system "org.bluez" "/org/bluez/hci0")))
(mapcar (lambda (device)
`(,(dbus-get-property :system
"org.bluez"
(concat "/org/bluez/hci0/" device)
"org.bluez.Device1"
"Alias")
. ,device))
bus-list)))
(defun bluetooth-connect-device ()
(interactive)
(progn
(bluetooth-turn-on)
(let* ((devices (bluetooth-get-devices))
(device (alist-get (completing-read "Device: " devices)
devices nil nil #'string=)))
(dbus-call-method-asynchronously
:system "org.bluez"
(concat "/org/bluez/hci0" device)
"org.bluez.Device1"
"Connect"
(lambda (&optional msg)
(when msg (message "%s" msg)))))))
Making my life easier
Bufler
Bufler is a package that organizes and lists buffers in a much better
way than how they are usually sorted. You can easily and quickly find
buffers by their group, not only by their name, and THIS is great
news! Also, no helm
please! And for some reasons the keybindings are
borked by default, so let’s redefine them, and let’s also rebind SPC
to p
since it would conflict with my main general
prefix.
(use-package bufler
:straight (bufler :build t
:files (:defaults (:exclude "helm-bufler.el")))
:defer t
:general
(phundrak/evil
:keymaps 'bufler-list-mode-map
:packages 'bufler
"?" #'hydra:bufler/body
"g" #'bufler
"f" #'bufler-list-group-frame
"F" #'bufler-list-group-make-frame
"N" #'bufler-list-buffer-name-workspace
"k" #'bufler-list-buffer-kill
"p" #'bufler-list-buffer-peek
"s" #'bufler-list-buffer-save
"RET" #'bufler-list-buffer-switch))
Helpful
As the name tells, helpful
is a really helpful package which greatly
enhances a couple of built-in functions from Emacs, namely:
Vanilla Emacs Function | Helpful Function | Comment |
---|---|---|
describe-function |
helpful-callable |
Only interactive functions |
describe-function |
helpful-function |
Only actual functions (including interactive) |
describe-function |
helpful-macro |
|
describe-command |
helpful-command |
|
describe-key |
helpful-key |
|
describe-variable |
helpful-variable |
(use-package helpful
:straight (:build t)
:after (counsel ivy)
:custom
(counsel-describe-function-function #'helpful-callable)
(counsel-describe-variable-function #'helpful-variable)
:bind
([remap describe-function] . counsel-describe-function)
([remap describe-command] . helpful-command)
([remap describe-variable] . counsel-describe-variable)
([remap describe-key] . helpful-key))
LaTeX
(use-package auctex
:defer t
:straight (:build t)
:hook (tex-mode . lsp-deferred)
:hook (latex-mode . lsp-deferred)
:init
(setq TeX-command-default (if (executable-find "latexmk") "LatexMk" "LaTeX")
TeX-engine (if (executable-find "xetex") 'xetex 'default)
TeX-auto-save t
TeX-parse-self t
TeX-syntactic-comment t
TeX-auto-local ".auctex-auto"
TeX-style-local ".auctex-style"
TeX-source-correlate-mode t
TeX-source-correlate-method 'synctex
TeX-source-correlate-start-server nil
TeX-electric-sub-and-superscript t
TeX-fill-break-at-separators nil
TeX-save-query t)
:config
<<latex-fontification>>
(setq TeX-master t)
(setcar (cdr (assoc "Check" TeX-command-list)) "chktex -v6 -H %s")
(add-hook 'TeX-mode-hook (lambda ()
(setq ispell-parser 'tex
fill-nobreak-predicate (cons #'texmathp fill-nobreak-predicate))))
(add-hook 'TeX-mode-hook #'visual-line-mode)
(add-hook 'TeX-update-style-hook #'rainbow-delimiters-mode)
:general
(phundrak/major-leader-key
:packages 'lsp-mode
:keymaps '(latex-mode-map LaTeX-mode-map)
"l" '(:keymap lsp-command-map :which-key "lsp"))
(phundrak/major-leader-key
:packages 'auctex
:keymaps '(latex-mode-map LaTeX-mode-map)
"v" '(TeX-view :which-key "View")
"c" '(TeX-command-run-all :which-key "Compile")
"m" '(TeX-command-master :which-key "Run a command")))
From Doom Emacs’ configuration:
(setq font-latex-match-reference-keywords
'(;; BibLaTeX.
("printbibliography" "[{") ("addbibresource" "[{")
;; Standard commands.
("cite" "[{") ("citep" "[{")
("citet" "[{") ("Cite" "[{")
("parencite" "[{") ("Parencite" "[{")
("footcite" "[{") ("footcitetext" "[{")
;; Style-specific commands.
("textcite" "[{") ("Textcite" "[{")
("smartcite" "[{") ("Smartcite" "[{")
("cite*" "[{") ("parencite*" "[{")
("supercite" "[{")
;; Qualified citation lists.
("cites" "[{") ("Cites" "[{")
("parencites" "[{") ("Parencites" "[{")
("footcites" "[{") ("footcitetexts" "[{")
("smartcites" "[{") ("Smartcites" "[{")
("textcites" "[{") ("Textcites" "[{")
("supercites" "[{")
;; Style-independent commands.
("autocite" "[{") ("Autocite" "[{")
("autocite*" "[{") ("Autocite*" "[{")
("autocites" "[{") ("Autocites" "[{")
;; Text commands.
("citeauthor" "[{") ("Citeauthor" "[{")
("citetitle" "[{") ("citetitle*" "[{")
("citeyear" "[{") ("citedate" "[{")
("citeurl" "[{")
;; Special commands.
("fullcite" "[{")
;; Cleveref.
("cref" "{") ("Cref" "{")
("cpageref" "{") ("Cpageref" "{")
("cpagerefrange" "{") ("Cpagerefrange" "{")
("crefrange" "{") ("Crefrange" "{")
("labelcref" "{")))
(setq font-latex-match-textual-keywords
'(;; BibLaTeX brackets.
("parentext" "{") ("brackettext" "{")
("hybridblockquote" "[{")
;; Auxiliary commands.
("textelp" "{") ("textelp*" "{")
("textins" "{") ("textins*" "{")
;; Subcaption.
("subcaption" "[{")))
(setq font-latex-match-variable-keywords
'(;; Amsmath.
("numberwithin" "{")
;; Enumitem.
("setlist" "[{") ("setlist*" "[{")
("newlist" "{") ("renewlist" "{")
("setlistdepth" "{") ("restartlist" "{")
("crefname" "{")))
(use-package tex-mode
:defer t
:straight (:type built-in)
:config
(setq LaTeX-section-hook '(LaTeX-section-heading
LaTeX-section-title
LaTeX-section-toc
LaTeX-section-section
LaTeX-section-label)
LaTeX-fill-break-at-separators nil
LaTeX-item-indent 0))
(use-package preview
:defer t
:straight (:type built-in)
:config
(add-hook 'LaTeX-mode-hook #'LaTeX-preview-setup)
(setq-default preview-scale 1.4
preview-scale-function
(lambda () (* (/ 10.0 (preview-document-pt)) preview-scale)))
(setq preview-auto-cache-preamble nil)
(phundrak/major-leader-key
:packages 'auctex
:keymaps '(latex-mode-map LaTeX-mode-map)
"p" #'preview-at-point
"P" #'preview-clearout-at-point))
(use-package cdlatex
:defer t
:after auctex
:straight (:build t)
:hook (LaTeX-mode . cdlatex-mode)
:hook (org-mode . org-cdlatex-mode)
:config
(setq cdlatex-use-dollar-to-ensure-math nil)
:general
(phundrak/major-leader-key
:packages 'cdlatex
:keymaps 'cdlatex-mode-map
"$" nil
"(" nil
"{" nil
"[" nil
"|" nil
"<" nil
"^" nil
"_" nil
[(control return)] nil))
(use-package adaptive-wrap
:defer t
:after auctex
:straight (:build t)
:hook (LaTeX-mode . adaptative-wrap-prefix-mode)
:init (setq-default adaptative-wrap-extra-indent 0))
(use-package auctex-latexmk
:after auctex
:defer t
:straight (:build t)
:init
(setq auctex-latexmk-inherit-TeX-PDF-mode t)
(add-hook 'LaTeX-mode (lambda () (setq TeX-command-default "LatexMk")))
:config
(auctex-latexmk-setup))
(use-package company-auctex
:defer t
:after (company auctex)
:straight (:build t)
:config
(company-auctex-init))
(use-package company-math
:defer t
:straight (:build t)
:after (company auctex)
:config
(defun my-latex-mode-setup ()
(setq-local company-backends
(append '((company-math-symbols-latex company-latex-commands))
company-backends)))
(add-hook 'TeX-mode-hook #'my-latex-mode-setup))
Org-mode
Since recently, in order to make org-cite
compile properly, we need
the citeproc
package, a citation processor.
(use-package citeproc
:after (org)
:defer t
:straight (:build t))
Org is the main reason I am using Emacs. It is an extremely powerfu tool when you want to write anything that is not necessarily primarily programming-related, though it absolutely can be! Org can be a replacement for anything similar to LibreOffice Writer, LibreOffice Calc, and LibreOffice Impress. It is a much more powerful (and older) version of Markdown which can be exported to LaTeX and HTML at least, rendering writing web pages and technical, scientific documents much simpler than writing manually HTML and LaTeX code, especially when a single document source is meant to be exported for both formats. And since org is an Emacs package, that also means it can be greatly extended however we like!
(use-package org
:straight t
:defer t
:commands (orgtbl-mode)
:hook ((org-mode . visual-line-mode)
(org-mode . org-num-mode))
:custom-face
(org-macro ((t (:foreground "#b48ead"))))
:init
(auto-fill-mode -1)
:config
<<org-hydra-babel>>
(require 'ox-beamer)
(require 'org-protocol)
(setq org-hide-leading-stars nil
org-hide-macro-markers t
org-ellipsis " ⤵"
org-image-actual-width 600
org-redisplay-inline-images t
org-display-inline-images t
org-startup-with-inline-images "inlineimages"
org-pretty-entities t
org-fontify-whole-heading-line t
org-fontify-done-headline t
org-fontify-quote-and-verse-blocks t
org-startup-indented t
org-startup-align-all-tables t
org-use-property-inheritance t
org-list-allow-alphabetical t
org-M-RET-may-split-line nil
org-src-window-setup 'split-window-below
org-src-fontify-natively t
org-src-tab-acts-natively t
org-src-preserve-indentation t
org-log-done 'time
org-directory "~/org"
org-default-notes-file (expand-file-name "notes.org" org-directory))
(with-eval-after-load 'oc
(setq org-cite-global-bibliography '("~/org/bibliography/references.bib")))
<<org-agenda-files>>
<<org-behavior-electric>>
<<org-capture-target-files>>
<<org-capture-templates>>
<<org-create-emphasis-functions()>>
<<org-babel-load-languages>>
<<org-use-sub-superscripts>>
<<org-latex-compiler>>
<<org-latex-src-block-backend>>
<<org-latex-default-packages>>
<<org-export-latex-hyperref-format>>
<<org-latex-pdf-process>>
<<org-latex-logfiles-add-extensions>>
<<org-re-reveal>>
<<org-html-validation>>
<<org-latex-classes>>
<<org-publish-projects>>
<<org-mode-visual-prettify-symbols>>
:general
(phundrak/evil
:keymaps 'org-mode-map
:packages 'org
"RET" 'org-open-at-point)
(phundrak/major-leader-key
:keymaps 'org-mode-map
:packages 'org
<<general-keybindings-gen(table=org-keybinds-various)>>
<<general-keybindings-gen(table=org-keybinds-babel)>>
<<general-keybindings-gen(table=org-keybinds-dates)>>
<<general-keybindings-gen(table=org-keybinds-insert)>>
<<general-keybindings-gen(table=org-keybinds-jump)>>
<<general-keybindings-gen(table=org-keybinds-tables)>>
<<general-keybindings-gen(table=org-keybinds-toggles)>>)
<<org-capture-keybinds>>
(phundrak/major-leader-key
:packages 'org
:keymaps 'org-src-mode-map
"'" #'org-edit-src-exit
"k" #'org-edit-src-abort))
The main feature from evil-org
that I love is how easy it is to modify
some keybindings for keyboards layouts that do not have hjkl
, such as
the bépo layout (or Dvorak or Colemak if you are into that). But it
also adds a ton of default keybindings which are just much more
comfortable than the default ones you get with evil and org naked.
(use-package evil-org
:straight (:build t)
:after (org)
:hook (org-mode . evil-org-mode)
:config
(setq-default evil-org-movement-bindings
'((up . "s")
(down . "t")
(left . "c")
(right . "r")))
(evil-org-set-key-theme '(textobjects navigation calendar additional shift operators))
(require 'evil-org-agenda)
(evil-org-agenda-set-keys))
This package is a small package I’ve written that helps me when writing conlanging documents, with features such as creating syntax trees, converting translitterated text to its native script, etc…
(use-package conlanging
:straight (conlanging :build t
:type git
:repo "https://labs.phundrak.com/phundrak/conlanging.el")
:after org
:defer t)
Since very recently, the contrib/lisp/
directory of org moved out of
the main repository to this repository. On the other hand,
contrib/scripts/
moved to the worg repository, but I don’t need
it. The main reason I want org-contrib
is due to ox-extra
that allow
the usage of the :ignore:
tag in org.
(use-package org-contrib
:after (org)
:defer t
:straight (:build t)
:init
(require 'ox-extra)
(ox-extras-activate '(latex-header-blocks ignore-headlines)))
Agenda
(setq org-agenda-files (list "~/org/agenda" "~/org/notes.org"))
Babel
One of the amazing features of org-mode is its literary programming capacities by running code blocks from within Org-mode itself. But for that, only a couple of languages are supported directly by Org-mode itself, and they need to be activated. Here are the languages I activated in my Org-mode configuration:
C |
emacs-lisp |
gnuplot |
latex |
makefile |
plantuml |
python |
sass |
shell |
sql |
'((C . t) (emacs-lisp . t) (gnuplot . t) (latex . t) (makefile . t) (plantuml . t) (python . t) (sass . t) (shell . t) (sql . t))
The corresponding code is as follows:
(org-babel-do-load-languages
'org-babel-load-languages
<<org-babel-languages-gen()>>)
Some languages can run asynchronously with the help of ob-async
.
(use-package ob-async
:straight (:build t)
:defer t
:after (org ob))
A package I use from time to time is ob-latex-as-png
which allows me
to easily convert a LaTeX snippet into a PNG, regardless of the
exporter I use afterwards. Its installation is pretty simple:
(use-package ob-latex-as-png
:after org
:straight (:build t))
A nice thing to have when working with REST APIs is to have a REST client. Even better if it can work inside org-mode!
(use-package ob-restclient
:straight (:build t)
:defer t
:after (org ob)
:init
(add-to-list 'org-babel-load-languages '(restclient . t)))
Behavior
A useful package I like is toc-org
which creates automatically a table
of contents. My main usage for this however is not just to create a
table of content of my files to quickly jump around my file (I have
counsel-org-goto
for that), but it is for creating table of contents
for org files that will be hosted and viewable on Github.
(use-package toc-org
:after (org markdown-mode)
:straight (:build t)
:init
(add-to-list 'org-tag-alist '("TOC" . ?T))
:hook (org-mode . toc-org-enable)
:hook (markdown-mode . toc-org-enable))
electric-mode
also bothers me a lot when editing org files, so let’s deactivate it:
(add-hook 'org-mode-hook (lambda ()
(interactive)
(electric-indent-local-mode -1)))
As explained in my blog post, org-mode is terrible with coming up with meaningful IDs for its headings. I actually wrote a package for this!
(use-package org-unique-id
:straight (org-unique-id :build t
:type git
:host github
:repo "Phundrak/org-unique-id")
:defer t
:after org
:init (add-hook 'before-save-hook #'org-unique-id-maybe))
Capture
Org capture is an amazing tool for taking quick notes, be it simple text, links, resources, or reminders. They are all organised is specified org files which are described below.
(defvar org-conlanging-file "~/org/conlanging.org")
(defvar org-notes-file "~/org/notes.org")
(defvar org-journal-file "~/org/journal.org")
(defvar org-linguistics-file "~/org/linguistics.org")
(defvar org-novel-file "~/org/novel.org")
(defvar org-agenda-file "~/org/agenda/private.org")
(defvar org-school-file "~/org/agenda/school.org")
(defvar org-worldbuilding-file "~/org/worldbuilding.org")
Let me describe a keybind to invoke org-capture from anywhere within Emacs.
(phundrak/leader-key
:packages 'org
:infix "o"
"" '(:ignore t :which-key "org")
"c" #'org-capture)
When org-capture
is invoked, it will ask which template we wish to
use. In the table /phundrak/config.phundrak.com/src/commit/b81099061193bb9480d89d4695ba3a95ccec5c85/org/config/org-capture-shortcuts-table, the key column
represents which keychord we need to hit, titled with name, we need to
hit in order to use the template, inserted in the designated file in
the manner described by insertion mode.
Shortcut | Name | Title | Insertion mode | file | template |
---|---|---|---|---|---|
e | |||||
ew | Write Email | Emails | file+headline | org-default-notes-file | email.orgcaptmpl |
j | Journal | file+datetree | org-journal-file | journal.orgcaptmpl | |
l | Link | ||||
ll | General | file+headline | org-default-notes-file | link.orgcaptmpl | |
ly | YouTube | file+headline | org-default-notes-file | youtube.orgcaptmpl | |
L | Protocol Link | Link | file+headline | org-default-notes-file | protocol-link.orgcaptmpl |
n | Notes | ||||
nc | Conlanging | Note | file+headline | org-conlanging-file | notes.orgcaptmpl |
nn | General | file+headline | org-default-notes-file | notes.orgcaptmpl | |
nN | Novel | Note | file+headline | org-novel-notes-file | notes.orgcaptmpl |
nq | Quote | file+headline | org-default-notes-file | notes-quote.orgcaptmpl | |
nw | Worldbuilding | Note | file+headline | org-wordbuilding-file | notes.orgcaptmpl |
N | Novel | ||||
Ni | Ideas | file+headline | org-novel-notes-file | notes.orgcaptmpl | |
p | Protocol | Link | file+headline | org-default-notes-file | protocol.orgcaptmpl |
r | Resources | ||||
rc | Conlanging | Resources | file+headline | org-conlanging-file | resource.orgcaptmpl |
re | Emacs | file+headline | org-default-notes-file | resource.orgcaptmpl | |
ri | Informatique | file+headline | org-default-notes-file | resource.orgcaptmpl | |
rl | Linguistics | file+headline | org-default-notes-file | resource.orgcaptmpl | |
rL | Linux | file+headline | org-default-notes-file | resource.orgcaptmpl | |
rw | Worldbuilding | Resources | file+headline | org-wordbuilding-file | resource.orgcaptmpl |
t | Tasks | ||||
tb | Birthday | file+headline | org-private-agenda-file | birthday.orgcaptmpl | |
te | Event | file+headline | org-private-agenda-file | event.orgcaptmpl | |
th | Health | file+headline | org-private-agenda-file | health.orgcaptmpl | |
ti | Informatique | file+headline | org-private-agenda-file | informatique.orgcaptmpl |
All templates can be found in my dotfiles’ repository.
(mapconcat (lambda (entry)
(let ((key (nth 0 entry))
(name (nth 1 entry))
(title (nth 2 entry))
(ins-mode (nth 3 entry))
(file (nth 4 entry))
(template (nth 5 entry)))
(if (string= "" ins-mode)
(format "%S" `(,key ,name))
(format "(\"%s\" \"%s\" entry\n %S\n %S)"
key name
`(,(intern ins-mode) ,(intern file) ,(if (string= "file+datetree" ins-mode)
(intern "")
(if (string= title "")
name
title)))
`(file ,(concat "~/org/capture/" template))))))
entries
"\n")
("e" "Email") ("ew" "Write Email" entry (file+headline org-default-notes-file "Emails") (file "~/org/capture/email.orgcaptmpl")) ("j" "Journal" entry (file+datetree org-journal-file ##) (file "~/org/capture/journal.orgcaptmpl")) ("l" "Link") ("ll" "General" entry (file+headline org-default-notes-file "General") (file "~/org/capture/link.orgcaptmpl")) ("ly" "YouTube" entry (file+headline org-default-notes-file "YouTube") (file "~/org/capture/youtube.orgcaptmpl")) ("L" "Protocol Link" entry (file+headline org-default-notes-file "Link") (file "~/org/capture/protocol-link.orgcaptmpl")) ("n" "Notes") ("nc" "Conlanging" entry (file+headline org-conlanging-file "Note") (file "~/org/capture/notes.orgcaptmpl")) ("nn" "General" entry (file+headline org-default-notes-file "General") (file "~/org/capture/notes.orgcaptmpl")) ("nN" "Novel" entry (file+headline org-novel-notes-file "Note") (file "~/org/capture/notes.orgcaptmpl")) ("nq" "Quote" entry (file+headline org-default-notes-file "Quote") (file "~/org/capture/notes-quote.orgcaptmpl")) ("nw" "Worldbuilding" entry (file+headline org-wordbuilding-file "Note") (file "~/org/capture/notes.orgcaptmpl")) ("N" "Novel") ("Ni" "Ideas" entry (file+headline org-novel-notes-file "Ideas") (file "~/org/capture/notes.orgcaptmpl")) ("p" "Protocol" entry (file+headline org-default-notes-file "Link") (file "~/org/capture/protocol.orgcaptmpl")) ("r" "Resources") ("rc" "Conlanging" entry (file+headline org-conlanging-file "Resources") (file "~/org/capture/resource.orgcaptmpl")) ("re" "Emacs" entry (file+headline org-default-notes-file "Emacs") (file "~/org/capture/resource.orgcaptmpl")) ("ri" "Informatique" entry (file+headline org-default-notes-file "Informatique") (file "~/org/capture/resource.orgcaptmpl")) ("rl" "Linguistics" entry (file+headline org-default-notes-file "Linguistics") (file "~/org/capture/resource.orgcaptmpl")) ("rL" "Linux" entry (file+headline org-default-notes-file "Linux") (file "~/org/capture/resource.orgcaptmpl")) ("rw" "Worldbuilding" entry (file+headline org-wordbuilding-file "Resources") (file "~/org/capture/resource.orgcaptmpl")) ("t" "Tasks") ("tb" "Birthday" entry (file+headline org-private-agenda-file "Birthday") (file "~/org/capture/birthday.orgcaptmpl")) ("te" "Event" entry (file+headline org-private-agenda-file "Event") (file "~/org/capture/event.orgcaptmpl")) ("th" "Health" entry (file+headline org-private-agenda-file "Health") (file "~/org/capture/health.orgcaptmpl")) ("ti" "Informatique" entry (file+headline org-private-agenda-file "Informatique") (file "~/org/capture/informatique.orgcaptmpl"))
The capture templates are set like so:
(setq org-capture-templates
'(
<<org-capture-shortcuts-gen()>>))
Custom functions
Emphasize text
Sometimes, I want to emphasize some text in my org-mode documents. It’s very possible to just go to the begining of the chosen text, add the marker, then go to the end of the text than needs emphasis and add another marker, and I’m sure most people are fine with that. But I also like being able to select a region and hit a keybind to emphasize it that way. The table /phundrak/config.phundrak.com/src/commit/b81099061193bb9480d89d4695ba3a95ccec5c85/org/config/org-emphasis-character lists the emphasis characters in org-mode, their role, and the character code of each emphasis character. From that, creating functions that emphasize a selected text is quite easy.
Emphasis | Character | Character code |
---|---|---|
bold | * |
42 |
italic | / |
47 |
underline | _ |
95 |
verbatim | = |
61 |
code | ~ |
126 |
strike-through | + |
43 |
(mapconcat (lambda (emphasis)
(let ((type (car emphasis))
(code (nth 2 emphasis)))
(format "(defun org-emphasize-%s ()
\"Emphasize as %s the current region.\"
(interactive)
(org-emphasize %s))"
type
type
code)))
emphasis-list
"\n")
(defun org-emphasize-bold ()
"Emphasize as bold the current region."
(interactive)
(org-emphasize 42))
(defun org-emphasize-italic ()
"Emphasize as italic the current region."
(interactive)
(org-emphasize 47))
(defun org-emphasize-underline ()
"Emphasize as underline the current region."
(interactive)
(org-emphasize 95))
(defun org-emphasize-verbatim ()
"Emphasize as verbatim the current region."
(interactive)
(org-emphasize 61))
(defun org-emphasize-code ()
"Emphasize as code the current region."
(interactive)
(org-emphasize 126))
(defun org-emphasize-strike-through ()
"Emphasize as strike-through the current region."
(interactive)
(org-emphasize 43))
You can find the keybinds for these functions in the chapter §#Packages-Configuration-Org-mode-Keybindingsv0e5fl6184j0.
phundrak/toggle-org-src-window-split
(defun phundrak/toggle-org-src-window-split ()
"This function allows the user to toggle the behavior of
`org-edit-src-code'. If the variable `org-src-window-setup' has
the value `split-window-right', then it will be changed to
`split-window-below'. Otherwise, it will be set back to
`split-window-right'"
(interactive)
(if (equal org-src-window-setup 'split-window-right)
(setq org-src-window-setup 'split-window-below)
(setq org-src-window-setup 'split-window-right))
(message "Org-src buffers will now split %s"
(if (equal org-src-window-setup 'split-window-right)
"vertically"
"horizontally")))
Exporters
I want to disable by default behavior of ^
and _
for only one
character, making it compulsory to use instead ^{}
and _{}
respectively. This is due to my frequent usage of the underscore in my
org files as a regular character and not a markup one, especially when
describing phonetics evolution. So, let’s disable it:
(setq org-use-sub-superscripts (quote {}))
Epub
A backend for exporting files through org I like is ox-epub
which, as
you can guess, exports org files to the Epub format.
(use-package ox-epub
:after (org ox)
:straight (:build t))
Gemini
(use-package ox-gemini
:defer t
:straight (:build t)
:after (ox org))
HTML
On HTML exports, Org-mode tries to include a validation link for the exported HTML. Let’s disable that since I never use it.
(setq org-html-validation-link nil)
;; (use-package htmlize
;; :defer t
;; :straight (:build t))
This package allows for live-previewing the HTML export of an org buffer in an XWidget Webkit browser window. But when testing it, it’s not great for large org files, I should keep its usage for smaller org files.
(use-package preview-org-html-mode
:defer t
:after (org)
:straight (preview-org-html-mode :build t
:type git
:host github
:repo "jakebox/preview-org-html-mode")
:general
(phundrak/major-leader-key
:keymaps 'org-mode-map
:packages 'preview-org-html-mode
:infix "P"
"" '(:ignore t :which-key "preview")
"h" #'preview-org-html-mode
"r" #'preview-org-html-refresh
"p" #'preview-org-html-pop-window-to-frame)
:config
(setq preview-org-html-refresh-configuration 'save))
Hugo
I manage my blog with Hugo. Although it natively supports the org format, it’s not great compared to its markdown support. So, instead, let’s directly export our org files as markdown files and let Hugo do the rest of the job for us!
(use-package ox-hugo
:defer t
:after ox
:straight t)
I also have a function for publishing my blog once I exported my
articles with ox-hugo
. It will compile blog into a public/
directory
and copy its content over to my remote server.
(defun phundrak/blog-publish ()
"Publish my blog through Hugo to my remote server."
(interactive)
(let* ((default-directory (expand-file-name "~/org/blog"))
(public-path (concat default-directory "/public"))
(target-path "/rsync:Tilo:/home/phundrak/www/phundrak.com/blog"))
(compile "hugo")
(let ((files (mapcar (lambda (file)
(f-relative file public-path))
(f-files public-path nil t))))
(dolist (file files)
(copy-file (concat public-path "/" file)
(concat target-path "/" file)
t nil t)))))
LaTeX
When it comes to exports, I want the LaTeX and PDF exports to be done with XeLaTeX only. This implies the modification of the following variable:
(setq org-latex-compiler "xelatex")
A new backend that was introduced in org-mode for LaTeX source block
coloring is engraved
.
(use-package engrave-faces
:defer t
:straight (:build t)
:after org)
(require 'engrave-faces)
(csetq org-latex-src-block-backend 'engraved)
The default packages break my LaTeX exports: for some reasons, images
are not loaded and exported in PDFs, so I needed to redifine the
default packages excluding the one that broke my exports; namely, I
need to remove inputenc
, fontenc
and grffile
. I also added some default
packages:
cleveref
for better references to various elements.svg
for inserting SVG files in PDF outputsbooktabs
for nicer tables- and
tabularx
for tabulars with adjustable columns
(dolist (package '(("AUTO" "inputenc" t ("pdflatex"))
("T1" "fontenc" t ("pdflatex"))
("" "grffile" t)))
(delete package org-latex-default-packages-alist))
(dolist (package '(("capitalize" "cleveref")
("" "booktabs")
("" "tabularx")))
(add-to-list 'org-latex-default-packages-alist package t))
(setq org-latex-reference-command "\\cref{%s}")
By the way, reference links in LaTeX should be written in this format,
since we are using cleveref
:
(setq org-export-latex-hyperref-format "\\ref{%s}")
Tectonic is awesome for processing LaTeX documents! Look how simple it is!
(setq org-latex-pdf-process
'("tectonic -Z shell-escape --synctex --outdir=%o %f"))
Finally, org-mode is supposed to automatically clean logfiles after it exports an org file to LaTeX. However, it misses a few, so I need to add their extension like so:
(dolist (ext '("bbl" "lot"))
(add-to-list 'org-latex-logfiles-extensions ext t))
Reveal.js
(use-package org-re-reveal
:defer t
:after org
:straight (:build t)
:init
(add-hook 'org-mode-hook (lambda () (require 'org-re-reveal)))
:config
(setq org-re-reveal-root "https://cdn.jsdelivr.net/npm/reveal.js"
org-re-reveal-revealjs-version "4"))
SSH Config
Yet another exporter I enjoy is ox-ssh
with which I manage my
$HOME/.ssh/config
file. You won’t find my org file for managing my
servers on my repos though.
(use-package ox-ssh
:after (ox org)
:straight (:build t))
Keybindings
Be prepared, I have a lot of keybindings for org-mode! They are all
prefixed with a comma ,
in normal mode.
Key chord | Function | Description |
---|---|---|
RET | org-ctrl-c-ret | |
* | org-ctrl-c-star | |
, | org-ctrl-c-ctrl-c | |
' | org-edit-special | |
- | org-ctrl-c-minus | |
a | org-agenda | |
c | org-capture | |
C | org-columns | |
e | org-export-dispatch | |
l | org-store-link | |
p | org-priority | |
r | org-reload |
I then have a couple of babel-related functions.
Key chord | Function | Description |
---|---|---|
b | nil | babel |
b. | org-babel-transient/body | |
bb | org-babel-execute-buffer | |
bc | org-babel-check-src-block | |
bC | org-babel-tangle-clean | |
be | org-babel-execute-maybe | |
bf | org-babel-tangle-file | |
bn | org-babel-next-src-block | |
bo | org-babel-open-src-block-result | |
bp | org-babel-previous-src-block | |
br | org-babel-remove-result-one-or-many | |
bR | org-babel-goto-named-result | |
bt | org-babel-tangle | |
bi | org-babel-view-src-block-info |
The org-babel-transient
hydra allows me to quickly navigate between
code blocks and interact with them. This code block was inspired by
one you can find in Spacemacs.
(defhydra org-babel-transient ()
"
^Navigate^ ^Interact
^^^^^^^^^^^------------------------------------------
[_t_/_s_] navigate src blocs [_x_] execute src block
[_g_]^^ goto named block [_'_] edit src block
[_z_]^^ recenter screen [_q_] quit
"
("q" nil :exit t)
("t" org-babel-next-src-block)
("s" org-babel-previous-src-block)
("g" org-babel-goto-named-src-block)
("z" recenter-top-bottom)
("x" org-babel-execute-maybe)
("'" org-edit-special :exit t))
We next have keybindings related to org-mode’s agenda capabilities. We can schedule a todo header for some dates, or set a deadline.
Key chord | Function | Description |
---|---|---|
d | nil | dates |
dd | org-deadline | |
ds | org-schedule | |
dt | org-time-stamp | |
dT | org-time-stamp-inactive |
Let’s now define some keybinds for inserting stuff in our org buffer:
Key chord | Function | Description |
---|---|---|
i | nil | insert |
ib | org-insert-structure-template | |
id | org-insert-drawer | |
ie | nil | emphasis |
ieb | org-emphasize-bold | |
iec | org-emphasize-code | |
iei | org-emphasize-italic | |
ies | org-emphasize-strike-through | |
ieu | org-emphasize-underline | |
iev | org-emphasize-verbatim | |
iE | org-set-effort | |
if | org-footnote-new | |
ih | org-insert-heading | |
iH | counsel-org-link | |
ii | org-insert-item | |
il | org-insert-link | |
in | org-add-note | |
ip | org-set-property | |
is | org-insert-subheading | |
it | org-set-tags-command |
There isn’t a lot of stuff I can jump to yet, but there’s still some:
Key chord | Function | Description |
---|---|---|
j | nil | jump |
ja | counsel-org-goto-all | |
jh | counsel-org-goto |
Tables get a bit more love:
Key chord | Function | Description |
---|---|---|
t | nil | tables |
tc | org-table-move-column-left | |
tt | org-table-move-row-down | |
ts | org-table-move-row-up | |
tr | org-table-move-column-right | |
ta | org-table-align | |
te | org-table-eval-formula | |
tf | org-table-field-info | |
tF | org-table-edit-formulas | |
th | org-table-convert | |
tl | org-table-recalculate | |
tp | org-plot/gnuplot | |
tS | org-table-sort-lines | |
tw | org-table-wrap-region | |
tx | org-table-shrink | |
tN | org-table-create-with-table.el | |
td | nil | delete |
tdc | org-table-delete-column | |
tdr | org-table-kill-row | |
ti | nil | insert |
tic | org-table-insert-column | |
tih | org-table-insert-hline | |
tir | org-table-insert-row | |
tiH | org-table-hline-and-move | |
tt | nil | toggle |
ttf | org-table-toggle-formula-debugger | |
tto | org-table-toggle-coordinate-overlays |
Finally, let’s make enabling and disabling stuff accessible:
Key chord | Function | Description |
---|---|---|
T | nil | toggle |
Tc | org-toggle-checkbox | |
Ti | org-toggle-inline-images | |
Tl | org-latex-preview | |
Tn | org-num-mode | |
Ts | phundrak/toggle-org-src-window-split | |
Tt | org-show-todo-tree | |
TT | org-todo |
LaTeX formats
I currently have two custom formats for my Org-mode exports: one for general use
(initialy for my conlanging files, hence its conlang
name), and one for beamer
exports.
Below is the declaration of the conlang
LaTeX class:
'("conlang"
"\\documentclass{book}"
("\\chapter{%s}" . "\\chapter*{%s}")
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}"))
And here is the declaration of the beamer
class:
`("beamer"
,(concat "\\documentclass[presentation]{beamer}\n"
"[DEFAULT-PACKAGES]"
"[PACKAGES]"
"[EXTRA]\n")
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}"))
Both these classes have to be added to org-latex-classes
like so:
(eval-after-load "ox-latex"
'(progn
(add-to-list 'org-latex-classes
<<org-latex-class-conlang>>)
(add-to-list 'org-latex-classes
<<org-latex-class-beamer>>)))
Projects
Another great features of Org-mode is the Org projects that allow the user to easily publish a bunch of org files to a remote location. Here is the current declaration of my projects, which will be detailed later:
<<org-proj-config-setup>>
<<org-proj-lang-setup>>
(setq org-publish-project-alist
`(<<org-proj-config-html>>
<<org-proj-config-static>>
<<org-proj-config>>
<<org-proj-lang-md>>
<<org-proj-lang>>))
Configuration website
This is my configuration for exporting my dotfiles to my website in a web format only. No PDFs or anything, just HTML. Please note that I do not use that often anymore, I much prefer the automatic script that I have which deploys through my Drone instance my website on git pushes.
And before we get into the actual configuration, I would like to introduce a couple of variables. This is a bit more verbose than if I declared everything manually, but now I can change all three values at the same time without a hasle.
(defvar phundrak//projects-config-target
"/ssh:Tilo:~/www/phundrak.com/config"
"Points to where exported files for config.phundrak.com should be put.")
(defvar phundrak//projects-config-source
"~/org/config/"
"Points to where the sources for config.phundrak.com are.")
(defvar phundrak//projects-config-language
"en"
"Language of the website config.phundrak.com.")
(defvar phundrak//projects-config-recursive
t
"Defines whether subdirectories should be parsed for config.phundrak.com.")
Now, here is my configuration. In this snippet, my org files located in my source directory get exported in the HTML format and published to my target directory on my remote server through RSYNC via TRAMP. A sitemap is automatically generated, which comes in handy with the online sitemap that is available through the navigation bar.
("config-website-html"
:base-directory ,phundrak//projects-config-source
:base-extension "org"
:publishing-directory ,phundrak//projects-config-target
:recursive ,phundrak//projects-config-recursive
:language ,phundrak//projects-config-language
:publishing-function org-html-publish-to-html
:headline-levels 5
:auto-sitemap t
:auto-preamble t)
We also have the component for all the static files needed to run the website (mostly images tbh).
("config-website-static"
:base-directory ,phundrak//projects-config-source
:base-extension "png\\|jpg\\|gif\\|webp\\|svg\\|jpeg\\|ttf\\|woff\\|txt\\|epub\\|md"
:publishing-directory ,phundrak//projects-config-target
:recursive ,phundrak//projects-config-recursive
:language ,phundrak//projects-config-language
:publishing-function org-publish-attachment)
The project is then defined like so:
("config-website"
:components ("config-website-org"
"config-website-static"))
Conlanging website
Conlangs, or constructed languages, are artificial languages created by individuals or by groups of individuals, unlike natural languages which evolve and appear naturally from other languages.
My conlanging website is made of only one project, exporting my org files to GitHub-flavored Markdown. As for the previous project, let’s declare a common value.
(defvar phundrak--projects-conlanging-common-root
"~/org/conlanging/docs/"
"Points to the common root of my source and target for my
conlang.phundrak.com org project.")
The only component is the one generating the Markdown files from the org files.
("conlang-phundrak-com-md"
:base-directory ,phundrak--projects-conlanging-common-root
:base-extension "org"
:exclude ,(rx (* print)
(or "CONTRIB"
"README"
"site-map"
"temp"
"private"
"svg-ink")
(* print))
:publishing-directory ,phundrak--projects-conlanging-common-root
:recursive t
:language "en"
:publishing-function org-gfm-publish-to-gfm
:headline-levels 5
:auto-sitemap nil
:auto-preamble nil)
The project is then defined like so:
("conlang-phundrak-com"
:components ("conlang-phundrak-com-md"))
Org-ref and Bibtex configuration
(use-package reftex
:commands turn-on-reftex
:init (setq reftex-default-bibliography "~/org/bibliography/references.bib"
reftex-plug-into-AUCTeX t))
(use-package org-ref
;; :after (org ox-bibtex pdf-tools)
:after org
:defer t
:straight (:build t)
:custom-face
(org-ref-cite-face ((t (:weight bold))))
:init
(setq org-ref-completion-library 'org-ref-ivy-cite
org-latex-logfiles-extensions '("lof" "lot" "aux" "idx" "out" "log" "fbd_latexmk"
"toc" "nav" "snm" "vrb" "dvi" "blg" "brf" "bflsb"
"entoc" "ps" "spl" "bbl" "pygtex" "pygstyle"))
(add-hook 'org-mode-hook (lambda () (require 'org-ref)))
:config
(setq bibtex-completion-pdf-field "file"
bibtex-completion-notes-path "~/org/bibliography/notes/"
bibtex-completion-bibliography "~/org/bibliography/references.bib"
bibtex-completion-library-path "~/org/bibliography/bibtex-pdfs/"
bibtex-completion-pdf-symbol "⌘"
bibtex-completion-notes-symbol "✎")
:general
(phundrak/evil
:keymaps 'bibtex-mode-map
:packages 'org-ref
"C-t" #'org-ref-bibtex-next-entry
"C-s" #'org-ref-bibtex-previous-entry
"gt" #'org-ref-bibtex-next-entry
"gs" #'org-ref-bibtex-previous-entry)
(phundrak/major-leader-key
:keymaps '(bibtex-mode-map)
:packages 'org-ref
;; Navigation
"t" #'org-ref-bibtex-next-entry
"s" #'org-ref-bibtex-previous-entry
;; Open
"b" #'org-ref-open-in-browser
"n" #'org-ref-open-bibtex-notes
"p" #'org-ref-open-bibtex-pdf
;; Misc
"h" #'org-ref-bibtex-hydra/body
"i" #'org-ref-bibtex-hydra/org-ref-bibtex-new-entry/body-and-exit
"s" #'org-ref-sort-bibtex-entry
"l" '(:ignore t :which-key "lookup")
"la" #'arxiv-add-bibtex-entry
"lA" #'arxiv-get-pdf-add-bibtex-entry
"ld" #'doi-utils-add-bibtex-entry-from-doi
"li" #'isbn-to-bibtex
"lp" #'pubmed-insert-bibtex-from-pmid)
(phundrak/major-leader-key
:keymaps 'org-mode-map
:pakages 'org-ref
"ic" #'org-ref-insert-link))
(use-package ivy-bibtex
:defer t
:straight (:build t)
:config
(setq bibtex-completion-pdf-open-function #'find-file)
:general
(phundrak/leader-key
:keymaps '(bibtex-mode-map)
:packages 'ivy-bibtex
"m" #'ivy-bibtex))
Org-present
org-present
allows its user to create presentations through org-mode
,
which is really nice! However, most of my configuration will be stolen
from Daviwil’s with minor changes.
(defun my/org-present-prepare-slide ()
(org-overview)
(org-show-entry)
(org-show-children)
(org-present-hide-cursor))
(defun my/org-present-init ()
(setq header-line-format " ")
(org-display-inline-images)
(my/org-present-prepare-slide))
(defun my/org-present-quit ()
(setq header-line-format nil)
(org-present-small)
(org-present-show-cursor))
(defun my/org-present-prev ()
(interactive)
(org-present-prev)
(my/org-present-prepare-slide))
(defun my/org-present-next ()
(interactive)
(org-present-next)
(my/org-present-prepare-slide))
(use-package org-present
:after org
:defer t
:straight (:build t)
:general
(phundrak/major-leader-key
:packages 'org-present
:keymaps 'org-mode-map
"P" #'org-present)
(phundrak/evil
:states 'normal
:packages 'org-present
:keymaps 'org-present-mode-keymap
"+" #'org-present-big
"-" #'org-present-small
"<" #'org-present-beginning
">" #'org-present-end
"«" #'org-present-beginning
"»" #'org-present-end
"c" #'org-present-hide-cursor
"C" #'org-present-show-cursor
"n" #'org-present-next
"p" #'org-present-prev
"r" #'org-present-read-only
"w" #'org-present-read-write
"q" #'org-present-quit)
:hook ((org-present-mode . my/org-present-init)
(org-present-mode-quit . my/org-present-quit)))
Visual Configuration
While most modes of Emacs are dedicated to development, and therefore
are much more comfortable with a fixed-pitch font, more literary modes
such as org-mode are much more enjoyable if you have a variable pitch
font enabled. BUT, these modes can also require some fixed-pitch fonts
for some elements of the buffer, such as code blocks with
org-mode. mixed-pitch
comes to the rescue!
(use-package mixed-pitch
:after org
:straight (:build t)
:hook
(org-mode . mixed-pitch-mode)
(emms-browser-mode . mixed-pitch-mode)
(emms-playlist-mode . mixed-pitch-mode)
:config
(add-hook 'org-agenda-mode-hook (lambda () (mixed-pitch-mode -1))))
I have an issue with org-mode’s emphasis markers: I find them ugly. I
can of course hide them if I simply set org-hide-emphasis-markers
to
t
, but it makes editing hard since I never know whether I am before or
after the emphasis marker when editing near the beginning/end of an
emphasized region. org-appear
fixes this issue so that it shows the
emphasis markers only when the cursor is in the emphasized region,
otherwise they will remain hidden! Very cool!
(use-package org-appear
:after org
:straight (:build t)
:hook (org-mode . org-appear-mode)
:config
(setq org-appear-autoemphasis t
org-hide-emphasis-markers t
org-appear-autolinks t
org-appear-autoentities t
org-appear-autosubmarkers t)
(run-at-time nil nil #'org-appear--set-elements))
Similarly, LaTeX fragments previews are nice and all, but if I have my cursor on it, I want to see the LaTeX source code and modify it, not just the generated image!
(use-package org-fragtog
:defer t
:after org
:straight (:build t)
:hook (org-mode . org-fragtog-mode))
Org-modern modernizes a bit the appearance of org buffers, including
tables, source blocks, and tags, and it applies settings similar to
org-superstar
which I used to use.
(use-package org-modern
:straight (:build t)
:after org
:defer t
:hook (org-mode . org-modern-mode)
:hook (org-agenda-finalize . org-modern-agenda))
org-fancy-priorities
change the priority of an org element such as #A
to anything user-defined. Let’s all-the-iconify this!
(use-package org-fancy-priorities
:after (org all-the-icons)
:straight (:build t)
:hook (org-mode . org-fancy-priorities-mode)
:hook (org-agenda-mode . org-fancy-priorities-mode)
:config
(setq org-fancy-priorities-list `(,(all-the-icons-faicon "flag" :height 1.1 :v-adjust 0.0)
,(all-the-icons-faicon "arrow-up" :height 1.1 :v-adjust 0.0)
,(all-the-icons-faicon "square" :height 1.1 :v-adjust 0.0))))
Org Outline Tree is a better way of managing my org files’ outline.
(use-package org-ol-tree
:after (org avy)
:defer t
:straight (org-ol-tree :build t
:host github
:type git
:repo "Townk/org-ol-tree")
:general
(phundrak/major-leader-key
:packages 'org-ol-tree
:keymaps 'org-mode-map
"O" #'org-ol-tree))
(add-hook 'org-mode-hook
(lambda ()
(dolist (pair '(("[ ]" . ?☐)
("[X]" . ?☑)
("[-]" . ?❍)
("#+title:" . ?📕)
("#+TITLE:" . ?📕)
("#+author:" . ?✎)
("#+AUTHOR:" . ?✎)
("#+email:" . ?📧)
("#+EMAIL:" . ?📧)
("#+include" . ?⭳)
("#+INCLUDE" . ?⭳)
("#+begin_src" . ?λ)
("#+BEGIN_SRC" . ?λ)
("#+end_src" . ?λ)
("#+END_SRC" . ?λ)))
(add-to-list 'prettify-symbols-alist pair))
(prettify-symbols-mode)))
Misc
org-tree-slide
is a presentation tool for org-mode.
(use-package org-tree-slide
:defer t
:after org
:straight (:build t)
:config
(setq org-tree-slide-skip-done nil)
:general
(phundrak/evil
:keymaps 'org-mode-map
:packages 'org-tree-slide
"<f8>" #'org-tree-slide-mode)
(phundrak/major-leader-key
:keymaps 'org-tree-slide-mode-map
:packages 'org-tree-slide
"d" (lambda () (interactive (setq org-tree-slide-skip-done (not org-tree-slide-skip-done))))
"p" #'org-tree-slide-move-next-tree
"n" #'org-tree-slide-move-previous-tree
"t" #'org-tree-slide-move-next-tree
"s" #'org-tree-slide-move-previous-tree
"u" #'org-tree-slide-content))
org-roll
is a simple package for tabletop RPGs for rolling dice.
(use-package org-roll
:defer t
:after org
:straight (:build t :type git :host github :repo "zaeph/org-roll"))
Programming
Tools
Treesitter
Treesit is a native Emacs tree-sitter implementation which provides a very fast and flexible way of performing code-highlighting in Emacs. It is built-in in Emacs 29 and newer, and I just need to tweak one variable for now so I can install grammars for different languages.
(use-package treesit
:defer t
:straight (:type built-in)
:hook ((bash-ts-mode c-ts-mode c++-ts-mode
html-ts-mode js-ts-mode typescript-ts-mode
json-ts-mode rust-ts-mode tsx-ts-mode python-ts-mode
css-ts-mode yaml-ts-mode) . lsp-deferred)
:init
(setq treesit-language-source-alist
'((bash "https://github.com/tree-sitter/tree-sitter-bash")
(c "https://github.com/tree-sitter/tree-sitter-c")
(cmake "https://github.com/uyha/tree-sitter-cmake")
(common-lisp "https://github.com/theHamsta/tree-sitter-commonlisp")
(cpp "https://github.com/tree-sitter/tree-sitter-cpp")
(css "https://github.com/tree-sitter/tree-sitter-css")
(csharp "https://github.com/tree-sitter/tree-sitter-c-sharp")
(elisp "https://github.com/Wilfred/tree-sitter-elisp")
(go "https://github.com/tree-sitter/tree-sitter-go")
(go-mod "https://github.com/camdencheek/tree-sitter-go-mod")
(html "https://github.com/tree-sitter/tree-sitter-html")
(js . ("https://github.com/tree-sitter/tree-sitter-javascript" "master" "src"))
(json "https://github.com/tree-sitter/tree-sitter-json")
(lua "https://github.com/Azganoth/tree-sitter-lua")
(make "https://github.com/alemuller/tree-sitter-make")
(markdown "https://github.com/ikatyang/tree-sitter-markdown")
(python "https://github.com/tree-sitter/tree-sitter-python")
(r "https://github.com/r-lib/tree-sitter-r")
(rust "https://github.com/tree-sitter/tree-sitter-rust")
(toml "https://github.com/tree-sitter/tree-sitter-toml")
(tsx . ("https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src"))
(typescript . ("https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src"))
(yaml "https://github.com/ikatyang/tree-sitter-yaml"))))
Appwrite
Appwrite is an open-source and self-hostable alternative to Firebase. I am currently working on a server SDK for Appwrite in Emacs, so here it is.
(use-package appwrite
:defer t
:straight (appwrite :build t
:type git
:host github
:repo "Phundrak/appwrite.el")
:config
(csetq appwrite-endpoint "https://appwrite.phundrak.com"
appwrite-devel t))
Databases
A really cool tool in Emacs for manipulating databases is emacsql
.
It’s able to manipulate Sqlite databases by default, but it’s also
possible to manipulate MariaDB and PostgreSQL databases by installing
additional packages. For now, I just need Sqlite and PostgreSQL
interfaces, so let’s install the relevant packages.
(use-package emacsql-psql
:defer t
:after (emacsql)
:straight (:build t))
(with-eval-after-load 'emacsql
(phundrak/major-leader-key
:keymaps 'emacs-lisp-mode-map
:packages '(emacsql)
"E" #'emacsql-fix-vector-indentation))
Flycheck
(use-package flycheck
:straight (:build t)
:defer t
:init
(global-flycheck-mode)
:config
(setq flycheck-emacs-lisp-load-path 'inherit)
;; Rerunning checks on every newline is a mote excessive.
(delq 'new-line flycheck-check-syntax-automatically)
;; And don’t recheck on idle as often
(setq flycheck-idle-change-delay 2.0)
;; For the above functionality, check syntax in a buffer that you
;; switched to on briefly. This allows “refreshing” the syntax check
;; state for several buffers quickly after e.g. changing a config
;; file.
(setq flycheck-buffer-switch-check-intermediate-buffers t)
;; Display errors a little quicker (default is 0.9s)
(setq flycheck-display-errors-delay 0.2))
(use-package flycheck-popup-tip
:straight (:build t)
:after (flycheck evil)
:hook (flycheck-mode . flycheck-popup-tip-mode)
:config
(setq flycheck-popup-tip-error-prefix "X ")
(with-eval-after-load 'evil
(add-hook 'evil-insert-state-entry-hook
#'flycheck-popup-tip-delete-popup)
(add-hook 'evil-replace-state-entry-hook
#'flycheck-popup-tip-delete-popup)))
(use-package flycheck-posframe
:straight (:build t)
:hook (flycheck-mode . flycheck-posframe-mode)
:config
(setq flycheck-posframe-warning-prefix "! "
flycheck-posframe-info-prefix "··· "
flycheck-posframe-error-prefix "X "))
Langtool
LanguageTool is a great tool for catching typos and grammatical errors in quite a few languages.
(use-package langtool
:defer t
:straight (:build t)
:commands (langtool-check
langtool-check-done
langtool-show-message-at-point
langtool-correct-buffer)
:custom
(langtool-default-language "en-US")
(langtool-mother-tongue "fr")
:config
(setq langtool-java-classpath (string-join '("/usr/share/languagetool"
"/usr/share/java/languagetool/*")
":"))
:general
(phundrak/leader-key
:packages 'langtool
:infix "l"
"" '(:ignore t :which-key "LangTool")
"B" #'langtool-correct-buffer
"b" #'langtool-check-buffer
"c" #'langtool-check
"d" #'langtool-check-done
"l" #'langtool-switch-default-language
"p" #'langtool-show-message-at-point))
(use-package writegood-mode
:defer t
:straight (:build t)
:hook org-mode latex-mode
:general
(phundrak/major-leader-key
:keymaps 'writegood-mode-map
"g" #'writegood-grade-level
"r" #'writegood-reading-ease))
Spellcheck
(use-package ispell
:if (executable-find "aspell")
:defer t
:straight (:type built-in)
:config
(add-to-list 'ispell-skip-region-alist '(":\\(PROPERTIES\\|LOGBOOK\\):" . ":END:"))
(add-to-list 'ispell-skip-region-alist '("#\\+BEGIN_SRC" . "#\\+END_SRC"))
(add-to-list 'ispell-skip-region-alist '("#\\+BEGIN_EXAMPLE" . "#\\+END_EXAMPLE"))
(setq ispell-program-name "aspell"
ispell-extra-args '("--sug-mode=ultra" "--run-together")
ispell-aspell-dict-dir (ispell-get-aspell-config-value "dict-dir")
ispell-aspell-data-dir (ispell-get-aspell-config-value "data-dir")
ispell-personal-dictionary (expand-file-name (concat "ispell/" ispell-dictionary ".pws")
user-emacs-directory)))
(use-package flyspell
:defer t
:straight (:type built-in)
:ghook 'org-mode 'markdown-mode 'TeX-mode
:init
(defhydra flyspell-hydra ()
"
Spell Commands^^ Add To Dictionary^^ Other
--------------^^---------- -----------------^^------------- -----^^---------------------------
[_b_] check whole buffer [_B_] add word to dict (buffer) [_t_] toggle spell check
[_r_] check region [_G_] add word to dict (global) [_q_] exit
[_d_] change dictionary [_S_] add word to dict (session) [_Q_] exit and disable spell check
[_n_] next error
[_c_] correct before point
[_s_] correct at point
"
("B" nil)
("b" flyspell-buffer)
("r" flyspell-region)
("d" ispell-change-dictionary)
("G" nil)
("n" flyspell-goto-next-error)
("c" flyspell-correct-wrapper)
("Q" flyspell-mode :exit t)
("q" nil :exit t)
("S" nil)
("s" flyspell-correct-at-point)
("t" nil))
:config
(provide 'ispell) ;; force loading ispell
(setq flyspell-issue-welcome-flag nil
flyspell-issue-message-flag nil))
(use-package flyspell-correct
:defer t
:straight (:build t)
:general ([remap ispell-word] #'flyspell-correct-at-point)
:config
(require 'flyspell-correct-ivy nil t))
(use-package flyspell-correct-ivy
:defer t
:straight (:build t)
:after flyspell-correct)
(use-package flyspell-lazy
:defer t
:straight (:build t)
:after flyspell
:config
(setq flyspell-lazy-idle-seconds 1
flyspell-lazy-window-idle-seconds 3)
(flyspell-lazy-mode +1))
LSP-Mode
lsp-mode
is a mode for Emacs which implements the Language Server
Protocol and offers Emacs an IDE-like experience. In short, it’s
awesome!
(use-package lsp-mode
:defer t
:straight (:build t)
:init
(setq lsp-keymap-prefix "C-c l"
read-process-output-max (* 3 1024 1024))
:hook ((c-mode . lsp-deferred)
(c++-mode . lsp-deferred)
(html-mode . lsp-deferred)
(sh-mode . lsp-deferred)
(lsp-mode . lsp-enable-which-key-integration)
(lsp-mode . lsp-ui-mode))
:commands (lsp lsp-deferred)
:custom
(lsp-rust-analyzer-cargo-watch-command "clippy")
(lsp-eldoc-render-all t)
(lsp-idle-delay 0.6)
(lsp-rust-analyzer-server-display-inlay-hints t)
(lsp-use-plist t)
:config
(lsp-register-client
(make-lsp-client :new-connection (lsp-tramp-connection "shellcheck")
:major-modes '(sh-mode)
:remote? t
:server-id 'shellcheck-remote)))
I also want all the visual enhancements LSP can provide.
(use-package lsp-ui
:after lsp
:defer t
:straight (:build t)
:commands lsp-ui-mode
:custom
(lsp-ui-peek-always-show t)
(lsp-ui-sideline-show-hover t)
(lsp-ui-doc-enable t)
:general
(phundrak/major-leader-key
:keymaps 'lsp-ui-peek-mode-map
:packages 'lsp-ui
"c" #'lsp-ui-pook--select-prev-file
"t" #'lsp-ui-pook--select-next
"s" #'lsp-ui-pook--select-prev
"r" #'lsp-ui-pook--select-next-file))
And let’s enable some intergration with ivy
.
(use-package lsp-ivy
:straight (:build t)
:defer t
:after lsp
:commands lsp-ivy-workspace-symbol)
(use-package lsp-treemacs
:defer t
:straight (:build t)
:requires treemacs
:config
(treemacs-resize-icons 15))
Key | Function | Description |
---|---|---|
treemacs | ||
c | create | |
cd | treemacs-create-dir | |
cf | treemacs-create-file | |
ci | treemacs-create-icon | |
ct | treemacs-create-theme | |
cw | treemacs-create-workspace | |
d | treemacs-delete-file | |
f | files | |
ff | treemacs-find-file | |
ft | treemacs-find-tag | |
l | lsp | |
ls | treemacs-expand-lsp-symbol | |
ld | treemacs-expand-lsp-treemacs-deps | |
lD | treemacs-collapse-lsp-treemacs-deps | |
lS | treemacs-collapse-lsp-symbol | |
p | projects | |
pa | treemacs-add-project | |
pf | treemacs-project-follow-mode | |
pn | treemacs-project-of-node | |
pp | treemacs-project-at-point | |
pr | treemacs-remove-project-from-workspace | |
pt | treemacs-move-project-down | |
ps | treemacs-move-project-up | |
r | rename | |
rf | treemacs-rename-file | |
rp | treemacs-rename-project | |
rr | treemacs-rename | |
rw | treemacs-rename-workspace | |
t | treemacs | |
T | toggles | |
Td | treemacs-toggle-show-dotfiles | |
Tn | treemacs-toggle-node | |
v | visit node | |
va | treemacs-visit-node-ace | |
vc | treemacs-visit-node-close-treemacs | |
vn | treemacs-visit-node-default | |
y | yank | |
ya | treemacs-copy-absolute-path-at-point | |
yp | treemacs-copy-project-path-at-point | |
yr | treemacs-copy-relative-path-at-point | |
yf | treemacs-copy-file |
(use-package exec-path-from-shell
:defer t
:straight (:build t)
:init (exec-path-from-shell-initialize))
(use-package consult-lsp
:defer t
:after lsp
:straight (:build t)
:general
(phundrak/evil
:keymaps 'lsp-mode-map
[remap xref-find-apropos] #'consult-lsp-symbols))
dap-mode
is an advanced debugging mode that works through lsp.
(use-package dap-mode
:after lsp
:defer t
:straight (:build t)
:config
(dap-ui-mode)
(dap-ui-controls-mode 1)
(require 'dap-lldb)
(require 'dap-gdb-lldb)
(dap-gdb-lldb-setup)
(dap-register-debug-template
"Rust::LLDB Run Configuration"
(list :type "lldb"
:request "launch"
:name "LLDB::Run"
:gdbpath "rust-lldb"
:target nil
:cwd nil)))
DSLs
DSLs, or Domain Specific Languages, are languages dedicated to some very tasks, such as configuration languages or non-general programming such as SQL.
Makefiles
(defun my/local-tab-indent ()
(setq-local indent-tabs-mode 1))
(add-hook 'makefile-mode-hook #'my/local-tab-indent)
Caddy
Caddy (or Caddyserver) is a web server akin to Nginx or Apache which I find much easier to configure that the latter two, plus it has built-in support for automatically generating SSL certificates with Letsencrypt! Automatic HTTPS, what more do you want?
All that is nice and all, but Emacs doesn’t support the syntax of
Caddy files natively, so let’s install caddyfile-mode
:
(use-package caddyfile-mode
:defer t
:straight (:build t)
:mode (("Caddyfile\\'" . caddyfile-mode)
("caddy\\.conf\\'" . caddyfile-mode)))
CMake
CMake is one of the standard tools for indicating how a project should be built. It is not as standard as some other tools such as automake, autoconfig, and the likes, but still pretty standard. CMake however doesn’t have a major mode available by default, so let’s provide one.
(use-package cmake-mode
:defer t
:straight (:build t))
Let’s enable first some autocompletion for it.
(use-package company-cmake
:straight (company-cmake :build t
:type git
:host github
:repo "purcell/company-cmake")
:after cmake-mode
:defer t)
And let’s also enable a more advanced CMake fontlock.
(use-package cmake-font-lock
:defer t
:after cmake-mode
:straight (:build t))
And finally, let’s enable some Eldoc integration for CMake.
(use-package eldoc-cmake
:straight (:build t)
:defer t
:after cmake-mode)
CSV
(use-package csv-mode
:straight (:build t)
:defer t
:general
(phundrak/major-leader-key
:keymaps 'csv-mode-map
"a" #'csv-align-fields
"d" #'csv-kill-fields
"h" #'csv-header-line
"i" #'csv-toggle-invisibility
"n" #'csv-forward-field
"p" #'csv-backward-field
"r" #'csv-reverse-region
"s" '(:ignore t :wk "sort")
"sf" #'csv-sort-fields
"sn" #'csv-sort-numeric-fields
"so" #'csv-toggle-descending
"t" #'csv-transpose
"u" #'csv-unalign-fields
"y" '(:ignore t :wk yank)
"yf" #'csv-yank-fields
"yt" #'csv-yank-as-new-table))
Dotenv
It is not rare to encounter a dotenv file, that is, a file with either
the .env
extension or simply called .env
. They contain environment
variables for projects which might rely on values you do not want to
upload to a public git repository and such. While their syntax is
similar to shell files, they’re not exactly shell files either since
there’s rarely any script inside running. So, let’s install a simple
package which will dumb down a lot sh-mode
for these dotenv files.
(use-package dotenv-mode
:defer t
:straight (:build t))
Gnuplot
This package is a front-end and major mode for the programming language Gnuplot. Let’s make some beautiful graphs, shall we?
(use-package gnuplot
:straight (:build t)
:defer t)
Graphviz
Graphviz, often known with dot
, allows to programatically create
visual graphs and networks.
(use-package graphviz-dot-mode
:defer t
:straight (:build t)
:after org
:mode (("\\.diag\\'" . graphviz-dot-mode)
("\\.blockdiag\\'" . graphviz-dot-mode)
("\\.nwdiag\\'" . graphviz-dot-mode)
("\\.rackdiag\\'" . graphviz-dot-mode)
("\\.dot\\'" . graphviz-dot-mode)
("\\.gv\\'" . graphviz-dot-mode))
:init
(setq graphviz-dot-indent-width tab-width)
(with-eval-after-load 'org
(defalias 'org-babel-execute:graphviz-dot #'org-babel-execute:dot)
(add-to-list 'org-babel-load-languages '(dot . t))
(require 'ob-dot)
(setq org-src-lang-modes
(append '(("dot" . graphviz-dot))
(delete '("dot" . fundamental) org-src-lang-modes))))
:general
(phundrak/major-leader-key
:keymaps 'graphviz-dot-mode-map
"=" #'graphviz-dot-indent-graph
"c" #'compile)
:config
(setq graphviz-dot-indent-width 4))
Markdown
Yes, I love org-mode and I largely prefer to use it instead of Markdown due to its far superior power and abilities. But still, sometimes I need to use Markdown because not everyone use org-mode, unfortunately.
(use-package markdown-mode
:defer t
:straight t
:mode
(("\\.mkd\\'" . markdown-mode)
("\\.mdk\\'" . markdown-mode)
("\\.mdx\\'" . markdown-mode))
:hook (markdown-mode . orgtbl-mode)
:hook (markdown-mode . visual-line-mode)
:general
(phundrak/evil
:keymaps 'markdown-mode-map
:packages '(markdown-mode evil)
"M-RET" #'markdown-insert-list-item
"M-c" #'markdown-promote
"M-t" #'markdown-move-down
"M-s" #'markdown-move-up
"M-r" #'markdown-demote
"t" #'evil-next-visual-line
"s" #'evil-previous-visual-line)
(phundrak/major-leader-key
:keymaps 'markdown-mode-map
:packages 'markdown-mode
"{" #'markdown-backward-paragraph
"}" #'markdown-forward-paragraph
"]" #'markdown-complete
">" #'markdown-indent-region
"»" #'markdown-indent-region
"<" #'markdown-outdent-region
"«" #'markdown-outdent-region
"n" #'markdown-next-link
"p" #'markdown-previous-link
"f" #'markdown-follow-thing-at-point
"k" #'markdown-kill-thing-at-point
"c" '(:ignore t :which-key "command")
"c]" #'markdown-complete-buffer
"cc" #'markdown-check-refs
"ce" #'markdown-export
"cm" #'markdown-other-window
"cn" #'markdown-cleanup-list-numbers
"co" #'markdown-open
"cp" #'markdown-preview
"cv" #'markdown-export-and-preview
"cw" #'markdown-kill-ring-save
"h" '(:ignore t :which-key "headings")
"hi" #'markdown-insert-header-dwim
"hI" #'markdown-insert-header-setext-dwim
"h1" #'markdown-insert-header-atx-1
"h2" #'markdown-insert-header-atx-2
"h3" #'markdown-insert-header-atx-3
"h4" #'markdown-insert-header-atx-4
"h5" #'markdown-insert-header-atx-5
"h6" #'markdown-insert-header-atx-6
"h!" #'markdown-insert-header-setext-1
"h@" #'markdown-insert-header-setext-2
"i" '(:ignore t :which-key "insert")
"i-" #'markdown-insert-hr
"if" #'markdown-insert-footnote
"ii" #'markdown-insert-image
"il" #'markdown-insert-link
"it" #'markdown-insert-table
"iw" #'markdown-insert-wiki-link
"l" '(:ignore t :which-key "lists")
"li" #'markdown-insert-list-item
"T" '(:ignore t :which-key "toggle")
"Ti" #'markdown-toggle-inline-images
"Tu" #'markdown-toggle-url-hiding
"Tm" #'markdown-toggle-markup-hiding
"Tt" #'markdown-toggle-gfm-checkbox
"Tw" #'markdown-toggle-wiki-links
"t" '(:ignore t :which-key "table")
"tc" #'markdown-table-move-column-left
"tt" #'markdown-table-move-row-down
"ts" #'markdown-table-move-row-up
"tr" #'markdown-table-move-column-right
"ts" #'markdown-table-sort-lines
"tC" #'markdown-table-convert-region
"tt" #'markdown-table-transpose
"td" '(:ignore t :which-key "delete")
"tdc" #'markdown-table-delete-column
"tdr" #'markdown-table-delete-row
"ti" '(:ignore t :which-key "insert")
"tic" #'markdown-table-insert-column
"tir" #'markdown-table-insert-row
"x" '(:ignore t :which-key "text")
"xb" #'markdown-insert-bold
"xB" #'markdown-insert-gfm-checkbox
"xc" #'markdown-insert-code
"xC" #'markdown-insert-gfm-code-block
"xi" #'markdown-insert-italic
"xk" #'markdown-insert-kbd
"xp" #'markdown-insert-pre
"xP" #'markdown-pre-region
"xs" #'markdown-insert-strike-through
"xq" #'markdown-blockquote-region)
:config
(setq markdown-fontify-code-blocks-natively t))
Since most of my Markdown files are related to Github, I’d like to be able to render Markdown through its API.
(use-package gh-md
:defer t
:after markdown-mode
:straight (:build t)
:general
(phundrak/major-leader-key
:packages 'gh-md
:keymaps 'markdown-mode-map
"cr" #'gh-md-render-buffer))
Sometimes, I have to work with Github’s markdown flavour, but I’m not
really a huge fan of writing it by hand. So instead, I’ll write it in
org-mode and then export it with ox-gfm
.
(use-package ox-gfm
:straight (:build t)
:defer t
:after (org ox))
Nuxt has its own flavour of Markdown, called MDC (MarkDown Components)
which is a godsend to write content for Nuxt websites! However, no
mdc-mode
existed when I began working with it, so I’m working on one.
(use-package mdc-mode
:defer t
:after markdown-mode
:straight (mdc-mode :type git
:host github
:repo "Phundrak/mdc-mode"
:build t))
Tables of content are always nice to have for large files, just like
with the toc-org
package for org-mode.
(use-package markdown-toc
:defer t
:after markdown-mode
:straight (:build t)
:general
(phundrak/major-leader-key
:packages 'markdown-toc
:keymaps 'markdown-mode-map
"iT" #'markdown-toc-generate-toc))
Lastly, edit-indirect
is a package that allows to edit code blocks as
in org-mode but with other major modes, such as code blocks in
Markdown.
(use-package edit-indirect
:straight (:build t)
:defer t)
Nginx
Nginx is another webserver, older and more mature than Caddy. A couple of packages are required in order to be able to properly work with Nginx configuration files. First, we need the correct mode for editing Nginx configuration files.
(use-package nginx-mode
:straight (:build t)
:defer t)
We then also have an autocompletion package that adds to company
the
Nginx syntax.
(use-package company-nginx
:straight (company-nginx :build t
:type git
:host github
:repo "emacsmirror/company-nginx")
:defer t
:config
(add-hook 'nginx-mode-hook (lambda ()
(add-to-list 'company-backends #'company-nginx))))
PKGBUILD
As I am an ArchLinux user, I sometimes have to interact with PKGBUILD files, both from the AUR when I want to install something from there or some I write myself.
(use-package pkgbuild-mode
:straight (:build t)
:defer t
:general
(phundrak/major-leader-key
:keymaps 'pkgbuild-mode-map
"c" #'pkgbuild-syntax-check
"i" #'pkgbuild-initialize
"I" #'pkgbuild-increase-release-tag
"m" #'pkgbuild-makepkg
"u" '(:ignore :wk "update")
"us" #'pkgbuild-update-sums-line
"uS" #'pkgbuild-update-srcinfo))
PlantUML
(use-package plantuml-mode
:defer t
:straight (:build t)
:mode ("\\.\\(pum\\|puml\\)\\'" . plantuml-mode)
:after ob
:init
(add-to-list 'org-babel-load-languages '(plantuml . t))
:general
(phundrak/major-leader-key
:keymaps 'plantuml-mode-map
:packages 'plantuml-mode
"c" '(:ignore t :which-key "compile")
"cc" #'plantuml-preview
"co" #'plantuml-set-output-type)
:config
(setq plantuml-default-exec-mode 'jar
plantuml-jar-path "~/.local/bin/plantuml.jar"
org-plantuml-jar-path "~/.local/bin/plantuml.jar"))
Shells
Aside from Eshell, my main shell on my machine is fish (see my fish config), therefore I need a mode for it.
(use-package fish-mode
:straight (:build t)
:defer t)
When editing some scripts though, I need to use the built-in shell-mode
.
(use-package shell
:defer t
:straight (:type built-in)
:hook (shell-mode . tree-sitter-hl-mode))
SSH Config files
(use-package ssh-config-mode
:defer t
:straight (:build t))
Systemd
(use-package systemd
:defer t
:straight (:build t)
:general
(phundrak/major-leader-key
:keymaps '(systemd-mode-map)
"d" '(systemd-doc-directives :which-key "directives manpage")
"o" 'systemd-doc-open))
Toml
(use-package toml-mode
:straight (:build t)
:defer t
:mode "/\\(Cargo.lock\\|\\.cargo/config\\)\\'")
Yaml
(use-package yaml-mode
:defer t
:straight (:build t)
:mode "\\.yml\\'"
:mode "\\.yaml\\'")
General Programming Languages
C/C++
I know, I know, C and C++ no longer are closely related languages, each one of them went their own way and learning C won’t make you a good C++ programmer, neither will the other way around. But, They are still somewhat related, and Emacs thinks so too.
(use-package cc-mode
:straight (:type built-in)
:defer t
:init
(put 'c-c++-backend 'safe-local-variable 'symbolp)
(add-hook 'c-mode-hook #'tree-sitter-hl-mode)
(add-hook 'c++-mode-hook #'tree-sitter-hl-mode)
:config
(require 'compile)
:general
(phundrak/undefine
:keymaps '(c-mode-map c++-mode-map)
";" nil)
(phundrak/major-leader-key
:keymaps '(c-mode-map c++-mode-map)
"l" '(:keymap lsp-command-map :which-key "lsp" :package lsp-mode))
(phundrak/evil
:keymaps '(c-mode-map c++-mode-map)
"ga" #'projectile-find-other-file
"gA" #'projectile-find-other-file-other-window))
Something that is also important when working with these languages is
respecting the .clang-format
file that may be provided by a project.
(use-package clang-format+
:straight (:build t)
:defer t
:init
(add-hook 'c-mode-common-hook #'clang-format+-mode))
However, Emacs’ notion of C++ is somewhat outdated, so we need to update its fontlock.
(use-package modern-cpp-font-lock
:straight (:build t)
:defer t
:hook (c++-mode . modern-c++-font-lock-mode))
CommonLisp
In Lisp buffers, let’s enable parinfer-rust-mode
.
(use-package lisp-mode
:straight (:type built-in)
:defer t
:after parinfer-rust-mode
:hook (lisp-mode . parinfer-rust-mode)
:config
(put 'defcommand 'lisp-indent-function 'defun)
(setq inferior-lisp-program "/usr/bin/sbcl --noinform"))
My current window manager is StumpWM, inspired by Emacs and written in
CommonLisp. stumpwm-mode
offers some integration between Emacs and
StumpWM so we can evaluate CLisp code and see its effects in StumpWM
immediately. Since my only use for CommonLisp is for my StumpWM
configuration, it should be automatically enabled when entering
lisp-mode
.
(use-package stumpwm-mode
:straight (:build t)
:defer t
:hook lisp-mode
:config
(phundrak/major-leader-key
:keymaps 'stumpwm-mode-map
:packages 'stumpwm-mode
"e" '(:ignore t :which-key "eval")
"ee" #'stumpwm-eval-last-sexp
"ed" #'stumpwm-eval-defun
"er" #'stumpwm-eval-region))
Sly enables some deep interactivity between Emacs and a CommonLisp application running the Slynk backend. For an example, see my Sly configuration for StumpWM.
(use-package sly
:defer t
:straight (:build t))
Dart
(use-package dart-mode
:straight (:build t)
:defer t
:hook (dart-mode . lsp-deferred)
:mode "\\.dart\\'")
(use-package lsp-dart
:straight (:build t)
:defer t
:general
(phundrak/major-leader-key
:keymaps 'dart-mode-map
:packages '(lsp-mode lsp-dart)
"l" '(:keymap lsp-command-map :which-key "lsp")))
EmacsLisp
This package displays the function’s arglist or variable’s docstring in the echo area at the bottom of the frame. Quite useful indeed.
(use-package eldoc
:defer t
:after company
:init
(eldoc-add-command 'company-complete-selection
'company-complete-common
'company-capf
'company-abort))
(add-hook 'emacs-lisp-mode-hook (lambda () (smartparens-mode -1)))
Still on the topic of documentation, I sometimes find it lacks
examples on how to use Elisp functions. elisp-demos
got you covered!
(use-package elisp-demos
:defer t
:straight (:build t)
:config
(advice-add 'helpful-update :after #'elisp-demos-advice-helpful-update))
(use-package epdh
:straight (epdh :type git
:host github
:repo "alphapapa/emacs-package-dev-handbook"
:build t)
:defer t)
Let’s also declare some Elisp-dedicated keybindings, prefixed by a comma.
(phundrak/major-leader-key
:keymaps 'emacs-lisp-mode-map
"'" #'ielm
"c" '(emacs-lisp-byte-compile :which-key "Byte compile")
"C" '(:ignore t :which-key "checkdoc")
"Cc" #'checkdoc
"Cs" #'checkdoc-start
"e" '(:ignore t :which-key "eval")
"eb" #'eval-buffer
"ed" #'eval-defun
"ee" #'eval-last-sexp
"er" #'eval-region
"h" '(:ignore t :which-key "help")
"hh" #'helpful-at-point
"t" '(:ignore t :wk "toggle")
"tP" '(:ignore t :wk "parinfer")
"tPs" #'parinfer-rust-switch-mode
"tPd" #'parinfer-rust-mode-disable
"tPp" #'parinfer-rust-toggle-paren-mode)
Package linting is important when you want to publish your packages to the world.
(use-package package-lint
:defer t
:straight (:build t)
:general
(phundrak/major-leader-key
:keymaps 'emacs-lisp-mode-map
:packages 'package-lint
"l" #'package-lint-current-buffer))
If I need to run CI on a package, Cask manages its dependencies.
(use-package cask-mode
:defer t
:straight (:build t))
However, I recently began using Eask more and more, I find it nicer to work with and it has a lot more features than Cask.
(use-package eask-api
:defer t
:straight (eask-api :type git
:host github
:repo "emacs-eask/eask-api"))
(use-package eask-mode
:defer t
:straight (eask-mode :type git
:host github
:repo "emacs-eask/eask-mode"))
Java
Emacs has built-in support for Java, but it still lacks some features. The main one is being able to replace an IDE, so let’s install its LSP package.
(use-package lsp-java
:requires lsp
:straight (:build t)
:after lsp
:hook (java-mode . lsp-deferred))
Python
First, we need to set up the main Python mode. With this, we’ll also add Python to the list of LSP languages and to the list of languages org-babel supports.
(use-package python
:defer t
:straight (:build t)
:after ob
:mode (("SConstruct\\'" . python-mode)
("SConscript\\'" . python-mode)
("[./]flake8\\'" . conf-mode)
("/Pipfile\\'" . conf-mode))
:init
(setq python-indent-guess-indent-offset-verbose nil)
(add-hook 'python-mode-local-vars-hook #'lsp)
:config
(setq python-indent-guess-indent-offset-verbose nil)
(when (and (executable-find "python3")
(string= python-shell-interpreter "python"))
(setq python-shell-interpreter "python3"))
:general
(phundrak/major-leader-key
:keymaps 'python-mode-map
:packages 'lsp-mode
"l" '(:keymap lsp-command-map :which-key "lsp")))
Now let’s add a package for pytest.
(use-package pytest
:defer t
:straight (:build t)
:commands (pytest-one
pytest-pdb-one
pytest-all
pytest-pdb-all
pytest-last-failed
pytest-pdb-last-failed
pytest-module
pytest-pdb-module)
:config
(add-to-list 'pytest-project-root-files "setup.cfg")
:general
(phundrak/major-leader-key
:keymaps 'python-mode-map
:infix "t"
:packages 'pytest
"" '(:ignore t :which-key "test")
"a" #'python-pytest
"f" #'python-pytest-file-dwim
"F" #'python-pytest-file
"t" #'python-pytest-function-dwim
"T" #'python-pytest-function
"r" #'python-pytest-repeat
"p" #'python-pytest-dispatch))
Poetry is a nice tool with which we can manage our Python runtime version as well as our dependencies.
(use-package poetry
:defer t
:straight (:build t)
:commands (poetry-venv-toggle
poetry-tracking-mode)
:config
(setq poetry-tracking-strategy 'switch-buffer)
(add-hook 'python-mode-hook #'poetry-tracking-mode))
This package will bring a new major mode for editing pip requirements.
(use-package pip-requirements
:defer t
:straight (:build t))
Why use the command line to interact with pip when we can do it with an Emacs frontend?
(use-package pippel
:defer t
:straight (:build t)
:general
(phundrak/major-leader-key
:keymaps 'python-mode-map
:packages 'pippel
"P" #'pippel-list-packages))
This is a pipenv porcelain
(use-package pipenv
:defer t
:straight (:build t)
:commands (pipenv-activate
pipenv-deactivate
pipenv-shell
pipenv-open
pipenv-install
pipenv-uninstall)
:hook (python-mode . pipenv-mode)
:init (setq pipenv-with-projectile nil)
:general
(phundrak/major-leader-key
:keymaps 'python-mode-map
:packages 'pipenv
:infix "e"
"" '(:ignore t :which-key "pipenv")
"a" #'pipenv-activate
"d" #'pipenv-deactivate
"i" #'pipenv-install
"l" #'pipenv-lock
"o" #'pipenv-open
"r" #'pipenv-run
"s" #'pipenv-shell
"u" #'pipenv-uninstall))
This integrates pyenv
into python-mode
.
(use-package pyenv
:defer t
:straight (:build t)
:config
(add-hook 'python-mode-hook #'pyenv-track-virtualenv)
(add-to-list 'global-mode-string
'(pyenv-virtual-env-name (" venv:" pyenv-virtual-env-name " "))
'append))
Let’s also add a mode for pyenv
:
(use-package pyenv-mode
:defer t
:after python
:straight (:build t)
:if (executable-find "pyenv")
:commands (pyenv-mode-versions)
:general
(phundrak/major-leader-key
:packages 'pyenv-mode
:keymaps 'python-mode-map
:infix "v"
"u" #'pyenv-mode-unset
"s" #'pyenv-mode-set))
This package automatically imports packages we forgot to import.
(use-package pyimport
:defer t
:straight (:build t)
:general
(phundrak/major-leader-key
:packages 'pyimport
:keymaps 'python-mode-map
:infix "i"
"" '(:ignore t :which-key "imports")
"i" #'pyimport-insert-missing
"r" #'pyimport-remove-unused))
On the other hand, this one sorts our imports to make them more readable.
(use-package py-isort
:defer t
:straight (:build t)
:general
(phundrak/major-leader-key
:keymaps 'python-mode-map
:packages 'py-isort
:infix "i"
"" '(:ignore t :which-key "imports")
"s" #'py-isort-buffer
"R" #'py-isort-region))
Access pydoc through counsel.
(use-package counsel-pydoc
:defer t
:straight (:build t))
This generates Python documentation that is meant to be compatible with Sphinx, a documentation generaton for Python.
(use-package sphinx-doc
:defer t
:straight (:build t)
:init
(add-hook 'python-mode-hook #'sphinx-doc-mode)
:general
(phundrak/major-leader-key
:keymaps 'python-mode-map
:packages 'sphinx-doc
:infix "S"
"" '(:ignore t :which-key "sphinx-doc")
"e" #'sphinx-doc-mode
"d" #'sphinx-doc))
Cython is a Python to C compiler. It also introduces the extended Cython programming language which makes writing C for Python easier. This package is a major mode for the Cython programming language.
(use-package cython-mode
:defer t
:straight (:build t)
:mode "\\.p\\(yx\\|x[di]\\)\\'"
:config
(setq cython-default-compile-format "cython -a %s")
:general
(phundrak/major-leader-key
:keymaps 'cython-mode-map
:packages 'cython-mode
:infix "c"
"" '(:ignore t :which-key "cython")
"c" #'cython-compile))
Flycheck can also be enabled for Cython:
(use-package flycheck-cython
:defer t
:straight (:build t)
:after cython-mode)
Blacken uses the black
formatter backend to format Python buffers.
(use-package blacken
:defer t
:straight (:build t)
:init
(add-hook 'python-mode-hook #'blacken-mode))
Finally, I’m using Pyright as my LSP backend for Python.
(use-package lsp-pyright
:after lsp-mode
:defer t
:straight (:buidl t))
Rust
Rust is a general programming language, akin to C++ in some ways, but
much more oriented towards safe code, and much better suited for web
development. First, let’s install the most important package,
rustic
.
(use-package rustic
:defer t
:straight (:build t)
:mode ("\\.rs\\'" . rustic-mode)
:hook (rustic-mode-local-vars . rustic-setup-lsp)
:hook (rustic-mode . lsp-deferred)
:init
(with-eval-after-load 'org
(defalias 'org-babel-execute:rust #'org-babel-execute:rustic)
(add-to-list 'org-src-lang-modes '("rust" . rustic)))
(setq rustic-lsp-client 'lsp-mode)
(add-hook 'rustic-mode-hook #'tree-sitter-hl-mode)
:general
(general-define-key
:keymaps 'rustic-mode-map
:packages 'lsp
"M-t" #'lsp-ui-imenu
"M-?" #'lsp-find-references)
(phundrak/major-leader-key
:keymaps 'rustic-mode-map
:packages 'lsp-mode
"l" '(:keymap lsp-command-map :which-key "lsp"))
(phundrak/major-leader-key
:keymaps 'rustic-mode-map
:packages 'rustic
"b" '(:ignore t :which-key "build")
"bb" #'rustic-cargo-build
"bB" #'rustic-cargo-bench
"bc" #'rustic-cargo-check
"bC" #'rustic-cargo-clippy
"bd" #'rustic-cargo-doc
"bf" #'rustic-cargo-fmt
"bn" #'rustic-cargo-new
"bo" #'rustic-cargo-outdated
"br" #'rustic-cargo-run
"t" '(:ignore t :which-key "cargo test")
"ta" #'rustic-cargo-test
"tt" #'rustic-cargo-current-test)
:config
(setq rustic-indent-method-chain t
rustic-babel-format-src-block nil
rustic-format-trigger nil)
(remove-hook 'rustic-mode-hook #'flycheck-mode)
(remove-hook 'rustic-mode-hook #'flymake-mode-off)
(remove-hook 'rustic-mode-hook #'rustic-setup-lsp))
Web programming
Emmet is a powerful templating engine that can generate through simple CSS-like expression some HTML so you don’t have to write everything by hand.
(use-package emmet-mode
:straight (:build t)
:defer t
:hook ((css-mode . emmet-mode)
(html-mode . emmet-mode)
(web-mode . emmet-mode)
(sass-mode . emmet-mode)
(scss-mode . emmet-mode)
(web-mode . emmet-mode))
:config
(general-define-key
:keymaps 'emmet-mode-keymap
"M-RET" #'emmet-expand-yas)
(phundrak/major-leader-key
:keymaps 'web-mode-map
:packages '(web-mode emmet-mode)
"e" '(:ignore t :which-key "emmet")
"ee" #'emmet-expand-line
"ep" #'emmet-preview
"eP" #'emmet-preview-mode
"ew" #'emmet-wrap-with-markup))
Impatient mode serves web buffers live over HTTP so you can see your editions as you type them.
(use-package impatient-mode
:straight (:build t)
:defer t)
Web mode is a sort of hybrid major mode that allows editing several languages in the same buffer, mainly HTML, CSS, and Javascript.
(use-package web-mode
:defer t
:straight (:build t)
:hook html-mode
:hook (web-mode . prettier-js-mode)
:hook (web-mode . lsp-deferred)
:mode (("\\.phtml\\'" . web-mode)
("\\.tpl\\.php\\'" . web-mode)
("\\.twig\\'" . web-mode)
("\\.xml\\'" . web-mode)
("\\.html\\'" . web-mode)
("\\.htm\\'" . web-mode)
("\\.[gj]sp\\'" . web-mode)
("\\.as[cp]x?\\'" . web-mode)
("\\.eex\\'" . web-mode)
("\\.erb\\'" . web-mode)
("\\.mustache\\'" . web-mode)
("\\.handlebars\\'" . web-mode)
("\\.hbs\\'" . web-mode)
("\\.eco\\'" . web-mode)
("\\.ejs\\'" . web-mode)
("\\.svelte\\'" . web-mode)
("\\.ctp\\'" . web-mode)
("\\.djhtml\\'" . web-mode)
("\\.vue\\'" . web-mode))
:config
(csetq web-mode-markup-indent-offset 2
web-mode-code-indent-offset 2
web-mode-css-indent-offset 2
web-mode-style-padding 0
web-mode-script-padding 0)
:general
(phundrak/major-leader-key
:keymaps 'web-mode-map
:packages 'web-mode
"=" '(:ignore t :which-key "format")
"E" '(:ignore t :which-key "errors")
"El" #'web-mode-dom-errors-show
"gb" #'web-mode-element-beginning
"g" '(:ignore t :which-key "goto")
"gc" #'web-mode-element-child
"gp" #'web-mode-element-parent
"gs" #'web-mode-element-sibling-next
"h" '(:ignore t :which-key "dom")
"hp" #'web-mode-dom-xpath
"r" '(:ignore t :which-key "refactor")
"rc" #'web-mode-element-clone
"rd" #'web-mode-element-vanish
"rk" #'web-mode-element-kill
"rr" #'web-mode-element-rename
"rw" #'web-mode-element-wrap
"z" #'web-mode-fold-or-unfold)
(phundrak/major-leader-key
:keymaps 'web-mode-map
:packages '(lsp-mode web-mode)
"l" '(:keymap lsp-command-map :which-key "lsp")))
Auto-completion for emmet-mode
, html-mode
, and web-mode
.
(use-package company-web
:defer t
:straight (:build t)
:after (emmet-mode web-mode))
CSS
Let’s customize a bit the built-in CSS mode.
(use-package css-mode
:defer t
:straight (:type built-in)
:hook (css-mode . smartparens-mode)
:hook (css-mode . lsp-deferred)
:hook (scss-mode . prettier-js-mode)
:init
(put 'css-indent-offset 'safe-local-variable #'integerp)
:general
(phundrak/major-leader-key
:keymaps 'css-mode-map
:packages 'lsp-mode
"l" '(:keymap lsp-command-map :which-key "lsp"))
(phundrak/major-leader-key
:keymaps 'css-mode-map
:packages 'css-mode
"=" '(:ignore :wk "format")
"g" '(:ignore :wk "goto")))
SCSS is much nicer to use than pure CSS in my opinion, so let’s add a mode for that.
(use-package scss-mode
:straight (:build t)
:hook (scss-mode . smartparens-mode)
:hook (scss-mode . lsp-deferred)
:hook (scss-mode . prettier-js-mode)
:defer t
:mode "\\.scss\\'")
And let’s add some autocompletion for CSS.
(use-package counsel-css
:straight (:build t)
:defer t
:init
(cl-loop for (mode-map . mode-hook) in '((css-mode-map . css-mode-hook)
(scss-mode-map . scss-mode-hook))
do (add-hook mode-hook #'counsel-css-imenu-setup)
(phundrak/major-leader-key
:keymaps mode-map
"gh" #'counsel-css)))
For some reason, although it is built-in, less-css-mode
does not
activate when I open .less
files by default. Let’s fix that.
(use-package less-css-mode
:straight (:type built-in)
:defer t
:mode "\\.less\\'"
:hook (less-css-mode . smartparens-mode)
:hook (less-css-mode . lsp-deferred)
:hook (less-css-mode . prettier-js-mode)
:general
(phundrak/major-leader-key
:keymaps 'less-css-mode-map
:packages 'lsp-mode
"l" '(:keymap lsp-command-map :which-key "lsp")))
Javascript
javascript-mode
is meh at best, while rjsx-mode
(Real JSX) is much
better: it supports both Javascript and .jsx
files for React and
Next.JS.
(use-package rjsx-mode
:defer t
:straight (:build t)
:after compile
:mode "\\.[mc]?jsx?\\'"
:mode "\\.es6\\'"
:mode "\\.pac\\'"
:interpreter "node"
:hook (rjsx-mode . rainbow-delimiters-mode)
:hook (rjsx-mode . lsp-deferred)
:init
(add-to-list 'compilation-error-regexp-alist 'node)
(add-to-list 'compilation-error-regexp-alist-alist
'(node "^[[:blank:]]*at \\(.*(\\|\\)\\(.+?\\):\\([[:digit:]]+\\):\\([[:digit:]]+\\)"
2 3 4))
:general
(phundrak/major-leader-key
:keymaps 'rjsx-mode-map
:packages 'lsp-mode
"l" '(:keymap lsp-command-map :which-key "lsp"))
:config
(setq js-chain-indent t
js2-basic-offset 2
;; ignore shebangs
js2-skip-preprocessor-directives t
;; Flycheck handles this already
js2-mode-show-parse-errors nil
js2-mode-show-strict-warnings nil
;; conflicting with eslint, Flycheck already handles this
js2-strict-missing-semi-warning nil
js2-highlight-level 3
js2-idle-timer-delay 0.15))
js2-refactor
is an amazing tool for refactoring Javascript code. I
mean, look at this! And the video is only from 2013 and it still
receives some commits!
(use-package js2-refactor
:defer t
:straight (:build t)
:after (js2-mode rjsx-mode)
:hook (js2-mode . js2-refactor-mode)
:hook (rjsx-mode . js2-refactor-mode))
Which Emacser prefers the command line over Emacs itself? I don’t. Let’s interact with NPM through Emacs then.
(use-package npm-transient
:defer t
:straight (npm-transient :build t
:type git
:host github
:repo "Phundrak/npm-transient"))
;; :general
;; (phundrak/major-leader-key
;; :packages '(npm-transient rjsx-mode web-mode)
;; :keymaps '(rjsx-mode-map web-mode-map)
;; "n" #'npm-transient))
And finally, here is a formatter for Javascript.
(use-package prettier-js
:defer t
:straight (:build t)
:after (rjsx-mode web-mode typescript-mode)
:hook ((rjsx-mode typescript-mode) . prettier-js-mode)
:config
(setq prettier-js-args '("--single-quote" "--jsx-single-quote")))
Typescript
Typescript is a safer alternative to Javascript. Let’s install its major mode then.
(use-package typescript-mode
:defer t
:straight (:build t)
:hook (typescript-mode . rainbow-delimiters-mode)
:hook (typescript-tsx-mode . rainbow-delimiters-mode)
:hook (typescript-mode . lsp-deferred)
:hook (typescript-tsx-mode . lsp-deferred)
:hook (typescript-mode . prettier-js-mode)
:hook (typescript-tsx-mode . prettier-js-mode)
:commands typescript-tsx-mode
:after flycheck
:init
(add-to-list 'auto-mode-alist '("\\.tsx\\'" . typescript-tsx-mode))
:general
(phundrak/major-leader-key
:keymaps '(typescript-mode-map typescript-tsx-mode-map)
:packages 'lsp-mode
"l" '(:keymap lsp-command-map :which-key "lsp"))
(phundrak/major-leader-key
:packages 'typescript-mode
:keymaps '(typescript-mode-map typescript-tsx-mode-map)
"n" '(:keymap npm-mode-command-keymap :which-key "npm"))
:config
(with-eval-after-load 'flycheck
(flycheck-add-mode 'javascript-eslint 'web-mode)
(flycheck-add-mode 'javascript-eslint 'typescript-mode)
(flycheck-add-mode 'javascript-eslint 'typescript-tsx-mode)
(flycheck-add-mode 'typescript-tslint 'typescript-tsx-mode))
(when (fboundp 'web-mode)
(define-derived-mode typescript-tsx-mode web-mode "TypeScript-TSX"))
(autoload 'js2-line-break "js2-mode" nil t))
Tide enabled interactivity with Typescript.
(use-package tide
:defer t
:straight (:build t)
:hook (tide-mode . tide-hl-identifier-mode)
:config
(setq tide-completion-detailed t
tide-always-show-documentation t
tide-server-may-response-length 524288
tide-completion-setup-company-backend nil)
(advice-add #'tide-setup :after #'eldoc-mode)
:general
(phundrak/major-leader-key
:keymaps 'tide-mode-map
"R" #'tide-restart-server
"f" #'tide-format
"rrs" #'tide-rename-symbol
"roi" #'tide-organize-imports))
Zig
Zig is to C kind of what Rust is to C++: a modern replacement without sacrificing performance. It is much safer than C while providing interop with it. Plus, its standard library is pretty complete.
First, here is its major mode.
(use-package zig-mode
:defer t
:straight (:build t)
:after flycheck
:hook (zig-mode . lsp-deferred)
:config
;; This is from DoomEmacs
(flycheck-define-checker zig
"A zig syntax checker using the zig-fmt interpreter."
:command ("zig" "fmt" (eval (buffer-file-name)))
:error-patterns
((error line-start (file-name) ":" line ":" column ": error: " (mesage) line-end))
:modes zig-mode)
(add-to-list 'flycheck-checkers 'zig)
:general
(phundrak/major-leader-key
:keymaps 'zip-mode-map
:packages 'lsp-mode
"l" '(:keymap lsp-command-map :which-key "lsp"))
(phundrak/major-leader-key
:packages 'zig-mode
:keymaps 'zig-mode-map
"c" #'zig-compile
"f" #'zig-format-buffer
"r" #'zig-run
"t" #'zig-test-buffer))
For LSP to work, we need zls
to be installed. In my case, as I am on
ArchLinux, I can install it from the AUR, and my AUR helper is paru
.
paru --skipreview --noconfirm -S zls-bin 2>&1
Visual Configuration
Dashboard
(use-package dashboard
:straight (:build t)
:ensure t
:after all-the-icons
:config
(setq dashboard-banner-logo-title "Phundrak’s Vanilla Emacs"
dashboard-startup-banner 'logo
dashboard-center-content t
dashboard-show-shortcuts t
dashboard-set-navigator t
dashboard-set-heading-icons t
dashboard-set-file-icons t
initial-buffer-choice (lambda () (get-buffer "*dashboard*"))
dashboard-projects-switch-function 'counsel-projectile-switch-project-by-name)
(setq dashboard-navigator-buttons
`(((,(all-the-icons-faicon "language" :height 1.1 :v-adjust 0.0)
"Linguistics Website"
""
(lambda (&rest _) (browse-url "https://langue.phundrak.com")))
(,(all-the-icons-faicon "firefox" :height 1.1 :v-adjust 0.0)
"Config Website"
""
(lambda (&rest _) (browse-url "https://config.phundrak.com"))))
((,(all-the-icons-octicon "git-branch" :height 1.1 :v-adjust 0.0)
"Dotfiles Sources"
""
(lambda (&rest _) (browse-url "https://labs.phundrak.com/phundrak/dotfiles")))
("!" "Issues" "Show issues" (lambda (&rest _)
(browse-url "https://labs.phundrak.com/phundrak/dotfiles/issues"))
warning))
((,(all-the-icons-faicon "level-up" :height 1.1 :v-adjust 0.0)
"Update Packages"
""
(lambda (&rest _) (progn
(require 'straight)
(straight-pull-all)
(straight-rebuild-all)))))))
(setq dashboard-items '((recents . 15)
(agenda . 10)
(projects . 10)))
(dashboard-setup-startup-hook)
:init
(add-hook 'after-init-hook 'dashboard-refresh-buffer))
Fringe
It’s nice to know which lines were modified since the last commit in a file.
(use-package git-gutter-fringe
:straight (:build t)
:hook ((prog-mode . git-gutter-mode)
(org-mode . git-gutter-mode)
(markdown-mode . git-gutter-mode)
(latex-mode . git-gutter-mode)))
Icons? Did someone say icons?
YES! ALL OF THEM!
Ahem…
The package all-the-icons
allows us to use a wide variety of icons in
Emacs for various purposes, wherever we want, and THAT is GREAT! I’ll
(ab)use this feature in my config, be warned! NOTE: The first time a
configuration with all-the-icons
loads on a machine, the needed fonts
might not be available, so you’ll need to install them with the
command M-x all-the-icons-install-fonts
.
(use-package all-the-icons
:defer t
:straight t)
prettify-symbols-mode
is also a nifty feature of Emacs, and it is
built-in! With that, I can replace strings of my choice by another
character of my choice! First, let’s declare the general symbols that
will be used everywhere.
(defun prog-mode-set-symbols-alist ()
(setq prettify-symbols-alist '(("lambda" . ?λ)
("null" . ?∅)
("NULL" . ?∅)))
(prettify-symbols-mode 1))
(add-hook 'prog-mode-hook #'prog-mode-set-symbols-alist)
We can now take care of the language-specific symbols. First, let’s declare some symbols for the Lisp languages.
(setq-default lisp-prettify-symbols-alist '(("lambda" . ?λ)
("defun" . ?𝑓)
("defvar" . ?𝑣)
("defcustom" . ?𝑐)
("defconst" . ?𝐶)))
(defun lisp-mode-prettify ()
(setq prettify-symbols-alist lisp-prettify-symbols-alist)
(prettify-symbols-mode -1)
(prettify-symbols-mode 1))
(dolist (lang '(emacs-lisp lisp common-lisp scheme))
(add-hook (intern (format "%S-mode-hook" lang))
#'lisp-mode-prettify))
Finally, similar to how org-appear
behaves, let’s show the real string
of our symbols when the cursor is on it.
(setq prettify-symbols-unprettify-at-point t)
Ligatures
The font I’m using (see §#Basic-configuration-Visual-Configuration-Fontsxfkjel6184j0) supports ligatures, but Emacs in GUI mode does not. And of course, there’s a package for that.
(use-package ligature
:straight (ligature :type git
:host github
:repo "mickeynp/ligature.el"
:build t)
:config
(ligature-set-ligatures 't
'("www"))
;; Enable traditional ligature support in eww-mode, if the
;; `variable-pitch' face supports it
(ligature-set-ligatures '(eww-mode org-mode elfeed-show-mode)
'("ff" "fi" "ffi"))
;; Enable all Cascadia Code ligatures in programming modes
(ligature-set-ligatures 'prog-mode
'("|||>" "<|||" "<==>" "<!--" "####" "~~>" "***" "||=" "||>"
":::" "::=" "=:=" "===" "==>" "=!=" "=>>" "=<<" "=/=" "!=="
"!!." ">=>" ">>=" ">>>" ">>-" ">->" "->>" "-->" "---" "-<<"
"<~~" "<~>" "<*>" "<||" "<|>" "<$>" "<==" "<=>" "<=<" "<->"
"<--" "<-<" "<<=" "<<-" "<<<" "<+>" "</>" "###" "#_(" "..<"
"..." "+++" "/==" "///" "_|_" "www" "&&" "^=" "~~" "~@" "~="
"~>" "~-" "**" "*>" "*/" "||" "|}" "|]" "|=" "|>" "|-" "{|"
"[|" "]#" "::" ":=" ":>" ":<" "$>" "==" "=>" "!=" "!!" ">:"
">=" ">>" ">-" "-~" "-|" "->" "--" "-<" "<~" "<*" "<|" "<:"
"<$" "<=" "<>" "<-" "<<" "<+" "</" "#{" "#[" "#:" "#=" "#!"
"##" "#(" "#?" "#_" "%%" ".=" ".-" ".." ".?" "+>" "++" "?:"
"?=" "?." "??" ";;" "/*" "/=" "/>" "//" "__" "~~" "(*" "*)"
"\\\\" "://"))
(global-ligature-mode t))
Modeline
The DoomEmacs modeline looks nice in my opinion, let’s use it.
(use-package doom-modeline
:straight (:build t)
:defer t
:init (doom-modeline-mode 1)
:config
(csetq doom-modeline-height 15
doom-modeline-enable-word-count t
doom-modeline-continuous-word-count-modes '(markdown-mode gfm-mode org-mode)
doom-modeline-mu4e t
doom-modeline-env-version t)
(mu4e-alert-enable-mode-line-display))
Pixel-perfect alignment of Markdown and org-mode tables
Usually, I have no issue with the alignment of the tables I write in
org-mode and (more rarely) Markdown. However, there are occurences
where I’ll use a character that does not exactly respect my monospace
font, which messes with the alignment of the table (often when I do
linguistics stuff). A solution to this is the package valign
. A little
caveat though, as its name implies valign
helps with vertical
alignment. If some lines are too high, they won’t exactly fit. Unless?
Unless valign-fancy-bar
is set to t
.
For now, I disabled the hook with org-mode and markdown-mode because it slows down opening these files quite a lot. I’ll re-enable the hook once it is fixed.
(use-package valign
:defer t
:straight (:build t)
:after (org markdown-mode)
;; :hook ((org-mode markdown-mode) . valign-mode)
:custom ((valign-fancy-bar t)))
Secret mode
Sometimes, I want to hide the text displayed by Emacs but not lock
altogether my computer. In this case, secret-mode
comes in handy.
(use-package secret-mode
:defer t
:straight (secret-mode :build t
:type git
:host github
:repo "bkaestner/secret-mode.el"))
Solaire: Incandescent Emacs
A common issue when you have a lot of windows opened in Emacs is
sometimes there’s just too much. Is the first window source code? Is
the other one just an open email? Oh, let’s not forget the *Messages*
buffer open next to another source buffer.
Solaire-mode applies a subtle but useful tweak to your current color scheme: the background of programming buffers is slightly lighter than the background of other buffers. (Or is it other buffers that have a slightly darker background? I’m not sure.)
(use-package solaire-mode
:defer t
:straight (:build t)
:init (solaire-global-mode +1))
Theme
You may have noticed I use the Nord theme pretty much everywhere on my computer, why not Emacs? In my opinion, its aurora variant is nicer than the default Nord theme since it is richer in colors — just a personal preference.
(use-package doom-themes
:straight (:build t)
:defer t
:init (load-theme 'doom-nord-aurora t))
Rainbow Delimiters
This makes Lisp especially more readable, but it’s also nice to have for any language that has delimiters like brackets too.
(use-package rainbow-delimiters
:straight (:build t)
:defer t
:hook (prog-mode . rainbow-delimiters-mode))
Y’all want some more COLORS?
It is possible to make info buffers much more colorful (and imo easier to read) with this simple package:
(use-package info-colors
:straight (:build t)
:commands info-colors-fnontify-node
:hook (Info-selection . info-colors-fontify-node)
:hook (Info-mode . mixed-pitch-mode))
Misc
ArchWiki pages
A small package I’ve written allows the user to view ArchLinux pages either in Emacs or in an external web browser. I prefer the defaults.
(use-package archwiki
:defer t
:straight (archwiki :build t
:type git
:repo "https://labs.phundrak.com/phundrak/archwiki.el"))
avy
avy
is a really convenient way of jumping around and performing
actions on these selections, but I’ll need some configuration to make
it bépo-compatible.
(use-package avy
:defer t
:straight t
:config
(csetq avy-keys '(?a ?u ?i ?e ?c ?t ?s ?r ?n)
avy-dispatch-alist '((?x . avy-action-kill-move)
(?X . avy-action-kill-stay)
(?T . avy-action-teleport)
(?m . avy-action-mark)
(?C . avy-action-copy)
(?y . avy-action-yank)
(?Y . avy-action-yank-line)
(?I . avy-action-ispell)
(?z . avy-action-zap-to-char)))
(defun my/avy-goto-url ()
"Jump to url with avy."
(interactive)
(avy-jump "https?://"))
(defun my/avy-open-url ()
"Open url selected with avy."
(interactive)
(my/avy-goto-url)
(browse-url-at-point))
:general
(phundrak/evil
:pakages 'avy
"gc" #'evil-avy-goto-char-timer
"gl" #'evil-avy-goto-line)
(phundrak/leader-key
:packages 'avy
:infix "j"
"b" #'avy-pop-mark
"c" #'evil-avy-goto-char-timer
"l" #'avy-goto-line)
(phundrak/leader-key
:packages 'avy
:infix "A"
"c" '(:ignore t :which-key "copy")
"cl" #'avy-copy-line
"cr" #'avy-copy-region
"k" '(:ignore t :which-key "kill")
"kl" #'avy-kill-whole-line
"kL" #'avy-kill-ring-save-whole-line
"kr" #'avy-kill-region
"kR" #'avy-kill-ring-save-region
"m" '(:ignore t :which-key "move")
"ml" #'avy-move-line
"mr" #'avy-move-region
"mt" #'avy-transpose-lines-in-region
"n" #'avy-next
"p" #'avy-prev
"u" #'my/avy-goto-url
"U" #'my/avy-open-url)
(phundrak/major-leader-key
:packages '(avy org)
:keymaps 'org-mode-map
"A" '(:ignore t :which-key "avy")
"Ar" #'avy-org-refile-as-child
"Ah" #'avy-org-goto-heading-timer))
Calc
Let’s give calc-mode
some better defaults.
(setq calc-angle-mode 'rad
calc-symbolic-mode t)
Elcord
What’s the point of using Emacs if you can’t tell everyone?
(use-package elcord
:straight (:built t)
:defer t
:config
(csetq elcord-use-major-mode-as-main-icon t
elcord-refresh-rate 5
elcord-boring-buffers-regexp-list `("^ "
,(rx "*" (+ any) "*")
,(rx bol (or "Re: "
"Fwd: ")))))
ivy-quick-find-files.el
This package is a small utility package I’ve written in order to quickly find files across my filesystem.
(use-package ivy-quick-find-files
:defer t
:straight (ivy-quick-find-files :type git
:host github
:repo "phundrak/ivy-quick-find-files.el"
:build t)
:config
(setq ivy-quick-find-files-program 'fd
ivy-quick-find-files-dirs-and-exts '(("~/org" . "org")
("~/Documents/conlanging" . "org")
("~/Documents/university" . "org"))))
Keycast
In case I am sharing my screen with people and I want to show which functions are called on my keystrokes since I don’t exactly use standard keybindings.
(use-package keycast
:defer t
:straight (:build t)
:config
(define-minor-mode keycast-mode
"Show current command and its key binding in the mode line."
:global t
(if keycast-mode
(add-hook 'pre-command-hook 'keycast--update t)
(remove-hook 'pre-command-hook 'keycast--update)))
(add-to-list 'global-mode-string '("" mode-line-keycast " ")))
Keyfreq
Keyfreq is a package that records all the commands I call from Emacs and builds a heatmap out of it.
(use-package keyfreq
:straight (:build t)
:init
(keyfreq-mode 1)
(keyfreq-autosave-mode 1)
:config
(setq keyfreq-excluded-commands '(self-insert-command org-self-insert-command
evil-previous-visual-line evil-next-visual-line
ivy-next-line evil-backward-char evil-forward-char
evil-next-line evil-previous-line evil-normal-state
text-scale-pinch)))
Mastodon
(use-package mastodon
:defer t
:ensure t
:straight (mastodon :type git
:host nil
:repo "https://codeberg.org/rougier/mastodon.el"
:branch "alt-timeline")
:config
(setq mastodon-instance-url "https://fosstodon.org"
mastodon-active-user "phundrak")
(require 'mastodon-alt-tl)
(mastodon-alt-tl-activate))
Mediawiki
(use-package mediawiki
:defer t
:straight (:build t)
:custom
(mediawiki-site-alist '(("PhundrakWiki" ; Title
"https://wiki.phundrak.com/" ; URL
"phundrak" ; username
nil ; password
nil ; LDAP
"Main Page")))) ; Default page
SICP
Who would get interested in Emacs and not want to read the SICP? Moreover, inside Emacs?
(use-package sicp
:straight (:build t)
:defer t)
Winum
Winum allows Emacs to associate windows with a specific number and navigate through these windows by directly refering to their associated number! This allows for faster window configuration than just going to the frame above, then left, left, and up.
(use-package winum
:straight (:build t)
:init (winum-mode))
Ytplay
ytplay
is a small package I’ve written with which you can choose at
which resolution to play a YouTube video in an external video player.
(use-package ytplay
:defer t
:straight (ytplay :build t
:type git
:repo "https://labs.phundrak.com/phundrak/ytplay.el"))
Keybindings
Undefining some stuff to make keybind prefixes work correctly.
(general-define-key
:keymaps 'global-map
"<mouse-2>" nil
"<mouse-3>" nil)
(phundrak/evil
:packages '(counsel)
"U" #'evil-redo
"C-a" #'beginning-of-line
"C-e" #'end-of-line
"C-y" #'yank
"M-y" #'counsel-yank-pop)
(phundrak/leader-key
"SPC" '(counsel-M-x :wk "M-x")
"'" #'shell-pop
<<general-keybindings-gen(table=keybinds-tabs, prefix="TAB ")>>
<<general-keybindings-gen(table=keybinds-apps, prefix="a")>>
<<general-keybindings-gen(table=keybinds-apps-shell, prefix="as")>>
<<general-keybindings-gen(table=treemacs-keybinds, prefix="at")>>
<<general-keybindings-gen(table=keybinds-buffers, prefix="b")>>
"c" '(:ignore t :wk "code")
"cl" #'evilnc-comment-or-uncomment-lines
<<keybindings-flycheck>>
<<general-keybindings-gen(table=keybinds-files, prefix="f")>>
<<keybinds-specific-files>>
<<general-keybindings-gen(table=keybinds-help, prefix="h")>>
"i" '(:ignore t :wk "insert")
"iu" #'counsel-unicode-char
<<general-keybindings-gen(table=keybinds-jump, prefix="j")>>
<<general-keybindings-gen(table=keybinds-toggle, prefix="t ")>>
<<general-keybindings-gen(table=keybinds-text, prefix="T")>>
<<general-keybindings-gen(table=keybinds-windows, prefix="w")>>
<<general-keybindings-gen(table=keybinds-quit, prefix="q")>>
"u" #'universal-argument
"U" #'undo-tree-visualize)
Apps
Here are my apps keybinds. Each one of them is prefixed by a
.
Key | Function | Description |
---|---|---|
apps | ||
c | calc | |
d | docker | |
E | elfeed | |
e | ||
ec | mu4e-compose-new | |
em | mu4e | |
k | keycast-mode | |
K | keycast-log-mode | |
T | tetris | |
w | wttrin | |
C | calendar |
I also have two main shell-related functions, prefixed with as
.
Key | Function | Description |
---|---|---|
shells | ||
e | eshell-new | |
v | vterm | |
V | multi-vterm |
Buffers
My buffer-related keybinds are all prefixed by b
.
Key | Function | Description |
---|---|---|
buffers | ||
b | bufler-switch-buffer | |
B | bury-buffer | |
c | clone-indirect-buffer | |
C | clone-indirect-buffer-other-window | |
l | bufler | |
d | kill-this-buffer | |
D | kill-buffer | |
h | dashboard-refresh-buffer | |
m | switch-to-messages-buffer | |
n | next-buffer | |
p | previous-buffer | |
r | counsel-buffer-or-recentf | |
s | switch-to-scratch-buffer |
Errors
(defhydra hydra-flycheck
(:pre (flycheck-list-errors)
:post (quit-windows-on "*Flycheck errors*")
:hint nil)
("f" flycheck-error-list-set-filter "Filter")
("t" flycheck-next-error "Next")
("s" flycheck-previous-error "Previous")
("gg" flycheck-first-error "First")
("G" (progn (goto-char (point-max)) (flycheck-previous-error)) "Last")
("q" nil))
"e" '(:ignore t :which-key "errors")
"e." '(hydra-flycheck/body :wk "hydra")
"el" #'counsel-flycheck
"ee" '(:keymap flycheck-command-map :wk "flycheck")
"ef" '(:keymap flyspell-mode-map :wk "flyspell")
"eF" #'flyspell-hydra/body
Files
My keybinds for file manipulation are prefixed by f
.
Key | Function | Description |
---|---|---|
files | ||
f | counsel-find-file | |
F | ivy-quick-find-files | |
h | hexl-find-file | |
r | counsel-recentf | |
s | save-buffer |
I also have some keybinds dedicated to opening specific files.
"fc" '((lambda ()
(interactive)
(find-file "~/org/config/emacs.org"))
:wk "emacs.org")
"fi" '((lambda ()
(interactive)
(find-file (concat user-emacs-directory "init.el")))
:which-key "init.el")
"fR" '((lambda ()
(interactive)
(counsel-find-file ""
(concat user-emacs-directory
(file-name-as-directory "straight")
(file-name-as-directory "repos"))))
:which-key "straight package")
"fS" '((lambda ()
(interactive)
(find-file "~/org/config/stumpwm.org"))
:which-key "stumpwm.org")
Help
My keybinds for help are prefixed by h
.
Key | Function | Description |
---|---|---|
help | ||
k | which-key-show-top-level | |
i | info | |
I | info-display-manual | |
d | describe | |
dc | describe-char | |
dC | helpful-command | |
df | helpful-callable | |
di | describe-input-method | |
dk | helpful-key | |
dm | helpful-macro | |
dM | helpful-mode | |
dp | describe-package | |
ds | helpful-symbol | |
dv | helpful-variable |
Jump
My keybinds for jumping around are prefixed by j
.
Key | Function | Description |
---|---|---|
jump | ||
f | counsel-file-jump | |
d | dirvish-dwim | |
D | dired-jump-other-window |
Project
My keybinds for my projects are prefixed by p
.
Key | Function | Description |
---|---|---|
project | ||
! | projectile-run-shell-command-in-root | |
& | projectile-run-async-shell-command-in-root | |
b | counsel-projectile-switch-to-buffer | |
c | counsel-projectile | |
d | counsel-projectile-find-dir | |
e | projectile-edit-dir-locals | |
f | counsel-projectile-find-file | |
g | projectile-find-tag | |
k | project-kill-buffers | |
p | counsel-projectile-switch-project | |
t | ivy-magit-todos | |
v | projectile-vc |
Tabs
Emacs has native tabs available, which can be interesting when working on multiple projects at once between which we may want to switch. Tabs allow the user not to have multiple frames while keeping the advantages of having multiple frames.
My keybinds are prefixed by SPC TAB
.
Key | Function | Description |
---|---|---|
tabs | ||
TAB | tab-switch | |
» | tab-next | |
« | tab-previous | |
c | tab-new | |
C | tab-new-to | |
d | tab-close | |
n | tab-next | |
p | tab-previous | |
r | tab-rename |
Text
The prefix here is T
.
Key | Function | Description |
---|---|---|
text | ||
e | string-edit-at-point | |
u | downcase-region | |
U | upcase-region | |
z | hydra-zoom/body |
Toggles
My toggle keybinds are prefixed by t
.
Key | Function | Description |
---|---|---|
toggles | ||
TAB | tab-bar-mode | |
t | my/modify-frame-alpha-background/body | |
T | counsel-load-theme | |
d | debug | |
de | toggle-debug-on-error | |
dq | toggle-debug-on-quit | |
i | input method | |
it | toggle-input-method | |
is | set-input-method |
Windows
A couple of keybinds are hidden from which-key, otherwise there’s not
much to say. The prefix here is w
.
Key | Function | Description |
---|---|---|
windows | ||
c | evil-window-left | |
t | evil-window-down | |
s | evil-window-up | |
r | evil-window-right | |
. | windows-adjust-size/body | |
- | split-window-below-and-focus | |
/ | split-window-right-and-focus | |
$ | winum-select-window-by-number | |
0 | winum-select-window-0-or-10 | none |
1 | winum-select-window-1 | none |
2 | winum-select-window-2 | none |
3 | winum-select-window-3 | none |
4 | winum-select-window-4 | none |
5 | winum-select-window-5 | none |
6 | winum-select-window-6 | none |
7 | winum-select-window-7 | none |
8 | winum-select-window-8 | none |
9 | winum-select-window-9 | none |
b | kill-buffer-and-delete-window | |
d | delete-window | |
o | other-window | |
D | delete-other-windows | |
w | writeroom | |
w. | writeroom-buffer-width/body | |
ww | writeroom-mode |
Quit
Why would I ever use any of these keybinds? They are prefixed with q
.
Key | Function | Description |
---|---|---|
quit | ||
f | delete-frame | |
q | save-buffers-kill-terminal | |
Q | kill-emacs |