Welcome to my Emacs configuration file! I use Emacs often, and have for quite some time now. As a consequence, this config has gotten large and fit to my particular preferences. I don’t expect (or recommend) that you use it in its entirety, but I’ve decided to share it in the hope that you can skim it and extract a few good ideas or code-snippets to improve your own config. Good luck!
If you do use large parts of this config file, make sure to peruse the
js:custom
customisation group and use the menus to change the settings. You
don’t want to start sending emails with my name in the From line! If you’re new
to Emacs, move your cursor inside the following source-block and hit C-c C-c
(ie: press Control + C
, twice).
(customize-group 'js:custom)
After using Vim as my daily text editor for over 15 years, these days I exclusively use Emacs. I still feel that Vim is a better text editor, but Emacs is a “text editor” in the same way that aircraft carriers are “vehicles.” Emacs has a way of slowly taking over the responsibilities of every other app on your machine. As Emacs continues to take over my life, I’m sure this config file will grow, but at this time, this file configures Emacs to be a:
- Lightweight IDE for many different programming languages.
- Email client.
- Note-taking system.
- Static website generator.
- Calendar.
- TODO list / agenda manager.
- git client.
- Slideshow editor.
- File manager.
- Spreadsheet editor.
- UML diagram editor.
- Personal knowledge base.
- Journaling tool.
If you want to use Emacs for any of these activities, you may find something interesting in this config file.
For the most part, you can ignore the sections of this config that cover topics
that don’t concern you. You don’t need to know what magit
is, if you don’t
plan to use Emacs for git
. However, there are a few concepts that permeate
this config.
I’m sure by now, all my chatter has tipped you off that this isn’t a typical
config file. Emacs is typically configured with an emacs-lisp (.el
) file, but
this file is instead, an org-mode (.org
) file. org-mode is a markup language
(like Markdown but more powerful) that lets us write this config file in the
literate programming style.
Emacs—especially when including the constellation of available packages, is
massive. A year after you’ve added something, you’re not going to remember
exactly how it works, or what you did X
instead of Y
. By using org-mode, we
can split this config into logical sections and make as many notes as are
needed.
The Emacs package, use-package, provides a tidy and convenient function for configuring third-party packages, and this config uses dozens of those.
The Emacs package, straight.el, provides a package manager that integrates with use-package. If you call use-package on a package that isn’t installed, straight.el will download and build it for you automatically.
straight.el comes with a community-maintained list of “recipes” for the most popular packages, so it’s usually invisible. If you want to use a private/rare package, you’ll need to tell straight how to download it.
To use this .org
file as your Emacs config, you need to setup your Emacs Init
File to bootstraps org-mode
and use babel
to compile it into emacs-lisp. The
example Init File, shown below, will do this for you. The version I’m using
these days (see init.el) is a bit more complex, but this simple version is
more suitable in most cases.
This example Emacs Init File ensures that straight.el, use-package, and org-mode
are installed. It then compiles this file (README.org
) into emacs-lisp code,
and loads it into Emacs. At this point, you’re good to go!
;; 1. Load straight.el: Package manager.
;; (https://github.com/raxod502/straight.el)
(setq straight-use-package-by-default t)
(defvar bootstrap-version)
(let ((bootstrap-file
(locate-user-emacs-file "straight/repos/straight.el/bootstrap.el"))
(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))
;; 2. Load use-package: Package configuration manager.
(straight-use-package 'use-package)
;; 3. Load org-mode: To compile our config file.
(use-package org :ensure org-plus-contrib)
;; 4. Compile ("tangle") and load the org-mode config file.
(org-babel-load-file (locate-user-emacs-file "README.org"))
This config file pulls in many third-party packages that require additional programs to be installed. For instance, Emacs’ git UI (magit) needs git to be installed (duh!) and the org-plantuml package needs plantuml (duh!!).
If you happen to use NixOS or the Nix package manager, you can use the Nix Flake that comes with this project. This flake will build a version of Emacs that comes pre-installed with all the necessary dependencies.
If you use a different package manager, here is a (probably out-of-date) list of third-party dependencies:
- all-the-icons
- Several nice icon-fonts.
- Cabal
- A Haskell build tool.
- deno
- For LSP’s JavaScript support.
- eslint
- For Flycheck’s JavaScript support.
- exiv2
- Extracts metadata from image files.
- git
- For both straight.el and magit.
- haskell-language-server
- For LSP’s Haskell support.
- hlint
- For Flycheck’s Haskell support.
- ImageMagick
- Used to manipulate images when generating static websites.
- isync
- Email IMAP client.
- jsonlint
- For Flycheck’s JSON support.
- msmtp
- Email SMTP client.
- mu
- Email indexer (See mu4e).
- MultiMarkdown
- For markdown-mode.
- nix-linter
- For Flycheck’s Nix support.
- PlantUML
- A UML diagram generator. Sub-dependencies: Java and Graphviz.
- postcss
- For Flycheck’s CSS and SCSS support.
- proselint
- For Flycheck’s plaintext support.
- rnix-lsp
- For LSP’s Nix support.
- rubocop
- For Flycheck’s ruby support.
- rubocop-rails
- For Flycheck’s Ruby on Rails support.
- semistandard
- For Flycheck’s JavaScript support (a second one).
- Solargraph
- For LSP’s ruby support.
- stylelint
- For Flycheck’s CSS support (a second one).
- TeX Live
- So org-mode can export LaTeX files.
- textlint
- For Flycheck’s plaintext support (a second one).
- vscode-css-languageserver-bin
- For LSP’s CSS support.
- vscode-html-languageserver-bin
- For LSP’s HTML support.
- xdg-utils
- Allows
dired
to open files with the correct program.
When you weigh Emacs down with dozens of packages, it can take 5-6 seconds to start up. This is far, far too long. Fortunately, we can get away with just starting Emacs a single time, by running it as a systemd user service.
Once you have the Emacs service running, you can connect to it by running
emacsclient -c
. If you don’t want a GUI window, and want to edit a file on the
console, you can instead use emacsclient -c --tty
. It should open instantly. I
find it handy to create shell aliases for these two commands:
alias e="emacsclient -c --tty" # Open Emacs on the terminal.
alias eg="emacsclient -c" # Open Emacs in a GUI.
Path: ~/.config/systemd/user/emacs.service
[Unit]
Description=Emacs text editor
Documentation=info:emacs man:emacs(1) https://gnu.org/software/emacs/
X-RestartIfChanged=false
[Service]
ExecStart=bash -l -c "emacs --fg-daemon"
Restart=on-failure
SuccessExitStatus=15
Type=notify
[Install]
WantedBy=default.target
With this file in place, you can enable the service with:
systemctl --user daemon-reload
systemctl --user --now enable emacs
After running Emacs as a service for a few months, I noticed a problem. If you simultaneously connect to the daemon with both GUI and TTY clients, things can start to go a bit haywire—double-so if you’re connected over SSH.
One common problem was windows appearing in the wrong client. I might open the minibuffer in the GUI client, but it would actually appear on the TTY client. Or, I’d open a new file in the TTY client, but it would instead appear in the GUI window. Confusing!
A good solution I found for this was to run a second Emacs service, only for the TTY clients. It means you can’t share windows between the TTY and GUI clients, but I never want to do that anyway.
The systemd user service for the TTY-only Emacs daemon is the same as the one above, with the exception of one line. We give this daemon’s socket a different name, to differentiate it, and tell Emacs to start without GUI support.
ExecStart=bash -l -c "emacs --fg-daemon=tty --no-window-system"
Once this system service is installed and running, we can connect to it, by specifying its socket name:
emacsclient -c --tty -s 'tty'
We can update our shell aliases, and we’re off to the races.
alias e="emacsclient -c --tty -s 'tty'"
alias eg="emacsclient -c"
One-off or host-specific settings usually wind up in custom.el
.
Emacs provides a settings-management feature known as “Customisations.” While
most customisation comes from this config file, Emacs can automatically maintain
a list of “overrides” that supersede theme. These overrides are stored in an
external emacs-lisp
file which Emacs automatically updates (see (customize)
).
From what I read online, a lot of people disable this feature, but I find it be valuable. I use Emacs on a few different machines, no two of which are exactly the same. Having a method to implement minor, host-specific tweaks is handy! Moreover, it keeps this file from being clutter with host-specific edge-cases.
(setq custom-file (locate-user-emacs-file "custom.el"))
(when (file-readable-p custom-file) (load custom-file))
The long lost Emacs string manipulation library.
s.el provides useful string manipulation functions, used in this config.
(use-package s)
f.el is a modern API for working with files and directories in Emacs.
(use-package f)
If you have features that rely on apps being installed, it can be handy to know
if they’re available on the $PATH
.
(defun js:path:find-exe (name)
"Return the absolute path to NAME in `$PATH', or `nil'."
(locate-file name exec-path exec-suffixes 'executable))
From How can I find the path to an executable with Emacs Lisp?
(defmacro js:macro:call-path-exe (name &rest args)
"Call NAME with ARGS command-line arguments, if NAME is on
`$PATH', otherwise return `nil'."
`(let ((path (js:path:find-exe ,name)))
(when path ,(append `(start-process ,name nil path) args))))
(defmacro js:macro:call-path-exe+ (name &rest args)
"Call NAME with ARGS command-line arguments, if NAME is on
`$PATH', otherwise raise an `error'."
`(let ((path (js:path:find-exe ,name)))
(if path ,(append `(start-process ,name nil path) args)
(error ,(format "'%s' is not in $PATH" name)))))
These common directories may be useful. See XDG Base Directory Specification.
(defvar xdg/cache-home (or (getenv "XDG_CACHE_HOME") "~/.cache")
"The value of environmental variable, $XDG_CACHE_HOME.")
(defvar xdg/config-home (or (getenv "XDG_CONFIG_HOME") "~/.config")
"The value of environmental variable, $XDG_CONFIG_HOME.")
(defvar xdg/data-home (or (getenv "XDG_DATA_HOME") "~/.local/share")
"The value of environmental variable, $XDG_DATA_HOME.")
(defvar xdg/state-home (or (getenv "XDG_STATE_HOME") "~/.local/state")
"The value of environmental variable, $XDG_STATE_HOME.")
(defvar xdg/runtime-dir (getenv "XDG_RUNTIME_DIR")
"The value of environmental variable, $XDG_RUNTIME_DIR.")
To help you avoid losing unsaved changes, Emacs will create backup files as you
edit. This is great, but it normally dumps these files right beside the file
you’re editing, littering up the filesystem. Instead, lets have Emacs save them
all in $XDG_CACHE_HOME/emacs/
.
Note that Emacs doesn’t backup files already under version control (like git),
unless you (setq vc-make-backup-files t)
.
(let ((cache-dir (expand-file-name "emacs/backup" xdg/cache-home)))
(make-directory cache-dir t)
(customize-set-variable 'backup-directory-alist `(("." . ,cache-dir)))
(custom-set-variables
'(delete-old-versions t) ; Automatically delete excess backups.
'(kept-new-versions 20)
'(kept-old-versions 10))
'(version-control t)) ; Use version numbers on backups.
Emacs’ default method of making backups is to move the existing file into the
backup folder, then create a new local file. If you’re using inotify-wait
, or
any tools that trigger an action when you save your files, this method will
trick those tools into thinking you’ve updated your file every time it makes a
backup.
(customize-set-variable 'backup-by-copying t)
There are some types of configuration that we don’t necessary want to hard-code in this config file. For instance: if you want to use this config on multiple computers and want to use different font sizes, or you want to share your config with friends and don’t want them to accidentally copy/paste your email address into their email settings.
For these types of things, we can define personal customisations. Like other
customisations, these settings will be automatically stored in custom.el
: a
.gitignore
file. Feel safe to run (customize-group 'js:custom)
and enter
whatever data you like.
(defgroup js:custom nil
"Customizations defined in the README.org config file."
:tag "README.org customizations"
:link `(file-link ,(locate-user-emacs-file "README.org"))
:group 'emacs)
I usually choose a particular “programming font” to serve as my default Emacs font. Fortunately, we can configure Emacs to use fallback fonts for ranges of unicode charpoints.
(defgroup js:fonts nil
"Fonts"
:tag "Fonts"
:group 'js:custom)
(defcustom js:fonts:han "Noto Sans CJK"
"A font to render Han characters."
:tag "Han Font"
:group 'js:fonts
:type 'string)
(defun js:fonts:set-fallbacks:han (&optional frame)
(set-fontset-font "fontset-default" 'han
(font-spec :name js:fonts:han) frame))
Emoji characters seem to be spread around, and not every font supports every icon. For emoji, we’ll specify an particular emoji font to use for particular code points.
(defcustom js:fonts:emoji "Noto Color Emoji"
"A font to render emoji characters."
:tag "Emoji Font"
:group 'js:fonts
:type 'string)
(defcustom js:fonts:emoji-charpoints
'(#x203c #x2049 #x20e3 #x2139 (#x21a9 . #x21aa) (#x231a . #x231b) #x2328
#x23cf (#x23e9 . #x23f3) (#x23f8 . #x23fa) #x24c2 (#x25fb . #x25fe)
(#x2600 . #x2604) #x260e #x2611 (#x2614 . #x2615) #x2618 #x261d #x2620
(#x2622 . #x2623) #x2626 #x262a (#x262e . #x262f) (#x2638 . #x263a) #x2640
#x2642 (#x2648 . #x2653) (#x265f . #x2660) #x2663 (#x2665 . #x2666) #x2668
#x267b (#x267e . #x267f) (#x2692 . #x2697) #x2699 (#x269b . #x269c) #x26a7
(#x26aa . #x26ab) (#x26b0 . #x26b1) (#x26bd . #x26be) (#x26c4 . #x26c5)
#x26c8 (#x26ce . #x26cf) #x26d1 (#x26d3 . #x26d4) (#x26e9 . #x26ea)
(#x26f0 . #x26f5) (#x26f7 . #x26fa) #x26fd #x2702 #x2705 (#x2708 . #x270d)
#x270f #x2712 #x2714 #x2716 #x271d #x2721 #x2728 (#x2733 . #x2734) #x2744
#x2747 #x274c #x274e (#x2753 . #x2755) #x2757 (#x2763 . #x2764)
(#x2795 . #x2797) #x27a1 #x27b0 #x27bf (#x2934 . #x2935) (#x2b05 . #x2b07)
(#x2b1b . #x2b1c) #x2b50 #x2b55 #x3030 #x303d #x3297 #x3299
(#x1f000 . #xff000))
"A fontface to render Emoji characters."
:tag "Emoji font charpoints"
:group 'js:fonts
:type '(repeat (radio (integer :tag "Codepoint")
(cons :tag "Range"
(integer :tag "First")
(integer :tag "Last")))))
(defun js:fonts:set-fallbacks:emoji (&optional frame)
(let ((font (font-spec :name js:fonts:emoji)))
(dolist (chars js:fonts:emoji-charpoints)
(set-fontset-font "fontset-default" chars font frame))))
We can use the fontconfig
package to figure out what charpoints are supported
by a font. Here’s a useful shell script.
#! /usr/bin/env -S nix shell
#! nix-shell -i bash -p fontconfig
# General font info.
fc-match -v --format='%{file}\n' "$1"
# Print each codepoint.
for range in $(fc-match --format='%{charset}\n' "$1"); do
for n in $(seq "0x${range%-*}" "0x${range#*-}"); do
n_hex=$(printf "%04x" "$n")
# using \U for 5-hex-digits
printf "%-5s\U$n_hex\t " "$n_hex"
count=$((count + 1))
if [ $((count % 10)) = 0 ]; then
printf "\n"
fi
done
done
printf "\n"
# Print a compact list of codepoint ranges.
fc-query --format='%{charset}\n' "$(fc-match --format='%{file}\n' "$1")"
For some reason we need to set the fallback fonts for each new window that opens. See wasamasa/dotemacs: Fix the display of Emoji.
(dolist (fn '(js:fonts:set-fallbacks:han
js:fonts:set-fallbacks:emoji))
(add-hook 'after-make-frame-functions fn)
(funcall fn))
This is a custom dark-mode theme, somewhat based on Solarized. It’s not very good.
(load-theme 'sangster-09 t)
This key is normally set to transpose-chars
, which I rarely ever use. C-t is
centrally located in Colemak, so I’d rather use it as a prefix key. Throughout
this config, I’ve added my most-used functions to this keymap.
(bind-keys :map global-map :prefix-map sangster-map :prefix "C-t")
(bind-key "r" #'revert-buffer sangster-map)
C-z
acts similarly to how it does in a terminal emulator, suspending the
editor. Personally, I find this annoying. Disabled!
(global-unset-key (kbd "C-z"))
After using Emacs for a few years, the splash page doesn’t have much utility. Skip it.
(custom-set-variables
'(inhibit-splash-screen t)
'(inhibit-startup-screen t)
'(initial-scratch-message ""))
A tree layout file explorer for Emacs. Homepage.
(use-package treemacs
:defer t
:init
(with-eval-after-load 'winum
(define-key winum-keymap (kbd "M-0") #'treemacs-select-window))
(add-hook 'treemacs-select-functions #'js:treemacs:expand-when-first-used)
(add-hook 'treemacs-switch-workspace-hook #'js:treemacs:expand-when-first-used)
:config
;; The default width and height of the icons is 22 pixels. If you are
;; using a Hi-DPI display, uncomment this to double the icon size.
;;(treemacs-resize-icons 44)
:bind
(:map global-map
("C-x t 1" . treemacs-delete-other-windows)
("C-x t B" . treemacs-bookmark)
("C-x t c" . js:treemacs:cwd)
("C-x t C-t" . treemacs-find-file)
("C-x t C-w" . treemacs-edit-workspaces)
("C-x t M-t" . treemacs-find-tag)
("C-x t t" . treemacs)
("C-x t w" . treemacs-switch-workspace)
("M-0" . treemacs-select-window)))
(use-package treemacs-projectile
:after (treemacs projectile)
:ensure t)
(use-package treemacs-icons-dired
:after (treemacs dired)
:config (treemacs-icons-dired-mode))
(use-package treemacs-magit
:after (treemacs magit)
:ensure t)
;; TODO: This is buggy and sometimes permanently deletes other workspaces!
(defun js:treemacs:cwd ()
"Like `treemacs-do-switch-workspace', but follows the current
file. If the current file doesn't exist in any workspace, a
temporary workspace will be created."
(interactive)
(let ((current (treemacs-find-workspace-by-path (buffer-file-name)))
(buf (current-buffer)))
(if current
(treemacs-do-switch-workspace current)
(progn (treemacs-do-remove-workspace "treemacs-cwd")
(let ((treemacs-current-workspace
(cadr (treemacs-do-create-workspace "treemacs-cwd"))))
(treemacs--invalidate-buffer-project-cache)
(treemacs-do-add-project-to-workspace "/" "treemacs-cwd")
(treemacs-display-current-project-exclusively)))
(switch-to-buffer-other-window buf))
(treemacs--follow)))
;; TODO: XMonad-like workspaces. This might be good.
;; See https://github.com/Bad-ptr/persp-mode.el
;; (use-package treemacs-persp ;;treemacs-perspective if you use perspective.el vs. persp-mode
;; :after (treemacs persp-mode) ;;or perspective vs. persp-mode
;; :ensure t
;; :config (treemacs-set-scope-type 'Perspectives))
This hooks configures Treemacs to automatically expand workspaces when they’re
first selected. It’s used in the (use-package treemacs :init)
above.
GitHub: Alexander-Miller/treemacs - issue #740
(defun js:treemacs:expand-when-first-used (&optional visibility)
(when (or (null visibility) (eq visibility 'none))
(treemacs-do-for-button-state
:on-root-node-closed (treemacs-toggle-node)
:no-error t)))
I frequently have several Emacs windows open at once. This function configures
Emacs to allow you to use shift+arrowkey
to move between windows (in addition
to C-x o
).
Note that this conflicts with some org-mode keys. See *Shifting between windows.
(windmove-default-keybindings)
When there are two windows,
ace-window
will callother-window
. If there are more, each window will have the first character of its window label highlighted at the upper left of the window. GitHub: abo-abo/ace-window
(use-package ace-window
:init (setq aw-ignore-current t
aw-scope 'frame)
:bind (:map sangster-map
("o" . ace-window)
("O" . ace-swap-window)))
(set-face-attribute 'default nil :height 130) ; 13 pt.
(set-frame-parameter (selected-frame) 'alpha 80)
(add-to-list 'default-frame-alist '(alpha . 80))
Turn off the Emacs toolbar. The one with icons on it, not the “File, Edit, …” one.
(tool-bar-mode -1)
The minibuffer, at the bottom of the window, has a tiny and pointless scrollbar. Remove it.
;; TODO: Does this need to added to `after-make-frame-functions' hook?
(set-window-scroll-bars (minibuffer-window) nil nil)
Allow the mouse to be used in terminals that support it.
(xterm-mouse-mode 1)
Turn on line numbers in prog-mode and org-mode.
(dolist (mode '(org-mode-hook
prog-mode-hook))
(add-hook mode (lambda () (display-line-numbers-mode 1))))
(column-number-mode t)
The “fill column” acts as a text document’s right margin. It affects code formatters and where Emacs will automatically wrap text, if configured to do so.
80 columns is the traditional default, and allows us to show multiple files side-by-side on large displays. Nice!
(customize-set-variable 'fill-column 80)
Show a thin line where the fill column is.
(global-display-fill-column-indicator-mode)
Helm is an Emacs framework for incremental completions and narrowing selections. It helps to rapidly complete file names, buffer names, or any other Emacs interactions requiring selecting an item from a list of possible choices. Homepage.
(use-package helm
:bind (("M-x" . helm-M-x)
("C-x C-f" . helm-find-files)
("C-x C-b" . helm-buffers-list))
:delight
:custom
(helm-buffer-max-length 40) ; Default truncates filenames too short.
:config
(helm-mode 1))
(use-package helm-org-rifle)
(use-package helm-projectile
:bind ("C-c f" . helm-projectile)
:config
(helm-projectile-on))
(use-package helm-rg
:bind ("C-c r" . helm-projectile-rg))
(fset 'yes-or-no-p 'y-or-n-p)
By default Emacs will scroll by half a screen-height when scrolling past the bottom of the screen. This is very jarring and makes it difficult to keep your place. These settings make scrolling emulate vim’s behaviour: It scrolls 1 line at a time, but leaves a margin of a certain number of lines (8 in this case).
(custom-set-variables
'(scroll-margin 8)
'(scroll-step 1)
'(scroll-conservatively 10000)
'(scroll-preserve-screen-position 1))
Helpful is a package that adds a lot more detail to Emacs built-in help system.
(use-package helpful
:bind
(("C-h f" . helpful-callable)
("C-h v" . helpful-variable)
("C-h k" . helpful-key)
("C-h F" . helpful-function)
("C-h C" . helpful-command)))
emacs-which-key is a minor mode that pops up a list of possible key bindings when you partially enter a hotkey. It’s really handy for navigating Emacs’ vast constellation of hotkeys.
(use-package which-key
:config
(which-key-mode)
(which-key-setup-side-window-right-bottom)
(which-key-add-key-based-replacements
"C-c C-r d" "Review: daily"
"C-c C-r w" "Review: weekly"
"C-c C-r m" "Review: monthly"
"C-c C-r y" "Review: yearly")
:delight)
iedit is a minor mode that allows you to edit every instance of some text in the current buffer, in place.
(use-package iedit
:bind (:map sangster-map (";" . iedit-mode)))
Enables you to customise the mode names displayed in the mode line.
This package is used via use-package
.
(use-package delight)
;; Emacs built-in modes.
(delight '((overwrite-mode " OVERWRITE!" t)
(emacs-lisp-mode "Elisp" :major)
(flyspell-mode nil t)
(auto-fill-function " AF" t)))
My current theme, sangster-09-theme.el, is set to use JetBrains Mono as its
default font. This font supports a great number of ligatures. Emacs implements
ligature rendering with auto-composition-mode
, which (I believe) is enabled by
default. We only need to inform it what ligatures to render.
This ligature mapping is from Andrey Listopadov’s blog article ”Programming ligatures in Emacs.”
(let ((ligatures
`((?- . ,(regexp-opt '("-|" "-~" "---" "-<<" "-<" "--" "->" "->>" "-->")))
(?/ . ,(regexp-opt '("/**" "/*" "///" "/=" "/==" "/>" "//")))
(?* . ,(regexp-opt '("*>" "***" "*/")))
(?< . ,(regexp-opt '("<-" "<<-" "<=>" "<=" "<|" "<||" "<|||::=" "<|>" "<:" "<>" "<-<"
"<<<" "<==" "<<=" "<=<" "<==>" "<-|" "<<" "<~>" "<=|" "<~~" "<~"
"<$>" "<$" "<+>" "<+" "</>" "</" "<*" "<*>" "<->" "<!--")))
(?: . ,(regexp-opt '(":>" ":<" ":::" "::" ":?" ":?>" ":=")))
(?= . ,(regexp-opt '("=>>" "==>" "=/=" "=!=" "=>" "===" "=:=" "==")))
(?! . ,(regexp-opt '("!==" "!!" "!=")))
(?> . ,(regexp-opt '(">]" ">:" ">>-" ">>=" ">=>" ">>>" ">-" ">=")))
(?& . ,(regexp-opt '("&&&" "&&")))
(?| . ,(regexp-opt '("|||>" "||>" "|>" "|]" "|}" "|=>" "|->" "|=" "||-" "|-" "||=" "||")))
(?. . ,(regexp-opt '(".." ".?" ".=" ".-" "..<" "...")))
(?+ . ,(regexp-opt '("+++" "+>" "++")))
(?\[ . ,(regexp-opt '("[||]" "[<" "[|")))
(?\{ . ,(regexp-opt '("{|")))
(?\? . ,(regexp-opt '("??" "?." "?=" "?:")))
(?# . ,(regexp-opt '("####" "###" "#[" "#{" "#=" "#!" "#:" "#_(" "#_" "#?" "#(" "##")))
(?\; . ,(regexp-opt '(";;")))
(?_ . ,(regexp-opt '("_|_" "__")))
(?\\ . ,(regexp-opt '("\\" "\\/")))
(?~ . ,(regexp-opt '("~~" "~~>" "~>" "~=" "~-" "~@")))
(?$ . ,(regexp-opt '("$>")))
(?^ . ,(regexp-opt '("^=")))
(?\] . ,(regexp-opt '("]#"))))))
(dolist (char-regexp ligatures)
(set-char-table-range composition-function-table (car char-regexp)
`([,(cdr char-regexp) 0 font-shape-gstring]))))
Font ligatures only make sense in prog-mode
. Also, the git-diff buffers ought
to show characters as-is.
(add-hook 'prog-mode-hook #'auto-composition-mode)
(global-auto-composition-mode -1)
(save-buffer)
does not update the file if the buffer is unchanged. Sometimes,
file changes are needed to trigger some inotify hook, so C-t C-s
can be
used to force the file to be written either way.
(defun js:save-buffer-always ()
"Save the buffer even if it is not modified."
(interactive)
(set-buffer-modified-p t)
(save-buffer))
(bind-key "C-s" #'js:save-buffer-always sangster-map)
Auto-revert mode will automatically revert-buffer
when a file is changed and
the open buffer is unchanged.
(global-auto-revert-mode)
xdg-open will open a file in the “preferred application.”
(defun js:dired:xdg-open-file ()
"In dired-mode, open the file named on this line."
(interactive)
(js:macro:call-path-exe+ "xdg-open" (dired-get-filename nil t)))
(defcustom js:video-player-exe "mpv"
"The application to play video files with."
:tag "Video player executable"
:group 'js:custom
:type 'string)
(defun js:dired:open-video-file ()
"In dired-mode, open the file named on this line."
(interactive)
(js:macro:call-path-exe+ js:video-player-exe (dired-get-filename nil t)))
(add-hook
'dired-mode-hook
(lambda ()
(bind-key "C-t C-o" #'js:dired:xdg-open-file 'dired-mode-map
(derived-mode-p 'dired-mode))
(bind-keys :map dired-mode-map
:prefix-map sangster-dired-map
:prefix "C-t C-d"
:filter (derived-mode-p 'dired-mode)
("m" . js:dired:open-video-file))))
Magit is a complete text-based user interface to Git. Homepage.
Magit’s diff-mode allows you to quickly jump to source files. Unfortunately, Magit gets lost if that line happens to be invisible (folded, for example). This function auto-reveals that area. See Magit FAQ.
(require 'reveal)
(defun js:magit:reveal-if-invisible ()
(cond ((derived-mode-p 'org-mode) (org-reveal '(4)))
(t (reveal-post-command))))
(use-package magit
:custom
(git-commit-summary-max-length 50)
(git-commit-fill-column 72)
(vc-follow-symlinks t)
:hook
(magit-diff-visit-file . js:magit:reveal-if-invisible)
(git-commit-setup . git-commit-turn-on-flyspell) ; Spellcheck
:bind (:map sangster-map
;; Commit history of (buffer-file-name).
("C-v f" . magit-log-buffer-file)))
git-timemachine is a minor mode that allows us to navigate between previous
versions of opened files with p
and n
.
(use-package git-timemachine
:bind (:map sangster-map ("C-v t" . git-timemachine-toggle)))
Company is a text completion framework for Emacs. The name stands for “complete anything”. It uses pluggable back-ends and front-ends to retrieve and display completion candidates.
(use-package company
:bind (:map company-active-map
("C-n" . company-select-next)
("C-p" . company-select-previous))
:delight
:init
;; Don't make all suggestions lowercase.
;; See https://emacs.stackexchange.com/a/10838
(customize-set-variable 'company-dabbrev-downcase nil)
(global-company-mode))
company-box provides icons for Company.
(use-package company-box
:delight
:hook (company-mode . company-box-mode))
YASnippet is a template system for Emacs. It allows you to type an abbreviation and automatically expand it into function templates. Homepage.
(use-package yasnippet
:custom
(yas-prompt-functions '(yas-ido-prompt))
:config
(use-package yasnippet-snippets)
(yas-global-mode t)
(bind-key "y" #'yas-expand sangster-map)
(add-to-list #'yas-snippet-dirs (locate-user-emacs-file "snippets"))
(yas-reload-all)
:delight yas-minor-mode)
Make only one space after a period necessary to end a sentence (instead of two).
(customize-set-variable 'sentence-end-double-space nil)
This will automatically remove trailing spaces from the end of each line before saving the file.
(add-hook 'prog-mode-hook
(lambda () (add-hook 'before-save-hook 'delete-trailing-whitespace)))
Do not use tab-characters for indentation.
(customize-set-variable 'indent-tabs-mode nil)
(bind-key "M-SPC" 'cycle-spacing)
(add-hook 'prog-mode-hook
(lambda ()
(font-lock-add-keywords nil
'(("\\<\\(FIXME\\|TODO\\|BUG\\)" 1 font-lock-warning-face t)))))
(defun js:increment-number:decimal (&optional arg)
"Increment the number forward from point by 'arg'."
(interactive "p*")
(save-excursion
(save-match-data
(let (inc-by field-width answer)
(setq inc-by (if arg arg 1))
(skip-chars-backward "0123456789")
(when (re-search-forward "[0-9]+" nil t)
(setq field-width (- (match-end 0) (match-beginning 0)))
(setq answer (+ (string-to-number (match-string 0) 10) inc-by))
(when (< answer 0)
(setq answer (+ (expt 10 field-width) answer)))
(replace-match (format (concat "%0" (int-to-string field-width) "d")
answer)))))))
(bind-key "+" 'js:increment-number:decimal sangster-map)
(defun js:increment-number:hexadecimal (&optional arg)
"Increment the number forward from point by 'arg'."
(interactive "p*")
(save-excursion
(save-match-data
(let (inc-by field-width answer hex-format)
(setq inc-by (if arg arg 1))
(skip-chars-backward "0123456789abcdefABCDEF")
(when (re-search-forward "[0-9a-fA-F]+" nil t)
(setq field-width (- (match-end 0) (match-beginning 0)))
(setq answer (+ (string-to-number (match-string 0) 16) inc-by))
(when (< answer 0)
(setq answer (+ (expt 16 field-width) answer)))
(if (equal (match-string 0) (upcase (match-string 0)))
(setq hex-format "X")
(setq hex-format "x"))
(replace-match (format (concat "%0" (int-to-string field-width)
hex-format)
answer)))))))
C-o
and C-S-o
are both mapped to open-line
, which creates a blank line
after the current one. Unfortunately, if the cursor is in the middle of the
current line, it moves the remainder of the text to the next line.
The version below creates a blank line, but doesn’t affect the current line.
(defun js:open-line (&optional prev)
"Create a blank line, indented on the next or PREV line."
(interactive "*p")
(if (and prev (> prev 1))
(progn (message "PREV! %S" prev)
(move-beginning-of-line nil)
(open-line 1)
(indent-according-to-mode))
(progn (message "NEXT! %S" prev)
(move-end-of-line nil)
(newline-and-indent))))
Org-mode enhances C-o
to modify org-tables, if point
is in a table. This
functions recreates that, using the above function:
(defun js:org:open-line (n)
"A version of `org-open-line' that delegates to `js:open-line'
instead of `open-line''."
(interactive "*p")
(if (and org-special-ctrl-o (/= (point) 1) (org-at-table-p))
(org-table-insert-row)
(js:open-line n)))
(defun js:org:open-line:prev ()
(interactive)
(js:org:open-line 4))
(global-set-key (kbd "C-o") 'js:org:open-line)
(global-set-key (kbd "C-S-o") 'js:org:open-line:prev)
Shortcuts for commonly used unicode characters. Inspired by Sacha Chua’s Emacs configuration.
(defun js:insert-unicode (name)
"Insert the unicode character named NAME."
(interactive "sName: ")
(insert-char (gethash name (ucs-names))))
(defmacro js:macro:insert-unicode (name)
"Define a function to insert the unicode character named NAME."
`(defun ,(intern (concat "js:unicode:" (s-replace " " "-" name))) ()
(interactive)
(js:insert-unicode ,name)))
(bind-key "8 z" (js:macro:insert-unicode "ZERO WIDTH SPACE") 'ctl-x-map)
(bind-key "8 d" (js:macro:insert-unicode "EN DASH") 'ctl-x-map)
(bind-key "8 D" (js:macro:insert-unicode "EM DASH") 'ctl-x-map)
(bind-key "8 e t" (js:macro:insert-unicode "THINKING FACE") 'ctl-x-map)
A handy mode for browsing RFCs. Execute (rfc-mode-browse)
to get browse the
available RFCs.
(use-package rfc-mode
:custom
(rfc-mode-directory (expand-file-name "emacs/RFCs" xdg/cache-home)))
Tree-sitter is a fast syntax parser that supports several languages. Emacs (see emacs-tree-sitter) can use it for faster syntax highlighting.
(use-package tree-sitter
:config
(global-tree-sitter-mode)
(add-hook 'tree-sitter-after-on-hook #'tree-sitter-hl-mode)
;; HTML+ mode
(add-to-list 'tree-sitter-major-mode-language-alist '(mhtml-mode . html)))
(use-package tree-sitter-langs
:after tree-sitter)
lsp-mode aims to provide IDE-like experience by providing optional integration with the most popular Emacs packages like company, flycheck and projectile. Website.
See Configuring Emacs for Rust development.
(use-package lsp-mode
:init
;; set prefix for lsp-command-keymap (few alternatives - "C-l", "C-c l")
(customize-set-variable 'lsp-keymap-prefix "C-t t")
:hook (;; See https://emacs-lsp.github.io/lsp-mode/page/languages
(css-mode . lsp)
(haskell-mode . lsp)
(html-mode . lsp)
(javascript-mode . lsp)
(nix-mode . lsp)
(ruby-mode . lsp)
(scss-mode . lsp)
(xml-mode . lsp)
;; TODO: How does this clash with `rustic` below?
;; (rust-mode . lsp) ;; TODO: https://emacs-lsp.github.io/lsp-mode/page/lsp-rust
;; (haskell-mode . lsp) ;; TODO: https://emacs-lsp.github.io/lsp-haskell
(lsp-mode . lsp-enable-which-key-integration))
:custom
;; (lsp-eldoc-render-all t) ;; TODO: what does this do?
(lsp-idle-delay 0.6)
(lsp-rust-analyzer-cargo-watch-command "clippy")
(lsp-rust-analyzer-server-display-inlay-hints t)
:commands lsp)
LSP recommends certain settings for the sake of performance. You can test your current settings by running the interactive command src_emacs-lisp[:exports code]{(lsp-doctor)}.
(custom-set-variables
'(gc-cons-threshold (* 1 1024 1024 1024)) ;; 1 GiB
'(read-process-output-max (* 4 1024 1024)) ;; 4 MiB
'(lsp-use-plists t)) ;; ENV var LSP_USE_PLISTS must also be set
https://emacs-lsp.github.io/lsp-ui
(use-package lsp-ui
:config
(custom-set-variables
;; Sideline
'(lsp-ui-sideline-enable t)
'(lsp-ui-sideline-show-diagnostics t)
'(lsp-ui-sideline-delay 1)
'(lsp-ui-sideline-show-hover t)
;; Peek
'(lsp-ui-peek-always-show t)
;; Doc
'(lsp-ui-doc-enable t)
'(lsp-ui-doc-delay 3)))
LSP current breaks Flycheck’s “next-checker” feature. Flycheck is able to
daisy-chain multiple syntax checkers, running one after the other. You can run
src_emacs-lisp[:export code]{(flycheck-verify-setup)} and have a look at each
entry’s “next checkers.” However, the checker supplied by LSP, lsp
, runs in
many different modes and doesn’t have any “next checkers.” Flycheck wasn’t
designed to allow a single checker to have different “next checkers” depending
on the mode of the current buffer.
See flycheck issue #1762: “Correct way to chain checkers to lsp”.
To implement the hack-fix, from the above link, we need to set LSP’s
“next-checker” in the new flycheck-local-checkers
variable in a hook for each
mode, like:
(use-package haskell-mode
:hook
(haskell-mode . (lambda () (js:flycheck:lsp:next-checkers
'(haskell-stack-ghc haskell-hlist)))))
(defvar-local flycheck-local-checkers nil
"Buffer-local Flycheck checkers.")
(defun js:advice-around:flycheck-checker-get(fn checker property)
(or (alist-get property (alist-get checker flycheck-local-checkers))
(funcall fn checker property)))
(advice-add 'flycheck-checker-get
:around #'js:advice-around:flycheck-checker-get)
(defun js:flycheck:lsp:next-checkers (checkers)
"Set CHECKERS as the LSP checker's next-checkers in the local buffer."
(setq flycheck-local-checkers `((lsp . ((next-checkers . ,checkers))))))
Projectile is a project interaction library for Emacs. Its goal is to provide a nice set of features operating on a project level without introducing external dependencies (when feasible). Website.
(use-package projectile
:config
(define-key projectile-mode-map (kbd "C-t p") 'projectile-command-map)
(projectile-mode +1)
;; Hide mode name, but show project name.
:delight '(:eval (concat " [" (projectile-project-name) "]")))
(use-package flycheck
:delight
:hook ((org-mode prog-mode text-mode) . flycheck-mode)
:custom
(flycheck-textlint-config (locate-user-emacs-file "tools/textlint.json"))
:config
(add-to-list 'flycheck-textlint-plugin-alist '(html-mode . "html"))
;; TODO This relies on the "orga" NPM package, to parse ORG files into a
;; javascript-frieldly AST. Unfortunately this package (as of v.2.6.0) is
;; pretty broken. For example: it starts to raise exceptions if a link
;; includes a linebreak in its label. ex:
;; [[http://example.com][Link to
;; Example.com!]]
;; (add-to-list 'flycheck-textlint-plugin-alist '(org-mode . "org"))
(flycheck-add-next-checker 'proselint 'textlint))
(use-package helm-swoop)
(customize-set-variable 'tags-revert-without-query t
"Don't prompt when TAGS file is updated.")
(use-package rainbow-delimiters
:hook (prog-mode . rainbow-delimiters-mode))
(use-package jinja2-mode
:mode "\\.j2\\'")
(use-package yaml-mode
:mode "\\.yaml\\'")
(use-package csharp-mode
:mode "\\.cs\\'")
(use-package color-identifiers-mode
:hook ((ruby-mode)
(javascript-mode))
)
(add-hook
'css-mode-hook
(lambda ()
(js:flycheck:lsp:next-checkers '(css-stylelint))
(setq flycheck-stylelintrc
(locate-user-emacs-file "tools/stylelint/css-default.json"))))
(use-package scss-mode
:mode "\\.scss\\'"
:hook
;; Flycheck config
(scss-mode . (lambda ()
(js:flycheck:lsp:next-checkers '(scss-stylelint))
(setq flycheck-stylelintrc
(locate-user-emacs-file "tools/stylelint/scss-default.json")))))
(flycheck-define-checker scss-stylelint
"A SCSS syntax and style checker using stylelint.
This version of scss-stylelint overrides the default version
supplied by flycheck. The upstream stylelint project recently
removed the --style flag, which this checker uses to specify SCSS
syntax. In the new version of stylelint, different languages need
to specify different `flycheck-stylelintrc' files.
See URL `https://github.com/flycheck/flycheck/issues/1912'."
:command ("stylelint"
(eval flycheck-stylelint-args)
(option-flag "--quiet" flycheck-stylelint-quiet)
(config-file "--config" flycheck-stylelintrc))
:standard-input t
:error-parser flycheck-parse-stylelint
:predicate flycheck-buffer-nonempty-p
:modes (scss-mode))
(setq css-indent-offset 2)
Uniquely colours every unique identifier. Only works for some languages.
(use-package csv-mode
:mode "\\.csv\\'")
(add-hook 'emacs-lisp-mode-hook #'show-paren-mode)
(use-package haskell-mode
:mode "\\.hs\\'"
:hook
(haskell-mode . interactive-haskell-mode)
(haskell-mode . (lambda () (js:flycheck:lsp:next-checkers
'(haskell-stack-ghc haskell-hlist))))
:custom
(haskell-process-suggest-remove-import-lines t "Suggest removing unused imports.")
(haskell-process-auto-import-loaded-modules t "Auto-import modules.")
(haskell-process-log t "Enable debug logging."))
(use-package flycheck-haskell
:hook (haskell-mode . flycheck-haskell-setup))
(use-package lsp-haskell)
(custom-set-variables
'(js-indent-level 2)
'(jsx-indent-level 2))
(customize-set-variable 'flycheck-javascript-standard-executable "semistandard")
(use-package json-mode
:mode "\\.json\\'")
(use-package lua-mode
:mode "\\.lua\\'")
Make leading tabs visible.
(add-hook 'makefile-mode-hook
(lambda () (let ((whitespace-style '(face tabs tab-mark)))
(whitespace-mode t))))
(use-package markdown-mode
:mode ("\\.md\\'" . gfm-mode) ; GitHub-flavoured markdown
:custom
(markdown-command "multimarkdown"))
(use-package nix-mode
:mode "\\.nix\\'"
:hook
(nix-mode . (lambda () (js:flycheck:lsp:next-checkers '(nix)))))
(use-package php-mode
:mode "\\.php\\'")
(use-package plantuml-mode
:mode "\\.plantuml\\'"
)
(org-babel-do-load-languages 'org-babel-load-languages '((plantuml . t)))
(defun js:nix:plantuml:jar-path ()
"Return the path to PlantUML's JAR file. It can be set with the
`PLANTUML_JAR' environmental variable, or if unset, the path will
be derived from `nix path-info'. `nil' if PlantUML isn't
installed."
(or (getenv "PLANTUML_JAR")
(with-temp-buffer
(when (eq 0 (js:macro:call-path-exe "nix" "path-info" "nixpkgs#plantuml"))
(concat (s-trim-right (buffer-string)) "/lib/plantuml.jar")))))
(customize-set-variable 'org-plantuml-jar-path (js:nix:plantuml:jar-path))
(use-package haml-mode
:mode "\\.haml\\'")
(org-babel-do-load-languages 'org-babel-load-languages '((ruby . t)))
Use =rubocop-emacs= to automatically lint ruby files with RuboCop.
(use-package rubocop
:init (add-hook 'ruby-mode-hook #'rubocop-mode))
See Configuring Emacs for Rust development.
(use-package rustic
:ensure
:bind (:map rustic-mode-map
("M-j" . lsp-ui-imenu)
("M-?" . lsp-find-references)
("C-c C-c l" . flycheck-list-errors)
("C-c C-c a" . lsp-execute-code-action)
("C-c C-c r" . lsp-rename)
("C-c C-c q" . lsp-workspace-restart)
("C-c C-c Q" . lsp-workspace-shutdown)
("C-c C-c s" . lsp-rust-analyzer-status))
:config
;; uncomment for less flashiness
;; (setq lsp-eldoc-hook nil)
;; (setq lsp-enable-symbol-highlighting nil)
;; (setq lsp-signature-auto-activate nil)
;; comment to disable rustfmt on save
(setq rustic-format-on-save t)
:hook
(rustic-mode . js:rustic:disable-save-query))
(defun js:rustic:disable-save-query ()
"So that run C-c C-c C-r works without having to confirm."
(setq-local buffer-save-without-query t))
(org-babel-do-load-languages 'org-babel-load-languages '((shell . t)))
(use-package sqlup-mode
:straight (sqlup-mode :type git :host github :repo "Trevoke/sqlup-mode.el")
:mode "\\.sql\\'"
:hook (sql-mode . sql-interactive-mode))
(use-package typescript-mode
:mode "\\.ts\\'")
org-contrib
contains a collection of moderately useful extensions to org-mode.
(use-package org-contrib
:straight (:includes (org-checklist))
:config
(add-to-list 'org-modules 'org-checklist))
RESET_CHECK_BOXES
property- If set to
t
, when the TODO state is set to done all checkboxes under that item are cleared. LIST_EXPORT_BASENAME
property- If set to
t
, a file will be created using the value of that property plus a timestamp, containing all the items in the list which are not checked. Additionally the user will be prompted to print the list.
Automatically enable spell-check in org-mode, to avoid embarrassing typos!
(add-hook 'org-mode-hook #'flyspell-mode)
When using org-mode
’s “agenda” feature to manage a large number of TODO items,
it’s handy to visually distinguish them with icons. org-mode
supports this by
allowing you to specify regexp/icon pairs (See
org-agenda-category-icon-alist
). If a TODO heading matches the regexp, then
that item will be shown in the agenda view alongside the associated icon.
In this section, we create a customisation, js:org:agenda-categories
, to
create these regexp/icon pairs using icons from all-the-icons.
org-agenda-category-icon-alist
supports various kinds of icons, but sticking
to all-the-icons
is nice because the icons will show up both in the GUI and on
the terminal (with the correct fonts installed).
all-the-icons
provides a huge number of icons, so this customisation creates
menus to allow you to choose the icon from a menu. Changing the value of this
customisation will automatically update org-agenda-category-icon-alist
.
;; TODO: Make this a widget.
(defun js:all-the-icons-choices:icon (icon)
"Create a text widget to describe ICON, like ':) \"smile\"'."
`(const :tag ,(concat (cdr icon) " " (car icon)) ,(car icon)))
;; TODO: Make this a widget.
(defun js:all-the-icons-choices (name fn icons-alist)
"Create a widget to select an Icon Set/Icon Name pair."
`(cons :tag ,name
(function-item ,fn)
,(append '(choice :tag "Icon Name")
(mapcar #'js:all-the-icons-choices:icon icons-alist))))
(defun js:create-custom:org-agenda-categories ()
(defcustom js:org:agenda-categories
'(("inbox" all-the-icons-faicon . "envelope")
("todo" all-the-icons-faicon . "check-square-o")
("Home" all-the-icons-material . "home")
("Recreation" all-the-icons-faicon . "smile-o")
("Work" all-the-icons-faicon . "wrench"))
"The org-mode agenda categories, and their icons."
:tag "org-mode Agenda Categories"
:group 'js:custom
:type
`(repeat
(cons :tag "Agenda Category"
(regexp :tag "Regexp matching Category")
;; TODO: Make this a widget.
(choice
,(js:all-the-icons-choices "All the Icons"
#'all-the-icons-alltheicon all-the-icons-data/alltheicons-alist)
,(js:all-the-icons-choices "Font Awesome"
#'all-the-icons-faicon all-the-icons-data/fa-icon-alist)
,(js:all-the-icons-choices "Atom File Icons"
#'all-the-icons-fileicon all-the-icons-data/file-icon-alist)
,(js:all-the-icons-choices "Material Icons"
#'all-the-icons-material all-the-icons-data/material-icons-alist)
,(js:all-the-icons-choices "GitHub Octicons"
#'all-the-icons-ocicon all-the-icons-data/octicons-alist)
,(js:all-the-icons-choices "Weather Icons"
#'all-the-icons-wicon all-the-icons-data/weather-icons-alist))))
:set
(lambda (symbol categories)
(set-default symbol categories)
(js:apply-custom:org:agenda-categories categories))))
This function should be applied when the all-the-icons
package is loaded and
anytime js:org:agenda-categories
changes.
(defun js:apply-custom:org:agenda-categories (categories)
"Apply CATEGORIES to `org-agenda-category-icon-alist'."
(let ((mkicon
(lambda (icon)
`(,(car icon)
(,(funcall (cadr icon) (cddr icon))) nil nil :ascent center))))
(customize-set-variable 'org-agenda-category-icon-alist
(mapcar mkicon categories))))
(use-package all-the-icons
:config
(js:create-custom:org-agenda-categories)
(js:apply-custom:org:agenda-categories js:org:agenda-categories))
Since all-the-icons
are not fixed-width, separate the TODO heading with a tab,
to ensure they’re aligned.
(customize-set-variable
'org-agenda-prefix-format
'((agenda . "%i %-10:c%?-10t\t% s")
(todo . " %i %-10:c")
(tags . " %i %-10:c")
(search . " %i %-10:c")))
org-superstar is a minor mode that replaces the leading asterisks in org-mode headings with icons. Ditto for org-lists.
(use-package org-superstar
:custom
(org-superstar-headline-bullets-list '(?■ ?● ?◈ ?◉ ?○ ?▷))
(org-superstar-cycle-headline-bullets nil)
(org-superstar-leading-bullet " ⋅")
(org-superstar-leading-fallback ?\s "Hide leading bullets on terminal.")
(org-superstar-item-bullet-alist '((?* . ?•)
(?+ . ?+)
(?- . ?–)))
:hook (org-mode . org-superstar-mode))
(customize-set-variable 'org-ellipsis "⤶")
Turn on auto-fill mode in prose-based modes.
(add-hook 'org-mode-hook #'turn-on-auto-fill)
(add-hook 'text-mode-hook #'turn-on-auto-fill)
See The Org Manual: 16.13.2 Packages that conflict with Org mode.
(customize-set-variable 'org-support-shift-select 'always)
(add-hook 'org-shiftup-final-hook #'windmove-up)
(add-hook 'org-shiftleft-final-hook #'windmove-left)
(add-hook 'org-shiftdown-final-hook #'windmove-down)
(add-hook 'org-shiftright-final-hook #'windmove-right)
Disable electric indent mode in org-mode. This is normally a very useful feature
when writing code, but it misbehaves inside begin_src
blocks. Sometimes, when
you type RET
, it indents the entire block to the right, no matter what. This
can lead to the code block being indented by dozens of spaces.
Electric Indent mode is a global minor mode that automatically indents the line after every
RET
you type. This mode is enabled by default.
(add-hook 'org-mode-hook (lambda () (electric-indent-local-mode -1)))
(customize-set-variable 'org-log-into-drawer t)
(add-to-list 'org-structure-template-alist
'("L" . "src emacs-lisp"))
(defcustom js:org:root "~/org-files"
"The root directory for org-mode agenda and capture files."
:tag "org-mode Root Directory"
:group 'js:custom
:type 'directory
:set (lambda (symbol dir)
(set-default symbol dir)
(customize-set-variable 'org-directory dir)))
(defcustom js:org:agenda-files '("~/org-files/inbox.org"
"~/org-files/email-drafts.org"
"~/org-files/work.org")
"See `org-agenda-files'."
:tag "org-mode Agenda Files"
:group 'js:custom
:type '(repeat (file :must-match t))
:set (lambda (symbol files)
(set-default symbol files)
(customize-set-variable 'org-agenda-files files)))
(customize-set-variable 'org-directory js:org:root)
(customize-set-variable 'org-default-notes-file
(expand-file-name "notes.org" js:org:root))
(customize-set-variable 'org-agenda-files js:org:agenda-files)
(global-set-key (kbd "C-c l") 'org-store-link)
(global-set-key (kbd "C-c a") 'org-agenda)
(global-set-key (kbd "C-c c") 'org-capture)
(use-package doct
:commands (doct))
Instead of one file per entry, let’s collect each month’s journal entries in a
single .org
file.
(defcustom js:journal:root "~/journal-files"
"The root directory for org-mode journal files."
:tag "Journal Root Directory"
:group 'js:custom
:type 'directory)
(defun js:journal:montly-file-name ()
"The path to this month's journal file, like:
'~/journal-files/journal-2020-12.org'."
(expand-file-name (format "journal-%s.org" (format-time-string "%Y-%m"))
js:journal:root))
(defun js:journal:find-entry ()
"Append to end of or create Org entry with date heading."
(let ((heading (concat "* " (format-time-string "%F w%V %A"))))
(save-match-data
(goto-char (point-min))
(unless (re-search-forward heading nil 'no-error)
(end-of-line)
(insert heading))
(org-end-of-subtree))))
These are the capture templates that I find useful these days:
- Todo (
t
) - Used to quickly capture a random thought or TODO item before I forget it. This is my most generic capture template.
- Clipboard (
v
) - Create a note using contents of the clipboard.
- New Draft (
e
) - Quickly create an email draft, using org-mime.
- Project (
p
) - Quickly define a new project that I want to start. Projects are medium-sized life goals that require a series of actions.
- Someday (
s
) - Projects that I certainly don’t want to do now, but might at some point in the future. I try to review this list every year, but usually forget.
- Maybe (
m
) - Project that might be a good idea or might not be. This things need to be researched further.
- Journal (
j
) - Add a new entry to my personal journal/diary.
(customize-set-variable
'org-capture-templates
(doct `((:group "inbox"
:file ,(expand-file-name "inbox.org" org-directory)
:headline "New"
:todo-state "TODO"
:children
(("Todo (inbox)"
:keys "t"
:template ("* %{todo-state} %?"
" %i"
" %a"))
("Clipboard (inbox)"
:keys "v"
:template ("* %{todo-state} %?"
" %x"
" %i"
" %a"))))
(:group "emails"
:file ,(expand-file-name "email-drafts.org" org-directory)
:children
(("New Draft"
:keys "e"
:headline "Drafts"
:template ("* %(js:email:subject-prepend-re \"%:subject\") %? :EMAIL:"
":PROPERTIES:"
":MAIL_TO: %:replyto"
":MAIL_CC:"
":MAIL_BCC:"
":CREATED: %U"
":EMAIL-SOURCE: %l"
":END:"))))
(:group "PARA"
:file ,(expand-file-name "projects.org" org-directory)
:children (
("Project"
:keys "p"
:template-file ,(locate-user-emacs-file "org/templates/project-new.org"))))
(:group "someday"
:file ,(expand-file-name "someday.org" org-directory)
:headline "Someday / Maybe"
:children (
("Someday" :keys "s" :template ("* SOMEDAY %?"))
("Maybe" :keys "m" :template ("* MAYBE %?"))))
("Journal"
:keys "j"
:type plain
:file js:journal:montly-file-name
:function js:journal:find-entry
:template ("** %?"
":PROPERTIES:"
":CREATED: %U"
;; ":ANNOTATION: %a" ;; TODO: Why would I want an this?
":END:"
""
"%i")
:kill-buffer t
:empty-lines 1))))
(customize-set-variable
'org-refile-targets
`(((,(expand-file-name "projects.org" org-directory)) :maxlevel . 3)
((,(expand-file-name "categories.org" org-directory)) :maxlevel . 3)
((,(expand-file-name "resources.org" org-directory)) :maxlevel . 3)
((,(expand-file-name "contacts.org" org-directory)) :maxlevel . 3)))
Allows org-capture to be used as a standalone popup.
Call with:
emacsclient -c -F '(quote (name . "capture"))' -e '(js:org:activate-capture-frame)'
(defadvice js:org-capture:finalize
(after delete-capture-frame activate)
"Advise capture-finalize to close the frame"
(when (and (equal "capture" (frame-parameter nil 'name))
(not (eq this-command 'js:org-capture:refile)))
(delete-frame)))
(defadvice js:org-capture:refile
(after delete-capture-frame activate)
"Advise org-refile to close the frame"
(delete-frame))
(defadvice js:org:switch-to-buffer-other-window
(after supress-window-splitting activate)
"Delete the extra window if we're in a capture frame"
(if (equal "capture" (frame-parameter nil 'name))
(delete-other-windows)))
(defadvice js:org-capture:finalize
(after delete-capture-frame activate)
"Advise capture-finalize to close the frame"
(if (equal "capture" (frame-parameter nil 'name))
(delete-frame)))
(defun js:org:activate-capture-frame ()
"run org-capture in capture frame"
(select-frame-by-name "capture")
(switch-to-buffer (get-buffer-create "*scratch*"))
(org-capture))
To manage my oh-so modern life and all the digital information that entails, I use a personalised mixture of the Getting Things Done (GTD) and Projects—Areas—Resources—Archives (PARA) methodologies. It is very much a work in progress.
Sources:
- The PARA Method: A Universal System for Organizing Digital Information
- https://github.com/mwfogleman/.emacs.d/blob/master/michael.org
- https://www.youtube.com/watch?v=Bpmkeh4D98s
- https://gist.github.com/mwfogleman
To keep track of what happens to you, it’s handy to do a personal review at the
end of every day, week, and month. I find it’s not necessary to hold onto these
files, so I dump them into /tmp/
.
(defun js:review:daily ()
(interactive)
(let ((org-capture-templates
`(("d" "Review: Daily Review" entry (file+olp+datetree "/tmp/reviews.org")
(file ,(locate-user-emacs-file "org/templates/review-daily.org"))))))
(progn
(org-capture nil "d")
(js:org-capture:finalize t)
(org-speed-move-safe 'outline-up-heading)
(org-narrow-to-subtree)
(fetch-calendar)
(org-clock-in))))
(defun js:review:weekly ()
(interactive)
(let ((org-capture-templates
`(("w" "Review: Weekly Review" entry (file+olp+datetree "/tmp/reviews.org")
(file ,(locate-user-emacs-file "org/templates/review-weekly.org"))))))
(progn
(org-capture nil "w")
(js:org-capture:finalize t)
(org-speed-move-safe 'outline-up-heading)
(org-narrow-to-subtree)
(fetch-calendar)
(org-clock-in))))
(defun js:review:monthly ()
(interactive)
(let ((org-capture-templates
'(("m" "Review: Monthly Review" entry (file+olp+datetree "/tmp/reviews.org")
(file (locate-user-emacs-file "org/templates/review-monthly.org"))))))
(progn
(org-capture nil "m")
(js:org-capture:finalize t)
(org-speed-move-safe 'outline-up-heading)
(org-narrow-to-subtree)
(fetch-calendar)
(org-clock-in))))
(defun js:review:yearly ()
(interactive)
(let ((org-capture-templates
'(("y" "Review: Yearly Review" entry (file+olp+datetree "/tmp/reviews.org")
(file (locate-user-emacs-file "org/templates/review-yearly.org"))))))
(progn
(org-capture nil "y")
(js:org-capture:finalize t)
(org-speed-move-safe 'outline-up-heading)
(org-narrow-to-subtree)
(fetch-calendar)
(org-clock-in))))
(bind-keys :map sangster-map
:prefix-map review-map
:prefix "C-r"
("d" . js:review:daily)
("w" . js:review:weekly)
("m" . js:review:monthly)
("y" . js:review:yearly))
(f-touch "/tmp/reviews.org") ; TODO: why create this file ahead of time?
A project is “any outcome that will take more than one action step to complete.” As a result of implementing Tiago Forte’s “PARA” system, I can ensure that I always have an up to date project list.
(defun js:projects:find-file ()
(interactive)
(find-file (expand-file-name "projects.org" org-directory))
(widen)
(beginning-of-buffer)
(re-search-forward "* ")
(beginning-of-line))
(defun js:projects:overview ()
(interactive)
(js:projects:find-file)
(org-narrow-to-subtree)
(org-sort-entries t ?p)
(org-columns))
(defun js:projects:overview:deadlines ()
(interactive)
(js:projects:find-file)
(org-narrow-to-subtree)
(org-sort-entries t ?d)
(org-columns))
The concept of Stuck Projects comes from David Allen’s GTD. A stuck project is a project without any action steps or tasks associated with it.
Org-Mode has the ability to tell you which subtrees don’t have tasks associated with them. You can also configure what it recognizes as a stuck project. Unfortunately, by default, this functionality picks up a lot of noise.
This function creates an agenda of stuck projects that is restricted to my “Projects” subtree.
(defun js:org-agenda:list-stuck-projects ()
(interactive)
(js:projects:find-file)
(org-agenda nil "#" 'subtree))
(defun js:categories:find-file ()
(interactive)
(find-file (expand-file-name "categories.org" org-directory))
(widen)
(beginning-of-buffer)
(re-search-forward "* Categories")
(beginning-of-line))
(defun js:categories:overview ()
(interactive)
(js:categories:find-file)
(org-narrow-to-subtree)
(org-columns))
(defun js:categories:list ()
(let ((headings nil)
(match-categories "ITEM=\"Categories\"")
(path-list (list (expand-file-name "categories.org" org-directory))))
(org-map-entries
(lambda ()
(org-map-entries
(lambda () (push (nth 4 (org-heading-components)) headings))
nil
'tree))
match-categories path-list)
(symbol-value 'headings)))
(defun js:categories:list:completing-read ()
(completing-read "Category: " (js:categories:list)))
(defun js:categories:org-set-property ()
(interactive)
(org-set-property "CATEGORY"
(completing-read "Category: " (js:categories:list))))
(customize-set-variable
'org-todo-keywords
'((sequence "TODO(t)" "NEXT(n)" "STARTED(s)" "WAITING(w)"
"SOMEDAY(.)" "MAYBE(m)"
"|" "DONE(x!)" "CANCELLED(c)")))
(customize-set-variable
'org-todo-keyword-faces
'(("TODO" . (:weight bold :foreground "cyan"))
("NEXT" . (:weight bold :foreground "magenta"))
("STARTED" . (:weight bold :foreground "chocolate"))
("WAITING" . (:weight bold :foreground "khaki"))
("SOMEDAY" . (:weight bold :foreground "slate blue"))
("MAYBE" . (:weight bold :foreground "violet"))
("DONE" . (:weight bold :foreground "sea green"))
("CANCELLED" . (:weight bold :foreground "dim gray" :strike-through t))))
Use HTML5 by default.
(customize-set-variable 'org-html-doctype "html5")
This section adds default CSS to the <head>
of exported HTML.
References:
- StackOverflow: how to tell org-mode to embed my css file on HTML export?
- StackExchange: Get contents of a named source block
- Org Element API
- Emacs Lisp Text Processing: find-file vs with-temp-buffer
This CSS file is the Solarized CSS dark mode, with some modifications. This stylesheet is designed for org-mode.
- Remove fonts loaded from
fonts.googleapis.com
. html
andbody
:- Change
background-color
from#073642
to#121212
. - Remove borders.
- Change
color
from#839496
torgba(255, 255, 255, 0.6)
.
- Change
code
:- Change
background-color
from#002b36
torgba(255, 255, 255, 0.03)
.
- Change
pre
:- Change
background-color
from#002b36
torgba(255, 255, 255, 0.03)
. - Change
box-shadow
(long). - Change borders.
- Change
blockquote
:- Create custom styling (there was none before).
.notes
:- Custom style.
.linenr
(line numbers for exported source code):- Custom style.
- Add border and spacing to tables.
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
nav,
section,
summary {
display: block;
}
audio,
canvas,
video {
display: inline-block;
}
audio:not([controls]) {
display: none;
height: 0;
}
[hidden] {
display: none;
}
html {
font-family: sans-serif;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
body {
margin: 0;
}
a:focus {
outline: thin dotted;
}
a:active,
a:hover {
outline: 0;
}
h1 {
font-size: 2em;
}
abbr[title] {
border-bottom: 1px dotted;
}
b,
strong {
font-weight: bold;
}
dfn {
font-style: italic;
}
mark {
background: #ff0;
color: #000;
}
code,
kbd,
pre,
samp {
font-family: monospace, serif;
font-size: 1em;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
q {
quotes: "\201C" "\201D" "\2018" "\2019";
}
small {
font-size: 80%;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
img {
border: 0;
max-width: 100%;
}
svg:not(:root) {
overflow: hidden;
}
figure {
margin: 0;
}
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
legend {
border: 0;
padding: 0;
}
button,
input,
select,
textarea {
font-family: inherit;
font-size: 100%;
margin: 0;
}
button,
input {
line-height: normal;
}
button,
html input[type="button"],
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button;
cursor: pointer;
}
button[disabled],
input[disabled] {
cursor: default;
}
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box;
padding: 0;
}
input[type="search"] {
-webkit-appearance: textfield;
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box;
box-sizing: content-box;
}
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
textarea {
overflow: auto;
vertical-align: top;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
pre,
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 700;
}
html {
background-color: #121212;
color: rgba(255, 255, 255, 0.6);
margin: 1em;
}
body {
background-color: #121212;
margin: 0 auto;
max-width: 23cm;
padding: 1em;
}
code {
background-color: rgba(255, 255, 255, 0.03);
padding: 2px;
}
a {
color: #b58900;
}
a:visited {
color: #cb4b16;
}
a:hover {
color: #cb4b16;
}
h1 {
color: #d33682;
}
h2,
h3,
h4,
h5,
h6 {
color: #859900;
}
pre {
background-color: rgba(0, 0, 0, 0.03);
padding: 1em;
border-left: 15px solid #002b36;
border-right: 2px solid #002b36;
}
pre code {
background-color: #002b36;
}
h1 {
font-size: 2.8em;
}
h2 {
font-size: 2.4em;
}
h3 {
font-size: 1.8em;
}
h4 {
font-size: 1.4em;
}
h5 {
font-size: 1.3em;
}
h6 {
font-size: 1.15em;
}
.tag {
background-color: #073642;
color: #d33682;
padding: 0 0.2em;
}
.todo,
.next,
.done {
color: #002b36;
background-color: #dc322f;
padding: 0 0.2em;
}
.tag {
-webkit-border-radius: 0.35em;
-moz-border-radius: 0.35em;
border-radius: 0.35em;
}
.TODO {
-webkit-border-radius: 0.2em;
-moz-border-radius: 0.2em;
border-radius: 0.2em;
background-color: #2aa198;
}
.NEXT {
-webkit-border-radius: 0.2em;
-moz-border-radius: 0.2em;
border-radius: 0.2em;
background-color: #268bd2;
}
.ACTIVE {
-webkit-border-radius: 0.2em;
-moz-border-radius: 0.2em;
border-radius: 0.2em;
background-color: #268bd2;
}
.DONE {
-webkit-border-radius: 0.2em;
-moz-border-radius: 0.2em;
border-radius: 0.2em;
background-color: #859900;
}
.WAITING {
-webkit-border-radius: 0.2em;
-moz-border-radius: 0.2em;
border-radius: 0.2em;
background-color: #cb4b16;
}
.HOLD {
-webkit-border-radius: 0.2em;
-moz-border-radius: 0.2em;
border-radius: 0.2em;
background-color: #d33682;
}
.NOTE {
-webkit-border-radius: 0.2em;
-moz-border-radius: 0.2em;
border-radius: 0.2em;
background-color: #d33682;
}
.CANCELLED {
-webkit-border-radius: 0.2em;
-moz-border-radius: 0.2em;
border-radius: 0.2em;
background-color: #859900;
}
blockquote {
font-style: italic;
color: #586e75;
display: block;
background: rgba(255, 255, 255, 0.03);
padding: 15px 20px 15px 45px;
margin: 0 0 20px;
border-left: 15px solid #586e75;
border-right: 2px solid #586e75;
}
blockquote em, blockquote i {
font-style: normal;
}
.notes ul {
padding: 0;
}
.notes li {
list-style: none;
background: rgba(0, 0, 0, 0.03);
padding: 15px 20px 15px 45px;
margin: 0 0 20px;
border-left: 15px solid #d33682;
border-right: 2px solid #d33682;
}
blockquote,
pre,
.notes li {
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 3px 1px -2px rgba(0, 0, 0, 0.12),
0 1px 5px 0 rgba(0, 0, 0, 0.2);
}
.linenr {
opacity: 0.25;
font-size: 0.66em;
vertical-align: middle;
user-select: none;
cursor: default;
}
table { width: 100%; }
th, td {
padding: 1ex;
}
th {
background: #268bd2;
color: #121212;
}
th + th { border-left: 0.25ex solid #121212; }
td + td { border-left: 0.25ex solid #268bd2; }
.org-left { text-align: left; }
.org-right { text-align: right; }
.org-src-container {
background: #000;
}
This block creates a hook to insert the content of a named begin_src
block
into <head>
, when exporting HTML.
(defun js:org:inline-custom-css (exporter)
"Insert custom inline css"
(when (eq exporter 'html)
(let ((css-src-block-name "default-css"))
(customize-set-variable 'org-html-head-include-default-style nil)
(customize-set-variable
'org-html-head (concat
"<style type=\"text/css\">\n"
(save-excursion
(with-temp-buffer
(insert-file-contents (locate-user-emacs-file "README.org"))
(cadr (org-babel-lob--src-info css-src-block-name))))
"</style>\n")))))
(add-hook 'org-export-before-processing-hook #'js:org:inline-custom-css)
; TODO this line fails to load because org-html-html5-elements isn't available
; here. (add-to-list 'org-html-html5-elements '"cite")
This adds additional HTML5 “special block” types. When using the HTML5 docktype with org-mode HTML export, you can create certain HTML elements using “special blocks,” like:
#+ATTR_HTML: :class some-class #+begin\_TAG Content! #+end\_TAG
Get translated into HTML tags like:
(customize-set-variable 'org-html-validation-link nil)
Export to =reveal.js= slideshow
(use-package org-re-reveal)
(use-package ox-hugo
:after ox)
(defgroup js:custom:www nil
"Websites settings."
:tag "Websites"
:group 'js:custom)
(defgroup js:custom:www:photography nil
"Photography gallery website settings."
:tag "Photography website"
:group 'js:custom:www)
(defcustom js:www:photography:watermark
"https://photography.example.com"
"The watermark used by `js:www:photography:process-image'."
:tag "Photography Website Image Watermark"
:group 'js:custom:www:photography
:type 'string)
(defun js:www:photography:process-image ()
"Add a watermark to the given image, link to it, and append an
org-table with its EXIF data."
(interactive)
(let* ((file (read-file-name "Image File: "))
(base (file-name-nondirectory file))
(asset (js:www:watermark-image file js:www:photography:watermark "assets"))
(link (concat "[[./" asset "][" base "]]"))
(cmd (concat "exiv2 " asset))
(stdout (shell-command-to-string cmd))
(without-filename
(replace-regexp-in-string
"File name.+" (concat "File name : " link) stdout))
(fixed-copyright
(replace-regexp-in-string
"\\(Copyright *\\):.+" "\\1: Jon Sangster" without-filename))
(table (replace-regexp-in-string
"\\(.+?\\):\\(.*\\)" "| \\1 | \\2 |" fixed-copyright)))
(save-excursion
(org-set-property "EXPORT_HUGO_CUSTOM_FRONT_MATTER"
(concat ":useRelativeCover t :cover " base))
(insert "- [ ] [[file:" asset "][" base "]]\n\n"
"#+hugo: more\n"
"#+begin_details\n"
"#+begin_summary\nEXIF Data\n#+end_summary\n"
"| Attribute | Value |\n"
"|-----------|-------|\n"
table)
(delete-backward-char 1)
(insert "#+end_details\n")
(previous-line 2)
(org-table-align))
(forward-char 8)))
(defun js:www:asset-path (dirname src-path)
"Return the assets path for the given image. IE: ../dirname"
(let* ((dir (file-name-directory src-path))
(parent (file-name-directory (directory-file-name dir)))
(dest-dir (expand-file-name dirname parent))
(base (file-name-nondirectory src-path)))
(expand-file-name base dest-dir)))
(defun js:www:watermark-image (file label dirname)
"Use ImageMagick's 'convert' to create a watermarked image"
(let* ((dest (js:www:asset-path dirname file))
(cmd (concat "convert "
file
" -gravity SouthEast"
" -stroke '#000c' -strokewidth 2 -annotate 0 '" label " '"
" -stroke none -fill white -annotate 0 '" label " '"
" " dest)))
(make-directory (file-name-directory dest) :PARENTS)
(shell-command cmd)
dest))
(defun js:www:set-ox-hugo-bundle-property ()
(interactive)
(org-set-property "EXPORT_FILE_NAME" "index")
(org-set-property "EXPORT_HUGO_BUNDLE" (org-hugo-slug (org-get-heading t t)))
)
(defun js:www:set-ox-hugo-file-name-property ()
(interactive)
(org-set-property "EXPORT_FILE_NAME" (org-hugo-slug (org-get-heading t t))))
Setting up mu4e is complicated. Configuring your email client to interact with idiosyncratic IMAP servers is even more complicated. Here are some resources to help figure things out:
- Use plaintext email
- email + git = <3
- Emacs From Scratch
- Fastmail setup with Emacs, mu4e and mbsync on macOS
- A Complete Guide to Email in Emacs using Mu and Mu4e
- Emacs as Email Client
- My E-Mail configuration: Nix & Home-Manager, Notmuch & mbsync
- A.4 Viewing images inline
mu4e
is the Emacs UI to interface with the mu
mail indexer.
Note: This configuration assumes that mail is stored in $XDG_DATA_HOME/mail
,
which is not the default mail directory.
mu4e searches and bookmarks aren’t “context-aware” and will return mail from all
your mailboxes. This function will modify your bookmarks so they only return
results from a given mailbox directory (relative to mu4e-maildir
).
(defun js:mu4e:mbox-bookmarks (mbox bookmarks)
"Wrap the queries in BOOKMARKS so they are within MBOX.
This replaces each `:query' property with a wrapped version:
`(query) AND maildir:\"MBOX\"'."
(mapcar
(lambda (bkmrk)
(let* ((query (plist-get bkmrk :query))
(context-query (concat "(" query ") AND \"maildir:" mbox "\"")))
(if query (plist-put bkmrk :query context-query) query)))
bookmarks))
Most Emacs packages can be automatically installed by straight
with
(use-package)
; however, mu4e
is installed via mu
: a package installed by
the system package manager. This function returns the path to these lisp files
on the local NixOS system.
(defun js:nix:mu4e:site-lisp-directory ()
"Return the path to mu's mu4e lisp files. It can be set with
the `MU4E_SITE_LISP' environmental variable, or if unset, the
path will be derived from `nix path-info'. `nil' if mu isn't
installed."
(or (getenv "MU4E_SITE_LISP")
(with-temp-buffer
(when (eq 0 (js:macro:call-path-exe "nix" "path-info" "nixpkgs#mu"))
(concat (s-trim-right (buffer-string))
"/share/emacs/site-lisp/mu4e")))))
When storing a link to an email entry, The capture template can refer to
properties of the email, like :to
and :from
(See org-capture-templates
);
however, there are no properties for the Reply-To
address.
This function advises the mu4e function to add in :replyto
, :replytoname
,
and :replytoaddress
. If the email has no Reply-To
field, these properties
will be the same as :from
.
This advice is inspired by (mu4e~org-store-link-message)
and
(org-link-store-props)
.
(defun js:advice-after:mu4e~org-store-link-message:reply-to (&rest r)
"Advise `mu4e~org-store-link-message' to add `:replyto',
`:replytoname', and `:replytoaddress'."
(let* ((plist org-store-link-plist)
(msg (mu4e-message-at-point))
(reply-to (or (car-safe (plist-get msg :reply-to))
(car-safe (plist-get msg :from))))
(reply-to-adr (when reply-to (mu4e~org-address reply-to)))
(adr (mail-extract-address-components reply-to-adr)))
(when reply-to
(setq plist (plist-put plist :replyto reply-to-adr))
(setq plist (plist-put plist :replytoname (car adr)))
(setq plist (plist-put plist :replytoaddress (nth 1 adr))))))
Add the advice:
(advice-add 'mu4e~org-store-link-message
:after #'js:advice-after:mu4e~org-store-link-message:reply-to)
This function simply prepends “RE:” to a string, unless it’s already there. It’s
intended to be used to generate reply subjects, when capturing a new TODO. It
can be used in org-capture-templates
like this:
%(js:email:subject-prepend-re \"%:subject\")
.
(defun js:email:subject-prepend-re (subject)
"Prepend 'RE:' to SUBJECT, unless it's already there, or
empty."
(cond ((or (not subject) (string-equal "" subject)) "")
((string-prefix-p "RE:" subject t) subject)
(t (concat "RE: " subject))))
(defvar js:mu4e-query-trash "(flag:trashed OR maildir:/\\/Spam$/)"
"A mu4e search for trash or spam emails.")
(defun js:mu4e:not-trash (query)
(concat query " AND NOT " js:mu4e-query-trash))
(defgroup js:custom:mail nil
"Email account settings."
:tag "Email"
:group 'js:custom)
(defgroup js:custom:mail:personal nil
"Personal email account settings."
:tag "Personal Email"
:group 'js:custom:mail)
(defcustom js:mail:personal:mailbox
"/personal/"
"The mbsync mailbox directory of my personal email account."
:tag "Personal Mailbox Subdirectory"
:group 'js:custom:mail:personal
:type 'string)
(defcustom js:mail:personal:user-name
"Jon Sangster"
"The user-name of my personal email account."
:tag "Personal Email User's Name"
:group 'js:custom:mail:personal
:type 'string)
(defcustom js:mail:personal:user-address
"personal@example.com"
"The email address of my personal email account."
:tag "Personal Email Address"
:group 'js:custom:mail:personal
:type 'string)
(let ((mbox js:mail:personal:mailbox))
(setq js:mu4e-mailbox-personal
`((user-full-name . ,js:mail:personal:user-name)
(user-mail-address . ,js:mail:personal:user-address)
(mu4e-drafts-folder . ,(concat mbox "Drafts"))
(mu4e-refile-folder . ,(concat mbox "Archive"))
(mu4e-sent-folder . ,(concat mbox "Sent"))
(mu4e-trash-folder . ,(concat mbox "Trash"))
(mu4e-maildir-shortcuts
. ((:maildir ,(concat mbox "Inbox") :key ?i)
(:maildir ,(concat mbox "Drafts") :key ?d)
(:maildir ,(concat mbox "Archive") :key ?a :hide-unread t)
(:maildir ,(concat mbox "Sent") :key ?s :hide-unread t)
(:maildir ,(concat mbox "Spam") :key ?S :hide-unread t)
(:maildir ,(concat mbox "Trash") :key ?t :hide-unread t)))
(mu4e-bookmarks
. ,(js:mu4e:mbox-bookmarks mbox
`((:name "Unread" :key ?u
:query ,(js:mu4e:not-trash "flag:unread"))
(:name "Today's mail" :key ?t
:query ,(js:mu4e:not-trash "date:today..now"))
(:name "Last 7 days" :key ?w :hide-unread t
:query ,(js:mu4e:not-trash "date:7d..now"))
(:name "With images" :key ?p
:query ,(js:mu4e:not-trash "mime:image/*"))))))))
(defun js:mu4e:make-context:personal ()
(make-mu4e-context
:name "Personal"
:vars js:mu4e-mailbox-personal
:match-func
(lambda (msg)
(when msg (string-prefix-p js:mail:personal:mailbox
(mu4e-message-field msg :maildir))))))
(defgroup js:custom:mail:work nil
"Work email account settings."
:tag "Work Email"
:group 'js:custom:mail)
(defcustom js:mail:work:mailbox
"/work/"
"The mbsync mailbox directory of my work email account."
:tag "Work Mailbox Subdirectory"
:group 'js:custom:mail:work
:type 'string)
(defcustom js:mail:work:user-name
"Jon Sangster"
"The user-name of my work email account."
:tag "Work Email User's Name"
:group 'js:custom:mail:work
:type 'string)
(defcustom js:mail:work:user-address
"work@example.com"
"The email address of my work email account."
:tag "Work Email Address"
:group 'js:custom:mail:work
:type 'string)
(defcustom js:mail:work:signature
(concat "Jon Sangster :: Professional professional\n"
" work@example.com\n"
" www.example.com")
"The signature line for my work email account."
:tag "Work Email Signature Line"
:group 'js:custom:mail:work
:type 'string)
(let ((mbox js:mail:work:mailbox))
(setq js:mu4e-mailbox-work
`((user-full-name . ,js:mail:work:user-name)
(user-mail-address . ,js:mail:work:user-address)
(mu4e-compose-signature . ,js:mail:work:signature)
(mu4e-drafts-folder . ,(concat mbox "[Gmail]/Drafts"))
(mu4e-sent-folder . ,(concat mbox "[Gmail]/Sent Mail"))
(mu4e-refile-folder . ,(concat mbox "[Gmail]/All Mail"))
(mu4e-trash-folder . ,(concat mbox "[Gmail]/Trash"))
;; Don't move to "Sent Messages." Gmail/IMAP takes care of this.
(mu4e-sent-messages-behavior . delete)
(mu4e-maildir-shortcuts
. ((:maildir ,(concat mbox "Inbox") :key ?i)
(:maildir ,(concat mbox "GitHub") :key ?g)
(:maildir ,(concat mbox "Metrics") :key ?m)
(:maildir ,(concat mbox "[Gmail]/Important") :key ?I)
(:maildir ,(concat mbox "[Gmail]/Drafts") :key ?d)
(:maildir ,(concat mbox "[Gmail]/All Mail") :key ?a :hide-unread t)
(:maildir ,(concat mbox "[Gmail]/Sent Mail") :key ?s :hide-unread t)
(:maildir ,(concat mbox "[Gmail]/Spam") :key ?S :hide-unread t)
(:maildir ,(concat mbox "[Gmail]/Trash") :key ?t :hide-unread t)))
(mu4e-bookmarks
. ,(js:mu4e:mbox-bookmarks mbox
`((:name "Unread" :key ?u
:query ,(js:mu4e:not-trash "flag:unread"))
(:name "Today's mail" :key ?t
:query ,(js:mu4e:not-trash "date:today..now"))
(:name "Last 7 days" :key ?w :hide-unread t
:query ,(js:mu4e:not-trash "date:7d..now"))
(:name "With images" :key ?p
:query ,(js:mu4e:not-trash "mime:image/*"))))))))
(defun js:mu4e:make-context:work ()
(make-mu4e-context
:name "Work"
:vars js:mu4e-mailbox-work
:match-func
(lambda (msg)
(when msg (string-prefix-p js:mail:work:mailbox
(mu4e-message-field msg :maildir))))))
(let ((mu4e-site-lisp (js:nix:mu4e:site-lisp-directory)))
(if (and mu4e-site-lisp (file-exists-p mu4e-site-lisp))
(use-package mu4e
:straight `(:local-repo ,mu4e-site-lisp :pre-build ())
:defer 10 ; Wait 10 seconds before starting.
:bind (:map sangster-map ("m" . mu4e))
:custom
(message-confirm-send t) ; Prompte before sending mail
(message-send-mail-function 'message-send-mail-with-sendmail)
(mu4e-attachments-dir "~/system/xdg/download/") ; TODO: hardcoded path
(mu4e-change-filenames-when-moving t)
(mu4e-compose-context-policy 'ask-if-none)
(mu4e-compose-format-flowed t) ; Use format=flowed mimetype
(mu4e-context-policy 'ask-if-none)
(mu4e-date-format "%Y-%m-%d")
(mu4e-headers-date-format "%Y-%m-%d")
(mu4e-headers-skip-duplicates t)
(mu4e-maildir (expand-file-name "mail" xdg/data-home))
(mu4e-update-interval (* 10 60)) ; Check for new mail every 10 mins.
(mu4e-use-fancy-chars t) ; Use unicode
(mu4e-view-show-addresses t) ; Show contact emails (vs. names only)
(mu4e-view-show-images nil) ; Avoid tracking images
;; This setting allows to re-sync and re-index mail by pressing U
(mu4e-get-mail-command "mbsync -a")
:config
;; TODO: Change custom callback
(setq mu4e-contexts (list (js:mu4e:make-context:personal)
(js:mu4e:make-context:work))
mu4e-headers-flagged-mark '("F" . "⚑")
mu4e-headers-passed-mark '("P" . "⇉")
mu4e-headers-replied-mark '("R" . "↵")
mu4e-headers-seen-mark '("S" . " ")
mu4e-headers-signed-mark '("s" . "✍")
mu4e-headers-trashed-mark '("T" . "×")
mu4e-headers-unread-mark '("u" . "✉")
mu4e-headers-thread-child-prefix '("├>" . "├▶ ")
mu4e-headers-thread-last-child-prefix '("└>" . "└▶ ")
mu4e-headers-thread-connection-prefix '("│" . "│ ")
mu4e-headers-thread-orphan-prefix '("┬>" . "┬▶ ")
mu4e-headers-thread-single-orphan-prefix '("─>" . "─▶ ")))))
The updated marks and thread prefixes are from mu-discuss@googlegroups.com: FYI nicer threading characters.
Inspired by Improve modeline in headers mode.
Show desktop notifications when new mail arrives.
(use-package mu4e-alert
:after mu4e
:custom
(mu4e-alert-style 'libnotify)
(mu4e-alert-interesting-mail-query (js:mu4e:not-trash "flag:unread AND date:7d..now"))
:config
(mu4e-alert-enable-notifications)
(mu4e-alert-enable-mode-line-display))
This package allows you to fold threads under their original message.
(use-package mu4e-thread-folding
:straight (mu4e-thread-folding :type git :host github
:repo "rougier/mu4e-thread-folding")
:hook
(mu4e-headers-mode . mu4e-thread-folding-mode)
:custom-face
(mu4e-thread-folding-root-unfolded-face
((t (:extend t :background "#080833" :weight bold :overline nil :underline nil))))
(mu4e-thread-folding-root-folded-face
((t (:extend t :background "grey5" :overline nil :underline nil))))
(mu4e-thread-folding-child-face
((t (:extend t :background "gray5" :underline nil))))
(mu4e-thread-folding-root-prefix-face
((t (:extend t :background "gray10" :overline nil :underline nil))))
:config
(add-to-list 'mu4e-header-info-custom
'(:empty . (:name "Empty"
:shortname ""
:function (lambda (msg) " "))))
(setq mu4e-headers-fields '((:empty . 2)
(:human-date . 12)
(:flags . 6)
(:mailing-list . 10)
(:from . 22)
(:subject . nil)))
:bind (:map mu4e-headers-mode-map
("<tab>" . #'mu4e-headers-toggle-at-point)
("<S-tab>" . #'mu4e-headers-toggle-fold-all)))
(defun js:org-mime:inline-css ()
"Modify the inline CSS of exported HTML elements"
;; Use "dark mode" source code blocks.
(org-mime-change-element-style
"pre" (format "color: %s; background-color: %s; padding: 0.5em;"
"#d8d8d8" "#1b1e20"))
;; Indent blockquotes.
(org-mime-change-element-style
"blockquote" "border-left: 2px solid gray; padding-left: 4px;"))
(use-package org-mime
:hook
(message-send . org-mime-confirm-when-no-multipart) ; Prompt if HTML is missing
(org-mime-html . js:org-mime:inline-css)
(message-mode . (lambda ()
(setq fill-column 72) ; Plaintext emails use 72 columns.
(local-set-key (kbd "C-t M-o") 'org-mime-htmlize)))
(org-mime-src-mode . (lambda () (setq fill-column 72)))
(org-mode . (lambda ()
(local-set-key (kbd "C-t M-o") 'org-mime-org-buffer-htmlize)
(local-set-key (kbd "C-t M-O") 'org-mime-org-subtree-htmlize)))
:config
(setq org-mime-export-options '(:section-numbers nil :with-author nil :with-toc nil :with-latex dvipng)
org-mime-export-ascii 'utf-8))