I use doom emacs to write notes, articles, and code, as an email client, and to keep track of projects and appointments. Most of this happens in org-mode.
;;; config.el -*- lexical-binding: t; -*-
(advice-remove 'set-window-buffer #'ad-Advice-set-window-buffer)
(setq user-full-name "Wolfgang Schwarz"
user-mail-address "wo@umsu.de")
(setq projectile-project-search-path '("~/words/" "~/notes/" "~/programming/"))
(evil-put-command-property 'evil-yank-line :motion 'evil-line)
;; (setq org-insert-heading-respect-content nil)
(map! :after evil-org
:map evil-org-mode-map
:ni "C-<return>" #'org-insert-heading-respect-content)
(setq-default delete-by-moving-to-trash t)
(setq undo-limit 80000000
evil-want-fine-undo t)
(auto-save-visited-mode 1)
(setq auto-save-timeout 20)
(setq auto-save-file-name-transforms
`((".*" "~/.emacsbup/" t)))
(setq backup-by-copying t ; don't clobber symlinks
backup-directory-alist '(("." . "~/.emacsbup"))
delete-old-versions t
make-backup-files t
vc-make-backup-files t ; backup files even if version controlled
kept-new-versions 6
kept-old-versions 2
version-control t) ; use versioned backups
Why is it so hard to set up spell-checking with multiple dictionaries?
;; (setq ispell-local-dictionary "en_GB")
;; (setq ispell-program-name "hunspell")
;; (setq ispell-hunspell-dictionary-alist '(("de_DE"
;; "[[:alpha:]]"
;; "[^[:alpha:]]"
;; "['.ß-]" 'many-otherchars
;; ("-r" "-d" "de_DE") nil utf-8)
;; ("en_GB"
;; "[[:alpha:]]"
;; "[^[:alpha:]]"
;; "[']" nil
;; ("-r" "-d" "en_GB") nil utf-8)))
;; (when (boundp 'ispell-hunspell-dictionary-alist)
;; (setq ispell-hunspell-dictionary-alist ispell-local-dictionary-alist))
;; For saving words to the personal dictionary, don't infer it from
;; the locale, otherwise it would save to ~/.hunspell_en_GB.
;; (setq ispell-personal-dictionary "~/.hunspell_personal")
;; The personal dictionary file has to exist, otherwise hunspell will
;; silently not use it.
The guess-language package guesses which spellchecker to use, but I can’t get it to work properly.
;; (setq guess-language-langcodes
;; '((en . ("en_GB" "English"))
;; (de . ("de_DE" "German"))))
;; (setq guess-language-languages '(en de))
;; (add-hook 'org-mode-hook (lambda () (guess-language-mode 1)))
So I’m switching manually:
;; (defun fd-switch-dictionary()
;; (interactive)
;; (let* ((dic ispell-current-dictionary)
;; (change (if (string= dic "deutsch8") "english" "deutsch8")))
;; (ispell-change-dictionary change)
;; (message "Dictionary switched from %s to %s" dic change)
;; ))
;; (global-set-key (kbd "<f9>") 'fd-switch-dictionary)
I get confused by automatically inserted closing brackets and parentheses.
(remove-hook 'doom-first-buffer-hook #'smartparens-global-mode)
;; (add-hook 'org-mode-hook 'turn-off-smartparens-mode)
Don’t erase whitespace just before cursor on auto-save, and prevent resulting error message, from here:
(after! ws-butler
(setq ws-butler-keep-whitespace-before-point t)
(setq ws-butler-trim-predicate
(lambda (beg end)
(let* ((current-line (line-number-at-pos))
(beg-line (line-number-at-pos beg))
(end-line (line-number-at-pos end))
;; Assuming the use of evil-mode for insert mode detection. Adjust if using a different system.
(in-insert-mode (and (bound-and-true-p evil-mode)
(eq 'insert evil-state))))
;; Return true (allow trimming) unless in insert mode and the current line is within the region.
(not (and in-insert-mode
(>= current-line beg-line)
(<= current-line end-line))))))
)
from stackoverflow:
(defun rename-file-and-buffer ()
"Renames current buffer and file it is visiting."
(interactive)
(let ((name (buffer-name))
(filename (buffer-file-name)))
(if (not (and filename (file-exists-p filename)))
(message "Buffer '%s' is not visiting a file!" name)
(let ((new-name (read-file-name "New name: " filename)))
(cond ((get-buffer new-name)
(message "A buffer named '%s' already exists!" new-name))
(t
(rename-file name new-name 1)
(rename-buffer new-name)
(set-visited-file-name new-name)
(set-buffer-modified-p nil)))))))
(add-to-list 'default-frame-alist '(fullscreen . maximized))
Show only headings on opening:
(setq org-startup-folded 'content)
(setq-default line-spacing 0.2)
I’m going back and forth between relative line numbers and no line numbers.
(setq display-line-numbers-type nil)
;(setq display-line-numbers-type 'relative)
(setq doom-modeline-enable-word-count t)
(setq
doom-font (font-spec :family "monospace" :size 15)
doom-theme 'doom-one
doom-enable-brighter-comments 1
+doom-dashboard-banner-file (expand-file-name "logo.png" doom-user-dir)
)
(after! org
(set-face-attribute 'org-link nil :weight 'normal :background nil)
(set-face-attribute 'org-code nil :foreground "#a9a1e1" :background nil)
(set-face-attribute 'org-date nil :foreground "#5B6268" :background nil)
(set-face-attribute 'org-level-1 nil :foreground "steelblue2" :background nil :height 1.2 :weight 'bold)
(set-face-attribute 'org-level-2 nil :foreground "slategray2" :background nil :height 1.1 :weight 'bold)
(set-face-attribute 'org-level-3 nil :foreground "SkyBlue2" :background nil :height 1.0 :weight 'normal)
(set-face-attribute 'org-level-4 nil :foreground "DodgerBlue2" :background nil :height 1.0 :weight 'normal)
(set-face-attribute 'org-level-5 nil :weight 'normal) (set-face-attribute 'org-level-6 nil :weight 'normal)
(set-face-attribute 'org-document-title nil :foreground "SlateGray1" :background nil :height 1.75 :weight 'bold)
)
(after! org
(setq org-ellipsis " ▾ "
org-bullets-bullet-list '("·"))
)
Don’t indent:
(setq org-startup-indented nil
org-adapt-indentation nil)
Center text:
(use-package olivetti
:commands olivetti-mode
:config
(setq olivetti-body-width 0.7)
(setq olivetti-minimum-body-width 90))
(add-hook 'org-mode-hook #'olivetti-mode)
(map!
:leader
:desc "toggle olivetti-mode" "t o" #'olivetti-mode
)
Hide slashes and stars:
(after! org
;; (setq org-hide-emphasis-markers t)
(setq org-hide-emphasis-markers nil)
)
Add colour to italics:
(after! org
(add-to-list 'org-emphasis-alist '("/" (italic :foreground "#dddd99")))
)
Properly display sub- and superscripts:
(after! org
(setq org-pretty-entities-include-sub-superscripts t)
)
(defun highlight-org-comment-blocks-with-comment-face ()
(set-face-foreground 'font-lock-comment-face "#99aabb")
(font-lock-add-keywords nil
'(("^[ \t]*#\\+begin_comment[ \t]*$" 0 'font-lock-comment-face t)
("^[ \t]*#\\+end_comment[ \t]*$" 0 'font-lock-comment-face t)
("^[ \t]*#\\+begin_comment[ \t]*\\([[:space:]\n]*\\(.\\|\n\\)*?\\)[ \t]*#\\+end_comment[ \t]*$"
(1 'font-lock-comment-face t)))))
(add-hook 'org-mode-hook 'highlight-org-comment-blocks-with-comment-face)
I often have two ‘+’ in a line, and never want to strike through text.
(after! org
(add-to-list 'org-emphasis-alist '("+" (:strike-through f)))
)
(use-package! org-appear
:hook (org-mode . org-appear-mode)
:config
(setq org-appear-autoemphasis t
org-appear-autosubmarkers t
org-appear-autolinks nil)
;; for proper first-time setup, `org-appear--set-elements'
;; needs to be run after other hooks have acted.
(run-at-time nil nil #'org-appear--set-elements))
This way, I can simply type LaTeX commands like ∀ or ¢ or \aleph to insert the relevant symbols:
(after! org
(setq org-pretty-entities t)
)
Some symbols I often use aren’t standardly recognised by org-pretty-entities. But we can add them:
(after! org
(setq org-entities-user '(
("bot" "\\bot" nil "" "" "" "⊥")
("top" "\⊤" nil "" "" "" "⊤")
("box" "$\\box$" nil "" "" "" "□")
("diamond" "$\\diamond$" nil "" "" "" "◇")
("Box" "$\\Box$" nil "" "" "" "□")
("Diamond" "$\Diamond$" nil "" "" "" "◇")
("boxright" "$\\boxright$" nil "" "" "" "□→")
("models" "$\\models$" nil "" "" "" "⊨")
("vdash" "$\\vdash$" nil "" "" "" "⊢")
("leadsto" "$\leadsto$" nil "" "" "" "↝")
("llb" "$\\llbracket$" nil "" "" "" "⟦")
("rrb" "$\\rrbracket$" nil "" "" "" "⟧")
)
)
)
Preview LaTeX environments in org buffers, mostly adapted from tecosaur:
(after! org
; cdlatex allows, among other things, inserting latex environments with C-c {:
;; (add-hook 'org-mode-hook 'turn-on-org-cdlatex)
; toggle LaTeX preview as cursor moves in/out:
(add-hook 'org-mode-hook 'org-fragtog-mode)
; the default dvipng program cuts off qtree lines, so we use dvisvgm instead:
(setq org-preview-latex-default-process 'dvisvgm)
; make LaTex snippets look better:
(setq org-highlight-latex-and-related '(native script entities))
; automatically preview latex when file is opened:
(setq org-startup-latex-with-latex-preview t)
)
Customize rendering of LaTeX fragments:
(setq org-format-latex-header "\\documentclass{article}
\\usepackage[usenames]{xcolor}
\\usepackage[T1]{fontenc}
\\usepackage{mathtools}
\\usepackage{textcomp,txfonts,latexsym,amssymb}
\\usepackage[makeroom]{cancel}
\\usepackage{qtree}
\\usepackage{booktabs}
\\newcommand{\\sem}[2][]{\\mbox{$[\\![ \#2 ]\\!]^{\#1}$}}
\\newcommand{\\Fr}[1][]{\\mathfrak{\#1}}
\\newcommand{\\Sc}[1][]{\\mathfrak{\#1}}
\\renewcommand{\\t}[1]{\\ensuremath{\\langle #1 \\makebox[.2ex]{}\\rangle}}
\\pagestyle{empty}
\\setlength{\\textwidth}{\\paperwidth}
\\addtolength{\\textwidth}{-3cm}
\\setlength{\\oddsidemargin}{1.5cm}
\\addtolength{\\oddsidemargin}{-2.54cm}
\\setlength{\\evensidemargin}{\\oddsidemargin}
\\setlength{\\textheight}{\\paperheight}
\\addtolength{\\textheight}{-\\headheight}
\\addtolength{\\textheight}{-\\headsep}
\\addtolength{\\textheight}{-\\footskip}
\\addtolength{\\textheight}{-3cm}
\\setlength{\\topmargin}{1.5cm}
\\addtolength{\\topmargin}{-2.54cm}
\\usepackage{arev}
\\usepackage{arevmath}
")
Increase font-size:
(after! org
(setq org-format-latex-options (plist-put org-format-latex-options :scale 1.2))
)
Snippets are useful for quickly inserting environments, complex logic expressions and the like. (C-s brings up the menu of predefined snippets, as per below; M-x yas-new-snippet creates a new snippet.)
(setq yas-snippet-dirs '("~/.doom.d/snippets"))
The company package suggests completions for words. I rarely use this.
(after! company
(setq company-idle-delay 2
company-minimum-prefix-length 1)
(setq company-show-quick-access t)
;; only autocomplete words, not numerals:
(setq company-dabbrev-char-regexp "[A-z:-]")
;; make aborting less annoying:
(add-hook 'evil-normal-state-entry-hook #'company-abort)
)
I use separate org files for different projects (e.g. research, teaching, supervision, software projects). Often these org files lie in dedicated project directories, but they are all symlinked to my ~/org directory.
(after! org
(setq org-directory "~/org")
(setq org-agenda-files '("~/org"))
)
I use SPC - to quickly access the project files. (This doesn’t seem work if ~/org is a git repository because then symlinks are ignored.)
(map!
:leader
:desc "open ~/org file" "-" '(lambda () (interactive) (ido-find-file-in-dir "~/org/"))
)
(after! org
(setq org-todo-keywords '((sequence "TODO(t)" "NEXT(n)" "ACTV(a)" "WAIT(w)" "URGT(u)" "|" "DONE(d)" "CANC(c)")))
(setq org-todo-keyword-faces
(quote (("TODO" :foreground "#69f" :weight bold)
("NEXT" :foreground "#6cc" :weight bold)
("ACTV" :foreground "#fc6" :weight bold)
("URGT" :foreground "#f66" :weight bold)
("WAIT" :foreground "#699" :weight bold)
("DONE" :foreground "#676" :weight bold)
("CANC" :foreground "#676" :weight bold))))
)
Basic config:
(use-package! org-agenda
:init
(map! "<f1>"'(lambda (&optional arg) (interactive) (org-agenda arg " ")))
(setq org-agenda-skip-scheduled-if-done t
org-agenda-skip-deadline-if-done t
org-agenda-include-deadlines t
org-agenda-include-diary t ;; Include diary entries (birthdays)
org-agenda-block-separator nil
org-log-repeat nil ; don't log state changes
; show clocked items in the agenda:
; org-agenda-start-with-log-mode t
)
;; :config
;; (setq org-columns-default-format "%40ITEM(Task) %Effort(EE){:} %CLOCKSUM(Time Spent) %SCHEDULED(Scheduled) %DEADLINE(Deadline)")
)
Show birthdays:
;; (use-package! memoize
;; :ensure t)
;(use-package! org-contacts
; :after org
; :init
; (setq org-contacts-birthday-property "BIRTHDAY")
; (setq org-contacts-files '("~/org/contacts.org"))
;; :config
;; (require 'memoize) ;; Ensure memoize is loaded explicitly
;)
My custom agenda view:
(use-package! org-super-agenda
:after org-agenda
:init
;; don't break evil on org-super-agenda headings, see https://github.com/alphapapa/org-super-agenda/issues/50
(setq org-super-agenda-header-map (make-sparse-keymap))
;; (setq org-agenda-span 1; show only current day
;; org-agenda-start-day nil
;; )
(setq org-agenda-custom-commands
'((" " "Today"
((agenda "" ((org-agenda-span 1)
(org-agenda-start-day nil)
(org-agenda-overriding-header "Day Agenda\n")
(org-super-agenda-groups
'((:name "" :time-grid t :date today :order 1)
(:name "Deadlines" :deadline t :order 2)
;; catch "Other Items", e.g. scheduled yesterday:
(:name " " :date t :order 2)
))))
(alltodo "" ((org-agenda-overriding-header "")
(org-super-agenda-groups
'(
(:name "Write" :and(:tag "write" :todo ("ACTV" "URGT")))
(:name "Read" :and(:tag "read" :todo ("ACTV" "URGT")))
(:name "Programming" :and(:tag "prog" :todo ("ACTV" "URGT")))
(:name "Other" :todo ("ACTV" "URGT"))
(:name "Routines" :tag "routine")
(:name "Deadlines" :deadline t :order 2)
(:name "To Read" :and(:tag "read" :todo "NEXT"))
(:name "To Write" :and(:tag "write" :todo "NEXT"))
(:name "Programming tasks" :and(:tag "prog" :todo "NEXT"))
(:name "Other tasks" :todo "NEXT")
(:name "Waiting" :todo "WAIT")
;; (:name "To refile" :file-path "inbox.org")
;; (:name "Active projects"
;; :file-path "journal/")
;; (:name "Future Schedule"
;; :scheduled future
;; :order 8)
;; (:name "Projects"
;; :tag "project"
;; :order 5)
(:discard (:anything t))
)
)
)
))
))
)
(custom-set-faces!
'(org-agenda-day :foreground "#ff0000"))
:config
(org-super-agenda-mode)
)
(use-package! calfw
:after org
:init
(map! "<f2>"'(lambda (&optional arg) (interactive) (cfw:open-org-calendar)))
(setq cfw:render-line-breaker 'cfw:render-line-breaker-wordwrap) ; doesn't seem to work
(setq calendar-week-start-day 1)
)
Display UK bank holidays only (from https://emacs.stackexchange.com/questions/44851/uk-holidays-definitions):
(setq calendar-holidays
'((holiday-fixed 1 1 "New Year's Day")
(holiday-new-year-bank-holiday)
(holiday-fixed 2 14 "Valentine's Day")
(holiday-fixed 3 17 "St. Patrick's Day")
(holiday-fixed 4 1 "April Fools' Day")
(holiday-easter-etc -47 "Shrove Tuesday")
(holiday-easter-etc -21 "Mother's Day")
(holiday-easter-etc -2 "Good Friday")
(holiday-easter-etc 0 "Easter Sunday")
(holiday-easter-etc 1 "Easter Monday")
(holiday-float 5 1 1 "Early May Bank Holiday")
(holiday-float 5 1 -1 "Spring Bank Holiday")
(holiday-float 6 0 3 "Father's Day")
(holiday-float 8 1 -1 "Summer Bank Holiday")
(holiday-fixed 10 31 "Halloween")
(holiday-fixed 12 24 "Christmas Eve")
(holiday-fixed 12 25 "Christmas Day")
(holiday-fixed 12 26 "Boxing Day")
(holiday-christmas-bank-holidays)
(holiday-fixed 12 31 "New Year's Eve")))
;; N.B. It is assumed that 1 January is defined with holiday-fixed -
;; this function only returns any extra bank holiday that is allocated
;; (if any) to compensate for New Year's Day falling on a weekend.
;;
;; Where 1 January falls on a weekend, the following Monday is a bank
;; holiday.
(defun holiday-new-year-bank-holiday ()
(let ((m displayed-month)
(y displayed-year))
(calendar-increment-month m y 1)
(when (<= m 3)
(let ((d (calendar-day-of-week (list 1 1 y))))
(cond ((= d 6)
(list (list (list 1 3 y)
"New Year's Day Bank Holiday")))
((= d 0)
(list (list (list 1 2 y)
"New Year's Day Bank Holiday"))))))))
;; N.B. It is assumed that 25th and 26th are defined with holiday-fixed -
;; this function only returns any extra bank holiday(s) that are
;; allocated (if any) to compensate for Christmas Day and/or Boxing Day
;; falling on a weekend.
(defun holiday-christmas-bank-holidays ()
(let ((m displayed-month)
(y displayed-year))
(calendar-increment-month m y -1)
(when (>= m 10)
(let ((d (calendar-day-of-week (list 12 25 y))))
(cond ((= d 5)
(list (list (list 12 28 y)
"Boxing Day Bank Holiday")))
((= d 6)
(list (list (list 12 27 y)
"Boxing Day Bank Holiday")
(list (list 12 28 y)
"Christmas Day Bank Holiday")))
((= d 0)
(list (list (list 12 27 y)
"Christmas Day Bank Holiday"))))))))
(defun my-appt-send-notification (min-to-app new-time msg)
"Send a notification using notify-send."
(call-process "notify-send" nil 0 nil
"-u" "critical" ;; Set urgency to critical
"-t" "10000" ;; Show notification for 10 seconds
(format "Appointment in %s minutes" min-to-app) msg))
(setq appt-disp-window-function 'my-appt-send-notification)
(setq appt-message-warning-time 15) ;; first notification 15 minutes before
(setq appt-display-interval 10) ;; second notification 5 minutes before
(defun my-refresh-appt ()
"Refresh appointments from the Org agenda."
(setq appt-time-msg-list nil)
(org-agenda-to-appt))
(appt-activate 1)
; refresh appt on startup, after creating the agenda, and every hour:
(my-refresh-appt)
(add-hook 'org-agenda-finalize-hook 'my-refresh-appt)
(run-at-time "24:01" 3600 'my-refresh-appt)
From stackoverflow. Usage:
(defun diary-last-day-of-month (date)
"Return `t` if DATE is the last day of the month."
(let* ((day (calendar-extract-day date))
(month (calendar-extract-month date))
(year (calendar-extract-year date))
(last-day-of-month
(calendar-last-day-of-month month year)))
(= day last-day-of-month)))
(after! org-capture
(setq org-capture-templates '(
;; ("t" "single task (todo.org)" entry (file+headline "todo.org" "Single Tasks")
;; "\n* TODO %?")
("t" "TODO (single task)")
("tt" "General (todo.org)" entry (file+headline "todo.org" "Single Tasks")
"\n* TODO %?" :empty-lines 1)
("tr" "Research (research.org)" entry (file+headline "research.org" "Single Tasks")
"\n* TODO %?" :empty-lines 1)
("ta" "Uni Admin (admin.org)" entry (file+headline "admin.org" "Single Tasks")
"\n* TODO %?" :empty-lines 1)
("to" "Philosophical Progress (opp.org)" entry (file+headline "opp.org" "Tasks")
"\n* TODO %? %(org-set-tags \"prog\")" :empty-lines 1)
("ti" "Investing (investing.org)" entry (file+headline "investing.org" "App Tasks")
"\n* TODO %? %(org-set-tags \"prog\")" :empty-lines 1)
("tp" "Tree Proof Generator (trees-todo.org)" entry (file+headline "tpg.org" "Tasks")
"\n* TODO %? %(org-set-tags \"prog\")" :empty-lines 1)
("s" "scheduled task (schedule.org)" entry (file+headline "schedule.org" "Tickler")
"\n* TODO %?\nSCHEDULED: %^t\n" :empty-lines 1)
("b" "buy (add to shopping list in todo.org)" entry (file+headline "todo.org" "Shopping list")
"\n* TODO buy %?" :empty-lines 1)
("a" "appointment (schedule.org)" entry (file+headline "schedule.org" "Calendar")
"\n* %?\n%^t" :empty-lines 1)
("i" "inbox entry" entry (file "inbox.org")
"\n\n* %?" :empty-lines 1)
("j" "journal/logbook entry (logbook.org)" entry (file+datetree "logbook.org")
"* %<%H:%M>\n%?\n" :tree-type week)
; from browser:
("l" "link (from browser)" entry (file "inbox.org")
;; "* %a\n %?\n %i" :immediate-finish txx
"\n* %a\n %?\n %i\n")
)
)
(setq org-protocol-default-template-key "l")
)
(after! org
(defadvice! dan/+org--restart-mode-h-careful-restart (fn &rest args)
:around #'+org--restart-mode-h
(let ((old-org-capture-current-plist (and (bound-and-true-p org-capture-mode)
(bound-and-true-p org-capture-current-plist))))
(apply fn args)
(when old-org-capture-current-plist
(setq-local org-capture-current-plist old-org-capture-current-plist)
(org-capture-mode +1)))))
Create new parent nodes when refiling by adding /New Heading in the prompt:
(after! org
(setq org-refile-allow-creating-parent-nodes 'confirm)
)
I need to learn how to refile better.
;; org-refile:
;; (setq org-refile-targets (quote (("projects.org" :maxlevel . 5)
;; ("archived_projects.org" :maxlevel . 5))))
;; (setq org-outline-path-complete-in-steps nil ; Refile in a single go
;; org-refile-use-outline-path t) ; Show full paths for refiling
(defun my-review-layout ()
(interactive)
(delete-other-windows) ;; Start with a clean slate
(set-window-buffer (selected-window) (find-file-noselect "~/org/logbook.org"))
(split-window-right) ;; Two columns
(other-window 1)
(set-window-buffer (selected-window) (find-file-noselect "~/org/journal/2025-phil.org"))
(evil-window-split)
(other-window 1)
(set-window-buffer (selected-window) (find-file-noselect "~/org/journal/2025-neben.org"))
(evil-window-split)
(other-window 1)
(set-window-buffer (selected-window) (find-file-noselect "~/org/journal/2025-privat.org"))
(other-window 1)
)
Distraction-free prose writing. This comes from the :ui zen module.
(setq +zen-text-scale 0.9
writeroom-extra-line-spacing 0.3
doom-variable-pitch-font (font-spec :family "Fira Sans" :size 18)
writeroom-fullscreen-effect t
)
Make TAB insert 4 spaces but do all the smart stuff it does in programming mode.
(after! org
(setq-local tab-width 4)
(setq indent-line-function 'indent-relative-maybe)
(map! :map org-mode-map
:i [tab] #'indent-for-tab-command
:i "TAB" #'indent-for-tab-command
:n [tab] #'indent-for-tab-command
:n "TAB" #'indent-for-tab-command))
I sometimes like automatic line breaks when I write prose:
; (after! org
; (add-hook 'org-mode-hook #'auto-fill-mode)
; )
(map!
:leader
:desc "toggle auto-fill-mode" "t a" #'auto-fill-mode
)
(defun my-semantic-linebreaks-in-paragraph ()
"Modify the current paragraph by removing line breaks and adding line breaks after punctuation etc."
(interactive)
(let ((orig-point (point)))
(save-excursion
(let ((paragraph-start (progn (backward-paragraph) (point)))
(paragraph-end (progn (forward-paragraph) (point))))
(save-restriction
(narrow-to-region paragraph-start paragraph-end)
(goto-char (point-min))
(while (re-search-forward "\\([^[:cntrl:]]\\)\n *" nil :noerror)
(replace-match "\\1 " nil nil))
(goto-char (point-min))
(while (re-search-forward "\\([.,;:]\\|iff\\|that\\) " nil :noerror)
(replace-match "\\1\n" nil)))))
(goto-char orig-point)))
Citation management used to be a mess. Now it’s fairly easy with the new org-internal citation format and the citar package.
(use-package! citar
:hook
(LaTeX-mode . citar-capf-setup)
(org-mode . citar-capf-setup)) ; doesn't work :(
:config
(setq! citar-bibliography '("~/notes/literature.bib")
citar-library-paths '("~/papers/[A-Z]/")
citar-notes-paths '("~/notes/literature/")
org-cite-global-bibliography '("~/notes/literature.bib")
citar-org-roam-note-title-template "${author editor:*%sn} ${date year issued:4} ${title}"
citar-org-roam-cpature-template-key "l"
citar-org-roam-subdir "literature"
)
;; (use-package citar-org-roam
;; :after (citar org-roam)
;; :config (citar-org-roam-mode))
(after! oc-csl
(setq org-cite-csl-styles-dir "~/Zotero/styles"))
(defun my-update-literature-bib-from-zotero ()
"export literature.bib from zotero"
(interactive)
(call-process-shell-command
"curl http://127.0.0.1:23119/better-bibtex/export/library?/1/library.bibtex > ~/notes/literature.bib"
nil 0)
(sleep-for 1)
)
This function replaces the content of the current buffer.
(defun my-reformat-citations ()
(interactive)
(shell-command-on-region
; mark whole buffer:
(point-min)
(point-max)
; the command:
"python3 /home/wo/notes/update-cite-format.py"
; output:
(current-buffer)
; replace:
t
; name of error buffer:
"*tex2org Error Buffer*"
; show error buffer:
t))
I use links by custom_id to refer to section headings. The following code lets me insert such links from a list of custom_ids currently in the buffer. (This is bound to C-c l.)
(defun matches-in-buffer (regexp &optional buffer)
"return a list of matches of REGEXP in BUFFER or the current buffer if not given."
(let ((matches))
(save-match-data
(save-excursion
(with-current-buffer (or buffer (current-buffer))
(save-restriction
(widen)
(goto-char 1)
(while (search-forward-regexp regexp nil t 1)
(push (match-string 1) matches)))))
matches)))
(defun my-insert-custom-id-link ()
"choose from a CUSTOM_ID in the file and insert link to it"""
(interactive)
(let* ((custom-id (completing-read
"Custom ID: "
(matches-in-buffer "^[ \t]*:CUSTOM_ID:[ \t]+\\(\\S-+\\)[ \t]*$"))))
(when custom-id
(org-insert-link nil (concat "#" custom-id) custom-id))))
This adds a ‘Log’ section to the bottom of the current org file, and inserts a new entry with today’s date.
When working on a larger project, either a paper or a software project, I like to keep a log of what I’ve done, so that, for example, I can skim the log to resume the project after a break, or so that I can remember why I made a certain choice.
(defun insert-log-entry ()
"Inserts a log entry with today's date at the end of the current org file."
(interactive)
(save-excursion
(goto-char (point-max))
(unless (org-at-heading-p)
(org-return))
(if (re-search-backward "^\\* Log\\b" nil t)
(progn
(goto-char (point-max))
(unless (org-at-heading-p)
(org-return))
(insert "** " (format-time-string "[%Y-%m-%d %a]") "\n"))
(progn
(insert "* Log\n** " (format-time-string "[%Y-%m-%d %a]") "\n"))))
(goto-char (point-max))
(evil-insert-state)
)
(map! :leader
:desc "Insert Log Entry"
"i l" #'insert-log-entry)
I write my blog posts as org-roam notes. This function converts a note to HTML and submits it to my server. If the relevant post already exists, it updates it.
(defun my-post-to-server ()
(interactive)
(save-buffer)
(shell-command
(format "python3 /home/wo/notes/blog/post_to_server.py %s"
(shell-quote-argument (buffer-file-name))))
(revert-buffer t t t)
)
This function only updates the tags associated with the current post.
(defun my-update-tags-on-server ()
(interactive)
(save-buffer)
(shell-command
(format "python3 /home/wo/notes/blog/update_tags_on_server.py %s"
(shell-quote-argument (buffer-file-name))))
(revert-buffer t t t)
)
(defun my-webpplify ()
(interactive)
(save-buffer)
(shell-command
(format "python3 /home/wo/programming/webpplview/org2html.py %s"
(shell-quote-argument (buffer-file-name))))
(revert-buffer t t t)
(browse-url-firefox "file:///home/wo/programming/webpplview/index.html")
)
Emacs has built-in functions for exporting org documents as LaTeX or pdf. But customising this process is cumbersome. I need a lot of extra preprocessing and postprocessing to make the PDFs come out as I want, so I’ve written a python script that does the conversions (with the help of pandoc).
(defun my-org2latex ()
(interactive)
(save-buffer)
(let ((output-dir (concat (file-name-directory (buffer-file-name))
(file-name-base (buffer-file-name)))))
(async-shell-command
(format "org2pdf --latex --template ~/notes/papers/.article-template.tex %s %s"
(shell-quote-argument (buffer-file-name))
(shell-quote-argument output-dir)))
(revert-buffer t t t)
)
)
(defun my-org2pdf ()
(interactive)
(save-buffer)
(let ((vertico-sort-function nil)) ;; Disable Vertico sorting
(let* ((template-type (completing-read "Choose template: "
'("article (single-run)" "article" "note" "handout")
nil t nil nil "article (single-run)"))
(template-file (cond
((string-equal template-type "note")
(expand-file-name "~/notes/papers/template-note.tex"))
((string-equal template-type "handout")
(expand-file-name "~/notes/papers/template-handout.tex"))
(t
(expand-file-name "~/notes/papers/template-article.tex"))))
(single-run (not (string-equal template-type "article")))
(output-dir (concat (file-name-directory (buffer-file-name))
(file-name-base (buffer-file-name))))
(command (format "org2pdf --template %s %s %s%s"
(shell-quote-argument template-file)
(shell-quote-argument (buffer-file-name))
(shell-quote-argument output-dir)
(if single-run " --single-run" ""))))
(async-shell-command command)
(revert-buffer t t t))))
(defun my-org2docx ()
(interactive)
(save-buffer)
(let ((output-dir (concat (file-name-directory (buffer-file-name))
(file-name-base (buffer-file-name)))))
(async-shell-command
(format "org2docx %s %s"
(shell-quote-argument (buffer-file-name))
(shell-quote-argument output-dir)))
(revert-buffer t t t)
)
)
(defun my-org2html ()
(interactive)
(save-buffer)
(async-shell-command
(format "org2html %s"
(shell-quote-argument (buffer-file-name))))
(revert-buffer t t t)
)
(setq org-roam-v2-ack t)
(setq org-roam-directory (file-truename "/home/wo/notes/"))
(after! org-roam
(add-hook 'after-init-hook 'org-roam-mode)
)
;(use-package! org-roam-bibtex
; :after org-roam
; :load-path "~/notes/literature.bib"
; :hook (org-roam-mode . org-roam-bibtex-mode)
; :config
; (require 'org-ref)
;)
(setq org-id-method 'ts)
; don't include nanoseconds in the timestamp:
(setq org-id-ts-format "%Y%m%dT%H%M%S")
(after! org-roam
;; (setq orb-preformat-keywords
;; '("citekey" "title" "year" "author-or-editor" "file")
;; orb-process-file-keyword t
;; orb-file-field-extensions '("pdf"))
(setq org-roam-capture-templates
(list
'("n" "default note" plain "%?"
:if-new (file+head "%<%Y%m%d>-${slug}.org"
"#+TITLE: ${title}\n\n")
:unnarrowed t)
'("b" "blog post" plain "%?"
:if-new (file+head "blog/%<%Y%m%d>-${slug}.org"
"#+TITLE: ${title}\n\n")
:unnarrowed t)
'("p" "new paper" plain "%?"
:if-new (file+head "papers/%<%Y>-${slug}.org"
"#+TITLE: ${title}\n\n")
:unnarrowed t)
'("l" "literature note" plain "%?"
:if-new (file+head "literature/${citekey}.org"
"#+TITLE: ${author-or-editor} ${year} ${title}\n")
:unnarrowed t)
)
)
)
Emulate subdirectories-as-tags behaviour from v1:
(cl-defmethod org-roam-node-directories ((node org-roam-node))
(if-let ((dirs (file-name-directory (file-relative-name (org-roam-node-file node) org-roam-directory))))
(format "(%s)" (string-join (f-split dirs) "/"))
""))
(setq org-roam-node-display-template "${directories:10} ${title:*} ${tags:10}")
(defun my-org-roam-search ()
"Search org-roam directory using consult-ripgrep. With live-preview."
(interactive)
(let ((consult-ripgrep-command "rg --null --ignore-case --type org --line-buffered --color=always --max-columns=500 --no-heading --line-number . -e ARG OPTS"))
(consult-ripgrep org-roam-directory)))
This updates the buffer name, filename, and links. (From the org-roam discourse group.)
(defun my-org-roam-change-title ()
"Modify title of org-roam current node and update all backlinks in roam database."
(interactive)
(unless (org-roam-buffer-p) (error "Not in an org-roam buffer."))
(save-some-buffers t)
(let* ((old-title (org-roam-get-keyword "title"))
(ID (org-entry-get (point) "ID"))
(new-title (read-string "Enter new title: " old-title)))
(org-roam-set-keyword "title" new-title)
(save-buffer)
(let* ((new-slug (org-roam-node-slug (org-roam-node-at-point)))
(new-file-name (replace-regexp-in-string "-.*\\.org" (format "-%s.org" new-slug) (buffer-file-name)))
(new-buffer-name (file-name-nondirectory new-file-name)))
(rename-buffer new-buffer-name)
(rename-file (buffer-file-name) new-file-name 1)
(set-visited-file-name new-file-name))
(save-buffer)
;; Rename backlinks in the rest of the Org-roam database.
(let* ((search (format "[[id:%s][%s]]" ID old-title))
(replace (format "[[id:%s][%s]]" ID new-title))
(rg-command (format "rg -t org -lF %s ~/Org/roam/" search))
(file-list (split-string (shell-command-to-string rg-command))))
(dolist (file file-list)
(let ((file-open (get-file-buffer file)))
(find-file file)
(beginning-of-buffer)
(while (search-forward search nil t)
(replace-match replace))
(save-buffer)
(unless file-open
(kill-buffer)))))))
This function replaces the content of the current buffer.
(defun my-tex2org ()
(interactive)
(shell-command-on-region
; mark whole buffer:
(point-min)
(point-max)
; the command:
"python3 /home/wo/notes/tex2org.py"
; output:
(current-buffer)
; replace:
t
; name of error buffer:
"*tex2org Error Buffer*"
; show error buffer:
t))
(map!
:map evil-window-map
:desc "close other windows" "1" 'delete-other-windows
)
I often insert inactive timestamps to document when an event/conversation took place, and I don’t want to enter normal mode and press SPC m d T each time.
(map! :after org
:map org-mode-map
"C-c ," nil
)
(map!
:desc "insert inactive timestamp" "C-c ," #'org-time-stamp-inactive
)
(map! :map org-mode-map
"M-y" #'yank-pop
"M-q" #'org-fill-paragraph
)
I use org-capture all the time to enter todo items or update logbook.org.
(map!
:leader
:desc "org-capture" "x" #'org-capture
)
(after! org-roam
(map! :leader
:prefix "n"
:desc "org-roam-buffer-toggle" "r" #'org-roam-buffer-toggle
:desc "org-roam-node-insert" "i" #'org-roam-node-insert
:desc "org-roam-node-find" "f" #'org-roam-node-find
:desc "org-roam-show-graph" "g" #'org-roam-show-graph
:desc "org-roam-capture" "c" #'org-roam-capture
:desc "my-org-roam-search" "d" #'my-org-roam-search
)
)
(map!
:leader
:desc "open org note for literature item" "n p" #'citar-open-notes
)
(map!
:leader
:prefix "n"
:desc "add org-roam tag" "t" #'org-roam-tag-add
:desc "remove org-roam tag" "T" #'org-roam-tag-remove
)
(map! :desc "insert snippet" "C-s" #'yas-insert-snippet)
(map!
:desc "insert citation" "C-c c" #'citar-insert-citation
)
I don’t want to enter normal mode just to insert a link to another note:
(defun my-insert-link-to-note ()
"insert link to org node and prompt for link text"
(interactive)
(org-roam-node-insert)
(call-interactively #'org-insert-link)
)
(after! org-roam
(map!
:desc "insert link to node" "C-c i" #'my-insert-link-to-note
)
)
(map!
:desc "link to heading" "C-c l" #'my-insert-custom-id-link
)
(map!
:desc "footnote action" "C-c f" #'org-footnote-action
)
(defun my-access-footnote-menu ()
(interactive)
(org-footnote-action t)
)
Mu4e’s built-in sync command takes to long, blocking emacs.
(defun my-mail-fetch ()
(interactive)
(save-buffer)
(call-process-shell-command "/usr/local/bin/mbsync --pull -a&" nil 0)
)
(defun my-mail-send
(interactive) ()
(save-buffer)
(call-process-shell-command "/usr/local/bin/mbsync --push -a" nil 0)
)
(map! :leader
:prefix "m"
:desc "fetch mail" "y" #'my-mail-fetch
:desc "send mail" "z" #'my-mail-send
)
(setq reftex-default-bibliography '("/home/wo/notes/literature.bib"))
Entry format in bibtex files:
(setq bibtex-align-at-equal-sign t ; fields aligned at equal sign
bibtex-autokey-name-year-separator ""
bibtex-autokey-year-title-separator ""
bibtex-autokey-titleword-first-ignore '("the" "a" "if" "and" "an")
bibtex-autokey-year-length 2
bibtex-autokey-titlewords 1
bibtex-autokey-titlewords-stretch 1
bibtex-autokey-titleword-length 20
; additional default fields:
;bibtex-user-optional-fields '("summary", "comments")
; reformat/realign entry on C-c C-c:
bibtex-entry-format t
)
(after! latex
(add-to-list 'TeX-command-list '("XeLaTeX" "%`xelatex --synctex=1%(mode)%' %t" TeX-run-TeX nil t)))
(setq org-latex-pdf-process
'("latexmk -f -pdf -xelatex -shell-escape -interaction=nonstopmode -output-directory=%o %f"))
(add-hook 'LaTeX-mode-hook #'olivetti-mode)
(setq python-fill-docstring-style 'symmetric)
(setq python-shell-interpreter "python3")
Don’t insert template into new html pages:
(set-file-template! "\\.html$" nil)
Activate javascript mode for webppl files:
(add-to-list 'auto-mode-alist '("\\.wppl\\'" . js2-mode))
Accept code completion from copilot; fallback to company:
(use-package! copilot
:hook (prog-mode . copilot-mode)
;; :hook (org-mode . copilot-mode)
:bind (:map copilot-completion-map
("<tab>" . 'copilot-accept-completion)
("TAB" . 'copilot-accept-completion)
("C-TAB" . 'copilot-accept-completion-by-word)
("C-<tab>" . 'copilot-accept-completion-by-word)
("C-p" . 'copilot-previous-completion)
("C-n" . 'copilot-next-completion)
))
:config
(setq copilot-max-char -1)
(setq copilot-indent-offset-warning-disable t)
; Need Node.js v18+.
(setq copilot-node-executable "/home/wo/.nvm/versions/node/v18.20.2/bin/node")
(map!
:leader
:desc "toggle copilot" "t p" #'copilot-mode
)
Prevent copilot warnings in org mode:
(after! copilot
(add-to-list
'copilot-indentation-alist
'(org-mode 4))
)
(defun my-update-course-pages()
"update course pages on wolfgangschwarz.net"
(interactive)
(shell-command
"cd /home/wo/programming/wolfgangschwarz.net && python3 createpages.py -c")
)
I use the gptel package.
(load-file "~/.doom.d/gptel-api-key.el")
(setq gptel-default-mode 'org-mode)
A function to start a new session:
(defun my-gptel-new ()
(interactive)
(delete-region (point-min) (point-max))
(insert "*** "))
I use mu4e for email. Mails are synchronised with mbsync into a local ~/.mail folder. The mbsync configuration resides in ~/.mbsyncrc.
I manually installed a newer version of mu/mu4e manually, which doom doesn’t find without assistance:
(add-to-list 'load-path "/usr/local/share/emacs/site-lisp/mu4e")
General settings:
(setq
;; mu4e-index-cleanup nil ; speed up indexing
;; mu4e-index-lazy-check t ; speed up indexing
mu4e-update-interval nil ; refresh index every n seconds
mu4e-headers-show-threads t ; Keep non-threaded by default 'P' to change
mu4e-view-show-images t ; show images inline
mu4e-compose-format-flowed t) ; no hard linebreaks in composed emails
Now let’s configure my mail accounts. First my Uni Edinburgh account:
(set-email-account! "UoE"
'((mu4e-sent-folder . "/UoE/Sent Mail")
(mu4e-drafts-folder . "/UoE/Drafts")
(mu4e-trash-folder . "/UoE/Trash")
(mu4e-refile-folder . "/UoE/Archive")
(smtpmail-smtp-server . "outlook.office365.com")
(smtpmail-smtp-service . 587)
(smtpmail-smtp-user . "wschwarz@ed.ac.uk")
;; (mu4e-compose-signature . "\nBest,\nWolfgang")
)
t)
Next my Gmail account:
(set-email-account! "wo@umsu"
'((mu4e-sent-folder . "/wo@umsu/Sent Mail")
(mu4e-drafts-folder . "/wo@umsu/Drafts")
(mu4e-trash-folder . "/wo@umsu/Bin")
(mu4e-refile-folder . "/wo@umsu/All Mail")
(smtpmail-default-smtp-server . "smtp.gmail.com")
(smtpmail-smtp-server . "smtp.gmail.com")
(smtpmail-smtp-service . 587)
(smtpmail-debug-info . t)
(smtpmail-debug-verbose . t)
(smtpmail-smtp-user . "wo@umsu.de")
;; (mu4e-compose-signature . "\nBest,\nWolfgang"))
)
t)
;; (auth-source-pass-enable)
;; (setq auth-sources '(password-store))
;; (setq auth-source-debug t)
;; (setq auth-source-do-cache nil)
I need to tell doom that this a gmail account so that deleting, archiving, etc. works properly:
(setq +mu4e-gmail-accounts '(("wo@umsu.de" . "/wo@umsu")))
A bookmark for the combined inbox of all accounts:
(after! mu4e
(add-to-list 'mu4e-bookmarks '("m:/wo@umsu/Inbox or m:/UoE/Inbox" "Inbox" ?i)))
Improve display of html mails in dark mode (from reddit):
(after! mu4e
(setq mu4e-html2text-command 'mu4e-shr2text)
(setq shr-color-visible-luminance-min 60)
(setq shr-color-visible-distance-min 5)
(setq shr-use-colors nil)
(advice-add #'shr-colorize-region :around (defun shr-no-colourise-region (&rest ignore))))
(setq
browse-url-browser-function 'browse-url-generic
browse-url-generic-program "firefox")
(after! mu4e
(add-to-list 'mu4e-view-actions '("browser" . mu4e-action-view-in-browser)))
(setq mu4e-compose-complete-only-after (format-time-string
"%Y-%m-%d"
(time-subtract (current-time) (days-to-time 350))))