dotfiles

My personal shell configs and stuff
git clone git://git.alex.balgavy.eu/dotfiles.git
Log | Files | Refs | Submodules | README | LICENSE

commit 8cd61c31dbe519c77e04ca02afac0e29c0bcc30a
parent 3a3acbd4b5296e279397a97eda211d124fddc723
Author: Alex Balgavy <alex@balgavy.eu>
Date:   Sun, 21 Aug 2022 21:56:11 +0200

emacs: reorganize configuration

Diffstat:
Memacs/config.org | 3690++++++++++++++++++++++++++++++++++++-------------------------------------------
1 file changed, 1669 insertions(+), 2021 deletions(-)

diff --git a/emacs/config.org b/emacs/config.org @@ -2,74 +2,29 @@ On macOS, I use Homebrew to install Emacs from daviderestivo/emacs-head/emacs-head@28. I install with ~--HEAD --with-dbus --with-cocoa --with-xwidgets --with-native-comp~. -* Keep track of custom key bindings -I want an easy way to see which global key bindings I have. -Some packages provide things like this, but I want it to be simple and always available, so I'll roll my own. -These functions allow a way to store personal global key bindings and view them as a list. - -I'll use a hash table to map a key (in ~kbd~ format, the inverse of this is ~key-description~) to its function (and its previous function): - -#+begin_src emacs-lisp - (defvar za/personal-keybinds (make-hash-table :test 'equal) "A hash table for my personal keybindings.") -#+end_src - -A function to bind keys: - -#+begin_src emacs-lisp - (defun za/global-set-key (keys fun) - "Bind KEYS to FUN. This will be saved to the za/personal-keybinds hash table." - (let* (;; Store the previous function KEYS were bound to - (lookup-result (lookup-key (current-global-map) keys)) - (previous-fun (cond ((not lookup-result) nil) - ((numberp lookup-result) nil) - ((equal lookup-result fun) nil) - (t lookup-result))) - ;; Try to see if it already has a custom binding - (existing-entry (gethash keys za/personal-keybinds))) - - ;; If KEYS already have a custom binding, fail, unless we're just repeating the same binding - (if (and existing-entry - (not (equal (car existing-entry) fun))) - (user-error (format "Keys are already bound to %s" (car existing-entry)))) - - (puthash keys (list fun previous-fun) za/personal-keybinds) - (global-set-key keys fun))) -#+end_src - -And one to remove bindings: +* Why I choose use-package +- provides bind-key by default +- is mostly just macros that wrap the needed stuff from package.el. can check that with ~macroexpand~. +- adds a bunch of performance improvements +* quelpa +use-package downloads from ELPA/MELPA, sometimes I need to get stuff that's not in those repos. +Quelpa lets you install from local or remote source (like git). 'Quelle', like source, get it? +With quelpa-use-package, I can use the keyword ~:quelpa~ to install via quelpa. #+begin_src emacs-lisp - (defun za/global-unset-key (keys) - "Remove a custom key binding from za/personal-keybinds and unbind the keys." - (remhash keys za/personal-keybinds) - (global-unset-key keys)) + (use-package quelpa) + (use-package quelpa-use-package) #+end_src -Then a way to retrieve keybindings: - -#+begin_src emacs-lisp - (defun za/get-personal-keybinds () - "Return the contents of za/personal-keybinds as a string." - (let ((result nil) - (format-entry (lambda (k v) - (let ((get-func (lambda (e) (nth 0 e))) - (get-prev-func (lambda (e) (nth 1 e)))) - (push - (concat (format "%s\t\t%s" (key-description k) (funcall get-func v)) - (if (funcall get-prev-func v) - (format "\t\t(previously %s)" (funcall get-prev-func v)))) - result))))) - (maphash format-entry za/personal-keybinds) - (mapconcat 'identity (sort result 'string<) "\n"))) -#+end_src -(za/global-unset-key (kbd "C-x C-s")) -And a way to display all of the keybindings: +* exec-path-from-shell (macOS) +In macOS, the path is not set correctly (i.e. as it is in the terminal) in the GUI app. This fixes it. #+begin_src emacs-lisp - (defun za/list-personal-keybinds () - (interactive) - (with-output-to-temp-buffer "*Personal Keybindings*" - (princ (za/get-personal-keybinds)))) + (when (string-equal system-type "darwin") + (use-package exec-path-from-shell + :config + (add-to-list 'exec-path-from-shell-variables "NOTMUCH_CONFIG") + (exec-path-from-shell-initialize))) #+end_src * Emacs file locations @@ -112,77 +67,33 @@ The second one actually loads them. (unless (fboundp 'system-move-file-to-trash) (setq trash-directory "~/.Trash")) #+end_src - -* Theme -Icons required for some parts of the doom theme: - -#+begin_src emacs-lisp - (use-package all-the-icons) -#+end_src - -Load Doom Emacs themes: - -#+begin_src emacs-lisp - (use-package doom-themes - :config - ;; Global settings (defaults) - (setq doom-themes-enable-bold t ; if nil, bold is universally disabled - doom-themes-enable-italic t) ; if nil, italics is universally disabled - - ;; Enable flashing mode-line on errors - (doom-themes-visual-bell-config) - - ;; Corrects (and improves) org-mode's native fontification. - (doom-themes-org-config)) -#+end_src - -Define the themes I want: +* Daemon +I want to have a way to kill the Emacs daemon. +So, define a function that kills the frame, and with a prefix kills emacs. #+begin_src emacs-lisp - (defconst za/dark-theme-name 'doom-one "A symbol representing the name of the dark theme I use.") - (defconst za/light-theme-name 'jokull "A symbol representing the name of the light theme I use.") - ;; I used to use doom-acario-light before writing my own theme - - (defun za/dark-theme () - "Switch to dark theme" - (interactive) - (mapc #'disable-theme custom-enabled-themes) - (load-theme za/dark-theme-name t) - (add-hook 'pdf-view-mode-hook #'pdf-view-midnight-minor-mode)) - - (defun za/light-theme () - "Switch to light theme" - (interactive) - (mapc #'disable-theme custom-enabled-themes) - (load-theme za/light-theme-name t) - (remove-hook 'pdf-view-mode-hook #'pdf-view-midnight-minor-mode)) + (defun za/emacsclient-c-x-c-c (&optional arg) + "If running in emacsclient, make C-x C-c exit frame, and C-u C-x C-c exit Emacs." + (interactive "P") ; prefix arg in raw form + (if arg + (save-buffers-kill-emacs) + (save-buffers-kill-terminal))) #+end_src -Change theme depending on the current system theme. -The way I check for dark mode is defined in 'dark-mode-p'; currently I use the presence of the ~/.config/dark-theme file to indicate when dark theme is set. -I quote the call to ~file-exists-p~ because I want to evaluate it on-demand, not immediately. -A function ending in '-p' is a predicate, i.e. returns true or false. -If calling a function that's in a variable, you have to use 'funcall'. -To evaluate a quoted form, use 'eval'. +Then, if I'm in an emacsclient, I want to bind C-x C-c to that function (if not, I just want the default keybinding): #+begin_src emacs-lisp - (defun za/auto-select-theme (&rest _) - "Automatically select dark/light theme based on presence of ~/.config/dark-theme" - (let ((dark-mode-p '(file-exists-p "~/.config/dark-theme"))) - (if (eval dark-mode-p) - (za/dark-theme) - (za/light-theme)))) - - (za/auto-select-theme) + ;; If not running in emacsclient, use the default bindings + (if (daemonp) + (bind-key "C-x C-c" #'za/emacsclient-c-x-c-c)) #+end_src -* Font -I want Menlo, size 14: +Furthermore, I want to set the theme correctly whenever I connect with 'emacsclient': #+begin_src emacs-lisp - (add-to-list 'default-frame-alist '(font . "Menlo-14")) + (if (daemonp) + (add-hook 'after-make-frame-functions #'za/auto-select-theme)) #+end_src - * Garbage collection Garbage-collect on focus-out, Emacs /should/ feel snappier. @@ -190,2462 +101,2199 @@ Garbage-collect on focus-out, Emacs /should/ feel snappier. (add-hook 'focus-out-hook #'garbage-collect) #+end_src -* Packages -** quelpa -Quelpa lets you install from local or remote source (like git). -With quelpa-use-package, I can use the keyword ~:quelpa~ to install via quelpa. +* Sound support +On macOS, you can use afplay: #+begin_src emacs-lisp - (use-package quelpa) - (use-package quelpa-use-package) + (defun za/play-sound-file-macos (file &optional volume device) + "Play sound using `afplay` on macOS" + (unless (file-readable-p file) + (user-error "File %s not readable." file)) + + ;; the `apply` is required here because I need to build a list of arguments + (apply 'start-process `("afplay" nil + "afplay" + ,@(if volume (list "-v" (int-to-string volume))) + ,file))) #+end_src -** exec-path-from-shell (macOS) -In macOS, the path is not set correctly (i.e. as it is in the terminal) in the GUI app. This fixes it. +Then redefine the play-sound-file function where needed: #+begin_src emacs-lisp - (when (string-equal system-type "darwin") - (use-package exec-path-from-shell - :config - (add-to-list 'exec-path-from-shell-variables "NOTMUCH_CONFIG") - (exec-path-from-shell-initialize))) + (cond ((and (not (fboundp 'play-sound-internal)) + (eq system-type 'darwin)) + (advice-add 'play-sound-file :override #'za/play-sound-file-macos))) #+end_src +* DISABLED Fix non-dbus macOS notification +macOS version might not be compiled with dbus support; in that case you can use e.g. terminal-notifier. +If you use the ~sender~ option, notifications don't show +unless the app is in the background. [[https://github.com/julienXX/terminal-notifier/issues/68][See this Github issue.]] -** emacs-which-key -Minor mode for Emacs that displays the key bindings following your currently entered incomplete command (a prefix) in a popup. - -#+BEGIN_SRC emacs-lisp - (use-package which-key - :config - (which-key-mode)) +#+begin_src emacs-lisp :tangle no + ;; on mac without dbus: + (org-show-notification-handler + (lambda (str) (start-process "terminal-notifier" nil (executable-find "terminal-notifier") + "-title" "Timer done" + "-message" str + "-group" "org.gnu.Emacs" + "-ignoreDnD" + "-activate" "org.gnu.Emacs"))) #+end_src -** counsel + ivy + swiper + prescient -Better incremental completion and selection narrowing. -And a bunch more. -Generally makes for nicer interactivity, like ido mode on steroids. -Switched to this from Helm, it's more lightweight. - -The components: -- ivy: generic completion mechanism -- counsel: collection of common Emacs commands enhanced using ivy -- swiper: search enhanced using ivy -- prescient: scoring system for M-x +* Editing +** Overwrite selection on typing +Normally, when I select something and start typing, Emacs clears the selection, i.e. it deselects and inserts text after the cursor. +I want to replace the selection. #+begin_src emacs-lisp - (use-package counsel :demand - :config - (setq ivy-use-virtual-buffers t ; extend searching to bookmarks and - ivy-height 20 ; set height of the ivy window - ivy-count-format "(%d/%d) " ; count format, from the ivy help page - ivy-display-style 'fancy - ivy-format-function 'ivy-format-function-line) - (ivy-mode) - (counsel-mode) - - (defun edit-script () - "Edit a file in ~/.scripts/" - (interactive) - (let ((input (ivy--input))) - (ivy-quit-and-run (counsel-file-jump nil "~/.scripts/")))) - (defun edit-config () - "Edit a file in ~/.dotfiles/" - (interactive) - (let ((input (ivy--input))) - (ivy-quit-and-run (counsel-file-jump nil "~/.dotfiles/"))))) - - (use-package prescient :config (prescient-persist-mode)) - (use-package ivy-prescient - :after counsel - :config - (setq ivy-prescient-retain-classic-highlighting t) - (ivy-prescient-mode)) + (delete-selection-mode t) #+end_src -Set the key bindings: +** Strip trailing whitespace +You can show trailing whitespace by setting show-trailing-whitespace to 't'. +But I want to automatically strip trailing whitespace. +Luckily there's already a function for that, I just need to call it in a hook: #+begin_src emacs-lisp - (za/global-set-key (kbd "C-s") #'swiper-isearch) - (za/global-set-key (kbd "C-r") #'swiper-isearch-backward) - (za/global-set-key (kbd "M-x") #'counsel-M-x) - (za/global-set-key (kbd "C-x C-f") #'counsel-find-file) - (za/global-set-key (kbd "M-y") #'counsel-yank-pop) - (za/global-set-key (kbd "C-x b") #'ivy-switch-buffer) - (za/global-set-key (kbd "C-c v") #'ivy-push-view) - (za/global-set-key (kbd "C-c V") #'ivy-pop-view) - (za/global-set-key (kbd "C-c c") #'counsel-compile) - (za/global-set-key (kbd "M-s g") #'counsel-ag) - (za/global-set-key (kbd "M-s f") #'counsel-fzf) - (za/global-set-key (kbd "C-c b") #'counsel-bookmark) - (za/global-set-key (kbd "C-c p") #'counsel-recentf) - (za/global-set-key (kbd "C-c o") #'counsel-outline) - (za/global-set-key (kbd "C-h f") #'counsel-describe-function) - (za/global-set-key (kbd "C-h v") #'counsel-describe-variable) - (za/global-set-key (kbd "C-h o") #'counsel-describe-symbol) + (add-hook 'before-save-hook #'delete-trailing-whitespace) #+end_src +** Formatting & indentation -I like having ivy in a popup. -Problem: posframe does not work if emacs is too old and on macos. -See here: https://github.com/tumashu/posframe/issues/30 -On Mac, ~brew install --HEAD emacs~ doesn't work either. -Solution: ~brew tap daviderestivo/emacs-head && brew install emacs-head@28 --with-cocoa~ +Show a tab as 8 spaces: #+begin_src emacs-lisp - (if (and ( version< emacs-version "28") (equal system-type 'darwin)) - (message "ivy-posframe won't work properly, run `brew install daviderestivo/emacs-head/emacs-head@28 --with-cocoa`") - (use-package ivy-posframe - :config - (setq ivy-posframe-display-functions-alist '((t . ivy-posframe-display-at-frame-center))) - (setq ivy-posframe-parameters - '((left-fringe . 8) - (right-fringe . 8))) - (setq ivy-posframe-border-width 3) - (custom-set-faces '(ivy-posframe-border ((t (:inherit mode-line-inactive))))) - (ivy-posframe-mode 1))) + (setq-default tab-width 8) #+end_src -Have to set this to let me select exactly what I'm typing as a candidate: +Never insert tabs with indentation by default: #+begin_src emacs-lisp - (setq ivy-use-selectable-prompt t) + (setq-default indent-tabs-mode nil) #+end_src -Also, accidentally pressing shift-space deletes input, because by default, shift-space is bound to ~ivy-restrict-to-matches~ in the ivy minibuffer. I don't want that, I want it bound to shift-tab: +Allow switching between the two easily: #+begin_src emacs-lisp - (define-key ivy-minibuffer-map (kbd "S-SPC") (lambda () (interactive) (insert ?\s))) - (define-key ivy-minibuffer-map (kbd "<backtab>") #'ivy-restrict-to-matches) + (defun indent-tabs () + (interactive) + (setq indent-tabs-mode t)) + (defun indent-spaces () + (interactive) + (setq indent-tabs-mode nil)) #+end_src -Custom Ivy actions: +Indentation for various modes: #+begin_src emacs-lisp - (ivy-add-actions - 'counsel-dired - '(("f" (lambda (dir) (counsel-fzf nil dir)) "Fzf in directory") - ("g" (lambda (dir) (counsel-ag nil dir)) "Ag in directory"))) - (ivy-add-actions - 'dired - '(("f" (lambda (dir) (ivy-exit-with-action (counsel-fzf nil dir))) "Fzf in directory") - ("g" (lambda (dir) (ivy-exit-with-action (counsel-ag nil dir))) "Ag in directory"))) + (setq-default sh-basic-offset 2 + c-basic-offset 4) #+end_src -** company -Good completion. -#+begin_src emacs-lisp - (use-package company - :hook (ledger-mode . company-mode)) -#+end_src -** wgrep -#+begin_src emacs-lisp - (use-package wgrep) -#+end_src -** avy -This lets me jump to any position in Emacs rather quickly, sometimes it's useful. -~avy-goto-char-timer~ lets me type a part of the text before avy kicks in. +** Wrapping +A function to toggle wrapping: #+begin_src emacs-lisp - (use-package avy - :bind - (("C-:" . 'avy-goto-char-timer))) -#+end_src + (defvar-local za/wrapping nil "Wrapping changes per buffer.") -** Org -*** Installation -Install Org and require additional components that I use. + (defun za/toggle-wrap (&optional enable) + "Toggle line wrapping settings. With ENABLE a positive number, enable wrapping. If ENABLE is negative or zero, disable wrapping." + (interactive "P") ; prefix arg in raw form -#+begin_src emacs-lisp - (use-package org - :config - (require 'org-tempo) - (require 'org-habit) - (require 'org-agenda) - (require 'org-id)) -#+end_src + ;; If an argument is provided, prefix or otherwise + (if enable + (let ((enable (cond ((numberp enable) + enable) + ((booleanp enable) + (if enable 1 0)) + ((or (listp enable) (string= "-" enable)) + (prefix-numeric-value enable))))) + ;; If zero or negative, we want to disable wrapping, so pretend it's currently enabled. + ;; And vice versa. + (cond ((<= enable 0) (setq za/wrapping t)) + ((> enable 0) (setq za/wrapping nil))))) -*** Key bindings -Global: -#+begin_src emacs-lisp - (za/global-set-key (kbd "C-c a") #'org-agenda) - (za/global-set-key (kbd "C-c n") #'org-capture) - (za/global-set-key (kbd "C-c l") #'org-store-link) -#+end_src -Org mode: + (let ((disable-wrapping (lambda () + (visual-line-mode -1) + (toggle-truncate-lines t))) + (enable-wrapping (lambda () + (toggle-truncate-lines -1) + (visual-line-mode)))) -#+begin_src emacs-lisp - (defun za/keybinds-org-mode () - "Function to set org-mode keybindings, run via org-mode-hook." - (define-key org-mode-map (kbd "C-M-<return>") #'org-insert-todo-heading) - (define-key org-mode-map (kbd "C-c M-y") #'org-yank-link-url) - (define-key org-mode-map (kbd "C-c N") #'org-noter) - (define-key org-mode-map (kbd "C-M-i") #'completion-at-point) - (define-key org-mode-map (kbd "C-c q t") #'org-timestone-set-org-current-time-effective)) + ;; If za/wrapping is not locally set, infer its values from the enabled modes + (unless (boundp 'za/wrapping) + (setq za/wrapping (and visual-line-mode + (not truncate-lines)))) - (add-hook 'org-mode-hook #'za/keybinds-org-mode) + ;; Toggle wrapping based on current value + (cond (za/wrapping + (funcall disable-wrapping) + (setq za/wrapping nil) + (message "Wrapping disabled.")) + (t + (funcall enable-wrapping) + (setq za/wrapping t) + (message "Wrapping enabled."))))) #+end_src -Easier link following. Actual enter is still possible with ~C-q C-j~. -[[https://google.com][Goggles]] -#+begin_src emacs-lisp - (setq org-return-follows-link t) -#+end_src -*** Nicer bullets -In org mode, I want to use bullets instead of stars, so I also install ~org-superstar~. +And a keybinding to toggle wrapping: #+begin_src emacs-lisp - (use-package org-superstar - :config - (setq org-superstar-leading-bullet ?\s)) + (bind-key "C-c q w" #'za/toggle-wrap) #+end_src -*** More languages +** Pager toggle +M-x view-mode enables pager behavior. +I want read-only files to automatically use pager mode: + #+begin_src emacs-lisp -(use-package inf-ruby) + (setq view-read-only t) #+end_src +** Prefer newer file loading #+begin_src emacs-lisp - (org-babel-do-load-languages - 'org-babel-load-languages - '((emacs-lisp . t) - (R . t) - (python . t) - (ruby . t) - (shell . t))) - - (setq org-babel-python-command "python3") - (setq org-confirm-babel-evaluate nil) + (setq load-prefer-newer t) #+end_src -*** Org mode settings -Furthermore, tags were getting cut off, so I manually set the best column to display them. + +** Automatically find tags file +When opening a file in a git repo, try to discover the etags file: #+begin_src emacs-lisp - (defun za/settings-org-mode () - "My settings for org mode" - (org-superstar-mode 1) - (za/toggle-wrap t) - (org-indent-mode) - (setq org-tags-column (- 10 (window-total-width))) - (setq org-src-tab-acts-natively t) ; a tab in a code block indents the code as it should - ;; Realign tags - (org-set-tags-command '(4))) + (defun current-tags-file () + "Get current tags file" + (let* ((tagspath ".git/etags") + (git-root (locate-dominating-file (buffer-file-name) tagspath))) + (if git-root + (expand-file-name tagspath git-root)))) - (add-hook 'org-mode-hook #'za/settings-org-mode) + (setq default-tags-table-function #'current-tags-file) #+end_src -Link settings: +There's probably a better way to write this. I need to ask Reddit for feedback at some point. + +** End sentences with one space +Emacs uses the rather old-fashioned convention of treating a period followed by double spaces as end of sentence. However, it is more common these days to end sentences with a period followed by a single space. + +Let a period followed by a single space be treated as end of sentence: #+begin_src emacs-lisp - (setq org-link-elisp-confirm-function #'y-or-n-p - org-link-elisp-skip-confirm-regexp "^org-noter$") + (setq sentence-end-double-space nil) #+end_src - -Clock sound: +* Keybindings +** Expansion/completion +Use hippie expand instead of dabbrev-expand: #+begin_src emacs-lisp - (setq org-clock-sound (concat user-emacs-directory "notification.wav")) + (bind-key "M-/" #'hippie-expand) #+end_src -*** Enable linking to email via notmuch -To be able to link to emails via notmuch, I use ol-notmuch: +** Zap up to char +It's more useful for me to be able to delete up to a character instead of to and including a character: #+begin_src emacs-lisp - (use-package ol-notmuch :quelpa (:upgrade t)) + (bind-key "M-z" #'zap-up-to-char) #+end_src -*** Agenda & GTD -**** Agenda mode settings -Fix tag display by dynamically calculating the column. -#+begin_src emacs-lisp - (defun za/settings-org-agenda-mode () - "My settings for org agenda mode" - (setq org-agenda-tags-column (- 10 (window-total-width)))) +** Forward-word and forward-to-word +Change M-f to stop at the start of the word: - (add-hook 'org-agenda-mode-hook #'za/settings-org-agenda-mode) +#+begin_src emacs-lisp + (bind-key "M-f" #'forward-to-word) #+end_src -Binding to show current agenda thing as narrowed subtree +Bind ESC M-f to the old functionality of M-f (stop at end of word) #+begin_src emacs-lisp - (defun za/org-agenda-goto-narrowed-subtree () - "Jump to current agenda item and narrow to its subtree." - (interactive) - (delete-other-windows) - (org-agenda-goto) - (org-narrow-to-subtree) - (other-window 1)) - (define-key org-agenda-mode-map (kbd "C-c TAB") #'za/org-agenda-goto-narrowed-subtree) + (bind-key "ESC M-f" #'forward-word) #+end_src -**** Set file locations -Which files should be included in the agenda (I have to use ~list~ to evaluate the variables, because org-agenda-files expects strings): +** Rectangle insert string #+begin_src emacs-lisp - (setq org-agenda-files (list za/org-life-main - za/org-life-inbox - za/org-life-tickler)) + (bind-key "C-x r I" #'string-insert-rectangle) + (bind-key "C-x r R" #'replace-rectangle) #+end_src -I want to search all Org files in the life directory: +** Toggle auto-revert-mode +Sometimes I want to toggle auto reverting (or autoread) of buffer: #+begin_src emacs-lisp - (setq org-agenda-text-search-extra-files (directory-files za/org-life-dir t (rx bol (not ?.) (* anything) ".org"))) + (bind-key "C-c q a" #'auto-revert-mode) #+end_src - -Convenience functions to make opening the main file faster: +** Fast access to view-mode (pager) +I want to bind view-mode to a key for easy access: #+begin_src emacs-lisp - (defun gtd () "GTD: main file" (interactive) (find-file za/org-life-main)) - (defun gtd-inbox () "GTD: inbox" (interactive) (find-file za/org-life-inbox)) - (defun gtd-archive () "GTD: archive" (interactive) (find-file za/org-life-archive)) - (defun gtd-someday () "GTD: someday" (interactive) (find-file za/org-life-someday)) - (defun gtd-tickler () "GTD: tickler" (interactive) (find-file za/org-life-tickler)) + (bind-key "C-c q r" 'view-mode) #+end_src -Bind keys to those functions: +** Kill this buffer +I like to be able to kill a buffer instantly: #+begin_src emacs-lisp - (za/global-set-key (kbd "M-g t i") #'gtd-inbox) - (za/global-set-key (kbd "M-g t g") #'gtd) - (za/global-set-key (kbd "M-g t a") #'gtd-archive) - (za/global-set-key (kbd "M-g t s") #'gtd-someday) - (za/global-set-key (kbd "M-g t t") #'gtd-tickler) + (bind-key "s-<backspace>" 'kill-current-buffer) #+end_src -**** Refiling & archiving -Where I want to be able to move subtrees (doesn't include inbox because I never refile to that, and the archive has its own keybining): - +** Delete this file (and kill the buffer) #+begin_src emacs-lisp - (setq org-refile-targets `((,za/org-life-main :maxlevel . 3) - (,za/org-life-someday :level . 1) - (,za/org-life-tickler :maxlevel . 2))) + (defun za/delete-this-file () + "Kill the current buffer and delete its associated file." + (interactive) + (let ((fname (buffer-file-name)) + (buf (current-buffer))) + (unless (and fname (file-exists-p fname)) + (user-error (format "Buffer has no associated file."))) + + (unless (y-or-n-p (format "Really delete %s and its buffer?" fname)) + (user-error "User cancelled.")) + + (delete-file fname 'trash-if-enabled) + (kill-buffer buf) + (message "Deleted %s and killed its buffer." fname))) + + (bind-key "C-c s-<backspace>" #'za/delete-this-file) #+end_src -I want to archive to a specific file, in a date tree: +** Toggle fullscreen +I'll use the keybinding that's standard on macOS: #+begin_src emacs-lisp - (setq org-archive-location (concat za/org-life-archive "::datetree/")) + (bind-key "C-s-f" #'toggle-frame-fullscreen) #+end_src -Include the destination file as an element in the path to a heading, and to use the full paths as completion targets rather than just the heading text itself: +** Sexp manipulation +When I write lisp, sometimes I want to switch two sexps (e.g. ~(one) (two)~ → ~(two) (one)~), so a key binding is nice for that: #+begin_src emacs-lisp - (setq org-refile-use-outline-path 'file) + (bind-key "C-S-t" #'transpose-sexps) #+end_src -Tell Org that I don’t want to complete in steps; I want Org to generate all of the possible completions and present them at once (necessary for Helm/Ivy): +Also, to raise a sexp (e.g. ~(one (two))~ → ~(two)~): #+begin_src emacs-lisp - (setq org-outline-path-complete-in-steps nil) + (bind-key "C-S-u" #'raise-sexp) #+end_src -Allow me to tack new heading names onto the end of my outline path, and if I am asking to create new ones, make me confirm it: +** Dedicated windows +Sometimes I want to avoid Emacs overriding a window's contents. +So I create a keybinding to toggle dedicated on a window: #+begin_src emacs-lisp - (setq org-refile-allow-creating-parent-nodes 'confirm) -#+end_src + (defun za/toggle-window-dedicated-p () + "Toggle set-window-dedicated-p on current window" + (interactive) + (cond ((window-dedicated-p (selected-window)) + (set-window-dedicated-p (selected-window) nil) + (message "Window no longer dedicated")) + (t + (set-window-dedicated-p (selected-window) t) + (message "Window marked as dedicated")))) -**** Quick capture -Quick capture lets me send something to my inbox very quickly, without thinking about where it should go. -The inbox is processed later. + (bind-key "C-x 9" #'za/toggle-window-dedicated-p) -Templates for quick capture: +#+end_src +** Rotate windows horizontal ↔ vertical #+begin_src emacs-lisp - (setq org-capture-templates `(("t" "Todo [inbox]" entry - (file ,za/org-life-inbox) - "* TODO %i%?") + (defun za/rotate-windows () + (interactive) + (if (= (count-windows) 2) + (let* ((this-win-buffer (window-buffer)) + (next-win-buffer (window-buffer (next-window))) + (this-win-edges (window-edges (selected-window))) + (next-win-edges (window-edges (next-window))) + (this-win-2nd (not (and (<= (car this-win-edges) + (car next-win-edges)) + (<= (cadr this-win-edges) + (cadr next-win-edges))))) + (splitter + (if (= (car this-win-edges) + (car (window-edges (next-window)))) + 'split-window-horizontally + 'split-window-vertically))) + (delete-other-windows) + (let ((first-win (selected-window))) + (funcall splitter) + (if this-win-2nd (other-window 1)) + (set-window-buffer (selected-window) this-win-buffer) + (set-window-buffer (next-window) next-win-buffer) + (select-window first-win) + (if this-win-2nd (other-window 1)))))) +#+end_src - ("s" "Save for read/watch/listen" entry - (file+headline ,za/org-life-tickler "Read/watch/listen") - "* TODO %?[[%^{link}][%^{description}]] %^G"))) +#+begin_src emacs-lisp + (bind-key "C-x 7" #'za/rotate-windows) #+end_src -**** Todo & custom agenda views -Todo keywords based on the GTD system (pipe separates incomplete from complete). -Apart from the logging-on-done configured [[*Logging][below]], I also want to log a note & timestamp when I start waiting on something. -In ~org-todo-keywords~, ~@~ means note+timestamp, ~!~ means timestamp, ~@/!~ means note+timestamp on state entry and timestamp on leave. +** Open line like in Vim +I prefer to open-line the way o/O works in Vim: #+begin_src emacs-lisp - (setq org-todo-keywords '((sequence "TODO(t)" "NEXT(n)" "WAITING(w@)" "STARTED(s)" "|" "DONE(d)" "CANCELLED(c)")) - org-todo-keyword-faces '(("TODO" . org-todo) - ("NEXT" . org-todo) - ("WAITING" . org-todo) - ("STARTED" . org-todo) - ("DONE" . org-done) - ("CANCELLED" . org-done))) + ;; Autoindent open-*-lines + (defvar za/open-line-newline-and-indent t + "Modify the behavior of the open-*-line functions to cause them to autoindent.") + + (defun za/open-line (prefix) + "Open line like `o`/`O` in Vim. Negative prefix for line above, positive for below." + (interactive "p") + (cond ((< prefix 0) + (beginning-of-line) + (open-line (abs prefix))) + (t + (end-of-line) + (open-line prefix) + (forward-line 1))) + (when za/open-line-newline-and-indent + (indent-according-to-mode))) + + (defun za/open-line-keep-point (prefix) + "Open line like `o`/`O` in Vim but don't move point. Negative prefix for line above, positive for below." + (interactive "p") + (save-mark-and-excursion (za/open-line prefix))) #+end_src -I decided that projects will not be TODO items, but their progress will be tracked with a progress cookie ([x/y]). This function converts an item to a project: it adds a PROJECT tag, sets the progress indicator to count all checkboxes in sub-items (only TODO items), and removes any existing TODO keywords. Finally, PROJECT tags shouldn't be inherited (i.e. subtasks shouldn't be marked as projects). +And keybindings: #+begin_src emacs-lisp - (defun za/mark-as-project () - "This function makes sure that the current heading has - (1) the tag PROJECT - (2) the property COOKIE_DATA set to \"todo recursive\" - (3) a leading progress indicator" - (interactive) - (org-set-property "TODO" "") - (org-toggle-tag "PROJECT" 'on) - (org-set-property "COOKIE_DATA" "todo recursive") - (org-back-to-heading t) - (forward-whitespace 1) - (insert "[/] ") - (org-update-statistics-cookies nil)) + (bind-key "C-o" #'za/open-line) + (bind-key "C-M-o" #'za/open-line-keep-point) #+end_src -Only the top-level project headlines should be tagged as projects, so disable inheritance of that tag: +** Unfill region/paragraph +Taken from here: https://www.emacswiki.org/emacs/UnfillParagraph #+begin_src emacs-lisp - (setq org-tags-exclude-from-inheritance '("PROJECT")) + (defun za/unfill-paragraph (&optional region) + "Takes a multi-line paragraph and makes it into a single line of text." + (interactive (progn (barf-if-buffer-read-only) '(t))) + (let ((fill-column (point-max)) + ;; This would override `fill-column' if it's an integer. + (emacs-lisp-docstring-fill-column t)) + (fill-paragraph nil region))) + + (bind-key "M-Q" #'za/unfill-paragraph) #+end_src +** Easily edit my config +Bind a keyboard shortcut to open my config. +The "(interactive)" means that it can be called from a keybinding or from M-x. -Define a function to skip items if they're part of a project (i.e. one of their parents has a "PROJECT" tag). -The problem is, the "PROJECT" tag isn't inherited. So, we temporarily disable excluding from inheritance, just for the ~org-get-tags~ call. Then check if "PROJECT" is one of the tags. +#+begin_src emacs-lisp + (defun za/edit-config-org () + "Edit my config.org file" + (interactive) + (find-file (expand-file-name "config.org" user-emacs-directory))) +#+end_src #+begin_src emacs-lisp - (defun za/skip-if-in-project () - "Skip items that are part of a project" - (let ((subtree-end (save-excursion (org-end-of-subtree t))) - (item-tags (let ((org-tags-exclude-from-inheritance nil)) (org-get-tags)))) - (if (member "PROJECT" item-tags) - subtree-end - nil))) + (bind-key "C-c E" 'za/edit-config-org) +#+end_src +* Custom functions +** Make region readonly or writable +#+begin_src emacs-lisp + (defun za/set-region-read-only (begin end) + "Sets the read-only text property on the marked region. + Use `set-region-writeable' to remove this property." + ;; See https://stackoverflow.com/questions/7410125 + (interactive "r") + (with-silent-modifications + (put-text-property begin end 'read-only t))) + (defun za/set-region-writeable (begin end) + "Removes the read-only text property from the marked region. + Use `set-region-read-only' to set this property." + ;; See https://stackoverflow.com/questions/7410125 + (interactive "r") + (with-silent-modifications + (remove-text-properties begin end '(read-only t)))) #+end_src +** Insert macro as Lisp +From here: https://www.masteringemacs.org/article/keyboard-macros-are-misunderstood -Also, define a function to skip tasks (trees) that are not habits (i.e. don't have the STYLE property ~habit~): +#+begin_src emacs-lisp + (use-package kmacro + :ensure nil ; included with Emacs + :bind (:map kmacro-keymap + ("I" . kmacro-insert-macro)) + :config + (defalias 'kmacro-insert-macro 'insert-kbd-macro) + ;; Add advice to ignore errors on `kmacro-keyboard-macro-p`, it was + ;; messing up because of some entry in `obarray` + (advice-add #'kmacro-keyboard-macro-p :around (lambda (fun sym) "Ignore errors." (ignore-errors (funcall fun sym))))) +#+end_src +** Show local help at point when idling #+begin_src emacs-lisp - (defun za/skip-unless-habit () - "Skip trees that are not habits" - (let ((subtree-end (save-excursion (org-end-of-subtree t)))) - (if (string= (org-entry-get nil "STYLE") "habit") - nil - subtree-end))) + (defun za/echo-area-tooltips () + "Show tooltips in the echo area automatically for current buffer." + (setq-local help-at-pt-display-when-idle t + help-at-pt-timer-delay 0) + (help-at-pt-cancel-timer) + (help-at-pt-set-timer)) #+end_src -And one to skip tasks that /are/ habits: +** Info manual functions +For some reason, these things don't show up in the index: #+begin_src emacs-lisp - (defun za/skip-if-habit () - "Skip trees that are not habits" - (let ((subtree-end (save-excursion (org-end-of-subtree t)))) - (if (string= (org-entry-get nil "STYLE") "habit") - subtree-end - nil))) + (defun elisp-info (&optional node) + "Read documentation for Elisp in the info system. + With optional NODE, go directly to that node." + (interactive) + (info (format "(elisp)%s" (or node "")))) #+end_src +Though I can also just use ~info-display-manual~. -And another function, to skip tasks that are blocked: +** Radio +Just a wrapper function to my radio script: #+begin_src emacs-lisp - (defun za/skip-if-blocked () - "Skip trees that are blocked by previous tasks" - (let ((subtree-end (save-excursion (org-end-of-subtree t)))) - (if (org-entry-blocked-p) - subtree-end - nil))) + (defun radio () + "Play an internet radio" + (interactive) + (ansi-term "radio" "*radio*")) #+end_src -Create custom agenda view based on those keywords. -Agenda views are made up of blocks, appearing in the order that you declare them. -The first two strings are what shows up in the agenda dispatcher (the key to press and the description). +** no-op +#+begin_src emacs-lisp + (defun za/no-op (&rest args)) +#+end_src +** Syncthing +Some functions to start/stop syncthing. #+begin_src emacs-lisp - (setq org-agenda-custom-commands - '(("n" "Next actions" - todo "NEXT" ((org-agenda-overriding-header "Next actions:") - (org-agenda-sorting-strategy '(priority-down alpha-up)))) - ("W" "Waiting" - ((todo "WAITING" ((org-agenda-overriding-header "Waiting:"))))) - ("S" . "Saved for later...") - ("Sw" "Saved to watch" - ((tags-todo "WATCH" ((org-agenda-overriding-header "To watch:"))))) - ("Sr" "Saved to read" - ((tags-todo "READ" ((org-agenda-overriding-header "To read:"))))) - ("Sl" "Saved to listen" - ((tags-todo "LISTEN" ((org-agenda-overriding-header "To listen:"))))) - - ("a" . "Agenda with schedule only...") - ("aw" "This week" - ((agenda "" ((org-agenda-span 'week))))) - ("ad" "Today" - ((agenda "" ((org-agenda-span 'day))))) - ("at" "Tomorrow" - ((agenda "" ((org-agenda-span 'day) - (org-agenda-start-day "+1d"))))) - - ("w" "Week Agenda + Next Actions" - ((agenda "" ((org-agenda-overriding-header "Week agenda:"))) - (todo "NEXT" ((org-agenda-overriding-header "Next actions:"))))) - - ("o" "Month agenda" - ((agenda "" ((org-agenda-overriding-header "Month agenda:") - (org-agenda-span 'month))))) - - ("d" "Day Agenda + Next Actions + Habits" - ((agenda "" ((org-agenda-overriding-header "Day:") - (org-agenda-span 'day) - (org-habit-show-habits nil))) - (todo "NEXT" ((org-agenda-overriding-header "Next actions:"))) - (agenda "" ((org-agenda-overriding-header "Habits:") - (org-agenda-span 'day) - (org-agenda-use-time-grid nil) - (org-agenda-skip-function 'za/skip-unless-habit) - (org-habit-show-habits t) (org-habit-show-habits-only-for-today nil) - (org-habit-show-all-today t))) - (todo "WAITING" ((org-agenda-overriding-header "Waiting:"))))) - - ("k" "Kanban view" - ((todo "DONE" ((org-agenda-overriding-header "Done:") (org-agenda-sorting-strategy '(deadline-up priority-down alpha-up)))) - (todo "STARTED" ((org-agenda-overriding-header "In progress:") (org-agenda-sorting-strategy '(deadline-up priority-down alpha-up)))) - (todo "NEXT" ((org-agenda-overriding-header "To do:") (org-agenda-sorting-strategy '(deadline-up priority-down alpha-up)))))) - - ("p" "Projects" - ((tags "PROJECT" ((org-agenda-overriding-header "Projects:") - (org-agenda-prefix-format '((tags . " %i %-22(let ((deadline (org-entry-get nil \"DEADLINE\"))) (if deadline deadline \"\"))"))) - (org-agenda-sorting-strategy '((tags deadline-up alpha-down))))))) - - ("f" "Finished tasks that aren't in a project" - ((tags "TODO=\"DONE\"|TODO=\"CANCELLED\"" ((org-agenda-overriding-header "Finished tasks:") - (org-agenda-skip-function 'za/skip-if-in-project))))) - - ;; Useful thread for opening calfw: https://github.com/kiwanami/emacs-calfw/issues/18 - ("c" "Calendar view" (lambda (&rest _) - (interactive) - (let ((org-agenda-skip-function 'za/skip-if-habit)) - (cfw:open-org-calendar)))))) -#+end_src - -In calfw, I don't want to show habits: - -#+begin_src emacs-lisp - (add-hook 'cfw:calendar-mode-hook (setq-local org-agenda-skip-function 'za/skip-if-habit)) -#+end_src - -**** Logging for tasks -I want to log into the LOGBOOK drawer (useful when I want to take quick notes): - -#+begin_src emacs-lisp - (setq org-log-into-drawer "LOGBOOK") -#+end_src - -I also want to log when I finish a task (useful for archiving). -Furthermore, when I'm done, I want to add a note (any important -workarounds/tips). And when I reschedule, I want to know the reason. -I can disable logging on state change for a specific task by adding ~:LOGGING: nil~ to the ~:PROPERTIES:~ drawer. - -#+begin_src emacs-lisp - (setq org-log-done 'note - org-log-reschedule 'note) -#+end_src - -I want to hide drawers on startup. This variable has options: -- 'overview': Top-level headlines only. -- 'content': All headlines. -- 'showall': No folding on any entry. -- 'show2levels: Headline levels 1-2. -- 'show3levels: Headline levels 1-3. -- 'show4levels: Headline levels 1-4. -- 'show5levels: Headline levels 1-5. -- 'showeverything: Show even drawer contents. - -#+begin_src emacs-lisp - (setq org-startup-folded 'content) -#+end_src - -**** Task ordering -Some tasks should be ordered, i.e. they should be done in steps. -Those have the ~:ORDERED: t~ setting in ~:PROPERTIES:~, and it should be enforced: - -#+begin_src emacs-lisp - (setq org-enforce-todo-dependencies t) -#+end_src - -Furthermore, tasks that are ordered and can't be done yet because of previous steps should be dimmed in the agenda: - -#+begin_src emacs-lisp - (setq org-agenda-dim-blocked-tasks t) -#+end_src - -I might also want to set ~org-enforce-todo-checkbox-dependencies~, but not convinced on that one yet. - -**** Time tracking & effort -Time tracking should be done in its own drawer: - -#+begin_src emacs-lisp - (setq org-clock-into-drawer "CLOCK") -#+end_src - -And to customize how clock tables work: - -#+begin_src emacs-lisp - (setq org-clocktable-defaults '(:lang "en" :scope agenda-with-archives :wstart 1 :mstart 1 :compact t :maxlevel nil)) - (setq org-agenda-clockreport-parameter-plist '(:link t :maxlevel nil)) -#+end_src - -I want to set effort in hours:minutes: - -#+begin_src emacs-lisp - (add-to-list 'org-global-properties '("Effort_ALL" . "0:05 0:10 0:15 0:20 0:30 0:45 1:00 1:30 2:00 4:00 6:00 8:00")) -#+end_src - -I want column view to look like this: - -| To do | Task | Tags | Sum of time elapsed | Sum of time estimated (effort) | -|--------------+-----------+------+---------------------+--------------------------------| -| todo keyword | task name | tags | sum of clock | sum of estimated time | -| ... | ... | ... | ... | ... | - -#+begin_src emacs-lisp - (setq org-columns-default-format "%7TODO (To Do) %32ITEM(Task) %TAGS(Tags) %11CLOCKSUM_T(Clock) %10Difficulty(Difficulty) %8Effort(Effort){:}") -#+end_src - -Fix column alignment in agenda. - -#+begin_src emacs-lisp - (set-face-attribute 'org-column nil - :height (face-attribute 'default :height) - :family (face-attribute 'default :family)) - (set-face-attribute 'org-agenda-date-today nil - :height (face-attribute 'default :height)) -#+end_src - -**** Calculate time since timestamp -#+begin_src emacs-lisp - (defun za/org-time-since () - "Print the amount of time between the timestamp at point and the current date and time." - (interactive) - (unless (org-at-timestamp-p 'lax) - (user-error "Not at timestamp")) - - (when (org-at-timestamp-p 'lax) - (let ((timestamp (match-string 0))) - (with-temp-buffer - (insert timestamp - "--" - (org-time-stamp '(16))) - (org-evaluate-time-range))))) -#+end_src - -*** Priorities: how important something is -I usually have a lot of 'next' actions, so I prefer 4 priority levels instead of the default 3: A (urgent, ASAP), B (important), C (not super important), D (do in free time): - -#+begin_src emacs-lisp - (setq org-priority-highest ?A - org-priority-lowest ?D - org-priority-default ?C) -#+end_src - -Faces for priorities in agenda: - -#+begin_src emacs-lisp - (setq org-priority-faces `((?A . (:foreground ,(face-foreground 'error))) - (?B . (:foreground ,(face-foreground 'org-todo))) - (?C . (:foreground ,(face-foreground 'font-lock-constant-face) :weight semi-light)) - (?D . (:foreground ,(face-foreground 'font-lock-string-face) :slant italic :weight light)))) -#+end_src - -*** Energy requirement: how difficult something is -#+begin_src emacs-lisp - (add-to-list 'org-global-properties '("Difficulty_ALL" . "low medium high")) -#+end_src -*** Custom functions -**** Get number of headlines in a file -#+begin_src emacs-lisp - (defun za/org-count-headlines-in-file (level filename) - "Count number of level LEVEL headlines in FILENAME. If LEVEL is 0, count all." - (let ((headline-str (cond ((zerop level) "^\*+") - (t (format "^%s " (apply 'concat (make-list level "\\*"))))))) - (save-mark-and-excursion - (with-temp-buffer - (insert-file-contents filename) - (count-matches headline-str (point-min) (point-max)))))) -#+end_src - -**** Yank URL -#+begin_src emacs-lisp - (defun org-yank-link-url () - (interactive) - (kill-new (org-element-property :raw-link (org-element-context))) - (message "Link copied to clipboard")) -#+end_src -**** Manipulating time -#+begin_src emacs-lisp - (use-package org-timestone - :quelpa (org-timestone :repo "thezeroalpha/org-timestone.el" :fetcher github) - :ensure nil) -#+end_src -*** Tempo expansions - -#+begin_src emacs-lisp - (add-to-list 'org-structure-template-alist '("se" . "src emacs-lisp")) - (add-to-list 'org-structure-template-alist '("sb" . "src bibtex")) - (add-to-list 'org-structure-template-alist '("ss" . "src sh")) -#+end_src - -*** Exporting -#+begin_src emacs-lisp -(custom-set-variables '(org-export-backends '(ascii html icalendar latex md odt org))) -#+end_src - -*** Catch invisible edits -Sometimes when text is folded away, I might accidentally edit text inside of it. -This option prevents that. -I wanted to do 'smart', but that has a 'fixme' so it might change in the future... -Instead, show what's being edited, but don't perform the edit. - -#+begin_src emacs-lisp - (setq org-catch-invisible-edits 'show-and-error) -#+end_src - -*** Notification -macOS version might not be compiled with dbus support; in that case you can use e.g. terminal-notifier. -If you use the ~sender~ option, notifications don't show -unless the app is in the background. [[https://github.com/julienXX/terminal-notifier/issues/68][See this Github issue.]] - -#+begin_src emacs-lisp - ;; on mac without dbus: - ;; (setq org-show-notification-handler - ;; (lambda (str) (start-process "terminal-notifier" nil (executable-find "terminal-notifier") - ;; "-title" "Timer done" - ;; "-message" str - ;; "-group" "org.gnu.Emacs" - ;; "-ignoreDnD" - ;; "-activate" "org.gnu.Emacs")))) -#+end_src - -*** org-caldav -This lets me sync my Org agenda to my CalDAV server. -The main reason is because Orgzly doesn't have a calendar view and can't (yet) search for events on a specific day, so if someone asks "are you free on that day", it's a bit hard for me to answer if I don't have my computer with me. -This way, I can just check my calendar. - -#+begin_src emacs-lisp - (use-package org-caldav) -#+end_src - -A lot of these variables are from my secret.el file, they're not something I can share publicly. -I use ~/.authinfo.gpg to store authorization info for the server. - -#+begin_src emacs-lisp - (setq org-caldav-url za/caldav-url - org-caldav-calendar-id za/caldav-org-calendar-id - za/org-life-calendar-inbox (concat za/org-life-dir "calendar-inbox.org") - org-caldav-inbox za/org-life-calendar-inbox - org-caldav-files (cons (car (split-string org-archive-location "::")) org-agenda-files) - org-icalendar-include-todo 'all - org-icalendar-use-deadline '(event-if-todo event-if-not-todo todo-due) - org-icalendar-use-scheduled '(todo-start event-if-todo event-if-not-todo)) - -#+end_src - -I don't want to export habits, because those will just clutter up my calendar. -The calendar is supposed to be for one-off stuff, or rarely repeating stuff. -Yes, I have to manually add the "HABIT" tag to every habit. -Perhaps nicer would be to exclude based on the property ~:STYLE: habit~, but I haven't figured that one out yet. - -#+begin_src emacs-lisp - (setq org-caldav-exclude-tags '("HABIT")) -#+end_src - -Maybe check [[https://old.reddit.com/r/orgmode/comments/8rl8ep/making_orgcaldav_useable/e0sb5j0/][this]] for a way to sync on save. - -** org-contrib -#+begin_src emacs-lisp - (use-package org-contrib - :config - (require 'org-checklist)) -#+end_src -** org-ref -#+begin_src emacs-lisp - (use-package org-ref) -#+end_src -** org-roam -#+begin_src emacs-lisp - (use-package org-roam - :custom - (org-roam-directory za/org-roam-dir) - (org-roam-completion-everywhere t) - - :config - (org-roam-setup)) - (require 'org-roam-export) -#+end_src - -#+begin_src emacs-lisp - (za/global-set-key (kbd "C-c w n") #'org-roam-capture) - (za/global-set-key (kbd "C-c w f") #'org-roam-node-find) - (za/global-set-key (kbd "C-c w w") #'org-roam-buffer-toggle) - (za/global-set-key (kbd "C-c w i") #'org-roam-node-insert) -#+end_src -** org-roam-ui -#+begin_src emacs-lisp - (use-package org-roam-ui) -#+end_src -** org-download -Drag-and-drop images to Emacs Org mode. - -#+begin_src emacs-lisp - (use-package org-download - :config - (setq org-download-method 'attach - org-download-backend 'curl)) -#+end_src -** org-sticky-header -Displays in the header-line the Org heading for the node that’s at the top of the window. - -#+begin_src emacs-lisp - (use-package org-sticky-header) -#+end_src -** org publishing -I decided, after trying many different things, to settle on org-publish. - -#+begin_src emacs-lisp - (defconst za/org-roam-top-name "Top" "The name of the top-level Org-roam node.") - (defun za/org-roam-sitemap-function (title list) - "Customized function to generate sitemap for org-roam, almost the same as `org-publish-sitemap-default`." - (concat "#+TITLE: " title "\n\n" - (format "[[file:%s][%s]]\n\n" - (file-name-nondirectory (org-roam-node-file - (org-roam-node-from-title-or-alias za/org-roam-top-name))) - "Click here for entrypoint."))) - ;; (org-list-to-org list))) <-- this is taken care of by Zola - -#+end_src - -To make this work with Zola, I need to export Github-flavored markdown (fenced code blocks with language): - -#+begin_src emacs-lisp - (require 'ox-publish) - (require 'ox-md) - - ;; See here: https://github.com/larstvei/ox-gfm/issues/36 - (use-package ox-gfm - :defer 1 - :after org) -#+end_src - -First difficulty: Zola needs front matter with ~+++...+++~. -The default Markdown backend doesn't provide that, so need to customize it by advising the default ~org-md-template~. - -#+begin_src emacs-lisp - (defun za/org-md-template-zola (contents info) - "Markdown template compatible with Zola (generates the necessary front matter from CONTENTS and INFO)." - (let ((title (org-md-plain-text (org-element-interpret-data (plist-get info :title)) info))) - (concat "+++\n" - (format "title = \"%s\"" (string-replace "\"" "'" title)) - "\n+++\n" - (format "# %s\n" title) - contents))) -#+end_src - -Second difficulty: links need to be reformatted and changed for static data (like images). -This function filters the return value of ~org-md-link~. - -#+begin_src emacs-lisp - (defun za/org-md-link-zola (linkstr) - "A filter function for the return value of - `org-md-link` (LINKSTR) to generate a link compatible with Zola." - (cond ((string-match-p (rx ".md") linkstr) - (string-replace "](" "](@/org-roam/" linkstr)) - ((string-match-p (rx "](/") linkstr) - (replace-regexp-in-string (rx "](/" (* any) "/org-roam/data") "](/org-roam-data" linkstr)) - (t linkstr))) -#+end_src - -And here's the custom publish function that adds/removes the necessary advice: - -#+begin_src emacs-lisp - (defun za/org-gfm-publish-to-gfm-zola (plist filename pub-dir) - "Run `org-gfm-publish-to-gfm`, advising the necessary - functions to generate Zola-compatible markdown." - (let ((advice '((org-md-template :override za/org-md-template-zola) - (org-md-link :filter-return za/org-md-link-zola) - (org-gfm-table :override org-md--convert-to-html)))) ; Zola uses CommonMark, so doesn't support Markdown tables - - (dolist (orig-type-new advice) (apply #'advice-add orig-type-new)) - (org-gfm-publish-to-gfm plist filename pub-dir) - (dolist (orig-type-new advice) - (advice-remove (nth 0 orig-type-new) - (nth 2 orig-type-new))))) -#+end_src - -Finally, the list of things we can publish with their respective publishin functions: - -#+begin_src emacs-lisp - (setq org-publish-project-alist - `( - ("org-notes" - :base-directory ,za/org-roam-dir - :base-extension "org" - :publishing-directory ,(concat za/my-website-dir "content/org-roam/") - :publishing-function za/org-gfm-publish-to-gfm-zola - :recursive t - :sitemap-filename "_index.md" - :sitemap-title "Org Roam" - :sitemap-function za/org-roam-sitemap-function - :auto-sitemap t) - - ("org-notes-data" - :base-directory ,(concat za/org-roam-dir "/data") - :base-extension any - :publishing-directory ,(concat za/my-website-dir "static/org-roam-data/") - :recursive t - :publishing-function org-publish-attachment) - - ("org-roam" :components ("org-notes" "org-notes-data")) - )) -#+end_src - -And a function to rsync to my VPS: - -#+begin_src emacs-lisp - (defun za/publish-upload-to-website () - "Upload my website to my VPS" + (defconst za/st-buffer-name "*syncthing*" "Buffer name for the syncthing process.") + (defun za/st () + "Start syncthing" (interactive) - (async-shell-command (format "cd %s && zola build && yes|publish" za/my-website-dir) "*Async Shell publish*")) -#+end_src -*** TODO the path for org-roam export and data export should be configurable, not hard-coded -** calfw -Basically provides a way to show the org agenda as a standard GUI calendar app would. - -#+begin_src emacs-lisp - (use-package calfw - :config - (use-package calfw-org) - (setq cfw:org-overwrite-default-keybinding t) - (setq calendar-week-start-day 1)) -#+end_src - -** vanish: hide parts of the file -#+begin_src emacs-lisp - (use-package vanish - :quelpa (vanish :fetcher github :repo "thezeroalpha/vanish.el" :branch "develop") - :ensure nil) -#+end_src - -Customize the prefix: - -#+begin_src emacs-lisp - (define-key vanish-mode-map (kbd "C-c q h") #'vanish-toggle) -#+end_src -** lean-mode -Specifically for the Lean prover. -I also install company-lean and helm-lean, which are suggested on the [[https://github.com/leanprover/lean-mode][Github page]]. -Then I map company-complete only for lean-mode. - -#+begin_src emacs-lisp - (use-package lean-mode - :config - (use-package company-lean) - :hook - (lean-mode . (lambda () ))) -#+end_src - -#+begin_src emacs-lisp - (defun za/keybinds-lean-mode () - "Function to set lean-mode keybindings, run via lean-mode-hook." - (define-key lean-mode-map (kbd "S-SPC") #'company-complete)) - - (add-hook 'lean-mode-hook #'za/keybinds-lean-mode) -#+end_src - -** magit -#+begin_src emacs-lisp - (use-package magit) -#+end_src - -** vterm -Emacs has a bunch of built-in terminal emulators. -And they all suck. -(OK not really, eshell is alright, but not for interactive terminal programs like newsboat/neomutt) - -Also use emacsclient inside vterm as an editor, because that'll open documents in the existing Emacs session. -And I'm not gonna be a heretic and open Vim inside of Emacs. - -#+begin_src emacs-lisp - (use-package vterm - :hook - (vterm-mode . (lambda () (unless server-process (server-start))))) -#+end_src - -I'll bind a key to start a vterm or switch to the running vterm: - -#+begin_src emacs-lisp - (defun switch-to-vterm () "Switch to a running vterm, or start one and switch to it." - (interactive) - (if (get-buffer vterm-buffer-name) - (switch-to-buffer vterm-buffer-name) - (vterm))) - (za/global-set-key (kbd "C-c t") 'switch-to-vterm) -#+end_src - -** sr-speedbar -Make speed bar show in the current frame. - -#+begin_src emacs-lisp - (use-package sr-speedbar - :config - (setq sr-speedbar-right-side nil)) -#+end_src - -#+begin_src emacs-lisp - (defun za/keybinds-speedbar-mode () - "Function to set speedbar-mode keybindings, run via speedbar-mode-hook." - (define-key speedbar-mode-map (kbd "q") 'sr-speedbar-close)) - - (add-hook 'speedbar-mode-hook #'za/keybinds-speedbar-mode) -#+end_src - -Jump to speedbar. sr-speedbar-exist-p can be void, so I check if it's bound first. -If it's not bound, or if it's false, first open the speedbar. -Then, select it. + (if (get-buffer-process za/st-buffer-name) + (user-error "Syncthing is already running.")) + (async-shell-command "syncthing serve --no-browser" za/st-buffer-name)) -#+begin_src emacs-lisp - (defun za/jump-to-speedbar-or-open () - "Open a speedbar or jump to it if already open." + (defun za/st-kill () + "Stop syncthing" (interactive) - (if (or (not (boundp 'sr-speedbar-exist-p)) - (not (sr-speedbar-exist-p))) - (sr-speedbar-open)) - (sr-speedbar-select-window)) -#+end_src - - - -#+begin_src emacs-lisp - (za/global-set-key (kbd "C-c F") 'za/jump-to-speedbar-or-open) -#+end_src -** expand-region -Expand the selected region semantically. - -#+begin_src emacs-lisp - (use-package expand-region - :bind ("C-=" . er/expand-region)) -#+end_src - -** flycheck -Install flycheck, and enable it by default in certain major modes: - -#+begin_src emacs-lisp - (use-package flycheck - :hook (sh-mode . flycheck-mode)) -#+end_src - -** anki-editor -Some extra keybindings that are not set up by default. -anki-editor doesn't provide a keymap so I have to set one up here: - -#+begin_src emacs-lisp - (use-package anki-editor - :config - (defvar anki-editor-mode-map (make-sparse-keymap)) - (add-to-list 'minor-mode-map-alist (cons 'anki-editor-mode - anki-editor-mode-map)) - - (setq anki-editor-use-math-jax t) - - :hook - (anki-editor-mode . (lambda () - (define-key anki-editor-mode-map (kbd "C-c t") #'org-property-next-allowed-value) - (define-key anki-editor-mode-map (kbd "C-c i") #'anki-editor-insert-note) - (define-key anki-editor-mode-map (kbd "C-c p") #'anki-editor-push-notes) - (define-key anki-editor-mode-map (kbd "C-c c") #'anki-editor-cloze-dwim)))) -#+end_src - -** rainbow-mode -'rainbow-mode' lets you visualise hex colors: - -#+begin_src emacs-lisp - (use-package rainbow-mode) -#+end_src - -** pdf-tools -A better replacement for DocView: - -#+begin_src emacs-lisp - (use-package pdf-tools - :config - (setq-default pdf-annot-default-annotation-properties '((t - (label . "Alex Balgavy")) - (text - (icon . "Note") - (color . "#0088ff")) - (highlight - (color . "yellow")) - (squiggly - (color . "orange")) - (strike-out - (color . "red")) - (underline - (color . "blue")))) - :hook - (pdf-annot-list-mode . pdf-annot-list-follow-minor-mode) - (pdf-annot-edit-contents-minor-mode . org-mode) - (pdf-view-mode . (lambda () (display-line-numbers-mode 0))) - (pdf-view-mode . (lambda () (define-key pdf-isearch-minor-mode-map (kbd "C-s") #'isearch-forward)))) - (pdf-tools-install) -#+end_src - -Save position and jump back: - -#+begin_src emacs-lisp - (define-key pdf-view-mode-map (kbd "C-SPC") - (lambda () (interactive) (message "Position saved") (pdf-view-position-to-register ?x))) - (define-key pdf-view-mode-map (kbd "C-u C-SPC") - (lambda () (interactive) (pdf-view-jump-to-register ?x))) -#+end_src - -*** TODO this clobbers register x. Find a way to not clobber a register - -*** Fix up arrow -The arrow tooltip does not show properly when jumping to a location. -Maybe this is a Mac-only thing. See here: https://github.com/politza/pdf-tools/issues/145 - -This ~:override~ advice fixes it, color is customized via ~tooltip~ face: - -#+begin_src emacs-lisp - (defun za/pdf-util-tooltip-arrow (image-top &optional timeout) - "Fix up `pdf-util-tooltip-arrow`, the original doesn't show the arrow." - (pdf-util-assert-pdf-window) - (when (floatp image-top) - (setq image-top - (round (* image-top (cdr (pdf-view-image-size)))))) - (let* (x-gtk-use-system-tooltips ;allow for display property in tooltip - (dx (+ (or (car (window-margins)) 0) - (car (window-fringes)))) - (dy image-top) - (pos (list dx dy dx (+ dy (* 2 (frame-char-height))))) - (vscroll - (pdf-util-required-vscroll pos)) - (tooltip-frame-parameters - `((border-width . 0) - (internal-border-width . 0) - ,@tooltip-frame-parameters)) - (tooltip-hide-delay (or timeout 3))) - (when vscroll - (image-set-window-vscroll vscroll)) - (setq dy (max 0 (- dy - (cdr (pdf-view-image-offset)) - (window-vscroll nil t) - (frame-char-height)))) - (when (overlay-get (pdf-view-current-overlay) 'before-string) - (let* ((e (window-inside-pixel-edges)) - (xw (pdf-util-with-edges (e) e-width))) - (cl-incf dx (/ (- xw (car (pdf-view-image-size t))) 2)))) - (pdf-util-tooltip-in-window "\u2192" dx dy))) - (advice-add #'pdf-util-tooltip-arrow :override #'za/pdf-util-tooltip-arrow) -#+end_src - -** virtualenvwrapper -Like virtualenvwrapper.sh, but for Emacs. - -#+begin_src emacs-lisp - (use-package virtualenvwrapper - :config - (venv-initialize-interactive-shells) - (venv-initialize-eshell) - (setq venv-location "~/.config/virtualenvs")) -#+end_src - -** org-noter -#+begin_src emacs-lisp - (use-package org-noter) -#+end_src - -Fix disabling of line wrap by no-opping set-notes-scroll: - -#+begin_src emacs-lisp - (defun za/no-op (&rest args)) - (advice-add 'org-noter--set-notes-scroll :override 'za/no-op) -#+end_src - -** hl-todo -I want to highlight TODO keywords in comments: - -#+begin_src emacs-lisp - (use-package hl-todo - :custom-face - (hl-todo ((t (:inherit hl-todo :underline t)))) - :config - (setq hl-todo-keyword-faces - '(("TODO" . "#ff7060") - ("FIXME" . "#caa000"))) - (global-hl-todo-mode t)) + (unless (get-buffer-process za/st-buffer-name) + (user-error "Syncthing is not running.")) + (async-shell-command "syncthing cli operations shutdown")) #+end_src -** undo-tree -Sometimes it's better to look at undo history as a tree: +* Interface +** Theme +Icons required for some parts of the doom theme: #+begin_src emacs-lisp - (use-package undo-tree - :config - (global-undo-tree-mode)) + (use-package all-the-icons) #+end_src -Save undo files into ~/.emacs.d/undo-tree - -#+begin_src emacs-lisp - (let ((undo-tree-dir (concat user-emacs-directory "undo-tree/"))) - (unless (file-directory-p undo-tree-dir) (make-directory undo-tree-dir)) - (setq undo-tree-history-directory-alist `(("." . ,undo-tree-dir)))) -#+end_src +Load Doom Emacs themes: -** ledger #+begin_src emacs-lisp - (use-package ledger-mode - :mode ("\\.ledger\\'") + (use-package doom-themes :config - (setq ledger-clear-whole-transactions t - ledger-reconcile-default-commodity "eur")) -#+end_src - -org-capture lets me add transactions from anywhere in Emacs: - -#+begin_src emacs-lisp - (add-to-list 'org-capture-templates `("$" "Ledger entry" plain - (file ,za/ledger-file) - "%(ledger-read-date \"Date\") %^{Payee} - %^{Account} %^{Amount} - Assets:ABN Checking%? - " - :empty-lines-before 1)) - + ;; Global settings (defaults) + (setq doom-themes-enable-bold t ; if nil, bold is universally disabled + doom-themes-enable-italic t) ; if nil, italics is universally disabled -#+end_src + ;; Enable flashing mode-line on errors + (doom-themes-visual-bell-config) -Budget throws an error when there's multiple commodities involved. -See discussion here: https://github.com/ledger/ledger/issues/1450#issuecomment-390067165 -#+begin_src emacs-lisp - (defconst za/ledger-budget-fix-string - "-X eur -F '%(justify(scrub(get_at(display_total, 0)), 20, -1, true, false)) %(justify(get_at(display_total, 1) ? -scrub(get_at(display_total, 1)) : 0.0, 20, 20 + 1 + 20, true, false)) %(justify(get_at(display_total, 1) ? (get_at(display_total, 0) ? -(scrub(get_at(display_total, 1) + get_at(display_total, 0))) : -(scrub(get_at(display_total, 1)))) : -(scrub(get_at(display_total, 0))), 20, 20 + 1 + 20 + 1 + 20, true, false))%(get_at(display_total, 1) and (abs(quantity(scrub(get_at(display_total, 0))) / quantity(scrub(get_at(display_total, 1)))) >= 1) ? \" \" : \" \")%(justify((get_at(display_total, 1) ? (100% * (get_at(display_total, 0) ? scrub(get_at(display_total, 0)) : 0.0)) / -scrub(get_at(display_total, 1)) : \"na\"), 5, -1, true, false)) %(!options.flat ? depth_spacer : \"\")%-(partial_account(options.flat))\n%/%$2 %$3 %$4 %$6\n%/%(prepend_width ? \" \" * int(prepend_width) : \"\") ---------------- ---------------- ---------------- -----\n'" - "Append this to a ledger budget to fix errors with multiple commodities.") + ;; Corrects (and improves) org-mode's native fontification. + (doom-themes-org-config)) #+end_src -Custom reports: +Define the themes I want: #+begin_src emacs-lisp - (custom-set-variables - '(ledger-reports - '(("budget-last-month" "%(binary) -f %(ledger-file) --start-of-week=1 --period \"last month\" budget ^expenses") - ("budget-this-month" "%(binary) -f %(ledger-file) --start-of-week=1 --period \"this month\" budget ^expenses") - ("expenses-this-month-vs-budget" "%(binary) -f %(ledger-file) --start-of-week=1 --period \"this month\" --period-sort \"(amount)\" bal ^expenses --budget") - ("expenses-last-month-vs-budget" "%(binary) -f %(ledger-file) --start-of-week=1 --period \"last month\" --period-sort \"(amount)\" bal ^expenses --budget") - ("expenses-this-month" "%(binary) -f %(ledger-file) --start-of-week=1 --period \"this month\" --period-sort \"(amount)\" bal ^income ^expenses -X eur") - ("expenses-last-month" "%(binary) -f %(ledger-file) --start-of-week=1 --period \"last month\" --period-sort \"(amount)\" bal ^expenses -X eur") - ("expenses-this-month" "%(binary) -f %(ledger-file) --start-of-week=1 --period \"this month\" --period-sort \"(amount)\" bal ^expenses -X eur") - ("expenses-vs-income-this-month" "%(binary) -f %(ledger-file) --start-of-week=1 --effective --period \"this month\" --period-sort \"(amount)\" bal ^income ^expenses -X eur") - ("expenses-vs-income-last-month" "%(binary) -f %(ledger-file) --start-of-week=1 --effective --period \"last month\" --period-sort \"(amount)\" bal ^expenses ^income -X eur") - ("bal-assets-czk" "%(binary) -f %(ledger-file) --start-of-week=1 bal Assets Liabilities -X czk") - ("bal-assets" "%(binary) -f %(ledger-file) --start-of-week=1 bal Assets Liabilities") - ("bal" "%(binary) -f %(ledger-file) --start-of-week=1 bal -B") - ("bal-assets-eur" "%(binary) -f %(ledger-file) --start-of-week=1 bal Assets Liabilities -X eur") - ("monthly-balance-abn-checking" "%(binary) -f %(ledger-file) --start-of-week=1 --effective reg --monthly 'Assets:ABN Checking'") - ("monthly-expenses" "%(binary) -f %(ledger-file) --monthly register ^expenses --collapse -X eur") - ("reg" "%(binary) -f %(ledger-file) --start-of-week=1 reg") - ("payee" "%(binary) -f %(ledger-file) --start-of-week=1 reg @%(payee)") - ("account" "%(binary) -f %(ledger-file) --start-of-week=1 reg %(account)")))) -#+end_src -** osm -#+begin_src emacs-lisp - (use-package osm - :bind (("C-c M h" . osm-home) - ("C-c M s" . osm-search) - ("C-c M v" . osm-server) - ("C-c M t" . osm-goto) - ("C-c M x" . osm-gpx-show) - ("C-c M j" . osm-bookmark-jump)) + (defconst za/dark-theme-name 'doom-one "A symbol representing the name of the dark theme I use.") + (defconst za/light-theme-name 'jokull "A symbol representing the name of the light theme I use.") + ;; I used to use doom-acario-light before writing my own theme - :custom - ;; Take a look at the customization group `osm' for more options. - (osm-server 'default) ;; Configure the tile server - (osm-copyright nil) ;; Display the copyright information + (defun za/dark-theme () + "Switch to dark theme" + (interactive) + (mapc #'disable-theme custom-enabled-themes) + (load-theme za/dark-theme-name t) + (add-hook 'pdf-view-mode-hook #'pdf-view-midnight-minor-mode)) - :init - ;; Load Org link support - (with-eval-after-load 'org - (require 'osm-ol))) -#+end_src -** ess -#+begin_src emacs-lisp - (use-package ess) + (defun za/light-theme () + "Switch to light theme" + (interactive) + (mapc #'disable-theme custom-enabled-themes) + (load-theme za/light-theme-name t) + (remove-hook 'pdf-view-mode-hook #'pdf-view-midnight-minor-mode)) #+end_src -** eglot -A good LSP plugin. + +Change theme depending on the current system theme. +The way I check for dark mode is defined in 'dark-mode-p'; currently I use the presence of the ~/.config/dark-theme file to indicate when dark theme is set. +I quote the call to ~file-exists-p~ because I want to evaluate it on-demand, not immediately. +A function ending in '-p' is a predicate, i.e. returns true or false. +If calling a function that's in a variable, you have to use 'funcall'. +To evaluate a quoted form, use 'eval'. #+begin_src emacs-lisp - (use-package eglot) + (defun za/auto-select-theme (&rest _) + "Automatically select dark/light theme based on presence of ~/.config/dark-theme" + (let ((dark-mode-p '(file-exists-p "~/.config/dark-theme"))) + (if (eval dark-mode-p) + (za/dark-theme) + (za/light-theme)))) + + (za/auto-select-theme) #+end_src -** crdt -Collaborative editing in Emacs: +** Font +I want Menlo, size 14: #+begin_src emacs-lisp - (use-package crdt) + (add-to-list 'default-frame-alist '(font . "Menlo-14")) #+end_src -** git gutter -General git gutter: +** Cursor +The default box cursor isn't really accurate, because the cursor is actually between letters, not on a letter. +So, I want a bar instead of a box: #+begin_src emacs-lisp - (use-package git-gutter - :config - (global-git-gutter-mode 1)) + (setq-default cursor-type '(bar . 4) + cursor-in-non-selected-windows 'hollow) #+end_src -Some bindings: +(I use ~setq-default~ here because cursor-type is automatically buffer-local when it's set) -#+begin_src emacs-lisp - (za/global-set-key (kbd "C-c d n") #'git-gutter:next-hunk) - (za/global-set-key (kbd "C-c d p") #'git-gutter:previous-hunk) +** Matching parentheses +Don't add a delay to show matching parenthesis. +Must come before show-paren-mode enable. +#+begin_src emacs-lisp + (setq show-paren-delay 0) #+end_src -** keycast -In case I want to show what keys I'm pressing. +Show matching parentheses: #+begin_src emacs-lisp -(use-package keycast) + (show-paren-mode t) #+end_src - -** helpful -An alternative to the built-in Emacs help that provides much more contextual information. +** Line numbers +Relative line numbers: #+begin_src emacs-lisp - (use-package helpful) + (setq display-line-numbers-type 'relative) + (global-display-line-numbers-mode) #+end_src -I use counsel, so I use the keybindings in [[*counsel + ivy + swiper]]. -I just augment the functions counsel uses: +Function to hide them: #+begin_src emacs-lisp - (setq counsel-describe-symbol-function #'helpful-symbol - counsel-describe-function-function #'helpful-callable - counsel-describe-variable-function #'helpful-variable) + (defun za/hide-line-numbers () + "Hide line numbers" + (display-line-numbers-mode 0)) #+end_src - -Also, counsel doesn't provide some keybindings that I can get from helpful: +Don't display them in specific modes. For each of the modes in +'mode-hooks', add a function to hide line numbers when the mode +activates (which triggers the 'mode'-hook). #+begin_src emacs-lisp - (za/global-set-key (kbd "C-h k") #'helpful-key) - (za/global-set-key (kbd "C-h C") #'helpful-command) + (let ((mode-hooks '(doc-view-mode-hook vterm-mode-hook mpc-status-mode-hook mpc-tagbrowser-mode-hook))) + (mapc + (lambda (mode-name) + (add-hook mode-name #'za/hide-line-numbers)) + mode-hooks)) #+end_src - -Then, a way to jump forward and backward in the window: +** Modeline +I want to show the time and date in the modeline: #+begin_src emacs-lisp - (defvar za/helpful-buffer-ring-size 20 - "How many buffers are stored for use with `helpful-next'.") - - (defvar za/helpful--buffer-ring (make-ring za/helpful-buffer-ring-size) - "Ring that stores the current Helpful buffer history.") - - (defun za/helpful--buffer-index (&optional buffer) - "If BUFFER is a Helpful buffer, return it’s index in the buffer ring." - (let ((buf (or buffer (current-buffer)))) - (and (eq (buffer-local-value 'major-mode buf) 'helpful-mode) - (seq-position (ring-elements za/helpful--buffer-ring) buf #'eq)))) - - (defun za/helpful--new-buffer-a (help-buf) - "Update the buffer ring according to the current buffer and HELP-BUF." - :filter-return #'helpful--buffer - (let ((buf-ring za/helpful--buffer-ring)) - (let ((newer-buffers (or (za/helpful--buffer-index) 0))) - (dotimes (_ newer-buffers) (ring-remove buf-ring 0))) - (when (/= (ring-size buf-ring) za/helpful-buffer-ring-size) - (ring-resize buf-ring za/helpful-buffer-ring-size)) - (ring-insert buf-ring help-buf))) - - (advice-add #'helpful--buffer :filter-return #'za/helpful--new-buffer-a) - - (defun za/helpful--next (&optional buffer) - "Return the next live Helpful buffer relative to BUFFER." - (let ((buf-ring za/helpful--buffer-ring) - (index (or (za/helpful--buffer-index buffer) -1))) - (cl-block nil - (while (> index 0) - (cl-decf index) - (let ((buf (ring-ref buf-ring index))) - (if (buffer-live-p buf) (cl-return buf))) - (ring-remove buf-ring index))))) - - - (defun za/helpful--previous (&optional buffer) - "Return the previous live Helpful buffer relative to BUFFER." - (let ((buf-ring za/helpful--buffer-ring) - (index (1+ (or (za/helpful--buffer-index buffer) -1)))) - (cl-block nil - (while (< index (ring-length buf-ring)) - (let ((buf (ring-ref buf-ring index))) - (if (buffer-live-p buf) (cl-return buf))) - (ring-remove buf-ring index))))) - - (defun za/helpful-next () - "Go to the next Helpful buffer." - (interactive) - (when-let (buf (za/helpful--next)) - (funcall helpful-switch-buffer-function buf))) - - (defun za/helpful-previous () - "Go to the previous Helpful buffer." - (interactive) - (when-let (buf (za/helpful--previous)) - (funcall helpful-switch-buffer-function buf))) + (setq display-time-day-and-date t ; also the date + display-time-default-load-average nil ; don't show load average + display-time-format "%I:%M%p %e %b (%a)") ; "HR:MIN(AM/PM) day-of-month Month (Day)" + (display-time-mode 1) ; enable time mode #+end_src -** ace-window -Window switching with ~other-window~ sucks when I have more than 2 windows open. Too much cognitive load. -This lets me select a window to jump to using a single key, sort of like ~avy~. +And to set the modeline format: #+begin_src emacs-lisp - (use-package ace-window) - (za/global-set-key (kbd "M-o") #'ace-window) + (setq-default mode-line-format '("%e" mode-line-front-space mode-line-mule-info mode-line-client mode-line-modified mode-line-remote mode-line-frame-identification mode-line-buffer-identification " " mode-line-position + (vc-mode vc-mode) + " " mode-line-modes mode-line-misc-info mode-line-end-spaces)) #+end_src -I prefer using home-row keys instead of numbers: +I want to hide certain modes from the modeline, they're always on: #+begin_src emacs-lisp - (setq aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l)) + (use-package diminish + :config + (let ((modes-to-hide '(ivy-mode counsel-mode which-key-mode hl-todo-mode undo-tree-mode ivy-posframe-mode git-gutter-mode))) + (mapc #'diminish modes-to-hide)) + (diminish 'view-mode " 👓")) #+end_src - -Also, I want something a little more contrasty: - +** Transparent title bar #+begin_src emacs-lisp - (custom-set-faces - '(aw-leading-char-face - ((t (:inherit font-lock-keyword-face :height 2.0))))) + (add-to-list 'default-frame-alist '(ns-transparent-titlebar . t)) #+end_src +** Tab bar +Only show tab bar if there's more than 1 tab: -** decide-mode for dice rolling #+begin_src emacs-lisp - (use-package decide - :quelpa (decide :repo "lifelike/decide-mode" :fetcher github) - :ensure nil) + (setq tab-bar-show 1) #+end_src +** Buffer displaying -I want a custom keybinding to easily toggle: +So, this is a bit hard to grok. But basically the alist contains a +regular expression to match a buffer name, then a list of functions to +use in order for displaying the list, and then options for those functions (each of which is an alist). #+begin_src emacs-lisp - (za/global-set-key (kbd "C-c q ?") #'decide-mode) + (setq + ;; Maximum number of side-windows to create on (left top right bottom) + window-sides-slots '(0 ;; left + 1 ;; top + 3 ;; right + 1 ) ;; bottom + + display-buffer-alist `( + ;; Right side + (,(rx (or "*Help*" (seq "*helpful " (* anything) "*"))) + (display-buffer-reuse-window display-buffer-in-side-window) + (side . right) + (slot . -1) + (inhibit-same-window . t)) + (,(rx "*Async Shell " (* anything) "*") + (display-buffer-reuse-window display-buffer-in-side-window) + (side . right) + (slot . 0) + (inhibit-same-window . t)) + (,(rx "magit-process: " (* anything)) + (display-buffer-reuse-window display-buffer-in-side-window) + (side . right) + (slot . 0) + (inhibit-same-window . t)) + + ;; Top side + (,(rx "*Info*") + (display-buffer-reuse-window display-buffer-in-side-window) + (side . top) + (slot . 0)) + (,(rx "*Man " (* anything) "*") + (display-buffer-reuse-window display-buffer-in-side-window) + (side . top) + (slot . 0)) + + ;; Bottom + (,(rx "*Flycheck errors*") + (display-buffer-reuse-window display-buffer-in-side-window) + (side . bottom) + (slot . 0)))) #+end_src -** try: try out different packages +And a way to toggle those side windows: + #+begin_src emacs-lisp - (use-package try) + (bind-key "C-c W" #'window-toggle-side-windows) #+end_src -** dumb-jump -"jump to definition" package, minimal configuration with no stored indexes. -Uses The Silver Searcher ag, ripgrep rg, or grep to find potential definitions of a function or variable under point. +** Eldoc +When editing Elisp and other supported major-modes, Eldoc will display useful information about the construct at point in the echo area. #+begin_src emacs-lisp - (use-package dumb-jump) + (global-eldoc-mode 1) #+end_src -Enable xref backend: +** Pulse line +When you switch windows, Emacs can flash the cursor briefly to guide your eyes; I like that. +Set some options for pulsing: #+begin_src emacs-lisp - (add-hook 'xref-backend-functions #'dumb-jump-xref-activate) - (setq xref-show-definitions-function #'xref-show-definitions-completing-read) + (setq pulse-iterations 10) + (setq pulse-delay 0.05) #+end_src -** (disabled) command-log-mode -Simple real-time logger of commands. +Define the pulse function: -#+begin_src emacs-lisp :tangle no - (use-package command-log-mode) +#+begin_src emacs-lisp + (defun pulse-line (&rest _) + "Pulse the current line." + (pulse-momentary-highlight-one-line (point))) #+end_src -** package-lint -Linter for the metadata in Emacs Lisp files which are intended to be packages. +Run it in certain cases: scrolling up/down, recentering, switching windows. +'dolist' binds 'command' to each value in the list in turn, and runs the body. +'advice-add' makes the pulse-line function run after 'command'. #+begin_src emacs-lisp - (use-package package-lint) - (use-package flycheck-package) - (eval-after-load 'flycheck - '(flycheck-package-setup)) + (dolist (command '(scroll-up-command scroll-down-command recenter-top-bottom other-window)) + (advice-add command :after #'pulse-line)) #+end_src -** prism -Prism changes the color of text depending on their depth. Makes it easier to see where something is at a glance. +And set the pulse color: #+begin_src emacs-lisp -(use-package prism) + (custom-set-faces '(pulse-highlight-start-face ((t (:background "CadetBlue2"))))) #+end_src -** olivetti: distraction-free writing + +** Enable all commands +By default, Emacs disables some commands. +I want to have these enabled so I don't get a prompt whenever I try to use a disabled command. + #+begin_src emacs-lisp - (use-package olivetti) + (setq disabled-command-function nil) #+end_src -** nov.el: EPUB support +** More extensive apropos #+begin_src emacs-lisp - (use-package nov) - (add-to-list 'auto-mode-alist '("\\.epub\\'" . nov-mode)) + (setq apropos-do-all t) #+end_src -* Interface -** Start debugger on error +** Enable recursive minibuffers #+begin_src emacs-lisp - ;; (toggle-debug-on-error t) + (setq enable-recursive-minibuffers t + minibuffer-depth-indicate-mode t) #+end_src - -** Messages -Hide some messages I don't need, and add a list of recent files. +** View webp and other formats +Emacs handles common image formats internally, but for stuff like webp, you need an external converter: #+begin_src emacs-lisp - (recentf-mode) - (setq inhibit-startup-message t - initial-major-mode #'org-mode - initial-scratch-message - (concat "Welcome to Emacs\n\n" - "Recent:\n" - (mapconcat - (lambda (x) (format "- [[%s]]" x)) recentf-list "\n") - "\n\nELISP Evaluation area:\n#+begin_src emacs-lisp\n\n#+end_src")) - + (setq image-use-external-converter t) #+end_src -*** TODO maybe look into fancy-startup-text and fancy-splash-image -** Appearance -*** Cursor line -Highlight the current line: +You also need imagemagick installed. +** Repeat mode: easy repeating of commands #+begin_src emacs-lisp - (global-hl-line-mode) + (repeat-mode 1) #+end_src -*** Matching parentheses -Don't add a delay to show matching parenthesis. -Must come before show-paren-mode enable. + +** Messages +Hide some messages I don't need, and add a list of recent files. #+begin_src emacs-lisp - (setq show-paren-delay 0) + (recentf-mode) + (setq inhibit-startup-message t + initial-major-mode #'org-mode + initial-scratch-message + (concat "Welcome to Emacs\n\n" + "Recent:\n" + (mapconcat + (lambda (x) (format "- [[%s]]" x)) recentf-list "\n") + "\n\nELISP Evaluation area:\n#+begin_src emacs-lisp\n\n#+end_src")) + #+end_src -Show matching parentheses: +* General packages +** which-key +Minor mode for Emacs that displays the key bindings following your currently entered incomplete command (a prefix) in a popup. -#+begin_src emacs-lisp - (show-paren-mode t) +#+BEGIN_SRC emacs-lisp + (use-package which-key + :config + (which-key-mode)) #+end_src -*** Cursor -The default box cursor isn't really accurate, because the cursor is actually between letters, not on a letter. -So, I want a bar instead of a box: +** counsel + ivy + swiper + prescient +Better incremental completion and selection narrowing. +And a bunch more. +Generally makes for nicer interactivity, like ido mode on steroids. +Switched to this from Helm, it's more lightweight. +*** ivy: generic completion mechanism #+begin_src emacs-lisp - (setq-default cursor-type '(bar . 4) - cursor-in-non-selected-windows 'hollow) -#+end_src + (use-package ivy + :custom + (ivy-use-virtual-buffers t "extend searching to bookmarks") + (ivy-height 20 "set height of the ivy window") + (ivy-count-format "(%d/%d) " "count format, from the ivy help page") + (ivy-display-style 'fancy) + (ivy-format-function 'ivy-format-function-line) + (ivy-use-selectable-prompt t "to let me select exactly what I'm typing as a candidate") + + :bind (("C-x b" . ivy-switch-buffer) + ("C-c v" . ivy-push-view) + ("C-c V" . ivy-pop-view) + + ;; accidentally pressing shift-space deletes input, because + ;; by default, shift-space is bound to + ;; ~ivy-restrict-to-matches~ in the ivy minibuffer. + :map ivy-minibuffer-map + ("S-SPC" . (lambda () (interactive) (insert ?\s))) + ("<backtab>" . ivy-restrict-to-matches)) + :config + (ivy-add-actions + 'counsel-dired + '(("f" (lambda (dir) (counsel-fzf nil dir)) "Fzf in directory") + ("g" (lambda (dir) (counsel-ag nil dir)) "Ag in directory"))) + (ivy-add-actions + 'dired + '(("f" (lambda (dir) (ivy-exit-with-action (counsel-fzf nil dir))) "Fzf in directory") + ("g" (lambda (dir) (ivy-exit-with-action (counsel-ag nil dir))) "Ag in directory"))) + (ivy-mode) -(I use ~setq-default~ here because cursor-type is automatically buffer-local when it's set) + (defun edit-script () + "Edit a file in ~/.scripts/" + (interactive) + (let ((input (ivy--input))) + (ivy-quit-and-run (counsel-file-jump nil "~/.scripts/")))) -*** Line numbers -Relative line numbers: + (defun edit-config () + "Edit a file in ~/.dotfiles/" + (interactive) + (let ((input (ivy--input))) + (ivy-quit-and-run (counsel-file-jump nil "~/.dotfiles/"))))) +#+end_src +*** counsel: collection of common Emacs commands enhanced using ivy #+begin_src emacs-lisp - (setq display-line-numbers-type 'relative) - (global-display-line-numbers-mode) + (use-package counsel + :demand + :config + (counsel-mode) + :bind (("M-x" . counsel-M-x) + ("C-x C-f" . counsel-find-file) + ("M-y" . counsel-yank-pop) + ("C-c c" . counsel-compile) + ("M-s g" . counsel-ag) + ("M-s f" . counsel-fzf) + ("C-c b" . counsel-bookmark) + ("C-c p" . counsel-recentf) + ("C-c o" . counsel-outline) + ("C-h f" . counsel-describe-function) + ("C-h v" . counsel-describe-variable) + ("C-h o" . counsel-describe-symbol))) +#+end_src +*** swiper: search enhanced using ivy +#+begin_src emacs-lisp + (use-package swiper + :bind (("C-s" . swiper-isearch) + ("C-r" . swiper-isearch-backward))) +#+end_src +*** prescient: scoring system for M-x +#+begin_src emacs-lisp + (use-package prescient + :config (prescient-persist-mode)) + + (use-package ivy-prescient + :after counsel + :custom (ivy-prescient-retain-classic-highlighting t) + :config (ivy-prescient-mode)) #+end_src -Function to hide them: +*** ivy-posframe: ivy in a popup +I like having ivy in a popup. +Problem: posframe does not work if emacs is too old and on macos. +See here: https://github.com/tumashu/posframe/issues/30 +On Mac, ~brew install --HEAD emacs~ doesn't work either. +Solution: ~brew tap daviderestivo/emacs-head && brew install emacs-head@28 --with-cocoa~ #+begin_src emacs-lisp - (defun za/hide-line-numbers () - "Hide line numbers" - (display-line-numbers-mode 0)) + (if (and (version< emacs-version "28") (equal system-type 'darwin)) + (message "ivy-posframe won't work properly, run `brew install daviderestivo/emacs-head/emacs-head@28 --with-cocoa`") + (use-package ivy-posframe + :custom + (ivy-posframe-display-functions-alist '((t . ivy-posframe-display-at-frame-center))) + (ivy-posframe-parameters + '((left-fringe . 8) + (right-fringe . 8))) + (ivy-posframe-border-width 3) + :custom-face + (ivy-posframe-border ((t (:inherit mode-line-inactive)))) + :config + (ivy-posframe-mode 1))) #+end_src -Don't display them in specific modes. For each of the modes in -'mode-hooks', add a function to hide line numbers when the mode -activates (which triggers the 'mode'-hook). +** company: completion mechanism #+begin_src emacs-lisp - (let ((mode-hooks '(doc-view-mode-hook vterm-mode-hook mpc-status-mode-hook mpc-tagbrowser-mode-hook))) - (mapc - (lambda (mode-name) - (add-hook mode-name #'za/hide-line-numbers)) - mode-hooks)) + (use-package company) #+end_src -*** Modeline -I want to show the time and date in the modeline: +** wgrep: writable grep #+begin_src emacs-lisp - (setq display-time-day-and-date t ; also the date - display-time-default-load-average nil ; don't show load average - display-time-format "%I:%M%p %e %b (%a)") ; "HR:MIN(AM/PM) day-of-month Month (Day)" - (display-time-mode 1) ; enable time mode + (use-package wgrep) #+end_src - -And to set the modeline format: +** avy: jump to any position +This lets me jump to any position in Emacs rather quickly, sometimes it's useful. +~avy-goto-char-timer~ lets me type a part of the text before avy kicks in. #+begin_src emacs-lisp - (setq-default mode-line-format '("%e" mode-line-front-space mode-line-mule-info mode-line-client mode-line-modified mode-line-remote mode-line-frame-identification mode-line-buffer-identification " " mode-line-position - (vc-mode vc-mode) - " " mode-line-modes mode-line-misc-info mode-line-end-spaces)) + (use-package avy + :bind + (("C-:" . avy-goto-char-timer))) #+end_src -I want to hide certain modes from the modeline, they're always on: +** DISABLED calfw: graphical calendar +Basically provides a way to show the org agenda as a standard GUI calendar app would. -#+begin_src emacs-lisp - (use-package diminish +#+begin_src emacs-lisp :tangle no + (use-package calfw :config - (let ((modes-to-hide '(ivy-mode counsel-mode which-key-mode hl-todo-mode undo-tree-mode ivy-posframe-mode git-gutter-mode))) - (mapc #'diminish modes-to-hide)) - (diminish 'view-mode " 👓")) + (use-package calfw-org) + :custom + (cfw:org-overwrite-default-keybinding t) + (calendar-week-start-day 1)) #+end_src -*** Transparent title bar + +** vanish: hide parts of the file #+begin_src emacs-lisp - (add-to-list 'default-frame-alist '(ns-transparent-titlebar . t)) + (use-package vanish + :quelpa (vanish :fetcher github :repo "thezeroalpha/vanish.el" :branch "develop") + :ensure nil + :bind (:map vanish-mode-map + ("C-c q h" . vanish-toggle))) #+end_src -*** Tab bar -Only show tab bar if there's more than 1 tab: - +** magit #+begin_src emacs-lisp - (setq tab-bar-show 1) + (use-package magit) #+end_src +** vterm +Emacs has a bunch of built-in terminal emulators. +And they all suck. +(OK not really, eshell is alright, but not for interactive terminal programs like newsboat/neomutt) -** Buffer displaying - -So, this is a bit hard to grok. But basically the alist contains a -regular expression to match a buffer name, then a list of functions to -use in order for displaying the list, and then options for those functions (each of which is an alist). +Also use emacsclient inside vterm as an editor, because that'll open documents in the existing Emacs session. +And I'm not gonna be a heretic and open Vim inside of Emacs. #+begin_src emacs-lisp - (setq - ;; Maximum number of side-windows to create on (left top right bottom) - window-sides-slots '(0 ;; left - 1 ;; top - 3 ;; right - 1 ) ;; bottom - - display-buffer-alist `( - ;; Right side - (,(rx (or "*Help*" (seq "*helpful " (* anything) "*"))) - (display-buffer-reuse-window display-buffer-in-side-window) - (side . right) - (slot . -1) - (inhibit-same-window . t)) - (,(rx "*Async Shell " (* anything) "*") - (display-buffer-reuse-window display-buffer-in-side-window) - (side . right) - (slot . 0) - (inhibit-same-window . t)) - (,(rx "magit-process: " (* anything)) - (display-buffer-reuse-window display-buffer-in-side-window) - (side . right) - (slot . 0) - (inhibit-same-window . t)) + (use-package vterm + :hook + (vterm-mode . (lambda () (unless server-process (server-start)))) + :bind (("C-c t" . switch-to-vterm)) + :config + (defun switch-to-vterm () + "Switch to a running vterm, or start one and switch to it." + (interactive) + (if (get-buffer vterm-buffer-name) + (switch-to-buffer vterm-buffer-name) + (vterm)))) +#+end_src +** sr-speedbar +Make speed bar show in the current frame. - ;; Top side - (,(rx "*Info*") - (display-buffer-reuse-window display-buffer-in-side-window) - (side . top) - (slot . 0)) - (,(rx "*Man " (* anything) "*") - (display-buffer-reuse-window display-buffer-in-side-window) - (side . top) - (slot . 0)) +#+begin_src emacs-lisp + (use-package sr-speedbar + :bind (("C-c F" . za/jump-to-speedbar-or-open) + :map speedbar-mode-map + ("q" . sr-speedbar-close)) + :custom + (sr-speedbar-right-side nil) - ;; Bottom - (,(rx "*Flycheck errors*") - (display-buffer-reuse-window display-buffer-in-side-window) - (side . bottom) - (slot . 0)))) + :config + (defun za/jump-to-speedbar-or-open () + "Open a speedbar or jump to it if already open." + (interactive) + (if (or (not (boundp 'sr-speedbar-exist-p)) + (not (sr-speedbar-exist-p))) + (sr-speedbar-open)) + (sr-speedbar-select-window))) #+end_src - -And a way to toggle those side windows: +** expand-region +Expand the selected region semantically. #+begin_src emacs-lisp - (za/global-set-key (kbd "C-c W") #'window-toggle-side-windows) + (use-package expand-region + :bind ("C-=" . er/expand-region)) #+end_src -** Eldoc -When editing Elisp and other supported major-modes, Eldoc will display useful information about the construct at point in the echo area. +** flycheck +Install flycheck: #+begin_src emacs-lisp - (global-eldoc-mode 1) + (use-package flycheck) #+end_src - -* Editor -** Overwrite selection on typing -Normally, when I select something and start typing, Emacs clears the selection, i.e. it deselects and inserts text after the cursor. -I want to replace the selection. +** rainbow-mode: visualise hex colors +'rainbow-mode' lets you visualise hex colors: #+begin_src emacs-lisp - (delete-selection-mode t) + (use-package rainbow-mode) #+end_src - -** Strip trailing whitespace -You can show trailing whitespace by setting show-trailing-whitespace to 't'. -But I want to automatically strip trailing whitespace. -Luckily there's already a function for that, I just need to call it in a hook: +** hl-todo: highlight TODO keywords +I want to highlight TODO keywords in comments: #+begin_src emacs-lisp - (add-hook 'before-save-hook #'delete-trailing-whitespace) + (use-package hl-todo + :custom-face + (hl-todo ((t (:inherit hl-todo :underline t)))) + :custom + (hl-todo-keyword-faces '(("TODO" . "#ff7060") + ("FIXME" . "#caa000"))) + :config + (global-hl-todo-mode t)) #+end_src -** Formatting & indentation - -Show a tab as 8 spaces: +** undo-tree +Sometimes it's better to look at undo history as a tree: #+begin_src emacs-lisp - (setq-default tab-width 8) + (use-package undo-tree + :custom + (undo-tree-history-directory-alist + (progn (let ((undo-tree-dir (concat user-emacs-directory "undo-tree/"))) + (unless (file-directory-p undo-tree-dir) (make-directory undo-tree-dir)) + `(("." . ,undo-tree-dir))))) + + :config + (global-undo-tree-mode)) #+end_src -Never insert tabs with indentation by default: +*** TODO undo tree dir should be configurable +** eglot +A good LSP plugin. #+begin_src emacs-lisp - (setq-default indent-tabs-mode nil) + (use-package eglot) #+end_src - -Allow switching between the two easily: +** crdt +Collaborative editing in Emacs: #+begin_src emacs-lisp - (defun indent-tabs () - (interactive) - (setq indent-tabs-mode t)) - (defun indent-spaces () - (interactive) - (setq indent-tabs-mode nil)) + (use-package crdt) #+end_src - -Indentation for various modes: +** git gutter +General git gutter: #+begin_src emacs-lisp - (setq-default sh-basic-offset 2 - c-basic-offset 4) + (use-package git-gutter + :bind (("C-c d n" . git-gutter:next-hunk) + ("C-c d p" . git-gutter:previous-hunk)) + :config + (global-git-gutter-mode 1)) #+end_src - -** Wrapping - -A function to toggle wrapping: +** keycast +In case I want to show what keys I'm pressing. #+begin_src emacs-lisp - (defvar-local za/wrapping nil "Wrapping changes per buffer.") - - (defun za/toggle-wrap (&optional enable) - "Toggle line wrapping settings. With ENABLE a positive number, enable wrapping. If ENABLE is negative or zero, disable wrapping." - (interactive "P") ; prefix arg in raw form + (use-package keycast) +#+end_src +** ace-window: better window switching +Window switching with ~other-window~ sucks when I have more than 2 windows open. Too much cognitive load. +This lets me select a window to jump to using a single key, sort of like ~avy~. - ;; If an argument is provided, prefix or otherwise - (if enable - (let ((enable (cond ((numberp enable) - enable) - ((booleanp enable) - (if enable 1 0)) - ((or (listp enable) (string= "-" enable)) - (prefix-numeric-value enable))))) - ;; If zero or negative, we want to disable wrapping, so pretend it's currently enabled. - ;; And vice versa. - (cond ((<= enable 0) (setq za/wrapping t)) - ((> enable 0) (setq za/wrapping nil))))) +#+begin_src emacs-lisp + (use-package ace-window + :custom + (aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l) "I prefer using home-row keys instead of numbers") + :custom-face + ;; I want something a little more contrasty + (aw-leading-char-face ((t (:inherit font-lock-keyword-face :height 2.0)))) - (let ((disable-wrapping (lambda () - (visual-line-mode -1) - (toggle-truncate-lines t))) - (enable-wrapping (lambda () - (toggle-truncate-lines -1) - (visual-line-mode)))) + :bind ("M-o" . ace-window)) +#+end_src +** decide-mode for dice rolling +#+begin_src emacs-lisp + (use-package decide + :quelpa (decide :repo "lifelike/decide-mode" :fetcher github) + :ensure nil) +#+end_src - ;; If za/wrapping is not locally set, infer its values from the enabled modes - (unless (boundp 'za/wrapping) - (setq za/wrapping (and visual-line-mode - (not truncate-lines)))) +I want a custom keybinding to easily toggle: - ;; Toggle wrapping based on current value - (cond (za/wrapping - (funcall disable-wrapping) - (setq za/wrapping nil) - (message "Wrapping disabled.")) - (t - (funcall enable-wrapping) - (setq za/wrapping t) - (message "Wrapping enabled."))))) +#+begin_src emacs-lisp + (bind-key "C-c q ?" #'decide-mode) +#+end_src +** try: try out different packages +#+begin_src emacs-lisp + (use-package try) +#+end_src +** dumb-jump +"jump to definition" package, minimal configuration with no stored indexes. +Uses The Silver Searcher ag, ripgrep rg, or grep to find potential definitions of a function or variable under point. + +#+begin_src emacs-lisp + (use-package dumb-jump) #+end_src -And a keybinding to toggle wrapping: +Enable xref backend: #+begin_src emacs-lisp - (za/global-set-key (kbd "C-c q w") #'za/toggle-wrap) + (add-hook 'xref-backend-functions #'dumb-jump-xref-activate) + (setq xref-show-definitions-function #'xref-show-definitions-completing-read) #+end_src +** DISABLED command-log-mode +Simple real-time logger of commands. -I want to wrap text at window boundary for some modes: +#+begin_src emacs-lisp :tangle no + (use-package command-log-mode) +#+end_src +** package-lint +Linter for the metadata in Emacs Lisp files which are intended to be packages. #+begin_src emacs-lisp - (defun za/settings-help-mode () - "Help mode settings" - (za/toggle-wrap t)) - (add-hook 'help-mode-hook #'za/settings-help-mode) + (use-package package-lint) + (use-package flycheck-package) + (eval-after-load 'flycheck + '(flycheck-package-setup)) #+end_src +** prism: change color of text depending on depth +Prism changes the color of text depending on their depth. Makes it easier to see where something is at a glance. #+begin_src emacs-lisp - (defun za/settings-helpful-mode () - "Helpful mode settings" - (za/toggle-wrap t) - (define-key helpful-mode-map (kbd "l") #'za/helpful-previous) - (define-key helpful-mode-map (kbd "r") #'za/helpful-next)) - (add-hook 'helpful-mode-hook #'za/settings-helpful-mode) + (use-package prism) #+end_src - -** Pulse line -When you switch windows, Emacs can flash the cursor briefly to guide your eyes; I like that. -Set some options for pulsing: - +** olivetti: distraction-free writing #+begin_src emacs-lisp - (setq pulse-iterations 10) - (setq pulse-delay 0.05) + (use-package olivetti) #+end_src - -Define the pulse function: - +** nov.el: EPUB support #+begin_src emacs-lisp - (defun pulse-line (&rest _) - "Pulse the current line." - (pulse-momentary-highlight-one-line (point))) + (use-package nov) + (add-to-list 'auto-mode-alist '("\\.epub\\'" . nov-mode)) #+end_src - -Run it in certain cases: scrolling up/down, recentering, switching windows. -'dolist' binds 'command' to each value in the list in turn, and runs the body. -'advice-add' makes the pulse-line function run after 'command'. +* Mode/language specific packages +** Org +*** Installation +Install Org and require additional components that I use. #+begin_src emacs-lisp - (dolist (command '(scroll-up-command scroll-down-command recenter-top-bottom other-window)) - (advice-add command :after #'pulse-line)) + (use-package org + :custom + (org-return-follows-link t "Easier link following. Actual enter is still possible with ~C-q C-j~.") + (org-babel-python-command "python3") + (org-confirm-babel-evaluate nil) + (org-link-elisp-confirm-function #'y-or-n-p) + (org-link-elisp-skip-confirm-regexp "^org-noter$") + (org-clock-sound (concat user-emacs-directory "notification.wav")) + (org-export-backends '(ascii html icalendar latex md odt org)) + (org-catch-invisible-edits 'show-and-error + "Sometimes when text is folded away, I might accidentally edit text inside of it. This option prevents that. I wanted to do 'smart', but that has a 'fixme' so it might change in the future...Instead, show what's being edited, but don't perform the edit.") + (org-src-tab-acts-natively t "a tab in a code block indents the code as it should") + + :bind (("C-c a" . org-agenda) + ("C-c n" . org-capture) + ("C-c l" . org-store-link) + :map org-mode-map + ("C-M-<return>" . org-insert-todo-heading) + ("C-c M-y" . org-yank-link-url) + ("C-c N" . org-noter) + ("C-M-i" . completion-at-point) + ("C-c q t" . org-timestone-set-org-current-time-effective)) + :hook ((org-mode . abbrev-mode) + (org-mode . za/echo-area-tooltips) + (org-mode . org-superstar-mode) + (org-mode . org-indent-mode) + (org-mode . za/settings-on-org-mode)) + :config + (defun za/settings-on-org-mode () + "Settings on enabling org mode" + (za/toggle-wrap t) + (setq org-tags-column (- 10 (window-total-width))) + ;; Realign tags + (org-set-tags-command '(4))) + + (require 'org-tempo) + (require 'org-habit) + (require 'org-id) + (org-babel-do-load-languages + 'org-babel-load-languages + '((emacs-lisp . t) + (R . t) + (python . t) + (ruby . t) + (shell . t))) + (use-package inf-ruby) + (use-package org-superstar + :custom + (org-superstar-leading-bullet ?\s)) + + ;; Linking to emails via notmuch + (use-package ol-notmuch :quelpa) + + ;; Tempo expansions + (add-to-list 'org-structure-template-alist '("se" . "src emacs-lisp")) + (add-to-list 'org-structure-template-alist '("sb" . "src bibtex")) + (add-to-list 'org-structure-template-alist '("ss" . "src sh"))) #+end_src -And set the pulse color: - +*** Agenda & GTD +**** Agenda mode settings #+begin_src emacs-lisp - (custom-set-faces '(pulse-highlight-start-face ((t (:background "CadetBlue2"))))) -#+end_src + (use-package org-agenda + :ensure org + :bind (:map org-agenda-mode-map + ("C-c TAB" . za/org-agenda-goto-narrowed-subtree)) + :custom + (org-agenda-files (list za/org-life-main + za/org-life-inbox + za/org-life-tickler)) + (org-agenda-text-search-extra-files + (directory-files za/org-life-dir t (rx bol (not ?.) (* anything) ".org")) + "I want to search all Org files in the life directory") -** Pager toggle keybinding -M-x view-mode enables pager behavior. -I want read-only files to automatically use pager mode: + :config + (defun za/org-agenda-goto-narrowed-subtree () + "Jump to current agenda item and narrow to its subtree." + (interactive) + (delete-other-windows) + (org-agenda-goto) + (org-narrow-to-subtree) + (other-window 1))) -#+begin_src emacs-lisp - (setq view-read-only t) #+end_src -** Mail mode for neomutt -When editing a message from neomutt, I want to use mail mode. -Even though I won't be sending the email from there, I like the syntax highlighting :) + +Fix tag display by dynamically calculating the column. #+begin_src emacs-lisp - (add-to-list 'auto-mode-alist '("/neomutt-" . mail-mode)) + (defun za/settings-org-agenda-mode () + "My settings for org agenda mode" + (setq org-agenda-tags-column (- 10 (window-total-width)))) + (add-hook 'org-agenda-mode-hook #'za/settings-org-agenda-mode) #+end_src -** Zap up to char -It's more useful for me to be able to delete up to a character instead of to and including a character: + +**** Opening files +Convenience functions to make opening the main file faster: #+begin_src emacs-lisp - (za/global-set-key (kbd "M-z") 'zap-up-to-char) + (defun gtd () "GTD: main file" (interactive) (find-file za/org-life-main)) + (defun gtd-inbox () "GTD: inbox" (interactive) (find-file za/org-life-inbox)) + (defun gtd-archive () "GTD: archive" (interactive) (find-file za/org-life-archive)) + (defun gtd-someday () "GTD: someday" (interactive) (find-file za/org-life-someday)) + (defun gtd-tickler () "GTD: tickler" (interactive) (find-file za/org-life-tickler)) #+end_src -** Expansion/completion -Use hippie expand instead of dabbrev-expand: + +Bind keys to those functions: #+begin_src emacs-lisp - (za/global-set-key (kbd "M-/") 'hippie-expand) + (bind-keys :prefix "M-g t" + :prefix-map za/gtd-files-map + :prefix-docstring "Visit GTD file" + ("i" . gtd-inbox) + ("g" . gtd) + ("a" . gtd-archive) + ("s" . gtd-someday) + ("t" . gtd-tickler)) #+end_src -** Prefer newer file loading +*** Refiling & archiving #+begin_src emacs-lisp - (setq load-prefer-newer t) -#+end_src + (use-package org-refile + :ensure org + :custom + (org-refile-targets `((,za/org-life-main :maxlevel . 3) + (,za/org-life-someday :level . 1) + (,za/org-life-tickler :maxlevel . 2)) + "Where I want to be able to move subtrees (doesn't include inbox because I never refile to that, and the archive has its own keybining)") + (org-archive-location (concat za/org-life-archive "::datetree/") + "I want to archive to a specific file, in a date tree") + (org-refile-use-outline-path 'file + "Include the destination file as an element in the path to a heading, and to use the full paths as completion targets rather than just the heading text itself") + (org-outline-path-complete-in-steps nil + "Tell Org that I don’t want to complete in steps; I want Org to generate all of the possible completions and present them at once (necessary for Helm/Ivy)") + (org-refile-allow-creating-parent-nodes 'confirm + "Allow me to tack new heading names onto the end of my outline path, and if I am asking to create new ones, make me confirm it")) +#+end_src + +*** Quick capture +Quick capture lets me send something to my inbox very quickly, without thinking about where it should go. +The inbox is processed later. -** Automatically find tags file -When opening a file in a git repo, try to discover the etags file: +Templates for quick capture: #+begin_src emacs-lisp - (defun current-tags-file () - "Get current tags file" - (let* ((tagspath ".git/etags") - (git-root (locate-dominating-file (buffer-file-name) tagspath))) - (if git-root - (expand-file-name tagspath git-root)))) + (use-package org-capture + :ensure org + :custom + (org-capture-templates `(("t" "Todo [inbox]" entry + (file ,za/org-life-inbox) + "* TODO %i%?") - (setq default-tags-table-function #'current-tags-file) + ("s" "Save for read/watch/listen" entry + (file+headline ,za/org-life-tickler "Read/watch/listen") + "* TODO %?[[%^{link}][%^{description}]] %^G")))) #+end_src -There's probably a better way to write this. I need to ask Reddit for feedback at some point. +*** Todo & custom agenda views +Todo keywords based on the GTD system (pipe separates incomplete from complete). +Apart from the logging-on-done configured [[*Logging][below]], I also want to log a note & timestamp when I start waiting on something. +In ~org-todo-keywords~, ~@~ means note+timestamp, ~!~ means timestamp, ~@/!~ means note+timestamp on state entry and timestamp on leave. -** Semantic mode -Disabled for now, don't use it much. +#+begin_src emacs-lisp + (custom-set-variables '(org-todo-keywords '((sequence "TODO(t)" "NEXT(n)" "WAITING(w@)" "STARTED(s)" "|" "DONE(d)" "CANCELLED(c)"))) + '(org-todo-keyword-faces '(("TODO" . org-todo) + ("NEXT" . org-todo) + ("WAITING" . org-todo) + ("STARTED" . org-todo) + ("DONE" . org-done) + ("CANCELLED" . org-done)))) +#+end_src -Set default submodes: +I decided that projects will not be TODO items, but their progress will be tracked with a progress cookie ([x/y]). This function converts an item to a project: it adds a PROJECT tag, sets the progress indicator to count all checkboxes in sub-items (only TODO items), and removes any existing TODO keywords. Finally, PROJECT tags shouldn't be inherited (i.e. subtasks shouldn't be marked as projects). -#+begin_src emacs-lisp :tangle no - (setq semantic-default-submodes '(global-semantic-idle-scheduler-mode ; reparse buffer when idle - global-semanticdb-minor-mode ; maintain database - global-semantic-idle-summary-mode)) ; show information (e.g. types) about tag at point - global-semantic-stickyfunc-mode)) ; show current func in header line +#+begin_src emacs-lisp + (defun za/mark-as-project () + "This function makes sure that the current heading has + (1) the tag PROJECT + (2) the property COOKIE_DATA set to \"todo recursive\" + (3) a leading progress indicator" + (interactive) + (org-set-property "TODO" "") + (org-toggle-tag "PROJECT" 'on) + (org-set-property "COOKIE_DATA" "todo recursive") + (org-back-to-heading t) + (forward-whitespace 1) + (insert "[/] ") + (org-update-statistics-cookies nil)) #+end_src -Add some keybindings: +Only the top-level project headlines should be tagged as projects, so disable inheritance of that tag: -#+begin_src emacs-lisp :tangle no - (with-eval-after-load 'semantic - (define-key semantic-mode-map (kbd "C-c , .") #'semantic-ia-show-summary)) +#+begin_src emacs-lisp + (custom-set-variables '(org-tags-exclude-from-inheritance '("PROJECT"))) #+end_src -SemanticDB is written into ~/.emacs.d/semanticdb/. - -Enable semantic mode for major modes: +Define a function to skip items if they're part of a project (i.e. one of their parents has a "PROJECT" tag). +The problem is, the "PROJECT" tag isn't inherited. So, we temporarily disable excluding from inheritance, just for the ~org-get-tags~ call. Then check if "PROJECT" is one of the tags. -#+begin_src emacs-lisp :tangle no - (defun za/settings-c-mode () - "C mode settings" - (semantic-mode 1)) -#+end_src +#+begin_src emacs-lisp + (defun za/skip-if-in-project () + "Skip items that are part of a project" + (let ((subtree-end (save-excursion (org-end-of-subtree t))) + (item-tags (let ((org-tags-exclude-from-inheritance nil)) (org-get-tags)))) + (if (member "PROJECT" item-tags) + subtree-end + nil))) -#+begin_src emacs-lisp :tangle no - (let ((mode-hooks '(c-mode-common-hook))) - (dolist (mode-name mode-hooks) - (add-hook mode-name #'za/settings-c-mode))) #+end_src -** Forward-word and forward-to-word -Change M-f to stop at the start of the word: +Also, define a function to skip tasks (trees) that are not habits (i.e. don't have the STYLE property ~habit~): #+begin_src emacs-lisp - (za/global-set-key (kbd "M-f") 'forward-to-word) + (defun za/skip-unless-habit () + "Skip trees that are not habits" + (let ((subtree-end (save-excursion (org-end-of-subtree t)))) + (if (string= (org-entry-get nil "STYLE") "habit") + nil + subtree-end))) #+end_src -Bind ESC M-f to the old functionality of M-f (stop at end of word) +And one to skip tasks that /are/ habits: #+begin_src emacs-lisp - (za/global-set-key (kbd "ESC M-f") 'forward-word) + (defun za/skip-if-habit () + "Skip trees that are not habits" + (let ((subtree-end (save-excursion (org-end-of-subtree t)))) + (if (string= (org-entry-get nil "STYLE") "habit") + subtree-end + nil))) #+end_src -** Rectangle insert string -#+begin_src emacs-lisp - (za/global-set-key (kbd "C-x r I") 'string-insert-rectangle) - (za/global-set-key (kbd "C-x r R") 'replace-rectangle) -#+end_src -** End sentences with one space -Emacs uses the rather old-fashioned convention of treating a period followed by double spaces as end of sentence. However, it is more common these days to end sentences with a period followed by a single space. -Let a period followed by a single space be treated as end of sentence: +And another function, to skip tasks that are blocked: #+begin_src emacs-lisp - (setq sentence-end-double-space nil) + (defun za/skip-if-blocked () + "Skip trees that are blocked by previous tasks" + (let ((subtree-end (save-excursion (org-end-of-subtree t)))) + (if (org-entry-blocked-p) + subtree-end + nil))) #+end_src -** Make region readonly or writable -#+begin_src emacs-lisp - (defun za/set-region-read-only (begin end) - "Sets the read-only text property on the marked region. - Use `set-region-writeable' to remove this property." - ;; See https://stackoverflow.com/questions/7410125 - (interactive "r") - (with-silent-modifications - (put-text-property begin end 'read-only t))) - (defun za/set-region-writeable (begin end) - "Removes the read-only text property from the marked region. - Use `set-region-read-only' to set this property." - ;; See https://stackoverflow.com/questions/7410125 - (interactive "r") - (with-silent-modifications - (remove-text-properties begin end '(read-only t)))) -#+end_src -** Open line like in Vim -I prefer to open-line the way o/O works in Vim: +Create custom agenda view based on those keywords. +Agenda views are made up of blocks, appearing in the order that you declare them. +The first two strings are what shows up in the agenda dispatcher (the key to press and the description). #+begin_src emacs-lisp - ;; Autoindent open-*-lines - (defvar za/open-line-newline-and-indent t - "Modify the behavior of the open-*-line functions to cause them to autoindent.") + (setq org-agenda-custom-commands + '(("n" "Next actions" + todo "NEXT" ((org-agenda-overriding-header "Next actions:") + (org-agenda-sorting-strategy '(priority-down alpha-up)))) + ("W" "Waiting" + ((todo "WAITING" ((org-agenda-overriding-header "Waiting:"))))) + ("S" . "Saved for later...") + ("Sw" "Saved to watch" + ((tags-todo "WATCH" ((org-agenda-overriding-header "To watch:"))))) + ("Sr" "Saved to read" + ((tags-todo "READ" ((org-agenda-overriding-header "To read:"))))) + ("Sl" "Saved to listen" + ((tags-todo "LISTEN" ((org-agenda-overriding-header "To listen:"))))) - (defun za/open-line (prefix) - "Open line like `o`/`O` in Vim. Negative prefix for line above, positive for below." - (interactive "p") - (cond ((< prefix 0) - (beginning-of-line) - (open-line (abs prefix))) - (t - (end-of-line) - (open-line prefix) - (forward-line 1))) - (when za/open-line-newline-and-indent - (indent-according-to-mode))) + ("a" . "Agenda with schedule only...") + ("aw" "This week" + ((agenda "" ((org-agenda-span 'week))))) + ("ad" "Today" + ((agenda "" ((org-agenda-span 'day))))) + ("at" "Tomorrow" + ((agenda "" ((org-agenda-span 'day) + (org-agenda-start-day "+1d"))))) + + ("w" "Week Agenda + Next Actions" + ((agenda "" ((org-agenda-overriding-header "Week agenda:"))) + (todo "NEXT" ((org-agenda-overriding-header "Next actions:"))))) + + ("o" "Month agenda" + ((agenda "" ((org-agenda-overriding-header "Month agenda:") + (org-agenda-span 'month))))) + + ("d" "Day Agenda + Next Actions + Habits" + ((agenda "" ((org-agenda-overriding-header "Day:") + (org-agenda-span 'day) + (org-habit-show-habits nil))) + (todo "NEXT" ((org-agenda-overriding-header "Next actions:"))) + (agenda "" ((org-agenda-overriding-header "Habits:") + (org-agenda-span 'day) + (org-agenda-use-time-grid nil) + (org-agenda-skip-function 'za/skip-unless-habit) + (org-habit-show-habits t) (org-habit-show-habits-only-for-today nil) + (org-habit-show-all-today t))) + (todo "WAITING" ((org-agenda-overriding-header "Waiting:"))))) + + ("k" "Kanban view" + ((todo "DONE" ((org-agenda-overriding-header "Done:") (org-agenda-sorting-strategy '(deadline-up priority-down alpha-up)))) + (todo "STARTED" ((org-agenda-overriding-header "In progress:") (org-agenda-sorting-strategy '(deadline-up priority-down alpha-up)))) + (todo "NEXT" ((org-agenda-overriding-header "To do:") (org-agenda-sorting-strategy '(deadline-up priority-down alpha-up)))))) - (defun za/open-line-keep-point (prefix) - "Open line like `o`/`O` in Vim but don't move point. Negative prefix for line above, positive for below." - (interactive "p") - (save-mark-and-excursion (za/open-line prefix))) -#+end_src + ("p" "Projects" + ((tags "PROJECT" ((org-agenda-overriding-header "Projects:") + (org-agenda-prefix-format '((tags . " %i %-22(let ((deadline (org-entry-get nil \"DEADLINE\"))) (if deadline deadline \"\"))"))) + (org-agenda-sorting-strategy '((tags deadline-up alpha-down))))))) -And keybindings: + ("f" "Finished tasks that aren't in a project" + ((tags "TODO=\"DONE\"|TODO=\"CANCELLED\"" ((org-agenda-overriding-header "Finished tasks:") + (org-agenda-skip-function 'za/skip-if-in-project))))) -#+begin_src emacs-lisp - (za/global-set-key (kbd "C-o") #'za/open-line) - (za/global-set-key (kbd "C-M-o") #'za/open-line-keep-point) + ;; Useful thread for opening calfw: https://github.com/kiwanami/emacs-calfw/issues/18 + ("c" "Calendar view" (lambda (&rest _) + (interactive) + (let ((org-agenda-skip-function 'za/skip-if-habit)) + (cfw:open-org-calendar)))))) #+end_src -** Repeat mode: easy repeating of commans + +In calfw, I don't want to show habits: + #+begin_src emacs-lisp - (repeat-mode 1) + (add-hook 'cfw:calendar-mode-hook (setq-local org-agenda-skip-function 'za/skip-if-habit)) #+end_src -** Toggle auto-revert-mode -Sometimes I want to toggle auto reverting (or autoread) of buffer: + +*** Logging for tasks +I want to log into the LOGBOOK drawer (useful when I want to take quick notes): #+begin_src emacs-lisp - (za/global-set-key (kbd "C-c q a") #'auto-revert-mode) + (setq org-log-into-drawer "LOGBOOK") #+end_src -** Unfill region/paragraph -Taken from here: https://www.emacswiki.org/emacs/UnfillParagraph -#+begin_src emacs-lisp - (defun za/unfill-paragraph (&optional region) - "Takes a multi-line paragraph and makes it into a single line of text." - (interactive (progn (barf-if-buffer-read-only) '(t))) - (let ((fill-column (point-max)) - ;; This would override `fill-column' if it's an integer. - (emacs-lisp-docstring-fill-column t)) - (fill-paragraph nil region))) +I also want to log when I finish a task (useful for archiving). +Furthermore, when I'm done, I want to add a note (any important +workarounds/tips). And when I reschedule, I want to know the reason. +I can disable logging on state change for a specific task by adding ~:LOGGING: nil~ to the ~:PROPERTIES:~ drawer. - (za/global-set-key (kbd "M-Q") #'za/unfill-paragraph) +#+begin_src emacs-lisp + (setq org-log-done 'note + org-log-reschedule 'note) #+end_src -** Insert macro as Lisp -From here: https://www.masteringemacs.org/article/keyboard-macros-are-misunderstood + +I want to hide drawers on startup. This variable has options: +- 'overview': Top-level headlines only. +- 'content': All headlines. +- 'showall': No folding on any entry. +- 'show2levels: Headline levels 1-2. +- 'show3levels: Headline levels 1-3. +- 'show4levels: Headline levels 1-4. +- 'show5levels: Headline levels 1-5. +- 'showeverything: Show even drawer contents. #+begin_src emacs-lisp - (require 'kmacro) - (defalias 'kmacro-insert-macro 'insert-kbd-macro) - ;; Add advice to ignore errors on `kmacro-keyboard-macro-p`, it was messing up because of some entry in `obarray` - (advice-add #'kmacro-keyboard-macro-p :around (lambda (fun sym) "Ignore errors." (ignore-errors (funcall fun sym)))) - (define-key kmacro-keymap (kbd "I") #'kmacro-insert-macro) + (setq org-startup-folded 'content) #+end_src -** Show local help at point when idling + +*** Task ordering +Some tasks should be ordered, i.e. they should be done in steps. +Those have the ~:ORDERED: t~ setting in ~:PROPERTIES:~, and it should be enforced: + #+begin_src emacs-lisp - (defun za/echo-area-tooltips () - "Show tooltips in the echo area automatically for current buffer." - (setq-local help-at-pt-display-when-idle t - help-at-pt-timer-delay 0) - (help-at-pt-cancel-timer) - (help-at-pt-set-timer)) + (setq org-enforce-todo-dependencies t) #+end_src -* Markdown -Markdown mode settings. +Furthermore, tasks that are ordered and can't be done yet because of previous steps should be dimmed in the agenda: #+begin_src emacs-lisp - (defun za/settings-markdown-mode () - "My settings for markdown mode" - (auto-fill-mode 0) - (flyspell-mode 1) - (za/toggle-wrap t)) - - (add-hook 'markdown-mode-hook #'za-settings-markdown-mode) + (setq org-agenda-dim-blocked-tasks t) #+end_src -* Bib(la)tex +I might also want to set ~org-enforce-todo-checkbox-dependencies~, but not convinced on that one yet. + +*** Time tracking & effort +Time tracking should be done in its own drawer: + #+begin_src emacs-lisp - (defun za/settings-bibtex-mode () - "My settings for bibtex mode" - (bibtex-set-dialect "biblatex")) + (setq org-clock-into-drawer "CLOCK") #+end_src +And to customize how clock tables work: + #+begin_src emacs-lisp - (add-hook 'bibtex-mode-hook #'za/settings-bibtex-mode) + (setq org-clocktable-defaults '(:lang "en" :scope agenda-with-archives :wstart 1 :mstart 1 :compact t :maxlevel nil)) + (setq org-agenda-clockreport-parameter-plist '(:link t :maxlevel nil)) #+end_src -* Python -In Python, I want to enable flycheck and semantic mode: +I want to set effort in hours:minutes: #+begin_src emacs-lisp - (add-hook 'python-mode-hook #'flycheck-mode) - ;;(add-hook 'python-mode-hook #'semantic-mode) + (add-to-list 'org-global-properties '("Effort_ALL" . "0:05 0:10 0:15 0:20 0:30 0:45 1:00 1:30 2:00 4:00 6:00 8:00")) #+end_src -* Misc settings -** Enable all commands -By default, Emacs disables some commands. -I want to have these enabled so I don't get a prompt whenever I try to use a disabled command. +I want column view to look like this: + +| To do | Task | Tags | Sum of time elapsed | Sum of time estimated (effort) | +|--------------+-----------+------+---------------------+--------------------------------| +| todo keyword | task name | tags | sum of clock | sum of estimated time | +| ... | ... | ... | ... | ... | #+begin_src emacs-lisp - (setq disabled-command-function nil) + (setq org-columns-default-format "%7TODO (To Do) %32ITEM(Task) %TAGS(Tags) %11CLOCKSUM_T(Clock) %10Difficulty(Difficulty) %8Effort(Effort){:}") #+end_src -** More extensive apropos +Fix column alignment in agenda. + #+begin_src emacs-lisp - (setq apropos-do-all t) + (set-face-attribute 'org-column nil + :height (face-attribute 'default :height) + :family (face-attribute 'default :family)) + (set-face-attribute 'org-agenda-date-today nil + :height (face-attribute 'default :height)) #+end_src -** Easily edit my config -Bind a keyboard shortcut to open my config. -The "(interactive)" means that it can be called from a keybinding or from M-x. +*** Calculate time since timestamp #+begin_src emacs-lisp - (defun za/edit-config-org () - "Edit my config.org file" + (defun za/org-time-since () + "Print the amount of time between the timestamp at point and the current date and time." (interactive) - (find-file (expand-file-name "config.org" user-emacs-directory))) + (unless (org-at-timestamp-p 'lax) + (user-error "Not at timestamp")) + + (when (org-at-timestamp-p 'lax) + (let ((timestamp (match-string 0))) + (with-temp-buffer + (insert timestamp + "--" + (org-time-stamp '(16))) + (org-evaluate-time-range))))) #+end_src +*** Priorities: how important something is +I usually have a lot of 'next' actions, so I prefer 4 priority levels instead of the default 3: A (urgent, ASAP), B (important), C (not super important), D (do in free time): + #+begin_src emacs-lisp - (za/global-set-key (kbd "C-c E") 'za/edit-config-org) + (setq org-priority-highest ?A + org-priority-lowest ?D + org-priority-default ?C) #+end_src -(describe-key (kbd "C-c E")) -** Fast access to view-mode (pager) -I want to bind view-mode to a key for easy access: + +Faces for priorities in agenda: #+begin_src emacs-lisp - (za/global-set-key (kbd "C-c q r") 'view-mode) + (setq org-priority-faces `((?A . (:foreground ,(face-foreground 'error))) + (?B . (:foreground ,(face-foreground 'org-todo))) + (?C . (:foreground ,(face-foreground 'font-lock-constant-face) :weight semi-light)) + (?D . (:foreground ,(face-foreground 'font-lock-string-face) :slant italic :weight light)))) #+end_src -** Kill this buffer -I like to be able to kill a buffer instantly: - +*** Energy requirement: how difficult something is +#+begin_src emacs-lisp + (add-to-list 'org-global-properties '("Difficulty_ALL" . "low medium high")) +#+end_src +*** Custom functions +**** Get number of headlines in a file #+begin_src emacs-lisp - (za/global-set-key (kbd "s-<backspace>") 'kill-current-buffer) + (defun za/org-count-headlines-in-file (level filename) + "Count number of level LEVEL headlines in FILENAME. If LEVEL is 0, count all." + (let ((headline-str (cond ((zerop level) "^\*+") + (t (format "^%s " (apply 'concat (make-list level "\\*"))))))) + (save-mark-and-excursion + (with-temp-buffer + (insert-file-contents filename) + (count-matches headline-str (point-min) (point-max)))))) #+end_src -** Delete this file (and kill the buffer) +**** Yank URL #+begin_src emacs-lisp - (defun za/delete-this-file () - "Kill the current buffer and delete its associated file." + (defun org-yank-link-url () (interactive) - (let ((fname (buffer-file-name)) - (buf (current-buffer))) - (unless (and fname (file-exists-p fname)) - (user-error (format "Buffer has no associated file."))) + (kill-new (org-element-property :raw-link (org-element-context))) + (message "Link copied to clipboard")) +#+end_src +*** org publishing +I decided, after trying many different things, to settle on org-publish. - (unless (y-or-n-p (format "Really delete %s and its buffer?" fname)) - (user-error "User cancelled.")) +#+begin_src emacs-lisp + (defconst za/org-roam-top-name "Top" "The name of the top-level Org-roam node.") + (defun za/org-roam-sitemap-function (title list) + "Customized function to generate sitemap for org-roam, almost the same as `org-publish-sitemap-default`." + (concat "#+TITLE: " title "\n\n" + (format "[[file:%s][%s]]\n\n" + (file-name-nondirectory (org-roam-node-file + (org-roam-node-from-title-or-alias za/org-roam-top-name))) + "Click here for entrypoint."))) + ;; (org-list-to-org list))) <-- this is taken care of by Zola - (delete-file fname 'trash-if-enabled) - (kill-buffer buf) - (message "Deleted %s and killed its buffer." fname))) +#+end_src + +To make this work with Zola, I need to export Github-flavored markdown (fenced code blocks with language): + +#+begin_src emacs-lisp + (require 'ox-publish) + (require 'ox-md) - (za/global-set-key (kbd "C-c s-<backspace>") #'za/delete-this-file) + (use-package ox-gfm + :init + (with-eval-after-load 'org (require 'ox-gfm))) #+end_src -** Toggle fullscreen -I'll use the keybinding that's standard on macOS: +First difficulty: Zola needs front matter with ~+++...+++~. +The default Markdown backend doesn't provide that, so need to customize it by advising the default ~org-md-template~. #+begin_src emacs-lisp - (za/global-set-key (kbd "C-s-f") #'toggle-frame-fullscreen) + (defun za/org-md-template-zola (contents info) + "Markdown template compatible with Zola (generates the necessary front matter from CONTENTS and INFO)." + (let ((title (org-md-plain-text (org-element-interpret-data (plist-get info :title)) info))) + (concat "+++\n" + (format "title = \"%s\"" (string-replace "\"" "'" title)) + "\n+++\n" + (format "# %s\n" title) + contents))) #+end_src -** Enable recursive minibuffers +Second difficulty: links need to be reformatted and changed for static data (like images). +This function filters the return value of ~org-md-link~. + #+begin_src emacs-lisp - (setq enable-recursive-minibuffers t - minibuffer-depth-indicate-mode t) + (defun za/org-md-link-zola (linkstr) + "A filter function for the return value of + `org-md-link` (LINKSTR) to generate a link compatible with Zola." + (cond ((string-match-p (rx ".md") linkstr) + (string-replace "](" "](@/org-roam/" linkstr)) + ((string-match-p (rx "](/") linkstr) + (replace-regexp-in-string (rx "](/" (* any) "/org-roam/data") "](/org-roam-data" linkstr)) + (t linkstr))) #+end_src -** Sexp manipulation -When I write lisp, sometimes I want to switch two sexps (e.g. ~(one) (two)~ → ~(two) (one)~), so a key binding is nice for that: +And here's the custom publish function that adds/removes the necessary advice: #+begin_src emacs-lisp - (za/global-set-key (kbd "C-S-t") #'transpose-sexps) + (defun za/org-gfm-publish-to-gfm-zola (plist filename pub-dir) + "Run `org-gfm-publish-to-gfm`, advising the necessary + functions to generate Zola-compatible markdown." + (let ((advice '((org-md-template :override za/org-md-template-zola) + (org-md-link :filter-return za/org-md-link-zola) + (org-gfm-table :override org-md--convert-to-html)))) ; Zola uses CommonMark, so doesn't support Markdown tables + + (dolist (orig-type-new advice) (apply #'advice-add orig-type-new)) + (org-gfm-publish-to-gfm plist filename pub-dir) + (dolist (orig-type-new advice) + (advice-remove (nth 0 orig-type-new) + (nth 2 orig-type-new))))) #+end_src -Also, to raise a sexp (e.g. ~(one (two))~ → ~(two)~): +Finally, the list of things we can publish with their respective publishin functions: + +#+begin_src emacs-lisp + (setq org-publish-project-alist + `( + ("org-notes" + :base-directory ,za/org-roam-dir + :base-extension "org" + :publishing-directory ,(concat za/my-website-dir "content/org-roam/") + :publishing-function za/org-gfm-publish-to-gfm-zola + :recursive t + :sitemap-filename "_index.md" + :sitemap-title "Org Roam" + :sitemap-function za/org-roam-sitemap-function + :auto-sitemap t) + + ("org-notes-data" + :base-directory ,(concat za/org-roam-dir "/data") + :base-extension any + :publishing-directory ,(concat za/my-website-dir "static/org-roam-data/") + :recursive t + :publishing-function org-publish-attachment) -#+begin_src emacs-lisp - (za/global-set-key (kbd "C-S-u") #'raise-sexp) + ("org-roam" :components ("org-notes" "org-notes-data")) + )) #+end_src -** Dedicated windows -Sometimes I want to avoid Emacs overriding a window's contents. -So I create a keybinding to toggle dedicated on a window: +And a function to rsync to my VPS: #+begin_src emacs-lisp - (defun za/toggle-window-dedicated-p () - "Toggle set-window-dedicated-p on current window" + (defun za/publish-upload-to-website () + "Upload my website to my VPS" (interactive) - (cond ((window-dedicated-p (selected-window)) - (set-window-dedicated-p (selected-window) nil) - (message "Window no longer dedicated")) - (t - (set-window-dedicated-p (selected-window) t) - (message "Window marked as dedicated")))) - - (za/global-set-key (kbd "C-x 9") #'za/toggle-window-dedicated-p) - -#+end_src - -** Rebuild org cache -#+begin_src emacs-lisp -(defun za/force-org-rebuild-cache () - "Rebuild the `org-mode' and `org-roam' cache." - (interactive) - (org-id-update-id-locations) - ;; Note: you may need `org-roam-db-clear-all' - ;; followed by `org-roam-db-sync' - (org-roam-db-sync) - (org-roam-update-org-id-locations)) + (async-shell-command (format "cd %s && zola build && yes|publish" za/my-website-dir) "*Async Shell publish*")) #+end_src - -** Info manual functions -For some reason, these things don't show up in the index: - +*** Rebuild org cache #+begin_src emacs-lisp - (defun elisp-info (&optional node) - "Read documentation for Elisp in the info system. - With optional NODE, go directly to that node." + (defun za/force-org-rebuild-cache () + "Rebuild the `org-mode' and `org-roam' cache." (interactive) - (info (format "(elisp)%s" (or node "")))) + (org-id-update-id-locations) + ;; Note: you may need `org-roam-db-clear-all' + ;; followed by `org-roam-db-sync' + (org-roam-db-sync) + (org-roam-update-org-id-locations)) #+end_src -Though I can also just use ~info-display-manual~. - -** View webp and other formats -Emacs handles common image formats internally, but for stuff like webp, you need an external converter: +*** org-caldav +This lets me sync my Org agenda to my CalDAV server. +The main reason is because Orgzly doesn't have a calendar view and can't (yet) search for events on a specific day, so if someone asks "are you free on that day", it's a bit hard for me to answer if I don't have my computer with me. +This way, I can just check my calendar. #+begin_src emacs-lisp - (setq image-use-external-converter t) + (use-package org-caldav + :init + (defconst za/org-life-calendar-inbox (concat za/org-life-dir "calendar-inbox.org")) + :custom + (org-caldav-url za/caldav-url) + (org-caldav-calendar-id za/caldav-org-calendar-id) + (org-caldav-inbox za/org-life-calendar-inbox) + (org-caldav-files (cons (car (split-string org-archive-location "::")) org-agenda-files)) + (org-icalendar-include-todo 'all) + (org-icalendar-use-deadline '(event-if-todo event-if-not-todo todo-due)) + (org-icalendar-use-scheduled '(todo-start event-if-todo event-if-not-todo)) + (org-caldav-exclude-tags '("HABIT") + "I don't want to export habits, because those will just clutter up my calendar. The calendar is supposed to be for one-off stuff, or rarely repeating stuff. Yes, I have to manually add the HABIT tag to every habit. Perhaps nicer would be to exclude based on the property ~:STYLE: habit~, but I haven't figured that one out yet.")) #+end_src -You also need imagemagick installed. - -** Rotate windows horizontal ↔ vertical -#+begin_src emacs-lisp - (defun za/rotate-windows () - (interactive) - (if (= (count-windows) 2) - (let* ((this-win-buffer (window-buffer)) - (next-win-buffer (window-buffer (next-window))) - (this-win-edges (window-edges (selected-window))) - (next-win-edges (window-edges (next-window))) - (this-win-2nd (not (and (<= (car this-win-edges) - (car next-win-edges)) - (<= (cadr this-win-edges) - (cadr next-win-edges))))) - (splitter - (if (= (car this-win-edges) - (car (window-edges (next-window)))) - 'split-window-horizontally - 'split-window-vertically))) - (delete-other-windows) - (let ((first-win (selected-window))) - (funcall splitter) - (if this-win-2nd (other-window 1)) - (set-window-buffer (selected-window) this-win-buffer) - (set-window-buffer (next-window) next-win-buffer) - (select-window first-win) - (if this-win-2nd (other-window 1)))))) -#+end_src +Maybe check [[https://old.reddit.com/r/orgmode/comments/8rl8ep/making_orgcaldav_useable/e0sb5j0/][this]] for a way to sync on save. +*** org-contrib #+begin_src emacs-lisp - (za/global-set-key (kbd "C-x 7") #'za/rotate-windows) + (use-package org-contrib + :config + (require 'org-checklist)) #+end_src - -* Language-specific +*** org-ref #+begin_src emacs-lisp - (let ((modes '((emacs-lisp-mode-hook . (flycheck-mode - rainbow-mode - outline-minor-mode - company-mode)) - (org-mode-hook . (abbrev-mode - za/echo-area-tooltips))))) - (dolist (major-minorlist modes) - (let ((major (car major-minorlist)) - (minor-modes (cdr major-minorlist))) - (dolist (minor minor-modes) (add-hook major minor))))) + (use-package org-ref) #+end_src - -* Sound support -On macOS, you can use afplay: - +*** org-roam #+begin_src emacs-lisp - (defun za/play-sound-file-macos (file &optional volume device) - "Play sound using `afplay` on macOS" - (unless (file-readable-p file) - (user-error "File %s not readable." file)) + (use-package org-roam + :custom + (org-roam-directory za/org-roam-dir) + (org-roam-completion-everywhere t) - ;; the `apply` is required here because I need to build a list of arguments - (apply 'start-process `("afplay" nil - "afplay" - ,@(if volume (list "-v" (int-to-string volume))) - ,file))) + :config + (org-roam-setup) + (bind-keys :prefix "C-c w" + :prefix-map za/org-roam-map + :prefix-docstring "Org roam" + ("n" . org-roam-capture) + ("f" . org-roam-node-find) + ("w" . org-roam-buffer-toggle) + ("i" . org-roam-node-insert)) + (require 'org-roam-export)) #+end_src -Then redefine the play-sound-file function where needed: - +*** org-roam-ui #+begin_src emacs-lisp - (cond ((and (not (fboundp 'play-sound-internal)) - (eq system-type 'darwin)) - (advice-add 'play-sound-file :override #'za/play-sound-file-macos))) + (use-package org-roam-ui) #+end_src - -* Daemon -I want to have a way to kill the Emacs daemon. -So, define a function that kills the frame, and with a prefix kills emacs. +*** org-download +Drag-and-drop images to Emacs Org mode. #+begin_src emacs-lisp - (defun za/emacsclient-c-x-c-c (&optional arg) - "If running in emacsclient, make C-x C-c exit frame, and C-u C-x C-c exit Emacs." - (interactive "P") ; prefix arg in raw form - (if arg - (save-buffers-kill-emacs) - (save-buffers-kill-terminal))) + (use-package org-download + :custom + (org-download-method 'attach) + (org-download-backend 'curl)) #+end_src -Then, if I'm in an emacsclient, I want to bind C-x C-c to that function (if not, I just want the default keybinding): +*** org-sticky-header +Displays in the header-line the Org heading for the node that’s at the top of the window. #+begin_src emacs-lisp - ;; If not running in emacsclient, use the default bindings - (if (daemonp) - (za/global-set-key (kbd "C-x C-c") #'za/emacsclient-c-x-c-c)) + (use-package org-sticky-header) #+end_src - -Furthermore, I want to set the theme correctly whenever I connect with 'emacsclient': - +*** org-timestone #+begin_src emacs-lisp - (if (daemonp) - (add-hook 'after-make-frame-functions #'za/auto-select-theme)) + (use-package org-timestone + :quelpa (org-timestone :repo "thezeroalpha/org-timestone.el" :fetcher github) + :after org + :ensure nil) #+end_src -* Notmuch -Define some saved searches (i.e. mailboxes): - +*** org-noter #+begin_src emacs-lisp - (setq notmuch-saved-searches - `((:name "inbox: personal" :query ,(format "folder:/%s/ tag:inbox" za/email-personal) :key ,(kbd "ip") :search-type 'tree) - (:name "inbox: school" :query ,(format "folder:/%s/ tag:inbox" za/email-vu) :key ,(kbd "is") :search-type 'tree) - (:name "archive: personal" :query ,(format "folder:/%s/ tag:archive" za/email-personal) :key ,(kbd "ap") :search-type 'tree) - (:name "archive: school" :query ,(format "folder:/%s/ tag:archive" za/email-vu) :key ,(kbd "as") :search-type 'tree))) + (use-package org-noter + :config + ;; Fix disabling of line wrap by no-opping set-notes-scroll + (advice-add 'org-noter--set-notes-scroll :override 'za/no-op)) #+end_src -Define the main screen sections: +*** TODO the path for org-roam export and data export should be configurable, not hard-coded +** Mail mode for neomutt +When editing a message from neomutt, I want to use mail mode. +Even though I won't be sending the email from there, I like the syntax highlighting :) #+begin_src emacs-lisp - (setq notmuch-hello-sections - '(notmuch-hello-insert-header - notmuch-hello-insert-saved-searches - notmuch-hello-insert-search - notmuch-hello-insert-alltags - notmuch-hello-insert-footer)) + (add-to-list 'auto-mode-alist '("/neomutt-" . mail-mode)) #+end_src +** DISABLED Semantic mode +Disabled for now, don't use it much. +SemanticDB is written into ~/.emacs.d/semanticdb/. + +#+begin_src emacs-lisp :tangle no + (use-package semantic + :bind (:map semantic-mode-map + ("C-c , ." . semantic-ia-show-summary)) + :custom + (semantic-default-submodes '(global-semantic-idle-scheduler-mode ; reparse buffer when idle + global-semanticdb-minor-mode ; maintain database + global-semantic-idle-summary-mode ; show information (e.g. types) about tag at point + global-semantic-stickyfunc-mode))) ; show current func in header line -Global keybindings: -#+begin_src emacs-lisp - (za/global-set-key (kbd "C-c m") #'notmuch) #+end_src -Show newest mail first: - +** Bib(la)tex #+begin_src emacs-lisp - (setq notmuch-search-oldest-first nil) + (use-package bibtex + :config + (bibtex-set-dialect "biblatex")) #+end_src -Set tags: +** Python +In Python, I want to enable flycheck and semantic mode: #+begin_src emacs-lisp - (setq notmuch-archive-tags '("-inbox" "+archive")) - (setq notmuch-show-mark-unread-tags '("+unread")) - (setq notmuch-delete-tags '("-inbox" "+trash")) - - (setq notmuch-tagging-keys '(("a" notmuch-archive-tags "Archive") - ("r" notmuch-show-mark-read-tags "Mark read") - ("u" notmuch-show-mark-unread-tags "Mark unread") - ("d" notmuch-delete-tags "Delete"))) + (add-hook 'python-mode-hook #'flycheck-mode) + ;;(add-hook 'python-mode-hook #'semantic-mode) #+end_src -Run notmuch-hook script on hello refresh, to move messages to folders according to their tags: - +** Elisp #+begin_src emacs-lisp - (defun za/notmuch-hook-tags2folders () - "Run notmuch-hook to organise email in folders based on tags." - (start-process "notmuch-hook" nil "notmuch-hook" "tags2folders")) + (use-package emacs-lisp + :ensure nil ; preinstalled + :hook ((emacs-lisp-mode . flycheck-mode) + (emacs-lisp-mode . rainbow-mode) + (emacs-lisp-mode . outline-minor-mode) + (emacs-lisp-mode . company-mode))) #+end_src +** lean-mode +Specifically for the Lean prover. +I also install company-lean and helm-lean, which are suggested on the [[https://github.com/leanprover/lean-mode][Github page]]. +Then I map company-complete only for lean-mode. #+begin_src emacs-lisp - (add-hook 'notmuch-hello-refresh-hook #'za/notmuch-hook-tags2folders) + (use-package lean-mode + :config + (use-package company-lean) + :bind (:map lean-mode-map + ("S-SPC" . company-complete))) #+end_src -Sort with newest first: - -#+begin_src emacs-lisp - (setq-default notmuch-search-oldest-first nil) +** sh-mode +#+begin_src emacs-lisp :results value + (use-package sh-script + :hook (sh-mode . flycheck-mode)) #+end_src -* MPC -Set the windows I want to show: +** anki-editor +Some extra keybindings that are not set up by default. +anki-editor doesn't provide a keymap so I have to set one up here: #+begin_src emacs-lisp - (setq mpc-browser-tags '(AlbumArtist Album Genre Playlist)) -#+end_src + (use-package anki-editor + :init + (defvar anki-editor-mode-map (make-sparse-keymap)) + (add-to-list 'minor-mode-map-alist (cons 'anki-editor-mode + anki-editor-mode-map)) + :custom + (anki-editor-use-math-jax t) -Define some functions: + :bind (:map anki-editor-mode-map + ("C-c t" . org-property-next-allowed-value) + ("C-c i" . anki-editor-insert-note) + ("C-c p" . anki-editor-push-notes) + ("C-c c" . anki-editor-cloze-dwim))) +#+end_src +** pdf-tools +A better replacement for DocView: #+begin_src emacs-lisp - (defun za/mpc-seek-forward-20-seconds () - "Seek forward 20 seconds" - (interactive) - (mpc-seek-current "+20")) + (use-package pdf-tools + :custom + (pdf-annot-default-annotation-properties '((t + (label . "Alex Balgavy")) + (text + (icon . "Note") + (color . "#0088ff")) + (highlight + (color . "yellow")) + (squiggly + (color . "orange")) + (strike-out + (color . "red")) + (underline + (color . "blue")))) + :bind (:map pdf-isearch-minor-mode-map + ("C-s" . isearch-forward) + :map pdf-view-mode-map + ;; Save position & jump back + ("C-SPC" . (lambda () (interactive) (message "Position saved") (pdf-view-position-to-register ?x))) + ("C-u C-SPC" . (lambda () (interactive) (pdf-view-jump-to-register ?x)))) + :hook + (pdf-annot-list-mode . pdf-annot-list-follow-minor-mode) + (pdf-annot-edit-contents-minor-mode . org-mode) + (pdf-view-mode . (lambda () (display-line-numbers-mode 0))) + + :config + (pdf-tools-install) + + ;; The arrow tooltip does not show properly when jumping to a + ;; location. Maybe this is a Mac-only thing. See here: + ;; https://github.com/politza/pdf-tools/issues/145 + ;; This ~:override~ advice fixes it, color is customized via ~tooltip~ face + (advice-add #'pdf-util-tooltip-arrow :override #'za/pdf-util-tooltip-arrow) + (defun za/pdf-util-tooltip-arrow (image-top &optional timeout) + "Fix up `pdf-util-tooltip-arrow`, the original doesn't show the arrow." + (pdf-util-assert-pdf-window) + (when (floatp image-top) + (setq image-top + (round (* image-top (cdr (pdf-view-image-size)))))) + (let* (x-gtk-use-system-tooltips ;allow for display property in tooltip + (dx (+ (or (car (window-margins)) 0) + (car (window-fringes)))) + (dy image-top) + (pos (list dx dy dx (+ dy (* 2 (frame-char-height))))) + (vscroll + (pdf-util-required-vscroll pos)) + (tooltip-frame-parameters + `((border-width . 0) + (internal-border-width . 0) + ,@tooltip-frame-parameters)) + (tooltip-hide-delay (or timeout 3))) + (when vscroll + (image-set-window-vscroll vscroll)) + (setq dy (max 0 (- dy + (cdr (pdf-view-image-offset)) + (window-vscroll nil t) + (frame-char-height)))) + (when (overlay-get (pdf-view-current-overlay) 'before-string) + (let* ((e (window-inside-pixel-edges)) + (xw (pdf-util-with-edges (e) e-width))) + (cl-incf dx (/ (- xw (car (pdf-view-image-size t))) 2)))) + (pdf-util-tooltip-in-window "\u2192" dx dy)))) - (defun za/mpc-seek-backward-20-seconds () - "Seek backward 20 seconds" - (interactive) - (mpc-seek-current "-20")) #+end_src -Define some keybindings: +*** TODO this clobbers register x. Find a way to not clobber a register +** virtualenvwrapper +Like virtualenvwrapper.sh, but for Emacs. #+begin_src emacs-lisp - (defun za/mpc-mode-settings () - "MPC mode settings" - (define-key mpc-mode-map "a" #'mpc-playlist-add) - (define-key mpc-mode-map "P" #'mpc-playlist) - (define-key mpc-mode-map "x" #'mpc-playlist-delete) - (define-key mpc-mode-map "p" #'mpc-toggle-play) - (define-key mpc-mode-map "t" #'mpc-select-toggle) - (define-key mpc-mode-map "f" #'za/mpc-seek-forward-20-seconds) - (define-key mpc-mode-map "b" #'za/mpc-seek-backward-20-seconds)) + (use-package virtualenvwrapper + :custom + (venv-location "~/.config/virtualenvs") + + :config + (venv-initialize-interactive-shells) + (venv-initialize-eshell)) #+end_src +** ledger #+begin_src emacs-lisp - (add-hook 'mpc-mode-hook #'za/mpc-mode-settings) + (use-package ledger-mode + :mode ("\\.ledger\\'") + :hook (ledger-mode . company-mode) + :custom + (ledger-clear-whole-transactions t) + (ledger-reconcile-default-commodity "eur") + (ledger-reports + '(("budget-last-month" "%(binary) -f %(ledger-file) --start-of-week=1 --period \"last month\" budget ^expenses") + ("budget-this-month" "%(binary) -f %(ledger-file) --start-of-week=1 --period \"this month\" budget ^expenses") + ("expenses-this-month-vs-budget" "%(binary) -f %(ledger-file) --start-of-week=1 --period \"this month\" --period-sort \"(amount)\" bal ^expenses --budget") + ("expenses-last-month-vs-budget" "%(binary) -f %(ledger-file) --start-of-week=1 --period \"last month\" --period-sort \"(amount)\" bal ^expenses --budget") + ("expenses-this-month" "%(binary) -f %(ledger-file) --start-of-week=1 --period \"this month\" --period-sort \"(amount)\" bal ^income ^expenses -X eur") + ("expenses-last-month" "%(binary) -f %(ledger-file) --start-of-week=1 --period \"last month\" --period-sort \"(amount)\" bal ^expenses -X eur") + ("expenses-this-month" "%(binary) -f %(ledger-file) --start-of-week=1 --period \"this month\" --period-sort \"(amount)\" bal ^expenses -X eur") + ("expenses-vs-income-this-month" "%(binary) -f %(ledger-file) --start-of-week=1 --effective --period \"this month\" --period-sort \"(amount)\" bal ^income ^expenses -X eur") + ("expenses-vs-income-last-month" "%(binary) -f %(ledger-file) --start-of-week=1 --effective --period \"last month\" --period-sort \"(amount)\" bal ^expenses ^income -X eur") + ("bal-assets-czk" "%(binary) -f %(ledger-file) --start-of-week=1 bal Assets Liabilities -X czk") + ("bal-assets" "%(binary) -f %(ledger-file) --start-of-week=1 bal Assets Liabilities") + ("bal" "%(binary) -f %(ledger-file) --start-of-week=1 bal -B") + ("bal-assets-eur" "%(binary) -f %(ledger-file) --start-of-week=1 bal Assets Liabilities -X eur") + ("monthly-balance-abn-checking" "%(binary) -f %(ledger-file) --start-of-week=1 --effective reg --monthly 'Assets:ABN Checking'") + ("monthly-expenses" "%(binary) -f %(ledger-file) --monthly register ^expenses --collapse -X eur") + ("reg" "%(binary) -f %(ledger-file) --start-of-week=1 reg") + ("payee" "%(binary) -f %(ledger-file) --start-of-week=1 reg @%(payee)") + ("account" "%(binary) -f %(ledger-file) --start-of-week=1 reg %(account)") + ("degiro-changes" "%(binary) -f %(ledger-file) --start-of-week=1 reg Investments:Degiro -X eur")))) #+end_src -Unfortunately the lambda keybindings don't show up documented properly, but oh well. That's a minor problem. +org-capture lets me add transactions from anywhere in Emacs: -* Radio -Just a wrapper function to my radio script: +Budget throws an error when there's multiple commodities involved. +See discussion here: https://github.com/ledger/ledger/issues/1450#issuecomment-390067165 #+begin_src emacs-lisp - (defun radio () - "Play an internet radio" - (interactive) - (ansi-term "radio" "*radio*")) + (defconst za/ledger-budget-fix-string + "-X eur -F '%(justify(scrub(get_at(display_total, 0)), 20, -1, true, false)) %(justify(get_at(display_total, 1) ? -scrub(get_at(display_total, 1)) : 0.0, 20, 20 + 1 + 20, true, false)) %(justify(get_at(display_total, 1) ? (get_at(display_total, 0) ? -(scrub(get_at(display_total, 1) + get_at(display_total, 0))) : -(scrub(get_at(display_total, 1)))) : -(scrub(get_at(display_total, 0))), 20, 20 + 1 + 20 + 1 + 20, true, false))%(get_at(display_total, 1) and (abs(quantity(scrub(get_at(display_total, 0))) / quantity(scrub(get_at(display_total, 1)))) >= 1) ? \" \" : \" \")%(justify((get_at(display_total, 1) ? (100% * (get_at(display_total, 0) ? scrub(get_at(display_total, 0)) : 0.0)) / -scrub(get_at(display_total, 1)) : \"na\"), 5, -1, true, false)) %(!options.flat ? depth_spacer : \"\")%-(partial_account(options.flat))\n%/%$2 %$3 %$4 %$6\n%/%(prepend_width ? \" \" * int(prepend_width) : \"\") ---------------- ---------------- ---------------- -----\n'" + "Append this to a ledger budget to fix errors with multiple commodities.") #+end_src -* Dired -'i' expands subdirs, so I want to be able to close them too. - +** Notmuch #+begin_src emacs-lisp - (define-key dired-mode-map (kbd "M-k") #'dired-kill-subdir) + (use-package notmuch + :custom + (notmuch-saved-searches + `((:name "inbox: personal" :query ,(format "folder:/%s/ tag:inbox" za/email-personal) :key ,(kbd "ip") :search-type 'tree) + (:name "inbox: school" :query ,(format "folder:/%s/ tag:inbox" za/email-vu) :key ,(kbd "is") :search-type 'tree) + (:name "archive: personal" :query ,(format "folder:/%s/ tag:archive" za/email-personal) :key ,(kbd "ap") :search-type 'tree) + (:name "archive: school" :query ,(format "folder:/%s/ tag:archive" za/email-vu) :key ,(kbd "as") :search-type 'tree)) + "Define some saved searches (i.e. mailboxes)") + (notmuch-hello-sections + '(notmuch-hello-insert-header + notmuch-hello-insert-saved-searches + notmuch-hello-insert-search + notmuch-hello-insert-alltags + notmuch-hello-insert-footer) + "Define the main screen sections") + (notmuch-search-oldest-first nil "Show newest mail first") + (notmuch-archive-tags '("-inbox" "+archive")) + (notmuch-show-mark-unread-tags '("+unread")) + (notmuch-delete-tags '("-inbox" "+trash")) + (notmuch-tagging-keys '(("a" notmuch-archive-tags "Archive") + ("r" notmuch-show-mark-read-tags "Mark read") + ("u" notmuch-show-mark-unread-tags "Mark unread") + ("d" notmuch-delete-tags "Delete"))) + + :bind ("C-c m" . notmuch) + ;; Run notmuch-hook script on hello refresh, to move messages to + ;; folders according to their tags: + :hook (notmuch-hello-refresh . za/notmuch-hook-tags2folders) + :config + (defun za/notmuch-hook-tags2folders () + "Run notmuch-hook to organise email in folders based on tags." + (start-process "notmuch-hook" nil "notmuch-hook" "tags2folders"))) #+end_src -Set up listing display: +** MPC +#+begin_src emacs-lisp + (use-package mpc + :custom + (mpc-browser-tags '(AlbumArtist Album Genre Playlist) + "Set the windows I want to show") + + :bind (:map mpc-mode-map + ("a" . mpc-playlist-add) + ("P" . mpc-playlist) + ("x" . mpc-playlist-delete) + ("p" . mpc-toggle-play) + ("t" . mpc-select-toggle) + ("f" . za/mpc-seek-forward-20-seconds) + ("b" . za/mpc-seek-backward-20-seconds)) + :config + (defun za/mpc-seek-forward-20-seconds () + "Seek forward 20 seconds" + (interactive) + (mpc-seek-current "+20")) + (defun za/mpc-seek-backward-20-seconds () + "Seek backward 20 seconds" + (interactive) + (mpc-seek-current "-20"))) +#+end_src +** Dired #+begin_src emacs-lisp - (setq-default dired-listing-switches "-alhv") + (use-package dired + :ensure nil ; installed with Emacs + :bind (:map dired-mode-map + ;; 'i' expands subdirs, so I want to be able to close them too. + ("M-k" . dired-kill-subdir)) + :custom + (dired-listing-switches "-alhv") + (dired-dwim-target t "If I have another dired window open, use that as target") + ;; By default, hide details (show again by pressing oparen): + :hook (dired-mode . dired-hide-details-mode)) #+end_src -By default, hide details (show again by pressing oparen): - +** ess: statistics (R, SAS...) #+begin_src emacs-lisp - (add-hook 'dired-mode-hook #'dired-hide-details-mode) + (use-package ess) #+end_src -If I have another dired window open, use that as target: - +** help mode #+begin_src emacs-lisp - (setq dired-dwim-target t) + (use-package help-mode + :ensure nil ; included with Emacs + :hook (help-mode . za/settings-on-help-mode) + :config + (defun za/settings-on-help-mode () + "Settings on enabling help mode" + (za/toggle-wrap t))) #+end_src +** helpful +An alternative to the built-in Emacs help that provides much more contextual information. +I use counsel, so I use the keybindings in [[*counsel + ivy + swiper]]. +I just augment the functions counsel uses. +Also, counsel doesn't provide some keybindings that I can get from helpful. -* Syncthing -Some functions to start/stop syncthing. #+begin_src emacs-lisp - (defconst za/st-buffer-name "*syncthing*" "Buffer name for the syncthing process.") - (defun za/st () - "Start syncthing" - (interactive) - (if (get-buffer-process za/st-buffer-name) - (user-error "Syncthing is already running.")) - (async-shell-command "syncthing serve --no-browser" za/st-buffer-name)) + (use-package helpful + :custom + (counsel-describe-symbol-function #'helpful-symbol) + (counsel-describe-function-function #'helpful-callable) + (counsel-describe-variable-function #'helpful-variable) - (defun za/st-kill () - "Stop syncthing" - (interactive) - (unless (get-buffer-process za/st-buffer-name) - (user-error "Syncthing is not running.")) - (async-shell-command "syncthing cli operations shutdown")) -#+end_src + :bind (("C-h k" . helpful-key) + ("C-h C" . helpful-command) + :map helpful-mode-map + ("l" . za/helpful-previous) + ("r" . za/helpful-next)) + + :hook (helpful-mode . za/settings-on-helpful-mode) + :config + (defun za/settings-on-helpful-mode () + "Settings on enabling helpful mode" + (za/toggle-wrap t)) + + ;; Then, a way to jump forward and backward in the window: + (defvar za/helpful-buffer-ring-size 20 + "How many buffers are stored for use with `helpful-next'.") + + (defvar za/helpful--buffer-ring (make-ring za/helpful-buffer-ring-size) + "Ring that stores the current Helpful buffer history.") + + (defun za/helpful--buffer-index (&optional buffer) + "If BUFFER is a Helpful buffer, return it’s index in the buffer ring." + (let ((buf (or buffer (current-buffer)))) + (and (eq (buffer-local-value 'major-mode buf) 'helpful-mode) + (seq-position (ring-elements za/helpful--buffer-ring) buf #'eq)))) + + (defun za/helpful--new-buffer-a (help-buf) + "Update the buffer ring according to the current buffer and HELP-BUF." + :filter-return #'helpful--buffer + (let ((buf-ring za/helpful--buffer-ring)) + (let ((newer-buffers (or (za/helpful--buffer-index) 0))) + (dotimes (_ newer-buffers) (ring-remove buf-ring 0))) + (when (/= (ring-size buf-ring) za/helpful-buffer-ring-size) + (ring-resize buf-ring za/helpful-buffer-ring-size)) + (ring-insert buf-ring help-buf))) + + (advice-add #'helpful--buffer :filter-return #'za/helpful--new-buffer-a) + + (defun za/helpful--next (&optional buffer) + "Return the next live Helpful buffer relative to BUFFER." + (let ((buf-ring za/helpful--buffer-ring) + (index (or (za/helpful--buffer-index buffer) -1))) + (cl-block nil + (while (> index 0) + (cl-decf index) + (let ((buf (ring-ref buf-ring index))) + (if (buffer-live-p buf) (cl-return buf))) + (ring-remove buf-ring index))))) + + + (defun za/helpful--previous (&optional buffer) + "Return the previous live Helpful buffer relative to BUFFER." + (let ((buf-ring za/helpful--buffer-ring) + (index (1+ (or (za/helpful--buffer-index buffer) -1)))) + (cl-block nil + (while (< index (ring-length buf-ring)) + (let ((buf (ring-ref buf-ring index))) + (if (buffer-live-p buf) (cl-return buf))) + (ring-remove buf-ring index))))) + + (defun za/helpful-next () + "Go to the next Helpful buffer." + (interactive) + (when-let (buf (za/helpful--next)) + (funcall helpful-switch-buffer-function buf))) + + (defun za/helpful-previous () + "Go to the previous Helpful buffer." + (interactive) + (when-let (buf (za/helpful--previous)) + (funcall helpful-switch-buffer-function buf)))) +#+end_src * References Here's a list of good articles I encountered about configging emacs: - [[https://karthinks.com/software/batteries-included-with-emacs/][Batteries included with Emacs]]