Nathan Dwarshuis b96539afc8 | ||
---|---|---|
.github/workflows | ||
bin | ||
etc | ||
local/lib | ||
straight/versions | ||
.gitignore | ||
README.org | ||
init.el | ||
runtime_pkgs | ||
straight-boot.el |
README.org
- overview
- library
- user interface
- low-level config
- editing
- standardization
- auto completion
- auto formatting
- parenthesis matching
- sudo edit
- formats and languages
- testing
- org-mode
- tools
- keybindings
This is my personal emacs config. It is quite massive.
overview
features and use cases
- full GTD implementation with
org-mode
to help me stay organized - unified interface for common linux tools (dired, shell, git, ediff)
- fully customizable email client with
mu4e
- optimizations for some of my favorite languages (R, Lisp, Haskell, Lua, Python)
- document preparation with latex
for new users
Feel free to take bits and pieces for your own configuration file. Like many things in emacs, the config file is quite self documenting; however, there are some useful ramblings that decribe why I made some design choices over others. As someone who learned from countless emacs configs of other experienced users, I thought it was extremely beneficial to see the thought process behind their workflow and code, and I hope my annotations pay that forward. Finally, please don't just blindly copy this config into your ~/.emacs.d
. I don't care if you do, but you will learn more if you build from scratch.
config structure
The "config file" is actually multiple files.
init.el
The "root" is init.el
which is the file explicitly loaded by emacs. Most users have their entire config in this file but I put most of my actuall settings in another file as explained in the next paragraph. Here init.el
has minimum functionality, including setting the repositories, configuring use-package
(which installs all other packages and ensures they are available, useful if I move this elsewhere), and load paths for other config file.
conf.org/conf.el
Once loaded, the init.el
pulls in another file called conf.el
with the function org-babel-load-file
. conf.el
is actually sourced from an org-mode file called conf.org
(this file).
Using an org file like this offers several advantages:
- org files are foldable in emacs which makes navigation easy.
- org files allow code snippets (the bit that actually go into
conf.el
) which allows for explanatory prose to be written around them, making documentation easy and clear. org-mode
has an automatic table of contents through thetoc-org
package, which makes naviagation even easier.- github itself is awesome enough to recognize org files as valid markdown and will render all the text, code snippets, headers, and table of contents in the nice html that you are reading now if on github.
The result is a nearly self-documenting, self-organizing configuration that is easy to maintain and also easy to view for other users.
personal modules
conf.el
in turn also loads "personal modules" located in local/lib
. These modules are effectively packages that I use only for myself (and may submit to an offcial repo if I clean them up and deem them general enough for more than just me).
In general, code from conf.org
is moved into a module for any of several reasons:
- They represent large sections of code that have a small public API (perhaps one interactive function that in turn calls 20 supporting functions).
- They are worth testing independently
-
They are worth byte-compiling independently (for speed and/or compile-time checks)
- In my experience
init.el
andconf.el
aren't worth byte compiling because there are often many false-flag errors and scoping errors due to the order of loading certain components
- In my experience
- The code might become an official package in the future
- The code is executed inside some toplevel macro and I don't feel like indenting hundreds of lines
continuous integration
In the root of this directory is a .github
folder with some simple tests to ensure this config is 'valid'. I'm experimenting with this and it may not turn out to be worth it, but the main reason these exist is that I would like to ensure I can transfer my emacs config to another system and have it work with no problems.
The danger with only having emacs on my daily driver is that I could silently introduce a dependency on some system library, and this may or may not be present when I unpack this config on a different machine. For now, the CI pipeline simply tests whether or not this config will initialize and build correctly on a "bare" system, and also tests if I can pull a list of dependencies using my somewhat hacky API so they can be installed via the package manager.
library
This is code that is used generally throughout the emacs config
external
Some useful external libraries that I use all over the place
string manipulation
(use-package s
:straight t)
functional programming
(use-package dash
:straight t
:config)
file operations
(use-package f
:straight t)
hash tables
(use-package ht
:straight t)
internal
Define a path to internal libraries (either things I am developing or external .el
files I find useful)
(defvar nd/local-pkg-directory "local/share/")
(defvar nd/lib-directory "local/lib/")
(defun nd/expand-local-pkg-directory (path)
(f-join user-emacs-directory nd/local-pkg-directory path))
(defun nd/expand-lib-directory (path)
(f-join user-emacs-directory nd/lib-directory path))
system dependencies
Definitions
;; dependency declaration
(defvar nd/required-exes nil
"Running list of executables required to run various configuations.")
(defconst nd/valid-package-types '(:pacman :aur :gem :local))
(defun nd/require-bin (bin-or-path &optional pkg-type pkg-name)
"Indicate that a binary executable is required.
BIN-OR-PATH is a string indicating the executable name or path to
the executable. PKG-TYPE indicates how BIN-ON-PATH must be
installed (see `nd/required-exes' for available types). PKG-NAME
indicates the package name to install which provides BIN-OR-PATH,
which defaults to BIN-OR-PATH if not given."
(let* ((pt (or pkg-type :pacman))
(name (f-base bin-or-path))
(pn (or pkg-name name))
(new (list :full-path (executable-find bin-or-path)
:pkg-type pt
:pkg-name pn)))
(cond
((not (memq pt nd/valid-package-types))
(warn "Invalid dependency type: %s" pt))
((ht-get nd/required-exes name)
(warn "Dependency already required: %s" name))
(t
(ht-set nd/required-exes name new)))))
(defmacro nd/if-bin (bin then &rest else)
"Execute THEN if BIN exists, otherwise do ELSE."
(declare (indent 1))
(if-let (x (ht-get nd/required-exes bin))
`(if ,(plist-get x :full-path) ,then ,@else)
(warn "executable '%s' must be required" bin)))
(defmacro nd/when-bin (bin &rest body)
"Execute BODY if the program BIN exists."
(declare (indent 1))
`(nd/if-bin ,bin (progn ,@body)
(message "Executable %s not found. Skipping." ,bin)))
(defun nd/get-dependencies (keys)
"Return list of all dependencies.
KEYS is a list of keywords that indicate the :pkg-type of
dependencies to return."
(->> (ht-values nd/required-exes)
(--filter (memq (plist-get it :pkg-type) keys))
(--map (plist-get it :pkg-name))
(-uniq)))
;; pacman-specific introspection
(defvar nd/aur-helper nil
"The aur helper command installed on this system.")
(defun nd/find-aur-helper ()
"Return the current aur helper installed (or nil if none)."
;; add more as needed
(let ((helpers (list "yay")))
(-first #'executable-find helpers)))
(defun nd/pacman-dependencies (&optional uninstalled?)
"Return pacman and aur dependencies.
If UNINSTALLED? is non-nil, return only the packages that are not
installed but required by this config."
(let ((ps (nd/get-dependencies '(:pacman)))
(as (nd/get-dependencies '(:aur))))
(if (not uninstalled?) `(,ps ,as)
(let ((is (->> (shell-command-to-string "pacman -Qq")
(s-split "\n"))))
(list (--remove (member it is) ps)
(--remove (member it is) as))))))
(defun nd/shell-test (cmd &rest args)
"Return t if CMD with ARGS succeeds."
(= 0 (apply #'call-process cmd nil nil nil args)))
(defun nd/pacman-pkg-exists (pkg)
"Return t if PKG exists in the pacman repositories."
(nd/shell-test "pacman" "-Ss" (format "^%s$" pkg)))
(defun nd/aur-pkg-exists (pkg)
"Return t if PKG exists in the aur repositories."
;; add more commands as needed
(cond
((equal nd/aur-helper "yay")
(let ((out (->> (format "yay -Ssaq %s" pkg)
(shell-command-to-string)
(s-split "\n"))))
(and (member pkg out) t)))
(t
(warn "No aur helper found"))))
(defun nd/invalid-pacman-pkgs ()
"Return pacman and aur packages that don't exist."
(-let* (((pacman aur) (nd/pacman-dependencies t))
(ps (-remove #'nd/pacman-pkg-exists pacman))
(as (-remove #'nd/aur-pkg-exists aur)))
(list ps as)))
(defun nd/warn-invalid-pacman-deps ()
"Warn user of any invalid pacman/aur packages in this config."
(-let (((pacman aur) (nd/invalid-pacman-pkgs)))
(--each pacman
(message "Pacman package does not exist in any configured repo: %s" it))
(--each aur
(message "Pacman package does not exist in AUR repo: %s" it))))
(defun nd/install-arch-dependencies ()
"Install all missing pacman/aur dependencies."
(cl-flet
((try-install
(what cmd args pkgs)
(if (not pkgs) (message "No %s packages to install" what)
(let ((res (if (apply #'nd/shell-test cmd (append args pkgs))
"Installed"
"Failed to install")))
(message "%s %s packages: %s" res what (s-join ", " pkgs))))))
(-let (((pacman aur) (nd/pacman-dependencies t)))
(try-install "official" "pacman" '("-S") pacman)
(if nd/aur-helper
;; TODO add MAKEFLAGS=j8 to this to make it faster?
(let ((aur-args (list "--needed" "--noconfirm" "--norebuild"
"--removemake" "-S")))
(try-install "unofficial" nd/aur-helper aur-args aur))
(message "No aur helper found")))
nil))
(defun nd/dump-arch-dependencies ()
(-let (((pacman aur) (nd/pacman-dependencies nil)))
(print (s-join "\n" (append pacman aur)))))
Setup
;; zero this out so reload won't complain about things already present
(setq nd/required-exes (ht-create #'equal)
nd/aur-helper (nd/find-aur-helper))
(nd/warn-invalid-pacman-deps)
macros
;; lovingly stolen from aaron harris
(defmacro nd/with-advice (adlist &rest body)
"Execute BODY with temporary advice in ADLIST.
Each element of ADLIST should be a list of the form
(SYMBOL WHERE FUNCTION [PROPS])
suitable for passing to `advice-add'. The BODY is wrapped in an
`unwind-protect' form, so the advice will be removed even in the
event of an error or nonlocal exit."
(declare (debug ((&rest (&rest form)) body))
(indent 1))
`(progn
,@(mapcar (lambda (adform)
(cons 'advice-add adform))
adlist)
(unwind-protect (progn ,@body)
,@(mapcar (lambda (adform)
`(advice-remove ,(car adform) ,(nth 2 adform)))
adlist))))
(defmacro nd/when-os (os &rest body)
"Execute BODY if the operating system is OS.
OS is one of those in `system-type'."
(declare (indent 1))
`(if (eq system-type ,os) (progn ,@body)
(print "Skipping OS-restricted code")))
(defmacro nd/when-not-os (os &rest body)
"Execute BODY if the operating system is not OS.
OS is one of those in `system-type'."
(declare (indent 1))
`(when (not (eq system-type ,os)) (progn ,@body)
(print "Skipping OS-restricted code")))
(defmacro nd/time-exec (&rest body)
"Measure time it takes to execute BODY."
`(let ((-time (current-time)))
,@body
(->> -time time-since float-time
(format "Run time: %.06f seconds"))))
functions
(defun nd/move-key (keymap-from keymap-to key)
"Move KEY from KEYMAP-FROM keymap to KEYMAP-TO keymap."
(define-key keymap-to key (lookup-key keymap-from key))
(define-key keymap-from key nil))
(defun nd/get-apps-from-mime (mimetype)
"Return all applications that can open a given MIMETYPE.
The list is comprised of alists where pairs are of the form (name . command)."
(let ((case-fold-search nil)
(mime-regex (->> (regexp-quote mimetype)
(format "^MimeType=.*%s;?.*$"))))
(->> (list "/usr/share/applications"
"/usr/local/share/applications"
"~/.local/share/applications")
(-filter #'f-exists-p)
(--mapcat (directory-files it t ".*\\.desktop" t))
(--map (let ((tb (f-read-text it 'utf-8)))
(when (s-match mime-regex tb)
(let ((exec (cadr (s-match "^Exec=\\(.*\\)$" tb))))
(-> (or (cadr (s-match "^Name=\\(.*\\)$" tb)) exec)
(cons exec))))))
(-non-nil))))
(defun nd/get-apps-bulk-from-mime (mimetype)
"Like `nd/get-apps-from-mime' but only includes apps that can open
multiple files at once for given MIMETYPE."
(let ((case-fold-search nil))
(seq-filter (lambda (a) (string-match ".*%[FU].*" (car a))) (nd/get-apps-from-mime mimetype))))
(defun nd/execute-desktop-command (cmd file)
"Opens FILE using CMD in separate process where CMD is from a
desktop file exec directive."
(--> (shell-quote-argument file)
(replace-regexp-in-string "%[fuFU]" it cmd t t)
(format "%s &" it)
(call-process-shell-command it)))
(defun nd/get-mime-type (file)
"Get the mime type of FILE."
(->> (shell-quote-argument file)
(format "file --mime-type -b %s")
(shell-command-to-string)
(s-trim)))
(defconst nd/device-mount-dirs
(list
(f-join "/tmp/media" (user-login-name))
(f-join "/run" "media" (user-login-name))))
(defun nd/get-mounted-directories ()
"Return list of mountpoints for active devices.
Will only consider directories in `nd/device-mount-dirs'."
(->> (-filter #'f-exists? nd/device-mount-dirs)
(-mapcat #'f-directories)
(-filter #'file-directory-p)))
(defun nd/print-args (orig-fun &rest args)
"Prints ARGS of ORIG-FUN. Intended as :around advice."
(print args)
(apply orig-fun args))
(defun nd/plist-put-append (plist prop value &optional front)
"Like `plist-put' but append VALUE to current values in PLIST for PROP.
If FRONT is t, append to the front of current values instead of the back."
(let* ((cur (plist-get plist prop))
(new (if front (append value cur) (append cur value))))
(plist-put plist prop new)))
(defun nd/plist-put-list (plist prop value &optional front)
"Like `plist-put' but append (list VALUE) to current values in PLIST for PROP.
If FRONT is t, do to the front of current values instead of the back."
(let* ((cur (plist-get plist prop))
(new (if front (append (list value) cur) (append cur (list value)))))
(plist-put plist prop new)))
(defun nd/remove-bindings (f keymap)
"Remove all bindings for function F in KEYMAP."
(--each
(where-is-internal f keymap nil nil)
(define-key keymap it nil)))
(defun nd/run-at-hour-minute (hh mm rep cmd &rest args)
"Run CMD starting at next integral HH and MM repeating every REP seconds.
For example, to run every hour at 45 minutes HH would be 1 and MM would be 45."
(let* ((m (* mm 60))
(h (* hh 3600))
(now (float-time))
(next (- (+ m (* h (ceiling (/ (- now m) h)))) now)))
(apply #'run-at-time next rep cmd args)))
interactive
(defun nd/split-and-follow-horizontally ()
"Split window horizontally and move focus."
(interactive)
(split-window-below)
(balance-windows)
(other-window 1))
(defun nd/split-and-follow-vertically ()
"Split window vertically and move focus."
(interactive)
(split-window-right)
(balance-windows)
(other-window 1))
(defun nd/switch-to-last-window ()
"Switch to most recently used window."
(interactive)
(aw-switch-to-window (get-mru-window t t t)))
(defun nd/switch-to-previous-buffer ()
"Switch the buffer to the last opened buffer."
(interactive)
(switch-to-buffer (other-buffer (current-buffer) 1)))
(defun nd/config-reload ()
"Reloads main configuration file at runtime."
(interactive)
(org-babel-load-file nd/conf-main))
(defun nd/config-visit ()
"Opens the main conf.org file (the one that really matters)."
(interactive)
(find-file nd/conf-main))
(defun nd/kill-current-buffer ()
"Kill the current buffer."
(interactive)
(kill-buffer (current-buffer)))
(defun nd/close-all-buffers ()
"Kill all buffers without regard for their origin."
(interactive)
(mapc 'kill-buffer (buffer-list)))
(defun nd/org-close-all-buffers ()
"Kill all org buffers."
(interactive)
(mapc 'kill-buffer (org-buffer-list)))
(defun nd/open-urxvt ()
"Launch urxvt in the current directory."
(interactive)
(let ((cwd (expand-file-name default-directory)))
(call-process "urxvt" nil 0 nil "-cd" cwd)))
(defun nd/open-fm ()
"Launch filemanager in the current directory."
(interactive)
(let ((cwd (expand-file-name default-directory)))
(call-process "pcmanfm" nil 0 nil cwd)))
;; (defun nd/sh-send-line-or-region (&optional step)
;; (interactive)
;; (let ((proc (get-process "*ansi-term*"))
;; pbuf min max command)
;; (unless proc
;; (let ((currbuff (current-buffer)))
;; (call-interactively #'ansi-term)
;; (switch-to-buffer currbuff)
;; (setq proc (get-process "*ansi-term*"))))
;; (setq pbuff (process-buffer proc))
;; (if (use-region-p)
;; (setq min (region-beginning)
;; max (region-end))
;; (setq min (point-at-bol)
;; max (point-at-eol)))
;; (setq command (concat (buffer-substring min max) "\n"))
;; ;; (with-current-buffer pbuff
;; ;; (goto-char (process-mark proc))
;; ;; (insert command)
;; ;; (move-marker (process-mark proc) (point)))
;; ;;pop-to-buffer does not work with save-current-buffer -- bug?
;; (process-send-string proc command)
;; (display-buffer (process-buffer proc) t)
;; (when step (goto-char max) (next-line))))
user interface
The general look and feel, as well as interactive functionality
theme
This theme has good functionality for many different modes without being over-the-top or overly complex. It also comes with an easy way to set custom colors.
(use-package spacemacs-theme
:straight t
:defer t
:config
(setq spacemacs-theme-custom-colors '((lnum . "#64707c"))))
Since I run emacs in client/server mode, the loaded theme can change depending on if the client is a terminal or server (terminals have far fewer colors). This makes the theme reset when terminal is loaded before gui or vice versa.
(defvar nd/theme nil)
(defvar nd/theme-dark nil)
(defvar nd/theme-window-loaded nil)
(defvar nd/theme-terminal-loaded nil)
(defun nd/read-use-dark-mode-p ()
"Return t if flag file permits us to use dark mode."
(let ((path "/tmp/emacs-use-dark-mode"))
(when (f-exists-p path)
(equal (s-trim (f-read-text path)) "1"))))
(setq nd/theme-dark (nd/read-use-dark-mode-p))
(defmacro nd/choose-theme (light-stuff dark-stuff)
"Do LIGHT-STUFF or DARK-STUFF depending on the selected theme."
`(if nd/theme-dark ,dark-stuff ,light-stuff))
(setq nd/theme (nd/choose-theme 'spacemacs-light 'spacemacs-dark))
(setq default-frame-alist '((font . "Dejavu Sans Mono-11")))
;; required for emacsclient/daemon setup
(if (daemonp)
(add-hook 'after-make-frame-functions
(lambda (frame)
(select-frame frame)
;;(set-default-font "Dejavu Sans Mono-11")
(if (window-system frame)
(unless nd/theme-window-loaded
(if nd/theme-terminal-loaded
(enable-theme nd/theme)
(load-theme nd/theme t))
(setq nd/theme-window-loaded t))
(unless nd/theme-terminal-loaded
(if nd/theme-window-loaded
(enable-theme nd/theme)
(load-theme nd/theme t))
(setq nd/theme-terminal-loaded t)))))
(progn
(load-theme nd/theme t)
(if (display-graphic-p)
(setq nd/theme-window-loaded t)
(setq nd/theme-terminal-loaded t))))
frame
(setq frame-title-format
'(multiple-frames "%b" ("" invocation-name "@" system-name)))
modeline
This modeline goes along with the spacemacs-theme
. It also has nice integration with evil-mode
(see keybindings below).
(use-package spaceline
:straight t
:config
(require 'spaceline-config)
(setq powerline-default-separator 'arrow
spaceline-buffer-size-p nil
spaceline-buffer-encoding-abbrev-p nil)
(spaceline-spacemacs-theme))
(line-number-mode 1)
(column-number-mode 1)
delight
I like to keep the modeline clean and uncluttered. This package prevents certain mode names from showing in the modeline (it also has support for use-package
through the :delight
keyword)
(use-package delight
:straight t)
remove interface bars
Emacs comes with some useless garbage by default. IMHO (in my haughty opinion), text editors should be boxes with text in them. No menu bars, scroll bars, or toolbars (and certainly no ribbons).
(defmacro nd/disable-when-bound (fun)
`(when (fboundp (function ,fun))
(,fun -1)))
(nd/disable-when-bound tool-bar-mode)
(nd/disable-when-bound menu-bar-mode)
(nd/disable-when-bound scroll-bar-mode)
startup screen
Default startup screen is silly
(setq inhibit-startup-screen t)
Instead use a dashboard, and display days until predicted death…you know, as a pick-me-up ;)
(defvar nd/user-birthday 727506000
"User date of birth in unix time")
(defvar nd/predicted-age-at-death 71.5
"Expected age that user will die.")
(defun nd/deathclock (list-size)
(let ((death-ut (-> nd/predicted-age-at-death
(* 31557600)
(+ nd/user-birthday))))
(insert (--> (float-time)
(- death-ut it)
(/ it 86400)
(round it)
(format "%s days until death" it)))))
(use-package dashboard
:straight t
:after package
:config
(setq dashboard-banner-logo-title nil
dashboard-startup-banner (no-littering-expand-etc-file-name
"dashlogo.png")
dashboard-items '(deathclock))
(add-to-list 'dashboard-item-generators '(deathclock . nd/deathclock))
(dashboard-setup-startup-hook))
windows
popup windows
Some modes like to make popup windows (eg ediff). This prevents that.
(setq pop-up-windows nil)
ace-window
This is an elegant window selector. It displays a number in the corner when activated, and windows may be chosen by pressing the corresponding number. Note that spacemacs fails to make the numbers look nice so the theme code is a workaround to make them smaller and prettier.
(defvar nd/ace-window-fg nil)
(defvar nd/ace-window-bg nil)
(setq nd/ace-window-fg (nd/choose-theme "#F6E0F9" "#292b2e"))
(setq nd/ace-window-bg (nd/choose-theme "#7A1686" "#292b2e"))
(use-package ace-window
:straight t
:config
(setq aw-background t)
(custom-set-faces `(aw-leading-char-face
((t (:foreground ,nd/ace-window-fg
:background ,nd/ace-window-bg
:height 1.0
:box nil))))))
navigation
ivy
(defun nd/ivy-swith-buffer-transformer-fn (b)
(with-current-buffer b
(-if-let (f (buffer-file-name))
(format "%-50s %s" b f)
b)))
(defvar nd/ivy-match-bg nil)
(setq nd/ivy-match-bg (nd/choose-theme "#D4C7F3" "#534573"))
(use-package ivy
:straight t
:delight
:config
(setq ivy-use-virtual-buffers nil
ivy-sort-max-size 30000
ivy-display-functions-alist
'((counsel-irony . ivy-display-function-overlay)
;; not a fan of ivy overlay since it only appears sometimes
;; (ivy-completion-in-region . ivy-display-function-overlay)
(t))
ivy-re-builders-alist
'((t . ivy--regex-ignore-order))
ivy-sort-matches-functions-alist
'((t . nil)
(ivy-switch-buffer . ivy-sort-function-buffer)
(counsel-describe-function . ivy--shorter-matches-first)
(counsel-describe-variable . ivy--shorter-matches-first)
(counsel-M-x . ivy--shorter-matches-first))
;; the initial inputs are weird and get in the way
ivy-initial-inputs-alist nil)
(ivy--alist-set 'ivy-format-functions-alist t #'ivy-format-function-line)
(ivy-configure 'ivy-switch-buffer
:display-transformer-fn #'nd/ivy-swith-buffer-transformer-fn)
(ivy-mode)
(set-face-attribute 'ivy-current-match nil :inherit 'bold :extend t :background nd/ivy-match-bg)
;;(ivy-current-match ((t (:inherit bold :extend t :background 'nd/ivy-match-bg))))
)
;; ensure counsel and swiper are loaded
(use-package counsel
:straight t)
(use-package swiper
:straight t)
avy
Allows jumping to any character in any window with a few keystrokes. Goodbye mouse :)
(use-package avy
:straight t
:config
(setq avy-background t))
cursor
This makes a nice glowy effect on the cursor when switching window focus. Very elegant way of saving time in finding where you left off.
(use-package beacon
:straight t
:delight
:init
(beacon-mode 1)
:config
(setq beacon-blink-duration 0.2
beacon-blink-delay 0.1
beacon-size 20))
(blink-cursor-mode 0)
misc
line wrap
I don't like line wrap
(set-default 'truncate-lines t)
smooth scrolling
This makes scrolling smoother
(setq scroll-conservatively 100)
imagemagick
(when (fboundp 'imagemagick-register-types)
(imagemagick-register-types))
yes-no prompt
Some prompts require literal "yes" or "no" to decide action. Life is short and I would rather not waste keystrokes typing whole words. This makes all "yes/no" prompts only require "y" or "n."
(defalias 'yes-or-no-p 'y-or-n-p)
folding
(use-package origami
:straight t
:config
;; weirdly, delight does not do this automatically
(unless (assq 'origami-mode minor-mode-alist)
(setq minor-mode-alist (cons '(origami-mode "Origami")
minor-mode-alist)))
(delight 'origami-mode "Ω" "origami"))
dired
(setq dired-free-space nil)
low-level config
General configuation for behind-the-scenes behavior
user information
(setq user-full-name "Dwarshuis, Nathan J")
autosave
Saving files continuously is actually really annoying and clutters my disk. Turn it off.
(setq make-backup-files nil)
(setq auto-save-default nil)
async
Allows certain processes to run in multithreaded manner. For things like IO this makes sense.
(use-package async
:straight t
:delight dired-async-mode
:init
(dired-async-mode 1))
file IO
Emacs will warn user when opening a file over a certain limit. Raise this to 1GB.
(setq large-file-warning-threshold 1000000000)
undo
Memory is cheap (kinda). Keep more undo entries to make up for the fact that I don't commit enough.
(setq undo-limit 1600000
undo-strong-limit 2400000)
editing
For options that specifically affect programming or editing modes
standardization
tabs and alignment
Who uses tabs in their programs? Make tabs actually equal 4 spaces. Also, allegedly I could make more money if I use spaces :)
(setq-default indent-tabs-mode nil
tab-width 4)
short column width
Alot of languages at least semi-adhere to the 80-characters-per-line rule. fci-mode
displays a line as a guide for column width.
(setq-default fill-column 80)
(use-package fill-column-indicator
:straight t
:config
(setq fci-rule-use-dashes t)
:hook
(prog-mode . fci-mode))
spell checking
Use the built-in flyspell-mode
to handle spellchecking with favorite completion engine.
(use-package flyspell-correct-ivy
:straight t
:config
(setq flyspell-correct-interface #'flyspell-correct-ivy))
This will spell-check comments in programming languages.
(add-hook 'prog-mode-hook #'flyspell-prog-mode)
(setq flyspell-issue-message-flag nil)
Since flyspell mode is enabled in so many buffers, use a short modeline alias.
(delight 'flyspell-mode "σ" "flyspell")
syntax checking
Flycheck will highlight and explain syntax errors in code and formatting. See each language below for external tools that need to be installed to make flycheck work to the fullest.
(defun nd/flycheck-executable-find (exe)
"Find EXE like `flycheck-default-executable-find' with exceptions.
These exceptions mostly apply to finding binaries in environments
where the PATH is not obvious"
(cond
;; hlint should be installed in the compiler-tools directory for each
;; GHC version; since flycheck has no idea what this means, intervene
;; manually
((equal exe "hlint")
(or (->> "stack path --compiler-tools-bin 2> /dev/null"
(shell-command-to-string)
(s-trim)
(list)
(locate-file exe))
(flycheck-default-executable-find exe)))
(t (flycheck-default-executable-find exe))))
(use-package flycheck
:straight t
:hook
(prog-mode . flycheck-mode)
:config
(setq flycheck-check-syntax-automatically '(save
idle-change
mode-enabled)
flycheck-idle-change-delay 2
flycheck-error-list-minimum-level 'warning
flycheck-executable-find #'nd/flycheck-executable-find
flycheck-navigation-minimum-level 'warning)
(delight 'flycheck-mode "ϕ" "flycheck"))
packaging
(use-package flycheck-package
:straight t
:after flycheck
:config
(eval-after-load 'flycheck '(flycheck-package-setup)))
auto completion
Company provides a dropdown of completion options. It has many backends which are configured in each language and format elsewhere.
(use-package company
:straight t
:delight "κ"
:config
(setq company-idle-delay 0
company-dabbrev-minimum-length 5
company-dabbrev-other-buffers nil
company-minimum-prefix-length 3))
auto formatting
Most languages have a plugin/command to make their code "look pretty" (usually on save). This package is a catch-all formatter for many languages that can be added as a minor mode.
(use-package format-all
:straight t
:hook ((format-all-mode . format-all-ensure-formatter))
:config
(setcdr (assoc "Haskell" format-all-default-formatters) '(fourmolu)))
(delight 'format-all-mode "α" "format-all")
parenthesis matching
This color-codes matching parenthesis. Enable pretty much everywhere.
(use-package rainbow-delimiters
:straight t
:delight
:hook
((prog-mode . rainbow-delimiters-mode)
(inferior-ess-mode . rainbow-delimiters-mode)
(ess-mode . rainbow-delimiters-mode)
(LaTeX-mode . rainbow-delimiters-mode)
(Tex-latex-mode . rainbow-delimiters-mode)))
Use pretty symbols (like lambda in lisp)
(add-hook 'prog-mode-hook #'prettify-symbols-mode)
sudo edit
Allows opening a file with sudo elevation.
(use-package sudo-edit
:straight t)
formats and languages
Elisp
Elisp can use vanilla company with no plugins
(add-hook 'emacs-lisp-mode-hook 'company-mode)
(add-hook 'emacs-lisp-mode-hook 'origami-mode)
(add-hook 'emacs-lisp-mode-hook 'dash-fontify-mode)
(use-package lispy
:straight t)
(use-package emr
:straight t)
Clojure
(nd/require-bin "lein" :pacman "leiningen")
(nd/when-bin "lein"
(use-package cider
:straight t
:hook ((cider-mode . company-mode))))
Conda
Conda is a package manager and virtual environment manager. I use it for python and R. It needs to be set up before any of the languages that use it are set up, which is why it is in its own section here.
Note when I write "conda," I really mean "mamba," which is basically the same thing but faster.
Also, this seems to have no relation to the anaconda.el
package for python.
(defconst nd/conda-home
(-> (xdg-data-home)
(f-join "mambaforge")
(f-canonical))
"Path to conda (which really means mamba) installation.")
(nd/require-bin (f-join nd/conda-home "bin" "conda") :local)
(nd/when-bin "conda"
(use-package conda
:straight t
:config
(setq
conda-anaconda-home nd/conda-home
;; this should reflect what is in condarc (which does not appear to be read
;; by this package)
conda-env-home-directory (f-join (xdg-data-home) "conda"))))
ESS (Emacs Speaks Statistics)
For me this means R but ess also supports S-plus, SAS, Stata, and other statistical black-magic languages. ESS is not part of prog-mode
so it must be added manually to hooks.
Begin rant:
R is a pain to install and maintain. I've tried various things over the years that all kinda sucked.
One way to run R is by installing it systemwide and then installing things through CRAN. This works, but then things will break everytime the systemwide binary upgrades. This would happen for most any language run like this but is especially bad for R given the number of binary dependencies required to make it (kinda) fast.
The next solution is to use something like packrat to keep all R installations in a nice, neat environment. In my experience, this is extremely slow and unusable for anything involving more than a few packages (tidyverse and friends).
Another solution (which I used for a long time) is to use pre-built docker images such as rocker and add to them as needed. This actually works really well. Just make a few pre-built images for each project and call them as needed. Unfortunately this is a giant hack that requires a super convoluted wrapper around R which reads a magic file containing the name of the docker image before calling docker run r-image etc etc etc
(details pertaining to X11 omitted for sanity and brevity here). Also, if you need to update or add even a 0.5MB package, the entire image needs to be rebuilt.
There's a solution to all this madness. Just use conda. Build an environment with R in it, activate it in Emacs, and Get Things Done (TM). No docker. No systemwide breakage. No convoluted scripts. No waiting for packrat to install 500MB because it forgot where my cache is. The only caveat is to actually using mamba
and not conda
to install things…unless you like waiting.
End rant.
Oh yes, and to get linting to work, also install r-lintr and r-styler in the conda environment. In general it seems better and less risky to install things from conda
rather than from within an R session.
(nd/when-bin "conda"
(use-package xterm-color
:straight t)
(use-package ess
:straight t
:init
(require 'ess-r-mode)
:after xterm-color
:hook
((ess-mode . flycheck-mode)
(ess-mode . company-mode)
(ess-mode . origami-mode)
(ess-mode . prettify-symbols-mode)
(ess-mode . fci-mode)
(inferior-ess-mode . company-mode)
(inferior-ess-mode . prettify-symbols-mode))
:config
(setq inferior-R-program "R"
inferior-R-args "--quiet --no-save"
ess-history-file "session.Rhistory"
ess-history-directory (substitute-in-file-name "${XDG_CONFIG_HOME}/r/")
;; this plays more nicely with r-styler (seeing as it was designed
;; for RStudio by the people who maintain RStudio
ess-style 'RStudio
;; always start from the current file
ess-startup-directory 'default-directory))
;; Workaround for https://github.com/emacs-ess/ESS/issues/1193
(defun my-inferior-ess-init ()
(add-hook 'comint-preoutput-filter-functions #'xterm-color-filter -90 t)
(setq-local ansi-color-for-comint-mode nil))
(add-hook 'inferior-ess-mode-hook #'my-inferior-ess-init))
;; ;; fast compile
;; (defun nd/ess-r-add-env (orig-fun inf-buf proc-name start-args)
;; (let ((process-environment (cons "MAKEFLAGS=-j8" process-environment)))
;; (funcall orig-fun inf-buf proc-name start-args)))
;; (defun nd/ess-r-start-env (orig-fun &rest args)
;; (nd/with-advice
;; ((#'inferior-ess--start-process :around #'nd/ess-r-add-env))
;; (apply orig-fun args)))
;; (advice-add #'run-ess-r :around #'nd/ess-r-start-env)
C
(defun nd/init-c-company ()
"Set the company backends for anaconda mode."
(setq-local company-backends '(company-c-headers
company-dabbrev-code
company-irony)))
;; requires clang (duh)
(nd/require-bin "clang")
(nd/when-bin "clang"
(use-package flycheck-clang-analyzer
:straight t
:after flycheck
:config
(flycheck-clang-analyzer-setup)))
;; requires cmake/llvm
(nd/require-bin "cmake")
(nd/when-bin "cmake"
(use-package irony
:straight t
:hook ((irony-mode . irony-cdb-autosetup-compile-options)))
(use-package company-irony
:straight t))
(use-package company-c-headers
:straight t)
(use-package c-eldoc
:straight t)
(use-package c-mode
:after flycheck
:hook ((c-mode . company-mode)
(c-mode . irony-mode)
(c-mode . c-turn-on-eldoc-mode)
(c-mode . nd/init-c-company)))
Python
inferior shell
I don't really use elpy, but it has really nice inferior process commands, so import but don't call elpy-enable
.
(use-package elpy
:straight t)
anaconda, ipython, and flycheck
Anaconda is much lighter and easier than elpy. Also use ipython instead of the built-in shell. (Note this requires ipython to be installed externally).
Note that all my checkers/formatters/interactive shells are managed through conda
rather than installed system-wide (not to be confused with anaconda
above).
(defun nd/init-anaconda-company ()
"Set the company backends for anaconda mode."
(setq-local company-backends '(company-anaconda)))
(use-package python
:after flycheck
:hook ((python-mode . flycheck-mode)
(python-mode . origami-mode)
(python-mode . anaconda-mode)
(python-mode . company-mode)
(python-mode . nd/init-anaconda-company)
(python-mode . blacken-mode)
(inferior-python-mode . company-mode)
(inferior-python-mode . nd/init-anaconda-company))
:config
(progn
(nd/when-bin "conda"
(setq indent-tabs-mode nil
python-shell-interpreter "ipython"
python-shell-interpreter-args "-i --simple-prompt --quiet --no-banner"))
(nd/when-bin "conda"
(flycheck-add-next-checker 'python-flake8 'python-pylint))))
(use-package anaconda-mode
:straight t
:after python)
(use-package company-anaconda
:straight t
:after (python company anaconda))
formatting
Black is a really nice syntax formatter. It must be externally installed to work.
(nd/when-bin "conda"
(use-package blacken
:straight t))
pyenv
For isolation I use pyenv and pyenv-virtualenv. The only external addition needed to make this work is to add ${PYENV_ROOT}/shims
to PATH as well as adding a .python-version
file in the project root specifying the desired version/environment.
Note this also requires all external packages to be installed in each environement (eg ipython, black, flake8, and pylint).
;;(nd/require-bin "pyenv")
;;
;;(nd/when-bin "pyenv"
;; (use-package pyenv-mode
;; :straight t
;; :after python
;; :hook ((python-mode . pyenv-mode))
;; :init (-some--> (getenv "PYENV_ROOT")
;; (f-join it "versions")
;; (add-to-list 'exec-path it)))
;;
;; ;; resolve symlinks when setting the pyenv, otherwise we get some
;; ;; strange errors when activating a symlinked env
;; (advice-add #'pyenv-mode-full-path :filter-return #'file-truename))
Snakemake
(use-package snakemake-mode
:straight t
:hook ((snakemake-mode . format-all-mode)))
Ruby
(defun nd/init-robe-company ()
"Set the company backends for robe mode."
(setq-local company-backends '(company-robe)))
(nd/require-bin "irb" :pacman "ruby-irb")
(nd/when-bin "irb"
(use-package inf-ruby
:straight t
:hook (ruby-mode . inf-ruby-minor-mode))
(use-package robe
:straight t
:hook ((ruby-mode . robe-mode)
(roby-mode . nd/init-robe-company))))
(use-package ruby-test-mode
:straight t)
(use-package rvm
:straight t)
Haskell
stack
On Arch, all packages are dynamically linked (very bad for Haskell). The solution is to install stack via the stack-static
package through the AUR and then install all Haskell programs through stack using static linking.
major mode
The major mode package haskell-mode
is quite comprehensive and has most of what I need out of the box, including:
- syntax highlighting
- indentation
- autocompletion
- flycheck integration
- type checking/insertion/annotation
- function info
Since most of these need GHCi to run properly, I added a hook to load haskell source into GHCi when opened.
I have also found this to be much simpler and conflicting with other packages such as dante
and intero
(and probably haskell-ide-engine
and friends).
(nd/require-bin "stack" :aur "stack-static")
(nd/when-bin "stack"
(defun nd/init-haskell-company ()
"Set the company backends for haskell mode."
(setq-local company-backends
;; capf is standard completion and dabbrev provides
;; local completions in 'where' and 'let' clauses
'((company-capf company-dabbrev-code))))
;; flycheck setup (needed to get flycheck to pay attention to flags/extensions
;; in cabal files)
(use-package flycheck-haskell
:straight t
:after flycheck
:config
;; fix whatever they did here: https://github.com/flycheck/flycheck-haskell/pull/118
(setq flycheck-haskell-runghc-command
(let ((stack-exe (funcall flycheck-executable-find "stack")))
`(,stack-exe "--verbosity" "silent" "runghc" "--no-ghc-package-path" "--" "-i"
"-packageCabal"
"-packagebase"
"-packagebytestring"
"-packagecontainers"
"-packageprocess"
"-packagedirectory"
"-packagefilepath"))))
(use-package haskell-mode
:straight t
:hook ((haskell-mode . origami-mode)
(haskell-mode . company-mode)
(haskell-mode . flycheck-haskell-setup)
(haskell-mode . haskell-indentation-mode)
;; this enables better integration with the running GHCi process
;; NOTE this is NOT the same is haskell-interactive-mode which is used
;; in the repl that is launched within projects when loading files
(haskell-mode . interactive-haskell-mode)
(haskell-mode . nd/init-haskell-company)
;; camelcase is defacto for haskell
(haskell-mode . subword-mode)
;; don't use haskell-mode's builtin stylish plugin for formatting
(haskell-mode . format-all-mode))
:config
(setq haskell-interactive-popup-errors nil
;; we use stack...which counterintuitively means we set the
;; cabal build command to be stack
haskell-compile-cabal-build-command "stack build"
;; don't use stylish
haskell-stylish-on-save nil
;; use some handy suggestions
haskell-process-suggest-remove-import-lines t
haskell-process-auto-import-loaded-modules t
;; use TAGS file (requires hasktags binary to be in $PATH)
haskell-tags-on-save t))
;; this minor mode name is long and unnecessary
(delight 'interactive-haskell-mode nil "haskell")
;; unnecessary to see on the modeline
(delight 'subword-mode nil "subword"))
helper functions
Other helper functions that make haskell even more fun.
(defun nd/haskell-switch-to-process ()
"Switch to the current session buffer (after starting if it doesn't exist)."
(interactive)
(-if-let (buf (alist-get 'interactive-buffer haskell-session))
(if (-contains? (buffer-list) buf)
(pop-to-buffer buf)
(haskell-process-load-or-reload))
(haskell-process-load-or-reload)))
Dhall
Dhall is a functional/typed configuration language (bout time).
(use-package dhall-mode
:straight t
:mode "\\.dhall\\'"
:config
(setq dhall-type-check-inactivity-timeout 1))
(defun nd/dhall-toggle-type-check ()
"Turn dhall type checking on/off."
(interactive)
(let ((msg (if dhall-use-header-line "disabled" "enabled")))
(setq dhall-use-header-line (not dhall-use-header-line))
(message "Dhall type check %s" msg)))
Lua
For flycheck, install luacheck
(from AUR on Arch).
(nd/require-bin "luacheck" :aur)
(nd/when-bin "luacheck"
(use-package lua-mode
:straight t))
TeX
AUCTeX
Install auctex through emacs as this is OS independent and more automatic. Note that the Tex package libraries (eg TeXLive) still need to be installed to do anything useful.
(use-package tex
:straight auctex
:hook
((LaTeX-mode . flycheck-mode)
(LaTeX-mode . flyspell-mode)
(LaTeX-mode . fci-mode)
(LaTeX-mode . reftex-mode)
;; sync tex buffer positions to output pdf
(LaTeX-mode . TeX-source-correlate-mode))
:config
(turn-on-reftex)
(setq TeX-after-compilation-finished-functions '(TeX-revert-document-buffer)
reftex-plug-into-AUCTeX t
reftex-ref-style-default-list '("Cleveref" "Default")))
;; add cleveref support
(with-eval-after-load 'latex
(TeX-add-style-hook
"cleveref"
(lambda ()
(when (boundp 'reftex-ref-style-alist)
(add-to-list 'reftex-ref-style-alist
'("Cleveref" "cleveref" (("\\cref" ?c)
("\\Cref" ?C)
("\\cpageref" ?d)
("\\Cpageref" ?D)))))
(reftex-ref-style-activate "Cleveref")
(TeX-add-symbols
'("cref" TeX-arg-ref)
'("Cref" TeX-arg-ref)
'("cpageref" TeX-arg-ref)
'("Cpageref" TeX-arg-ref)))))
external viewers
AUCTeX can launch external viewers to show compiled documents.
(setq TeX-view-program-selection
'(((output-dvi has-no-display-manager) "dvi2tty")
((output-dvi style-pstricks) "dvips and gv")
(output-dvi "xdvi")
(output-pdf "PDF Tools")
(output-html "xdg-open")))
outline mode
folding
I like how org-mode
folds with the TAB key, so bring the same thing to AUCTeX here with outline-magic
.
(use-package outline-magic
:straight t
:after outline
:hook
((LaTeX-mode . outline-minor-mode)))
fonts
The section fonts are too big by default. Now the fonts are all kept equal with hatchet, axe, and saw :)
(setq font-latex-fontify-sectioning 'color)
auto completion
There are two backends which (kinda) complement each other. The company-math
package should privide completion for math symbols and the company-auctex
package should cover pretty much everything else.
(defun nd/init-company-auctex ()
"Set the company backends for auctex modes."
(company-mode)
(setq-local company-backends '((company-auctex-labels
company-auctex-bibs
company-auctex-macros
company-auctex-symbols
company-auctex-environments
;; company-latex-commands
company-math-symbols-latex
company-math-symbols-unicode))))
(use-package company-math
:straight t
:after (tex company)
:config
(setq company-math-allow-unicode-symbols-in-faces '(font-latex-math-face)
company-math-disallow-latex-symbols-in-faces nil))
(use-package company-auctex
:straight t
:after (tex company company-math)
:hook
((LaTeX-mode . nd/init-company-auctex)))
line wrap
I like having my lines short and readable (also easier to git). Turn on autofill here and also make a nice vertical line at 80 chars (visual-line-mode
).
(defun nd/turn-on-auto-fill-maybe ()
"Prompts user to turn on `auto-fill-mode'."
(when (y-or-n-p "Activate Auto Fill Mode? ")
(turn-on-auto-fill)))
(add-hook 'LaTeX-mode-hook #'nd/turn-on-auto-fill-maybe)
local variables
(with-eval-after-load 'tex
(add-to-list 'safe-local-variable-values
'(TeX-command-extra-options . "-shell-escape")))
BibTeX
database management
(use-package ebib
:straight t
:config
(setq ebib-autogenerate-keys t
ebib-uniquify-keys t))
citation search and insertion
Together, org-ref
and ivy-bibtex
provide a nice pipeline to search a BibTex database and insert citations.
(use-package org-ref
:straight t
:after org
:config
(setq reftex-default-bibliography (list (expand-file-name "~/BibTeX/master.bib"))
org-ref-bibliography-notes (expand-file-name "~/BibTeX/notes.org")
org-ref-default-bibliography (list (expand-file-name "~/BibTeX/master.bib"))))
(use-package ivy-bibtex
:straight t
:after ivy
:config
(setq bibtex-completion-bibliography (expand-file-name "~/BibTeX/master.bib")
bibtex-completion-library-path (expand-file-name "~/BibTeX/pdf")
bibtex-completion-pdf-field "File"
;; I want to insert citations by default
ivy-bibtex-default-action 'ivy-bibtex-insert-citation))
HTML
For flycheck, install tidy
(privides the html-tidy
binary).
(nd/require-bin "html-tidy" :pacman "tidy")
(use-package impatient-mode
:straight t
:config
(setq httpd-port 18080))
CSS
Overlays hex color codes with matching colors in certain modes like css and html. For flycheck, install stylelint
(from the AUR on Arch).
(nd/require-bin "stylelint")
(use-package rainbow-mode
:straight t)
Jinja2
(use-package jinja2-mode
:straight t
:hook
((jinja2-mode . fci-mode)))
Javascript
tabs
An exception to the rule
(setq js-indent-level 2)
inferior mode
(nd/require-bin "node" :pacman "nodejs")
(nd/when-bin "node"
;; TODO nodejs-repl might be more complete if ESS/elpy behavior is desired
(use-package js-comint
:straight t))
JSON
(use-package json-mode
:straight t
:hook (json-mode . origami-mode))
PHP
(use-package php-mode
:straight t)
markdown
Make font sizes smaller and less intrusive for headers
(use-package markdown-mode
:straight t
:hook ((markdown-mode . outline-minor-mode)
(markdown-mode . fci-mode)))
(add-hook 'markdown-mode-hook
(lambda ()
(let ((heading-height 1.15))
(set-face-attribute 'markdown-header-face-1 nil :weight 'bold :height heading-height)
(set-face-attribute 'markdown-header-face-2 nil :weight 'semi-bold :height heading-height)
(set-face-attribute 'markdown-header-face-3 nil :weight 'normal :height heading-height)
(set-face-attribute 'markdown-header-face-4 nil :weight 'normal :height heading-height)
(set-face-attribute 'markdown-header-face-5 nil :weight 'normal :height heading-height))))
(add-hook 'markdown-mode-hook #'nd/turn-on-auto-fill-maybe)
R-markdown
R-markdown is enabled via polymode, which allows multiple modes in one buffer (this is actually as crazy as it sounds). In this case, the modes are yaml, R, markdown, and others. Installing poly-R
will pull in all required dependencies.
(use-package poly-R
:straight t
:mode
(("\\.[rR]md\\'" . poly-markdown+r-mode)
("\\.[rR]md\\'" . poly-gfm+r-mode)))
YAML
(use-package highlight-indentation
:straight t
:config
(set-face-attribute 'highlight-indentation-face nil :background "gray20")
(set-face-attribute 'highlight-indentation-current-column-face
nil :background "dark slate blue"))
(use-package yaml-mode
:straight t
:hook ((yaml-mode . fci-mode)))
csv files
This adds support for csv files. Almost makes them editable like a spreadsheet. The lambda function enables alignment by default.
(use-package csv-mode
:straight t
:hook (csv-mode . (lambda () (csv-align-fields nil (point-min) (point-max)))))
Arch Linux
(use-package pkgbuild-mode
:straight t)
;; (use-package systemd
;; :straight systemd)
Unix Shell
No custom code here, but flycheck needs shellcheck
(a Haskell program).
(nd/require-bin "shellcheck" :aur "shellcheck-bin")
;;(add-to-list 'load-path (nd/expand-local-pkg-directory "essh"))
;;(require 'essh)
(use-package bats-mode
:straight t)
SQL
No custom code here, but flycheck needs sqlint
(a ruby gem).
(nd/require-bin "sqlint" :gem)
Docker
(nd/require-bin "docker")
(nd/when-bin "docker"
(use-package dockerfile-mode
:straight t))
AMPL
Code shamelessly ripped off from here. It is not in MELPA and is short enough for me to just put in a block in my config.
(defvar ampl-mode-hook nil
"*List of functions to call when entering Ampl mode.")
(defvar ampl-mode-map nil
"Keymap for Ampl major mode.")
(if ampl-mode-map
nil
(setq ampl-mode-map (make-sparse-keymap))
(define-key ampl-mode-map "\C-co" 'ampl-insert-comment))
(setq auto-mode-alist
(append
'(("\\(.mod\\|.dat\\|.ampl\\)\\'" . ampl-mode))
auto-mode-alist))
(autoload 'ampl-mode "Ampl" "Entering Ampl mode..." t)
(defconst ampl-font-lock-model-data
(list '( "\\(data\\|model\\)\\(.*;\\)" . (1 font-lock-builtin-face keep t)))
"Reserved keywords highlighting.")
(defconst ampl-font-lock-model-data-names
(append ampl-font-lock-model-data
(list '( "\\(data\\|model\\)\\(.*\\)\\(;\\)" . (2 font-lock-constant-face keep t))))
"Model and data filenames highlighting.")
(defconst ampl-font-lock-keywords-reserved
(append ampl-font-lock-model-data-names
(list '("\\(^\\|[ \t]+\\|[({\[][ \t]*\\)\\(I\\(?:N\\(?:OUT\\)?\\|nfinity\\)\\|LOCAL\\|OUT\\|a\\(?:nd\\|r\\(?:c\\|ity\\)\\)\\|b\\(?:\\(?:inar\\)?y\\)\\|c\\(?:ard\\|heck\\|ircular\\|o\\(?:eff\\|mplements\\|ver\\)\\)\\|d\\(?:ata\\|efault\\|i\\(?:ff\\|men\\|splay\\)\\)\\|e\\(?:lse\\|xists\\)\\|f\\(?:irst\\|orall\\|rom\\)\\|i\\(?:n\\(?:clude\\|dexarity\\|te\\(?:ger\\|r\\(?:val\\)?\\)\\)\\|n\\)\\|l\\(?:ast\\|e\\(?:ss\\|t\\)\\)\\|m\\(?:aximize\\|ember\\|inimize\\)\\|n\\(?:extw?\\|o\\(?:de\\|t\\)\\)\\|o\\(?:bj\\|ption\\|r\\(?:d\\(?:0\\|ered\\)?\\)?\\)\\|p\\(?:aram\\|r\\(?:evw?\\|intf\\)\\)\\|re\\(?:peat\\|versed\\)\\|s\\(?:\\.t\\.\\|et\\(?:of\\)?\\|olve\\|u\\(?:bject to\\|ffix\\)\\|ymbolic\\)\\|t\\(?:able\\|hen\\|o\\)\\|un\\(?:ion\\|til\\)\\|var\\|w\\(?:hile\\|ithin\\)\\)\\({\\|[ \t]+\\|[:;]\\)" . (2 font-lock-builtin-face keep t))))
"Reserved keywords highlighting-1.")
;; 'if' may take the forms if(i=1), if( i=1 ), if ( i=1 ), if i==1, etc.
(defconst ampl-font-lock-keywords-reserved2
(append ampl-font-lock-keywords-reserved
(list '("\\(^\\|[ \t]+\\|[({\[][ \t]*\\)\\(if\\)\\([ \t]*(\\|[ \t]+\\)" . (2 font-lock-builtin-face keep t))))
"Reserved keywords highlighting-2.")
;; 'Infinity' is another special case as it may appear as -Infinity...
(defconst ampl-font-lock-keywords-reserved3
(append ampl-font-lock-keywords-reserved2
(list '("\\(^\\|[ \t]+\\|[({\[][ \t]*\\)\\(-[ \t]*\\)\\(Infinity\\)\\([ \t]*(\\|[ \t]+\\)" . (3 font-lock-builtin-face keep t))))
"Reserved keywords highlighting-3.")
;; Built-in operators highlighting must be followed by an opening parenthesis
(defconst ampl-font-lock-keywords-ops
(append ampl-font-lock-keywords-reserved3
(list '("\\(a\\(?:bs\\|cosh?\\|lias\\|sinh?\\|tan[2h]?\\)\\|c\\(?:eil\\|os\\|time\\)\\|exp\\|floor\\|log\\(?:10\\)?\\|m\\(?:ax\\|in\\)\\|precision\\|round\\|s\\(?:inh?\\|qrt\\)\\|t\\(?:anh?\\|ime\\|runc\\)\\)\\([ \t]*(\\)" . (1 font-lock-function-name-face t t))))
"Built-in operators highlighting.")
;; Random number generation functions must be followed by an opening parenthesis
(defconst ampl-font-lock-keywords-rand
(append ampl-font-lock-keywords-ops
(list '("\\(Beta\\|Cauchy\\|Exponential\\|Gamma\\|Irand224\\|Normal\\(?:01\\)?\\|Poisson\\|Uniform\\(?:01\\)?\\)\\([ \t]*(\\)" . (1 font-lock-function-name-face t t))))
"Random number generation functions.")
;; Built-in operators with iterators must be followed by an opening curly brace
(defconst ampl-font-lock-keywords-iterate
(append ampl-font-lock-keywords-rand
(list '("\\(prod\\|sum\\)\\([ \t]*{\\)" . (1 font-lock-function-name-face t t))))
"Built-in operators with iterators.")
;; Constants, parameters and names follow the keywords param, let, set, var,
;; minimize, maximize, option or 'subject to'
(defconst ampl-font-lock-constants1
(append ampl-font-lock-keywords-iterate
(list '("\\(^[ \t]*\\)\\(display\\|let\\|m\\(?:\\(?:ax\\|in\\)imize\\)\\|option\\|param\\|s\\(?:\\.t\\.\\|et\\|ubject to\\)\\|var\\)\\([ \t]*\\)\\([a-zA-Z0-9\-_]+\\)\\([ \t]*.*[;:]\\)" . (4 font-lock-constant-face t t))))
"Constants, parameters and names.")
;; Constants may also be defined after a set specification. This does not
;; involve 'option' e.g. let {i in 1..5} x[i] := 0;
(defconst ampl-font-lock-constants2
(append ampl-font-lock-constants1
(list '("\\(^[ \t]*\\)\\(display\\|let\\|m\\(?:\\(?:ax\\|in\\)imize\\)\\|param\\|s\\(?:\\.t\\.\\|et\\|ubject to\\)\\|var\\)\\([ \t]+\\)\\({.*}\\)\\([ \t]*\\)\\([a-zA-Z0-9\-_]+\\)\\([ \t]*.*[;:]\\)" . (6 font-lock-constant-face t t))))
"Constants, parameters and names.")
;; Comments start with a hash, end with a newline
(setq comment-start "#")
(defconst ampl-font-lock-comments
(append ampl-font-lock-constants2
(list '( "\\(#\\).*$" . (0 font-lock-comment-face t t))))
"Comments.")
;; Define default highlighting level
(defvar ampl-font-lock-keywords ampl-font-lock-comments
"Default syntax highlighting level in Ampl mode.")
;; Indentation --- Fairly simple for now
;; 1) If a line ends with a semicolon, the next line is flush left
;; 2) If a line ends with a colon or an equal sign, the next line is indented.
(defun ampl-indent-line ()
"Indent current line of Ampl code."
(interactive)
(let ((position 0)
(reason nil))
(save-excursion
(beginning-of-line)
(if (bobp)
(prog1
(setq position 0)
(setq reason "top of buffer"))
(progn
(forward-line -1)
(if (looking-at ".*[:=][ \t]*$")
(prog1
(setq position tab-width)
(setq reason "previous line ends in : or ="))
(prog1
(setq position 0)
(setq reason "nothing special"))))))
(message "Indentation column will be %d (%s)" position reason)
(indent-line-to position)))
(defvar ampl-auto-close-parenthesis t
"Automatically insert closing parenthesis if non-nil.")
(defvar ampl-auto-close-brackets t
"Automatically insert closing square bracket if non-nil.")
(defvar ampl-auto-close-curlies t
"Automatically insert closing curly brace if non-nil.")
(defvar ampl-auto-close-double-quote t
"Automatically insert closing double quote if non-nil.")
(defvar ampl-auto-close-single-quote t
"Automatically insert closing single quote if non-nil.")
(defvar ampl-user-comment
"#####
## %
#####
"
"User-defined comment template." )
(defvar ampl-mode-syntax-table nil
"Syntax table for Ampl mode.")
(defun ampl-create-syntax-table ()
"Create AMPL-mode syntax table."
(unless ampl-mode-syntax-table
(setq ampl-mode-syntax-table (make-syntax-table))
(set-syntax-table ampl-mode-syntax-table)
(modify-syntax-entry ?_ "w" ampl-mode-syntax-table)
(modify-syntax-entry ?# "<" ampl-mode-syntax-table)
(modify-syntax-entry ?\n ">" ampl-mode-syntax-table)))
(defun ampl-mode ()
"Major mode for editing Ampl models."
(interactive)
(kill-all-local-variables)
(ampl-create-syntax-table)
(make-local-variable 'font-lock-defaults)
(setq font-lock-defaults '(ampl-font-lock-keywords))
(make-local-variable 'indent-line-function)
(setq indent-line-function 'ampl-indent-line)
(defun ampl-insert-comment ()
"Insert a comment template defined by `ampl-user-comment'."
(interactive)
(let ((point-a (point))
(use-comment ampl-user-comment)
point-b point-c)
(insert ampl-user-comment)
(setq point-b (point))
(goto-char point-a)
(if (re-search-forward "%" point-b t)
(progn
(setq point-c (match-beginning 0))
(replace-match ""))
(goto-char point-b))))
(setq major-mode 'ampl-mode)
(setq mode-name "Ampl")
(use-local-map ampl-mode-map)
(run-mode-hooks 'ampl-mode-hook))
GraphViz
Used for making fancy flowchart with dot.
(use-package graphviz-dot-mode
:straight t
:hook ((graphviz-dot-mode . company-mode)))
nix
(use-package nix-mode
:straight t
:mode "\\.nix\\'")
testing
buttercup
Include this so I can have the docs and indentation specs handy when writing test suites
(use-package buttercup
:straight t)
org-mode
low-level config
modules
Org has several extensions in the form of loadable modules. org-protocol
is used as a backend for external programs to communicate with org-mode
.
I used to use org-habit
which allows for repeaters that show up with colored indicators in the agenda showing how well you have 'stuck' to the habit. I found these to be too complicated to be worth it. If one doesn't care about the colored indicator (I don't) one could get the same effect with a restart deadline and a warning (eg <2112-01-01 Tue .+3d -2d>, as opposed to <2112-01-01 Tue .+2/3d> with the habit syntax). This is both easier to understand/configure and easier to program.
(org-set-modules 'org-modules '(org-protocol))
;; pull in other org files to ensure that my customizations below work on load
(require 'org-agenda)
(require 'org-protocol)
(require 'org-clock)
files
Firstly, I keep all my Org files in one place.
Secondly, I made my own variables (all the org-x-
symbols below) to group my org files together by purpose. In general this makes it much easier to keep track of them, and it forces me to stay organized with my org files rather than dump headlines wherever I wish at any given moment.
Additionally, using specialized file variables makes it much easier and faster to manage agenda views (see much further below). Rather than set org-agenda=files
globally, I scope this variable dynamically for each agenda view, which makes each view much faster to display. It also is much easier to draw sharp lines between different groups; rather than use file-level tags and/or properties (what I used to do) I can simply exclude certain files if I don't want to see a certain type of content.
(setq org-directory "~/Org"
org-x-action-files (list "general.org" "projects/*.org")
org-x-incubator-files (list "incubator.org")
org-x-capture-file "capture.org"
org-x-reference-files (list "reference/idea.org"
"reference/questions.org"
"reference/record.org"
"reference/resources.org")
org-x-endpoint-goal-file "reference/goals/endpoint.org"
org-x-lifetime-goal-file "reference/goals/lifetime.org"
org-x-survival-goal-file "reference/goals/survival.org"
org-x-daily-plan-file "plans/daily.org"
org-x-weekly-plan-file "plans/weekly.org"
org-x-quarterly-plan-file "plans/quarterly.org"
org-refile-targets '((org-x-get-action-files :maxlevel . 9)
(org-x-get-incubator-files :maxlevel . 9)
(org-x-get-endpoint-goal-file :maxlevel . 9)
(org-x-get-lifetime-goal-file :maxlevel . 9)
(org-x-get-reference-files :maxlevel . 9)))
autosave
Save all org buffers 1 minute before the hour, then commit whatever I saved.
(defun nd/org-save-all-org-buffers ()
"Save org buffers without confirmation or message (unlike default)."
(save-some-buffers t (lambda () (derived-mode-p 'org-mode)))
(when (featurep 'org-id) (org-id-locations-save)))
(defun nd/org-commit-repos ()
"Commit org git repos automatically"
(let* ((cmd (format "autogit \"%s\"" org-directory)))
(async-shell-command cmd)))
;; TODO don't hardcode this name
(add-to-list 'display-buffer-alist
'("\\*Async Shell Command\\*" display-buffer-no-window))
(nd/run-at-hour-minute 1 59 3600 #'nd/org-save-all-org-buffers)
(nd/run-at-hour-minute 1 0 3600 #'nd/org-commit-repos)
stateless configuration
org-ml
provides stateless functions for operating on org buffers.
;; (use-package org-ml
;; :straight t
;; :config
;; ;; make the match functions super fast with memoization
;; (setq org-ml-memoize-match-patterns t))
personal library
My org config became so huge that I decided to move it all to a separate library. Anything starting with org-x-
is from this library.
The advantage of doing it this way is that I can byte-compile and test independent of the other messy things in the main config. Furthermore, I can use it as a testing ground for new packages if I deem some functionality useful enough for more than just me.
(defun nd/load-and-compile (dir)
(add-to-list 'load-path dir)
(--each (directory-files dir t ".*\\.el$") (byte-recompile-file it nil 0)))
(nd/load-and-compile (nd/expand-lib-directory "either"))
(nd/load-and-compile (nd/expand-lib-directory "interval"))
(nd/load-and-compile (nd/expand-lib-directory "dag"))
(nd/load-and-compile (nd/expand-lib-directory "org-x"))
(nd/load-and-compile (nd/expand-local-pkg-directory "org-ml"))
(require 'dag)
(require 'org-x)
(require 'org-ml)
buffer interface
folding
Org 9.4 by default makes files open with the outline totally unfolded. I don't like this; it makes it seem like my laptop is screaming at me whenever I view an org file.
(setq org-startup-folded t)
Org 9.6 seemed to switch the default folding style which breaks search/replace. Change it back like so.
(setq org-fold-core-style 'overlays)
line wrap
I often write long, lengthy prose in org buffers, so use visual-line-mode
to make lines wrap in automatic and sane manner.
(add-hook 'org-mode-hook #'visual-line-mode)
(delight 'visual-line-mode nil 'simple)
indentation
By default all org content is squished to the left side of the buffer regardless of its level in the outline. This is annoying and I would rather have content indented based on its level just like most bulleted lists. This is what org-indent-mode
does.
(setq org-startup-indented t)
(delight 'org-indent-mode nil "org-indent")
special key behavior
Some nice modifiers to key behavior. These still work in evil mode (see keybindings section).
(setq org-special-ctrl-a/e t ;; in evil mode this affects what I/A do
org-yank-adjusted-subtrees t)
bullets
Replace the default stars with unicode. These are just so much better to read.
(use-package org-bullets
:straight t
:hook
(org-mode . org-bullets-mode)
:config
;; this might speed up bullet rendering at the expense of larger memory footprint
(setq inhibit-compacting-font-caches t))
font
The fonts in org headings bug me; make them smaller and less invasive.
(add-hook 'org-mode-hook
(lambda ()
(let ((heading-height 1.15))
(set-face-attribute 'org-level-1 nil :weight 'bold :height heading-height)
(set-face-attribute 'org-level-2 nil :weight 'semi-bold :height heading-height)
(set-face-attribute 'org-level-3 nil :weight 'normal :height heading-height)
(set-face-attribute 'org-level-4 nil :weight 'normal :height heading-height)
(set-face-attribute 'org-level-5 nil :weight 'normal :height heading-height))))
Org 9.4 added an extra font to "DONE" headlines. I'm not a fan, so revert to old behavior
(setq org-fontify-done-headline nil)
src blocks
Enable shortcuts for embedding code in org text bodies.
(setq org-src-window-setup 'current-window
org-src-fontify-natively t
org-edit-src-content-indentation 0
org-babel-load-languages '((emacs-lisp . t)
(org . t)))
(add-to-list 'org-structure-template-alist '("el" . "src emacs-lisp"))
todo insertion
Make todo insertion respect contents
(setq org-insert-heading-respect-content t)
flights
To remind myself to check into flights and stuff
(defun nd/org-insert-flight (arg)
"Insert a flight.
Add a prefix ARG to add check in date."
(interactive "P")
(cl-flet*
((try-until
(try-fun test-fun msg)
(let ((res))
(while (not (funcall test-fun (setq res (funcall try-fun))))
(message msg))
res))
(read-airport
(prompt)
(try-until (lambda () (read-from-minibuffer (format "%s: " prompt)))
(lambda (r) (< 0 (length r)))
"Enter a valid location (Ex. YYZ)"))
(read-date
(prompt)
(try-until (lambda () (float-time (org-read-date t t nil prompt)))
(lambda (r) (or (not r) (< 0 (- r (float-time)))))
"Enter a valid datetime in the future"))
(mk-flight-headline
(level loc1 loc2 flight-time)
(let ((contents (--> (org-ml-unixtime-to-time-long flight-time)
(org-ml-build-timestamp! it :active t)
(org-ml-build-paragraph it)
(list it)))
(title (format "%s -> %s" loc1 loc2)))
(->> (org-ml-build-headline! :level level
:title-text title
:tags (list org-x-tag-errand))
(org-x-element-headline-add-created nil)
(org-ml-headline-set-contents (org-x-logbook-config) contents))))
(mk-checkin-headline
(level loc flight-time)
(let* ((pl (--> (- flight-time (* 24 60 60))
(org-ml-unixtime-to-time-long it)
(org-ml-build-timestamp! it :active t)
(org-ml-build-planning :scheduled it)))
(title (format "check into %s flight" loc)))
(->> (org-ml-build-headline! :level level
:title-text title
:todo-keyword org-x-kw-todo
:tags (list org-x-tag-laptop))
(org-x-element-headline-add-created nil)
(org-ml-headline-set-planning pl)
(org-ml-headline-set-node-property "Effort" "0:15")))))
(let ((t1 (read-date "Depart date"))
(t2 (read-date "Return date")))
(if (< t2 t1) (error "Return time must be after depart time")
(let* ((level (or (org-ml-get-property :level (org-ml-parse-this-headline)) 1))
(loc1 (read-airport "Depart location"))
(loc2 (read-airport "Arrive location"))
(fh1 (mk-flight-headline level loc1 loc2 t1))
(fh2 (mk-flight-headline level loc2 loc1 t2))
(ch1 (when (equal arg '(4)) (mk-checkin-headline level loc1 t1)))
(ch2 (when (equal arg '(4)) (mk-checkin-headline level loc2 t2))))
(->> (list ch1 fh1 ch2 fh2)
(-non-nil)
(org-ml-insert (1+ (org-end-of-subtree)))))))))
table of contents
Since I use org mode as my config file, makes sense to have a table of contents so others can easily naviagate this crazy empire I have created :)
(use-package toc-org
:straight t
:hook
(org-mode . toc-org-mode))
column view
Set org columns view to be more informative with clocksums and effort.
(setq org-columns-default-format
(s-join
" "
(list "%25ITEM"
"%4TODO"
"%TAGS"
"%5Effort(EFFRT){:}"
"%5CLOCKSUM(CLKSM){:}"
"%ALLOCATE(ALLOC)"))
;; this is really just meant for the meeting view but I can't figure
;; out how to make it only apply to one agenda view
org-columns-default-format-for-agenda
(s-join
" "
(list "%25ITEM"
"%SCHEDULED(DATE)"
"%5Effort(EFF)")))
(set-face-attribute 'org-column nil :background "#1e2023")
navigation
Some common functions that I use often that don't seem to exist
(defun nd/org-goto-last-child-headline ()
"Go to the last child headline under the current headline."
(interactive)
(-if-let (level (-some->> (org-ml-parse-this-headline)
(org-ml-get-property :level)
(1+)))
(progn
(org-show-children)
(org-end-of-subtree)
(org-back-to-heading)
(while (< level (org-current-level))
(org-up-heading-safe)))
(message "Not on a headline")))
calfw
This is a nifty calendar. Sometimes it is way faster than the agenda buffer for looking at long term things.
(use-package calfw
:straight t
:config
(setq cfw:fchar-junction ?╋
cfw:fchar-vertical-line ?┃
cfw:fchar-horizontal-line ?━
cfw:fchar-left-junction ?┣
cfw:fchar-right-junction ?┫
cfw:fchar-top-junction ?┯
cfw:fchar-top-left-corner ?┏
cfw:fchar-top-right-corner ?┓))
(use-package calfw-org
:straight t
:after calfw
:config
(setq cfw:org-agenda-schedule-args
'(:deadline* :scheduled* :timestamp)))
exporting
latex to pdf command
Use latexmk
instead of pdflatex
as it is more flexible and doesn't require running the process zillion times just to make a bibliography work. Importantly, add support here for BibTeX as well as the custom output directory (see below).
(setq org-latex-pdf-process
'("latexmk -output-directory=%o -shell-escape -bibtex -f -pdf %f"))
custom output directory
By default org export files to the same location as the buffer. This is insanity and clutters my org directory with .tex
and friends. Force org to export to a separate location.
(defvar nd/org-export-publishing-directory
(expand-file-name "org-exports" (getenv "XDG_CACHE_HOME"))
"The target directory to for all org exports.")
(defun nd/org-export-output-file-name (orig-fun extension &optional subtreep pub-dir)
"Change the target export directory for org exports."
(unless pub-dir
(setq pub-dir nd/org-export-publishing-directory)
(unless (file-directory-p pub-dir)
(make-directory pub-dir)))
(apply orig-fun extension subtreep pub-dir nil))
(advice-add 'org-export-output-file-name :around #'nd/org-export-output-file-name)
html5
The default is XHTML for some reason. Use the much-superior html5.
(setq org-html-doctype "html5")
project management
TaskJuggler is software that is most likely used by some super-intelligent alien species to plan their invasions of nearby planets and develop sophisticated means of social control.
Basically it is really complicated and powerful. For now I use it to make cute gantt charts.
Taskjuggler is provided by an external package that provides the command line tools (available in the AUR for Arch Linux). Org-mode has "native" export support through a contrib module. I maintain a separate package with extra functions with taskjuggler web interface support in a separate package loaded here.
;; (require 'ox-taskjuggler)
;; from here: https://www.skamphausen.de/cgi-bin/ska/taskjuggler-mode.el
;;(add-to-list 'load-path (nd/expand-local-pkg-directory "taskjuggler"))
;;(require 'taskjuggler-mode)
;; nice and short :)
;;(setq org-tj-report-tag "τrep"
;; org-tj-project-tag "τprj"
;; org-tj-resource-tag "τres")
;; my own package
;;(add-to-list 'load-path (nd/expand-local-pkg-directory "org-tj"))
;;(require 'org-tj)
;; force org to listen to the ORDERED property
;;(setq org-enforce-todo-dependencies nil)
;;
;;(setq org-tj-valid-report-attributes
;; '(headline columns definitions timeformat hideresource
;; hidetask loadunit sorttasks formats period header center))
gtd implementation
overview
This section is meant to be a big-picture overview of how GTD works in this setup. For specifics, see each section following this for further explanation and code. I should also say that most of the ideas for the code came from Bernt Hansen's very detailed guide.
workflow
GTD as described in its original form is divided into five steps as explained further below. Here I attempt to explain how I implement each of these into org-mode
.
collect
The whole point of GTD is to get stuff out of one's head, and this is purpose of the collect step. Basically if a thought or task pops in my head or interrupts me, I record it somewhere. These thoughts can happen any time and anywhere, so it is important to keep them out of consciousness so that I can concentrate on whatever I am doing.
When org-mode
is in front of me, I use org-capture
(see below for org-capture-templates
). The "things" that could be collected include anything from random ideas, things I remember to do, appointments I need to attend, etc. I also capture emails with mu4e
(which links to org-mode
through org-protocol
). Everything collected with org-capture
gets sent to a dedicated file where I deal with it later (see process step).
When org-mode
is not in front of me, I record my thoughts in the Orgzly app on my phone. It doesn't sync the way I want so I transfer everything manually.
process
Collecting only records things; it doesn't make decisions. The point of the process step is to decide if the task/note is worth my time and when. This involves several key questions.
The first question to ask is if the task is actionable. If yes, it gets moved to a project file or a general task file. If not, I ask it can either be moved to the "incubator" (a place for things I might do), be moved any number of reference files (for storing inportant information), or flat-out deleted if I think it is stupid or no longer relevant.
In org-mode
these decisions are made and recorded by moving headlines between files with org-refile
. To facilitate this process I have an agenda view to filter out captured tasks. From there it is easy to refile to wherever the headers need to go.
This step happens daily along with organize below.
organize
The organize step is basically the second half of the process step (I honestly think of these as a single task because that's how they are implemented in org-mode
, but the original GTD workflow describes them seperately).
After refiling with org-refile
, the next step is to add any remaining meta information to each task, which is later used to decide what to do and when. This information includes context, effort, delegation, and timestamps. In the case of projects this also includes choosing a NEXT tasks if one hasn't been chosen already.
Delegation (assingning something to someone else) is simple and is represented by a simple property which is filled with the initials of the person doing the work. It filter and view this with org-columns
and org-agenda-columns
.
When tasks don't have a specific date, GTD outlines a four-criteria model for deciding what to do: context, required attention, available energy, and priority. Context describes required locations and resources for tasks, and I represent them with tags (see org-tags-alist
). Required attention is represented by the Effort
property (see org-default-properties
below). Available energy is subjective and not represented in org-mode
. Priority is again represented with tags, here chosen from one of seven "life categories."
In assigning timestamps, org-mode
offers several possibilities out of the box. Putting a plain active timestamp denotes an appointment (something at which I need to show up). A scheduled timestamp denotes a task that I want to work on starting at a certain time. A deadline denotes a task that must be finished by a certain time. I try to only use these for "hard" times as anything "soft" risks me not fulfilling to the timestamp and hence diminishing the value of timestamps in general.
I have three main agenda views for handling this. The first is a daily view that shows the tasks needed for today, including anything with a timestamp. The second has all tasks that are not timestamps (eg things that can be done at any time). The third is a project view that shows the top level headline for collections of tasks (this is where I find any projects that need a NEXT task).
The organize step may seem like it requires too much work but luckily org-mode
allows enough automation that some of this meta information can be added in the collect and process phases. For instance, timestamps and tags can be added (forcibly) in org-capture
depending on what template is used. Furthermore, the priority tag and some context tags are added when the task is refiled to its proper file or project; this happens via tag inheritance, defined at either the file level or a parent headline (for instance, a computer-related tasks may be filed under environmental/computer
where environment
has the _env
tag and computer
has the #laptop
tag).
review
In order to keep the entire workflow moving smoothly, it is necessary to do a high-level review.
This happens weekly and involves several things.
- Scheduling important tasks and resolve conflicts. For this I use
calfw
(basically a calendar) to look at the next week and check if anything overlaps and move things around. I also "reload" repeater tasks usingorg-x-clone-subtree-with-time-shift
. - Moving tasks to the archive as they are available. This keeps
org-mode
fast and uncluttered. - Reviewing the incubator and moving tasks out that I actually decide to do.
- Reviewing reference material and moving it to appropriate tasks.
- Assessing projects based on their status (see below for the definition of "status"). Ideally all projects are "active," and if they are not I try to make them active by assigning NEXT.
- Reviewing inert tasks and projects (eg those with no recent activity) and moving them to the incubator if I don't deem them worthy of immediate attention (see below for definition of "inert").
I have specialized agenda views and commands for facilitating all of this.
execute
Execute involves doing the predefined work laid out in the previous four steps. Generally I work through two agenda views (in order). The first being all my tasks that need to get done in the day, and the second being all tasks with no specific timestamp.
Besides physically doing the tasks here, the other special thing in org-mode
that I use is clocking. If a clock is running on a headline, it means I'm paying attention to whatever that headline represents. This implies that multitasking isn't allowed, which is bad idea in general.
file hierarchy and structure
All org files are kept in one place (see org-directory
). This is futher subdivided into directories for project (as per terms and definitions, these are any tasks that involve at least on subtask) and reference files. At the top level are files for incubated tasks, captured tasks, and catchall general tasks (which also includes small projects that don't fit anywhere else).
In order to make sorting easier and minimize work during processing, the files are further subdivided using tags at the file level and headline level that will automatically categorize tasks when they are refiled to a certain location. For example, some project may be to create a computer program, so I would set #+FILETAGS: #laptop
because every task in this project will require a laptop. See the tags section below for more information on tags.
repetition
This deserves special attention because it comprises a significant percentage of tasks I do (and likely everyone does). I personally never liked the org's repeated task functionality. It is way too temporally rigid to be useful to me, and offers very little flexibility in mutating a task as it moves forward. Habits don't fix this problem despite appearing to be more flexible.
My (somewhat convoluted) solution was to use org-clone-subtree-with-time-shift
, which creates an easy way to make repeated tasks from some template, but also allows modification. The only problem with the vanilla implementation is that it lacks automation and agenda-block awareness (they all get treated as regular tasks which I don't want). This is partially fixed with my own org-x-clone-subtree-with-time-shift
which automaticlly resets tasks which are cloned (eg clearing checkboxes and resetting todo state). The remainding problems I fixed by defining several properties to be applied to repeated groupings under a headline (see properties).
The first property is called PARENT_TYPE
and has two values iterator
and periodical
. The first applies to repeated tasks and second which applies to timestamped headlines such as appointments. These are mostly useful for agenda sorting, where I have views specifically for managing repeated tasks. The second property is TIME_SHIFT
; org-x-clone-subtree-with-time-shift
is aware of this value and automatically shifts cloned tasks accordingly if available.
In practice, I use this for tasks like workouts, paying bills, maintenance, grocery shopping, work meetings, GTD reviews, etc. These are all almost consistent but may change slightly in their timing, action items, effort, context, etc. If any of these change, it is easy enough to modify one headline without disrupting the rest.
In an org tree these look like this:
`***** clean room
:PROPERTIES:
:PARENT_TYPE: iterator
:TIME_SHIFT: +1m
:END:
`****** DONE clean room [0/2]
CLOSED: [2018-11-21 Wed 22:13] SCHEDULED: <2018-10-29 Mon>
:PROPERTIES:
:Effort: 0:15
:END:
- [ ] vacuum
- [ ] throw away trash
`****** TODO clean room [0/2]
SCHEDULED: <2018-11-29 Thu>
:PROPERTIES:
:Effort: 0:30
:END:
- [ ] vacuum room
- [ ] throw away trash
block agenda views
The heart of this implementation is an army of block agenda views (basically filters on the underlying org trees that bring whatever I need into focus). These have become tailored enough to my workflow that I don't even use the built-in views anymore (I also have not found an "easy" way to turn these off). Besides projects, these agenda views are primarily driven using skip functions.
projects
When it comes to the agenda view, I never liked how org-mode by default handled "projects" (see how that is defined in "terms and definitions"). It mostly falls short because of the number of todo keywords I insist on using. The solution I implemented was to used "statuscodes" (which are just keywords in lisp) to define higher-level descriptions based on the keyword content of a project. For example a "stuck" project (with statuscode :stuck
) is a project with only TODO
keywords. Adding a NEXT
status turns the statuscode to :active
. Likewise WAIT
makes :waiting
. This seems straightforward, except that NEXT
trumps WAIT
, WAIT
trumps HOLD
, etc. Furthermore, there are errors I wish to catch to ensure subtrees get efficiently cleaned out, such as a project headline with DONE
that still has a TODO
underneath.
For a full overview of how these statuscodes are implemented, see org-x-headline-get-project-status
.
repeaters
Similarly to projects, repeaters (eg iterators and periodicals) are assessed via a statuscode (after all they are a group of headlings and thus depending on the evaluation of todo keywoards and timestamps in aggregate). These prove much simpler than projects as essentially all I need are codes for uninitialized (there is nothing in the repeater), empty (all subheadings are in the past and therefore irrelevant), and active (there are some subtasks in the future).
See org-x-headline-get-iterator-status
and org-x-headline-get-periodical-status
for how these statuscodes are implemented.
tasks
Tasks are mostly just defined by their todo keyword (or lack of one). Like projects and repeaters, I use statuscodes to do the agenda filtering, which is necessary to concisely keep track of not only the keywords but the timestamps and logbook status (for example, and "archivable" task is one that was completed a while ago and thus is ready to be archived).
See org-x-headline-get-task-status
for this implementation.
terms and definitions
These conventions are used throughout to be precise when naming functions/variables and describing their effects
headlines
- headline: the topmost part after the bullet in an org outline. Org-mode cannot seem to make up it's mind in calling it a header, heading, or headline, so I picked headline
- todoitem: any headline with a todo keyword
-
task: a todoitem with no todoitem children
- atomic: a task is not part of a project
-
project: a todoitem with that has todoitem children or other projects
- toplevel: a project that has no parents that have todo items
time
-
stale: headlines with timestamps that are in the past/present
- archivable: like stale but further specifies the timestamp is older than a cutoff that defines when tasks can be archived (usually 30 days)
- fresh: headlines with timestamps that are in the future
- inert: tasks that have not had a recent clock or logbook entry, see
org-x-headline-headline-is-inert-p
todo states
sequences
These keywords are used universally for all org files. Note that projects have a more specific meaning for these keywords in defining project status (see org-x-headline-get-project-status
).
In terms of logging, I like to record the time of each change upon leaving any state, and I like recording information in notes when waiting, holding, or canceling (as these usually have some external trigger or barrier that should be specified).
(setq org-todo-keywords
`((sequence
;; default undone state
,(format "%s(t/!)" org-x-kw-todo)
;; undone but available to do now (projects only)
,(format "%s(n/!)" org-x-kw-next) "|"
;; done and complete
,(format "%s(d/!)" org-x-kw-done))
(sequence
;; undone and waiting on some external dependency
,(format "%s(w@/!)" org-x-kw-wait)
;; undone but signifies tasks on which I don't wish to focus at the moment
,(format "%s(h@/!)" org-x-kw-hold) "|"
;; done but not complete
,(format "%s(c@/!)" org-x-kw-canc))))
colors
Aesthetically, I like all my keywords to have bold colors.
(setq
org-todo-keyword-faces
(-let (((todo next done wait hold canc)
(nd/choose-theme
'("indian red" "dark goldenrod" "forest green" "dark orange" "dark violet" "DodgerBlue1")
'("light coral" "khaki" "light green" "orange" "violet" "deep sky blue"))))
`((,org-x-kw-todo :foreground ,todo :weight bold)
(,org-x-kw-next :foreground ,next :weight bold)
(,org-x-kw-done :foreground ,done :weight bold)
(,org-x-kw-wait :foreground ,wait :weight bold)
(,org-x-kw-hold :foreground ,hold :weight bold)
(,org-x-kw-canc :foreground ,canc :weight bold))))
links and IDs
IDs and links are useful for meetings where I either reference tasks to discuss or reference action items to do in the future.
(setq org-id-link-to-org-use-id t)
tags
alist
I use tags for agenda filtering (primarily for GTD contexts, see below). Each tag here starts with a symbol to define its group (note, only the special chars "_", "@", "#", and "%" seem to be allowed; anything else will do weird things in the hotkey prompt). Some groups are mutually exclusive. By convention, any tag not part of these groups is ALLCAPS (not very common) and set at the file level.
(setq org-tag-alist
;; gtd location context
`((:startgroup)
(,org-x-tag-errand . ?e)
(,org-x-tag-home . ?h)
(,org-x-tag-work . ?w)
(,org-x-tag-travel . ?r)
(:endgroup)
;; gtd resource context
(,org-x-tag-laptop . ?l)
(,org-x-tag-phone . ?p)
;; misc tags
;; deep work (TM)
(,org-x-tag-deep . ?d)
;; denotes reference information
(,org-x-tag-note . ?n)
;; incubator (the someday/maybe list)
(,org-x-tag-incubated . ?i)
;; maybe (for things I might want to do, to be used with
;; `org-x-tag-incubated')
(,org-x-tag-maybe . ?m)
;; denotes tasks that need further subdivision to turn into true project
(,org-x-tag-subdivision . ?s)
;; catchall to mark important headings, usually for meetings
(,org-x-tag-flagged . ?f)
;; a tag to make meetings suck less
(,org-x-tag-meeting . ?g)
;; life categories, used for gtd priorities
(:startgroup)
(,(org-x-life-category-tag 'env) . ?E)
(,(org-x-life-category-tag 'fin) . ?F)
(,(org-x-life-category-tag 'int) . ?I)
(,(org-x-life-category-tag 'met) . ?M)
(,(org-x-life-category-tag 'phy) . ?H)
(,(org-x-life-category-tag 'pro) . ?P)
(,(org-x-life-category-tag 'rec) . ?R)
(,(org-x-life-category-tag 'soc) . ?S)
(:endgroup)))
colors
Each group also has its own color, defined by its prefix symbol.
(let ((grouped-tags (->> (-map #'car org-tag-alist)
(-filter #'stringp)
(--group-by (elt it 0)))))
(cl-flet
((add-foreground
(prefix color)
(->> (alist-get prefix grouped-tags)
(--map (list it :foreground color)))))
(setq org-tag-faces
(-let (((loc res misc cat)
(nd/choose-theme
'("medium sea green" "steel blue" "dark goldenrod" "dark violet")
'("PaleGreen" "SkyBlue" "PaleGoldenrod" "violet"))))
(append
(add-foreground org-x-tag-location-prefix loc)
(add-foreground org-x-tag-resource-prefix res)
(add-foreground org-x-tag-misc-prefix misc)
(add-foreground org-x-tag-category-prefix cat))))))
properties
The built-in effort
is used as the fourth and final homonymous GTD context (the other three being covered above using tags). It is further restricted with Effort_All
to allow easier filtering in the agenda.
Also here are the properties for repeateders and routine types.
(setq org-default-properties (->> (list org-x-prop-parent-type
org-x-prop-time-shift
org-x-prop-thread
org-x-prop-location
org-x-prop-routine
org-x-prop-created
org-x-prop-expire
org-x-prop-days-to-live
org-x-prop-goal)
(-union org-default-properties))
org-use-property-inheritance (list org-x-prop-parent-type
org-x-prop-time-shift
org-x-prop-goal))
(let ((effort-choices (list "0:05" "0:15" "0:30" "1:00" "1:30" "2:00" "3:00"
"4:00" "5:00" "6:00"))
(parent-type-choices (list org-x-prop-parent-type-periodical
org-x-prop-parent-type-iterator))
(routine-choices (list org-x-prop-routine-morning
org-x-prop-routine-evening)))
(cl-flet
((def-choices
(prop options &optional allow-other)
(let ((options* (if allow-other (-snoc options ":ETC") options)))
(cons (format "%s_ALL" prop) (s-join " " options*)))))
(setq org-global-properties
(list (def-choices org-x-prop-parent-type parent-type-choices)
(def-choices org-effort-property effort-choices t)
(def-choices org-x-prop-routine routine-choices)))))
capture
templates
As per Bernt's guide, capture is meant to be fast. The dispatcher is bound to F2
(see keybindings section) which allows access in just about every mode and brings a template up in two keystrokes.
NOTE: Capitalized entries store a link to the capture along with writing to the capture file. The :x-autolink
is a non-standard key that I interpret in a hook.
(defun nd/org-timestamp-future (days)
"Inserts an active org timestamp DAYS after the current time."
(format-time-string (org-time-stamp-format nil)
(time-add (current-time) (days-to-time 1))))
(let* ((capfile "~/Org/capture.org")
(todo-options `(entry (file ,capfile) "* %(eval org-x-kw-todo) %^{Task Name}\n%?"))
(deadline-options `(entry (file ,capfile)
,(concat "* %(eval org-x-kw-todo) %^{Deadline Name}\n"
"DEADLINE: %^t\n%?"))))
(setq org-capture-templates
;; regular TODO task
`(("t" "todo" ,@todo-options)
("T" "todo (store link)" ,@todo-options :x-autolink t)
;; for useful reference information that may be grouped with tasks
("n" "note" entry (file ,capfile)
"* %^{Note Title}\n%?")
;; for non-actionable events that happen at a certain time
("a" "appointment" entry (file ,capfile)
,(concat "* %^{Apt Title}\n"
"%^t\n%?"))
;; like appointment but multiple days
("s" "appointment-span" entry (file ,capfile)
,(concat "* %^{Apt Title}\n"
"%^t--%^t\n%?"))
;; task with a deadline
("d" "deadline" ,@deadline-options)
("D" "deadline (store link)" ,@deadline-options :x-autolink t)
;; for converting mu4e emails to tasks, defaults to next-day deadline
("e" "email" entry (file ,capfile)
,(concat "* %(eval org-x-kw-todo) Respond to %:fromaddress; Re: %:subject\t:%(eval org-x-tag-laptop):\n"
"DEADLINE: %(nd/org-timestamp-future 1)\n"
"%a\n"))
;; make meetings suck less
("m" "meeting" entry (file ,capfile)
,(concat "* TODO %^{Meeting Title}\t:%(eval org-x-tag-meeting):\n"
"SCHEDULED: %^t\n"
"%^{Effort}p"
"attendees:\n\n"
"notes:%?"))
;; because everyone needs a hill to climb
("g" "goal" entry (file ,capfile)
,(concat "* TODO %^{Goal title}\n"
"why? %?\n"
"what?"))
;; for capturing web pages with web browser
("p" "org-protocol" entry (file ,capfile)
,(concat "* %^{Title}\t:%(eval org-x-tag-note):\n"
"#+BEGIN_QUOTE\n"
"%i\n"
"#+END_QUOTE")
:immediate-finish t)
;; or capturing links with web browser
("L" "org-protocol link" entry (file ,capfile)
,(concat "* %^{Title} :%(eval org-x-tag-note):\n"
"[[%:link][%:description]]")
:immediate-finish t))))
insert mode
To save one more keystroke (since I use evil mode), trigger insert mode upon opening capture template.
(add-hook 'org-capture-mode-hook (lambda () (evil-append 1)))
autolink
In some capture templates I want to automatically store a link to the entry so I can use it later. This can be done using one the the capture-finalize hooks and simply running org-store-link
on the capture (note this only makes sense for headlines).
(add-hook 'org-capture-before-finalize-hook
(lambda ()
(when (org-capture-get :x-autolink)
(save-excursion
(org-back-to-heading)
(call-interactively #'org-x-id-store-link)))))
creation time
Add the creation time upon completing a capture.
(add-hook 'org-capture-before-finalize-hook
(lambda (&optional _always &rest _args)
(org-id-get-create)
(org-x-set-creation-time)))
refile
Refile (like capture) should be fast, and I search all org file simultaneously using ivy
(setting org-outline-path-complete-in-steps
to nil
makes search happen for entire trees at once and not just the current level). Refiling is easiest to do from a block agenda view (see below) where headings can be moved in bulk.
(setq org-refile-use-outline-path 'file
org-outline-path-complete-in-steps nil
org-refile-allow-creating-parent-nodes 'confirm
org-indirect-buffer-display 'current-window)
Prevent accidental refiling under tasks with done keywords
(setq org-refile-target-verify-function
(lambda () (not (member (nth 2 (org-heading-components)) org-done-keywords))))
;; TODO this no work, although does work if var is global
;; redfining the targets works for now
;; (add-hook 'org-agenda-mode-hook
;; (lambda ()
;; (when (equal (buffer-name) "*Org Agenda(A)*")
;; (setq-local org-refile-targets
;; '(("~/Org/journal/goals.org" :maxlevel . 9))))))
;; (lambda () (when (org-entry-get nil "GOAL") t))))))
;; (setq org-refile-targets '((nil :maxlevel . 9)
;; ("~/Org/reference/idea.org" :maxlevel . 9)
;; ("~/Org/journal/goals.org" :maxlevel . 9)
;; (org-agenda-files :maxlevel . 9))
clocking
general
Clocking = attention. If a task has a running clock, I pay attention to it (or at least that's the idea). I bound F4
to org-clock-goto
as an easy way to find my current/last clocked task in any mode (see keybindigs).
(setq org-clock-history-length 23
org-clock-out-when-done t
org-clock-persist t
org-clock-report-include-clocking-task t)
modeline
The modeline is a nice place to indicate if something is clocked in or out. Unfortunately, sometimes is is so crowded that I can't see the text for the currently clocked task.
Solution: flashy colors.
(defvar nd/spaceline-clock-fg nil)
(defvar nd/spaceline-clock-bg nil)
(setq nd/spaceline-clock-fg (nd/choose-theme "#3E3D31" "#3E3D31"))
(setq nd/spaceline-clock-bg (nd/choose-theme "#88F918" "#66cd00"))
(defface nd/spaceline-highlight-clocked-face
`((t (:background ,nd/spaceline-clock-bg
:foreground ,nd/spaceline-clock-fg
:inherit 'mode-line)))
"Default highlight face for spaceline.")
(defun nd/spaceline-highlight-face-clocked ()
"Set the spaceline highlight color depending on if the clock is running."
(if (and (fboundp 'org-clocking-p) (org-clocking-p))
'nd/spaceline-highlight-clocked-face
'spaceline-highlight-face))
(setq spaceline-highlight-face-func 'nd/spaceline-highlight-face-clocked)
aggregation
Org mode has no way of detecting if conflicts exist. It also has no way of alerting someone if they have overbooked their schedule.
The main code is defined in org-x
so the following is only to set some domain-specific options.
(setq org-x-agg-filtered-files '("incubator" "peripheral")
org-x-agg-filtered-keywords (list org-x-kw-canc org-x-kw-done))
logging
drawer
I prefer all logging to go in a seperate drawer (aptly named) which allows easier navigation and parsing for data analytics.
(setq org-log-into-drawer "LOGGING"
org-clock-into-drawer "CLOCKING")
events
Events are nice to record because it enables tracking of my behavior (eg how often I reschedule, which may indicate how well I can predict when things should happen).
(setq org-log-done 'time
org-log-redeadline 'time
org-log-reschedule 'time)
repeated tasks
In these cases, it is nice to know what happened during each cycle, so force notes.
(setq org-log-repeat 'note)
created time
Override the standard headline insertion function to add a timestamp for the time at which it was created.
(advice-add 'org-insert-heading :after
(lambda (&optional _always &rest _args)
(org-id-get-create)
(org-x-set-creation-time)))
agenda
appearence
sticky agendas
I personally like having sticky agendas by default so I can use multiple windows
(setq org-agenda-sticky t)
tag alignment
Make tags appear on the right side of the screen.
(setq org-agenda-tags-column 'auto)
time grid
(let ((intervals (->> (--unfold (when (<= it 20) (cons it (1+ it))) 8)
(--map (* 100 it)))))
(setq org-agenda-time-grid
`((daily today require-timed remove-match)
,intervals
"......" "----------------")))
prefix format
This controls what each line on the block agenda looks like. This is reformated to include effort and remove icons.
(setq org-agenda-prefix-format
'((agenda . " %-12:c %-5:e %?-12t% s")
(todo . " %-12:c")
(tags . " %-12:c %-5:e ")
(search . " %-12:c")))
modeline
Hide the various modules that may be present
(defun nd/org-agenda-trim-modeline (orig-fn &rest args)
"Advice to remove extra information from agenda modeline name."
(let ((org-agenda-include-diary nil)
(org-agenda-include-deadlines nil)
(org-agenda-use-time-grid nil))
(apply orig-fn args)))
(advice-add #'org-agenda-set-mode-name :around #'nd/org-agenda-trim-modeline)
misc
These are just some options to enable/disable some aesthetic things.
(setq org-agenda-dim-blocked-tasks nil
org-agenda-compact-blocks t
org-agenda-window-setup 'current-window
org-agenda-start-on-weekday 0
org-agenda-span 'day
org-agenda-current-time-string "### -- NOW -- ###")
bulk actions
These add to the existing bulk actions in the agenda view.
(setq org-agenda-bulk-custom-functions
'((?D org-x-agenda-delete-subtree)))
holidays and birthdays
If I don't include this, I actually forget about major holidays.
(setq holiday-bahai-holidays nil
holiday-hebrew-holidays nil
holiday-oriental-holidays nil
holiday-islamic-holidays nil)
(setq calendar-holidays (append holiday-general-holidays
holiday-christian-holidays))
super agenda
org-super-agenda
has many nice functions for grouping and filtering agenda blocks. I used to have a bunch of clunky custom functions, and this replaced most of them.
(use-package org-super-agenda
:straight t
:config
(let ((inhibit-message t)) (org-super-agenda-mode 1))
(add-hook 'org-agenda-mode-hook 'origami-mode))
;; make the super agenda headers actual agenda headers
(defun nd/org-super-agenda-add-header-property (orig-fun s)
"Add the default header property to header string S from ORIG-FUN."
(org-add-props (funcall orig-fun s) nil 'org-agenda-structural-header t))
(advice-add #'org-super-agenda--make-agenda-header :around
#'nd/org-super-agenda-add-header-property)
block agenda views
default sorting
This gives more flexibility in ignoring items with timestamps.
(setq org-agenda-tags-todo-honor-ignore-options t)
By default I want block agendas to sort based on the todo keyword (with NEXT being up top as these have priority).
(let* ((sort-order (list org-x-kw-next
org-x-kw-wait
org-x-kw-hold
org-x-kw-todo))
(get-rank `(lambda (it)
(-> (get-text-property 1 'todo-state it)
(member ',sort-order)
(length)
(-)))))
(setq org-agenda-cmp-user-defined `(lambda (a b)
(let ((pa (funcall ,get-rank a))
(pb (funcall ,get-rank b)))
(cond ((or (null pa) (null pb)) nil)
((> pa pb) +1)
((< pa pb) -1))))))
custom commands
These agenda commands are the center of the gtd workflow. See comments in the actual code for most specific descriptions of each.
(defmacro nd/org-with-raw-headline (agenda-line &rest body)
"Execute BODY on original headline referred to with AGENDA-LINE."
(declare (indent 1))
`(-when-let (marker (get-text-property 1 'org-marker ,agenda-line))
(with-current-buffer (marker-buffer marker)
(goto-char marker)
,@body)))
(defun nd/org-mk-super-agenda-pred (body)
"Return a predicate function with BODY.
The function will be a lambda form that takes one argument, the
current agenda line, and executes BODY at the point in the
original buffer pointed at by the agenda line."
`(lambda (agenda-line) (nd/org-with-raw-headline agenda-line ,@body)))
(defmacro nd/org-def-super-agenda-pred (name &rest body)
"Make super agenda predicate form with NAME and BODY.
Key-pairs at the end of BODY will be interpreted as a plist to append
to the end of the predicate form."
(declare (indent 1))
(-let* (((pred-body plist) (--split-with (not (keywordp it)) body))
(pred (nd/org-mk-super-agenda-pred pred-body)))
`(quote (:name ,name :pred ,pred ,@plist))))
(defun nd/org-mapper-title (level1 level2 status subtitle)
"Make an auto-mapper title.
The title will have the form 'LEVEL1.LEVEL2 STATUS (SUBTITLE)'."
(let ((status* (->> (symbol-name status)
(s-chop-prefix ":")
(s-replace "-" " ")
(s-titleize))))
(format "%s.%s %s (%s)" level1 level2 status* subtitle)))
(defmacro nd/org-def-super-agenda-automap-with (pre post &rest body)
"Make super agenda auto-map form with BODY.
PRE and POST are forms to add before and after the auto-map."
(declare (indent 0))
`(quote (,@pre
(:auto-map ,(nd/org-mk-super-agenda-pred body))
,@post
(:discard (:anything t)))))
(defmacro nd/org-def-super-agenda-automap (&rest body)
"Make super agenda auto-map form with BODY."
(declare (indent 0))
`(nd/org-def-super-agenda-automap-with nil nil ,@body))
(defmacro nd/org-mk-match-string (&rest body)
"Make an agenda match string from BODY."
(->> body
(--map (cond
((stringp it) it)
((boundp it) (eval it))
((symbolp it) (symbol-name it))
(t it)))
(s-join "")))
;; advice
;; The `org-tags-view' can filter tags along with TODO keywords (eg tags-todo)
;; but this automatically excludes keywords in `org-done-keywords'. Therefore,
;; if I want to include these in any custom agenda blocks, I need to use type
;; tags instead and skip the unwanted TODO keywords with a skip function. This
;; is far slower as it applies the skip function to EVERY headline. Fix that
;; here by nullifying `org--matcher-tags-todo-only' which controls how the
;; matcher is created for tags and tags-todo. Now I can select done keywords
;; using a match string like "+tag/DONE|CANC" (also much clearer in my opinion).
;; While this is usually more efficient, it may be counterproductive in cases
;; where skip functions can be used to ignore huge sections of an org file
;; (which is rarely for me; most only skip ahead to the next heading).
(defun org-x-tags-view-advice (orig-fn &rest args)
"Include done states in `org-tags-view' for tags-todo agenda types.
This is meant to be used as :around advice, where ORIG-FN is the
original function being advised and ARGS are the arguments."
(nd/with-advice
((#'org-make-tags-matcher
:around (lambda (f m)
(let ((org--matcher-tags-todo-only nil))
(funcall f m)))))
(apply orig-fn args)))
(advice-add #'org-tags-view :around #'org-x-tags-view-advice)
(defconst nd/org-headline-task-status-priorities
'((:archivable . -1)
(:complete . -1)
(:expired . 0)
(:done-unclosed . 0)
(:undone-closed . 0)
(:active . 1)
(:inert . 2)))
(defconst nd/org-x-project-status-priorities
'((:archivable . -1)
(:complete . -1)
(:scheduled-project . 0)
(:invalid-todostate . 0)
(:undone-complete . 0)
(:done-incomplete . 0)
(:stuck . 0)
(:wait . 1)
(:held . 2)
(:active . 3)
(:inert . 4)))
(defun org-x-agenda-run-series-advice (fun name settings)
(nd/with-advice
nil
;; ((#'org-agenda-list :override #'org-x-dag-show-daily-nodes))
(-if-let (org-agenda-files (->> (nth 1 settings)
(alist-get 'org-agenda-files)
(car)
(eval)))
(funcall fun name settings)
(funcall fun name settings))))
;; (advice-add #'org-agenda-run-series :around #'org-x-agenda-run-series-advice)
;; hide the daily tags
(setq org-agenda-hide-tags-regexp "[DMYQ][0-9]\\{1,2\\}")
;; (advice-remove #'org-agenda-run-series #'org-x-agenda-run-series-advice)
(org-x-dag-set-series-advice t)
(setq org-agenda-ignore-properties '(effort stats appt category))
(defun nd/org-agenda-run-series (name files cmds)
(declare (indent 2))
(catch 'exit
(let ((org-agenda-buffer-name (format "*Agenda: %s*" name)))
(org-agenda-run-series name `((,@cmds) ((org-agenda-files ',files)))))))
(defun nd/org-agenda-call (buffer-name header-name type match files settings)
(declare (indent 5))
(let* ((n (or header-name buffer-name))
(s `((org-agenda-overriding-header ,n) ,@settings)))
(nd/org-agenda-run-series buffer-name files `((,type ,match ,s)))))
(defun nd/org-agenda-call-agenda (buffer-name header-name files settings)
(declare (indent 3))
(nd/org-agenda-call buffer-name header-name 'agenda "" files settings))
(defun nd/org-agenda-call-headlines (buffer-name header-name files settings)
(declare (indent 3))
(nd/org-agenda-call buffer-name header-name 'search "*" files settings))
(defun nd/org-agenda-timeblock ()
"Show the timeblock agenda view.
In the order of display
1. morning tasks (to do immediately after waking)
2. daily calendar (for thing that begin today at a specific time)
3. evening tasks (to do immediately before sleeping)"
(interactive)
(let ((files (cons (org-x-get-daily-plan-file) (org-x-get-action-files))))
(nd/org-agenda-call-agenda "Timeblock" nil files
`((org-agenda-skip-function #'org-x-calendar-skip-function)
(org-agenda-sorting-strategy '(time-up deadline-up scheduled-up category-keep))
(org-agenda-include-diary t)
(org-super-agenda-groups
`(,(nd/org-def-super-agenda-pred "Morning routine"
(org-x-headline-has-property org-x-prop-routine
org-x-prop-routine-morning)
:order 0)
,(nd/org-def-super-agenda-pred "Evening routine"
(org-x-headline-has-property org-x-prop-routine
org-x-prop-routine-evening)
:order 3)
(:name "Calendar" :order 1 :time-grid t
:transformer (if (equal (get-text-property 1 'org-category it) "daily")
(propertize it 'face 'org-todo)
it))
(:discard (:anything t))))))))
(defun nd/org-agenda-goals ()
"Show the goals agenda view."
(interactive)
;; TODO this will only fire when the agenda view is first made, redo doesn't
;; call it again
(cl-flet*
((mk-pred
(prop id-list)
(nd/org-mk-super-agenda-pred
`((not (member (org-entry-get nil ,prop) ,id-list)))))
(mk-parent-pred
(id-list)
(mk-pred "ID" id-list))
(mk-child-pred
(id-list)
(mk-pred org-x-prop-goal id-list))
(mk-header
(category subtitle plist)
(let* ((c (s-capitalize category))
(n (if subtitle (format "%s (%s)" c subtitle) c)))
`(:name ,n ,@plist)))
(mk-memberless
(cat subname fun id-list)
(mk-header cat subname `(:and (:category ,cat :pred ,(funcall fun id-list)))))
(mk-childless
(cat id-list)
(mk-memberless cat "Childless" #'mk-parent-pred id-list))
(mk-parentless
(cat id-list)
(mk-memberless cat "Parentless" #'mk-child-pred id-list))
(mk-branch
(cat)
(mk-header cat "Branch" `(:and (:category ,cat :children todo))))
(mk-leaf
(cat)
(mk-header cat nil `(:category ,cat))))
(let* ((lt-ids '(append org-x-agenda-goal-task-ids
org-x-agenda-goal-endpoint-ids))
(gs
`((:order 6 ,@(mk-branch "lifetime"))
(:order 7 ,@(mk-branch "endpoint"))
(:order 1 ,@(mk-childless "lifetime" lt-ids))
(:order 2 ,@(mk-childless "endpoint" 'org-x-agenda-goal-task-ids))
(:order 3 ,@(mk-parentless "endpoint" 'org-x-agenda-lifetime-ids))
(:order 4 ,@(mk-leaf "lifetime"))
(:order 5 ,@(mk-leaf "endpoint"))))
(files (list (org-x-get-endpoint-goal-file)
(org-x-get-lifetime-goal-file))))
(org-x-update-goal-link-ids)
(nd/org-agenda-call "Goals" nil 'todo org-x-kw-todo files
`((org-agenda-sorting-strategy '(priority-down time-up))
(org-super-agenda-groups ',gs))))))
(defun nd/org-agenda-ranked-endpoint-goals ()
"Show the ranked endpoint goals agenda view."
(interactive)
(let ((f (list (org-x-get-endpoint-goal-file))))
(org-x-update-goal-link-ids)
(nd/org-agenda-call "Ranked EPGs" nil 'todo org-x-kw-todo f
`((org-agenda-sorting-strategy '(priority-down time-up))
(org-super-agenda-groups
',(nd/org-def-super-agenda-automap
(-if-let (score (org-x-endpoint-goal-get-score))
(format "Score: %s" score)
"No Score")))))))
(defun nd/org-agenda-ranked-lifetime-goals ()
"Show the ranked endpoint goals agenda view."
(interactive)
(let ((f (list (org-x-get-lifetime-goal-file))))
(org-x-update-goal-link-ids)
(nd/org-agenda-call "Ranked LTGs" nil 'todo org-x-kw-todo f
`((org-agenda-sorting-strategy '(priority-down time-up))
(org-super-agenda-groups
',(nd/org-def-super-agenda-automap
;; TODO not DRY
(-when-let (c (org-x-headline-get-category-tag))
(let ((n (alist-get c org-x--quarter-life-categories nil nil #'equal)))
(format "%s. %s" n (or (-some->> c (s-chop-prefix "_")) "NA"))))))))))
;; TODO this is slow and the code isn't pretty to look at, perhaps break into
;; several agenda views, or at least refactor the common bits
(defun nd/org-agenda-goal-groups ()
(interactive)
(let ((task-match (nd/org-mk-match-string
- org-x-tag-incubated
/ org-x-kw-todo
| org-x-kw-next
| org-x-kw-wait
| org-x-kw-hold
| org-x-kw-canc))
(proj-match (nd/org-mk-match-string - org-x-tag-incubated))
(files (org-x-get-action-and-incubator-files)))
(nd/org-agenda-run-series "Goal Groups" (org-x-get-action-files)
`((tags-todo
,task-match
((org-agenda-overriding-header "Tasks")
(org-agenda-sorting-strategy '(time-up scheduled-down))
(org-agenda-skip-function #'org-x-task-skip-function)
(org-super-agenda-groups
',(nd/org-def-super-agenda-automap
(let ((is-ind (org-x-headline-is-atomic-task-p))
(goal-status (-if-let ((f . h) (org-x-resolve-goal-id))
(format "%s | %s"
(s-capitalize (f-base f))
(org-ml-get-property :raw-value h))
"No Goal")))
(format "%s | %s" (if is-ind "Indep." "Project") goal-status))))))
(tags-todo
,proj-match
((org-agenda-overriding-header "Projects")
(org-agenda-skip-function #'org-x-project-skip-function)
(org-agenda-sorting-strategy '(category-keep))
(org-super-agenda-groups
',(nd/org-def-super-agenda-automap
(let* ((status (org-x-headline-get-project-status))
(priority (alist-get status nd/org-x-project-status-priorities)))
(unless (< priority 0)
(let ((is-sub (org-x-headline-has-task-parent))
(goal-status (-if-let ((f . h) (org-x-resolve-goal-id))
(format "%s | %s"
(s-capitalize (f-base f))
(org-ml-get-property :raw-value h))
"No Goal")))
(format "%s | %s" (if is-sub "Subproject" "Project") goal-status))))))))))))
(defun nd/org-agenda-daily ()
"Show the daily agenda view."
(interactive)
(nd/org-agenda-call-agenda "Daily" nil (org-x-get-action-and-incubator-files)
`((org-agenda-skip-function #'org-x-calendar-skip-function)
(org-agenda-sorting-strategy '(time-up deadline-up scheduled-up category-keep))
(org-agenda-include-diary t)
(org-super-agenda-groups
`((:discard (:time-grid t))
(:discard (:pred ,(nd/org-mk-super-agenda-pred
'((or (org-x-headline-has-property
org-x-prop-routine
org-x-prop-routine-evening)
(org-x-headline-has-property
org-x-prop-routine
org-x-prop-routine-morning))))))
,(nd/org-def-super-agenda-pred "Deadlined Projects"
(progn
;; TODO IDK why this is needed, but the point starts on the
;; deadline timestamp and then the project test fails
(org-back-to-heading t)
(and (org-x-headline-is-deadlined-p)
(org-x-headline-is-project-p)))
:order 7)
(:name "Deadlined Tasks" :order 5 :deadline t)
(:name "Scheduled" :order 4 :scheduled t))))))
(defun nd/org-agenda-tasks ()
"Show the tasks agenda view.
Distinguish between independent and project tasks, as well as
tasks that are inert (which I may move to the incubator during a
review phase)"
(interactive)
(let ((match (nd/org-mk-match-string
- org-x-tag-incubated
/ org-x-kw-todo
| org-x-kw-next
| org-x-kw-wait
| org-x-kw-hold
| org-x-kw-canc))
(files (org-x-get-action-files)))
(nd/org-agenda-call "Tasks" nil 'tags-todo match files
`((org-agenda-skip-function #'org-x-task-skip-function)
(org-agenda-todo-ignore-with-date t)
(org-agenda-sorting-strategy '(user-defined-up category-keep))
(org-super-agenda-groups
',(nd/org-def-super-agenda-automap
(let* ((is-atomic (org-x-headline-is-atomic-task-p))
;; lump inert and active non-atomic tasks together
(status (--> (org-x-headline-get-task-status)
(if (and (not is-atomic) (eq it :inert))
:active it)))
(priority (alist-get status nd/org-headline-task-status-priorities)))
(unless (< priority 0)
(-let (((level1 subtitle) (if is-atomic '(1 "α") '(0 "σ"))))
(nd/org-mapper-title level1 priority status subtitle))))))))))
(defun nd/org-agenda-projects ()
"Show the projects agenda view."
(interactive)
(let ((match (nd/org-mk-match-string - org-x-tag-incubated))
(files (org-x-get-action-and-incubator-files)))
(nd/org-agenda-call "Projects" nil 'tags-todo match files
`((org-agenda-skip-function #'org-x-project-skip-function)
(org-agenda-sorting-strategy '(category-keep))
(org-super-agenda-groups
',(nd/org-def-super-agenda-automap
(let* ((status (org-x-headline-get-project-status))
(priority (alist-get status nd/org-x-project-status-priorities)))
(unless (< priority 0)
(-let* ((is-subproject (org-x-headline-has-task-parent))
((level1 subtitle) (if is-subproject '(1 "σ") '(0 "τ"))))
(nd/org-mapper-title level1 priority status subtitle))))))))))
(defun nd/org-agenda-incubator ()
"Show the incubator agenda view."
(interactive)
(let ((match (nd/org-mk-match-string + org-x-tag-incubated))
(files (org-x-get-action-and-incubator-files)))
(nd/org-agenda-call "Incubator" nil 'tags-todo match files
`((org-agenda-skip-function #'org-x-incubator-skip-function)
(org-agenda-sorting-strategy '(category-keep))
(org-super-agenda-groups
`((:name "Past Deadlines" :deadline past)
(:name "Future Deadlines" :deadline future)
,(nd/org-def-super-agenda-pred "Stale Appointments"
(org-x-headline-is-stale-p))
,(nd/org-def-super-agenda-pred "Future Appointments"
(not (org-x-headline-is-todoitem-p)))
,(nd/org-def-super-agenda-pred "Tasks"
(org-x-headline-is-task-p))
,(nd/org-def-super-agenda-pred "Toplevel Projects"
(org-x-headline-is-toplevel-project-p))
,(nd/org-def-super-agenda-pred "Projects"
(org-x-headline-is-project-p))
(:discard (:anything t))))))))
(defun nd/org-agenda-periodical ()
"Show the periodical agenda view."
(interactive)
(let ((files (org-x-get-action-files)))
(nd/org-agenda-call-headlines "Periodicals" "Periodical Status" files
`((org-agenda-skip-function #'org-x-periodical-skip-function)
(org-agenda-sorting-strategy '(category-keep))
(org-super-agenda-groups
',(nd/org-def-super-agenda-automap
(cl-case (org-x-headline-get-periodical-status)
(:uninit "0. Uninitialized")
(:unscheduled "0. Unscheduled")
(:empt "1. Empty")
(:actv "2. Active")
(t "3. Other"))))))))
(defun nd/org-agenda-iterators ()
"Show the iterator agenda view."
(interactive)
(let ((files (org-x-get-action-files)))
(nd/org-agenda-call-headlines "Iterators" "Iterator Status" files
`((org-agenda-skip-function #'org-x-iterator-skip-function)
(org-agenda-sorting-strategy '(category-keep))
(org-super-agenda-groups
',(nd/org-def-super-agenda-automap
(cl-case (org-x-headline-get-iterator-status)
(:uninit "0. Uninitialized")
(:project-error "0. Project Error")
(:unscheduled "0. Unscheduled")
(:empt "1. Empty")
(:actv "2. Active")
(t "3. Other"))))))))
(defun nd/org-agenda-refile ()
"Show the refile agenda view."
(interactive)
(nd/org-agenda-call-headlines "Refile" "Tasks to Refile"
`(,(org-x-get-capture-file))
nil))
(defun nd/org-agenda-errors ()
"Show the critical errors agenda view."
(interactive)
(let ((match (nd/org-mk-match-string - org-x-tag-incubated)))
(nd/org-agenda-call "Errors" nil 'tags match (org-x-get-action-files)
`((org-agenda-skip-function #'org-x-error-skip-function)
(org-super-agenda-groups
`(,(nd/org-def-super-agenda-pred "Discontinuous Projects"
(org-x-headline-is-discontinous-project-task-p))
;; TODO this is redundant, only thing this checks is project headers
,(nd/org-def-super-agenda-pred "Done Unclosed"
(org-x-headline-is-done-unclosed-task-p))
,(nd/org-def-super-agenda-pred "Undone Closed"
(org-x-headline-is-undone-closed-task-p))
,(nd/org-def-super-agenda-pred "Missing Creation Timestamp"
(org-x-headline-is-task-without-creation-timestamp-p))
,(nd/org-def-super-agenda-pred "Missing Archive Target (iterators)"
(org-x-headline-is-iterator-without-archive-target-p))
,(nd/org-def-super-agenda-pred "Future Creation Timestamp"
(org-x-headline-is-task-with-future-creation-timestamp-p))
,(nd/org-def-super-agenda-pred "Meeting without Effort"
(org-x-headline-is-open-meeting-without-effort-p))
(:discard (:anything t))))))))
;; (defun nd/org-agenda-meetings ()
;; "Show the meetings agenda view."
;; (interactive)
;; (let ((match (nd/org-mk-match-string
;; - org-x-tag-refile
;; + org-x-tag-meeting)))
;; (nd/org-agenda-call "Meetings" 'tags-todo match
;; '((org-agenda-overriding-header "Meetings")
;; ;; seems like this should be in the agenda groups, but works fine here
;; (org-agenda-skip-function
;; (lambda ()
;; (-when-let (ts (org-x--headline-get-property-epoch-time "SCHEDULED"))
;; (when (< ts (- (float-time) 10368000))
;; (org-x-skip-heading)))))
;; (org-agenda-sorting-strategy '(time-up scheduled-down))
;; (org-super-agenda-groups
;; `(,(nd/org-def-super-agenda-pred "Open: Unscheduled Meetings"
;; (org-x-headline-is-open-unscheduled-meeting-p))
;; ,(nd/org-def-super-agenda-pred "Open: Invalid States"
;; (org-x-headline-is-open-meeting-with-invalid-keyword-p))
;; ,(nd/org-def-super-agenda-pred "Open: Needs Agenda Items"
;; (org-x-headline-is-open-meeting-without-agenda-p))
;; ,(nd/org-def-super-agenda-pred "Open: Missing Location"
;; (org-x-headline-is-open-meeting-without-location-p))
;; ,(nd/org-def-super-agenda-pred "Open: Scheduled"
;; (org-x-headline-is-open-meeting-p))
;; ,(nd/org-def-super-agenda-pred "Closed: Unresolved Agenda"
;; (org-x-headline-is-closed-meeting-with-unresolved-agenda-p))
;; ,(nd/org-def-super-agenda-pred "Closed: Needs Action Items"
;; (org-x-headline-is-closed-meeting-without-action-items-p))
;; ,(nd/org-def-super-agenda-pred "Closed: Resolved"
;; (org-x-headline-is-closed-meeting-p))
;; (:discard (:anything t))))))))
(defun nd/org-agenda-archive ()
"Show the archive agenda view."
(interactive)
(nd/org-agenda-call-headlines "Archive" nil (org-x-get-action-files)
`((org-agenda-skip-function #'org-x-archive-skip-function)
(org-agenda-sorting-strategy '(category-keep))
(org-super-agenda-groups
`(,(nd/org-def-super-agenda-pred "Atomic Tasks"
(org-x-headline-is-atomic-task-p))
,(nd/org-def-super-agenda-pred "Toplevel Projects"
(org-x-headline-is-toplevel-project-p))
,(nd/org-def-super-agenda-pred "Projects"
(org-x-headline-is-project-p))
(:name "Appointments" :anything))))))
tracking and analytics
Because org-mode
has rich metadata (clocking, logbook, tags, etc) and a robust long-term storage mechanism (archive which can be git-backed), it is a powerful lens with which to study one's own behavior.
Questions I am concerned with answering (non-exhaustive):
- What time of day am I most productive? (can be measured using
CLOSED
timestamps) - How good am I at estimating how long projects will take (can be measured using clocks and the
Effort
property) -
How well do I follow my habits? (measured with logbook entries and the
SCHEDULED
timestamp on the habit)- Specifically, are there certain habits I tend to skip? If so, how long does it take before I start skipping them?
- One important habit I track is sleeping. How consistent am in terms of start of sleep and length of sleep?
-
How many tasks do I write down and then forget about? (use task creation time and tags/category/properties to classify into subgroups)
- As an inverse corollary to this, what characteristics do the tasks I end up doing in a timely manner have in common?
- Which projects/life categories recieve most of my attention? (sum completed clocks and group by tag or project)
org-mode
itself has a few utilities and 3rd-party packages for answering these questions directly in emacs, but they are limited in that they often only look at an isolated slice of all org-mode
data within a limited time period (the agenda habit tracker for example, or column views in org buffers which can display clock summaries per project). I have data spanning years of time that I want to analyze comprehensively.
org-sql
is a package that can store org files in a SQL database, which which one can perform whatever analysis they want. Obviously this isn't done in Emacs and is quite complicated, but the advantage of using a system like this is that SQL itself is meant purely to be a data language and many analysis tools understand it.
I personally store my data in a postgreSQL database. For analysis and visualization I use a combination of Metabase (which is automatically awesome because it's written in Clojure) and R scripts.
(use-package org-sql
:straight t
:config
(setq org-sql-db-config '(postgres :database "org_sql"
:port 35432
:hostname "portnoy4prez.yavin4.ch"
:password "org_sql"
:username "org_sql")
;; some SQL code to denormalize my org-file data for visualization
org-sql-post-init-hooks `((file+ ,(f-join no-littering-etc-directory
"org-sql" "viz_setup.sql")))
org-sql-post-push-hooks '((sql+ "CALL make_vis_tables();"))
org-sql-debug t
org-sql-files '("~/Org/.archive/"
"~/Org/general.org_archive"
"~/Org/general.org"
"~/Org/incubator.org"
"~/Org/projects/"
"~/Org/repeater.org_archive")))
tomato mode
This really means "super awesome pomodoro implementation." Tomato-mode
sounds cooler and more emacs like.
;;(use-package sound-wav :straight t)
;;(add-to-list 'load-path (nd/expand-local-pkg-directory "org-tomato"))
;;(require 'org-tomato)
;;(setq org-tomato-timer-sound (no-littering-expand-etc-file-name
;; "you_suffer.wav"))
window splitting
Org mode is great and all, but the windows never show up in the right place. The solutions here are simple, but have the downside that the window sizing must be changed when tags/capture templates/todo items are changed. This is because the buffer size is not known at window creation time and I didn't feel like making a function to predict it
todo selection
I only need a teeny tiny window below my current window for todo selection
(defun nd/org-todo-window-advice (orig-fn &rest args)
"Advice to fix window placement in `org-fast-todo-selection'."
(let ((override '("\\*Org todo\\*" nd/org-todo-position)))
(nd/with-advice
((#'delete-other-windows :override #'ignore)
(#'split-window-vertically :filter-args (-partial (-const '(-4))))
(#'org-switch-to-buffer-other-window :override #'pop-to-buffer))
(unwind-protect (apply orig-fn args)))))
(advice-add #'org-fast-todo-selection :around #'nd/org-todo-window-advice)
tag selection
By default, the tag selection window obliterates all but the current window…how disorienting :/
(defun nd/org-tag-window-advice (orig-fn &rest args)
"Advice to fix window placement in `org-fast-tags-selection'."
(nd/with-advice
((#'delete-other-windows :override #'ignore)
;; pretty sure I just got lucky here...
(#'split-window-vertically :override #'(lambda (&optional size)
(split-window-below (or size -10)))))
(unwind-protect (apply orig-fn args))))
(advice-add #'org-fast-tag-selection :around #'nd/org-tag-window-advice)
capture
Capture should show up in the bottom of any currently active buffer
(defun nd/org-capture-position (buffer alist)
(let* ((n (length org-capture-templates))
(new (split-window (get-buffer-window) (- (+ 8 n)) 'below)))
(set-window-buffer new buffer)
new))
(defun nd/org-capture-window-advice (orig-fn &rest args)
"Advice to fix window placement in `org-capture-select-template'."
(let ((override '("\\*Org Select\\*" nd/org-capture-position)))
(add-to-list 'display-buffer-alist override)
(nd/with-advice
((#'org-switch-to-buffer-other-window :override #'pop-to-buffer))
(unwind-protect (apply orig-fn args)
(setq display-buffer-alist
(delete override display-buffer-alist))))))
(advice-add #'org-mks :around #'nd/org-capture-window-advice)
tools
printing
For some reason there is no default way to get a "print prompt." Instead one needs to either install some third-party helper or make a function like this.
(nd/require-bin "lpstat" :pacman "cups")
(nd/when-bin "lpstat"
(defun nd/find-printers ()
"Return a list of available printers on Unix systems."
(with-temp-buffer
(call-process "lpstat" nil t nil "-a")
(->> (buffer-string)
(s-split "\n")
(-remove-item "")
(--map (car (s-split " " it))))))
(defun nd/ivy-set-printer-name ()
"Set the printer name using ivy-completion to select printer."
(interactive)
(let ((pl (nd/find-printers)))
(when pl (setq printer-name (ivy-read "Printer: " pl))))))
magit
(nd/require-bin "git")
(nd/when-bin "git"
(use-package magit
:straight t
:config
:delight auto-revert-mode
(setq magit-push-always-verify nil
git-commit-summary-max-length 50)))
dired
compression
Only supports tar.gz, tar.bz2, tar.xz, and .zip by default. Add support for more fun algos such as lzo and zpaq
;; TODO use when-bin
(if (file-exists-p "/usr/bin/7z")
(add-to-list 'dired-compress-files-alist
'("\\.7z\\'" . "7z a %o %i")))
(if (file-exists-p "/usr/bin/lrzip")
(progn
(add-to-list 'dired-compress-files-alist
'("\\.lrz\\'" . "lrzip -L 9 -o %o %i &"))
(add-to-list 'dired-compress-files-alist
'("\\.lzo\\'" . "lrzip -l -L 9 -o %o %i &"))
(add-to-list 'dired-compress-files-alist
'("\\.zpaq\\'" . "lrzip -z -L 9 -o %o %i &"))))
;; NOTE: this must be after the shorter lrz algos otherwise it will
;; always default to .lrz and not .tar.lrz
(if (file-exists-p "/usr/bin/lrztar")
(progn
(add-to-list 'dired-compress-files-alist
'("\\.tar\\.lrz\\'" . "lrztar -L 9 -o %o %i &"))
(add-to-list 'dired-compress-files-alist
'("\\.tar\\.lzo\\'" . "lrztar -l -L 9 -o %o %i &"))
(add-to-list 'dired-compress-files-alist
'("\\.tar\\.zpaq\\'" . "lrztar -z -L 9 -o %o %i &"))))
formatting for humans
make sizes human readable
(setq dired-listing-switches "-vAlh --group-directories-first")
mu4e attachments
By default the included gnus-dired package does not understan mu4e, so override the existing gnus-dired-mail-buffers
function to fix. This allows going to a dired buffer, marking files, and attaching them interactively to mu4e draft buffers.
(nd/require-bin "mu" :aur)
(nd/when-bin "mu"
;; from here:
;; https://www.djcbsoftware.nl/code/mu/mu4e/Dired.html#Dired
(require 'gnus-dired)
(eval-after-load 'gnus-dired
'(defun gnus-dired-mail-buffers ()
"Return a list of active mu4e message buffers."
(let (buffers)
(save-current-buffer
(dolist (buffer (buffer-list t))
(set-buffer buffer)
(when (and (derived-mode-p 'message-mode)
(null message-sent-message-via))
(push (buffer-name buffer) buffers))))
(nreverse buffers))))
(setq gnus-dired-mail-mode 'mu4e-user-agent)
(add-hook 'dired-mode-hook 'turn-on-gnus-dired-mode))
directory sized
By default dired uses ls -whatever
to get its output. This does not have recursive directory contents by default. This nitfy package solves this. This is not on default because navigation is much slower and the du output adds very little in many situations (toggle when needed).
(use-package dired-du
:straight t
:config
(setq dired-du-size-format t))
mounted devices
I handle device mounting using rofi and a custom mounting script (elsewhere in my dotfiles). The only functionality I need/want here is the ability to quickly navigate to mounted directories using dired.
(defun nd/find-devices ()
"Navigate to mounted devices."
(interactive)
(-if-let (mounted (-annotate #'f-filename (nd/get-mounted-directories)))
(-when-let (dev (completing-read "Go to device: " mounted))
(-if-let (path (alist-get dev mounted nil nil #'equal))
(find-file path)
(message "Invalid device: %s" dev)))
(message "No mounted device")))
filtering
Filtering is useful for obvious reasons
(use-package dired-narrow
:straight t)
pdf-tools
;; TODO consider tagging this with :pin manual to upgrade seperately
(use-package pdf-tools
:straight t
:config
(pdf-tools-install t)
(setq pdf-view-display-size 'fit-page
pdf-view-resize-factor 1.1
pdf-annot-activate-created-annotations t)
(add-hook 'pdf-annot-list-mode-hook #'pdf-annot-list-follow-minor-mode))
mu4e
The following will only be defined if the mu
command is found (which it won't be if this is run on windows).
Initialize by running nd/mu-init
.
(nd/require-bin "pandoc" :aur "pandoc-bin")
(let ((acnts-path (f-join (nd/expand-lib-directory "mu4e") "accounts.el")))
(when (f-exists-p acnts-path)
(nd/when-bin "mu"
;; load mu itself
(require 'mu4e)
(require 'smtpmail)
;; (require 'smtpmail-async)
;;
;; apply common config shared b/t all accounts I use
;;
(defun nd/mu-init (maildir)
"Initialize the mu database using available contexts."
(->> mu4e-contexts
(--map (->> (mu4e-context-vars it)
(alist-get 'user-mail-address)
(format "--my-address=%s")))
(s-join " ")
(format "mu init --maildir %s %s" maildir)
(shell-command-to-string)))
(defun nd/mu4e-junk-folder (msg)
"Hacky function to return junk folder from context.
Must be bound to symbol `nd/mu4e-junk-folder`."
(->> (mu4e-context-determine msg nil)
(mu4e-context-vars)
(alist-get 'nd/mu4e-junk-folder)))
(defun nd/mu4e-headers-mark-for-junk ()
"Function to mark messages as junk."
(interactive)
(mu4e-headers-mark-and-next 'junk))
(defun nd/make-mu4e-context (name dir addr smtp-srv smtp-tls sent-behavior)
(-let* ((trash (format "/%s/trash" dir))
(drafts (format "/%s/drafts" dir))
(sent (format "/%s/sent" dir))
(archive (format "/%s/archive" dir))
(inbox (format "/%s/inbox" dir))
(junk (format "/%s/junk" dir))
(shortcuts (--map (list :maildir (car it) :key (cdr it))
`((,trash . ?t)
(,drafts . ?d)
(,sent . ?s)
(,archive . ?a)
(,inbox . ?i)
(,junk . ?j))))
;; either use TLS or no authentication for outgoing
((smtp-proto smtp-port) (if smtp-tls
'(starttls 587)
'(nil 25)))
(mf (lambda (d msg)
(-some--> msg
(mu4e-message-field it :maildir)
(string-prefix-p (concat "/" d) it)))))
(make-mu4e-context
:name name
:match-func (-partial mf dir) ; use lexical scope here
:vars `((mu4e-trash-folder . ,trash)
(mu4e-drafts-folder . ,drafts)
(mu4e-sent-folder . ,sent)
(mu4e-refile-folder . ,archive)
(nd/mu4e-junk-folder . ,junk)
(mu4e-sent-messages-behavior . ,sent-behavior)
(smtpmail-stream-type . ,smtp-proto)
(smtpmail-smtp-server . ,smtp-srv)
(smtpmail-smtp-service . ,smtp-port)
(smtpmail-smtp-user . ,addr)
(user-mail-address . ,addr)
(mu4e-maildir-shortcuts . ,shortcuts)))))
;; display mu4e in same window
(add-to-list 'display-buffer-alist
`(,(regexp-quote mu4e-main-buffer-name)
display-buffer-same-window))
;; special mark for junk (which is different from trash)
;; trash = delete later
;; junk = spam, which I don't want to delete so I can train spam filters
;;
;; NOTE weird cross symbol picked because it looks like a certain digit
(add-to-list
'mu4e-marks
'(junk :char ("j" . "┻")
:prompt "junk"
:dyn-target
(lambda (target msg) (nd/mu4e-junk-folder msg))
:action
(lambda
(docid msg target)
(mu4e--server-move docid
(mu4e--mark-check-target target)
"-N"))))
;; buttload of common settings I like
(setq mail-user-agent 'mu4e-user-agent
message-kill-buffer-on-exit t
;; misc
mu4e-change-filenames-when-moving t
mu4e-confirm-quit nil
mu4e-compose-dont-reply-to-self t
mu4e-get-mail-command "mbsync -a && mu-index-emacs-maybe"
mu4e-use-fancy-chars t
;; sub some fancy chars that don't have valid codes
mu4e-headers-trashed-mark '("T" . "Ω")
mu4e-headers-unread-mark '("U" . "✉")
mu4e-headers-personal-mark '("P" . "Ρ")
mu4e-headers-list-mark '("L" . "Λ")
mu4e-headers-attach-mark '("a" . "ɑ")
mu4e-headers-thread-root-prefix '("* " . "● ")
mu4e-headers-threaded-label '("T" . "Ψ")
mu4e-headers-related-label '("R" . "↔")
;; directories
mu4e-attachment-dir "~/Downloads"
;; headers
mu4e-headers-show-target nil
mu4e-headers-fields '((:human-date . 11)
(:flags . 5)
(:from . 22)
(:thread-subject))
mu4e-headers-date-format "%F"
mu4e-headers-time-format "%R"
;; view
mu4e-view-show-images t
mu4e-view-show-addresses t
mu4e-view-prefer-html t
;; compose
message-signature nil
;; aliases
mail-personal-alias-file (no-littering-expand-etc-file-name
"mailrc")
;; yanking (aka citing)
message-yank-prefix "" ;; the ">" characters are annoying
message-yank-cited-prefix ""
message-yank-empty-prefix ""
;; contexts (multiple inboxes)
mu4e-context-policy 'pick-first
mu4e-compose-context-policy 'ask-if-none)
;; enable visual line mode and spell checking
(add-hook 'mu4e-compose-mode-hook 'turn-off-auto-fill)
(add-hook 'mu4e-compose-mode-hook 'visual-line-mode)
(add-hook 'mu4e-view-mode-hook 'turn-off-auto-fill)
(add-hook 'mu4e-view-mode-hook 'visual-line-mode)
(add-hook 'mu4e-compose-mode-hook (lambda () (flyspell-mode 1)))
;; Outlook doesn't know how to fold mu4e messages by default
;; This is enabled by using 32 underscores followed by the addressing
;; info of the previou message(s).
(require 'nnheader) ; necessary for the header macros below
(defun nd/message-insert-citation-header ()
"Insert the header of the reply message."
(let* ((h message-reply-headers)
(sep "________________________________")
(from (concat "From: " (mail-header-from h)))
(date (concat "Sent: " (mail-header-date h)))
(to (concat "To: " user-full-name))
(subj (concat "Subject: " (message-strip-subject-re (mail-header-subject h)))))
(insert (string-join `("" ,sep ,from ,date ,to ,subj "") "\n"))))
(setq message-citation-line-function 'nd/message-insert-citation-header)
;; prevent html to text conversion from destroying links
(setq
mu4e-compose-pre-hook
(lambda ()
(let* ((msg mu4e-compose-parent-message)
(html (and msg (plist-get msg :body-html)))
;; oops, mu4e screwed up
(mu4e-html2text-command
(nd/if-bin "pandoc"
"pandoc -f html -t plain --reference-links"
'mu4e-shr2text)))
(when (and html mu4e-view-prefer-html (member mu4e-compose-type '(reply forward)))
;; hackity hack, since the normal mu4e-message-body-text function
;; does not render the desired html, do it here and force the
;; aforementioned function to only look at text by removing
;; the html
(plist-put msg :body-txt (mu4e~html2text-shell msg mu4e-html2text-command))
(plist-put msg :body-html nil)))))
(setq send-mail-function 'smtpmail-send-it
smtpmail-debug-info nil
auth-source-debug nil
message-send-mail-function 'smtpmail-send-it)
;; load instance-specific accounts
(load-file acnts-path))))
shell
(defadvice ansi-term (before force-bash)
(interactive (list "/bin/zsh")))
(ad-activate 'ansi-term)
(defun nd/term-send-raw-escape ()
"Send a raw escape character to the running terminal."
(interactive)
(term-send-raw-string "\e"))
(defun nd/term-send-raw-up ()
"Send a raw up arrow character to the running terminal."
(interactive)
(term-send-raw-string "\e[A"))
(defun nd/term-send-raw-down ()
"Send a raw down character to the running terminal."
(interactive)
(term-send-raw-string "\e[B"))
ediff
(setq ediff-window-setup-function 'ediff-setup-windows-plain)
keybindings
For the sake of my sanity, all bindings go here. Note this means I don't use :bind
in use-package forms.
setup
Most of my modifiers are reloacted using xkb and xcape.
The xkb layout can be found here.
Below is a summary of the remapped xcape keys.
original key | new xkb keycode | xcape keycode/shifted | comment |
---|---|---|---|
Tab | Super_L | Tab/ISO_Left_Tab | |
Backslash | Super_R | Backslash/Bar | |
Capslock | Control_L | Escape | |
Return | Control_R | Return | |
Left Control | Hyper_L | ||
Left Super | ISO_Level3_Shift | XF86Search | XF86Search for dmenu |
Space | Alt_R | Space | |
Right Alt | Hyper_R | ||
Right Control | Caps_Lock |
whichkey
Everyone forgets keybindings. When typing a key chord, this will display a window with all possible completions and their commands.
(use-package which-key
:straight t
:delight
:init
(which-key-mode))
company
(define-key company-active-map (kbd "<tab>") 'company-complete-selection)
(define-key company-active-map (kbd "<return>") 'company-complete-selection t)
(define-key company-active-map "\r" 'company-complete-selection t)
hydra
Hydra allows commands to be arranged on a set of keybindings like a tree.
(use-package hydra
:straight t)
common interfaces
Many programming modes have a common set of commands (compiling, sending to repl, looking up function doc, etc). Rather than memorize a bunch of esoteric keybindings from each individual mode, define a common interface here and map those functions to a common set of keys.
(defvar nd/hydra-standard-interactive-map
'(("M-i" :exit t)
(:send-line "M-i")
(:send-line-step "I" :exit nil)
(:send-line-follow "C-i")
(:send-group "p")
(:send-group-step "P" :exit nil)
(:send-group-follow "C-p")
(:send-region "r")
(:send-region-step "R" :exit nil)
(:send-region-follow "C-r")
(:send-buffer "b")
(:send-buffer-follow "C-b")
(:shell-start "z")
(:shell-start-follow "C-z")
(:shell-kill "k")
(:shell-kill-all "K"))
"Standard hydra keymap for interactive REPL workflow.")
(defvar nd/hydra-standard-navigation-map
'(("M-n" :exit t)
(:def-at "M-n")
(:def-at-new-win "N")
(:asgn-at "a")
(:asgn-at-new-win "A")
(:ref-at "r")
(:ref-at-new-win "R")
(:pop-marker-stack "b")
(:doc-at "d")
(:doc-at-new-win "D")
(:type-at "t")
(:type-at-new-win "T"))
"Standard hydra keymap for navigation and information workflow.")
(defmacro nd/hydra-standard (hydra-map suffix keymap &rest cmds)
"Create a standardized hydra keymap."
(unless (s-match "-mode-map" (symbol-name keymap))
(error "Not a valid keymap: %s" keymap))
(let* ((hydra-name (--> keymap
(symbol-name keymap)
(s-replace "-mode-map" "" it)
(format "*%s-%s" it suffix)
(make-symbol it)))
(docstring (format "%s %s hydra" hydra-name suffix))
(body (cons keymap (car hydra-map)))
(head-keys (cdr hydra-map))
(mk-head-form
(lambda (cmd)
(-if-let (head-key (alist-get (car it) head-keys))
(-insert-at 1 (cdr it) head-key)
(error "Invalid head keyword: %s" (car it)))))
(heads (--map (funcall mk-head-form it) cmds)))
`(progn
(defhydra ,hydra-name ,body ,docstring ,@heads)
(--> ',heads
(--map (nth 1 it) it)
(--map (where-is-internal it ,keymap nil t) it)
(--each it
(--each it (define-key ,keymap it nil)))))))
(defmacro nd/hydra-standard-int (keymap &rest cmds)
"Create a standardized interactive REPL hydra keymap.
KEYMAP is the keymap to which the hydra should be added and CMDS are
cons cells like (':kw' . 'command') where 'command is an interactive
command that corresponds to ':kw'.
See `nd/hydra-standard-interactive-map' which keywords are valid along
with their corresponding body/head hydra keys."
(declare (indent 1))
`(nd/hydra-standard ,nd/hydra-standard-interactive-map "int"
,keymap ,@cmds))
(defmacro nd/hydra-standard-nav (keymap &rest cmds)
"Create a standardized navigation hydra keymap.
KEYMAP is the keymap to which the hydra should be added and CMDS are
cons cells like (':kw' . 'command') where 'command is an interactive
command that corresponds to ':kw'.
See `nd/hydra-standard-navigation-map' which keywords are valid along
with their corresponding body/head hydra keys."
(declare (indent 1))
`(nd/hydra-standard ,nd/hydra-standard-navigation-map "nav"
,keymap ,@cmds))
evil
I like being evil, which means I think vim is a good editor and emacs is a good operating system. All package and custom bindings go here.
base
(use-package evil
:straight t
:init
;; this is required to make evil collection work
(setq evil-want-integration t
evil-want-keybinding nil
evil-undo-system 'undo-redo)
:config
;; I'm actually not a fan of this since the parens never seem to light up fast
;; enough when I'm zipping through code to not make me dizzy. Also I don't
;; really need it when I have rainbow parentheses anyways
(setq show-paren-mode nil)
(evil-mode 1))
search
By default search uses the default emacs built-in search module. Not evil enough (which really means vim search has features that I like)
(evil-select-search-module 'evil-search-module 'evil-search)
motion
By default, emacs counts a sentence as having at least 2 spaces after punctuation. Make this behave more like vim.
(setq sentence-end-double-space nil)
evil state defaults
Some modes use primitive emacs bindings by default. Educate them.
(add-to-list 'evil-motion-state-modes 'ess-help-mode)
(add-to-list 'evil-insert-state-modes 'inferior-ess-mode)
enhancements
delightfully ripped off from vim plugins
surround
(use-package evil-surround
:straight t
:after evil
:config
(global-evil-surround-mode 1))
commentary
(use-package evil-commentary
:straight t
:after evil
:delight
:config
(evil-commentary-mode))
replace with register
(use-package evil-replace-with-register
:straight t
:after evil
:config
(evil-replace-with-register-install))
twiddle case
(defun nd/evil-twiddle-case (beg end)
(interactive "r")
(when (use-region-p)
(let ((string (buffer-substring-no-properties beg end))
(deactivate-mark))
(funcall (cond
((string-equal string (upcase string)) #'downcase-region)
((string-equal string (downcase string)) #'capitalize-region)
(t #'upcase-region))
beg end))))
(define-key evil-visual-state-map "~" #'nd/evil-twiddle-case)
unbind emacs keys
Some of these commands just get in the way of being evil (which really means that I keep pressing them on accident). Rather than nullifying them completely, tuck them away in the emacs state map in case I actually want them.
(mapc (lambda (k) (nd/move-key global-map evil-emacs-state-map (eval k)))
'((kbd "C-s")
(kbd "C-p")
(kbd "C-n")
(kbd "C-f")
(kbd "C-b")
(kbd "C-a")
(kbd "C-e")
(kbd "C-r")
(kbd "C-<SPC>")
(kbd "C-x C-;")
(kbd "C-x C-l")
(kbd "C-x C-u")
(kbd "C-x C-z")
(kbd "C-x C-c")
(kbd "M-c")
(kbd "M-d")
(kbd "M-e")
(kbd "M-r")
(kbd "M-f")
(kbd "M-h")
(kbd "M-j")
(kbd "C-M-j")
(kbd "M-k")
(kbd "M-l")
(kbd "M-m")
(kbd "M-o")
(kbd "M-q")
(kbd "M-w")
(kbd "M-t")
(kbd "M-u")
(kbd "M-i")
(kbd "M-z")
(kbd "M-v")
(kbd "M-/")
(kbd "M-;")
(kbd "M-DEL")))
evil-org
(use-package evil-org
:straight t
:after (evil org)
:delight
:config
(add-hook 'org-mode-hook 'evil-org-mode)
(add-hook 'evil-org-mode-hook 'evil-org-set-key-theme)
(evil-define-key 'normal org-mode-map
"g]" #'nd/org-goto-last-child-headline)
(require 'evil-org-agenda)
(evil-org-agenda-set-keys)
;; some of the defaults bug me...
(evil-define-key 'motion org-agenda-mode-map
;;"C" #'org-x-agenda-helm-select-categories
"D" #'org-agenda-day-view
"W" #'org-agenda-week-view
"M" #'org-agenda-month-view
"Y" #'org-agenda-year-view
"Y" #'org-agenda-year-view
;; I keep accidentally archiving from the agenda, so take this out
"da" nil
"dA" nil
"ct" nil
"g]" #'org-agenda-later
"g[" #'org-agenda-earlier
"sC" #'org-x-agenda-filter-non-context
"sE" #'org-x-agenda-filter-non-effort
"sD" #'org-x-agenda-filter-delegate
"sP" #'org-x-agenda-filter-non-peripheral
"gk" #'org-x-agenda-previous-heading
"gj" #'org-x-agenda-next-heading
"e" #'org-agenda-set-effort
"ce" nil))
visual line mode
This is somewhat strange because all I really care about is moving between lines and to the beginning and end as normal. However, I like the idea of thinking of paragraphs as one line (eg df. deletes a sentence even if on multiple lines). Opinion subject to change.
(evil-define-key '(normal visual) 'visual-line-mode
"j" 'evil-next-visual-line
"k" 'evil-previous-visual-line
"0" 'beginning-of-visual-line
"$" 'end-of-visual-line)
outline-minor-mode
(evil-define-key '(normal visual) outline-minor-mode-map
"gk" #'outline-backward-same-level
"gj" #'outline-forward-same-level
(kbd "M-k") #'outline-move-subtree-up ; requires outline magic
(kbd "M-j") #'outline-move-subtree-down ; requires outline magic
(kbd "M-RET") #'outline-insert-heading)
collection
Most packages that don't have an evil version are in this one. Some don't behave the way I like so those are further modified below.
(use-package evil-collection
:straight t
:after evil
:init
(setq evil-collection-mode-list
'(anaconda-mode arc-mode cider company comint custom debug edebug dired
doc-view ebib ediff elfeed flycheck ivy help magit
minibuffer mu4e profiler reftex tar-mode term which-key
xref)
evil-collection-setup-minibuffer t
evil-collection-want-unimpaired-p nil)
:config
(evil-collection-init))
dired
Dired makes new buffers by default. Use find-alternate-file
to avoid this.
(defun nd/dired-move-to-parent-directory ()
"Move buffer to parent directory (like 'cd ..')."
(interactive)
(find-alternate-file ".."))
(defun nd/dired-xdg-open ()
"Open all non-text files in external app using xdg-open.
Only regular files are considered."
(interactive)
(let* ((file-list (seq-filter #'file-regular-p (dired-get-marked-files)))
(do-it (if (<= (length file-list) 5)
t
(y-or-n-p "Open more then 5 files? "))))
(when do-it
(mapc
(lambda (f) (let ((process-connection-type nil))
(start-process "" nil "xdg-open" f)))
file-list))))
(defun nd/dired-open-with ()
"Open marked files in external app.
If multiple apps are available (according to mime type), present
all options in a list to user."
(interactive)
(let* ((file-list (-filter #'file-regular-p (dired-get-marked-files)))
(app-list (->> (-map #'nd/get-mime-type file-list)
(-map #'nd/get-apps-from-mime)
(-reduce #'-intersection))))
(cl-flet
((exec
(cell)
(nd/execute-desktop-command (cdr cell) (s-join " " file-list))))
(cond
((and (= 1 (length file-list)) (= 0 (length app-list)))
(message "No apps found for file"))
((= 0 (length app-list))
(message "No common apps found for files"))
((= 0 (length file-list))
(message "No files selected"))
(t
(ivy-read "Open with" app-list :action #'exec))))))
(defun nd/dired-sort-by ()
"Sort current dired buffer by list of choices.
Note this assumes there are no sorting switches on `dired-ls'"
(interactive)
(cl-flet
((apply-switch
(cell)
(dired-sort-other (concat dired-listing-switches " " (cdr cell)))))
(let ((sort-alist '(("Name" . "")
("Date" . "-t")
("Size" . "-S")
("Extension" . "-X")
("Dirs First" . "--group-directories-first"))))
(ivy-read "Switches" sort-alist :action #'apply-switch))))
(put 'dired-find-alternate-file 'disabled nil)
(evil-define-key #'normal dired-mode-map
"a" #'dired-find-file
"za" #'gnus-dired-attach
"gs" #'nd/dired-sort-by
"gg" #'evil-goto-first-line
"G" #'evil-goto-line
"^" #'nd/dired-move-to-parent-directory
"q" #'nd/kill-current-buffer
(kbd "M-n") #'dired-narrow
(kbd "<return>") #'dired-find-alternate-file
(kbd "C-<return>") #'nd/dired-xdg-open
(kbd "M-<return>") #'nd/dired-open-with)
comint
common
;; (defun nd/comint-char-mode-evil-insert ()
;; "If not at the last line, go to the end of the buffer and enter insert mode. Else just enter insert mode."
;; (interactive)
;; (if (/= (line-number-at-pos (point)) (line-number-at-pos (point-max)))
;; (goto-char (point-max))))
(defun nd/comint-send-input-evil-insert (&optional send-input-cmd)
"Go into insert mode after calling SEND-INPUT-CMD which is usually
the function that send the command to the interactive process in the
REPL. If no SEND-INPUT-CMD then `comint-send-input' is used."
(interactive)
(if send-input-cmd (funcall send-input-cmd) (comint-send-input))
(evil-insert 1))
;; this makes more sense than what collection has
(evil-define-key '(normal insert) comint-mode-map
(kbd "C-k") 'comint-previous-input
(kbd "C-j") 'comint-next-input)
ess
(evil-define-key 'normal inferior-ess-mode-map
(kbd "RET") (lambda () nd/comint-send-input-evil-insert
'inferior-ess-send-input))
;; (add-hook 'inferior-ess-mode-hook
;; (lambda ()
;; (add-hook 'evil-insert-state-entry-hook
;; 'nd/comint-char-mode-evil-insert nil t)))
haskell interactive mode
Not to be confused with interactive-haskell-mode
which is part of the editing buffer
(evil-define-key '(normal insert) haskell-interactive-mode-map
(kbd "C-k") #'haskell-interactive-mode-history-previous
(kbd "C-j") #'haskell-interactive-mode-history-next)
(evil-define-key 'normal haskell-interactive-mode-map
(kbd "[[") #'haskell-interactive-mode-prompt-previous
(kbd "]]") #'haskell-interactive-mode-prompt-next)
pdf-view
;; Apparently it needs to be set up after pdf-view is launched
(add-hook 'pdf-view-mode-hook
(lambda () (evil-collection-pdf-setup)))
(evil-define-key '(normal visual) pdf-view-mode-map
"go" #'pdf-occur
"it" #'pdf-annot-add-text-annotation
"ih" #'pdf-annot-add-highlight-markup-annotation
"is" #'pdf-annot-add-squiggly-markup-annotation
"iu" #'pdf-annot-add-underline-markup-annotation
"io" #'pdf-annot-add-strikeout-markup-annotation)
term
Since I use vi mode in my terminal emulator, need to preserve the escape key's raw behavior
(evil-define-key 'insert term-raw-map
(kbd "<escape>") 'nd/term-send-raw-escape
(kbd "C-<escape>") 'evil-normal-state
(kbd "C-k") 'nd/term-send-raw-up
(kbd "H-k") 'nd/term-send-raw-up
(kbd "C-j") 'nd/term-send-raw-down
(kbd "H-j") 'nd/term-send-raw-down)
lisp
(evil-define-key 'normal emacs-lisp-mode-map
"gh" #'lispy-left
"gl" #'lispy-flow
"gj" #'lispy-down
"gk" #'lispy-up)
ivy
(evil-define-key 'normal ivy-minibuffer-map
"gg" #'ivy-beginning-of-buffer
"G" #'ivy-end-of-buffer)
cider
(nd/when-bin "lein"
(evil-define-key '(normal insert) cider-repl-mode-map
(kbd "C-k") 'cider-repl-previous-input
(kbd "C-j") 'cider-repl-next-input)
(evil-define-key 'normal cider-repl-mode-map
"gh" #'lispy-left
"gl" #'lispy-flow
"gj" #'lispy-down
"gk" #'lispy-up)
(evil-define-key 'normal cider-mode-map
"gh" #'lispy-left
"gl" #'lispy-flow
"gj" #'lispy-down
"gk" #'lispy-up))
mu4e
;; the old open attachment function broke in mu 1.6, fix it here
(nd/when-bin "mu"
(evil-define-key '(normal) mu4e-view-mode-map
"p" #'mu4e-view-mime-part-action)
(evil-define-key '(normal) mu4e-headers-mode-map
"." #'nd/mu4e-headers-mark-for-junk))
local
These are for mode-specific bindings that can/should be outside of the evil maps above (there are not many, and these may be merged with their evil bretheren in the future).
org-mode
base
(add-hook 'org-mode-hook
(lambda ()
;; use the hyper keys/vim arrows with the shifters instead of shift/arrows
(local-set-key (kbd "H-<up>") 'org-shiftup)
(local-set-key (kbd "H-<right>") 'org-shiftright)
(local-set-key (kbd "H-<down>") 'org-shiftdown)
(local-set-key (kbd "H-<left>") 'org-shiftleft)
;; storing links is important, make a shortcut
(local-set-key (kbd "C-c l") 'org-store-link)
(local-set-key (kbd "C-c L") 'org-x-id-store-link)
(local-set-key (kbd "C-c m") 'org-x-id-store-link-metablock)
;; this is just a useful function I made (actually I think I stole)
(local-set-key (kbd "C-c C-x x") 'org-x-mark-subtree-done)
;; this actually overrides org-clock-report (which I never use)
;; with a function to insert full clock entries for those times
;; I forget to clock in (often)
(local-set-key (kbd "C-c C-x C-r") 'org-x-clock-range)
;; override default org subtree cloning with something that clones and resets
(local-set-key (kbd "C-c C-x c") 'org-x-clone-subtree-with-time-shift)
;; add time shifter
(local-set-key (kbd "C-c C-x t") 'org-x-subtree-shift-timestamps)
;; add clock in/out functions for tomato mode
(local-set-key (kbd "C-x C-c C-x C-i") 'org-tomato-user-hl-clock-in)
(local-set-key (kbd "C-x C-c C-x C-o") 'org-tomato-user-hl-clock-out)
;; dag stuff
(local-set-key (kbd "C-x C-c l") 'org-x-dag-link-child-to-parent)
(local-set-key (kbd "C-x C-c L") 'org-x-dag-link-parent-to-child)
(local-set-key (kbd "C-x C-c s") 'org-x-dag-show-status)))
agenda
(add-hook 'org-agenda-mode-hook
(lambda ()
(local-set-key (kbd "C-c C-c") 'org-agenda-set-tags)
(local-set-key (kbd "C-c L") 'org-x-agenda-id-store-link)
(local-set-key (kbd "C-c m") 'org-x-agenda-id-store-link-metablock)
(local-set-key (kbd "C-c C-x c") 'org-x-agenda-clone-subtree-with-time-shift)
(local-set-key (kbd "C-c C-x C-b") 'org-x-agenda-toggle-checkbox)
(local-set-key (kbd "C-c C-x C-r") 'org-x-agenda-clock-range)
(local-set-key (kbd "C-x C-c C-x C-i") 'org-tomato-user-hl-agenda-clock-in)
(local-set-key (kbd "C-x C-c C-x C-o") 'org-tomato-user-hl-agenda-clock-out)
(local-set-key (kbd "C-x C-c m") 'org-x-agenda-meeting-add-agenda-item)
(local-set-key (kbd "C-x C-c M") 'org-x-agenda-meeting-add-action-item)
(local-set-key (kbd "C-x C-c l") 'org-x-dag-agenda-link-child-to-parent)
(local-set-key (kbd "C-x C-c L") 'org-x-dag-agenda-link-parent-to-child)
(local-set-key (kbd "C-x C-c s") 'org-x-dag-agenda-show-status)))
(setq org-super-agenda-header-map (make-sparse-keymap))
(define-key org-super-agenda-header-map (kbd "<tab>") #'origami-toggle-node)
mu4e
;; (defun nd/mu4e-open-attachment-in-emacs (&optional msg attnum)
;; "Open attachments using pdf tools or doc view."
;; (interactive)
;; (let* ((msg (or msg (mu4e-message-at-point)))
;; (attnum (or attnum (mu4e~view-get-attach-num "Attachment to open" msg))))
;; (mu4e-view-open-attachment-emacs msg attnum)))
(nd/when-bin "mu"
(defun nd/insert-mu4e-signature-at-point ()
(interactive)
(insert mu4e-compose-signature))
(define-key mu4e-compose-mode-map (kbd "C-c w") #'nd/insert-mu4e-signature-at-point)
(define-key mu4e-headers-mode-map (kbd "C-c C-l") #'org-store-link)
(define-key mu4e-view-mode-map (kbd "C-c C-l") #'org-store-link))
dired
(define-key dired-mode-map (kbd "C-x g") 'magit)
outline-magic
(define-key outline-minor-mode-map (kbd "<tab>") 'outline-cycle)
ess
They removed the underscore-inserts-arrow feature. Bring it back.
(nd/when-bin "conda"
(define-key ess-r-mode-map "_" #'ess-insert-assign)
(define-key inferior-ess-r-mode-map "_" #'ess-insert-assign)
(nd/hydra-standard-int
ess-r-mode-map
(:send-line . ess-eval-line)
(:send-line-step . ess-eval-line-and-step)
(:send-line-follow . ess-eval-line-and-go)
(:send-group . ess-eval-paragraph)
(:send-group-step . ess-eval-paragraph-and-step)
(:send-group-follow . ess-eval-paragraph-and-go)
(:send-region . ess-eval-region)
(:send-region-step . ess-eval-region-and-step)
(:send-region-follow . ess-eval-region-and-go)
(:send-buffer . ess-eval-buffer)
(:send-buffer-follow . ess-eval-buffer-and-go)
;; TODO add process kill commands
(:shell-start . ess-switch-to-inferior-or-script-buffer))
(nd/hydra-standard-nav
ess-r-mode-map
(:def-at . xref-find-definitions)
(:def-at-new-win . xref-find-definitions-other-window)
(:doc-at . ess-display-help-on-object)))
elisp
(nd/hydra-standard-int emacs-lisp-mode-map
(:send-line . eval-last-sexp)
(:send-group . eval-defun)
(:send-buffer . eval-buffer))
(nd/hydra-standard-nav emacs-lisp-mode-map
(:def-at . xref-find-definitions)
(:def-at-new-win . xref-find-definitions-other-window)
(:asgn-at . xref-find-references))
(define-key emacs-lisp-mode-map (kbd "M-RET") #'emr-show-refactor-menu)
clojure
(nd/when-bin "lein"
(require 'cider-connection)
(defun nd/cider-switch-to-repl-or-start (&optional set-ns)
"Switch to CIDER REPL or start a new one if none."
(interactive "P")
(if (cider-current-repl)
(cider-switch-to-repl-buffer set-ns)
(message "Starting New REPL")
(cider-jack-in nil)))
(with-eval-after-load 'clojure-mode
(nd/hydra-standard-int clojure-mode-map
(:send-group . cider-eval-sexp-at-point)
(:send-buffer . cider-eval-buffer)
(:shell-start . nd/cider-switch-to-repl-or-start)
(:shell-kill . cider-quit))
(nd/hydra-standard-nav clojure-mode-map
(:doc-at . cider-doc)))
(with-eval-after-load 'cider-repl-mode
(nd/hydra-standard-int cider-repl-mode-map
(:shell-kill . cider-quit))))
python
The only thing I like about elpy is the interactive shell
(nd/hydra-standard-int python-mode-map
(:send-line . elpy-shell-send-statement)
(:send-line-step . elpy-shell-send-statement-and-step)
(:send-line-follow . elpy-shell-send-statement-and-go)
(:send-group . elpy-shell-send-group)
(:send-group-step . elpy-shell-send-group-and-step)
(:send-group-follow . elpy-shell-send-group-and-go)
(:send-buffer . elpy-shell-send-region-or-buffer)
(:send-buffer-follow . elpy-shell-send-region-or-buffer-and-go)
(:shell-start . elpy-shell-switch-to-shell)
(:shell-kill . elpy-shell-kill)
(:shell-kill-all . elpy-shell-kill-all))
(nd/hydra-standard-nav python-mode-map
(:def-at . anaconda-mode-find-definitions)
(:def-at-new-win . anaconda-mode-find-definitions-other-window)
(:asgn-at . anaconda-mode-find-assignments)
(:asgn-at-new-win . anaconda-mode-find-assignments-other-window)
(:ref-at . anaconda-mode-find-references)
(:ref-at-new-win . anaconda-mode-find-references-other-window)
(:pop-marker-stack . xref-pop-marker-stack)
(:doc-at . anaconda-mode-show-doc))
javascript
(nd/hydra-standard-int js-mode-map
(:send-line . js-comint-send-last-sexp)
(:send-region . js-comint-send-region)
(:send-buffer . js-comint-send-buffer)
(:shell-start . js-comint-start-or-switch-to-repl))
haskell
(with-eval-after-load 'haskell-mode
(nd/hydra-standard-int
haskell-mode-map
(:send-buffer . haskell-process-load-or-reload)
(:shell-start . nd/haskell-switch-to-process))
(nd/hydra-standard-nav
haskell-mode-map
(:asgn-at . haskell-mode-jump-to-def-or-tag)
(:type-at . haskell-process-do-type-at)
(:pop-marker-stack . xref-pop-marker-stack)
(:doc-at . haskell-process-do-info)))
magit
;; interferes with window hydra
(define-key magit-mode-map (kbd "M-w") nil)
pyenv
This key collides with plenty of other stuff, notably scheduling in org mode
;;(nd/when-bin "pyenv"
;; (define-key pyenv-mode-map (kbd "C-c C-s") nil))
counsel
(define-key counsel-find-file-map (kbd "<tab>") #'ivy-alt-done)
global
function
The function keys are nice because they are almost (not always) free in every mode. Therefore I use these for functions that I need to access anywhere, but not necessary extremely often (because they are out of the way and harder to reach).
(defhydra hydra-modes (global-map "<f1>" :exit t)
"agenda views"
;; ("b" #'nd/org-agenda-timeblock)
;; ("d" #'nd/org-agenda-daily)
;; ("g" #'nd/org-agenda-goals)
;; ("r" #'nd/org-agenda-refile)
;; ("t" #'nd/org-agenda-tasks)
;; ("p" #'nd/org-agenda-projects)
;; ("i" #'nd/org-agenda-incubator)
;; ("I" #'nd/org-agenda-iterators)
;; ("P" #'nd/org-agenda-periodical)
;; ("a" #'nd/org-agenda-archive)
;; ("M" #'org-x-dag-add-daily-metablock)
;; ("D" #'org-x-dag-set-date)
;; ("C" #'org-x-dag-show-date)
;; ;; ("m" #'nd/org-agenda-meetings)
;; ("e" #'nd/org-agenda-errors))
("b" #'org-x-dag-agenda-timeblock)
("q" #'org-x-dag-agenda-quarterly-plan)
("w" #'org-x-dag-agenda-weekly-plan)
;; ("d" #'org-x-dag-agenda-daily)
("g" #'org-x-dag-agenda-goals)
("r" #'nd/org-agenda-refile)
("t" #'org-x-dag-agenda-tasks)
("p" #'org-x-dag-agenda-projects)
("i" #'org-x-dag-agenda-incubator)
("I" #'org-x-dag-agenda-iterators)
;; ("P" #'org-x-dag-agenda-periodical)
("a" #'org-x-dag-agenda-archive)
("M" #'org-x-dag-add-daily-metablock)
("D" #'org-x-dag-set-date)
("C" #'org-x-dag-show-date)
;; ("m" #'nd/org-agenda-meetings)
("e" #'org-x-dag-agenda-errors))
(global-set-key (kbd "<f2>") 'org-capture)
(global-set-key (kbd "<f3>") 'cfw:open-org-calendar)
(global-set-key (kbd "C-<f3>") 'org-x-agg-show-conflicts)
(global-set-key (kbd "C-S-<f3>") 'org-x-agg-show-overloads)
(global-set-key (kbd "<f4>") 'org-clock-goto)
(global-set-key (kbd "C-<f4>") 'org-tomato-user-get-summary)
(global-set-key (kbd "C-S-<f4>") 'org-tomato-user-pomodoro-goto)
(global-set-key (kbd "<f8> i") #'org-id-update-id-locations)
(global-set-key (kbd "<f8> s") #'swiper-thing-at-point)
(global-set-key (kbd "<f8> o") #'counsel-org-goto-all)
(global-set-key (kbd "<f8> b") #'ivy-bibtex-with-local-bibliography)
(global-set-key (kbd "<f8> f") #'flyspell-correct-at-point)
(defhydra hydra-modes (global-map "<f11>" :exit t)
"convenient mode toggles"
("v" visual-line-mode)
("h" global-hl-line-mode)
("r" rainbow-mode)
("h" highlight-indentation-mode)
("H" highlight-indentation-current-column-mode)
("r" rainbow-mode)
("n" display-line-numbers-mode))
(defhydra hydra-tools (global-map "<f12>" :exit t)
"convenient tool shortcuts"
("m" mu4e)
("e" elfeed)
("t" ansi-term)
("T" nd/open-urxvt)
("f" nd/open-fm))
control/meta
;; TODO this should not be in global map
(global-set-key (kbd "C-<SPC>") 'company-complete)
(global-set-key (kbd "C-c e") 'nd/config-visit)
(global-set-key (kbd "C-c r") 'nd/config-reload)
(global-set-key (kbd "C-c s") 'sudo-edit)
(global-set-key (kbd "C-h f") #'counsel-describe-function)
(global-set-key (kbd "C-h v") #'counsel-describe-variable)
(global-set-key (kbd "C-x 2") 'nd/split-and-follow-horizontally)
(global-set-key (kbd "C-x 3") 'nd/split-and-follow-vertically)
(global-unset-key (kbd "C-x c"))
(global-set-key (kbd "C-x k") 'nd/kill-current-buffer)
(global-set-key (kbd "C-x C-d") #'counsel-bookmarked-directory)
(global-set-key (kbd "C-x C-c C-d") 'nd/find-devices)
(global-set-key (kbd "C-x C-f") #'counsel-find-file)
(global-set-key (kbd "C-x C-b") #'ivy-switch-buffer)
(global-set-key (kbd "C-M-S-k") 'nd/close-all-buffers)
(global-set-key (kbd "C-M-S-o") 'nd/org-close-all-buffers)
(global-set-key (kbd "C-M-S-a") 'org-agenda-kill-all-agenda-buffers)
(global-set-key (kbd "C-M-S-e") #'flycheck-list-errors)
(global-set-key (kbd "M-b") 'nd/switch-to-previous-buffer)
(global-set-key (kbd "M-I") 'imenu)
(global-set-key (kbd "M-s") 'avy-goto-char)
(global-set-key (kbd "M-x") 'counsel-M-x)
(defhydra hydra-window (global-map "M-o")
"window"
("M-o" #'nd/switch-to-last-window :exit t)
("o" #'ace-window :exit t)
("s" #'ace-swap-window :exit t)
("k" #'windmove-up)
("j" #'windmove-down)
("h" #'windmove-left)
("l" #'windmove-right)
("K" #'enlarge-window)
("K" #'shrink-window)
("H" #'enlarge-window-horizontally)
("L" #'shrink-window-horizontally)
("=" #'balance-windows :exit t))
other
;; exchange point and marker (I never saw the use for this)
(global-unset-key (kbd "C-x C-x"))