emacs-config/etc/conf.org

4659 lines
174 KiB
Org Mode
Raw Normal View History

This is my personal emacs config. It is quite massive. Please use the table of contents below for easy navigation ;)
* table of contents :TOC:
- [[#overview][overview]]
- [[#features-and-use-cases][features and use cases]]
- [[#for-new-users][for new users]]
- [[#config-structure][config structure]]
- [[#library][library]]
- [[#external][external]]
2019-05-01 17:13:36 -04:00
- [[#internal][internal]]
- [[#macros][macros]]
- [[#functions][functions]]
- [[#interactive][interactive]]
- [[#user-interface][user interface]]
- [[#theme][theme]]
- [[#modeline][modeline]]
- [[#remove-interface-bars][remove interface bars]]
- [[#startup-screen][startup screen]]
- [[#windows][windows]]
- [[#navigation][navigation]]
- [[#cursor][cursor]]
- [[#misc][misc]]
- [[#low-level-config][low-level config]]
- [[#user-information][user information]]
- [[#autosave][autosave]]
- [[#async][async]]
2020-11-13 09:44:53 -05:00
- [[#file-io][file IO]]
- [[#editing][editing]]
- [[#standardization][standardization]]
- [[#auto-completion][auto completion]]
- [[#undo][undo]]
- [[#parenthesis-matching][parenthesis matching]]
- [[#sudo-edit][sudo edit]]
- [[#formats-and-languages][formats and languages]]
2020-08-02 21:56:43 -04:00
- [[#testing][testing]]
- [[#org-mode][org-mode]]
- [[#low-level-config-1][low-level config]]
- [[#buffer-interface][buffer interface]]
- [[#calfw][calfw]]
- [[#window-splitting][window splitting]]
- [[#exporting][exporting]]
- [[#project-management][project management]]
- [[#gtd-implementation][gtd implementation]]
- [[#gtd-next-generation][gtd next generation]]
- [[#tomato-mode][tomato mode]]
- [[#brain][brain]]
- [[#tools][tools]]
- [[#printing][printing]]
- [[#magit][magit]]
- [[#dired][dired]]
- [[#pdf-tools][pdf-tools]]
- [[#mu4e][mu4e]]
- [[#shell][shell]]
- [[#ediff][ediff]]
- [[#mulitmedia-controls][mulitmedia controls]]
- [[#keybindings][keybindings]]
- [[#setup][setup]]
- [[#whichkey][whichkey]]
- [[#hydra][hydra]]
- [[#evil][evil]]
- [[#local][local]]
- [[#global][global]]
* overview
** features and use cases
- full [[https://en.wikipedia.org/wiki/Getting_Things_Done][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 two files.
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.
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 [[https://en.wikipedia.org/wiki/Org-mode][org]] file called =conf.org=.
Using an org file like this offers several advantages. First, org files are foldable in emacs which makes navigation easy. Second, they 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. Third, =org-mode= has an automatic table of contents through the =toc-org= package, which makes naviagation even easier. Fourth, 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. Using the =init.el= itself would just have plain eLisp, which gets cluttered quickly. Some people break the =init.el= down into multiple files to keep everything sane, but I personally think it is easier to use one giant file that itself can be folded and abstracted to reduce the clutter.
* library
This is code that is used generally throughout the emacs config
** external
2019-05-01 17:21:45 -04:00
Some useful external libraries that I use all over the place
*** string manipulation
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 3e3ccda6-0bbb-44f0-8057-9eca89f6df4c
:END:
#+BEGIN_SRC emacs-lisp
(use-package s
:straight t)
#+END_SRC
2019-05-01 17:21:45 -04:00
*** functional programming
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 0d45f98c-4285-4a97-be38-a43d3621a4f7
:END:
#+BEGIN_SRC emacs-lisp
(use-package dash
:straight t
:config
2019-12-19 12:13:38 -05:00
(dash-enable-font-lock))
#+END_SRC
2019-05-01 17:21:45 -04:00
#+BEGIN_SRC emacs-lisp
(use-package dash-functional
:straight t)
#+END_SRC
2019-05-01 17:21:45 -04:00
*** file operations
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 57f6bcfd-3bb3-4380-b408-c23844c7da9c
:END:
#+BEGIN_SRC emacs-lisp
2019-05-01 17:21:45 -04:00
(use-package f
:straight t)
#+END_SRC
2019-05-01 17:13:36 -04:00
** internal
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: d0696764-48ab-4ec6-ab42-42775dc8f793
:END:
2019-05-01 18:24:32 -04:00
Define a path to internal libraries (either things I am developing or external =.el= files I find useful)
2019-05-01 17:13:36 -04:00
#+BEGIN_SRC emacs-lisp
2019-05-01 17:21:45 -04:00
(defvar nd/local-pkg-directory "local/share/")
2019-05-01 17:13:36 -04:00
(defun nd/expand-local-pkg-directory (path)
2019-05-01 17:21:45 -04:00
(f-join user-emacs-directory nd/local-pkg-directory path))
2019-05-01 17:13:36 -04:00
#+END_SRC
** macros
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: c83dc04a-754a-4ae4-b7da-cad984a7cb18
:END:
#+BEGIN_SRC emacs-lisp
;; 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")))
(defvar nd/required-exes '()
"Running list of executables required to run various configuations.")
(defmacro nd/when-bin (bin &rest body)
"Execute BODY if the program BIN exists."
(declare (indent 1))
`(if (executable-find ,bin)
(progn
(setq nd/required-exes (-union '(,bin) nd/required-exes))
,@body)
(print (format "Executable %s not found. Skipping." ,bin))))
2019-05-13 18:35:18 -04:00
(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"))))
#+END_SRC
** functions
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: c0d8cc5c-47e4-4f27-8a96-c5abee6d1e01
:END:
#+BEGIN_SRC emacs-lisp
(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 (concat "^MimeType=.*" mimetype ";?.*$"))
(desktop-dirs '("/usr/share/applications"
"/usr/local/share/applications"
"~/.local/share/applications"))
(desktop-files (mapcan (lambda (d) (directory-files d t ".*\\.desktop" t)) desktop-dirs))
(app-list))
(dolist (file desktop-files app-list)
(with-temp-buffer
(insert-file-contents file)
(let* ((tb (buffer-string)))
(if (string-match mime-regex tb)
(let* ((exec (progn (string-match "^Exec=\\(.*\\)$" tb)
(match-string 1 tb)))
(name (or
(progn (string-match "^Name=\\(.*\\)$" tb)
(match-string 1 tb))
exec)))
(setq app-list (cons `(,name . ,exec) app-list)))))))))
(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."
(let* ((cmd-arg (replace-regexp-in-string "%[fuFU]" file cmd t t)))
(call-process-shell-command (concat cmd-arg " &"))))
(defun nd/get-mime-type (file)
"Get the mime type of FILE."
(let* ((cmd (concat "file --mime-type -b " file))
(mt (shell-command-to-string cmd)))
(replace-regexp-in-string "\n\\'" "" mt)))
(defconst nd/device-mount-dirs
(list
(f-join "/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/detect-package-manager ()
"Return the package manager being used on this OS."
(cond
;; for now only pacman...because arch is the best (TM)
((file-exists-p "/usr/bin/pacman")
'pacman)))
(defun nd/pacman-find-owner (file)
"Return the pacman packages that owns FILE.
Assumes pacman is installed and FILE is an absolute path."
(-some->> (format "pacman -Fq %s" file)
(shell-command-to-string)
(s-trim)
(s-split "\n")
(--map (replace-regexp-in-string ".*/" "" it t))
(cons file)))
(defun nd/detect-dependencies ()
"Return a list of required packages for this configuration."
(--map (nd/pacman-find-owner (format "/usr/bin/%s" it)) nd/required-exes))
#+END_SRC
** interactive
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 2fdcb908-a078-4451-9a93-08eba95cde0a
:END:
#+BEGIN_SRC emacs-lisp
(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))))
#+END_SRC
* user interface
The general look and feel, as well as interactive functionality
** theme
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 69569592-9930-4aee-b157-816105f394c9
:END:
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.
#+BEGIN_SRC emacs-lisp
(use-package spacemacs-theme
:straight t
:defer t
:config
(setq spacemacs-theme-custom-colors '((lnum . "#64707c"))))
#+END_SRC
Since I run emacs in [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Emacs-Server.html][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.
#+BEGIN_SRC emacs-lisp
(defvar nd/theme 'spacemacs-dark)
(defvar nd/theme-window-loaded nil)
(defvar nd/theme-terminal-loaded nil)
2020-08-16 12:53:39 -04:00
(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)
2020-08-16 12:53:39 -04:00
;;(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))))
#+END_SRC
** modeline
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: b2a59968-a880-4ac7-b7e1-ff9738d442bf
:END:
This modeline goes along with the =spacemacs-theme=. It also has nice integration with =evil-mode= (see keybindings below).
#+BEGIN_SRC emacs-lisp
(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)
#+END_SRC
*** delight
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: c1af4f46-dfd9-41a0-87e1-4ae8286495eb
:END:
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)
#+BEGIN_SRC emacs-lisp
(use-package delight
:straight t)
#+END_SRC
** remove interface bars
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 4455f032-746d-40b8-b847-8173c1365bd2
:END:
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).
#+BEGIN_SRC emacs-lisp
(tool-bar-mode -1)
(menu-bar-mode -1)
(scroll-bar-mode -1)
#+END_SRC
** startup screen
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 6a966ba3-b4e2-45cd-b92d-b58ddc139bb0
:END:
Default startup screen is silly
#+BEGIN_SRC emacs-lisp
(setq inhibit-startup-screen t)
#+END_SRC
Instead use a dashboard, and display days until predicted death...you know, as a pick-me-up ;)
#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC
** windows
*** popup windows
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: cf715208-ec0f-4c3b-b9e8-5189de4e64c9
:END:
Some modes like to make popup windows (eg ediff). This prevents that.
#+BEGIN_SRC emacs-lisp
(setq pop-up-windows nil)
#+END_SRC
*** ace-window
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: dc3f296e-0373-4641-9ccd-7083bd01761b
:END:
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.
#+BEGIN_SRC emacs-lisp
(use-package ace-window
:straight t
:config
(setq aw-background t)
(custom-set-faces '(aw-leading-char-face
((t (:foreground "#292b2e"
:background "#bc6ec5"
:height 1.0
:box nil))))))
#+END_SRC
** navigation
*** ivy
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:CREATED: [2020-10-17 Sat 18:13]
:ID: 54e8e317-7696-4c67-a4bc-ebd920017e77
2020-06-25 11:00:52 -04:00
:END:
#+begin_src emacs-lisp
2020-10-18 09:58:56 -04:00
(defun nd/ivy-swith-buffer-transformer-fn (b)
(with-current-buffer b
(-if-let (f (buffer-file-name))
(format "%-50s %s" b f)
b)))
(use-package ivy
:straight t
2020-10-18 09:58:56 -04:00
:delight
2020-10-18 11:14:52 -04:00
:custom-face (ivy-current-match ((t (:inherit bold :extend t :background "#534573"))))
2020-10-18 09:58:56 -04:00
:config
(setq ivy-use-virtual-buffers nil
ivy-sort-max-size 30000
2020-11-23 23:34:14 -05:00
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))
2020-10-18 09:58:56 -04:00
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))
2020-10-19 08:51:52 -04:00
;; ensure counsel and swiper are loaded
(use-package counsel
:straight t)
(use-package swiper
:straight t)
#+end_src
*** avy
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 516f0610-4766-4711-a697-aaff0107a94b
:END:
Allows jumping to any character in any window with a few keystrokes. Goodbye mouse :)
#+BEGIN_SRC emacs-lisp
(use-package avy
:straight t
:config
(setq avy-background t))
#+END_SRC
** cursor
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 172b379f-817a-4e53-b0dd-17dcd3f89834
:END:
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.
#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC
** misc
*** line wrap
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 30d780b8-f904-4b37-9c99-0e423bc14869
:END:
I don't like line wrap
#+BEGIN_SRC emacs-lisp
(set-default 'truncate-lines t)
#+END_SRC
*** smooth scrolling
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 04e772b4-b011-4f04-bab2-9f78349f276a
:END:
This makes scrolling smoother
#+BEGIN_SRC emacs-lisp
(setq scroll-conservatively 100)
#+END_SRC
*** imagemagick
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 37664cf5-0abd-495a-86be-901278566d35
:END:
#+BEGIN_SRC emacs-lisp
(when (fboundp 'imagemagick-register-types)
(imagemagick-register-types))
#+END_SRC
*** yes-no prompt
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 68c6e54c-cd43-4387-b5d0-a7e2f2128015
:END:
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."
#+BEGIN_SRC emacs-lisp
(defalias 'yes-or-no-p 'y-or-n-p)
#+END_SRC
*** folding
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: da4dc93b-9895-4deb-a5bc-e8c68387b75b
:END:
#+BEGIN_SRC emacs-lisp
(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"))
#+END_SRC
* low-level config
General configuation for behind-the-scenes behavior
** user information
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 422a47ff-872b-4f14-acb1-406a36e0c237
:END:
#+BEGIN_SRC emacs-lisp
2019-05-01 17:01:46 -04:00
(setq user-full-name "Dwarshuis, Nathan J")
#+END_SRC
** autosave
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: c008f4fb-d814-44bf-a750-ddd48238ee1c
:END:
Saving files continuously is actually really annoying and clutters my disk. Turn it off.
#+BEGIN_SRC emacs-lisp
(setq make-backup-files nil)
(setq auto-save-default nil)
#+END_SRC
** async
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: cba2e250-ab93-472e-b747-b325bf6bc04a
:END:
Allows certain processes to run in multithreaded manner. For things like IO this makes sense.
#+BEGIN_SRC emacs-lisp
(use-package async
:straight t
:delight dired-async-mode
:init
(dired-async-mode 1))
#+END_SRC
2020-11-13 09:44:53 -05:00
** file IO
Emacs will warn user when opening a file over a certain limit. Raise this to 1GB.
#+begin_src emacs-lisp
(setq large-file-warning-threshold 1000000000)
#+end_src
* editing
For options that specifically affect programming or editing modes
** standardization
*** tabs and alignment
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 4206ea47-73d9-4b15-b2da-f09e8a85190a
:END:
2019-05-20 14:57:09 -04:00
Who uses tabs in their programs? Make tabs actually equal 4 spaces. Also, allegedly I could [[https://stackoverflow.blog/2017/06/15/developers-use-spaces-make-money-use-tabs/][make more money]] if I use spaces :)
#+BEGIN_SRC emacs-lisp
(setq-default indent-tabs-mode nil
tab-width 4)
#+END_SRC
*** short column width
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 41952f6d-ce56-4acb-ac23-4bbce4cb0d34
:END:
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.
#+BEGIN_SRC emacs-lisp
2020-03-22 12:21:39 -04:00
(setq-default fill-column 80)
(use-package fill-column-indicator
:straight t
:config
(setq fci-rule-use-dashes t)
:hook
(prog-mode . fci-mode))
#+END_SRC
*** spell checking
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: d2ea7a4e-e955-4f55-9e73-e71c8a167592
:END:
Use the built-in =flyspell-mode= to handle spellchecking with favorite completion engine.
#+BEGIN_SRC emacs-lisp
(use-package flyspell-correct-ivy
:straight t
:config
(setq flyspell-correct-interface #'flyspell-correct-ivy))
#+END_SRC
This will spell-check comments in programming languages.
#+BEGIN_SRC emacs-lisp
(add-hook 'prog-mode-hook #'flyspell-prog-mode)
(setq flyspell-issue-message-flag nil)
#+END_SRC
Since flyspell mode is enabled in so many buffers, use a short modeline alias.
#+BEGIN_SRC emacs-lisp
(delight 'flyspell-mode "σ" "flyspell")
#+END_SRC
*** syntax checking
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: efbac4ba-e2d4-498a-bd20-ad330aa2b8e8
:END:
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.
#+BEGIN_SRC emacs-lisp
(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-navigation-minimum-level 'warning)
(delight 'flycheck-mode "ϕ" "flycheck"))
#+END_SRC
*** packaging
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: ce31c62a-d4b9-44a7-9f30-5d035f8d0671
:END:
#+BEGIN_SRC emacs-lisp
(use-package flycheck-package
:straight t
:after flycheck
:config
(eval-after-load 'flycheck '(flycheck-package-setup)))
#+END_SRC
** auto completion
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 90b3798e-342d-4b1e-84a2-6f594dcec619
:END:
Company provides a dropdown of completion options. It has many backends which are configured in each language and format elsewhere.
#+BEGIN_SRC emacs-lisp
(use-package company
:straight t
:delight "κ"
:config
(setq company-idle-delay 0
company-minimum-prefix-length 3))
#+END_SRC
** undo
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 4e1b46fe-6e34-4b5e-9ff3-e4331c939f39
:END:
I find it weird that most programs do not have a tree-like tool to navigate undo information...because this is literally how most programs store this data.
=undo-tree= package adds a nice undo tree buffer to visualize history and also displays diffs to easily show what changed.
2020-10-12 17:08:40 -04:00
Undo tree is also used by evil mode (see below).
#+BEGIN_SRC emacs-lisp
(use-package undo-tree
:straight t
:delight
:config
(setq undo-tree-visualizer-diff t)
(global-undo-tree-mode))
#+END_SRC
** parenthesis matching
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: a8d75763-b67d-448e-a95f-04cfee0fb824
:END:
This color-codes matching parenthesis. Enable pretty much everywhere.
#+BEGIN_SRC emacs-lisp
(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)))
#+END_SRC
Use pretty symbols (like lambda in lisp)
#+BEGIN_SRC emacs-lisp
(add-hook 'prog-mode-hook #'prettify-symbols-mode)
#+END_SRC
** sudo edit
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 03b80001-f784-44b7-814f-01bcf5c8b77b
:END:
Allows opening a file with sudo elevation.
#+BEGIN_SRC emacs-lisp
(use-package sudo-edit
:straight t)
#+END_SRC
** formats and languages
*** Elisp
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: fb09bff5-002a-40b7-a00c-a21eb8dbfa78
:END:
Elisp can use vanilla company with no plugins
#+BEGIN_SRC emacs-lisp
(add-hook 'emacs-lisp-mode-hook 'company-mode)
(add-hook 'emacs-lisp-mode-hook 'origami-mode)
(use-package lispy
:straight t)
(use-package emr
:straight t)
#+END_SRC
*** ESS (Emacs Speaks Statistics)
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 7e1017a8-8780-403e-9222-9cb097380c57
:END:
For me this means R but ess also supports S-plus, SAS, Stata, and other statistical black-magic languages. Note that ESS is not part of =prog-mode= so it must be added manually to hooks.
Flycheck syntax checkers
- r-lintr (install from CRAN)
#+begin_src emacs-lisp
(nd/when-bin "R"
(use-package ess
:straight t
:init
(require 'ess-r-mode)
: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/")))
;; 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)
(nd/when-bin "docker"
(defun nd/ess-r-setwd-maybe (orig-fun &rest args)
(nd/with-advice
((#'ess-set-working-directory :override #'ignore))
(apply orig-fun args)))
(advice-add #'run-ess-r :around #'nd/ess-r-setwd-maybe)
;; force flycheck to use system R instead of whatever is in docker
(defun nd/flycheck-find-exe-no-docker (orig-fun exe)
(if (or (equal exe "R") (s-starts-with? "R " exe))
"/bin/R" (funcall orig-fun exe)))
(advice-add #'flycheck-default-executable-find :around
#'nd/flycheck-find-exe-no-docker)))
#+END_SRC
2020-08-10 18:06:51 -04:00
*** C
:PROPERTIES:
:ID: 0ee09480-e722-4a06-af8f-52f7dbf3f906
:END:
2020-08-10 18:06:51 -04:00
#+BEGIN_SRC emacs-lisp
(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/when-bin "clang"
(use-package flycheck-clang-analyzer
:straight t
:after flycheck
:config
(flycheck-clang-analyzer-setup)))
2020-08-10 18:06:51 -04:00
;; requires cmake/llvm
(nd/when-bin "cmake"
(use-package irony
:straight t
:hook ((irony-mode . irony-cdb-autosetup-compile-options)))
2020-08-10 18:06:51 -04:00
(use-package company-irony
:straight t))
2020-08-10 18:06:51 -04:00
(use-package company-c-headers
:straight t)
2020-08-19 20:47:14 -04:00
(use-package c-eldoc
:straight t)
2020-08-10 18:06:51 -04:00
(use-package c-mode
:after flycheck
:hook ((c-mode . company-mode)
(c-mode . irony-mode)
2020-08-19 20:47:14 -04:00
(c-mode . c-turn-on-eldoc-mode)
2020-08-10 18:06:51 -04:00
(c-mode . nd/init-c-company)))
#+END_SRC
*** Python
**** inferior shell
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 3e13c380-d9a6-4dc7-a0ca-03ee823271d8
:END:
I don't really use elpy, but it has really nice inferior process commands, so import but don't call =elpy-enable=.
#+BEGIN_SRC emacs-lisp
(use-package elpy
:straight t)
#+END_SRC
**** anaconda and ipython
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 320b60fe-2082-4644-913b-f7c703c1642e
:END:
Anaconda (not related to the Python/R distribution?) is much lighter and easier than elpy. Also use ipython instead of the built-in shell. (Note this requires ipython to be installed externally).
#+BEGIN_SRC emacs-lisp
(defun nd/init-anaconda-company ()
"Set the company backends for anaconda mode."
(setq-local company-backends '(company-anaconda)))
(use-package python
2020-06-07 14:24:18 -04:00
: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)
(python-mode . pyenv-mode)
(inferior-python-mode . company-mode)
(inferior-python-mode . nd/init-anaconda-company))
2020-05-26 14:23:10 -04:00
:config
(progn
(nd/when-bin "ipython"
(setq indent-tabs-mode nil
python-shell-interpreter "ipython"
python-shell-interpreter-args "-i --simple-prompt --quiet --no-banner"))
(nd/when-bin "flake8"
(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))
#+END_SRC
**** syntax checking
=Flycheck= has built in support for syntax checking and can be additionally enhanced by installing the following:
- flake8
- pylint
**** formatting
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 4ed019d1-fdce-4552-be1e-5644ebcacdb7
:END:
[[https://github.com/python/black][Black]] is a really nice syntax formatter. It must be externally installed to work.
#+BEGIN_SRC emacs-lisp
(nd/when-bin "black"
(use-package blacken
:straight t))
#+END_SRC
**** pyenv
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 77045cce-5b0c-4caa-aa24-24f6651e9dbb
:END:
2020-11-23 23:34:14 -05:00
For isolation I use [[https://github.com/pyenv/pyenv][pyenv]] and [[https://github.com/pyenv/pyenv-virtualenv][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).
#+BEGIN_SRC emacs-lisp
(nd/when-bin "pyenv"
(use-package pyenv-mode
:straight t
:after python
: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))
#+END_SRC
*** Ruby
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: bd1723ec-daec-4c45-82c5-41430e9b02fc
:END:
#+BEGIN_SRC emacs-lisp
(use-package inf-ruby
:straight t)
(use-package robe
:straight t
:hook (ruby-mode . robe))
(use-package ruby-test-mode
:straight t)
2019-05-30 17:11:14 -04:00
(use-package rvm
:straight t)
#+END_SRC
*** Haskell
**** stack
On Arch, all packages are dynamically linked (very bad for Haskell). The solution is to install [[https://docs.haskellstack.org/en/stable/README/][stack]] via the =stack-static= package through the AUR and then install all Haskell programs through stack using static linking.
**** major mode
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 825bc193-dde8-4978-8843-05ff76138159
:END:
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
2020-03-24 20:39:22 -04:00
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).
#+BEGIN_SRC emacs-lisp
(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))))
(use-package haskell-mode
:straight t
:hook ((haskell-mode . origami-mode)
(haskell-mode . company-mode)
(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))
: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"
;; use stylish (requires the stylish binary somewhere in $PATH)
haskell-stylish-on-save t
;; 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"))
#+END_SRC
**** hlint
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 004cd31c-efe1-47e1-9ded-b7fc375d2ee3
:END:
This is an additional syntax checker and requires the =hlint= binary (install through stack).
#+BEGIN_SRC emacs-lisp
(nd/when-bin "hlint"
(with-eval-after-load 'haskell
(flycheck-add-next-checker 'haskell-stack-ghc '(t . haskell-hlint))))
#+END_SRC
**** helper functions
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 056b3fc4-b853-4646-87d5-ec62dc84bd23
:END:
Other helper functions that make haskell even more fun.
#+BEGIN_SRC emacs-lisp
(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)))
#+END_SRC
*** Lua
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 31252e83-5cc2-4048-b5c2-70c10d1e271f
:END:
For flycheck, install =luacheck= (from AUR on Arch).
#+BEGIN_SRC emacs-lisp
(nd/when-bin "luacheck"
(use-package lua-mode
:straight t))
#+END_SRC
*** TeX
**** AUCTeX
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 48d49c12-7fac-4646-9ac0-14bf592fc0d1
:END:
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.
#+BEGIN_SRC emacs-lisp
(use-package tex
:straight auctex
:hook
((LaTeX-mode . flycheck-mode)
(LaTeX-mode . flyspell-mode)
(LaTeX-mode . fci-mode)
;; sync tex buffer positions to output pdf
(LaTeX-mode . TeX-source-correlate-mode))
:config
(setq TeX-after-compilation-finished-functions
'(TeX-revert-document-buffer)))
#+END_SRC
**** external viewers
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 65c80df3-5354-460b-9400-a6a13d9a4296
:END:
AUCTeX can launch external viewers to show compiled documents.
#+BEGIN_SRC emacs-lisp
(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")))
#+END_SRC
**** outline mode
***** folding
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: faf47efc-fccb-4ac4-9f8b-12fc09bb423a
:END:
I like how =org-mode= folds with the TAB key, so bring the same thing to AUCTeX here with =outline-magic=.
#+BEGIN_SRC emacs-lisp
(use-package outline-magic
:straight t
:after outline
:hook
((LaTeX-mode . outline-minor-mode)))
#+END_SRC
***** fonts
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: eb950e96-8c64-4a77-b007-fa38f3490527
:END:
The section fonts are too big by default. Now the fonts are all kept equal with hatchet, axe, and saw :)
#+BEGIN_SRC emacs-lisp
(setq font-latex-fontify-sectioning 'color)
#+END_SRC
**** auto completion
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: aa2837d5-1554-412b-bd81-a460a941032c
:END:
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.
#+BEGIN_SRC emacs-lisp
(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)))
#+END_SRC
**** line wrap
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: a6be7216-225f-4aec-abdd-77e94b9d8078
:END:
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=).
#+BEGIN_SRC emacs-lisp
(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)
#+END_SRC
**** local variables
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 9f0c94f8-e4c0-4f7b-93b4-d24d0abb773f
:END:
#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'tex
(add-to-list 'safe-local-variable-values
'(TeX-command-extra-options . "-shell-escape")))
#+END_SRC
**** BibTeX
***** database management
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: ef1365fe-eb66-4a98-8f7a-cb5c0d8e89bc
:END:
#+BEGIN_SRC emacs-lisp
(use-package ebib
:straight t
:config
(setq ebib-autogenerate-keys t
ebib-uniquify-keys t))
#+END_SRC
***** citation search and insertion
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 13b5981b-4f22-4565-97a7-933570009797
:END:
Together, =org-ref= and =ivy-bibtex= (also includes =ivy-bibtex=) provide a nice pipeline to search a BibTex database and insert citations.
#+BEGIN_SRC emacs-lisp
(use-package org-ref
:straight t
:after org
:config
(setq reftex-default-bibliography (expand-file-name "~/BibTeX/master.bib")
org-ref-bibliography-notes (expand-file-name "~/BibTeX/notes.org")
org-ref-default-bibliography (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"))
#+END_SRC
*** HTML
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: cc7e3f8a-1efc-4f41-9437-364d37b436d0
:END:
For flycheck, install =tidy= (privides the =html-tidy= binary).
#+BEGIN_SRC emacs-lisp
(use-package impatient-mode
:straight t
:config
(setq httpd-port 18080))
#+END_SRC
*** CSS
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 5a3ae52d-e091-404e-9af8-753fd399a05d
:END:
Overlays hex color codes with matching colors in certain modes like css and html. For flycheck, install =stylelint= (from the AUR on Arch).
#+BEGIN_SRC emacs-lisp
(use-package rainbow-mode
:straight t)
#+END_SRC
2020-08-07 16:28:54 -04:00
*** Jinja2
:PROPERTIES:
:ID: a38b0792-46fe-43cc-b57a-d8e3a189fdc5
:END:
2020-08-07 16:28:54 -04:00
#+BEGIN_SRC emacs-lisp
(use-package jinja2-mode
2020-08-07 23:26:59 -04:00
:straight t
:hook
((jinja2-mode . fci-mode)))
2020-08-07 16:28:54 -04:00
#+END_SRC
*** Javascript
**** tabs
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 09b95fc7-fc23-4fd9-8c1d-12fce4c0fad8
:END:
An exception to the rule
#+BEGIN_SRC emacs-lisp
(setq js-indent-level 2)
#+END_SRC
**** inferior mode
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: af009285-2261-47b1-8bf1-01434b87dec0
:END:
#+BEGIN_SRC emacs-lisp
(use-package js-comint
:straight t)
#+END_SRC
**** JSON
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 7fea0119-e73b-473c-987d-7dfb2f71604c
:END:
#+BEGIN_SRC emacs-lisp
(use-package json-mode
:straight t
:hook (json-mode . origami-mode))
#+END_SRC
2019-12-11 17:43:11 -05:00
*** PHP
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 6fded61d-5a77-464a-b22c-e3164371f091
:END:
2019-12-11 17:43:11 -05:00
#+BEGIN_SRC emacs-lisp
(use-package php-mode
:straight t)
#+END_SRC
*** markdown
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: ea4abeb2-fdc5-44ad-ad55-2e7aa3e4d45e
:END:
Make font sizes smaller and less intrusive for headers
#+BEGIN_SRC emacs-lisp
(use-package markdown-mode
:straight t
2019-12-06 13:02:44 -05:00
: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))))
2019-12-06 13:02:44 -05:00
(add-hook 'markdown-mode-hook #'nd/turn-on-auto-fill-maybe)
#+END_SRC
*** R-markdown
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 6b333283-36c7-4f22-8c95-f1dd78eb098a
:END:
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.
#+BEGIN_SRC emacs-lisp
(use-package poly-R
:straight t
:mode
(("\\.Rmd\\'" . poly-markdown+r-mode)
("\\.rmd\\'" . poly-markdown+r-mode)))
#+END_SRC
*** YAML
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 995cd47c-e0af-43a8-bd7c-a46ae1fae258
:END:
#+BEGIN_SRC emacs-lisp
(use-package yaml-mode
2020-08-11 22:16:09 -04:00
:straight t
:hook ((yaml-mode . fci-mode)))
#+END_SRC
*** csv files
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: f554238f-d7b3-4e2d-ad59-7b4e88ed39a9
:END:
This adds support for csv files. Almost makes them editable like a spreadsheet. The lambda function enables alignment by default.
#+BEGIN_SRC emacs-lisp
(use-package csv-mode
:straight t
:hook (csv-mode . (lambda () (csv-align-fields nil (point-min) (point-max)))))
#+END_SRC
*** Arch Linux
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 7d4a9077-3b71-47d9-998a-282f56f48d33
:END:
#+BEGIN_SRC emacs-lisp
(use-package pkgbuild-mode
:straight t)
(use-package systemd
:straight systemd)
#+END_SRC
*** Unix Shell
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 8d8cf098-eea1-469b-9ada-1d2e709c6977
:END:
No custom code here, but flycheck needs =shellcheck= (a Haskell program). On Arch (or any other distro that loves dynamic binding) easiest way to install is via =stack install ShellCheck=
#+BEGIN_SRC emacs-lisp
2019-05-01 17:13:36 -04:00
(add-to-list 'load-path (nd/expand-local-pkg-directory "essh"))
(require 'essh)
#+END_SRC
*** SQL
No custom code here, but flycheck needs =sqlint= (on Arch available through the AUR).
2019-06-13 15:44:55 -04:00
*** Docker
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: ce24b075-ede6-4d6c-81db-4c6aa40e4fd0
:END:
2019-06-13 15:44:55 -04:00
#+BEGIN_SRC emacs-lisp
(nd/when-bin "docker"
(use-package dockerfile-mode
:straight t))
2019-06-13 15:44:55 -04:00
#+END_SRC
2020-08-02 21:56:43 -04:00
** testing
*** buttercup
:PROPERTIES:
:ID: 9539395e-98aa-4e47-b2ff-4233b63d40b1
:END:
2020-08-02 21:56:43 -04:00
Include this so I can have the docs and indentation specs handy when writing test suites
#+BEGIN_SRC emacs-lisp
(use-package buttercup
:straight t)
#+END_SRC
* org-mode
** low-level config
*** modules
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: d4b978f4-7002-45e8-a84c-6c7bb40c02f6
:END:
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=. =org-habit= allows the habit todoitem which is used as a more flexible recurring task.
#+BEGIN_SRC emacs-lisp
(org-set-modules 'org-modules
(list 'org-habit ; for habit viewing in agenda
'org-protocol)) ; for external captures
;; required for 9.2
;;'org-tempo)) ; for autocomplete src blocks
;; make sure everything else works that I have customly defined
(require 'org-agenda)
(require 'org-protocol)
(require 'org-habit)
(require 'org-clock)
;;(require 'org-tempo) ;; required for 9.2
#+END_SRC
*** directory
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 69bfcaa5-db1d-4507-8397-7dee3cb902f5
:END:
I keep all my org files in one place.
#+BEGIN_SRC emacs-lisp
(setq org-directory "~/Org")
#+END_SRC
*** autosave
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 4320f373-175b-44c8-a564-bb54452da44f
:END:
Save all org buffers 1 minute before the hour.
#+BEGIN_SRC emacs-lisp
(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)))
(run-at-time "00:59" 3600 #'nd/org-save-all-org-buffers)
#+END_SRC
*** libraries
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 455ce793-920c-4244-a25d-ec40fdf74bc1
:END:
Org extras
#+BEGIN_SRC emacs-lisp
2019-05-01 17:13:36 -04:00
(add-to-list 'load-path (nd/expand-local-pkg-directory "org-x"))
2020-06-04 18:24:54 -04:00
(add-to-list 'load-path (nd/expand-local-pkg-directory "org-ml"))
(require 'org-ml)
(require 'org-x)
#+END_SRC
** buffer interface
*** line wrap
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 02b52d3a-485c-4bb7-bea5-f2b9abbc633f
:END:
I often write long, lengthy prose in org buffers, so use =visual-line-mode= to make lines wrap in automatic and sane manner.
#+BEGIN_SRC emacs-lisp
(add-hook 'org-mode-hook #'visual-line-mode)
(delight 'visual-line-mode nil 'simple)
#+END_SRC
*** indentation
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 3caee325-0567-4743-b78c-c51db1254a94
:END:
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.
#+BEGIN_SRC emacs-lisp
(setq org-startup-indented t)
(delight 'org-indent-mode nil "org-indent")
#+END_SRC
*** special key behavior
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: d72f63d5-7adc-469b-8ec1-f5198b2babac
:END:
TODO: These don't work in evil mode (using the usual line commands).
#+BEGIN_SRC emacs-lisp
(setq org-special-ctrl-a/e t
org-special-ctrl-k t
org-yank-adjusted-subtrees t)
#+END_SRC
*** bullets
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 109afbf1-164e-4da5-b6e8-6c1f6fc4b1fd
:END:
These are just so much better to read
#+BEGIN_SRC emacs-lisp
(use-package org-bullets
:straight t
:hook
2020-06-07 13:12:02 -04:00
(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))
#+END_SRC
*** font height
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: b3f385ab-ae8c-4fc6-b303-a1efd2212cb6
:END:
The fonts in org headings bug me; make them smaller and less invasive.
#+BEGIN_SRC emacs-lisp
(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))))
#+END_SRC
*** src blocks
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 2805a86e-065e-4680-b11f-f45a815ccac5
:END:
Enable shortcuts for embedding code in org text bodies.
#+BEGIN_SRC emacs-lisp
(setq org-src-window-setup 'current-window
org-src-fontify-natively t
2019-05-20 10:38:04 -04:00
org-edit-src-content-indentation 0
org-babel-load-languages '((emacs-lisp . t)
(org . t)))
2020-08-20 17:34:23 -04:00
(add-to-list 'org-structure-template-alist '("el" . "src emacs-lisp"))
#+END_SRC
*** todo insertion
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: af72b28d-6673-4237-8ff4-5de64360bea9
:END:
Make todo insertion respect contents
#+BEGIN_SRC emacs-lisp
(setq org-insert-heading-respect-content t)
#+END_SRC
*** table of contents
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 77cd66b2-08b8-4c53-bdd3-4af3b9eade2e
:END:
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 :)
#+BEGIN_SRC emacs-lisp
(use-package toc-org
:straight t
:hook
(org-mode . toc-org-mode))
#+END_SRC
*** column view
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 50532a03-13cf-47b3-92a3-2ee34a3b75ae
:END:
#+BEGIN_SRC emacs-lisp
2019-05-13 18:02:19 -04:00
(setq org-columns-default-format
(s-join
" "
'("%25ITEM" "%4TODO" "%TAGS" "%5Effort(EFFRT){:}"
"%5CLOCKSUM(CLKSM){:}" "%ALLOCATE(ALLOC)")))
2019-05-13 18:02:19 -04:00
(set-face-attribute 'org-column nil :background "#1e2023")
;; org-columns-summary-types
#+END_SRC
** calfw
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 57d3105c-eab1-4784-ab27-cf63e6c56b05
:END:
This is a nifty calendar...sometimes way faster than the agenda buffer for looking at long term things.
#+BEGIN_SRC emacs-lisp
(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 :timestamp)))
#+END_SRC
** 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
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 5c61f3ce-37b7-44ad-af8f-79546536df1a
:END:
I only need a teeny tiny window below my current window for todo selection
#+BEGIN_SRC emacs-lisp
(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)
#+END_SRC
*** tag selection
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: d4974e0b-8ee7-4522-97f9-58a8daf550ad
:END:
By default, the tag selection window obliterates all but the current window...how disorienting :/
#+BEGIN_SRC emacs-lisp
(defun nd/org-tag-window-advice (orig-fn current inherited table &optional todo-table)
"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 (funcall orig-fn current inherited table todo-table))))
(advice-add #'org-fast-tag-selection :around #'nd/org-tag-window-advice)
#+END_SRC
*** capture
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: db0d7970-452e-44d9-9ab0-a59939e3771d
:END:
Capture should show up in the bottom of any currently active buffer
#+BEGIN_SRC emacs-lisp
(defun nd/org-capture-position (buffer alist)
2020-06-25 11:00:52 -04:00
(let ((new (split-window (get-buffer-window) -19 'below)))
(set-window-buffer new buffer)
new))
(defun nd/org-capture-window-advice (orig-fn table title &optional prompt specials)
"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 (funcall orig-fn table title prompt specials)
(setq display-buffer-alist
(delete override display-buffer-alist))))))
(advice-add #'org-mks :around #'nd/org-capture-window-advice)
#+END_SRC
** exporting
*** latex to pdf command
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 20f66f2e-6358-4b89-be02-7bebbf0ad28f
:END:
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).
#+BEGIN_SRC emacs-lisp
(setq org-latex-pdf-process (list "latexmk -output-directory=%o -shell-escape -bibtex -f -pdf %f"))
#+END_SRC
*** custom output directory
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 11985805-b6bf-4760-8e31-00cd09e097ff
:END:
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.
#+BEGIN_SRC emacs-lisp
(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)
#+END_SRC
*** html5
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: fbe3cb50-3d30-4fb0-ba7f-3b7fa2bbdf46
:END:
The default is XHTML for some reason (which few use and makes certain barbaric word processors complain). Use the much-superior html5.
#+BEGIN_SRC emacs-lisp
(setq org-html-doctype "html5")
#+END_SRC
** project management
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 69ab9162-b137-48b5-833d-559c09cdffaa
:END:
[[https://github.com/taskjuggler/TaskJuggler][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.
#+BEGIN_SRC emacs-lisp
;; (require 'ox-taskjuggler)
;; from here: https://www.skamphausen.de/cgi-bin/ska/taskjuggler-mode.el
2019-05-01 17:13:36 -04:00
(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
2019-05-01 18:24:32 -04:00
(add-to-list 'load-path (nd/expand-local-pkg-directory "org-tj"))
(require 'org-tj)
;; force org to listen to the ORDERED property
2019-05-10 02:54:43 -04:00
(setq org-enforce-todo-dependencies nil)
2019-05-08 14:50:48 -04:00
(setq org-tj-valid-report-attributes
2019-05-08 14:50:48 -04:00
'(headline columns definitions timeformat hideresource
hidetask loadunit sorttasks formats period header center))
#+END_SRC
** 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 [[http://doc.norang.ca/org-mode.html#OrgFileStructure][Bernt Hansen's]] very detailed guide.
**** workflow
GTD as described in its [[https://en.wikipedia.org/wiki/Getting_Things_Done][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=). Everythign 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 android. It doesn't really sync 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 headings 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 time, available energy, and priority. Context describes required locations and resources for tasks, and I represent them with tags (see =org-tags-alist=). Required time 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 headings for collections of tasks (this is where I find any projects that need a NEXT task).
The /organize/ step may seem like it requires alot of 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 heading (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 using =nd/org-clone-subtree-with-timeshift=.
- 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.
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. In addition to tracking time spent, it also encourages clean breaks between tasks (eg no multitasking).
**** 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 heading 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 (which I use) are a partial fix for the first problem but do not aleviate the mutability problem.
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 heading (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 headings 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 heading without disrupting the rest.
In an org tree these look like this:
#+BEGIN_SRC
***** 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
#+END_SRC
**** 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 heading with =DONE= that still has a =TODO= underneath.
I used to take care of this problem with lots of skip functions, but it turned out to be unmaintainable and offered poor performance (eg if I wanted a block agenda for =N= statuscodes, I needed to scan the entire org tree =N= times). A far easier way to implement this was to embed the statuscodes in text properties in each agenda line, which could then be sorted and the prefix string formatted with the status code for identification in the block agenda view. Since this only requires one block, it only requires one scan, and is very fast.
***** repeaters
Similarly to projects, repeaters (eg iterators and periodicals) are assessed via a statuscode (after all they are a group of headings 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).
**** terms and definitions
These conventions are used throughout to be precise when naming functions/variables and describing their effects
***** headings
- heading: 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 heading
- todoitem: any heading with a todo keyword
- task: a todoitem with no todoitem children
- atomic: further specifies that the task is not part of a project
- project: a todoitem with that has todoitem children or other projects
- status(code): a keyword used to describe the overall status of a project. See skip functions in the block agenda section for their implementation.
***** time
- stale: refers to timestamps that are in the past/present
- archivable: further specifies that the timestamp is older than some cutoff that defines when tasks can be archived (usually 30 days)
- fresh: refers to timestamps that are in the future
*** todo states
**** sequences
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 5c1c4731-54a1-4a68-99f2-688505347dec
:END:
These keywords are used universally for all org files (see below on quick explanation for each, they are all quite straightforward). Note that projects have a more specific meaning for these keywords in defining project status (see the library of agenda function). Also, it looks way better in the agenda buffer when they are all the same number of chars.
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).
#+BEGIN_SRC emacs-lisp
(setq org-todo-keywords
'((sequence
;; default undone state
"TODO(t/!)"
;; undone but available to do now (projects only)
"NEXT(n/!)" "|"
;; done and complete
"DONE(d/!)")
(sequence
;; undone and waiting on some external dependency
"WAIT(w@/!)"
;; undone but signifies tasks on which I don't wish to focus at the moment
"HOLD(h@/!)" "|"
;; done but not complete
"CANC(c@/!)")))
#+END_SRC
**** colors
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: cd3770aa-4e16-4365-a4ec-c32eb17df9a5
:END:
Aesthetically, I like all my keywords to have bold colors.
#+BEGIN_SRC emacs-lisp
(setq org-todo-keyword-faces
'(("TODO" :foreground "light coral" :weight bold)
("NEXT" :foreground "khaki" :weight bold)
("DONE" :foreground "light green" :weight bold)
("WAIT" :foreground "orange" :weight bold)
("HOLD" :foreground "violet" :weight bold)
("CANC" :foreground "deep sky blue" :weight bold)))
#+END_SRC
**** habits
:PROPERTIES:
:ID: c02e0799-10e0-41c1-96dd-9d7ee335a408
:END:
Habits consider any "done" todo keyword as "complete." I have =CANC= as a done keyword, which I don't want to be displayed as "complete" in the habit tracker. Override this hardcoded behavior with advice.
#+BEGIN_SRC emacs-lisp
(defun nd/org-habit-parse-todo-advice (orig-fn &rest args)
"Advice to make the habit tracker only mark DONE habits as complete."
(let ((org-done-keywords '("DONE")))
(unwind-protect (apply orig-fn args))))
(advice-add #'org-habit-parse-todo :around #'nd/org-habit-parse-todo-advice)
#+END_SRC
2020-06-25 11:00:52 -04:00
*** links and IDs
:PROPERTIES:
:ID: 9131356e-b290-402e-86cf-15242082c622
:END:
IDs and links are useful for meetings where I either reference tasks to discuss or reference action items to do in the future.
#+BEGIN_SRC emacs-lisp
(setq org-id-link-to-org-use-id t)
#+END_SRC
*** tags
**** alist
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 2913dba5-20f6-4a37-b2dc-bac9efb7f098
:END:
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.
#+BEGIN_SRC emacs-lisp
(setq org-tag-alist
;; (@) gtd location context
`((:startgroup)
("@errand" . ?e)
("@home" . ?h)
("@work" . ?w)
("@travel" . ?r)
(:endgroup)
;; (#) gtd resource context
("#laptop" . ?l)
("#tcult" . ?t)
("#phone" . ?p)
;; (%) misc tags
;; denotes reference information
("%note" . ?n)
;; incubator (the someday/maybe list)
("%inc" . ?i)
;; maybe (for things I might want to do, to be used with %inc)
("%maybe" . ?m)
;; denotes tasks that need further subdivision to turn into true project
("%subdiv" . ?s)
;; catchall to mark important headings, usually for meetings
("%flag" . ?f)
;; taskjuggler
(:startgroup)
(,org-tj-project-tag . ?x)
(,org-tj-resource-tag . ?y)
(,org-tj-report-tag . ?z)
(:endgroup)
;; (_) life categories, used for gtd priorities
(:startgroup)
("_env" . ?E) ;; environmental
("_fin" . ?F) ;; financial
("_int" . ?I) ;; intellectual
("_met" . ?M) ;; metaphysical
("_phy" . ?H) ;; physical
("_pro" . ?P) ;; professional
("_rec" . ?R) ;; recreational
("_soc" . ?S) ;; social
(:endgroup)))
#+END_SRC
**** colors
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: aec2918e-a8a9-483c-9387-0974fa2e0e88
:END:
Each group also has its own color, defined by its prefix symbol.
#+BEGIN_SRC emacs-lisp
(defun nd/add-tag-face (fg-name prefix)
"Adds list of cons cells to org-tag-faces with foreground set to fg-name.
Start and end specify the positions in org-tag-alist which define the tags
to which the faces are applied"
(dolist (tag (org-x-filter-list-prefix prefix (mapcar #'car org-tag-alist)))
(push `(,tag . (:foreground ,fg-name)) org-tag-faces)))
(setq org-tag-faces '())
(nd/add-tag-face "PaleGreen" "@")
(nd/add-tag-face "SkyBlue" "#")
(nd/add-tag-face "PaleGoldenrod" "%")
(nd/add-tag-face "violet" "_")
(nd/add-tag-face "OrangeRed1" "τ")
#+END_SRC
*** properties
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 91bad6fb-d454-47a5-8335-f97853f78c31
:END:
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 repeated tasks and a few others (see comments in code).
#+BEGIN_SRC emacs-lisp
(mapc (lambda (i) (add-to-list 'org-default-properties i))
; defines a repeater group
'("PARENT_TYPE"
;; defines the time shift for repeater groups
"TIME_SHIFT"
2020-01-09 17:35:54 -05:00
;; defines an email thread
"THREAD"
;; defines a goal
"GOAL"
2020-07-07 23:23:38 -04:00
"X-ROUTINE"
;; for sorting routines in the agenda
;; date of header creation
"CREATED"))
(setq org-global-properties
'(("PARENT_TYPE_ALL" . "periodical iterator")
2020-07-07 23:23:38 -04:00
("Effort_ALL" . "0:05 0:15 0:30 1:00 1:30 2:00 3:00 4:00 5:00 6:00")
("X-ROUTINE" . "morning evening"))
org-use-property-inheritance
'("PARENT_TYPE" "TIME_SHIFT"))
#+END_SRC
*** capture
**** templates
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 582110fe-566f-476f-a64e-b432d513c921
:END:
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.
2020-06-25 11:00:52 -04:00
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 [[id:53df8748-78c6-4c5b-b8ff-c2a9598dca48][hook]].
#+BEGIN_SRC emacs-lisp
(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))))
2020-06-25 11:00:52 -04:00
(let* ((capfile "~/Org/capture.org")
(todo-options `(entry (file ,capfile) "* TODO %?\n"))
(deadline-options `(entry (file ,capfile) "* TODO %?\nDEADLINE: %^t\n")))
(setq org-capture-templates
;; regular TODO task
2020-06-25 11:00:52 -04:00
`(("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)
2019-06-17 13:56:52 -04:00
"* %?\n")
;; for non-actionable events that happen at a certain time
("a" "appointment" entry (file ,capfile)
2019-06-17 13:56:52 -04:00
"* %?\n%^t\n")
;; like appointment but multiple days
("s" "appointment-span" entry (file ,capfile)
2019-06-17 13:56:52 -04:00
"* %?\n%^t--%^t\n")
;; task with a deadline
2020-06-25 11:00:52 -04:00
("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)
"* TODO Respond to %:fromname; Re: %:subject :#laptop:\nDEADLINE: %(nd/org-timestamp-future 1)\n%a\n")
;; for interruptions that produce useful reference material
("m" "meeting" entry (file ,capfile)
2019-06-17 13:56:52 -04:00
"* meeting with%? :\\%note:\n")
;; TODO add entries here for asynchronously performed tasks
;; that can be tracked
;; target these to a specific headline in general.org
2020-06-25 11:00:52 -04:00
;; ("A" "Asynchronous")
;; eating food
;; prepping food
;; showering
;; driving
;; blablabla
;; for capturing web pages with web browser
("p" "org-protocol" entry (file ,capfile)
2019-06-17 13:56:52 -04:00
"* %^{Title} :\\%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)
2019-06-17 13:56:52 -04:00
"* %^{Title} :\\%note:\n[[%:link][%:description]]"
:immediate-finish t))))
#+END_SRC
**** insert mode
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 999d6090-7851-4882-9e7d-73084adbfc1a
:END:
To save one more keystroke (since I use evil mode), trigger insert mode upon opening capture template.
#+BEGIN_SRC emacs-lisp
(add-hook 'org-capture-mode-hook (lambda () (evil-append 1)))
#+END_SRC
2020-06-25 11:00:52 -04:00
**** autolink
:PROPERTIES:
:ID: 53df8748-78c6-4c5b-b8ff-c2a9598dca48
:END:
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).
#+BEGIN_SRC emacs-lisp
(add-hook 'org-capture-before-finalize-hook
(lambda ()
(when (org-capture-get :x-autolink)
(save-excursion
(org-back-to-heading)
(call-interactively #'org-store-link)))))
2020-06-25 11:00:52 -04:00
#+END_SRC
*** refile
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 8316d4a9-1365-40a7-89ab-e4670c30303c
:END:
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.
#+BEGIN_SRC emacs-lisp
(setq org-refile-targets '((nil :maxlevel . 9)
("~/Org/reference/idea.org" :maxlevel . 9)
2020-11-23 23:34:28 -05:00
("~/Org/reference/questions.org" :maxlevel . 9)
(org-agenda-files :maxlevel . 9))
org-refile-use-outline-path t
org-outline-path-complete-in-steps nil
org-refile-allow-creating-parent-nodes 'confirm
org-indirect-buffer-display 'current-window)
#+END_SRC
Prevent accidental refiling under tasks with done keywords
#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC
*** clocking
**** general
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 82484193-9c46-48be-9092-f62fd0b80f5d
:END:
Clocking is still new and experimental (I'm not a ninja like Bernt yet). I mostly use clocking now as a way to make clean breaks between tasks (eg to discourage "mixing" tasks which is a slippery multitasking slope). I bound =F4= to =org-clock-goto= as an easy way to find my current/last clocked task in any mode (see keybindigs).
#+BEGIN_SRC emacs-lisp
(setq org-clock-history-length 23
org-clock-out-when-done t
org-clock-persist t
org-clock-report-include-clocking-task t)
#+END_SRC
**** modeline
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 9d32382c-5860-4e15-a8ad-12afb45dc59c
:END:
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, use colors.
#+BEGIN_SRC emacs-lisp
(defface nd/spaceline-highlight-clocked-face
`((t (:background "chartreuse3"
:foreground "#3E3D31"
: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)
#+END_SRC
*** clustering
Org mode has no way of detecting if conflicts exist. It also has no way of alerting someone if they have overbooked their schedule
**** extraction filters
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 8384e715-dfe2-4197-9b0e-88bb32c3afa0
:END:
These control which types of headlines are processed by org-cluster
#+BEGIN_SRC emacs-lisp
(defvar nd/org-cluster-filter-files t
"Set to t if files should be filtered in org-cluster.
This option does nothing unless `nd/org-cluster-filtered-files' is
also non-nil.")
(defconst nd/org-cluster-filtered-files
'("incubator" "peripheral")
"Files that should be excluded from org-cluster analysis.
These are pattern-matched so they do not need to be exact names
or paths.")
(defvar nd/org-cluster-filter-todo t
"Set to t if todo keywords should be filtered in org-cluster.
This option does nothing unless `nd/org-cluster-filtered-todo' is
also non-nil.")
(defconst nd/org-cluster-filtered-todo
'("CANC" "DONE")
"TODO keywords that should be filtered from org-cluster analysis.")
(defvar nd/org-cluster-filter-past t
"Set to t to exclude files from before now in org-cluster analysis.")
(defvar nd/org-cluster-filter-habit nil
"Set to t to exclude habits from org-cluster analysis.")
#+END_SRC
**** timestamp extraction and filtering
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 3f39fe19-b89e-47f1-80d4-e6a532788527
:END:
Conflicts and overloads begin with the same list to process, which is created using =org-element-parse-buffer= and a variety of filtering functions to extract relevent timestamps.
The main object that is passed around during extraction and processing is the timestamp-plist as described in =nd/org-cluster-make-tsp= below.
#+BEGIN_SRC emacs-lisp
(defun nd/org-cluster-make-tsp (unixtime range offset fp hardness
&optional type)
"Construct a timestamp plist to be used in further processing.
UNIXTIME is the unixtime of the timestamp as an integer, RANGE is the
duration of the timestamp (could be 0), OFFSET is the character offset
of the timestamp in the file represented with filepath FP, HARDNESS
is a boolean denoting if the timestamp is 'hard' (has minutes and
hours) or 'soft' (only a date). TYPE can be optionally supplied to
denote kinds of timestamps (only 'scheduled' for now)."
(list :unixtime (round unixtime)
:range (or range 0)
:offset offset
:type type
:hardness hardness
:filepath fp))
(defun nd/org-cluster-ts-hard-p (ts)
"Return non-nil if the timestamp TS has hours/minutes."
(org-element-property :hour-start ts))
(defun nd/org-cluster-parse-ts (ts hl fp)
"Parse a timestamp TS belonging to headline HL and filepath FP.
TS is an object as described in the org-element API. Only active
or active-range types are considered. Returns a new timestamp-plist
for TS."
(when ts
(let* ((offset (org-element-property :begin hl))
(hardness (nd/org-cluster-ts-hard-p ts))
(split
(lambda (ts &optional end)
(--> ts
(org-timestamp-split-range it end)
(org-element-property :raw-value it)
(org-2ft it))))
(start (funcall split ts)))
(if (eq (org-element-property :type ts) 'active-range)
(let ((range (--> ts (funcall split it t) (- it start))))
(nd/org-cluster-make-tsp start range offset fp hardness))
(nd/org-cluster-make-tsp start 0 offset fp hardness)))))
(defun nd/org-cluster-effort-seconds (effort-str)
"Convert EFFORT-STR into an integer in seconds from HH:MM format."
(let ((effort-str (string-trim effort-str)))
(save-match-data
(cond
((string-match "^\\([0-9]+\\):\\([0-6][0-9]\\)$" effort-str)
(let ((hours (->> effort-str
(match-string 1)
string-to-number
(* 60))))
(->> effort-str
(match-string 2)
string-to-number
(+ hours)
(* 60))))
(t (error (format "Unknown effort: %s'" effort-str)))))))
(defun nd/org-cluster-extract (acc fun objs &rest args)
"Run FUN on each of OBJS and put results into accumulator ACC.
FUN is a function that takes the accumulator as its first argument,
one member of OBJS as the second, and ARGS as the rest if supplied."
(while objs
(setq acc (apply fun acc (car objs) args)
objs (cdr objs)))
acc)
(defun nd/org-cluster-extract-hl-sched (acc hl fp)
"Extract scheduled timestamp from headline HL in filepath FP.
Create a new timestamp-plist and add to accumulator ACC."
(let* ((ts (org-element-property :scheduled hl))
(unixtime (->> ts (org-element-property :raw-value) org-2ft))
(range (-some->> hl
(org-element-property :EFFORT)
nd/org-cluster-effort-seconds))
(hardness (nd/org-cluster-ts-hard-p ts))
(offset (org-element-property :begin hl)))
(if (= 0 unixtime) acc
(-> unixtime
(nd/org-cluster-make-tsp range offset fp hardness 'scheduled)
(cons acc)))))
(defun nd/org-cluster-extract-hl-ts (acc hl fp)
"Extract timestamps from headline HL in filepath FP.
All active timestamps that are not in drawers or the planning header
are considered. Each timestamp is converted into a new timestamp-plist
and added to accumulator ACC."
(--> hl
(assoc 'section it)
(org-element-contents it)
(--remove
(or (eq 'planning (org-element-type it))
(eq 'property-drawer (org-element-type it))
(eq 'drawer (org-element-type it)))
it)
(org-element-map it 'timestamp #'identity)
(--filter
(or (eq 'active (org-element-property :type it))
(eq 'active-range (org-element-property :type it)))
it)
(--map (nd/org-cluster-parse-ts it hl fp) it)
(append acc it)))
(defun nd/org-cluster-extract-hl (acc hl fp)
"Extract timestamps from headline HL in filepath FP and store in ACC."
(-> acc
(nd/org-cluster-extract-hl-sched hl fp)
(nd/org-cluster-extract-hl-ts hl fp)))
(defun nd/org-cluster-filter-todo (hls)
"Filter certain TODO keywords from headline list HLS."
(if (not nd/org-cluster-filter-todo) hls
(--remove
(member (org-element-property :todo-keyword it)
nd/org-cluster-filtered-todo)
hls)))
(defun nd/org-cluster-filter-files (fps)
"Filter certain file names from files list FPS."
(if (not nd/org-cluster-filter-files) fps
(--remove
(-find (lambda (s) (string-match-p s it)) nd/org-cluster-filtered-files)
fps)))
(defun nd/org-cluster-filter-past (tsps)
"Filter out timestamp-plists in list TSPS if they start in the past."
(if (not nd/org-cluster-filter-past) tsps
(let ((ft (float-time)))
(--remove (< (plist-get it :unixtime) ft) tsps))))
(defun nd/org-cluster-filter-habit (hls)
"Filter headlines from headline list HLS that are habits."
(if (not nd/org-cluster-filter-habit) hls
(--remove (org-element-property :STYLE it) hls)))
(defun nd/org-cluster-extract-file (acc fp)
"Extract timestamps from filepath FP and add to accumulator ACC."
(-->
fp
(find-file-noselect it t)
(with-current-buffer it (org-element-parse-buffer))
(org-element-map it 'headline #'identity)
(nd/org-cluster-filter-todo it)
(nd/org-cluster-filter-habit it)
(nd/org-cluster-extract acc #'nd/org-cluster-extract-hl it fp)))
(defun nd/org-cluster-get-unprocessed ()
"Return a list of timestamp-plists with desired filter settings."
(->>
;; (list "~/Org/reference/testconflict.org")
(org-agenda-files)
nd/org-cluster-filter-files
(nd/org-cluster-extract nil #'nd/org-cluster-extract-file)
nd/org-cluster-filter-past))
#+END_SRC
**** conflict detection
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 6b777ec6-3898-4855-8664-84b13f468920
:END:
This algorithm builds a list of pairs, with each pair being a two tasks that conflict and should be O(n) (best case/no conflicts) to O(n^2) (worst case/everything conflicts).
Steps for this:
1. make a list of all entries containing timestamps (active and scheduled)
2. sort timestamp list
3. Walk through list and compare entries immediately after (sorting ensures that entries can be skipped once one non-conflict is found). If conflicts are found push the pair to new list.
#+BEGIN_SRC emacs-lisp
(defun nd/org-cluster-conflicting-p (tsp-a tsp-b)
"Return t if timestamps TS-A and TS-B conflict."
;; assume that ts-a starts before ts-b
(let* ((start-a (plist-get tsp-a :unixtime))
(start-b (plist-get tsp-b :unixtime))
(end-a (-> tsp-a (plist-get :range) (+ start-a))))
(or (= start-a start-b) (< start-b end-a))))
(defun nd/org-cluster-find-conflict (tsp tsps conlist)
"Test if timestamp-plist TSP conflicts with any in TSPS.
If found, anything in TSPS is cons'd with TSP and added to CONLIST
as a pair. New CONLIST is returned."
(->> tsps
(--take-while (nd/org-cluster-conflicting-p tsp it))
(--map (cons tsp it))
(append conlist)))
(defun nd/org-cluster-build-conlist (tsps)
"Build a list of conflict pairs from timestamp-plist TSPS."
(let ((conlist))
(while (< 1 (length tsps))
(setq conlist (nd/org-cluster-find-conflict (car tsps)
(cdr tsps)
conlist)
tsps (cdr tsps)))
conlist))
(defun nd/org-cluster-get-conflicts ()
"Return a list of cons cells representing conflict pairs.
Each member in the cons cell is a timestamp-plist."
(->>
(nd/org-cluster-get-unprocessed)
(--filter (plist-get it :hardness))
(--sort (< (plist-get it :unixtime) (plist-get other :unixtime)))
nd/org-cluster-build-conlist))
#+END_SRC
**** overload detection
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: fe65a792-6b2c-47c0-84a1-98fc4115a04b
:END:
Overloads are defined as days that have more than 24 hours worth of scheduled material. The algorithm is O(n) as it is basically just a bunch of filtering functions that walk through the list.
Steps for the algorithm:
1. filter only ranged entries (unranged entries have zero time)
2. maybe split timestamps if they span multiple days
3. sort from earliest to latest starting time
4. sum the range of timestamps in each day, keeping those that exceed 24 hours
#+BEGIN_SRC emacs-lisp
(defun nd/org-cluster-split-day-bounds (tsps)
"Split timestamp-plists in TSPS via daily boundaries.
Returns a new timestamp-plist with equal or greater length depending
on how many members needed splitting."
(letrec
((new
(lambda (start end tsp)
(nd/org-cluster-make-tsp start
(- end start)
(plist-get tsp :offset)
(plist-get tsp :filepath)
(plist-get tsp :hardness)
(plist-get tsp :type))))
;; need to temporarily offset the epoch time so day
;; boundaries line up in local time
(split
(lambda (start end tsp)
(let* ((tzs-a (-> start current-time-zone car))
(tzs-b (-> end current-time-zone car))
(start* (-> end (+ tzs-b) (ceiling 86400) 1- (* 86400) (- tzs-b))))
(if (> start* (-> start (+ tzs-a) (floor 86400) (* 86400) (- tzs-a)))
(cons (funcall new start* end tsp)
(funcall split start start* tsp))
(list (funcall new start end tsp))))))
(split-maybe
(lambda (tsp)
(let* ((start (plist-get tsp :unixtime))
(end (+ start (plist-get tsp :range)))
(tzs (-> start current-time-zone car)))
(if (< (-> start (+ tzs) (ceiling 86400)) end)
(funcall split start end tsp)
tsp)))))
(--mapcat (funcall split-maybe it) tsps)))
(defun nd/org-cluster-daily-split (tsps)
"Group timestamp-plist TSPS into sublists for each day."
(letrec ((tz-shift (lambda (tsp) (-> tsp current-time-zone car (+ tsp)))))
(->>
tsps
(--partition-by (--> it
(plist-get it :unixtime)
(funcall tz-shift it)
(floor it 86400))))))
(defun nd/org-cluster-overloaded-p (tsps)
"Return t if total time of timestamp-plists in TSPS exceeds 24 hours.
It is assumed the TSPS represents tasks and appointments within one
day."
(letrec ((ts2diff
(lambda (tsp)
(let ((start (plist-get tsp :unixtime)))
(- (-> tsp (plist-get :range) (+ start)) start)))))
(->> tsps (--map (funcall ts2diff it)) -sum (<= 86400))))
(defun nd/org-cluster-get-overloads ()
"Return list of lists of timestamp-plists grouped by day.
Anything present represents all the tasks in a single day if that day
is overloaded. If a day is not overloaded there will be nothing for it
in the returned list."
(->>
(nd/org-cluster-get-unprocessed)
(--filter (< 0 (plist-get it :range)))
nd/org-cluster-split-day-bounds
(--sort (< (plist-get it :unixtime) (plist-get other :unixtime)))
nd/org-cluster-daily-split
(--filter (nd/org-cluster-overloaded-p it))))
#+END_SRC
**** frontend
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 43ad0b41-43a7-4d66-a260-0c0e3767469b
:END:
I could just fetch the org headings and throw them into a new buffer. But that's boring, and quite limiting. I basically want all the perks of an agenda buffer...tab-follow, the nice parent display at the bottom, time adjust hotkeys, etc. So the obvious and hacky solution is to throw together a quick-n-dirty agenda buffer.
#+BEGIN_SRC emacs-lisp
(defun nd/org-cluster-headline-text (ts-entry)
"Return string with text properties representing the org header for
MARKER for use in the conflict agenda view."
(let* ((offset (plist-get ts-entry :offset))
(ts-marker (--> ts-entry
(plist-get it :filepath)
(find-file-noselect it)
(with-current-buffer it
(copy-marker offset))))
(props (list
'face nil
'done-face 'org-agenda-done
'org-not-done-regexp org-not-done-regexp
'org-todo-regexp org-todo-regexp
'org-complex-heading-regexp org-complex-heading-regexp
'mouse-face 'highlight))
marker priority category level tags todo-state
ts-date ts-date-type ts-date-pair
txt beg end inherited-tags todo-state-end-pos)
(with-current-buffer (marker-buffer ts-marker)
(save-excursion
(goto-char ts-marker)
(setq marker (org-agenda-new-marker (point))
category (org-get-category)
ts-date-pair (org-agenda-entry-get-agenda-timestamp (point))
ts-date (car ts-date-pair)
ts-date-type (cdr ts-date-pair)
txt (org-get-heading t)
inherited-tags
(or (eq org-agenda-show-inherited-tags 'always)
(and (listp org-agenda-show-inherited-tags)
(memq 'todo org-agenda-show-inherited-tags))
(and (eq org-agenda-show-inherited-tags t)
(or (eq org-agenda-use-tag-inheritance t)
(memq 'todo org-agenda-use-tag-inheritance))))
tags (org-get-tags-at nil (not inherited-tags))
level (make-string (org-reduced-level (org-outline-level)) ? )
txt (org-agenda-format-item "" txt level category tags t)
priority (1+ (org-get-priority txt)))
(org-add-props txt props
'org-marker marker 'org-hd-marker marker
'priority priority
'level level
'ts-date ts-date
'type "timestamp")))))
(defun nd/org-cluster-ts-fmt (ts)
(let ((fmt "[%Y-%m-%d]"))
(--> ts (plist-get it :unixtime) (format-time-string fmt it))))
;; TODO...waaaaay too wet (not DRY)
(defun nd/org-cluster-show-conflicts (&optional arg)
(interactive "P")
(if org-agenda-overriding-arguments
(setq arg org-agenda-overriding-arguments))
(if (and (stringp arg) (not (string-match "\\S-" arg))) (setq arg nil))
(let* ((today (org-today))
(date (calendar-gregorian-from-absolute today))
(completion-ignore-case t)
(org-agenda-prefix-format '((agenda . " %-12:c %-5:e ")))
rtn rtnall files file pos)
(catch 'exit
(when org-agenda-sticky (setq org-agenda-buffer-name "*Org Conflicts*"))
(org-agenda-prepare)
;; (org-compile-prefix-format 'todo)
(org-compile-prefix-format 'agenda)
;; (org-set-sorting-strategy 'todo)
(setq org-agenda-redo-command '(nd/org-cluster-show-conflicts))
(insert "Conflicting Headings: \n")
(add-text-properties (point-min) (1- (point))
(list 'face 'org-agenda-structure
'short-heading "Conflicts"))
(org-agenda-mark-header-line (point-min))
(-some->
(nd/org-cluster-get-conflicts)
(--each
(insert (concat
"At " (nd/org-cluster-ts-fmt (car it)) "\n"
(nd/org-cluster-headline-text (car it)) "\n"
(nd/org-cluster-headline-text (cdr it)) "\n"
"\n"))))
;; clean up and finalize
(goto-char (point-min))
(or org-agenda-multi (org-agenda-fit-window-to-buffer))
(add-text-properties
(point-min) (point-max)
`(org-agenda-type agenda
org-last-args ,arg
org-redo-cmd ,org-agenda-redo-command
org-series-cmd ,org-cmd))
(org-agenda-finalize)
(setq buffer-read-only t))))
(defun nd/org-cluster-show-overloads (&optional arg)
(interactive "P")
(if org-agenda-overriding-arguments
(setq arg org-agenda-overriding-arguments))
(if (and (stringp arg) (not (string-match "\\S-" arg))) (setq arg nil))
(let* ((today (org-today))
(date (calendar-gregorian-from-absolute today))
(completion-ignore-case t)
(org-agenda-prefix-format '((agenda . " %-12:c %-5:e ")))
rtn rtnall files file pos)
(catch 'exit
(when org-agenda-sticky (setq org-agenda-buffer-name "*Org Overloads*"))
(org-agenda-prepare)
;; (org-compile-prefix-format 'todo)
(org-compile-prefix-format 'agenda)
;; (org-set-sorting-strategy 'todo)
(setq org-agenda-redo-command '(nd/org-cluster-show-overloads))
(insert "Overloaded Days: \n")
(add-text-properties (point-min) (1- (point))
(list 'face 'org-agenda-structure
'short-heading "Overloads"))
(org-agenda-mark-header-line (point-min))
(-some->
(nd/org-cluster-get-overloads)
(--each
(insert (concat
"On " (nd/org-cluster-ts-fmt (car it)) "\n"
(mapconcat #'nd/org-cluster-headline-text it "\n")
"\n"))))
;; clean up and finalize
(goto-char (point-min))
(or org-agenda-multi (org-agenda-fit-window-to-buffer))
(add-text-properties
(point-min) (point-max)
`(org-agenda-type agenda
org-last-args ,arg
org-redo-cmd ,org-agenda-redo-command
org-series-cmd ,org-cmd))
(org-agenda-finalize)
(setq buffer-read-only t))))
#+END_SRC
*** agenda
2020-08-07 16:29:44 -04:00
:PROPERTIES:
:ID: 73c154c8-e13e-4e90-8a1d-77c3be067502
:END:
**** targets
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 5807d294-b7a6-47d7-a3a2-913a4bac4f3d
:END:
The agenda files are limited to as few as possible to keep scanning and startup reasonably fast.
#+BEGIN_SRC emacs-lisp
(setq org-agenda-files '("~/Org"
"~/Org/projects"
"~/Org/reference/peripheral.org"))
#+END_SRC
**** appearence
***** sticky agendas
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: f0c465f4-d501-43de-a234-e2c5c22ab458
:END:
I personally like having sticky agendas by default so I can use multiple windows
#+BEGIN_SRC emacs-lisp
(setq org-agenda-sticky t)
#+END_SRC
***** tag alignment
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: fe6255b4-7569-41d1-b496-8db9888c0282
:END:
#+BEGIN_SRC emacs-lisp
(setq org-agenda-tags-column 'auto)
#+END_SRC
***** prefix format
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: e0e6d1e6-889d-489a-a542-f750551fd765
:END:
This controls what each line on the block agenda looks like. This is reformated to include effort and remove icons.
#+BEGIN_SRC emacs-lisp
(setq org-agenda-prefix-format
'((agenda . " %-12:c %-5:e %?-12t% s")
(todo . " %-12:c")
(tags . " %-12:c %-5:e ")
(search . " %-12:c")))
#+END_SRC
***** modeline
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: ee4c00a9-6144-4ad7-9f96-5cba07ee1e9c
:END:
Hide the various modules that may be present
#+BEGIN_SRC emacs-lisp
(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)
(org-habit-show-habits nil))
(apply orig-fn args)))
(advice-add #'org-agenda-set-mode-name :around #'nd/org-agenda-trim-modeline)
#+END_SRC
***** misc
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 154728a2-6212-41cf-b942-413b16d0843b
:END:
These are just some options to enable/disable some aesthetic things.
#+BEGIN_SRC emacs-lisp
(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 -- ###")
#+END_SRC
Based on my screen size and usage patterns, this seems to be a good value to enable the maximum habit history to be shown without compromising aesthetics.
#+BEGIN_SRC emacs-lisp
(setq org-habit-graph-column 50)
#+END_SRC
**** bulk actions
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 7fefa0dc-70f2-4208-8f48-d10cab8b5bee
:END:
These add to the existing bulk actions in the agenda view.
#+BEGIN_SRC emacs-lisp
(setq org-agenda-bulk-custom-functions
'((?D org-x-agenda-delete-subtree)))
#+END_SRC
**** holidays and birthdays
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: e7fe3dfb-b3e4-4d45-bf05-be49eb5f0479
:END:
If I don't include this, I actually forget about major holidays.
#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC
**** block agenda library
These are functions and variables exclusively for agenda block manipulation within the context of =org-custom-agenda-commands=.
***** constants
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: eea59f33-b2c5-462b-9377-4c0dedd90c56
:END:
#+BEGIN_SRC emacs-lisp
(defconst org-clone-iter-future-time (* 7 24 60 60)
"Iterators must have at least one task greater into the future to be active.")
;; TODO ;unscheduled should trump all
(defconst org-clone-iter-statuscodes '(:uninit :empt :actv :project-error :unscheduled)
"Iterators can have these statuscodes.")
(defconst org-clone-peri-future-time org-clone-iter-future-time
"Periodicals must have at least one heading greater into the future to be fresh.")
(defconst org-clone-peri-statuscodes '(:uninit :empt :actv :unscheduled))
#+END_SRC
***** task helper functions
These are the building blocks for skip functions.
****** repeater testing
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 44657755-b47e-4b2a-85c4-5f82d830618a
:END:
Iterators and periodicals are tested similarly to projects in that they have statuscodes.
#+BEGIN_SRC emacs-lisp
(defun org-clone-get-iterator-project-status (kw)
(cond
((or (org-x-is-scheduled-heading-p)
(member kw org-x-project-invalid-todostates)) :project-error)
;; canceled tasks add nothing
((equal kw "CANC") :empt)
;;
;; these require descending into the project subtasks
;;
;; done projects either add nothing (empty) or are not actually
;; done (project error)
((equal kw "DONE")
(org-x-descend-into-project
'(:empt :project-error)
'((:unscheduled . 1)
(:actv . 1))
(lambda (k)
(if (member k org-done-keywords) 0 1))
#'org-clone-get-iterator-project-status))
;; project with TODO states could be basically any status
((equal kw "TODO")
(org-x-descend-into-project
'(:unscheduled :empt :actv)
'(:project-error . 0)
(lambda (k)
(let ((ts (org-x-is-scheduled-heading-p)))
(cond
2019-05-13 17:55:38 -04:00
((not ts) 0)
((> org-clone-iter-future-time (- ts (float-time))) 1)
(t 2))))
#'org-clone-get-iterator-project-status))
(t (error (concat "invalid keyword detected: " kw)))))
(defun org-clone-get-iterator-status ()
"Get the status of an iterator where allowed statuscodes are in list
`nd/get-iter-statuscodes.' where latter codes in the list trump
earlier ones."
(let ((cur-status (first org-clone-iter-statuscodes))
(breaker-status (-last-item org-clone-iter-statuscodes))
(subtree-end (save-excursion (org-end-of-subtree t)))
(prev-point (point)))
(save-excursion
(outline-next-heading)
(while (and (not (eq cur-status breaker-status))
(< prev-point (point) subtree-end))
(let ((kw (org-x-is-todoitem-p))
(new-status))
(when kw
;; test if project of atomic task
;; assume that there are no todoitems above this headline
;; to make checking easier
(setq
new-status
(if (org-x-headline-has-children 'org-x-is-todoitem-p)
(org-clone-get-iterator-project-status kw)
(let ((ts (or (org-x-is-scheduled-heading-p)
(org-x-is-deadlined-heading-p))))
(cond
((member kw org-done-keywords) :empt)
((not ts) :unscheduled)
((< org-clone-iter-future-time (- ts (float-time))) :actv)
(t :empt)))))
(when (org-x-compare-statuscodes > new-status cur-status org-clone-iter-statuscodes)
(setq cur-status new-status))))
(setq prev-point (point))
(org-forward-heading-same-level 1 t)))
cur-status))
(defun org-clone-get-periodical-status ()
"Get the status of a periodical where allowed statuscodes are in list
`nd/get-peri-statuscodes.' where latter codes in the list trump
earlier ones."
(letrec
((max-ts
(lambda ()
(-some-->
(org-x-element-parse-headline)
(org-element-map it 'timestamp #'identity)
(--filter
(memq (org-element-property :type it) '(active active-range))
it)
(--map
(--> it
(org-timestamp-split-range it t)
(org-element-property :raw-value it)
(org-2ft it))
it)
(-max it))))
(compare
(lambda (s1 s2)
(if (org-x-compare-statuscodes > s1 s2 org-clone-peri-statuscodes) s1 s2)))
(new-status
(lambda (ts)
(-->
ts
(cond
((not it) :unscheduled)
((< org-clone-peri-future-time (- it (float-time))) :actv)
(t :empt))
(funcall compare it cur-status))))
(cur-status (first org-clone-peri-statuscodes))
(breaker-status (-last-item org-clone-peri-statuscodes))
(subtree-end (save-excursion (org-end-of-subtree t)))
(prev-point (point)))
(save-excursion
(outline-next-heading)
(while (and (not (eq cur-status breaker-status))
(< prev-point (point) subtree-end))
(setq cur-status (->> (funcall max-ts) (funcall new-status)))
(setq prev-point (point))
(org-forward-heading-same-level 1 t)))
cur-status))
#+END_SRC
**** super agenda
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 6bd2a7c9-2104-4b18-9f56-c1581ed86d82
:END:
#+BEGIN_SRC emacs-lisp
(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)
#+END_SRC
**** block agenda views
***** default sorting
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 5b2e0510-1100-421e-ad47-ddc663d6efad
:END:
This gives more flexibility in ignoring items with timestamps
#+BEGIN_SRC emacs-lisp
(setq org-agenda-tags-todo-honor-ignore-options t)
#+END_SRC
By default I want block agendas to sort based on the todo keyword (with NEXT being up top as these have priority).
#+BEGIN_SRC emacs-lisp
(setq org-agenda-cmp-user-defined
'(lambda (a b)
(let ((pa (- (length (member
(get-text-property 1 'todo-state a)
org-x-agenda-todo-sort-order))))
(pb (- (length (member
(get-text-property 1 'todo-state b)
org-x-agenda-todo-sort-order)))))
(cond ((or (null pa) (null pb)) nil)
((> pa pb) +1)
((< pa pb) -1)))))
#+END_SRC
***** custom commands
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: c82f432c-be19-477f-b20f-b768bc573c70
:END:
These agenda commands are the center of the gtd workflow.
#+BEGIN_SRC emacs-lisp
(defmacro nd/org-x-mk-skip-function (&rest body)
"Return a skip function with BODY.
The only thing this function does is `save-excursion' and `widen'."
`(lambda () (save-excursion (widen) ,@body)))
(defmacro nd/org-x-mk-super-agenda-pred (&rest body)
"Return a predicate function with BODY.
This is meant to be used in `org-super-agenda-groups'. For each item,
the returned function will navigate from the agenda buffer to the
original org entry before executing BODY."
`(lambda (item)
2020-07-07 23:23:38 -04:00
(-when-let (marker (get-text-property 1 'org-marker item))
(with-current-buffer (marker-buffer marker)
(goto-char marker)
,@body))))
(defconst nd/org-x-task-status-priorities
'((:archivable . -1)
(:complete . -1)
(: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)))
(setq
org-agenda-custom-commands
`(("a"
"Calendar View"
2020-07-07 23:23:38 -04:00
((agenda
""
((org-agenda-skip-function
'(org-x-skip-headings-with-tags
'("NA" "%maybe" "REFILE")))
2020-08-07 16:29:44 -04:00
(org-agenda-sorting-strategy '(time-up deadline-up scheduled-up category-keep))
2020-07-07 23:23:38 -04:00
(org-agenda-include-diary t)
(org-super-agenda-groups
`((:name "Morning routine"
:pred ,(nd/org-x-mk-super-agenda-pred
(equal "morning" (org-entry-get nil "X-ROUTINE")))
:order 0)
(:name "Evening routine"
:pred ,(nd/org-x-mk-super-agenda-pred
(equal "evening" (org-entry-get nil "X-ROUTINE")))
:order 3)
2020-08-07 16:29:44 -04:00
(:name "Calendar" :order 1 :time-grid t)
(:name "Habits" :order 6 :habit t)
(:name "Deadlined" :order 4 :deadline t)
(:name "Scheduled" :order 5 :scheduled t)))))))
("t"
"Task View"
((tags-todo
"-NA-REFILE-%inc/TODO|NEXT|WAIT|HOLD|CANC"
((org-agenda-overriding-header "Tasks")
(org-agenda-skip-function
,(nd/org-x-mk-skip-function
(let ((keyword (org-x-is-todoitem-p)))
;; currently we assume that periodicals have no TODOs
(cond
;; skip over held/canc projects
((and (member keyword org-x-project-skip-todostates)
(org-x-is-project-p))
(org-x-skip-subtree))
;; skip iterators
((org-x-is-iterator-heading-p)
(org-x-skip-heading))
;; skip project headings
((org-x-is-project-p)
(org-x-skip-heading))
;; skip canceled tasks
((and (equal keyword "CANC")
(org-x-is-task-p))
(org-x-skip-heading))
;; skip habits
((org-x-is-habit-heading-p)
(org-x-skip-heading))))))
(org-agenda-todo-ignore-with-date t)
(org-agenda-sorting-strategy '(user-defined-up category-keep))
(org-super-agenda-groups
`((:auto-map
,(nd/org-x-mk-super-agenda-pred
(let* ((is-atomic (org-x-is-atomic-task-p))
;; lump inert and active non-atomic tasks together
(status (--> (org-x-task-status)
(if (and (not is-atomic) (eq it :inert))
:active it)))
(priority (alist-get status nd/org-x-task-status-priorities)))
(unless (< priority 0)
(--> status
(symbol-name it)
(substring it 1)
(s-replace "-" " " it)
(s-titleize it)
(concat "%s.%s " it)
(format it priority (if is-atomic 1 0))
(concat it (if is-atomic " (α)" " (σ)")))))))
(:discard (:anything t))))))))
("p"
"Project View"
((tags-todo
2020-06-04 18:24:41 -04:00
"-NA-REFILE-%inc"
((org-agenda-overriding-header "Projects")
(org-agenda-skip-function
,(nd/org-x-mk-skip-function
(cond ((or (org-x-is-iterator-heading-p)
(org-x-is-periodical-heading-p))
(org-x-skip-subtree))
((not (org-x-is-project-p))
(org-x-skip-heading)))))
(org-agenda-sorting-strategy '(category-keep))
(org-super-agenda-groups
`((:auto-map
,(nd/org-x-mk-super-agenda-pred
;; TODO this ain't DRY
(let* ((status (org-x-get-project-status))
(priority (alist-get status nd/org-x-project-status-priorities))
(is-subproject (org-x-headline-has-parent 'org-x-is-todoitem-p))
;; skip anything that is in a CANC or HOLD project
(is-masked (when is-subproject
(or (org-x-is-todo-child "CANC")
(org-x-is-todo-child "HOLD")))))
(unless (or is-masked (< priority 0))
(--> status
(symbol-name it)
(substring it 1)
(s-replace "-" " " it)
(s-titleize it)
(concat "%s.%s " it)
(format it (if is-subproject 1 0) priority)
2020-06-04 18:24:41 -04:00
(concat it (if is-subproject " (σ)" " (τ)")))))))
(:discard (:anything t))))))))
("i"
"Incubator View"
((tags
"-NA-REFILE+%inc"
((org-agenda-overriding-header "Incubator")
(org-agenda-skip-function
,(nd/org-x-mk-skip-function
(let ((keyword (org-x-is-todoitem-p)))
(cond
;; skip done/canc projects
((and (member keyword org-done-keywords)
(org-x-is-project-p))
(org-x-skip-subtree))
;; skip project tasks
((and keyword (org-x-is-project-task-p))
(org-x-skip-heading))
;; skip done/canc tasks
((member keyword org-done-keywords)
(org-x-skip-heading))
;; skip non-tasks if they don't have a timestamp
((and (not keyword)
(not (org-x-is-timestamped-heading-p)))
(org-x-skip-heading))))))
(org-agenda-sorting-strategy '(category-keep))
(org-super-agenda-groups
`((:name "Past Deadlines" :deadline past)
(:name "Future Deadlines" :deadline future)
(:name "Stale Appointments" :pred
,(nd/org-x-mk-super-agenda-pred
(org-x-is-stale-heading-p)))
(:name "Future Appointments" :pred
,(nd/org-x-mk-super-agenda-pred
(and (not (org-x-is-todoitem-p)))))
(:name "Tasks" :pred
,(nd/org-x-mk-super-agenda-pred
(org-x-is-task-p)))
(:name "Toplevel Projects" :pred
,(nd/org-x-mk-super-agenda-pred
(and (not (org-x-headline-has-parent #'org-x-is-todoitem-p))
(org-x-is-project-p))))
(:name "Projects" :pred
,(nd/org-x-mk-super-agenda-pred
(and (org-x-headline-has-parent #'org-x-is-todoitem-p)
(org-x-is-project-p))))
(:discard (:anything t))))))))
("P"
"Periodical View"
((tags
"-NA-REFILE"
((org-agenda-overriding-header "Iterator Status")
(org-agenda-skip-function
,(nd/org-x-mk-skip-function
(cond
((not (org-x-is-periodical-heading-p))
(org-x-skip-heading))
((org-x-headline-has-parent 'org-x-is-periodical-heading-p)
(org-x-skip-heading)))))
(org-agenda-sorting-strategy '(category-keep))
(org-super-agenda-groups
`((:auto-map
,(nd/org-x-mk-super-agenda-pred
(cl-case (org-clone-get-periodical-status)
2019-12-30 01:27:46 -05:00
(:uninit "0. Uninitialized")
(:unscheduled "0. Unscheduled")
(:empt "1. Empty")
(:actv "2. Active")
(t "3. Other"))))))))))
("I"
"Iterator View"
((tags
"-NA-REFILE"
((org-agenda-overriding-header "Iterator Status")
(org-agenda-skip-function
,(nd/org-x-mk-skip-function
(cond
((not (org-x-is-iterator-heading-p))
(org-x-skip-heading))
((org-x-headline-has-parent 'org-x-is-iterator-heading-p)
(org-x-skip-heading)))))
(org-agenda-sorting-strategy '(category-keep))
(org-super-agenda-groups
`((:auto-map
,(nd/org-x-mk-super-agenda-pred
(cl-case (org-clone-get-iterator-status)
2019-12-22 10:56:56 -05:00
(:uninit "0. Uninitialized")
(:project-error "0. Project Error")
(:unscheduled "0. Unscheduled")
(:empt "1. Empty")
(:actv "2. Active")
(t "3. Other"))))))))))
("r" "Refile"
((tags "REFILE" ((org-agenda-overriding-header "Tasks to Refile"))
(org-tags-match-list-sublevels nil))))
("f" "Flagged"
((tags "%flag" ((org-agenda-overriding-header "Flagged Tasks")))))
("e"
"Critical Errors"
((tags
"-NA-REFILE-%inc"
((org-agenda-overriding-header "Critical Errors")
(org-agenda-skip-function
,(nd/org-x-mk-skip-function
(let ((keyword (org-x-is-todoitem-p)))
(cond
((org-x-is-habit-heading-p)
(org-x-skip-heading))
((org-x-is-iterator-heading-p)
(org-x-skip-subtree))
((org-x-is-periodical-heading-p)
(org-x-skip-subtree))))))
(org-super-agenda-groups
`((:name "Discontinuous Projects" :pred
,(nd/org-x-mk-super-agenda-pred
(and (org-x-is-todoitem-p)
(org-x-has-discontinuous-parent))))
;; TODO this is redundant, only thing this checks is
;; project headers
(:name "Done Unclosed" :pred
,(nd/org-x-mk-super-agenda-pred
(let ((keyword (org-x-is-todoitem-p)))
(and keyword
(member keyword org-done-keywords)
(not (org-x-is-closed-heading-p))))))
(:name "Undone Closed" :pred
,(nd/org-x-mk-super-agenda-pred
(let ((keyword (org-x-is-todoitem-p)))
(and keyword
(not (member keyword org-done-keywords))
(org-x-is-closed-heading-p)))))
(:name "Missing Creation Timestamp" :pred
,(nd/org-x-mk-super-agenda-pred
;; TODO extend this to non-todoitems
(-when-let (kw (org-x-is-task-p))
(and
(not (member kw org-done-keywords))
(not (org-x-is-created-heading-p))))))
(:name "Future Creation Timestamp" :pred
,(nd/org-x-mk-super-agenda-pred
;; TODO extend this to non-todoitems
(-when-let (kw (org-x-is-task-p))
(and
(not (member kw org-done-keywords))
(org-x-heading-compare-timestamp
#'org-x-is-created-heading-p 0 t)))))
(:discard (:anything t))))))))
("A"
"Archivable Tasks and Projects"
((tags
"-NA-REFILE"
((org-agenda-overriding-header "Archive")
(org-agenda-skip-function
,(nd/org-x-mk-skip-function
(let ((keyword (org-x-is-todoitem-p)))
(cond
;; skip all non-archivable projects
((and keyword
(org-x-is-project-p)
(not (eq :archivable (org-x-get-project-status))))
(org-x-skip-subtree))
;; skip all incubator tasks
((org-x-headline-has-tag-p "%inc")
(org-x-skip-heading))
;; skip all project tasks
((and keyword (org-x-is-project-task-p))
(org-x-skip-heading))
;; skip all tasks not marked done or archivable
((and keyword
(org-x-is-task-p)
(not (eq :archivable (org-x-task-status))))
(org-x-skip-heading))
;; skip all non-todoitems that are not stale
((and (not keyword)
(not (org-x-is-stale-heading-p)))
(org-x-skip-heading))))))
(org-agenda-sorting-strategy '(category-keep))
(org-super-agenda-groups
`((:name "Atomic Tasks" :pred
,(nd/org-x-mk-super-agenda-pred
(org-x-is-atomic-task-p)))
(:name "Toplevel Projects" :pred
,(nd/org-x-mk-super-agenda-pred
(and (not (org-x-headline-has-parent #'org-x-is-todoitem-p))
(org-x-is-project-p))))
(:name "Projects" :pred
,(nd/org-x-mk-super-agenda-pred
(and (org-x-headline-has-parent #'org-x-is-todoitem-p)
(org-x-is-project-p))))
(:name "Appointments" :anything)))))))))
#+END_SRC
** gtd next generation
GTD is great but has many limitations...mostly due to the fact that it was originally made on paper. This is meant to extend the GTD workflow into a comprehensive tracking engine that can be used and analyze and project long-term plans and goals.
*** logging
**** drawer
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: ff6c8da8-a1dd-442a-aa64-89eabe6ba21e
:END:
I prefer all logging to go in a seperate drawer (aptly named) which allows easier navigation and parsing for data analytics.
#+BEGIN_SRC emacs-lisp
(setq org-log-into-drawer "LOGGING"
org-clock-into-drawer "CLOCKING")
#+END_SRC
**** events
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 9d45274d-92c3-4a52-842e-14af7173a8c3
:END:
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).
#+BEGIN_SRC emacs-lisp
(setq org-log-done 'time
org-log-redeadline 'time
org-log-reschedule 'time)
#+END_SRC
**** repeated tasks
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 6da71583-1bb7-4031-92f1-9dc0e1f422bf
:END:
In these cases, it is nice to know what happened during each cycle, so force notes.
#+BEGIN_SRC emacs-lisp
(setq org-log-repeat 'note)
#+END_SRC
*** sqlite backend
Org mode is great and all, but in many cases, text files just won't cut it. Hardcore data analysis is one of them, so make functions to shove org files (specifically archive files) into a sqlite database
**** load path
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 78bf6eb7-d3d7-43e9-8a03-ecedd7ca19e3
:END:
#+BEGIN_SRC emacs-lisp
2019-05-01 17:13:36 -04:00
(add-to-list 'load-path (nd/expand-local-pkg-directory "org-sql"))
(require 'org-sql)
#+END_SRC
**** customized variables
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 9b5b3587-676a-45da-84c8-c88a5f0ac348
:END:
These are variables that I set for my use but will not go into the eventual package
#+BEGIN_SRC emacs-lisp
(setq org-sql-use-tag-inheritance t
2019-05-13 18:29:21 -04:00
org-sql-ignored-properties nil
org-sql-ignored-tags nil
org-sql-ignored-link-types 'all
org-sql-included-headline-planning-types '(:closed :scheduled :deadline)
org-sql-included-contents-timestamp-types '(active)
org-sql-store-planning-timestamps t
org-sql-store-logbook-other t
org-sql-store-logbook-planning-changes t
org-sql-store-logbook-state-changes t
org-sql-store-clock-notes t
org-sql-store-clocks t
org-sql-files '(
2019-05-13 18:29:21 -04:00
"~/Org/.archive/"
"~/Org/general.org_archive"
"~/Org/general.org"
;; "~/Org/test1.org_archive"
;; "~/Org/test2.org_archive"
"~/Org/incubator.org"
"~/Org/projects/"
2019-05-19 12:59:10 -04:00
"~/Org/repeater.org_archive"
))
#+END_SRC
** tomato mode
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 2ea3cbd8-ce5d-40d7-8528-2370a499d020
:END:
This really means "super awesome pomodoro implementation." =Tomato-mode= sounds cooler and more emacs like.
#+BEGIN_SRC emacs-lisp
2019-05-02 23:32:50 -04:00
(use-package sound-wav :straight t)
2019-05-01 17:13:36 -04:00
(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"))
#+END_SRC
** brain
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 08853295-71db-438f-b78f-20277d73ff46
:END:
#+BEGIN_SRC emacs-lisp
(use-package org-brain
:straight t
:init
(setq org-brain-path "~/Org/reference")
(with-eval-after-load 'evil
(evil-set-initial-state 'org-brain-visualize-mode 'emacs)))
#+END_SRC
* tools
** printing
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 43e2e3eb-81e7-4b95-90c2-0a5cc3efc3e6
:END:
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.
#+BEGIN_SRC emacs-lisp
(defun nd/find-printers ()
"Return a list of available printers on Unix systems."
(when (executable-find "lpstat")
(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)))))
#+END_SRC
** magit
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 67e11402-a9e5-4aae-8644-0e2c4f9ad2bc
:END:
#+BEGIN_SRC emacs-lisp
(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)))
#+END_SRC
** dired
*** compression
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 2b3bb88b-c161-4cc0-a6c9-2674ad067cdb
:END:
Only supports tar.gz, tar.bz2, tar.xz, and .zip by default. Add support for more fun algos such as lzo and zpaq
#+BEGIN_SRC emacs-lisp
(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 &"))))
#+END_SRC
*** formatting for humans
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 543a708a-d655-4adb-8cfd-2b14f840a552
:END:
make sizes human readable
#+BEGIN_SRC emacs-lisp
2020-09-13 15:09:20 -04:00
(setq dired-listing-switches "-vAlh --group-directories-first")
#+END_SRC
*** mu4e attachments
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: dcef1a29-0ef1-4404-893d-136589ed9b2f
:END:
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.
#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC
*** directory sized
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: ea8dd806-2c66-43b3-ac58-13a0e95f9e42
:END:
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).
#+BEGIN_SRC emacs-lisp
(use-package dired-du
:straight t
:config
(setq dired-du-size-format t))
#+END_SRC
*** mounted devices
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 8217345a-5ad4-4118-8c4f-8d5742cca393
:END:
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.
#+BEGIN_SRC emacs-lisp
2020-02-29 19:55:56 -05:00
(defun nd/find-devices ()
"Navigate to mounted devices."
(interactive)
2020-02-29 19:55:56 -05:00
(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)))))
#+END_SRC
*** filtering
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 90203849-e008-49a9-94de-029bb845c06b
:END:
Filtering is useful for obvious reasons
#+BEGIN_SRC emacs-lisp
(use-package dired-narrow
:straight t)
#+END_SRC
** pdf-tools
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 41b3ccf4-1169-45d1-b6db-88a216eed9c3
:END:
#+BEGIN_SRC emacs-lisp
;; 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))
#+END_SRC
** mu4e
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: c23cee1b-00bf-48bd-94f6-f280f01777ea
:END:
2020-04-28 17:37:17 -04:00
Initialize by running the following command in a shell to tell mu about my personal email addresses and where mail should be stored:
#+BEGIN_SRC sh
mu init --my-address=ndwar@yavin4.ch --my-address=natedwarshuis@gmail.com --my-address=ndwarshuis3@gatech.edu --my-address=ndwarsh@emory.edu --maildir=/mnt/data/Mail
#+END_SRC
The following will only run if the =mu= command is found (which it won't be if this is run on windows).
#+BEGIN_SRC emacs-lisp
(nd/when-bin "mu"
(require 'mu4e)
(defun nd/make-mu4e-context (name dir addr smtp-srv sent-behavior)
2020-04-28 17:37:17 -04:00
(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))
2020-04-28 17:37:17 -04:00
(shortcuts (--map (list :maildir (car it) :key (cdr it))
`((,trash . ?t)
(,drafts . ?d)
(,sent . ?s)
(,archive . ?a)
(,inbox . ?i))))
(mf (lambda (d msg)
2020-04-28 17:37:17 -04:00
(-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)
(mu4e-sent-messages-behavior . ,sent-behavior)
(smtpmail-stream-type . starttls)
(smtpmail-smtp-server . ,smtp-srv)
(smtpmail-smtp-service . 587)
(smtpmail-smtp-user . ,addr)
(user-mail-address . ,addr)
2020-04-28 17:37:17 -04:00
(mu4e-maildir-shortcuts . ,shortcuts)))))
(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 "systemctl --user start mbsync"
mu4e-use-fancy-chars nil
;; 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
mu4e-compose-signature-auto-include nil ;; sigs are annoying by default
mu4e-compose-signature
(string-join
'("Nathan Dwarshuis"
""
"PhD Candidate - Biomedical Engineering - Krish Roy Lab"
"Georgia Institute of Technology and Emory University"
"ndwarshuis3@gatech.edu - (616) 307-4957"
""
"NOTE - I do not use email on my mobile devices"
"Please use my phone number for urgent messages")
"\n")
;; 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
mu4e-contexts
(list
(nd/make-mu4e-context "personal"
"yavin4"
"ndwar@yavin4.ch"
"peart4prez.yavin4.ch"
'sent)
(nd/make-mu4e-context "alpha"
"gmail"
"natedwarshuis@gmail.com"
"smtp.gmail.com"
'delete)
(nd/make-mu4e-context "gatech"
"gatech"
"ndwarshuis3@gatech.edu"
"smtp.office365.com"
'sent)
(nd/make-mu4e-context "emory"
"emory"
"ndwarsh@emory.edu"
"smtp.office365.com"
'sent)))
;; 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
(if (executable-find "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)))))
(require 'smtpmail)
;; (require 'smtpmail-async)
;; (require 'secrets)
;; (setq secrets-enabled t)
;; (add-to-list 'auth-sources "secrets:default")
(setq send-mail-function 'smtpmail-send-it
2019-10-25 22:05:44 -04:00
smtpmail-debug-info nil
message-send-mail-function 'smtpmail-send-it)
2020-11-10 23:39:53 -05:00
(setq auth-sources
(list (no-littering-expand-etc-file-name "authinfo_mu4e.gpg")))
2020-06-21 10:05:00 -04:00
;; TODO get this to work
;; (setq auth-sources '("secret:Default Keyring"))
2020-11-10 23:39:53 -05:00
(defun nd/lookup-oauth-secret (type user)
(->> (format "secret-tool lookup user %s type %s" user type)
(shell-command-to-string)))
(defun nd/xoauth2-get-secrets (host user port)
(when
(or (and (string= host "smtp.office365.com")
(string= user "ndwarshuis3@gatech.edu")
(string= port "587"))
(and (string= host "smtp.gmail.com")
(string= user "natedwarshuis@gmail.com")
(string= port "587")))
2020-11-10 23:39:53 -05:00
(list :token-url (nd/lookup-oauth-secret "token_url" user)
:client-id (nd/lookup-oauth-secret "client_id" user)
:client-secret (nd/lookup-oauth-secret "client_secret" user)
:refresh-token (nd/lookup-oauth-secret "refresh_token" user))))
(use-package auth-source-xoauth2
:straight t
:after smtpmail
:config
(setq auth-source-xoauth2-creds #'nd/xoauth2-get-secrets)
(add-to-list 'smtpmail-auth-supported 'xoauth2)
(auth-source-xoauth2-enable))
(use-package org-mu4e
:after (org mu4e)
:config
(setq
;; for using mu4e in org-capture templates
org-mu4e-link-query-in-headers-mode nil
;; for composing rich-text emails using org mode
org-mu4e-convert-to-html t)))
#+END_SRC
** shell
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: ef1d527b-2cf3-418a-8112-9add9951ab64
:END:
#+begin_src emacs-lisp
(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"))
#+END_SRC
** ediff
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 209f5eb6-aa73-423d-87a1-038e97902c57
:END:
#+BEGIN_SRC emacs-lisp
(setq ediff-window-setup-function 'ediff-setup-windows-plain)
#+END_SRC
** mulitmedia controls
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 46d2458c-5c6d-4487-b066-ecf250911fe3
:END:
2019-05-26 16:19:40 -04:00
This controls the music player daemon (MPD) which in my case is modidy with several backends (notably a subsonic client).
#+BEGIN_SRC emacs-lisp
(use-package mingus
:straight t
:config
(setq mingus-mpd-host "localhost"
mingus-mpd-port 6600))
#+END_SRC
* keybindings
For the sake of my sanity, all bindings go here. Note this means I don't use =:bind= in use-package forms.
** setup
2020-06-06 14:28:51 -04:00
Most of my modifiers are reloacted using xkb and [[https://github.com/alols/xcape][xcape]].
The xkb layout can be found [[https://github.com/ndwarshuis/xkb-hypermode][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
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 34ea6396-6a8d-4db9-a2e2-c14b5a6c8f36
:END:
Everyone forgets keybindings. When typing a key chord, this will display a window with all possible completions and their commands.
#+BEGIN_SRC emacs-lisp
(use-package which-key
:straight t
:delight
:init
(which-key-mode))
#+END_SRC
** hydra
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 13437ca2-ef6a-40a4-b528-85ea019cceb1
:END:
#+BEGIN_SRC emacs-lisp
(use-package hydra
:straight t)
(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."
`(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."
`(nd/hydra-standard ,nd/hydra-standard-navigation-map "nav"
,keymap ,@cmds))
#+END_SRC
** evil
I like being evil. All package and custom bindings go here.
*** base
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 07d06b91-fa97-41f5-8d12-ac0d1e86c988
:END:
#+BEGIN_SRC emacs-lisp
(use-package evil
:straight t
2020-10-12 17:08:40 -04:00
:after undo-tree
:init
;; this is required to make evil collection work
2019-10-25 22:05:44 -04:00
(setq evil-want-integration t
2020-10-12 17:08:40 -04:00
evil-want-keybinding nil
;; evil now integrates with undo-tree
evil-undo-system 'undo-tree)
:config
(evil-mode 1))
#+END_SRC
*** search
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 475d48cc-c9a8-4dc5-89c4-93ff277521a9
:END:
By default search uses the default emacs built-in search module. Not evil enough (which really means vim search has features that I like)
#+BEGIN_SRC emacs-lisp
(evil-select-search-module 'evil-search-module 'evil-search)
#+END_SRC
*** motion
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: e0e0cdb4-4152-47a2-86bd-adaac1c8bbf9
:END:
By default, emacs counts a sentence as having at least 2 spaces after punctuation. Make this behave more like vim.
#+BEGIN_SRC emacs-lisp
(setq sentence-end-double-space nil)
#+END_SRC
*** evil state defaults
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 198d6658-b9e4-4ebc-9bcd-894d270561da
:END:
Some modes use primitive emacs bindings by default. Educate them.
#+BEGIN_SRC emacs-lisp
(add-to-list 'evil-motion-state-modes 'ess-help-mode)
(add-to-list 'evil-insert-state-modes 'inferior-ess-mode)
#+END_SRC
*** enhancements
delightfully ripped off from vim plugins
**** surround
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 3a7c0910-e15f-4cc6-857e-60abf7959834
:END:
#+BEGIN_SRC emacs-lisp
(use-package evil-surround
:straight t
:after evil
:config
(global-evil-surround-mode 1))
#+END_SRC
**** commentary
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: a6659e64-d165-408e-9225-801988ca36c6
:END:
#+BEGIN_SRC emacs-lisp
(use-package evil-commentary
:straight t
:after evil
:delight
:config
(evil-commentary-mode))
#+END_SRC
**** replace with register
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 7da0c78b-cf6f-4457-b9ba-5b797a8ce817
:END:
#+BEGIN_SRC emacs-lisp
(use-package evil-replace-with-register
:straight t
:after evil
:config
(evil-replace-with-register-install))
#+END_SRC
**** twiddle case
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 179a2614-176b-43d3-98f8-4eda09240539
:END:
#+BEGIN_SRC emacs-lisp
(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)
#+END_SRC
*** unbind emacs keys
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: eae4df13-d158-4a7c-8cce-5b00ae4325f4
:END:
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.
#+BEGIN_SRC emacs-lisp
(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")))
#+END_SRC
*** evil-org
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: ea04f214-0510-4a04-8ee4-2ac8e6955dde
:END:
#+BEGIN_SRC emacs-lisp
(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)
(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
"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))
#+END_SRC
*** evil-magit
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 97e88553-f0ae-46ae-9409-f473886c7850
:END:
#+BEGIN_SRC emacs-lisp
(use-package evil-magit
:straight t
:after (evil magit))
#+END_SRC
*** visual line mode
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 17a6fa9c-ba87-49d1-bf3d-d9253d632831
:END:
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.
#+BEGIN_SRC emacs-lisp
(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)
#+END_SRC
2019-05-08 15:35:22 -04:00
*** outline-minor-mode
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: f218f536-b80b-4b73-8059-ad013bed901b
:END:
2019-05-08 15:35:22 -04:00
#+BEGIN_SRC emacs-lisp
(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)
#+END_SRC
*** mingus
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: d4c70a6b-db7f-4707-a694-d5b0b96fff2d
:END:
#+BEGIN_SRC emacs-lisp
(evil-define-key '(visual normal) mingus-browse-map
(kbd "RET") #'mingus-down-dir-or-play-song
"^" #'mingus-open-parent
"s" #'mingus-search
"a" #'mingus-add-things-at-p)
(evil-define-key '(visual normal) mingus-playlist-map
(kbd "RET") #'mingus-play
"m" #'mingus-mark
"D" #'mingus-del-marked
"U" #'mingus-unmark-all)
#+END_SRC
*** collection
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: a209edf3-33d2-4d71-958d-02f11c7b74f8
:END:
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.
#+BEGIN_SRC emacs-lisp
(use-package evil-collection
:straight t
:after evil
:init
(setq evil-collection-mode-list
2020-05-31 12:43:39 -04:00
'(anaconda-mode arc-mode company comint custom debug edebug dired
doc-view ebib ediff elfeed flycheck ivy help
minibuffer mu4e profiler term which-key))
(setq evil-collection-setup-minibuffer t)
:config
(evil-collection-init))
#+END_SRC
**** dired
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 88bf6ee9-a893-4953-a7f1-b6235cffbbdc
:END:
Dired makes new buffers by default. Use =find-alternate-file= to avoid this.
#+BEGIN_SRC emacs-lisp
(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)
#+END_SRC
**** comint
***** common
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 6e4a7b7a-2898-4135-a9a8-268f096b8b3e
:END:
#+BEGIN_SRC emacs-lisp
;; (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)
#+END_SRC
***** ess
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 80febc0a-836b-49d9-bdb7-8d479a54af2c
:END:
#+BEGIN_SRC emacs-lisp
(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)))
#+END_SRC
***** haskell interactive mode
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: b163e760-8fbc-42c4-ac09-944870333732
:END:
Not to be confused with =interactive-haskell-mode= which is part of the editing buffer
#+BEGIN_SRC emacs-lisp
(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)
2020-04-15 19:12:06 -04:00
(evil-define-key 'normal haskell-interactive-mode-map
(kbd "[[") #'haskell-interactive-mode-prompt-previous
(kbd "]]") #'haskell-interactive-mode-prompt-next)
#+END_SRC
**** pdf-view
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: e2613015-2a9d-40c8-a9bc-5342ae90b5bf
:END:
#+BEGIN_SRC emacs-lisp
;; 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)
#+END_SRC
**** term
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 2d80ac4a-27ef-425c-ac5e-c31afd36d77c
:END:
Since I use vi mode in my terminal emulator, need to preserve the escape key's raw behavior
#+BEGIN_SRC emacs-lisp
(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)
#+END_SRC
**** lisp
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 4da53afd-d42d-46e1-9d4b-eb78a3d7b7d5
:END:
#+BEGIN_SRC emacs-lisp
(evil-define-key 'normal emacs-lisp-mode-map
"gh" #'lispy-left
"gl" #'lispy-flow
"gj" #'lispy-down
"gk" #'lispy-up)
#+END_SRC
2020-10-18 09:58:56 -04:00
**** ivy
:PROPERTIES:
:ID: a546bc23-bb8d-4246-b56b-964c1d5799c1
:END:
#+begin_src emacs-lisp
(evil-define-key 'normal ivy-minibuffer-map
"gg" #'ivy-beginning-of-buffer
"G" #'ivy-end-of-buffer)
#+end_src
** 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
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: b911386e-b056-4816-bbf5-fae2ec689a0e
:END:
#+BEGIN_SRC emacs-lisp
(add-hook 'org-mode-hook
(lambda ()
;; use the hyper keys/vim arrows with the shifters instead of shift/arrows
(local-set-key (kbd "H-k") 'org-shiftup)
(local-set-key (kbd "H-l") 'org-shiftright)
(local-set-key (kbd "H-j") 'org-shiftdown)
(local-set-key (kbd "H-h") 'org-shiftleft)
2020-06-25 11:00:52 -04:00
;; storing links is important, make a shortcut
(local-set-key (kbd "C-c l") 'org-store-link)
;; 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)
2019-05-10 02:54:34 -04:00
;; add time shifter
(local-set-key (kbd "C-c C-x t") 'org-x-time-shift)
;; 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)
;; add org-brain shortcuts
(local-set-key (kbd "C-c W") 'org-brain-refile)))
(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 W") 'org-brain-refile)
(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)))
(setq org-super-agenda-header-map (make-sparse-keymap))
(define-key org-super-agenda-header-map (kbd "<tab>") #'origami-toggle-node)
#+END_SRC
*** mu4e
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 63ca7479-3ddb-4480-a2ed-b55031238958
:END:
#+BEGIN_SRC emacs-lisp
;; (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))
#+END_SRC
*** dired
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 6fbd3521-81ae-48ad-a7e3-626073fd3fdc
:END:
#+BEGIN_SRC emacs-lisp
(define-key dired-mode-map (kbd "C-x g") 'magit)
#+END_SRC
*** outline-magic
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 2810a854-29d6-4f34-866e-767f9c64c3c1
:END:
#+BEGIN_SRC emacs-lisp
(define-key outline-minor-mode-map (kbd "<tab>") 'outline-cycle)
#+END_SRC
*** ess
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 2e826475-500e-40fc-8271-4dc835ebce04
:END:
They removed the underscore-inserts-arrow feature. Bring it back.
#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC
*** elisp
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 60ef91fb-d0ba-42d1-84f5-24e8a5cd0507
:END:
#+BEGIN_SRC emacs-lisp
(nd/hydra-standard-int emacs-lisp-mode-map
(:send-line . eval-last-sexp)
(:send-group . eval-defun)
(:send-buffer . eval-buffer))
(define-key emacs-lisp-mode-map (kbd "M-RET") #'emr-show-refactor-menu)
#+END_SRC
*** python
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: fa4e5b43-256c-4028-b1e0-c7a535f0750c
:END:
The only thing I like about elpy is the interactive shell
#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC
*** haskell
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 13ba50e8-b72d-498f-b7d8-d9f24cbb138d
:END:
#+BEGIN_SRC emacs-lisp
(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
2020-03-24 21:15:53 -04:00
(: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)))
#+END_SRC
*** magit
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: b9f20b72-adda-4b89-a053-4f391e4cc95e
:END:
#+BEGIN_SRC emacs-lisp
;; interferes with window hydra
(define-key magit-mode-map (kbd "M-w") nil)
#+END_SRC
*** pyenv
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 64283681-e36f-4199-9f51-72b668a15bac
:END:
This key collides with plenty of other stuff, notably scheduling in org mode
#+BEGIN_SRC emacs-lisp
(define-key pyenv-mode-map (kbd "C-c C-s") nil)
#+END_SRC
*** counsel
2020-11-23 23:34:14 -05:00
:PROPERTIES:
:ID: bd70f69a-95b3-4da1-9f71-090b63e0e6e7
:END:
#+begin_src emacs-lisp
(define-key counsel-find-file-map (kbd "<tab>") #'ivy-alt-done)
#+end_src
** global
*** function
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 0a301da7-8d12-45f6-8cfd-4feb9ff50504
:END:
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).
#+BEGIN_SRC emacs-lisp
(global-set-key (kbd "<f1>") 'org-agenda)
(global-set-key (kbd "C-<f1>") 'org-brain-visualize)
(global-set-key (kbd "<f2>") 'org-capture)
(global-set-key (kbd "<f3>") 'cfw:open-org-calendar)
(global-set-key (kbd "C-<f3>") 'nd/org-cluster-show-conflicts)
(global-set-key (kbd "C-S-<f3>") 'nd/org-cluster-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> s") #'swiper-thing-at-point)
(global-set-key (kbd "<f8> o") #'counsel-org-goto-all)
(global-set-key (kbd "<f8> f") #'flyspell-correct-at-point)
(defhydra hydra-multimedia (global-map "<f10>" :exit t)
"convenient multimedia controls"
("<f10>" mingus)
("b" mingus-browse)
("c" mingus-clear)
("p" mingus-pause)
("v" mingus-volume)
("r" mingus-repeat)
2019-05-26 16:18:01 -04:00
("s" mingus-stop)
("-" mingus-vol-down :exit nil)
("=" mingus-vol-up :exit nil)
("]" mingus-seek :exit nil)
("[" mingus-seek-backward :exit nil)
(">" mingus-next :exit nil)
("<" mingus-prev :exit nil))
(defhydra hydra-modes (global-map "<f11>" :exit t)
"convenient mode toggles"
("v" visual-line-mode)
("h" global-hl-line-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))
#+END_SRC
*** control/meta
2020-06-25 11:00:52 -04:00
:PROPERTIES:
:ID: 10325f1e-a084-4a37-9f37-6f8fd9acb960
:END:
#+BEGIN_SRC emacs-lisp
;; 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)
2020-10-18 09:58:56 -04:00
(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)
2020-02-29 19:55:56 -05:00
(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))
#+END_SRC
*** other
:PROPERTIES:
:ID: dff1f586-7231-4394-8f4c-2730dbe8a901
:END:
#+BEGIN_SRC emacs-lisp
;; exchange point and marker (I never saw the use for this)
(global-unset-key (kbd "C-x C-x"))
#+END_SRC