Skip to content

Instantly share code, notes, and snippets.

@DivineDominion
Last active September 24, 2023 12:15
Show Gist options
  • Save DivineDominion/e15c152f2fad785f4e1167b9a4df548b to your computer and use it in GitHub Desktop.
Save DivineDominion/e15c152f2fad785f4e1167b9a4df548b to your computer and use it in GitHub Desktop.
Emacs init.el from a literal org file

Early Init

The macOS-adjusted early-init.el approach is taken from https://raw.githubusercontent.com/dylanjm/teton/master/config/emacs/dotemacs.org

These settings are loaded before anything else and are intended to speed up all the actual package loading and configuration options.

early-init.el header

Garbage Collection Fix during init

Effectively disables garbage collection with the memory limits set for Emacs ages ago. Speeds up loading all of the actual code and packages in the real init file.

(defvar default-file-name-handler-alist file-name-handler-alist)

;; The default is 800 kilobytes.  Measured in bytes.
(defvar extended-gc-cons-threshold most-positive-fixnum)
(defvar default-gc-cons-threshold (* 100 1024 1024))

;; Native Compilation Vars
(setq-default native-comp-speed 2
              native-comp-deferred-compilation t)

;; Prevents libgccjit error
;; Solution found at: https://github.com/d12frosted/homebrew-emacs-plus/issues/323
(setenv "LIBRARY_PATH" "/opt/homebrew/opt/gcc/lib/gcc/11:/opt/homebrew/opt/libgccjit/lib/gcc/11:/opt/homebrew/opt/gcc/lib/gcc/11/gcc/aarch64-apple-darwin20/11.1.0:/usr/local/opt/gcc/lib/gcc/11:/usr/local/opt/libgccjit/lib/gcc/11:/usr/local/opt/gcc/lib/gcc/11/gcc/x86_64-apple-darwin20/11.2.0")

(setq-default auto-window-vscroll nil
              bidi-display-reordering 'left-to-right
              bidi-paragraph-direction 'left-to-right
              frame-inhibit-implied-resize t
              inhibit-default-init t
              site-run-file nil
              load-prefer-newer t
              read-process-output-max (* 1024 1024 3))

(setq file-name-handler-alist nil
      package-enable-at-startup nil
      gc-cons-threshold extended-gc-cons-threshold)

(defun arco/return-gc-to-default ()
  (setq-default gc-cons-threshold default-gc-cons-threshold
                load-prefer-newer nil))

(defun arco/reset-file-handler-alist-h ()
  (dolist (handler file-name-handler-alist)
    (add-to-list 'default-file-name-handler-alist handler))
  (setq file-name-handler-alist default-file-name-handler-alist))

(add-hook 'after-init-hook #'arco/reset-file-handler-alist-h)
(add-hook 'after-init-hook #'arco/return-gc-to-default)
(advice-add #'package--ensure-init-file :override #'ignore)

Main Font and Frame Settings

(defun ct/use-face (font height weight)
  (let ((the-font (format "%s-%d" font height)))
    (message the-font)
    (setq default-frame-alist
          (append (list
	               `(font . ,the-font)
	               '(min-height . 1)  '(height     . 45)
	               '(min-width  . 1)  '(width      . 101)
                   '(vertical-scroll-bars . nil)
                   '(internal-border-width . 24) ;; frame padding around the text
                   '(left-fringe    . 0)
                   '(right-fringe   . 0)
                   '(ns-transparent-titlebar . t)
                   '(menu-bar-lines . 0)
                   '(tool-bar-lines . 0)))))
  (set-face-attribute 'default nil
                      :font font
                      :weight weight
                      :height (* height 10))
  (set-face-attribute 'variable-pitch nil
                      :font "SF Pro Display"
                      :height (* height 10))
  (set-face-attribute 'fixed-pitch nil
                      :font font
                      :height (* height 10)))
(defun ct/use-medium-face ()
  "Use SF Mono Medium 18pt, for smaller screens"
  (interactive)
  (ct/use-face "JetBrains Mono" 18 'medium)
  (setq-default line-spacing .3))
(defun ct/use-regular-face ()
  "Use SF Mono Regular 22pt, for larger screens."
  (interactive)
  (ct/use-face "JetBrains Mono" 22 'light)
  (setq-default line-spacing .2))

(defun ct/toggle-face ()
  "Switch between large and small font settings."
  (interactive)
  (let ((font-height (/ (face-attribute 'default :height) 10)))
    (if (< font-height 20)
        (ct/use-regular-face)
      (ct/use-medium-face))))
(global-set-key (kbd "<f6>") #'ct/toggle-face)

(ct/use-regular-face)

;; Map SF Symbols to a compatible font
(set-fontset-font t '(?􀀀 . ?􏿽) "SF Pro Display")

Debugging

Profiling

When something takes suspiciously long, it’s easy to find out what happens with the built-in profiler:

  • M-x profiler-start, select CPU usage
  • scroll in the big file for a while
  • M-x profiler-stop
  • M-x profiler-report to show the measurements
  • for a clean session: M-x profiler-reset

Debugging

Enable some debugging flags to help with package setup and maintenance.

(setq debug-on-error nil)

early-init.el footer

Meta-Initialization

Original by Lars Tveito https://github.com/larstvei/dot-emacs/blob/master/init.org using vanilla emacs function to auto-tangle.

For the first run, use C-c C-v t to run org-babel-tangle to produce the .el file.

;;; -*- lexical-binding: t -*-

The .el file will be overwritten by the tangled version of this file. Only ever change the org file.

I want lexical scoping for the init-file, which can be specified in the header. The first line of the configuration is as follows:

(defun tangle-init ()
  "If the current buffer is 'init.org' the code-blocks are
tangled, and the tangled file is compiled."
  (when (equal (buffer-file-name)
               (file-truename (expand-file-name (file-name-concat user-emacs-directory "00-main.org"))))
    ;; Avoid running hooks when tangling.
    (let ((prog-mode-hook nil))
      (org-babel-tangle "00-main.org" "00-main.el")
      (byte-compile-file (file-name-concat user-emacs-directory "00-main.el")))))

(add-hook 'after-save-hook 'tangle-init)

org-auto-tangle

Asynchronous auto-tangling when you add #+auto_tangle: t to the file properties at the top. Bonus: it doesn’t shove the compilation window in my face!

2021-02-17: Says it tangles, but I don’t see no tangling.

(use-package org-auto-tangle
 :defer t
 :hook (org-mode . org-auto-tangle-mode))

Global Package Settings

use-package explained

I often forget stuff pertaining package configurations, and get confused when which part of the config is actually going to be run.

  • use :config to set up configurations that are lazily loaded
  • use :init to set up stuff that’s eagerly loaded, like (foo-mode +1) enablement
(use-package PACKAGE_NAME
  ;; when executing, load from ELPA and install
  :demand

  ;; Some packages don't expose (all) runnable commands; fix these
  ;; so they are available in :init
  :commands (command1 command2 ...)

  ;; hide mode line/lighter entry; explained below when the =delight= package is used
  :delight (foo-mode1 foo-mode2 ...)

  :init
  ;; eagerly executed commands; no need to group via progn

  :config
  ;; lazily executed after loading; no need to group via progn

  ;; key bindings -- note that these ARE grouped in lists for globals and maps!
  :bind
  (("C-f C-o C-o" . global-foo-command)
   ("M-a b c" . global-bar-command))
  (:map some-key-map
   ("C-i" . map-relative-fizz-command))

  ;; Define hooks; doesn't have to be hooks for this module, can also be
  ;; built-in hooks executing functions of this module
  :hook
  (after-init-hook . foo-bar-auto-setup)
  (foo-bar-mode-hook . hook-function-name))

Ensure all used packages are installed

(require 'use-package-ensure)
(setq use-package-always-ensure t)

Mode line settings

(setq-default mode-line-format
      '("%e"
        mode-line-front-space ;; XFK inserts mode info here
        mode-line-frame-identification
        mode-line-buffer-identification
        " " ;; Sometimes this touches in magit buffers
        mode-line-position
        " "
        mode-line-modes ;; to just show the major mode, use `mode-name'
        "%n"  ;; narrowing
        mode-line-misc-info
        mode-line-end-spaces))

delight: hide mode names from modeline/lighter

With delight, you can replace the minor mode identification string with something else. Configure per-package using :delight. Works like diminish, but also on dynamic mode line items, like projectile project names. So it overall works more reliably.

(use-package delight
  :demand)

Usage examples because I always forget how to chain delight config statements (TL;DR: just throw each on a line, no need to make a list):

;; Built-in modes that needn't be loaded:
(use-package emacs
  :delight
  (foo-bar-mode)          ;; de-light completely
  (fizz-buzz-mode " FB")) ;; replace with "FB"

;; Combine package loading with configuration
(use-package foo-bar
  :demand
  :delight ;; hide completely
  :config (foo-config))

Reinstall packages macro

https://emacsredux.com/blog/2020/09/12/reinstalling-emacs-packages/

  • Deactivate first
  • Ignore errors (to avoid stopping the update when dependencies would be violated)
(defun ct/reinstall-package (pkg)
  (interactive (list (intern (completing-read "Reinstall package: " (mapcar #'car package-alist)))))
  (ignore-errors
    (unload-feature pkg t))
  (package-reinstall pkg)
  (require pkg))

try: test packages without installing them in .emacs.d

(use-package try :ensure t)

Filter in package-list-packages for updateable items

via https://emacs.stackexchange.com/a/31874/18986

(defun package-menu-find-marks ()
  "Find packages marked for action in *Packages*."
  (interactive)
  (occur "^[A-Z]"))

;; Only in Emacs 25.1+
(defun ct/package-menu-filter-by-status (status)
  "Filter the *Packages* buffer by status."
  (interactive
   (list (completing-read
          "Status: " '("new" "installed" "dependency" "obsolete"))))
  (package-menu-filter-by-keyword (concat "status:" status)))

(define-key package-menu-mode-map "s" #'ct/package-menu-filter-by-status)
(define-key package-menu-mode-map "a" #'package-menu-find-marks)

save-hist to preserve history for auto-completion and file finding

(use-package savehist
  :ensure t
  :hook (after-init . savehist-mode))

OS specific settings

Environment Variables

(use-package exec-path-from-shell
  :if (memq window-system '(mac ns x))
  :init
  (setq exec-path-from-shell-check-startup-files nil
        exec-path-from-shell-variables '("SHELL" "PATH" "MANPATH")
        exec-path-from-shell-arguments '("-l"))
  (exec-path-from-shell-initialize))

Open in Finder

Reveals the current file in Finder, like Cmd+Shift+R does in a lot of macOS apps.

(use-package reveal-in-osx-finder
  :if (memq window-system '(mac ns))
  :ensure t
  :config (global-set-key (kbd "C-c z") 'reveal-in-osx-finder))

Do not use ls --dired on macOS

(use-package emacs
  :if (memq window-system '(mac ns))
  :config
  (setq dired-use-ls-dired nil))

Show in full-screen but without all the OS chrome

Shared settings between Mac and Linux:

(use-package emacs
  :if (memq window-system '(mac ns x))
  :config
  (setq mouse-wheel-scroll-amount '(1 ((shift) . hscroll) ((control)) ((meta) . 5))
        ;; Populate kill buffer from clipboard
        select-enable-clipboard t
        frame-resize-pixelwise t

        ;; Make non-native full-screen on macOS cover the menu bar; native fullscreen glitches into 2 workspaces when composing email
        ns-use-native-fullscreen nil)

  ;; When toggling fullscreen, ditch all existing posframes that would linger
  (advice-add #'toggle-frame-fullscreen
              :after
              (lambda (&rest r) (posframe-delete-all)))

  ;; Disable window chrome eagerly; using it as late as `after-init-hook` produces glitches.
  (set-frame-position nil 0 0)
  (tool-bar-mode 0)
  (scroll-bar-mode 0))

macOS Keyboard Shortcuts

(use-package emacs
  :if (memq window-system '(mac ns))
  :config
  (setq ns-alternate-modifier 'meta
        mac-option-modifier 'meta
        ns-right-alternate-modifier nil
        mac-right-option-modifier nil  ; Pass-through to regular option key
        ns-command-modifier 'super
        mac-command-modifier 'super
        ns-right-command-modifier 'hyper
        mac-right-command-modifier 'hyper
        ns-confirm-quit t))

Open frontmost Finder window in dired

Exposes ct/dired-finder-path as an interactive function on macOS to open Finder’s frontmost window’s location in dired.

(when (memq window-system '(mac ns))
  (defun ct/finder-path ()
    "Return path of the frontmost Finder window, or the empty string.

  Asks Finder for the path using AppleScript via `osascript', so
    this can take a second or two to execute."
    (let ($applescript $result)
      ;; Script via:  https://brettterpstra.com/2013/02/09/quick-tip-jumping-to-the-finder-location-in-terminal/
      (setq $applescript "tell application \"Finder\" to if (count of Finder windows) > 0 then get POSIX path of (target of front Finder window as text)")
      (setq $result (ns-do-applescript $applescript))
      (if $result
          (string-trim $result)
        "")))
  (defun ct/dired-finder-path ()
    (interactive)
    (let (($path (ct/finder-path)))
      (if (string-equal "" $path)
          (message "No Finder window found.")
        (dired $path)))))

sudo access

Helpers to reopen file with privileges and execute commands as super user.

(use-package sudo-edit
  :ensure)

macOS keychain access

Keychain access via the security command line tool by Brad Wright:

(use-package emacs
  :init
  (defun bw/chomp (str)
    "Chomp leading and tailing whitespace from `str'."
    (while (string-match "\\`\n+\\|^\\s-+\\|\\s-+$\\|\n+\\'" str)
      (setq str (replace-match "" t t str))) str)

  (defun ct/get-keychain-password (service-name)
    "Get SERVICE-NAME password from macOS Keychain.
Return nil if password was not found."
    (interactive "sService name: ")
    (when (executable-find "security")
      (let ((password))
        (setq password (bw/chomp
                        (shell-command-to-string
                         (concat "security find-generic-password -ws " service-name " 2>/dev/null"))))
        (if (string-empty-p password)
            nil
          password)))))

macOS File Descriptor Limit problem

There’s apparently no way for the Emacs-mac fork to bypass the limit of 1024 file descriptors.

I don’t know why so many file descriptors and watches accumulate anyway.

To close them, use this Emacs 29 code via Ben Simon:

(defun file-notify-rm-all-watches ()
  "Remove all existing file notification watches from Emacs."
  (interactive)
  (maphash
   (lambda (key _value)
     (file-notify-rm-watch key))
   file-notify-descriptors))

Theme & Eye candy

Line height

Line spacing actually adds spacing below the line; that means at 2x line height, the text would be stuck to the top and 1x line height would be added below. That means line backgrounds only extend downwards instead of vertically centering the text in the background-colored region.

The only “fix” for this seems to be to patch your font with more vertical spacing at the top :(

See https://stackoverflow.com/questions/26437034/emacs-line-height

I find 1.2x line height to not be as ugly as to be noticeable.

(use-package emacs
  :config
  (setq-default line-spacing .2)

  (defun ct/disable-local-line-spacing ()
    "Disable line spacing buffer-local, used for hooks."
    (setq-local line-spacing 0)))

Modus Themes: Modus Vivendi (dark) / Modus Operandi (light)

Theme configuration

These themes by Protesilaos Stavrou are based on accessibility standards to maximize contrast. Aka, they should be easy to read at all times. But with the relatively stark contrast between text and background colors, it’s probably not that easy on the eye. That’s what Solarized is for.

(use-package modus-themes
  :ensure
  :demand
  :init
  (require 'modus-themes)

  (setq modus-themes-to-toggle '(modus-operandi-deuteranopia modus-vivendi-deuteranopia))
  (setq modus-themes-italic-constructs t
	    modus-themes-bold-constructs t
	    modus-themes-variable-pitch-ui t
	    modus-themes-mixed-fonts t)

  ;; Theme overrides
  (customize-set-variable 'modus-themes-common-palette-overrides
		                  `(
		                    ;; Make the mode-line borderless
		                    (bg-mode-line-active bg-inactive)
		                    (fg-mode-line-active fg-main)
		                    (bg-mode-line-inactive bg-inactive)
		                    (fg-mode-line-active fg-dim)
		                    (border-mode-line-active bg-inactive)
		                    (border-mode-line-inactive bg-main)

		                    ;; macOS Selection colors
                            (bg-region "mac:selectedTextBackgroundColor")
                            (fg-region "mac:selectedTextColor")
                            ))
  ;; Inherit rest from faint style
  ;; ,@modus-themes-preset-overrides-faint))
  (customize-set-variable 'modus-operandi-deuteranopia-palette-overrides
	                      `(
		                    ;; More subtle gray for the inactive window and modeline
		                    (bg-inactive "#efefef")))
  (customize-set-variable 'modus-vivendi-deuteranopia-palette-overrides
	                      `(
		                    ;; More subtle gray for the inactive window and modeline
		                    (bg-inactive "#202020")))

  ;; Color customizations
  (setq modus-themes-prompts '(bold))
  (setq modus-themes-completions nil)
  (setq modus-themes-org-blocks 'gray-background)

  ;; Font sizes for titles and headings, including org
  (setq modus-themes-headings '((1 . (light variable-pitch 1.5))
                                (agenda-date . (1.3))
                                (agenda-structure . (variable-pitch light 1.8))
						        (t . (medium))))

  ;; By default, the ~org-mode~ TODO/NEXT/DONE faces look super ugly.
  ;; This adjusts the colors to the modus themes.
  ;; (setq org-todo-keyword-faces
  ;;       '(("TODO" . modus-themes-refine-blue)
  ;;         ("DOING" . (:inherit modus-themes-intense-yellow :weight bold))
  ;;         ("NEXT" . modus-themes-active-blue)
  ;;         ("WAITING" . modus-themes-intense-red)
  ;;         ("CANCELLED" . modus-themes-hl-line)
  ;;         ("HOLD" . modus-themes-intense-neutral)
  ;;         ("PROJECT" . (:inherit modus-themes-mark-symbol :weight light))
  ;;         ("DONE" . modus-themes-refine-green)))

  ;; Don't load immediately, but in after-init-hook so all other modifications further down can be prepared
  (defun ct/modus-themes-init ()
    (load-theme (car modus-themes-to-toggle)))

  :bind ("<f5>" . modus-themes-toggle)
  :hook (after-init . ct/modus-themes-init))

Org color examples

PROJECT project

todo

DOING doing

NEXT next

WAITING waiting

done

HOLD hold

CANCELLED cancelled

Window divider to accompany the suble modeline

(setq window-divider-default-right-width 2
      window-divider-default-bottom-width 2
      window-divider-default-places t)
(window-divider-mode)

mode-line customizations for variable pitch, lighter Modeline

While Emacs 29 is “experimental”, Prot will keep the variable mode-line settings as an opt-in feature. https://protesilaos.com/codelog/2021-12-02-note-modus-emacs-29/

To automatically inherit from the default Emacs mode-line on Emacs 29, implement something like:

(defun ct/modus-themes-customize-mode-line ()
  "Apply padding to mode-line via box that has the background color"
  (modus-themes-with-colors
    (custom-set-faces
     `(mode-line ((,c :box (:line-width 10 :color ,bg-mode-line-active))))
     `(mode-line-inactive ((,c :box (:line-width 10 :color ,bg-mode-line-inactive)))))))
(add-hook 'modus-themes-after-load-theme-hook #'ct/modus-themes-customize-mode-line)

command-log faces

The default command-log colors are too bright and have next to no contrast when the log window is in “disabled” background mode.

(defun ct/modus-themes-customize-command-log-faces ()
  (modus-themes-with-colors
    (custom-set-faces
     `(command-log-command ((,c :foreground ,fg-alt :background ,bg-dim))
     `(command-log-key ((,c :foreground ,fg-main :background ,bg-active)))))))
(when (package-installed-p 'command-log-mode)
  (add-hook 'modus-themes-after-load-theme-hook #'ct/modus-themes-customize-command-log-faces))

lin-mode changes line selection colors

Prot’s own lin-mode extends hl-line-mode to override the style of per-line seleciton interfaces, like consult, notmuch, org-agends, i.e. where you inherently operate on per-line content.

This frees hl-line-mode colors to apply to highlighting in text documents again.

(use-package lin
  :config
  (customize-set-variable 'lin-face 'lin-mac-override-fg)

  (require 'cl-seq)
  (customize-set-variable
   'lin-mode-hooks
   (cl-union lin-mode-hooks
             '(dired-sidebar-mode-hook
               dired-mode-hook
               magit-status-mode-hook
               magit-log-mode-hook)))

  (defun ct/neotree-lin-mode-hook ()
    "Prioritizes hl-line faces over neotree's before enabling lin."
    (hl-line-mode -1)
    (setq-local hl-line-overlay-priority +50)
    (lin-mode))
  (add-hook 'neotree-mode-hook #'ct/neotree-lin-mode-hook))

tab-bar color overrides so it’s less noise up there

(defun ct/modus-themes-tab-bar-colors ()
  "Override tab faces to have even less variety"
  (modus-themes-with-colors
      (custom-set-faces
       `(tab-bar ((,c
                   :height 0.8
                   :background ,bg-main
                   :box nil)))
       `(tab-bar-tab ((,c
                       :background ,bg-main
                       :underline (:color ,blue-intense :style line)
                       :box (:line-width 2 :style flat-button))))
      `(tab-bar-tab-inactive ((,c
                               :background ,bg-main
                               :box (:line-width 2 :style flat-button)))))))
(add-hook 'modus-themes-after-load-theme-hook #'ct/modus-themes-tab-bar-colors)

Emoji replacements for :) and <3 etc

Mapping actual emoji characters to a monochrome font that shows their outline:

;; Map Emoji to a monochrome Emoji outline font
(set-fontset-font t 'emoji "Noto Emoji")

This doesn’t insert the actual images in the text, but replaces :) and -_- and <3 etc. on the fly with an image. Useful to add visual hooks to plain text documents and org-agenda.

(use-package emojify
  :ensure t
  :config
  (setq emojify-emoji-set "openmoji-v13.0")
  (defun ct/disable-emojify-mode-in-prog-mode ()
    (emojify-mode -1))

  :init
  ;; (global-emojify-mode +1)
  (add-hook 'prog-mode-hook #'ct/disable-emojify-mode-in-prog-mode))

FontAwesome and other SVG icons

When all-the-icons produces chinese characters, that might be due to font precedence rules.

(use-package all-the-icons
  :delight
  :demand)
(use-package all-the-icons-dired
  :load-path "~/.emacs.d/src/all-the-icons-dired"
  :delight
  :after all-the-icons
  :demand
  :config
  (setq all-the-icons-dired-monochrome nil)
  :hook (dired-mode . all-the-icons-dired-mode))
(use-package all-the-icons-ibuffer
  :delight
  :after all-the-icons
  :demand
  :hook (ibuffer-mode . all-the-icons-ibuffer-mode))
(use-package all-the-icons-completion
  :delight
  :after all-the-icons
  :demand
  :hook (marginalia-mode . all-the-icons-completion-marginalia-setup))

Emacs Lisp

File path helpers

(defun ct/file-join (&rest args)
  "Joins ARGS into an absolute path using `expand-file-name`."
  ;; expand-file-name expects arg1 to be relative, arg2 to be base, so we need to reverse the order
  (seq-reduce (lambda (memo next) (expand-file-name next memo))
              (cdr args) (car args)))

Pretty print last expression

The vanilla key binding C-x C-e executes eval-last-sexp by default: if you place the point at the end of a line after an elisp expression, it’ll execute that and print the output in the minibuffer, unformatted.

Karthik Chikmagalur suggests to replace this with pp-eval-last-sexp. That will open a *Pp Output Buffer* to display a formatted result.

(global-set-key [remap eval-last-sexp] 'pp-eval-last-sexp)

With Xah-Fly-Keys, use a separate binding instead:

(with-eval-after-load "xah-fly-keys"
  ;; effectively <SPC , w>
  (define-key 'xah-fly-w-keymap (kbd "w") #'pp-eval-last-sexp))

Stub definition of function at point

Andrea’s function lets you call ct/stub-elisp-defun to insert a defun based on the wishful-programmed code at point.

With point in any of these functions:

(call-the-rest (message-wishes-to-some-people (make-a-list-of-people))))

ct/stub-elisp-defun will work by stubbing a function definition for each.

;; https://ag91.github.io/blog/2020/12/31/top-down-elisping-a-simple-snippet-to-stub-a-function-while-your-are-designing-your-code/
(defun ct/stub-elisp-defun ()
  "Stub an elisp function from symbol at point."
  (interactive)
  (let* ((fun (thing-at-point 'list 'no-properties)))
    (when fun
      (let* ((fun-list (car (read-from-string fun)))
             (name (symbol-name (nth 0 fun-list)))
             (args (cdr fun-list)))
        (save-excursion
          (or (search-backward "(defun" nil 't) (goto-char (point-min)))
          (insert
           (s-concat
            "(defun "
            name
            " "
            (format "%s" (--map (s-concat "arg" (number-to-string it)) (number-sequence 1 (length args))))
            "\n  \"SomeDocs\"\n  nil)\n\n")))))))

Async

emacs-async allows file transfers from the local computer to a remote machine to be run in the background. When uploading video to my NAS, I found that copying over hundreds of MiB was halting my work in Emacs.

(use-package async
  :config
  (autoload 'dired-async-mode "dired-async.el" nil t)
  (dired-async-mode 1))

Window switching and buffers

Switching buffers programmatically and interactively should obey the same rules

Automatic (or programmatic) window switching obeys different rules than manual (or interactive) buffer switching, up until Emacs 27.1, where the switch-to-buffer-obey-display-actions option was introduced. This makes manual – via https://www.masteringemacs.org/article/demystifying-emacs-window-manager

(setq switch-to-buffer-obey-display-actions t
      switch-to-buffer-in-dedicated-window 'pop)

Switch windows back and forth

C-x o goes to the next window, C-x O (same but with shift held) goes to the previous one.

(global-set-key (kbd "C-x O") (lambda () (interactive) (other-window -1)))

Dedicating (“locking”) windows interactively

By default, there’s no interactive way to change window-dedicated-p. via https://www.masteringemacs.org/article/demystifying-emacs-window-manager

(defun ct/toggle-window-dedication ()
  "Toggles window dedication in the selected window."
  (interactive)
  (set-window-dedicated-p (selected-window)
                          (not (window-dedicated-p (selected-window))))
  ;; When run interactively, report the result.
  (if (and (interactive-p)
           (window-dedicated-p (selected-window)))
      (message "Enabled window dedication")
    (message "Disabled window dedication")))

(defun ct/set-window-no-delete-other-windows ()
  (interactive)
  (set-window-parameter (selected-window) 'no-delete-other-windows t))

Showing buffer in a side window

(defun ct/show-buffer-in-bottom-split ()
  (interactive)
  (let ((consult--buffer-display (lambda (buffer &rest args)
                                   (display-buffer-in-side-window buffer '((side . bottom) (slot . 0) (dedicated . t) (height . 10))))))
    (call-interactively #'consult-buffer)))

Protecting dedicated windows from being used for something else interactively

(add-to-list 'display-buffer-alist
             '("\\*Help\\*"
               (display-buffer-reuse-window display-buffer-pop-up-window)
               (inhibit-same-window . t)))

(defun ct/display-buffer-compilation-mode-p (buffer-name action)
  "Determine whether BUFFER-NAME is a compilation buffer."
  (with-current-buffer buffer-name
    (or (derived-mode-p 'compilation-mode)
        (string-match (rx (| "*[Cc]ompilation*"
                             "*Compile-Log*"))
                      buffer-name))))

;; Make compilation buffers share a window (if visible) and hide by default.
(add-to-list 'display-buffer-alist
             '(ct/display-buffer-compilation-mode-p
               (display-buffer-reuse-mode-window display-buffer-reuse-window display-buffer-no-window)
               (allow-no-window . t)))
               ;; (mode compilation-mode))) ; My matcher should suffice and this might be too specific for the Elisp Compile-Log
(with-eval-after-load 'notmuch
  (defun ct/display-buffer-notmuch-content-p (buffer-name action)
    "Matches all notmuch/mail related buffers to show them in the same tab"
    (with-current-buffer buffer-name
      (derived-mode-p 'notmuch-hello-mode 'notmuch-search-mode 'notmuch-show-mode 'notmuch-tree-mode 'notmuch-message-mode 'message-mode)))

  (add-to-list 'display-buffer-alist
               '(ct/display-buffer-notmuch-content-p
                 (display-buffer-in-tab display-buffer-reuse-window)
                 (ignore-current-tab . t)
                 (tab-name . "Mail"))))

Shortcut for me in case I need to reset this:

(setq display-buffer-alist nil)

And interactive changes:

(customize-variable 'display-buffer-alist)

Toggle sidebar visibility (e.g. neotree), so-called side windows”

Picking the Xcode default shortcut to show/hide the sidebar: Cmd-Opt-0

(global-set-key (kbd "M-s-0") #'window-toggle-side-windows)

Minibuffer configuration

Recursive minibuffers enables nesting commands:

(setq enable-recursive-minibuffers +1)

ace-window adds functions to target windows with single characters

Press ? to list dispatch actions

(use-package ace-window
  :ensure
  :demand
  :config
  ;; Enable selector even if only 1 window is present:
  (setq aw-dispatch-always t)
  ;; Default selectors to the home row
  (setq aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l))

  :bind (("C-o" . ace-window)))

Add bindings for XFK, if loaded:

(with-eval-after-load "ace-window"
  (with-eval-after-load "xah-fly-keys"
    (define-key xah-fly-leader-key-map (kbd "w") #'ace-window) ; xah-fly-comma-keymap
    (define-key xah-fly-command-map (kbd "6") #'ace-window)))  ; xah-fly-upcase-sentence

winner-mode allows undoing changes to the window layout

(use-package emacs
  :hook
  (after-init . winner-mode))

Zooming/Focusing in and out of windows via winner-undo

I used zoom-window to temporarily show window in full frame (aka temporarily “unsplit”).

Instead of zooming, I can use winner-mode’s C-c <left> to undo and C-c <right> to redo window changes.

When bound to e.g. the XFK leader key, I can unsplit everything with 1 key and undo the unsplitting with another.

Hide cursor in non-active window

(use-package emacs
  :config
  (setq cursor-in-non-selected-windows nil))

Go back and forward between visited buffers (browser-like navigation)

On macOS, ⌃⌘ + arrow keys usually does the trick. (Or the square brackets, but I don’t particularly like that binding.)

(global-set-key (kbd "C-s-<left>") #'previous-buffer)
(global-set-key (kbd "C-s-<right>") #'next-buffer)

Use ibuffer for the buffer list

(use-package ibuffer
  :ensure t
  :init
  ;; Rewrite all programmatic calls to `list-buffers`. Should work without this.
  ;(defalias 'list-buffers 'ibuffer-other-window)
  ;; Override `list-buffers` shortcut with ibuffer
  (global-set-key (kbd "C-x C-b") 'ibuffer-other-window))

Navigation shortcuts

Add org-mode-inspired shortcuts to jump between filter groups (like “headings”). The default bindings are <TAB> and <s-TAB>.

(define-key ibuffer-mode-map (kbd "C-c C-n") #'ibuffer-forward-filter-group)
(define-key ibuffer-mode-map (kbd "C-c C-p") #'ibuffer-backward-filter-group)

Custom buffer groups

Hide empty filter groups:

(setq ibuffer-show-empty-filter-groups t)

Define custom groups for buffers

(setq ibuffer-saved-filter-groups
      `(("default"
         ("mail" (or
                  (mode . message-mode)
                  (mode . notmuch-hello-mode)
                  (mode . notmuch-search-mode)
                  (mode . notmuch-message-mode)
                  (mode . notmuch-show-mode)
                  (mode . notmuch-tree-mode)
                  (mode . bbdb-mode)
                  (mode . mail-mode)
                  (mode . mu4e-main-mode)
                  (mode . gnus-group-mode)
                  (mode . gnus-summary-mode)
                  (mode . gnus-article-mode)
                  (name . "^\\..bdb$")))
         ("org" (or
                 (mode . org-agenda-mode)
                 (mode . diary-mode)
                 (name . "^\\*Calendar\\*$")
                 (name . "^diary$")
                 (filename . "Pending/org/")))
         ("dired" (mode . dired-mode))
         ("emacs" (or
                   (name . "^\\*package.*results\\*$")
                   (name . "^\\*Shell.*Output\\*$")
                   (name . "^\\*Compile-Log\\*$")
                   (name . "^\\*Completions\\*$")
                   (name . "^\\*Backtrace\\*$")
                   (name . "^\\*dashboard\\*$")
                   (name . "^\\*Messages\\*$")
                   (name . "^\\*scratch\\*$")
                   (name . "^\\*Appointment Alert\\*$")
                   (name . "^\\*info\\*$")
                   (name . "^\\*Help\\*$")))
         )))
(defun ct/ibuffer-enable-saved-filter-groups ()
  (ibuffer-switch-to-saved-filter-groups "default"))

(add-hook 'ibuffer-mode-hook #'ct/ibuffer-enable-saved-filter-groups)

Hide size column

;; Modify the default ibuffer column format
(setq ibuffer-formats
      '((mark modified read-only locked " "
	      (name 20 20 :left :elide)
	      " "
	      (mode 16 16 :left :elide)
	      " "
	      filename-and-process)
	(mark " "
	      (name 16 -1)
	      " " filename)))

Move focus to new window after manual splitting

(defun ct/split-window-below (arg)
  (interactive "P")
  (split-window-below arg)
  (other-window 1))
(defun ct/split-window-right (arg)
  (interactive "P")
  (split-window-right arg)
  (other-window 1))
(global-set-key [remap split-window-below] #'ct/split-window-below)
(global-set-key [remap split-window-right] #'ct/split-window-right)

Automatically dim buffers that don’t have focus

The “modus” themes support this out of the box: the background of all inactive buffer windows are dimmed. Not the inactive windows, but the inactive buffers. That means if you open 2 windows for the same buffer, both are using the light color; if you have 2 windows for 2 buffers, one is darkened.

(use-package auto-dim-other-buffers
  :ensure
  :defer
  :commands auto-dim-other-buffers-mode
  :config
  (setq auto-dim-other-buffers-dim-on-switch-to-minibuffer nil)
  (setq auto-dim-other-buffers-dim-on-focus-out t))

Prefer current window

I never know where “other window” is going to be.

Code from David Wilson that favors the current window”

(setq display-buffer-base-action
  '((display-buffer-reuse-window  ;; Without the `popper' settings, this full-screens the org calendar
     display-buffer-reuse-mode-window
     display-buffer-same-window
     display-buffer-in-previous-window)))

popper Demotes some buffers to temporary popup buffers that audo-hide

Then when the compilation window jumps at me, I can hit C-` to close it. Think of it like the Quake console, or the iTerm floating window.

There are tons of example options at: https://github.com/karthink/popper

(use-package popper
  :ensure t
  :bind (("C-`"   . popper-toggle-latest)
         ("C-s-`" . popper-cycle)  ;; Use echo area's M- key bindings instead
         ("C-~"   . popper-toggle-type))
  :config
  ;; If I move the defun and setq calls into config, popper won't work right away on init buffers.
  :init
  (defun ct/popper-popup-at-bottom-noselect (buffer &optional _alist)
    "Display popup-buffer BUFFER at the bottom of the screen."
    (save-selected-window
      (display-buffer-in-side-window
       buffer
       `((window-height . ,popper-window-height)
         (side . bottom)
         (slot . 1)))))
  (setq popper-reference-buffers
        '("\\*Messages\\*"
          "Output\\*$"
          "Calendar"
          "\\*Async Shell Command\\*"
          help-mode
          ;; With 'user, these are actually handled by my display-buffer-alist
          (emacs-lisp-compilation-mode . hide)
          (compilation-mode . hide))
        popper-display-control 'user
        popper-display-function #'ct/popper-popup-at-bottom-noselect)
  (popper-mode +1)
  ;; Show instructions at the bottom.
  (popper-echo-mode +1))

Rotate window layout in frame

Got the wrong splits? Rotate the layout with s-r:

(use-package rotate
  :demand
  :bind ("s-r" . #'rotate-layout))

Create empty buffer

Via http://ergoemacs.org/emacs/emacs_new_empty_buffer.html – I bind this to <f1> for convenience.

(defun xah-new-empty-buffer ()
  "Create a new empty buffer.
New buffer will be named “untitled” or “untitled<2>”, “untitled<3>”, etc.

It returns the buffer (for elisp programing).

URL `http://ergoemacs.org/emacs/emacs_new_empty_buffer.html'
Version 2017-11-01"
  (interactive)
  (let (($buf (generate-new-buffer "untitled")))
    (switch-to-buffer $buf)
    (funcall initial-major-mode)
    (setq buffer-offer-save t)
    $buf))
;; (global-set-key (kbd "<f1>") #'xah-new-empty-buffer)

;; macOS-Specific Setting using the Cmd key
(when (memq window-system '(mac ns))
  (global-set-key (kbd "s-n") #'xah-new-empty-buffer))

Bindings to close/kill current buffer and frame

Since I can create buffers that don’t visit a file as scratchpads, I have to make sure I don’t accidentally kill them before having had a chance to save their contents. Buffers that aren’t visiting a file don’t ask to be saved. They just go away.

(defun ct/kill-current-buffer-ask-if-modified ()
  "Kills current buffer immediately, unless it is modified. Ask for confirmation then."
  (interactive)
  (let ((buf (current-buffer)))
    (if (buffer-modified-p buf)
        (when (yes-or-no-p "Buffer modified. Kill anyway? ")
          (kill-buffer buf))
      (kill-buffer buf))))

;; macOS-Specific Setting using the Cmd key
(when (memq window-system '(mac ns))
  (global-set-key (kbd "s-w") #'ct/kill-current-buffer-ask-if-modified)
  ;; s-w was delete-frame before, but Cmd-Shift-W to close macOS window with all its tabs fits better
  (global-set-key (kbd "s-W") #'delete-frame))

Center new frames on my huge screen

The whole code is from https://gist.github.com/ieure/80638

(defun ct/frame-recenter (&optional frame)
  "Center FRAME on the screen.
FRAME can be a frame name, a terminal name, or a frame.
If FRAME is omitted or nil, use currently selected frame."
  (interactive)
  (unless (eq 'maximised (frame-parameter nil 'fullscreen))
    (let* ((frame (or frame
                      (selected-frame)))
           (frame-w (frame-pixel-width frame))
           (frame-h (frame-pixel-height frame))
           ;; frame-monitor-workarea returns (x y width height) for the monitor
           (monitor-w (nth 2 (frame-monitor-workarea frame)))
           (monitor-h (nth 3 (frame-monitor-workarea frame)))
           (center (list (/ (- monitor-w frame-w) 2)
                         (/ (- monitor-h frame-h) 2))))
      (apply 'set-frame-position (flatten-list (list frame center))))))

To automatically center the frame after creation and at startuip:

(add-hook 'after-init-hook #'ct/frame-recenter)  ;; Is actually too early, need to wait for font and rest
(add-hook 'after-make-frame-functions #'ct/frame-recenter)

Create new frame

The default to split off a new frame, C-x 5 2, is a bit cumbersome.

;; macOS-Specific Setting using the Cmd key
(when (memq window-system '(mac ns))
  (global-set-key (kbd "s-n") #'make-frame-command))

Close emacs safely

If unsaved buffers exist, list them.

Source: https://archive.casouri.cat/note/2021/clean-exit/index.html

(defun ct/clean-exit ()
  "Exit Emacs cleanly.
If there are unsaved buffer, pop up a list for them to be saved
before existing. Replaces ‘save-buffers-kill-terminal’."
  (interactive)
  (if (frame-parameter nil 'client)
      (server-save-buffers-kill-terminal arg)
    (if-let ((buf-list (seq-filter (lambda (buf)
                                     (and (buffer-modified-p buf)
                                          (buffer-file-name buf)))
                                   (buffer-list))))
        (progn
          (pop-to-buffer (list-buffers-noselect t buf-list))
          (message "s to save, C-k to kill, x to execute"))
      (save-buffers-kill-emacs))))
(global-set-key [remap save-buffers-kill-terminal] #'ct/clean-exit)

Tabs

  • tab-bar since Emacs 27 means a list of window configurations. It’s displayed at the very top of the frame, outside the padding. Manage workspaces and appearances.
  • tab-line since Emacs 27 manages buffers, grouped by a criterion.

tab-bar modifications

(use-package emacs
  :config
  (defface ct/tab-bar-numbers
    '((t
       :inherit tab-bar
       :family "SF Compact"
       :weight light))
    "Face for tab numbers in both active and inactive tabs.")
  (defvar ct/circle-numbers-alist
    '((0 . "")
      (1 . "")
      (2 . "")
      (3 . "")
      (4 . "")
      (5 . "")
      (6 . "")
      (7 . "")
      (8 . "")
      (9 . ""))
    "Alist of integers to strings of circled unicode numbers.")
  (defvar ct/box-numbers-alist
    '((1 . "􀃊")
      (2 . "􀃌")
      (3 . "􀃎")
      (4 . "􀘙")
      (5 . "􀃒")
      (6 . "􀑵")
      (7 . "􀃖")
      (8 . "􀃘")
      (9 . "􀑷")
      (0 . "􀃈"))
    "Alist of integers to strings of SF Symbols with numbers in boxes.")
  (defun ct/tab-bar-tab-name-format-default (tab i)
    (let ((current-p (eq (car tab) 'current-tab)))
      (concat
       (propertize
        (when (and tab-bar-tab-hints (< i 10)) (alist-get i ct/box-numbers-alist))
        'face 'ct/tab-bar-numbers)
       " "
       (propertize
        (concat (alist-get 'name tab)
	            (or (and tab-bar-close-button-show
			             (not (eq tab-bar-close-button-show
				                  (if current-p 'non-selected 'selected)))
			             tab-bar-close-button)
		            ""))
        'face (funcall tab-bar-tab-face-function tab))
       " ")))
  (setq tab-bar-tab-name-format-function #'ct/tab-bar-tab-name-format-default
        tab-bar-tab-hints t)

  (setq tab-bar-close-button-show nil
	    tab-bar-close-button " \x00d7 ") ;; Cross multiplication character
  (setq tab-bar-new-button-show nil
	    tab-bar-new-button " + ")  ;; Thicker + than the flimsy default
  (setq tab-bar-separator nil)
  (setq tab-bar-format
	    '(;;tab-bar-format-history ;; forward/back buttons
	      tab-bar-format-tabs-groups
	      tab-bar-separator
          ;; tab-bar-format-add-tab ;; new tab button
	      tab-bar-format-align-right
	      tab-bar-format-global))

  ;; Display battery and time in `tab-bar-format-global' section:
  (display-battery-mode +1)
  (setq display-time-format "%Y-%m-%d %H:%M")   ;; Override time format.
  (setq display-time-default-load-average nil)  ;; Hide load average.
  (display-time-mode +1)

  ;; Bind 1-9 in the tab prefix map to switch to that tab.
  (mapcar (lambda (tab-number)
            (let ((funname (intern (format "ct/tab-bar-select-%d" tab-number)))
                  (docstring (format "Select tab %d by its absolute number." tab-number))
                  (key (kbd (format "%d" tab-number)))
                  (super-key (kbd (format "s-%d" tab-number))))
              (eval-expression `(defun ,funname ()
                                  ,docstring
                                  (interactive)
                                  (tab-bar-select-tab ,tab-number)))
              (eval-expression `(define-key tab-prefix-map ,key ',funname))
              (eval-expression `(global-set-key ,super-key ',funname))))
          (number-sequence 1 9))

  :hook
  (tab-bar-mode . tab-bar-history-mode)
  (after-init . tab-bar-mode)

  :bind
  ;; ("s-{" . tab-bar-switch-to-prev-tab)
  ;; ("s-}" . tab-bar-switch-to-next-tab)
  ("s-t" . tab-bar-new-tab)
  ("s-w" . tab-bar-close-tab) ; I constantly want to close a buffer this way.
  ("s-T" . tab-bar-undo-close-tab)
  (:map tab-prefix-map
	    ("<left>" . tab-bar-switch-to-prev-tab)
	    ("<right>" . tab-bar-switch-to-next-tab)
        ("n" . tab-bar-new-tab)
        ))

save-tab-excursion function to open tab in background

You can call this similar to save-buffer-excursion, only the tab will continue to exist; it’ll e.g. open a file in the background:

(save-tab-excursion (find-file "test.txt"))

This could potentially be bound to a prefix key, like other-tab-prefix, but doing the work in the background.

(defmacro save-tab-excursion (&rest body)
  "Opens a new tab in tab-line in the background and executes BODY
inside, then restores the previously selected tab."
  `(progn
     (tab-bar-new-tab)
     (unwind-protect (progn ,@body)
       (tab-bar-switch-to-recent-tab))))

Using swipe gestures (MX Master 3’s thumb buttons) for navigation

(use-package emacs
  :defer t
  :if (memq window-system '(mac ns x))
  :bind
  (:map help-mode-map
        ("<swipe-left>" . help-go-back)
        ("<swipe-right>" . help-go-forward))
  (:map Info-mode-map
        ("<swipe-left>" . Info-history-back)
        ("<swipe-right>" . Info-history-forward)))

File Settings

Avoid file picker popups in general

(use-package emacs
  :config
  ;; No file dialog
  (setq use-file-dialog nil)

  ;; No dialog box for y-or-n etc.
  (setq use-dialog-box nil)

  ;; Popup windows; set to nil to get ibuffer in a new frame instead
  (setq pop-up-windows t))

Remember recently open files

(use-package emacs
  :init
  ;; Disable auto-cleanup to fix a TRAMP related host lookup before we start recentf
  ;; URL https://stackoverflow.com/a/883010/1460929
  ;; URL https://www.emacswiki.org/emacs/RecentFiles#h5o-12
  (setq recentf-auto-cleanup 'never)
  (setq recentf-keep '(file-remote-p file-readable-p))
  (recentf-mode 1)
  (setq recentf-max-menu-items 50)
  (setq recentf-max-saved-items 50))

Auto-save visited files in the background when you don’t save them explicitly

Unlike auto-save-mode that saves to a temporary file, I auto-save-visited-mode cannot be disabled on a major mode basis in a hook. It’s a global setting and doesn’t work on a per-buffer/per-file basis.

I don’t want auto-save-visited-mode to be enabled for message-mode email drafts, for example.

  • real-auto-save is like auto-save-visited but allows local enablement/disablement
  • super-save has customizable triggers (e.g. when finding files, switching buffers, …) on top of save intervals
(use-package real-auto-save
  :demand
  :delight
  :init
  (setq real-auto-save-interval 10) ;; in seconds

  ;; Patching this in until https://github.com/ChillarAnand/real-auto-save/pull/55 is merged
  (defun turn-on-real-auto-save () (real-auto-save-mode 1))
  (defun turn-off-real-auto-save () (real-auto-save-mode -1))
  (define-globalized-minor-mode global-real-auto-save-mode
    real-auto-save-mode turn-on-real-auto-save)

  (global-real-auto-save-mode 1)
  (add-hook 'message-mode-hook #'turn-off-real-auto-save))

Auto-revert buffers to file contents on disk when files have changed

My main org folder contains a .dir-locals.el with (auto-revert-mode 1) in it so these files automatically revert when the disk contents change.

Could use (global-auto-revert-mode t) like https://www.nicklanasa.com/posts/emacs-syncing-dropbox-beorg suggests, but with revbufs, one can manually revert all buffers and get a conflict resolution list:

(use-package revbufs
  :commands (revbufs))

Store auto-save files and version backups in /tmp

(setq backup-directory-alist
      `((".*" . ,temporary-file-directory)))
(setq auto-save-file-name-transforms
      `((".*" ,temporary-file-directory t)))

Save silently

(setq save-silently t)

Reloading files

Beginning with Emacs 28, can also use C-x x g (revert-buffer-quickly)

(defun reload-file (&optional file)
  "Revert buffer for FILE without confirmation.

When FILE is nil, revert the current buffer. When no FILE is
non-nil but no buffer was found, do nothing."
  (interactive)
  (if file
      (if-let ((buffer (find-buffer-visiting file)))
          (if (and (boundp 'auto-revert-mode) auto-revert-mode)
              (auto-revert-buffer buffer)
            (with-current-buffer buffer
              (revert-buffer :ignore-auto :noconfirm)))
        (message "No buffer found for file"))
    (message "Reverting current buffer")
    (revert-buffer :ignore-auto :noconfirm)))

Trash Can / Recycle Bin settings

move-file-to-trash is an interactive function that can be used to trash instead of delete/rm’ing files. Emacs (for macOS at least) doesn’t define system-move-file-to-trash which would be called first, so the trash-directory value is used as the destination.

(when (memq window-system '(mac ns))
  (defun system-move-file-to-trash (path)
    "Moves file at PATH to the macOS Trash according to `move-file-to-trash' convention.

Relies on the command-line utility 'trash' to be installed.
Get it from:  <http://hasseg.org/trash/>"
    (shell-command (concat "trash -vF \"" path "\""
                           "| sed -e 's/^/Trashed: /'")
                   nil ;; Name of output buffer
                   "*Trash Error Buffer*")))
(setq trash-directory "~/.Trash")  ;; fallback for `move-file-to-trash'
(setq delete-by-moving-to-trash t) ;; enable trash by default

Save file positions

Useful to restore cursor position in a file when revisiting. Idea from Xah Lee.

(use-package emacs
  :init
  (save-place-mode +1))

Notifications

No audio notifications within emacs.

(setq ring-bell-function 'ignore)

Project and directory navigation

Dired

Enable using a to open the selected file/directory and exit the current buffer, instead of pushing another buffer on top of the stack:

(put 'dired-find-alternate-file 'disabled nil)

Enable guessing what to do (DWIM) with actions, like when 2 dired windows are next to each other, copy and rename/move will suggest the other side:

(setq dired-dwim-target t)

Notable shortcuts:

  • <a> to open directory and bury current buffer
  • <W> for browse-url-of-dired-file will open the system default application
  • <w> will enter wdired for text-based changes

Browse/find via dired

Analog to C-x f which opens a file, C-c f shows files in dired.

Use this to select a directory, and then a wildcard pattern to find files in the directory hierarchy below the selection. You can then operate on the flat file listing as you would work with regular dired buffers.

(global-set-key (kbd "C-c f") #'find-name-dired)

The output of find takes up more horizontal space than plain ls listings; but dired-hide-details-mode hides these columns just as well and makes the output usable.

Omit file details (owner, file size, date, …)

( inside of dired buffers toggles detail hiding. Hide details by default.

(require 'dired-x)
(add-hook 'dired-mode-hook #'dired-omit-mode)
(defun turn-on-dired-hide-details-mode () (dired-hide-details-mode +1))
(add-hook 'dired-mode-hook #'turn-on-dired-hide-details-mode)

diranged

Think Quicklook or mu4e/notmuch message list, where the file content is shown in a split-off window as you move the selection.

(use-package diranged
  :load-path "/Users/ctm/.dotfiles/emacs.d/src/diranged"
  :config
  (setq diranged-kill-on-move t        ; cleanup spawed buffers as we go
        diranged-kill-on-exit t        ; cleanup spawed buffers on exit
        diranged-max-file-size 10      ; MB size limit for previewing files
        diranged-steal-all-the-keys t) ; overwrite non-dired bindings
  (defun ct/diranged-quit ()
    (interactive)
    "Toggles `diranged-mode' off."
    (diranged-mode -1))
  :bind
  (:map dired-mode-map ("SPC" . diranged-mode))
  (:map diranged-mode-map ("q" . ct/diranged-quit)))

dired-subtree adds org-mode-like cycling to folders

(use-package dired-subtree
  :ensure
  :defer
  :bind (:map dired-mode-map ("TAB" . dired-subtree-toggle)))

dired-auto-readme renders README files below the file listing like on GitHub

From source via GitHub: https://github.com/amno1/dired-auto-readme

Displays README files inline, like i in dired would insert a sub directory at the bottom. Works with org and Markdown files.

(use-package dired-auto-readme
  :load-path "~/.emacs.d/src/dired-auto-readme"
  :demand
  :hook (dired-mode . dired-auto-readme-mode)
  :config)
(with-eval-after-load 'dired-auto-readme
  (with-eval-after-load 'org-view-mode
    (add-to-list 'dired-auto-readme-alist
                 '(org-mode . org-view-mode))))
(with-eval-after-load 'dired-auto-readme
  (with-eval-after-load 'markdown-mode
    (add-to-list 'dired-auto-readme-alist
                 '(markdown-mode . (lambda ()
                                     (markdown-view-mode)
                                     (font-lock-ensure))))))

dired-hist remembers previous locations to go back/forward

(use-package dired-hist
  :load-path "~/.emacs.d/src/dired-hist"
  :hook (dired-mode . dired-hist-mode)
  :bind
  (:map dired-mode-map
        ("l". #'dired-hist-go-back)
        ("r" . #'dired-hist-go-forward)))

dwim-shell-command acts on Dired selection to bulk process files

Posts and examples via xenodium.com:

(use-package dwim-shell-command
  :ensure t
  :bind (([remap shell-command] . dwim-shell-command)
         :map dired-mode-map
         ([remap dired-do-async-shell-command] . dwim-shell-command)
         ([remap dired-do-shell-command] . dwim-shell-command)
         ([remap dired-smart-shell-command] . dwim-shell-command)))

Include the optional conversion utilities that ship with the package:

(with-eval-after-load 'dwim-shell-command
  (require 'dwim-shell-commands))

Bookmarks

;; Disable highlight of bookmarked line (since v28)
(setq-default bookmark-set-fringe-mark nil)

Projectile

https://www.projectile.mx/en/latest/ https://www.emacswiki.org/emacs/NeoTree#toc7

Standard keybindings:

C-c p p counsel-projectile-switch-project Switch project C-c p f counsel-projectile-find-file Jump to a project file C-c p g counsel-projectile-find-file-dwim Jump to a project file using completion based on context C-c p d counsel-projectile-find-dir Jump to a project directory C-c p b counsel-projectile-switch-to-buffer Jump to a project buffer C-c p s g counsel-projectile-grep Search project with grep C-c p s s counsel-projectile-ag Search project with ag C-c p s r counsel-projectile-rg Search project with rg

(use-package projectile
  :demand
  :delight
  :config
  ;; Global configuration
  (setq projectile-switch-project-action 'treemacs-projectile ;'neotree-projectile-action
        projectile-enable-caching t
        projectile-create-missing-test-files t
        projectile-switch-project-action #'projectile-commander
        projectile-ignored-project-function 'file-remote-p)

   ;; PHP project type
  (projectile-register-project-type 'php '("composer.json")
                                    :src-dir "src"
				                    :test "composer test"
				                    :run "composer serve"
				                    :test-suffix "Test"
				                    :test-dir "tests")
  ;; PHP/Laravel project type
  (projectile-register-project-type 'php-laravel '("composer.json" "artisan" "app" "routes")
                                    :src-dir "app"
				                    :run "php artisan serve"
				                    :test "php artisan test"
				                    :test-suffix "Test"
				                    :test-dir "tests")
   ;; Nanoc website settings
  (projectile-register-project-type 'ruby-nanoc '("nanoc.yaml" "Rules" "Gemfile")
                                    :src-dir "content"
                                    :compile "bundle exec nanoc compile"
                                    :run "bundle exec nanoc view")
  ;; Always enable
  (projectile-mode 1)

  ;; Don't try to find projectile project roots in remote/SSH projects via Tramp
  ;; https://www.murilopereira.com/how-to-open-a-file-in-emacs/#searching-files
  (defadvice projectile-project-root (around ignore-remote first activate)
    (unless (file-remote-p default-directory 'no-identification) ad-do-it))

  :bind-keymap
  ("C-c p" . projectile-command-map))

project.el

Similar in prefix to Projectile, since Emacs 28.1, C-x p p will switch between projects, and C-x p f visits files in a project via the basename of the file (So C-x p f index will show all index.html files in all directories).

Like Projectile, it has C-x p c to compile, but nothing to distinguish different commands, e.g. compilation from testing.

Custom key bindings in the project keymap

The Silver Searcher aka “ag” search (which I can actually use as opposed to grep).

Bound to “a” because of “ag”.

(with-eval-after-load 'ag
  (define-key project-prefix-map (kbd "a") #'ag-project))

Ripgrep sadly only is available via projectile, not via programmatic deadgrep invocation. Bound to “q” because it’s close to “a” on QWERTY keyboards, and not occupied.

(with-eval-after-load 'projectile
  (define-key project-prefix-map (kbd "q") #'projectile-ripgrep))

Show Magit Status

With m, go right to Magit status.

(with-eval-after-load 'project
  ;; Replacement for `with-eval-after-load 'magit` that doesn't require Magit to have been used prior to adding the shortcut
  (when (fboundp 'magit)
    (defun ct/project-magit-status (&rest args)
      "Open `magit-status' in the project."
      (interactive)
      (magit-status (project-root (project-current t))))
    ;; The project-prefix-map binding is inherited for the project switching quick commands.
    (define-key project-prefix-map "m" #'ct/project-magit-status)
    (add-to-list 'project-switch-commands '(ct/project-magit-status "Magit") t)))

Create New Blog Post

I often switch to a blog post project directory to create a new post. This involved finding a file that doesn’t exist yet for the current date’s content/posts/YYYY/MM/ pattern – so even the folder might not exist at the beginning of a new month. This key binding fixes that.

(with-eval-after-load 'project
  ;; The project-prefix-map binding is inherited for the project switching quick commands.
  (define-key project-prefix-map "n" #'ct/project-create-post)
  (add-to-list 'project-switch-commands '(ct/project-create-post "New post") t))

Allow non-VC-backed directories

(defgroup project-local nil
  "Local, non-VC-backed project.el root directories."
  :group 'project)

(defcustom project-local-identifier ".project"
  "Filename(s) that identifies a directory as a project.
You can specify a single filename or a list of names."
  :type '(choice (string :tag "Single file")
                 (repeat (string :tag "Filename")))
  :group 'project-local)

(cl-defmethod project-root ((project (head local)))
  "Return root directory of current PROJECT."
  (cdr project))

(defun project-local-try-local (dir)
  "Determine if DIR is a non-VC project.
DIR must include a file with the name determined by the
variable `project-local-identifier' to be considered a project."
  (if-let ((root (if (listp project-local-identifier)
                     (seq-some (lambda (n)
                                 (locate-dominating-file dir n))
                               project-local-identifier)
                   (locate-dominating-file dir project-local-identifier))))
      (cons 'local root)))

(customize-set-variable 'project-find-functions
                        (list #'project-try-vc
                              #'project-local-try-local))

Treemacs

Unlike NeoTree, Treemacs defines a workspace of directories. NeoTree shows the directory structure on the side, and that’s it. Treemacs shows the structure of any directory you add to the workspace next to each other.

I used neotree for about 2 years until I found that you can dig into documents with treemacs: TAB will not only expand folder structures, but also expand files to show their TAGS – in Markdown documents, that means headings. Super useful to browse a project!

Remember to hit ? in treemacs to get a comprehensive list of key bindings.

(use-package treemacs
  :defer
  :demand
  :config
  (setq treemacs-position 'left
        treemacs-width 30
        treemacs-show-hidden-files t)

                                        ;(setq treemacs--icon-size 16)
  (treemacs-resize-icons 16)

  ;; Don't always focus the currently visited file
  (treemacs-follow-mode -1)

  (defun ct/treemacs-decrease-text-scale ()
    (text-scale-decrease 1))
  :bind
  ("<f8>" . treemacs)
  :hook
  (treemacs-mode . ct/treemacs-decrease-text-scale))

Treemacs theme / icons

Use all-the-icons to keep a consistent look across the system.

(use-package treemacs-all-the-icons
  :after treemacs
  :config
  (treemacs-load-theme "all-the-icons")

  ;; Have to rely on customize to override the face to fix slanted inheritance form modus-theme
                                        ; '(treemacs-all-the-icons-file-face ((t (:inherit treemacs-file-face))))
  )

Treemacs-Projectile

  • C-c C-p p to add projectile project to workspace
  • C-c C-p d to delete project from workspace
(use-package treemacs-projectile
  :demand
  :after (projectile treemacs))

Neotree

Neotree displays a foldable directory structure in a small-ish sidebar window. I prefer to keep the directory listing to the right; and I don’t want neotree to automatically reflect the directory of the current file within a project, but instead keep the project open.

(use-package neotree
  :defer t
  :after (all-the-icons)
  :config
  (defun ct/neotree-decrease-text-scale ()
    (text-scale-decrease 1))

  (setq neo-window-position 'left
        neo-window-width 30
        neo-mode-line-type 'none
        neo-hide-cursor t ;; Use hl-line instead

        ;; When the neotree window is opened, let it find current file and jump to node.
        neo-smart-open t
        neo-autorefresh nil

        ;; Show all-the-icons icons in tree mode; use 'nerd when not available
        neo-theme 'icons)
  :bind
  ;; Shift-F8 bound to projectile below (if available)
  (("<f8>" . neotree-toogle))
  (:map neotree-mode-map
        ("M-<up>" . neotree-select-up-node)
        ("M-<down>" . neotree-change-root)
        ("M-n" . neotree-create-node))
  :hook
  (neotree-mode . ct/neotree-decrease-text-scale)
  (neotree-mode . ct/disable-local-line-spacing)
  (neotree-mode . turn-off-visual-line-mode))

Projectile integration

Summon neotree for the project root with S-<f8>:

(with-eval-after-load "projectile"
  (with-eval-after-load "neotree"
    (defun ct/neotree-using-projectile-dir ()
      "Open NeoTree using the projectile dir/git root instead of the current directory."
      (interactive)
      (let ((project-dir (projectile-project-root))
            (file-name (buffer-file-name)))
        (neotree-toggle)
        (if project-dir
            (if (neo-global--window-exists-p)
                (progn
                  (neotree-dir project-dir)
                  (neotree-find file-name)))
          (message "Could not find git project root."))))

    (global-set-key (kbd "<S-f8>") #'ct/neotree-using-projectile-dir)))

XFK key bindings

(with-eval-after-load "xah-fly-keys"
  (with-eval-after-load "neotree"
    (define-key xah-fly-command-map (kbd "<f8>") #'neotree-toggle)
    (define-key xah-fly-insert-map (kbd "<f8>") #'neotree-toggle)))

Show variable-pitch in all neotree faces

(with-eval-after-load "neotree"
  (defun ct/neotree-variable-pitch-faces ()
    (let ((sans-serif-family (face-attribute 'variable-pitch :family))
          (all-neotree-faces '(neo-file-link-face neo-banner-face neo-button-face neo-header-face neo-vc-user-face neo-dir-link-face neo-root-dir-face neo-vc-added-face neo-vc-edited-face neo-expand-btn-face neo-vc-default-face neo-vc-ignored-face neo-vc-missing-face neo-vc-removed-face neo-vc-conflict-face neo-vc-up-to-date-face neo-vc-needs-merge-face neo-vc-needs-update-face neo-vc-unregistered-face neo-vc-unlocked-changes-face)))
      (dolist (face all-neotree-faces)
        (set-face-attribute face nil :family sans-serif-family))))
  (add-hook 'neotree-mode-hook #'ct/neotree-variable-pitch-faces)
  ;; When toggling light/dark modus-theme, reset the neotree fonts as well.
  (with-eval-after-load "modus-themes"
    (add-hook 'modus-themes-after-load-theme-hook #'ct/neotree-variable-pitch-faces)))

Speedbar

Built-in speedbar is capable of listing directory entries in a separate frame.

(require 'speedbar)
;; Enable file listing (as opposed to only directory listing)
(setq speedbar-show-unknown-files t)
(setq speedbar-indentation-width 4)

(set-face-attribute 'speedbar-button-face nil :bold nil)
(set-face-attribute 'speedbar-directory-face nil :bold nil)
(set-face-attribute 'speedbar-file-face nil :bold nil)

Completions and selections

;; Hide commands that do not work in the current mode when calling M-x (via vertico.el README)
                                        ; (setq read-extended-command-predicate
                                        ;       #'command-completion-default-include-p)

;; via https://github.com/milkypostman/dotemacs/blob/main/init.el
(defun ct/delete-backward-updir ()
  "Delete char before or go up directory. Unlike deleting a word, skips file extension dot."
  (interactive)
  (if (eq (char-before) ?/)
	  (save-excursion
	    (goto-char (1- (point)))
	    (when (search-backward "/" (point-min) t)
	      (delete-region (1+ (point)) (point-max))))
    (call-interactively 'backward-kill-word)))

vertico is a simpler minibuffer completion module, even than selectrum

Selectrum was quite unintrusive and minimal compared to ivy; vertico does even less and relies upon Emacs’s default completion and filtering system. Which is quite good!

Just displays a vertical completion list, instead of e.g. ido’s minibuffer in-line completions.

(use-package vertico
  :demand
  :ensure
  :delight
  :config
  (setq vertico-resize t)

  ;; Use `consult-completion-in-region', e.g for completions of M-: (eval-expression)
  (setq completion-in-region-function
        (lambda (&rest args)
          (apply (if vertico-mode
                     #'consult-completion-in-region
                   #'completion--in-region)
                 args)))

  :init
  (vertico-mode +1)

  :bind
  (:map vertico-map
	    ("C-p" . vertico-previous)
	    ("C-n" . vertico-next)
	    ("s-DEL" . ct/delete-backward-updir)))

Also enable mouse mode; am maintaining the package’s clone directly because the MELPA version does not include any of the extensions, and I want the mouse mode as backup.

(use-package vertico-mouse
  :load-path "~/.emacs.d/src/vertico/extensions"
  :demand
  :commands (vertico-mouse-mode)
  :after (vertico)
  :init
  (add-hook 'vertico-mode-hook #'vertico-mouse-mode))

orderless allows partial matching

Selectrum had similar capabilities out of the box. Vetico defaults to strict prefix matches.

When I remove this, check that this works the way I expect:

  • Switching buffers: “Mess” should match *Messages*
  • Selecting headings in org file: substring matches should work, e.g. “vert” should match **=vertico= bla bla

Orderless behaves like fzf in the shell: fuzzy matching with space-separated substrings.

Daniel Meindlers config: https://github.com/minad/consult/wiki#minads-orderless-configuration

(use-package orderless
  :demand t
  :config
  (defvar +orderless-dispatch-alist
    '((?% . char-fold-to-regexp)
      (?! . orderless-without-literal)
      (?` . orderless-initialism)
      (?= . orderless-literal)
      (?~ . orderless-flex)))

  (defun +orderless--suffix-regexp ()
    (if (and (boundp 'consult--tofu-char) (boundp 'consult--tofu-range))
        (format "[%c-%c]*$"
                consult--tofu-char
                (+ consult--tofu-char consult--tofu-range -1))
      "$"))

  ;; Recognizes the following patterns:
  ;; * ~flex flex~
  ;; * =literal literal=
  ;; * %char-fold char-fold%
  ;; * `initialism initialism`
  ;; * !without-literal without-literal!
  ;; * .ext (file extension)
  ;; * regexp$ (regexp matching at end)
  (defun +orderless-dispatch (word _index _total)
    (cond
     ;; Ensure that $ works with Consult commands, which add disambiguation suffixes
     ((string-suffix-p "$" word)
      `(orderless-regexp . ,(concat (substring word 0 -1) (+orderless--suffix-regexp))))
     ;; File extensions
     ((and (or minibuffer-completing-file-name
               (derived-mode-p 'eshell-mode))
           (string-match-p "\\`\\.." word))
      `(orderless-regexp . ,(concat "\\." (substring word 1) (+orderless--suffix-regexp))))
     ;; Ignore single !
     ((equal "!" word) `(orderless-literal . ""))
     ;; Prefix and suffix
     ((if-let (x (assq (aref word 0) +orderless-dispatch-alist))
          (cons (cdr x) (substring word 1))
        (when-let (x (assq (aref word (1- (length word))) +orderless-dispatch-alist))
          (cons (cdr x) (substring word 0 -1)))))))

  ;; Define orderless style with initialism by default
  (orderless-define-completion-style +orderless-with-initialism
    (orderless-matching-styles '(orderless-initialism orderless-literal orderless-regexp)))

  ;; You may want to combine the `orderless` style with `substring` and/or `basic`.
  ;; There are many details to consider, but the following configurations all work well.
  ;; Personally I (@minad) use option 3 currently. Also note that you may want to configure
  ;; special styles for special completion categories, e.g., partial-completion for files.
  ;;
  ;; 1. (setq completion-styles '(orderless))
  ;; This configuration results in a very coherent completion experience,
  ;; since orderless is used always and exclusively. But it may not work
  ;; in all scenarios. Prefix expansion with TAB is not possible.
  ;;
  ;; 2. (setq completion-styles '(substring orderless))
  ;; By trying substring before orderless, TAB expansion is possible.
  ;; The downside is that you can observe the switch from substring to orderless
  ;; during completion, less coherent.
  ;;
  ;; 3. (setq completion-styles '(orderless basic))
  ;; Certain dynamic completion tables (completion-table-dynamic)
  ;; do not work properly with orderless. One can add basic as a fallback.
  ;; Basic will only be used when orderless fails, which happens only for
  ;; these special tables.
  ;;
  ;; 4. (setq completion-styles '(substring orderless basic))
  ;; Combine substring, orderless and basic.
  ;;
  (setq completion-styles '(orderless basic)
        completion-category-defaults nil
        ;;; Enable partial-completion for files.
        ;;; Either give orderless precedence or partial-completion.
        ;;; Note that completion-category-overrides is not really an override,
        ;;; but rather prepended to the default completion-styles.
        ;; completion-category-overrides '((file (styles orderless partial-completion))) ;; orderless is tried first
        completion-category-overrides '((file (styles partial-completion)) ;; partial-completion is tried first
                                        ;; enable initialism by default for symbols
                                        (command (styles +orderless-with-initialism))
                                        (variable (styles +orderless-with-initialism))
                                        (symbol (styles +orderless-with-initialism)))
        orderless-component-separator #'orderless-escapable-split-on-space ;; allow escaping space with backslash!
        orderless-style-dispatchers '(+orderless-dispatch)))

consult is to built-in Icomplete what counsel is to ivy

This is pretty nifty:

  • C-b to switch buffers, then use narrowing to start switching to stuff based on its type:
    • default: b SPC / f SPC / m SPC / v SPC to narrow to buffers, files, bookmarks and views respectively.
    • via config below: < is the “narrow” prefix, so < b will narrow to buffers, too.
  • C-c o to navigate outlines navigates imenu tags well; not sure if I prefer my trusty old C-. C-. where I can see the breadcrumb trail, or less cramped lines instead.
  • M-g l to go to line (with matched text in it)
  • M-g e to go to error (relevant to prog-mode)
(use-package consult
  :bind (("C-. C-." . consult-imenu) ;; Got used to use this for jumping to symbols
         ("C-c h" . consult-history)
         ("C-x b" . consult-buffer)
         ("C-x 4 b" . consult-buffer-other-window)
         ("C-x 5 b" . consult-buffer-other-frame)
         ("C-x r x" . consult-register)
                                        ; ("C-x r b" . consult-bookmark) ;; Had trouble with this; ust `C-b < m` instead
         ("M-g g" . consult-goto-line)
         ("M-g M-g" . consult-goto-line)
         ;; M-g for "goto", but M-s for "search" also makes sense
         ("M-g o" . consult-outline) ;; "M-s o" is a good alternative
         ("M-g m" . consult-mark)    ;; "M-s m" is a good alternative
         ("M-g l" . consult-line)    ;; "M-s l" is a good alternative
         ("M-g i" . consult-imenu)   ;; "M-s i" is a good alternative
         ("M-g e" . consult-error)   ;; "M-s e" is a good alternative
         ("M-g m" . consult-multi-occur)
         ("<help> a" . consult-apropos)
         ("M-y" . consult-yank-from-kill-ring))
  :init

  ;; Replace functions (consult-multi-occur is a drop-in replacement)
  (fset 'multi-occur #'consult-multi-occur)

  :config

  ;; Optionally configure narrowing and widening keys.
  ;; Both < and C-+ work reasonably well.
  (setq consult-narrow-key "<"
        consult-widen-key ">")

  ;; Disable previews.
  (setq consult-preview-key nil)

  ;; Optional configure a "view" library to be used by `consult-buffer`.
  ;; The view library must provide two functions, one to open the view by name,
  ;; and one function which must return a list of views as strings.
  ;; Example: https://github.com/minad/bookmark-view/
  ;; (setq consult-view-open-function #'bookmark-jump
  ;;       consult-view-list-function #'bookmark-view-names)
  )

consult-dir allows directory picking, i.e. to go to recent directory when finding file

With consult-dir called directly, I can switch context to a directory and find a file inside. That’s like a poor man’s project.el

During minibuffer completions, consult-dir will allow picking a directory and insert its full path into the minibuffer. E.g. when saving an email attachment, this allows picking a recent target folder.

(use-package consult-dir
  :ensure
  :demand
  :config
  (define-key minibuffer-local-completion-map (kbd "C-x C-d") #'consult-dir)
  (define-key minibuffer-local-completion-map (kbd "C-x C-j") #'consult-dir-jump-file)
  (define-key global-map (kbd "C-x C-d") #'consult-dir))
(with-eval-after-load "xah-fly-keys"
  (with-eval-after-load "consult-dir"
    ;; SPC i o
    (define-key xah-fly-c-keymap (kbd "o") #'consult-dir)))

flycheck support

(use-package consult-flycheck
  :demand
  :after consult
  :bind (:map flycheck-command-map
              ("!" . consult-flycheck)))

marginalia: show documentation pieces in completion buffers

Shows parts of docstrings for commands, and some buffer metadata when switching buffers.

(use-package marginalia
  :after (vertico consult)
  :demand
  :init
  (marginalia-mode)
  (setq marginalia-annotators '(marginalia-annotators-heavy marginalia-annotators-light nil)))

embark acts as a contextual menu of sorts, with different binding for different keymaps

  • Prefix embark-act (C-,=) with =C-u to not leave the current context and act ‘transiently’, by which I mean: branch off temporaliry, but continue where you are once finished.
    • Example: C-x C-f to find a file, complete a path, and while you’re at it, C-u C-,= act on a file. When you're done acting on the file, you can continue with the completion in the minibuffer. If you omit the universal argument prefix, =embark-act replaces the completion context, so you’d have to start over again. You can add files as you find them in the completion to the kill ring via C-u C-, w for example, or attach a file to an email, etc.
  • In any “embark” context, you can use C-h to show a list of available actions and use auto-completion to explore what’s available.
  • Fifteen ways to use embark does a great job demonstrating its use!
    • embark-act
      • Open any buffer by splitting any window
      • Copy a file to a remote location when finding a file
      • Insert a minibuffer candidate into the buffer
      • Run a shell command on a minibuffer candidate file without losing your session
      • Open a file as root without losing your session
      • Upload a region of text to 0x0
      • Visit a package’s URL from the minibuffer
      • Set a variable from anywhere it appears in a buffer
      • Add a keybinding for a command name from anywhere it appears
    • embark-export
      • Export Emacs package candidates to a package menu
      • Collect imenu candidates in an “imenu-list”
        • perists imenu candidates in a buffer: like a table of contents; with embark-collect-direct-action-minor-mode turned on, allows you to use the embark-act key bindings directly from the collect buffer!
      • Export file candidates to a dired-buffer
      • Export buffer candidates to ibuffer
      • Export variable candidates to a customize buffer
      • Export grep or line candidates to a grep buffer
(use-package embark
  :ensure t
  :config
  ;; Noob mode: Always display available actions
  (setq embark-prompter #'embark-keymap-prompter)
  ;; Show a live-filterable list of key bindings when C-h'ing with active prefix/leader key
  (setq prefix-help-command #'embark-prefix-help-command)

  ;; Custom mailto: link matcher for email actions
  (defun embark-refine-mailto-url-type (_type url)
    "If URL is a mailto link, strip the mailto: and classify it as `mailto'."
    (if (string-prefix-p "mailto:" url)
        (cons 'mailto (string-remove-prefix "mailto:" url))
      (cons 'url url)))
  (add-to-list 'embark-transformer-alist '(url . embark-refine-mailto-url-type))

  (defun mailto-compose-new (recipient)
    "Compose mail to RECIPIENT."
    (interactive "sTo: ")
    (compose-mail recipient))

  (defvar-keymap embark-mailto-map
    :doc "Keymap for Embark mailto actions."
    :parent embark-url-map
    "c" #'mailto-compose-new)

  (add-to-list 'embark-keymap-alist '(mailto . embark-mailto-map))

  (defun ct/attach-file (file)
    "Attach FILE to an email message.
The message to which FILE is attached is chosen as for `gnus-dired-attach`."
    (interactive "fAttach: ")
    (require 'gnus-dired)
    (ct/with-notmuch-as-compose-mail
     (gnus-dired-attach (list file))))

  (defmacro ct/embark-split-action (fn split-type)
    `(defun ,(intern (concat "ct/embark-"
                             (symbol-name fn)
                             "-"
                             (car (last  (split-string
                                          (symbol-name split-type) "-"))))) ()
       (interactive)
       (funcall #',split-type)
       (call-interactively #',fn)))

  (define-key embark-file-map (kbd "2") (ct/embark-split-action find-file split-window-below))
  (define-key embark-file-map (kbd "3") (ct/embark-split-action find-file split-window-right))
  (define-key embark-buffer-map (kbd "2") (ct/embark-split-action switch-to-buffer split-window-below))
  (define-key embark-buffer-map (kbd "3") (ct/embark-split-action switch-to-buffer split-window-right))
  (define-key embark-bookmark-map (kbd "2") (ct/embark-split-action bookmark-jump split-window-below))
  (define-key embark-bookmark-map (kbd "3") (ct/embark-split-action bookmark-jump split-window-right))

  :hook
  (minibuffer-setup-hook . embark-collect-completions-after-input) ;;embark-live-occur-after-input)
  :bind
  (("C-," . embark-act)
   ("M-," . embark-dwim)  ;; Execute DWIM action w/o prompting for action
   (:map minibuffer-local-completion-map
         ("C-," . embark-act)
         ("C-o" . embark-export))
   (:map embark-region-map
         ("Xc" . calc-grab-region))
   (:map embark-file-map
         ("r" . reload-file)
         ("R" . rename-file)
         ("s" . sudo-edit-find-file)
         ("a" . ct/attach-file))
   (:map embark-become-file+buffer-map
         ("s" . sudo-edit-find-file))
   )
  )
(with-eval-after-load "embark"
  (with-eval-after-load "ace-window"
    (message "both loaded")

    (defmacro ct/embark-ace-action (fn)
      `(defun ,(intern (concat "ct/embark-ace-" (symbol-name fn))) ()
         (interactive)
         (with-demoted-errors "%s"
           (require 'ace-window)
           (aw-switch-to-window (aw-select nil))
           (call-interactively (symbol-function ',fn)))))

    (define-key embark-bookmark-map (kbd "o") (ct/embark-ace-action bookmark-jump))
    (define-key embark-buffer-map (kbd "o") (ct/embark-ace-action switch-to-buffer))
    (define-key embark-file-map (kbd "o") (ct/embark-ace-action find-file))
    ))

Closer integration between embark and consult

Makes embark-export work better with the results from e.g. consult-ripgrep or consult-find-file.

(use-package embark-consult
  :after (embark consult))

Built-in dabbrev suggests completions based on prefix matches (for abbrev-iations)

Dabbrev keeps a track of buffer contents.

When I manage changes in Magit, dabbrev is responsible for suggesting type names as I write commit messages.

On its own, dabbrev only offers cycling through candidates via M-/ and C-M-/, but with packages like fancy-dabbrev, this can become a completion framework with a pop-up UI on its own. The matching is just really basic.

(use-package dabbrev
  :custom
  ;; Don't try to populate symbols/words/... from images and PDFs.
  (dabbrev-ignored-buffer-regexps '("\\.\\(?:pdf\\|jpe?g\\|png\\)\\'")))

CTAGS database for static code completion

On macOS, install a global ctags database:

brew install --HEAD universal-ctags/universal-ctags/universal-ctags

On Linux, use ctags-snap, on Windows ctags-win32.

Auto-completion in buffers with overlays via corfu

Was initially looking into Corfu for fuzzy completions and found this config https://pastebin.com/raw/pndhHBUL originally via Reddit.

Unlike company, which I used for 3 years, Corfu turns out to come with posframe-based UI, hooks into (modern) Emacs completion functions, and plays well with other packages I have installed: orderless in particular which already provides some kind of fuzzy matching.

Corfu suggests to bind the tab key (maybe even on a per-buffer basic) to complete. I find robust indentation to be more important, so I rely on M-i.

  • M-h: show help buffer for function/suggestion
  • M-g: show definition
  • M-d: show doc popup
(setq-local tab-always-indent 'complete)
(use-package corfu
  :ensure t
  :demand t
  :config
  ;; (global-corfu-mode)
  (setq
   corfu-auto t
   corfu-auto-delay 0.1
   corfu-auto-prefix 3
   corfu-cycle t
   corfu-separator "*"
   corfu-quit-at-boundary 'separator ; Stop completion at e.g. spaces, but not at '*'.
   corfu-preselect-first t
   corfu-quit-no-match t
   corfu-preview-current 'insert
   corfu-on-exact-match 'insert
   corfu-scroll-margin 3)

  (defun corfu-move-to-minibuffer ()
    "Moves the completion candidates into the minibuffer.

This provides more robust fuzzy matching."
    (interactive)
    (let ((completion-extra-properties corfu--extra)
          completion-cycle-threshold completion-cycling)
      (apply #'consult-completion-in-region completion-in-region--data)))

  :hook
  (after-init . global-corfu-mode)
  :bind
  ("M-i" . completion-at-point)
  (:map corfu-map
        ("M-p" . corfu-doc-scroll-down) ;; was: corfu-next
        ("M-n" . corfu-doc-scroll-up)   ;; was: corfu-previous
        ("M-m" . corfu-move-to-minibuffer)))

Corfu and orderless: fuzzy matching with * between words

The best component separator for orderless is space – but only in minibuffer completions for commands, filtering variables/functions, and finding files. This is the default, and it’s good.

When using orderless with corfu for completions in the buffer, I need a non-space character there to trigger somewhat “fuzzy” component matching.

Using * enables me to write Ba*Br to match BananaBread.

In the minibuffer, this would work with space just fine. But I didn’t find a robust configuration that would allow space in the buffer without quitting Corfu’s completion UI when using space to separate words. While writing, this is getting annoying quickly.

On Reddit, there was a discussion using M-SPC to insert a Unicode character as a separator. Might as well use * or something else. (Non-breaking space, easily inserted via A-SPC on Mac, doesn’t work by the way.)

(with-eval-after-load 'corfu
  (with-eval-after-load 'orderless
    ;; Configure orderless default (→ in buffer) to separate with an asterisk:
    (setq orderless-component-separator "*")

    ;; Then re-enable SPC in the minibuffer as the completion separator.
    (add-hook 'minibuffer-setup-hook
	          (lambda () (setq-local orderless-component-separator " ")))
    ))

Eshell fix to not insert spaces when corfu would try to auto-complete

https://github.com/minad/corfu#completing-in-the-eshell-or-shell

(with-eval-after-load 'corfu
  (add-hook 'eshell-mode-hook
            (lambda ()
              (setq-local corfu-auto nil)
              (corfu-mode))))

CAPE offers backends as completion-at-point functions

Similar to how company ingests many sources, CAPE can patch multiple sources into the default Emacs completion-at-point mechanism, and thus into Corfu as well.

CAPE could also import company backends:

;; For `company-yasnippet'.
(use-package company :ensure t)
(defvar cape-yasnippet (cape-company-to-capf 'company-yasnippet))

(defun cape-setup ()
  "Set up `cape'."
  (let* ((has-t (memq 't completion-at-point-functions))
         (copy (delete 't completion-at-point-functions))
         (caps `(,@copy cape-dabbrev cape-keyword ,cape-yasnippet)))
    (when (memq major-mode '(emacs-lisp-mode))
      (setq caps (append caps '(cape-symbol))))
    (when caps
      (setq-local completion-at-point-functions
                  `(,(apply 'cape-super-capf caps))))
    (when has-t
      (setq-local completion-at-point-functions
                  (append completion-at-point-functions '(t))))))

(add-hook 'prog-mode-hook 'cape-setup 100)
(add-hook 'org-mode-hook 'cape-setup 100)

Actual setup for a general-purpose setup:

(use-package cape
  :ensure t
  :init
  :config
  (setq cape-dabbrev-min-length 1)
  :init
  (add-to-list 'completion-at-point-functions #'cape-dabbrev)
  (add-to-list 'completion-at-point-functions #'cape-file)
  (add-to-list 'completion-at-point-functions #'cape-history)
  (add-to-list 'completion-at-point-functions #'cape-keyword)
  ;;(add-to-list 'completion-at-point-functions #'cape-tex)
  ;;(add-to-list 'completion-at-point-functions #'cape-sgml)
  ;;(add-to-list 'completion-at-point-functions #'cape-rfc1345)
  ;;(add-to-list 'completion-at-point-functions #'cape-abbrev)
  ;;(add-to-list 'completion-at-point-functions #'cape-ispell)
  ;;(add-to-list 'completion-at-point-functions #'cape-dict)
  ;;(add-to-list 'completion-at-point-functions #'cape-symbol)
  ;;(add-to-list 'completion-at-point-functions #'cape-line)

  (define-prefix-command 'ct/cape-map)
  (global-set-key (kbd "M-I") 'ct/cape-map)
  (when (window-system)
    ;; unbind C-i from TAB in GUI emacs
    (define-key input-decode-map [?\C-i] [C-i])
    (global-set-key (kbd "<C-i>") 'indent-region))
  (define-key ct/cape-map "p" #'completion-at-point)
  (define-key ct/cape-map "t" #'complete-tag)   ; etags
  (define-key ct/cape-map "d" #'cape-dabbrev)   ; basically `dabbrev-completion'
  (define-key ct/cape-map "f" #'cape-file)
  (define-key ct/cape-map "k" #'cape-keyword)
  (define-key ct/cape-map "s" #'cape-symbol)
  (define-key ct/cape-map "a" #'cape-abbrev)
  (define-key ct/cape-map "h" #'cape-history)  ; prefer C-c h 'consult-history
  (define-key ct/cape-map "i" #'cape-ispell)
  (define-key ct/cape-map "l" #'cape-line)
  (define-key ct/cape-map "w" #'cape-dict)
  (define-key ct/cape-map "\\" #'cape-tex)
  (define-key ct/cape-map "_" #'cape-tex)
  (define-key ct/cape-map "^" #'cape-tex)
  (define-key ct/cape-map "&" #'cape-sgml)
  (define-key ct/cape-map "r" #'cape-rfc1345)
  )

org-babel block completions

Trigger with the completion hotkey (M-i in my case) after typing the < opening angle bracket used from org-tempo.

(use-package org-block-capf
  :load-path "~/.emacs.d/src/org-block-capf"
  :config
  (add-hook 'org-mode-hook #'org-block-capf-add-to-completion-at-point-functions))

Calendar

Set the first day of the week to Monday and time to 24h mode.

(use-package emacs
  :config
  (setq calendar-week-start-day 1)
  (setq display-time-24hr-format t))

Shells and terminal modes

Eshell

(with-eval-after-load 'eshell
  (defun ct/eshell-setup ()
    ;; Populate imenu; via <https://xenodium.com/imenu-on-emacs-eshell/>
    (setq-local imenu-generic-expression
                '(("Prompt" " $ \\(.*\\)" 1))))
  (add-hook 'eshell-mode-hook #'ct/eshell-setup)
  ;; Set pager to `cat` (instead of less) because that works better in Emacs buffers
  (add-hook 'eshell-load-hook
            (lambda () (setenv "PAGER" "/bin/cat"))))

Interactive ls file lists

Keymap to add interactive files and directories via: https://www.emacswiki.org/emacs/EshellEnhancedLS

(eval-after-load "em-ls"
  '(progn
     (defun ted-eshell-ls-find-file-at-point (point)
       "RET on Eshell's `ls' output to open files."
       (interactive "d")
       (find-file (buffer-substring-no-properties
                   (previous-single-property-change point 'help-echo)
                   (next-single-property-change point 'help-echo))))

     (defun pat-eshell-ls-find-file-at-mouse-click (event)
       "Middle click on Eshell's `ls' output to open files.
 From Patrick Anderson via the wiki."
       (interactive "e")
       (ted-eshell-ls-find-file-at-point (posn-point (event-end event))))

     (let ((map (make-sparse-keymap)))
       (define-key map (kbd "RET")      'ted-eshell-ls-find-file-at-point)
       (define-key map (kbd "<return>") 'ted-eshell-ls-find-file-at-point)
       (define-key map (kbd "<mouse-2>") 'pat-eshell-ls-find-file-at-mouse-click)
       (defvar eshell-ls-file-keymap map))

     (defadvice eshell-ls-decorated-name (after ted-electrify-ls activate)
       "Eshell's `ls' now lets you click or RET on file names to open them."
       (add-text-properties 0 (length ad-return-value)
                            (list 'help-echo "RET, mouse-2: visit this file"
                                  'mouse-face 'highlight
                                  'keymap eshell-ls-file-keymap)
                            ad-return-value)
       ad-return-value)))

Clickable icons in eshell file listings

Code from https://www.reddit.com/r/emacs/comments/xboh0y/how_to_put_icons_into_eshell_ls/

(with-eval-after-load 'all-the-icons
  (with-eval-after-load 'em-ls
    (defun lem-eshell-prettify (file)
      "Add features to listings in `eshell/ls' output.
The features are:
1. Add decoration like 'ls -F':
 * Mark directories with a `/'
 * Mark executables with a `*'
2. Make each listing into a clickable link to open the
corresponding file or directory.
3. Add icons (requires `all-the-icons`)
This function is meant to be used as advice around
`eshell-ls-annotate', where FILE is the cons describing the file."
      (let* ((name (car file))
             (icon (if (eq (cadr file) t)
                       (all-the-icons-icon-for-dir name)
                     (all-the-icons-icon-for-file name)))
             (suffix
              (cond
               ;; Directory
               ((eq (cadr file) t)
                "/")
               ;; Executable
               ((and (/= (user-uid) 0) ; root can execute anything
                     (eshell-ls-applicable (cdr file) 3 #'file-executable-p (car file)))
                "*"))))
        (cons
         (concat " "
                 icon
                 " "
                 (propertize name
                             'keymap eshell-ls-file-keymap
                             'mouse-face 'highlight
                             'file-name (expand-file-name (substring-no-properties (car file)) default-directory))
                 (when (and suffix (not (string-suffix-p suffix name)))
                   (propertize suffix 'face 'shadow)))
         (cdr file)
         )))

    (advice-add #'eshell-ls-annotate :filter-return #'lem-eshell-prettify)))

Org-mode (personal information manager)

  • INACTIVE timestamp (C-c !): Insert a date but don’t process for the agenda at all.
  • PLAIN timestamp (C-c .): Appointments/calendar events that don’t carry over after their time
  • SCHEDULED timestamp (C-c C-s): Shows on the agenda; if not completed, will show up until DONE.
  • DEADLINE timestamp (C-c C-d): By when something must be completed; comes with a warning ahead of time in the agenda.

org setup

(require 'org)
(require 'org-mouse) ;; Require mouse support before loading any org files
;; :load-path "~/.emacs.d/src/org-mode/lisp/"
(delight 'org-indent-mode)
(setq org-directory (ct/file-join "~" "Pending" "org")
      ct/org-gtd-directory (ct/file-join org-directory "gtd")
      org-default-notes-file (ct/file-join ct/org-gtd-directory "inbox.org")
      org-agenda-files (list ct/org-gtd-directory)

      org-imenu-depth 4
      org-startup-folded 'fold         ; per file: #+STARTUP: overview
      org-startup-indented t           ; per file: #+STARTUP: indent
      org-startup-with-inline-images t ; #+STARTUP: inlineimages
      org-hide-leading-stars t         ; per file: #+STARTUP: hidestars
      org-cycle-separator-lines 0      ; 0=Hide all blank lines between collapsed items
      org-fontify-whole-heading-line t ; Apply style to whole line for overlines/bg color

      org-tags-sort-function 'org-string-collate-lessp ; Sort tags

      ;; t          : Toggle visibility only in headlines
      ;; 'whitestart: Toggle outlines everywhere except at line beginnings (to indent there)
      org-cycle-emulate-tab t
      ;; Instead of "..." to expand, show this:
      org-ellipsis "•••")

;; Disable electric indentation of list items (inserting spaces at line start)
(defun ct/disable-electric-indent-local-mode () (electric-indent-local-mode -1))
(add-hook 'org-mode-hook #'ct/disable-electric-indent-local-mode)

(defun ct/turn-off-eldoc-mode () (eldoc-mode -1))
(add-hook 'org-mode-hook #'ct/turn-off-eldoc-mode)

;; I don't cycle through org files and want to free this shortcut.
(unbind-key "C-," org-mode-map)
(unbind-key "C-'" org-mode-map)
;; I also prefer shift-allowkey selection over priority shifts and TODO tag shifts
(setq org-support-shift-select t)
(unbind-key "S-<left>" org-mode-map)
(unbind-key "S-<right>" org-mode-map)
(unbind-key "S-<up>" org-mode-map)
(unbind-key "S-<down>" org-mode-map)

;; Free the prev/next tab keys on macOS
(unbind-key "M-{" org-mode-map)
(define-key org-mode-map (kbd "M-[") #'org-backward-element)
(unbind-key "M-}" org-mode-map)
(define-key org-mode-map (kbd "M-]") #'org-backward-element)

(global-set-key (kbd "<f12>") #'org-capture)
(global-set-key (kbd "C-c b") #'org-switchb)

Search heading in all org files

C-u 4 org-refile will not move the current item, but jump to the destination instead. Yiming Chen made a helper to do this programmatically. I want auto-completion to suggest this for “org-goto”

(defun org-goto-global/ct ()
  "Jump to org heading in any org buffer using `org-refile'."
  (interactive)
  (org-refile '(4)))

Backtab to fold current heading

(defun ct/org-foldup ()
  "Hide the entire subtree from root headline at point."
  (interactive)
  (while (ignore-errors (outline-up-heading 1)))
                                        ; Hide/fold subtree
  (org-flag-subtree t))
(defun ct/org-shifttab (&optional arg)
  (interactive "P")
  (if (or (null (org-current-level))     ; point is before 1st heading, or
          (and (= 1 (org-current-level)) ; at level-1 heading, or
               (org-at-heading-p))
          (org-at-table-p))              ; in a table (to preserve cell movement)
                                        ; perform org-shifttab at root level elements and inside tables
      (org-shifttab arg)
                                        ; try to fold up elsewhere
    (ct/org-foldup)))
(org-defkey org-mode-map (kbd "S-<tab>") #'ct/org-shifttab)

Emphasis

(defun ct/org-emphasize-below-point (&optional char)
  (interactive)
  (unless (region-active-p)
    (backward-word)
    (mark-word))
  (org-emphasize char))
(define-key org-mode-map [remap org-emphasize] #'ct/org-emphasize-below-point)

Hide emphasis syntax (FoldingText mode)

(setq org-hide-emphasis-markers t)

The auto-hiding makes editing emphasized text hard, though, because the point position doesn’t indicate if you’re inside or outside of the emphasis pairs. Make them appear when point is in emphasized text to fix this:

(use-package org-appear
  :demand
  :ensure
  :hook (org-mode . org-appear-mode))

Tags

Stole the idea for negative boxes and small tags from org-modern: https://github.com/minad/org-modern

(with-eval-after-load 'org
  (set-face-attribute
   'org-tag
   nil
   :family 'variable-pitch
   :width 'condensed :height 0.6 :weight 'medium
   :foreground (face-attribute 'default :foreground nil t)
   :background "#efefef"
   :box
   (let ((border (max 3 (cond
                         ((integerp line-spacing)
                          line-spacing)
                         ((floatp line-spacing)
                          (ceiling (* line-spacing (frame-char-height))))
                         (t (/ (frame-char-height) 10))))))
     (list :color (face-attribute 'default :background nil t)
           :line-width
           ;; Emacs 28 supports different line horizontal and vertical line widths
           (if (eval-when-compile (>= emacs-major-version 28))
               (cons 0 (- border))
             (- border))))))

Babel

(org-babel-do-load-languages
 'org-babel-load-languages
 '((dot . t)
   (emacs-lisp . t)
   (ruby . t)
   (markdown . t)
   (python . t)
   (shell . t)))

(setq org-src-tab-acts-natively t
      org-src-fontify-natively t
      org-edit-src-content-indentation 0
      ;; Threshold when to stop prepending lines with colons and make EXAMPLE block instead
      org-babel-min-lines-for-block-output 5)

;; Update images when running babel blocks
(add-hook 'org-babel-after-execute-hook #'org-redisplay-inline-images)

Markdown block evaluation results

ob-markdown is not part of any package repository, so I just downloaded it locally from source:

(use-package emacs
  :after (org markdown-mode)
  :ensure markdown-mode
  :init
  (load-file "~/.emacs.d/src/ob-markdown.el"))

See the org-babel :result header parameter docs: I don’t want the default where colons are prepended. A HTML block makes most sense for Markdown output.

;; (setq org-babel-default-header-args:markdown '())
(add-to-list 'org-babel-default-header-args:markdown
             '(:results . "output verbatim html"))

PlantUML graphics

(use-package plantuml-mode
  :defer :demand
  :config
  (setq org-plantuml-exec-mode 'plantuml)  ;; Use the plantuml executable instead of the Java JAR
  (add-to-list 'org-src-lang-modes '("plantuml" . plantuml))
  (org-babel-do-load-languages 'org-babel-load-languages '((plantuml . t))))

Tags

The string format of the alist (("orange" . 111) ("purple" . 112) ("blue" . 98) ("yellow" . 121) ("green" . 103) ("red" . 114)) as a string via (org-tag-alist-to-string org-tag-alist) is: ”orange(o) purple(p) blue(b) yellow(y) green(g) red(r)”.

;; Tags with fast selection keys
(setq org-tag-alist nil)
(setq org-tag-persistent-alist '(;; Development
                                 (:startgroup . nil)
                                 ("feature" . ?f)
                                 ("bug" . ?b)
                                 (:endgroup . nil)))

;; Allow setting single tags without the menu
(setq org-fast-tag-selection-single-key 'expert)

;; For tag searches ignore tasks with scheduled and deadline dates
(setq org-agenda-tags-todo-honor-ignore-options t)

Links

See: https://www.gnu.org/software/emacs/manual/html_node/org/Handling-links.html

(global-set-key (kbd "C-c l") 'org-store-link)

Zettelkasten link type via zettel: prefix

(defun org-thearchive-open (str)
  (browse-url (concat "thearchive://match/" str)))
(org-link-set-parameters
 "zettel"
 :follow #'org-thearchive-open

 ;; Don't fold links but show ID and description:
 ;;   [[zettel:202102101021][Title or description here]]”
 ;;:display 'full
 )

ol-notmuch: capture with notmuch mail link with the notmuch: prefix

(use-package ol-notmuch
  :after (org notmuch)
  :ensure notmuch)

Capture

(global-set-key (kbd "C-c c") 'org-capture)

(defvar org-created-property-name "CREATED"
  "The name of the org-mode property that stores the creation date of the entry")

(defun org-set-created-property (&optional active NAME)
  "Set a property on the entry giving the creation time.

By default the property is called CREATED. If given the `NAME'
argument will be used instead. If the property already exists, it
will not be modified."
  (interactive)
  (let* ((created (or NAME org-created-property-name))
         (fmt (if active "<%s>" "[%s]"))
         (now (format fmt (format-time-string "%Y-%m-%d %a %H:%M"))))
    (unless (org-entry-get (point) created nil)
      (org-set-property created now))))

(add-hook 'org-capture-before-finalize-hook #'org-set-created-property)

Generic Templates

(setq ct/org-diary-file (ct/file-join org-directory "diary" "diary.org"))
(setq org-capture-templates
      `(("t" "Task" entry (file org-default-notes-file)
         "* TODO %?\n%a\n")
        ("w" "Waiting for" entry (file ,(ct/file-join ct/org-gtd-directory "tickler.org"))
         "* WAITING %?\n%a\n")
        ("e" "Event" entry
         (file+headline ,(ct/file-join ct/org-gtd-directory "privat.org") "Events")
         "** %? \n %^T \n")
        ("j" "Journal" entry (file+datetree ct/org-diary-file)
         "* %?\n\n")
        ("n" "Naikan Therapy" entry (file+datetree ct/org-diary-file)
         "* Naikan :naikan:\n- What have I received from ...?\n- What have I given to ...?\n- What troubles and difficulties have I caused ...?\n- Affirmations:")
        ("w" "Work review" entry (file+datetree ct/org-diary-file)
         (file "~/.emacs.d/capture-templates/work-review.org"))))

App support capture templates

(with-eval-after-load 'org-capture
  ;; Source: https://emacs.stackexchange.com/a/5931/189b86
  (defun ct/org-get-target-headline (&optional targets prompt)
    "Prompt for a location in an org file and jump to it.

This is for promping for refile targets when doing captures.
Targets are selected from `org-refile-targets'. If TARGETS is
given it temporarily overrides `org-refile-targets'. PROMPT will
replace the default prompt message.

If CAPTURE-LOC is is given, capture to that location instead of
prompting."
    (let ((org-refile-targets (or targets org-refile-targets))
          (prompt (or prompt "Capture Location")))
      (org-refile t nil nil prompt)))

  (push '("f" "File email for apps") org-capture-templates)
  (push `("fa" "File > The Archive"
          entry (file+function ,(ct/file-join ct/org-gtd-directory "thearchive.org") 'ct/org-get-target-headline)
          "* TODO %?\n- %:from %u\n"
          :jump-to-captured t)
        org-capture-templates)
  (push `("ft" "File > TableFlip"
          entry (file+function ,(ct/file-join ct/org-gtd-directory "tableflip.org") 'ct/org-get-target-headline)
          "* TODO %?\n- %:from %u\n"
          :jump-to-captured t)
        org-capture-templates))

Refiling settings

(via https://blog.aaronbieber.com/2017/03/19/organizing-notes-with-refile.html)

;; Refile into agenda aware files & into the current file
(setq org-refile-targets (quote ((nil :maxlevel . 9)
                                 (org-agenda-files :maxlevel . 3))))
(setq org-refile-use-outline-path 'file) ;t)                      ;; 'file=Allow prepending file names when refiling
(setq org-outline-path-complete-in-steps nil)                 ;; nil=Generate all files and headings at once
(setq org-refile-allow-creating-parent-nodes 'confirm)        ;; Allow subheading creation
(setq org-completion-use-ido nil) ;; I'm using ivy

Exclude DONE state tasks from refile targets

from http://doc.norang.ca/org-mode.html#RefileSetup:

(defun bh/verify-refile-target ()
  "Exclude todo keywords with a done state from refile targets"
  (not (member (nth 2 (org-heading-components)) org-done-keywords)))

(setq org-refile-target-verify-function 'bh/verify-refile-target)

Checkboxes

By default, checkboxes would inherit the variable-pitch-mode’s value; but I find monospaced checkboxes look better:

(set-face-attribute 'org-checkbox nil :inherit 'fixed-pitch)

Don’t repeat 1st level org headline faces until level 16

Level 1 is set to large; skip repeating level-1 when encountering level-8.

Post: https://christiantietze.de/posts/2022/11/org-mode-more-outline-levels/

(defface org-level-9 '((t :inherit org-level-2))
  "Face used for level 9 headlines."
  :group 'org-faces)
(defface org-level-10 '((t :inherit org-level-3))
  "Face used for level 10 headlines."
  :group 'org-faces)
(defface org-level-11 '((t :inherit org-level-4))
  "Face used for level 11 headlines."
  :group 'org-faces)
(defface org-level-12 '((t :inherit org-level-5))
  "Face used for level 12 headlines."
  :group 'org-faces)
(defface org-level-13 '((t :inherit org-level-6))
  "Face used for level 13 headlines."
  :group 'org-faces)
(defface org-level-14 '((t :inherit org-level-7))
  "Face used for level 14 headlines."
  :group 'org-faces)
(defface org-level-15 '((t :inherit org-level-8))
  "Face used for level 15 headlines."
  :group 'org-faces)
(setq org-level-faces (append org-level-faces (list 'org-level-9 'org-level-10 'org-level-11 'org-level-12 'org-level-13 'org-level-14 'org-level-15)))
(setq org-n-level-faces (length org-level-faces))

Task management

Docs: When defining multiple sequences, make it so that no element appears in multiple sequences. Otherwise, org won’t be able to calculate the number of done/not-done items in callbacks properly, for example.

(setq org-deadline-warning-days 30)

;; Attach files as copies with UUID
(setq org-id-method (quote uuidgen))

(setq org-todo-keywords
      (quote ((sequence "TODO(t)" "NEXT(n)" "DOING(g)" "|" "DONE(d)")
              (sequence "WAITING(w@/!)" "HOLD(h@/!)")
              (sequence "REPORT(r)" "BUG(b)" "KNOWNCAUSE(k)" "|" "FIXED(f)")
              (sequence "|" "CANCELLED(c@/!)")
              (sequence "PROJECT(p)"))))
(setq org-todo-state-tags-triggers
      (quote (("CANCELLED" ("cancelled" . t))
              ("WAITING" ("waiting" . t))
              ("BUG" ("waiting") ("bug" . t))
              ("HOLD" ("waiting") ("hold" . t))
              ("TODO" ("waiting") ("cancelled") ("hold"))
              ("NEXT" ("waiting") ("cancelled") ("hold"))
              ("DOING" ("waiting") ("cancelled") ("hold"))
              ("DONE" ("waiting") ("cancelled") ("hold")))))
(setq org-use-fast-todo-selection t) ;; C-c C-t KEY

Make M-S-RET insert TODO item without remembering the current item’s state. That’s not useful in 99% of the cases.

(defun ct/org-insert-initial-todo-heading (&rest args)
  "Insert a new heading with the same level and initial TODO state.
Behaves like `org-insert-todo-heading' when called with prefix
argument: it doesn't keep the current heading's TODO state.

Actual prefix arguments will be forwarded, but defaults to universal prefix:
With two prefix args, force inserting at end of parent subtree."
  (interactive "P")
  (let ((current-prefix-arg (or current-prefix-arg
                                '(4))))
    (require 'org)
    (call-interactively #'org-insert-todo-heading)))

(define-key org-mode-map [remap org-insert-todo-heading] #'ct/org-insert-initial-todo-heading)

Change to DONE when subtasks are DONE

Via https://emacs.stackexchange.com/questions/19843/how-to-automatically-adjust-an-org-task-state-with-its-children-checkboxes

(defun org-todo-if-needed (state)
  "Change header state to STATE unless the current item is in STATE already."
  (unless (string-equal (org-get-todo-state) state)
    (org-todo state)))

(defun ct/org-summary-todo-cookie (n-done n-not-done)
  "Switch header state to DONE when all subentries are DONE, to TODO when none are DONE, and to DOING otherwise"
  (let (org-log-done org-log-states)   ; turn off logging
    (org-todo-if-needed (cond ((= n-done 0)
                               "TODO")
                              ((= n-not-done 0)
                               "DONE")
                              (t
                               "DOING")))))
(add-hook 'org-after-todo-statistics-hook #'ct/org-summary-todo-cookie)

(defun ct/org-summary-checkbox-cookie ()
  "Switch header state to DONE when all checkboxes are ticked, to TODO when none are ticked, and to DOING otherwise"
  (let (beg end)
    (unless (not (org-get-todo-state))
      (save-excursion
        (org-back-to-heading t)
        (setq beg (point))
        (end-of-line)
        (setq end (point))
        (goto-char beg)
        ;; Regex group 1: %-based cookie
        ;; Regex group 2 and 3: x/y cookie
        (if (re-search-forward "\\[\\([0-9]*%\\)\\]\\|\\[\\([0-9]*\\)/\\([0-9]*\\)\\]"
                               end t)
            (if (match-end 1)
                ;; [xx%] cookie support
                (cond ((equal (match-string 1) "100%")
                       (org-todo-if-needed "DONE"))
                      ((equal (match-string 1) "0%")
                       (org-todo-if-needed "TODO"))
                      (t
                       (org-todo-if-needed "DOING")))
              ;; [x/y] cookie support
              (if (> (match-end 2) (match-beginning 2)) ; = if not empty
                  (cond ((equal (match-string 2) (match-string 3))
                         (org-todo-if-needed "DONE"))
                        ((or (equal (string-trim (match-string 2)) "")
                             (equal (match-string 2) "0"))
                         (org-todo-if-needed "TODO"))
                        (t
                         (org-todo-if-needed "DOING")))
                (org-todo-if-needed "DOING"))))))))
(add-hook 'org-checkbox-statistics-hook #'ct/org-summary-checkbox-cookie)

Agenda

(global-set-key (kbd "C-c a") #'org-agenda)

;; Focus on 1 week by default
(setq org-agenda-span 'week)
(setq org-agenda-window-setup 'current-window)

(setq org-icalendar-combined-agenda-file "~/Pending/org/calendar-export.ics")

;; Limit org-agenda tags to show flush-right right at the 100th column via `-100`
;;and auto-enable olivetti-mode with that width.
(defvar ct/org-width 100)
(defvar ct/org-agenda-width ct/org-width)
(setq org-agenda-tags-column (- ct/org-agenda-width))
(setq org-tags-column 0) ;; Show tags right after heading; the spacing only causes layout problems
(setq org-agenda-remove-tags t)

;; Draw horizontal rule between agenda items (can also be set per agenda command in shared settings)
(setq org-agenda-compact-blocks nil)

;; Using a character value instead of a string with multiple chars would end up adding more characters than fit Olivetti's width.
(setq org-agenda-block-separator (make-string ct/org-agenda-width (string-to-char "")))

;; Show inbox item counter at top of agenda
(defun ct/count-org-items (path)
  "Return the number of outline items in a file at PATH."
  (with-temp-buffer
    (insert-file-contents path)
    (let* ((start 0)
           (counted 0))
      (while (string-match "^* " (buffer-string) start)
        (setq start (match-end 0))
        (setq counted (+ 1 counted)))
      counted)))

(defun ct/org-agenda-show-inbox-counter (&rest _ignore)
  (let* ((file org-default-notes-file)
         (counter (ct/count-org-items file))
         (label (cond ((eq 1 counter) "Item") (t "Items"))))
    (if (eq counter 0)
        (insert (propertize "Inbox Empty!\n" 'face 'org-agenda-structure))
      (insert (propertize (concat "["
                                  ;; link to inbox file
                                  "[file:" file "]"
                                  ;; display inbox item count
                                  "[" (number-to-string counter) " " label " in the Inbox]"
                                  "]\n")
                          'face 'org-agenda-structure)))))


(defun ct/iconified-header (icon text &optional skip-leading-newline)
  (concat (unless skip-leading-newline "\n")
          (all-the-icons-faicon icon :v-adjust 0.01) " " text))
(defun ct/inbox-task-header ()
  (concat (number-to-string (ct/count-org-items org-default-notes-file)) " Tasks in Inbox"))

(defun ct/org-agenda-set-wrap-prefix ()
  (setq-local wrap-prefix "                          "))
(add-hook 'org-agenda-mode-hook #'ct/org-agenda-set-wrap-prefix)

(setq org-agenda-custom-commands (quote
                                  ;; `C-a d` is my main Agenda
                                  (("d" "Daily Action List"
                                    (
                                     (tags "+focus"
                                           ((org-agenda-overriding-header (ct/iconified-header "compass" "Foci" t))
                                            (org-tags-match-list-sublevels nil)
                                            (org-agenda-todo-ignore-scheduled 'future)))

                                     (agenda "" ((org-agenda-overriding-header " ") ;; Non-empty string
                                                 (org-agenda-span 'day)
                                                 (org-agenda-ndays 1)
                                                 (org-agenda-sorting-strategy (quote ((agenda time-up priority-down tag-up))))

                                                 ;; Do not include scheduled, due or overdue items here
                                                 (org-deadline-warning-days 0)
                                                 (org-scheduled-past-days 0)
                                                 (org-deadline-past-days 0)

                                                 (org-agenda-skip-scheduled-if-done t)
                                                 (org-agenda-skip-timestamp-if-done t)
                                                 (org-agenda-skip-deadline-if-done t)))

                                     (agenda "" ((org-agenda-overriding-header "\nNext three days\n")
                                                 (org-agenda-start-on-weekday nil)
                                                 (org-agenda-start-day "+1d")
                                                 (org-agenda-span 3)
                                                 (org-deadline-warning-days 0)
                                                 (org-agenda-block-separator nil)
                                                 (org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))))

                                     (agenda "" ((org-agenda-overriding-header "\nOverdue")
                                                 (org-agenda-time-grid nil)
                                                 (org-agenda-start-on-weekday nil)
                                                 (org-agenda-show-all-dates nil)
                                                 (org-agenda-format-date "")  ;; Skip the date
                                                 (org-agenda-span 1)
                                                 (org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))
                                                 (org-agenda-entry-types '(:deadline :scheduled))
                                                 (org-scheduled-past-days 999)
                                                 (org-deadline-past-days 999)
                                                 (org-deadline-warning-days 0)))

                                     (agenda "" ((org-agenda-overriding-header "\nUpcoming deadlines (+14d)\n")
                                                 (org-agenda-time-grid nil)
                                                 (org-agenda-start-on-weekday nil)
                                                 ;; We don't want to replicate the previous section's
                                                 ;; three days, so we start counting from the day after.
                                                 (org-agenda-start-day "+3d")
                                                 (org-agenda-span 14)
                                                 (org-agenda-show-all-dates nil)
                                                 (org-agenda-time-grid nil)
                                                 (org-deadline-warning-days 0)
                                                 (org-agenda-block-separator nil)
                                                 (org-agenda-entry-types '(:deadline))
                                                 (org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))))

                                     (tags "+inbox"  ;; or limit to (org-agenda-files '(".../inbox.org"))
                                           ((org-agenda-overriding-header (ct/iconified-header "inbox" (ct/inbox-task-header)))
                                            (org-tags-match-list-sublevels nil)
                                        ; TODO: customize (org-agenda-sorting-strategy ...) for reversed order?
                                            (wrap-prefix "       ")
                                            (org-agenda-max-entries 5)))
                                     (tags-todo "TODO=\"NEXT\"-focus"
                                                ((org-agenda-overriding-header (ct/iconified-header "tasks" "Pull: Next Actions"))
                                                 (wrap-prefix "       ")
                                                 (org-tags-match-list-sublevels nil)
                                                 (org-agenda-todo-ignore-scheduled 'future)))
                                     (tags-todo "-TODO=\"CANCELLED\"+waiting|+TODO=\"WAITING\"|HOLD/!"
                                                ((org-agenda-overriding-header (ct/iconified-header "hourglass-end" "Waiting & Postponed"))
                                                 (wrap-prefix "       ")
                                                 (org-tags-match-list-sublevels nil)
                                                 (org-agenda-todo-ignore-scheduled 'future)))
                                     ))
                                   ("D" "Test Agenda"
                                    ((agenda "" nil)
                                     (tags "+inbox" ((org-agenda-overriding-header "Tasks in Inbox")
                                                     (org-tags-match-list-sublevels nil)
                                                     (org-agenda-max-entries 5)
                                                     ))
                                     (tags-todo "-CANCELLED+WAITING|HOLD/!"
                                                ((org-agenda-overriding-header "Waiting and Postponed Tasks")
                                                 (org-tags-match-list-sublevels nil)
                                                 (org-agenda-todo-ignore-scheduled 'future)
                                                 ))
                                     ))
                                   )))

Group Org buffers when they’re related to the agenda in a dedicated tab.

(defun ct/display-buffer-org-agenda-managed-p (buffer-name action)
  "Determine whether BUFFER-NAME is an org-agenda managed buffer."
  (with-current-buffer buffer-name
    (and (derived-mode-p 'org-mode)
         (member (buffer-file-name) (org-agenda-files)))))

(add-to-list 'display-buffer-alist
             `("\\*Org Agenda\\*"
               (display-buffer-in-tab display-buffer-reuse-mode-window)
               (ignore-current-tab . t)
               (tab-name . "Org Files")
               (window-width . ,ct/org-agenda-width)
               (dedicated . side)  ;; Make the Agenda a dedicated side-window
               (side . left)       ;; to the left so it always stays open.
               (inhibit-same-window . nil)))
(add-to-list 'display-buffer-alist
             '(ct/display-buffer-org-agenda-managed-p
               (display-buffer-reuse-mode-window  ;; Prioritize reuse of current window
                display-buffer-in-tab)            ;; over switching to the Org tab.
               ;; (ignore-current-tab . t)
               (tab-name . "Org Files")))

Ignore tasks targeted at Sascha from shared task lists

(defun ct/org-agenda-list-exclude-sascha-advice (orig-fn &rest args)
  "Exclude selected tags from `org-agenda-list'.
Intended as :around advice for `org-agenda-list'."
  (let ((org-agenda-tag-filter-preset '("-sascha")))
    (funcall orig-fn args)))

(advice-add #'org-agenda-list :around #'ct/org-agenda-list-exclude-sascha-advice)
(advice-add #'org-todo-list   :around #'ct/org-agenda-list-exclude-sascha-advice)

Appointment notifications

From https://lists.gnu.org/archive/html/emacs-orgmode/2013-02/msg00644.html and https://gist.github.com/justinhj/eb2d354d06631076566f#file-gistfile1-el

(setq
 appt-time-msg-list nil         ;; clear existing appt list upon initial launch
 appt-message-warning-time '15  ;; send first warning 15 minutes before appointment
 appt-display-interval '15
 appt-display-mode-line nil     ;; don't show in the modeline

 appt-display-format 'window   ;; pass warnings to the designated window function
 appt-disp-window-function (function ct/appt-display-native))

;; activate appointment notification
(appt-activate 1)

;; Wire org-agenda and appointments together
(defun ct/send-notification (title msg)
  (let ((notifier-path (executable-find "alerter")))
    (start-process
     "Appointment Alert"
     "*Appointment Alert*" ;; nil to not capture output
     notifier-path
     "-message" msg
     "-title" title
     "-sender" "org.gnu.Emacs"
     "-activate" "org.gnu.Emacs")))
(defun ct/appt-display-native (min-to-app new-time msg)
  (ct/send-notification
   (format "Appointment in %s minutes" min-to-app)    ;; passed to -title in terminal-notifier call
   (format "%s" msg)))                                ;; passed to -message in terminal-notifier call

;; Agenda-to-appointent hooks
(org-agenda-to-appt)

;; update appt list on agenda view and every hour
(run-at-time "24:01" 3600 'org-agenda-to-appt)
(add-hook 'org-finalize-agenda-hook #'org-agenda-to-appt)

org-autolist: Return continues list in current context

(use-package org-autolist
  :load-path "/Users/ctm/.emacs.d/src/org-autolist"
  :ensure t
  :delight
  :config
  (setq org-autolist-enable-delete nil)
  (add-hook 'org-mode-hook (lambda () (org-autolist-mode))))

org-contrib: since org 9.5, 3rd party package contributions now live elsewhere

https://git.sr.ht/~bzg/org-contrib

The transition is intended to move each module into its own package, and to not rely on the ‘contrib’ folder/repo at all.

org-trello: sync with Trello boards

(use-package org-trello
  :load-path "~/.dotfiles/emacs.d/src/org-trello"
  :after org
  :config
  (custom-set-variables '(org-trello-files
			    (seq-filter (lambda (f) (string-suffix-p "_trello.org" f))
				       (org-files-list)))))

org-menu displays a transient menu to discover functions

(use-package org-menu
  :after org
  :load-path "~/.dotfiles/emacs.d/src/org-menu"
  :config
  (define-key org-mode-map (kbd "C-c m") 'org-menu))

org-view-mode strips down Org to a file viewer for READMEs or presentations

(use-package org-view-mode
  :ensure)

Displaying ^L form feed as separator

(define-advice org-flag-region (:around (oldfun from to flag &optional spec) unfold-page-breaks)
  "Unfold all form feed characters lines inside folded region."
  (funcall oldfun from to flag spec)
  (when (and flag (not (eq 'visible spec)))
    (org-with-point-at from
      (while (re-search-forward "\n\u000c" to t)
	    (org-flag-region (match-beginning 0) (match-end 0) t 'visible)))))

Editor behavior

Make text mode the default (I never use Fundamental for anything)

(setq default-major-mode 'text-mode)

Default fill-column width: 100

We’re not on old IBM machines anymore, and the 80 character rule breaks down for me with org-mode when I indent 3 levels. I find 100 columns to work ok. That still gives me some margin in olivetti-mode, which is on by default when writing.

(use-package emacs
  :config
  (setq fill-column 100))

Scrolling

  • At first, good-scroll-mode looked nice because of the momentum, but it turns out that scrolling large folded org documents or mail histories doesn’t work at all: as if it cannot pass a threshold.
  • Built-in pixel-scroll-mode looked nice, too, until it broke down time and again in large org documents and emails with attachments. Scrolling down past an image worked ok, but not back up. In my org agenda, it wouls often hang when scrolling using the external mouse.
  • iscroll-mode, too, didn’t work properly with large images.

The best solution I dound is the Emacs mac fork: https://bitbucket.org/mituharu/emacs-mac/

;; Scrolling is moving the document, not my cursor
(setq scroll-preserve-screen-position nil)

Scroll bar

mlscroll draws a horizontal scroll bar in the modeline that replaces the % and top/bottom indicators, and also shows how much of the document you’re actually seeing right now.

(use-package mlscroll
  :hook
  (after-init . mlscroll-mode))

mlscroll-in-color references the region color, but modus-themes v4 uses e.g. a faint yellow selection color, which is not so sexy.

(with-eval-after-load "modus-themes"
  (with-eval-after-load "mlscroll"
    (defun ct/modus-themes-mlscroll-colors ()
      (modus-themes-with-colors
        (customize-set-variable 'mlscroll-in-color blue-faint)
        (customize-set-variable 'mlscroll-out-color bg-main)))
    (add-hook 'modus-themes-after-load-theme-hook #'ct/modus-themes-mlscroll-colors)))

Folding

Enable hs-minor-mode in e.g. source files to fold regions.

Default keybindings:

C-c @ C-M-s show all C-c @ C-M-h hide all C-c @ C-s show block C-c @ C-h hide block C-c @ C-c toggle hide/show

HideShow: Hiding leaf nodes only

From the extensions at EmacsWiki/HideShow, this does not hide everything up to the root level (e.g. classes in code) but to leaf nodes (e.g. methods in classes):

  • C-c @ C-M-s show all
  • C-c @ C-M-h hide all
  • C-c @ C-s show block
  • C-c @ C-h hide block
  • C-c @ C-c toggle hide/show

Cyclical folding by Karthik:

(use-package hideshow
  :config
  (defun hs-hide-leafs-recursive (minp maxp)
    "Hide blocks below point that do not contain further blocks in region (MINP MAXP)."
    (when (hs-find-block-beginning)
      (setq minp (1+ (point)))
      (funcall hs-forward-sexp-func 1)
      (setq maxp (1- (point))))
    (unless hs-allow-nesting
      (hs-discard-overlays minp maxp))
    (goto-char minp)
    (let ((leaf t))
      (while (progn
               (forward-comment (buffer-size))
               (and (< (point) maxp)
                    (re-search-forward hs-block-start-regexp maxp t)))
	    (setq pos (match-beginning hs-block-start-mdata-select))
	    (if (hs-hide-leafs-recursive minp maxp)
            (save-excursion
              (goto-char pos)
              (hs-hide-block-at-point t)))
	    (setq leaf nil))
      (goto-char maxp)
      leaf))

  (defun hs-hide-leafs ()
    "Hide all blocks in the buffer that do not contain subordinate blocks.  The hook `hs-hide-hook' is run; see `run-hooks'."
    (interactive)
    (hs-life-goes-on
     (save-excursion
       (message "Hiding blocks ...")
       (save-excursion
	     (goto-char (point-min))
	     (hs-hide-leafs-recursive (point-min) (point-max)))
       (message "Hiding blocks ... done"))
     (run-hooks 'hs-hide-hook)))

  (defun hs-cycle (&optional level)
    (interactive "p")
    (let (message-log-max
          (inhibit-message t))
      (if (= level 1)
          (pcase last-command
            ('hs-cycle
             (hs-hide-level 1)
             (setq this-command 'hs-cycle-children))
            ('hs-cycle-children
             ;; TODO: Fix this case. `hs-show-block' needs to be
             ;; called twice to open all folds of the parent
             ;; block.
             (save-excursion (hs-show-block))
             (hs-show-block)
             (setq this-command 'hs-cycle-subtree))
            ('hs-cycle-subtree
             (hs-hide-block))
            (_
             (if (not (hs-already-hidden-p))
		         (hs-hide-block)
               (hs-hide-level 1)
               (setq this-command 'hs-cycle-children))))
	    (hs-hide-level level)
	    (setq this-command 'hs-hide-level))))

  (defun hs-global-cycle ()
    (interactive)
    (pcase last-command
      ('hs-global-cycle
       (save-excursion (hs-show-all))
       (setq this-command 'hs-global-show))
      (_ (hs-hide-all))))

  (define-key hs-minor-mode-map (kbd "C-<tab>") #'hs-cycle)
  (add-hook 'prog-mode-hook #'hs-minor-mode))

Customized toggle shortcuts

(global-set-key (kbd "C-<tab>") 'hs-toggle-hiding)
(global-set-key (kbd "C-M-<tab>") 'hs-hide-leafs)

Line numbering configuration

The classic linum-mode is written in Lisp; but display-line-numbers-mode is available since Emacs 26 and written in C. It’s supposedly a ton faster and doesn’t stutter in large files, at all.

Relative line numbers used to be provided by linum-relative-mode, but display-line-numbers-mode provides the same functionality:

(setq-default display-line-numbers nil ;; Disable by default
              display-line-numbers-type 'visual) ;; 'relative + counting wrapped lines

When we show line numbers at all, show 4 characters worth of them to prevent re-layouting once we hit 100. That horizontal movement is quite annoying.

(setq-default display-line-numbers-width 4)

Bind the toggle to something convenient, overriding global line number mode:

(with-eval-after-load "xah-fly-keys"
  ;; Was: #'global-display-line-numbers-mode
  (define-key xah-fly-leader-key-map (kbd "l 4") #'display-line-numbers-mode))

Scaling text via C-- and C-+ doesn’t affect/update the line numbers by default:

(defun ct/post-text-scale-callback ()
  (let ((new-size (floor (* (face-attribute 'default :height)
                            (expt text-scale-mode-step text-scale-mode-amount)))))
    (set-face-attribute 'line-number nil :height new-size)
    (set-face-attribute 'line-number-current-line nil :height new-size)))
(add-hook 'text-scale-mode-hook 'ct/post-text-scale-callback)

Indentation: 4 Spaces, no Tabs

See Emacs indentation tutorial for details.

Indent by 4 spaces instead of adhering to tab stops:

(setq-default indent-tabs-mode nil
	          tab-width 4)

Consider not being smart about TAB-to-indent and always insert 4 spaces by adding this: (setq indent-line-function (quote insert-tab))

2-Space indentation helper

Used by some programming mode hooks to share the 2-spaces setting.

(defun ct/two-space-indentation ()
  (setq indent-tabs-mode nil)
  (setq tab-width 2))

Incrementally select surrounding semantic region (word, sentence, …) with expand-region

With repeated C-= presses, expand from point to word, sexp, quote, parens, paragraph, …

https://github.com/magnars/expand-region.el

(use-package expand-region
  :demand
  :bind
  ;; `C-- C-=` (negative prefix) shrinks region
  ("C-=" . er/expand-region))

In xah-fly-keys’s command mode, 8 expands to brackets and quotes, but the rest is a bit unforseeable. Move it to shift-8 and bind 8 to use expand-region instead:

(with-eval-after-load "xah-fly-keys"
  (with-eval-after-load "expand-region"
    (define-key xah-fly-command-map "8" #'er/expand-region)
    (define-key xah-fly-command-map "*" #'xah-extend-selection)))

Visual line mode everywhere

(use-package emacs
  :delight visual-line-mode
  :config
  (defun turn-off-visual-line-mode ()
    "Disable `visual-line-mode' in current buffer."
    (visual-line-mode -1))
  :hook (after-init . global-visual-line-mode))

Disable lighter for other default typing modes

(use-package emacs
  :delight
  (auto-fill-function " AF")
  (global-subword-mode)
  (subword-mode))

which-key shows what you can type

Discovered through Protesilaos and Xah Lee, which-key can be used to discover available key chords. So if you don’t know the projectile commands, hit C-c p and wait, and then a little window will tell you the available keys to press.

It’s basically printing a key map.

(use-package which-key
  :delight
  :config
  (setq which-key-dont-use-unicode t)
  (setq which-key-add-column-padding 2)
  (setq which-key-show-early-on-C-h nil)
  (setq which-key-idle-delay 1.0) ;most-positive-fixnum) ; set this to something like 0.8
  (setq which-key-idle-secondary-delay 0.05)
  (setq which-key-popup-type 'side-window)
  (setq which-key-show-prefix 'echo)
  (setq which-key-max-display-columns 3)
  (setq which-key-separator "  ")
  (setq which-key-special-keys nil)
  (setq which-key-paging-key "<next>")
  (which-key-mode 1))

discover is an optional dependency of some packages to format help via the ‘?’ differently

(use-package discover
  :ensure t)

Screencasting and presentation tools

command-log-mode displays a running history of commands

Use M-x command-log-mode to start/stop the logging, and M-x clm/open-command-log-buffer to show the log in a side window. Useful for screencasting. Xah Lee seems to use that.

The original repo is dormant since 2016 or so. Am using https://github.com/pierre-rouleau/command-log-mode instead.

(use-package command-log-mode
  :load-path "~/.emacs.d/src/command-log-mode"
  :ensure t
  :config
  (setq command-log-mode-window-text-scale -2)
  ;; Workaround for the bug that you have to show the buffer before enabling the mode.
  (setq command-log-mode-auto-show t)

  (defun ct/command-log-mode ()
    "Toggle command-log-mode and force showing/hiding the buffer, no matter what command-log-mode-auto-show says"
    (interactive)
    (if command-log-mode
        (progn
          (clm/close-command-log-buffer)
          (command-log-mode -1))
      (progn (clm/open-command-log-buffer)
             (command-log-mode +1))))
  :init
  (command-log-mode -1)
  (global-command-log-mode -1))

org-tree-slide transforms an outline into a set of (animatable) slides

https://github.com/takaxp/org-tree-slide

Used by Protesilaos Stavrou. David Wilson of SystemCrafters uses org-present.

(use-package org-tree-slide
  :ensure t
  :config
  (setq org-tree-slide-breadcrumbs t)
  (setq org-tree-slide-header nil)
  (setq org-tree-slide-slide-in-effect nil)
  (setq org-tree-slide-heading-emphasis nil)
  (setq org-tree-slide-never-touch-face t)
  (setq org-tree-slide-cursor-init nil) ;;t
  (setq org-tree-slide-modeline-display nil)
  (setq org-tree-slide-skip-done nil)
  (setq org-tree-slide-skip-comments t)
  (setq org-tree-slide-fold-subtrees-skipped t)
  ;; Skipping all subtrees makes it possible to advance to level-1 slides and then unwrap optional content
  (setq org-tree-slide-skip-outline-level 2);8

  (setq org-tree-slide-activate-message
        (format "Presentation %s" (propertize "ON" 'face 'success)))
  (setq org-tree-slide-deactivate-message
        (format "Presentation %s" (propertize "OFF" 'face 'error)))
  (let ((map org-tree-slide-mode-map))
    (define-key map (kbd "<C-down>") #'org-tree-slide-display-header-toggle)
    (define-key map (kbd "<C-right>") #'org-tree-slide-move-next-tree)
    (define-key map (kbd "<C-left>") #'org-tree-slide-move-previous-tree)))

logos-mode uses outline.el to narrow to outline items as “pages” and navigate between them

In org files, this would narrow to each heading level, omitting the * at the beginning of the line.

Enable logos-focus-mode to show the narrowed region in a special presentation style.

(use-package "logos"
  :config
  ;; If you want to use outlines instead of page breaks (the ^L)
  (setq logos-outlines-are-pages t)
  (setq logos-outline-regexp-alist
        `((emacs-lisp-mode . "^;;;+ ")
          (org-mode . "^\\*+ +")
          (markdown-mode . "^[\\#]+ +")
          (t . ,(or outline-regexp logos--page-delimiter))))

  ;; These apply when `logos-focus-mode' is enabled.  Their value is
  ;; buffer-local.
  (setq-default logos-hide-mode-line t
                logos-scroll-lock nil
                logos-variable-pitch t
                logos-hide-buffer-boundaries t
                logos-hide-fringe t
                logos-buffer-read-only nil
                ;; Olivetti might be on already, but better safe than sorry:
                logos-olivetti t)

  (let ((map global-map))
    (define-key map [remap narrow-to-region] #'logos-narrow-dwim)
    (define-key map [remap forward-page] #'logos-forward-page-dwim)
    (define-key map [remap backward-page] #'logos-backward-page-dwim)))

redacted-mode renders all text as Unicode block-drawing characters

Like the “BLOKK” font, but in Emacs, with color support https://github.com/bkaestner/redacted.el

(use-package redacted
  :defer
  :ensure
  :config
  ;; Disable editing of text we can't read
  :hook
  (redacted-mode . (lambda () (read-only-mode (if redacted-mode 1 -1)))))

Typing text

Cursor

I actually like the block cursor type, but the I-beam or “bar” works better with modal inputs like evil-mode or xah-fly-keys.

(use-package emacs
  :init
  (setq-default cursor-type '(bar . 2)))

Pulse-highlight regions

Abin kindly pointed me towards goggles.el which does the same and then some:

(use-package "pulsar"
  :config
  ;; Do not use setq but cutomize-set-variable to call update hooks:
  (customize-set-variable
   'pulsar-pulse-functions
   '(recenter-top-bottom
     move-to-window-line-top-bottom
     reposition-window
     bookmark-jump
     other-window
     delete-window
     delete-other-windows
     forward-page
     backward-page
     scroll-up-command
     scroll-down-command
     windmove-right
     windmove-left
     windmove-up
     windmove-down
     windmove-swap-states-right
     windmove-swap-states-left
     windmove-swap-states-up
     windmove-swap-states-down
     tab-new
     tab-close
     tab-next
     org-next-visible-heading
     org-previous-visible-heading
     org-forward-heading-same-level
     org-backward-heading-same-level
     outline-backward-same-level
     outline-forward-same-level
     outline-next-visible-heading
     outline-previous-visible-heading
     outline-up-heading))
  (setq pulsar-delay 0.04)

  (add-hook 'imenu-after-jump-hook #'pulsar-recenter-top)
  (add-hook 'imenu-after-jump-hook #'pulsar-reveal-entry))

Consult integration

(with-eval-after-load "pulsar"
  (with-eval-after-load "consult"
    (add-hook 'consult-after-jump-hook #'pulsar-recenter-top)
    (add-hook 'consult-after-jump-hook #'pulsar-reveal-entry)))

Dictionary

(use-package define-word
  :ensure t
  :config
  ;; (setq define-word-default-service 'webster)
  (global-set-key (kbd "C-c d") 'define-word-at-point)
  (global-set-key (kbd "C-c D") 'define-word))

Goto previous change

C-u SPC can put you back to a previous insertion point, but this puts you back to the point of a previous change.

;; Unset the "minimize window" keys that called (suspend-frame).
(global-unset-key (kbd "C-z"))

(use-package goto-last-change
  :ensure
  :bind ("C-z" . goto-last-change))

Sentence-level movement: do not require double spaces

(setq sentence-end-double-space nil)

Kill backward to beginning of line

(defun ct/kill-to-beginning-of-line-dwim ()
  "Kill from point to beginning of line.

In `prog-mode', delete up to beginning of actual, not visual
line, stopping at whitespace. Repeat to delete whitespace. In
other modes, e.g. when editing prose, delete to beginning of
visual line only."
  (interactive)
  (let ((current-point (point)))
    (if (not (derived-mode-p 'prog-mode))
        ;; In prose editing, kill to beginning of (visual) line.
        (if visual-line-mode
            (kill-visual-line 0)
          (kill-line 0))
      ;; When there's whitespace at the beginning of the line, go to
      ;; before the first non-whitespace char.
      (beginning-of-line)
      (when (search-forward-regexp (rx (not space)) (point-at-eol) t)
        ;; `search-forward' puts point after the find, i.e. first
        ;; non-whitespace char. Step back to capture it, too.
        (backward-char))
      (kill-region (point) current-point))))
(global-set-key (kbd "s-<backspace>") #'ct/kill-to-beginning-of-line-dwim)

Kill word or region

(defun ct/kill-word-dwim (arg)
  "Kill characters forward until end of a word, or the current region."
  (interactive "p")
  (if (use-region-p)
      (delete-active-region 'kill)
    (kill-word arg)))
(global-set-key (kbd "M-d") #'ct/kill-word-dwim)
(global-set-key (kbd "M-<delete>") #'ct/kill-word-dwim)

Delete char or region

The docs for C-d which is bound to delete-char by default say that for interactive use, one should favor delete-forward-char, because it respects the region. I wonder: why isn’t this the default then?

(global-set-key (kbd "C-d") #'delete-forward-char)

Overwrite selected text when typing

(delete-selection-mode +1)

Default fill-column width (for hard wrapping)

(setq fill-column 80)

Unfill hard line wrapped (“filled”) paragraphs

(use-package unfill
  :ensure t
  :bind (("M-Q" . 'unfill-paragraph)
         ;; Since ⌘⇧Q is bound to "log out user" on macOS, and rebinding this shortcut is flaky, use a toggle instead to get both
         ("M-q" . 'unfill-toggle)))

Transpose lines (move lines)

From EmacsWiki:

(defun move-text-internal (arg)
  (cond
   ((and mark-active transient-mark-mode)
    (if (> (point) (mark))
        (exchange-point-and-mark))
    (let ((column (current-column))
          (text (delete-and-extract-region (point) (mark))))
      (forward-line arg)
      (move-to-column column t)
      (set-mark (point))
      (insert text)
      (exchange-point-and-mark)
      (setq deactivate-mark nil)))
   (t
    (let ((column (current-column)))
      (beginning-of-line)
      (when (or (> arg 0) (not (bobp)))
        (forward-line)
        (when (or (< arg 0) (not (eobp)))
          (transpose-lines arg))
        (forward-line -1))
      (move-to-column column t)))))

(defun move-line-region-down (arg)
  "Move region (transient-mark-mode active) or current line
  arg lines down."
  (interactive "*p")
  (move-text-internal arg))

(defun move-line-region-up (arg)
  "Move region (transient-mark-mode active) or current line
  arg lines up."
  (interactive "*p")
  (move-text-internal (- arg)))

(global-set-key (kbd "C-s-<down>") 'move-line-region-down)
(global-set-key (kbd "C-s-<up>") 'move-line-region-up)

Move by paragraphs

M-n and M-p are unbound by default. Since C-n and C-p operate on a line level, I think these should operate on a paragraph level.

Also applies to code blocks.

(use-package emacs
  :config
  ;; M-n in message-mode does something with "abbrev"
  (unbind-key "M-n" message-mode-map)
  ;; M-n/M-p moves from link to link in markdown-mode
  (unbind-key "M-p" markdown-mode-map)
  (unbind-key "M-n" markdown-mode-map)
  :bind
  ("M-p" . #'backward-paragraph)
  ("M-n" . #'forward-paragraph))

Require terminating newline at EOF

(setq require-final-newline t)

Rectangular selection

CUA (Common User Actions) has a couple of convenience functions pertaining rectangular insert.

  • C-SPC to select text
  • C-x SPC to transform into rectangle (enables rectangle-mark-mode)

Then:

  • C-t to insert string in rectangle on each line (default)
  • C-return to activate CUA mode (my binding) where RET moves from corner to corner
(use-package emacs
  :init
  (require 'rect)
  :bind
  (:map rectangle-mark-mode-map ("<C-return>" . cua-rectangle-mark-mode)))

File search

  • grep is built in
  • ag aka thesilversearcher doesn’t have a great Elisp mode (I always get weird highlighting issues);
  • rg using deadgrep works well as is well-maintained.

Projectile comes with search support for rg/ripgrep and ag/silversearcher already.

deadgrep (rg frontend)

(use-package deadgrep
  :load-path "/Users/ctm/.emacs.d/src/deadgrep"
  :ensure
  :bind
  (:map deadgrep-mode-map
        ("w" . deadgrep-edit-mode)))

Open results in other window of my choosing, so I can use deadgrep result buffers like a sidebar and show the result in my main work window:

(with-eval-after-load "deadgrep"
  (with-eval-after-load "ace-window"
    (defun ct/find-file-ace-window (filename)
      (require 'ace-window)
      (let ((dir default-directory))
        (aw-switch-to-window (aw-select nil))
        (let ((default-directory dir))
          (apply #'find-file (list filename)))))
    (defun ct/deadgrep-visit-result-ace-window ()
      (interactive)
      (deadgrep--visit-result #'ct/find-file-ace-window))
    ;; "o" opens in other buffer, but I want to pick which with ace
    (define-key deadgrep-mode-map (kbd "o") #'ct/deadgrep-visit-result-ace-window)))

Edit search results in-place (wgrep)

Similar to wdired, but for search results, wgrep binds C-c C-p to toggle editing the search results in-place. That’s like find-and-replace in modern text editors, only you can (or: have to) edit each occurrence on its own, or use a search result buffer replace command.

To make the interface more like wdired, I bind w to start the editing mode.

(use-package wgrep
  :demand t
  :bind
  (:map grep-mode-map
        ("w" . wgrep-change-to-wgrep-mode)))
;; By default bound to `e' but keep things aligned in wdired and editing grep/ag/rg/... results
(define-key occur-mode-map (kbd "w") #'occur-edit-mode)
(use-package wgrep-deadgrep
  :after (wgrep deadgrep)
  :demand t
  :bind (:map deadgrep-mode-map ("w" . wgrep-change-to-wgrep-mode)))

xfk Xah Fly Keys

After executing this, you actually then have to call xah-fly-keys to enable it. I bound the toggle to F6 to turn it on quickly after launching emacs, and having a convenient way to turn it off again.

Switch between modes using f and home; on my Moonlander, the Home key is the left caps lock hit once, briefly. If these keys are not available, SPC SPC and ESC ESC do the same. Why not single ESC? Because then I cannot enter Meta-key commands on the Terminal.

SPC C-h produces a list of leader key bindings via Xah’s website: http://ergoemacs.org/misc/xah_fly_keys_leader_keys_2021-05-17.txt

;; Do not remove default bindings
(setq xah-fly-use-control-key nil)
(setq xah-fly-use-meta-key nil)

(use-package xah-fly-keys
  :load-path "~/.emacs.d/src/xah-fly-keys"
  :delight
  (xah-fly-keys) ;; Without param, doesn't hide lighter b/c it looks for -mode?
  :config
  (xah-fly-keys-set-layout "qwerty")

  ;; SPC SPC gets you into insert mode, M-SPC M-SPC takes me back to command mode.
  ;; On the Moonlander, ESC ESC is even better.
  (define-key xah-fly-insert-map (kbd "M-SPC M-SPC") #'xah-fly-command-mode-activate)
  (define-key xah-fly-insert-map (kbd "ESC ESC") #'xah-fly-command-mode-activate)

  ;; Bound to 'SPC o u' by default for whatever reason
  (define-key xah-fly-leader-key-map (kbd "o k") #'kill-rectangle)

  (define-key xah-fly-leader-key-map (kbd "1") #'winner-undo)
  (define-key xah-fly-leader-key-map (kbd "2") #'winner-redo)

  ;; `t' is for selecting; `T' is for going back (like C-SPC and C-u C-SPC)
  (define-key xah-fly-command-map (kbd "T") #'pop-to-mark-command)

  ;; tab-bar-mode's key map
  (define-key xah-fly-leader-key-map (kbd "t") tab-prefix-map)

  ;; Use consult buffer switching instead of stock:
  (define-key xah-fly-leader-key-map (kbd "f") #'consult-buffer)

  ;; Switch SPC g and g: By default, `g' deletes a block, but I never know what that is
  (define-key xah-fly-command-map (kbd "g") #'kill-line)
  (define-key xah-fly-leader-key-map (kbd "g") #'xah-delete-current-text-block)

  ;; C-x r b → SPC i i
  (defun ct/xah-open-file-fast ()
    "Fix w/o ido"
    (interactive)
    (require 'bookmark)
    (bookmark-maybe-load-default-file)
    (let (($thisBookmark (completing-read "Open bookmark: " (mapcar (lambda ($x) (car $x)) bookmark-alist))))
      (find-file (bookmark-get-filename $thisBookmark))))
  (define-key xah-fly-leader-key-map (kbd "i i") #'ct/xah-open-file-fast)  ;; #'bookmark-jump) ; but without setting cursor position
  (define-key xah-fly-leader-key-map (kbd "i b") #'bookmark-bmenu-list)

  (define-key xah-fly-leader-key-map (kbd ";") #'recenter-top-bottom)  ;; was save file
  (define-key xah-fly-leader-key-map (kbd "p") project-prefix-map)  ;; was 'recenter-top-bottom

  (defun ct/mark-paragraph ()
    "Mark the paragraph or structural element, depending on the mode."
    (interactive)
    (cond
     ((derived-mode-p 'org-mode)
      (org-mark-element))
     (t
      (mark-paragraph))))
  (define-key xah-fly-command-map (kbd "9") 'ct/mark-paragraph) ;; was 'xah-select-text-in-quote
  (define-key xah-fly-command-map (kbd "'") 'xah-select-text-in-quote) ;; was 'xah-cycle-hyphen-lowline-space

  ;; M-o → SPC O
  (define-key xah-fly-command-map (kbd "O") #'consult-outline)
  (define-key xah-fly-leader-key-map (kbd "o o") #'consult-outline)
  (define-key xah-fly-leader-key-map (kbd "o y") #'yank-rectangle) ;; was on `SPC o o' before

  ;; Page navigation, use after `narrow-to-page' of `narrow-to-region' (SPC l l)
  (define-key xah-fly-leader-key-map (kbd "l [") #'backward-page)
  (define-key xah-fly-leader-key-map (kbd "l ]") #'forward-page)

  ;; Bind these AGAIN because Xah unbinds all
  (global-set-key (kbd "s-<delete>") #'ct/kill-word)
  (global-set-key (kbd "s-DEL") #'backward-kill-word)

  ;; Was: xah-forward-right-bracket
  (define-key xah-fly-command-map (kbd ".") #'xah-goto-matching-bracket)

  (defun ct/xah-show-kill-ring ()
    "Insert all `kill-ring' content in a new buffer named *copy history*.

URL `http://ergoemacs.org/emacs/emacs_show_kill_ring.html'"
    (interactive)
    (let (($buf (generate-new-buffer "*copy history*"))
          (inhibit-read-only t))
      (progn
        (require 'shortdoc)
        (switch-to-buffer $buf)
        (shortdoc-mode)
        (hl-line-mode)  ;; pronounced highlight of current position, esp. for `n`/`p` movement
        (dolist ($killed-line kill-ring)
          (insert (propertize $killed-line
                              'shortdoc-section t
                              'shortdoc-function t)
                  "\n")
          (insert (propertize "\n" 'face 'shortdoc-separator)))
        (goto-char (point-min)))))
  (global-set-key [remap xah-show-kill-ring] #'ct/xah-show-kill-ring)

  (defun ct/xah-fly-keys-toggled ()
    "While XFK is on, use a block mode cursor for command mode"
    (if xah-fly-keys
        (setq-default cursor-type t) ;; t = use frame value, the value XFK is setting
      (setq-default cursor-type '(bar . 2))))
  (add-hook 'xah-fly-keys-hook #'ct/xah-fly-keys-toggled)

  (defvar ct/xfk-auto-insert-mode-fns '()
    "List of functions to automatically call xah-fly-insert-mode-activate on.")
  (setq ct/xfk-auto-insert-mode-fns
        '(org-meta-return
          org-insert-todo-heading-respect-content
          org-insert-heading-respect-content
          org-insert-link
          ct/markdown-insert-comment
          ;; More function names here
          ))
  (defun ct/xfk-auto-insert-mode-activate ()
    "Wires xah-fly-insert-mode-activate to all functions from ct/xfk-auto-insert-mode-fns."
    (dolist (element ct/xfk-auto-insert-mode-fns)
      (advice-add element :after #'xah-fly-insert-mode-activate)))
  (ct/xfk-auto-insert-mode-activate)

  (defun ct/turn-on-shift-select-mode (&rest r)
    "Enables `shift-select-mode'."
    (interactive)
    (setq shift-select-mode t))
  (add-hook 'xah-fly-keys-hook #'ct/turn-on-shift-select-mode)
  (add-hook 'after-init-hook #'xah-fly-keys))

Mode indicator color updates

(with-eval-after-load "xah-fly-keys"
  (with-eval-after-load "modus-themes"
    (defun ct/set-xah-fly-mode-indicators ()
      "Sets XFK indicators based on current (!) modus theme."
      (modus-themes-with-colors
        (setq xah-fly-command-mode-indicator
              (propertize "" 'face `(:foreground ,fg-dim)))
        (setq xah-fly-insert-mode-indicator
              (propertize "" 'face `(:foreground ,blue-intense)))))
    (add-hook 'modus-themes-after-load-theme-hook #'ct/set-xah-fly-mode-indicators)))

Default control/meta rebindings

;; Imported default Control re-bindings from XFK 2021-10-16
(with-eval-after-load "xah-fly-keys"
  ;; (global-set-key (kbd "<C-S-prior>") 'xah-previous-emacs-buffer)
  ;; (global-set-key (kbd "<C-S-next>") 'xah-next-emacs-buffer)

  ;; (global-set-key (kbd "<C-tab>") 'xah-next-user-buffer)
  ;; (global-set-key (kbd "<C-S-tab>") 'xah-previous-user-buffer)
  ;; (global-set-key (kbd "<C-S-iso-lefttab>") 'xah-previous-user-buffer)

  ;; (global-set-key (kbd "<C-prior>") 'xah-previous-user-buffer)
  ;; (global-set-key (kbd "<C-next>") 'xah-next-user-buffer)

  (define-key global-map (kbd "C-<tab>") #'tab-next)
  (define-key global-map (kbd "C-S-<tab>") #'tab-previous)

  (global-set-key (kbd "<f7>") 'xah-fly-leader-key-map)

  (global-set-key (kbd "C-1") 'nil)
  (global-set-key (kbd "C-2") 'pop-global-mark)
  (global-set-key (kbd "C-3") 'previous-error)
  (global-set-key (kbd "C-4") 'next-error)
  ;; (global-set-key (kbd "C-5") 'xah-previous-emacs-buffer)
  ;; (global-set-key (kbd "C-6") 'xah-next-emacs-buffer)
  (global-set-key (kbd "C-7") 'xah-previous-user-buffer)
  (global-set-key (kbd "C-8") 'xah-next-user-buffer)
  ;; (global-set-key (kbd "C-9") 'scroll-down-command)
  ;; (global-set-key (kbd "C-0") 'scroll-up-command)

  (global-set-key (kbd "C--") 'text-scale-decrease)
  (global-set-key (kbd "C-=") 'text-scale-increase)

  ;; (global-set-key (kbd "C-S-n") 'make-frame-command)
  ;; (global-set-key (kbd "C-S-s") 'write-file)
  ;; (global-set-key (kbd "C-S-t") 'xah-open-last-closed)

  (if (memq window-system '(mac ns))
      (progn
        (global-set-key (kbd "s-a") 'mark-whole-buffer)
        (global-set-key (kbd "s-n") 'xah-new-empty-buffer)
        (global-set-key (kbd "s-o") 'find-file)
        (global-set-key (kbd "s-s") 'save-buffer)
        (global-set-key (kbd "s-S") 'write-file)
        (global-set-key (kbd "s-x") 'xah-cut-all-or-region)
        (global-set-key (kbd "s-c") 'xah-copy-all-or-region) ;'kill-ring-save in vanilla
        (global-set-key (kbd "s-v") 'yank)
        ;; (global-set-key (kbd "s-T") 'xah-open-last-closed)
        (define-key isearch-mode-map (kbd "M-v") 'isearch-yank-kill)
        ;; (global-set-key (kbd "s-z") 'undo) 'undo-fu-only-redo
        ;; (global-set-key (kbd "s-w") 'xah-close-current-buffer)
        (global-set-key (kbd "s-<right>") 'forward-word)
        (global-set-key (kbd "s-<left>") 'backward-word)
        (global-set-key (kbd "s-<backspace>") #'ct/kill-to-beginning-of-line-dwim)
        (global-set-key (kbd "s-<delete>") #'kill-line)
        )
    (progn
      (global-set-key (kbd "C-a") 'mark-whole-buffer)
      (global-set-key (kbd "C-n") 'xah-new-empty-buffer)
      (global-set-key (kbd "C-o") 'find-file)
      (global-set-key (kbd "C-s") 'save-buffer)
      (global-set-key (kbd "C-v") 'yank)
      (define-key isearch-mode-map (kbd "C-v") 'isearch-yank-kill)
      (global-set-key (kbd "C-z") 'undo)
      (global-set-key (kbd "C-w") 'xah-close-current-buffer)))
  )

Undo/Redo without wrapping around

(use-package undo-fu
  :config
  (global-set-key [remap undo] 'undo-fu-only-undo)

  (if (memq window-system '(mac ns x))
      (global-set-key (kbd "s-Z") 'undo-fu-only-redo)
    (global-set-key (kbd "C-Z") 'undo-fu-only-redo))

  ;; Global default undo key cannot be shiftef, because S-/ is ?, so keep it as-is
  ;; (global-unset-key (kbd "C-/"))
  ;; (global-set-key (kbd "C-/")   'undo-fu-only-undo)
  ;; (global-set-key (kbd "C-Z") 'undo-fu-only-redo)
  )
;; XFK bindings in command and insert mode
(with-eval-after-load "undo-fu"
  (with-eval-after-load "xah-fly-keys"
    ;; XFK shared bindings in both modes
    (if (memq window-system '(mac ns x))
        (progn
          (define-key xah-fly-shared-map (kbd "s-z") #'undo-fu-only-undo)
          (define-key xah-fly-shared-map (kbd "s-S-Z") #'undo-fu-only-redo))
      (progn
        (define-key xah-fly-shared-map (kbd "C-z") #'undo-fu-only-undo)
        (define-key xah-fly-shared-map (kbd "C-Z") #'undo-fu-only-redo)))

    ;; XFK command mode bindings
    (define-key xah-fly-command-map "y" #'undo-fu-only-undo)
    (define-key xah-fly-command-map "Y" #'undo-fu-only-redo)
    ))

YASnippet tab-completion

YASnippets comes with auto-completion for language snippets in a format similar to TextMate’s snippet syntax (which can be imported, mostly). Actual snippets come from https://github.com/AndreaCrotti/yasnippet-snippets and are installed by default.

(use-package yasnippet
  :ensure t
  :delight yas-minor-mode
  :config
  ;; Yas messages stretches the status buffer when it starts up.
  (setq yas-verbosity 0)

  (add-hook 'prog-mode-hook #'yas-minor-mode)
  (add-hook 'message-mode-hook #'yas-minor-mode)

  ;; Once after launch reload all snippets. (Done automatically by yas-global-mode.)
  (yas-reload-all)
  :init
  (yas-global-mode))
(use-package yasnippet-snippets
  :ensure t
  :after yasnippet)

Change case of word at point

The built-in functions change the case of the word at point starting at point. I want the whole word to change when I use these shortcuts at all.

M-d is for word deletion, so the mnemonic of using “d” for “downcasing” doesn’t work. My default key binding is M-l for “lowercase”.

Published here: https://christiantietze.de/posts/2021/03/change-case-of-word-at-point/

(defun ct/word-boundary-at-point-or-region ()
  "Return the boundary (beginning and end) of the word at point, or region, if any.

URL: https://christiantietze.de/posts/2021/03/change-case-of-word-at-point/"
  (let ((deactivate-mark nil)
        $p1 $p2)
    (if (use-region-p)
        (setq $p1 (region-beginning)
              $p2 (region-end))
      (save-excursion
        (skip-chars-backward "[:alpha:]")
        (setq $p1 (point))
        (skip-chars-forward "[:alpha:]")
        (setq $p2 (point))))
    (list $p1 $p2)))

(defun ct/capitalize-region (p1 p2)
  "Capitalize whole words, downcasing all but the first letter."
  (downcase-region p1 p2)
  (upcase-initials-region p1 p2))

(defun ct/capitalize-word-at-point (&optional arg)
  "Capitalize, i.e. upcase the initial character of region of the word at point.
Lowercases other characters from region or word. Moves point to end of word.

When called with ARG (a C-u or numerical prefix), passes on to the
default `capitalize-word'."
  (interactive "*P")
  (if arg
      (call-interactively #'capitalize-word)
    (let* (($bounds (ct/word-boundary-at-point-or-region))
           ($p1 (car $bounds))
           ($p2 (cadr $bounds)))
      (ct/capitalize-region $p1 $p2)
      (goto-char $p2))))

(defun ct/downcase-word-at-point (&optional arg)
  "Lowercase characters in region, or whole word at point.
Moves point to end of word.

When called with ARG (a C-u or numerical prefix), passes on to the
 default `downcase-word'."
  (interactive "*P")
  (if arg
      (call-interactively #'downcase-word)
    (let* (($bounds (ct/word-boundary-at-point-or-region))
           ($p1 (car $bounds))
           ($p2 (cadr $bounds)))
      (downcase-region $p1 $p2)
      (goto-char $p2))))

(defun ct/upcase-word-at-point (&optional arg)
  "Upcase (all-caps) characters in region, or whole word at point.
Moves point to end of word.

When called with ARG (a C-u or numerical prefix), passes on to the
default `upcase-word'."
  (interactive "*P")
  (if arg
      (call-interactively #'upcase-word)
    (let* (($bounds (ct/word-boundary-at-point-or-region))
           ($p1 (car $bounds))
           ($p2 (cadr $bounds)))
      (upcase-region $p1 $p2)
      (goto-char $p2))))

;; Set global shortcuts
(global-set-key [remap capitalize-word] #'ct/capitalize-word-at-point)
(global-set-key [remap upcase-word] #'ct/upcase-word-at-point)
(global-set-key [remap downcase-word] #'ct/downcase-word-at-point)

RFC1345 and German Postfix input method to insert accents and umlauts with the & key

  • =’rfc1345=: Instead of pressing the Option key + u on the Mac to enter umlaut composition mode, use & as the prefix key to compose accented characters with help in the minibuffer.
  • =’german-postfix=: typing ae becomes “ä”.
(custom-set-variables '(default-input-method "german-postfix"))

Notable keys:

  • The C-\ key toggles this input method on or off. That’s useful to not have “does” combine into “dös”.
  • C-x RET C-\ configures what this key does, i.e. selects the buffer-local input method.

avy to jump to visible text on screen, across windows

The same idea as ace-window: character based movement. Issue a “go to link” command, then select a link on screen. Excellent overview of interactions: https://karthinks.com/software/avy-can-do-anything/

(use-package avy
  :ensure
  :demand
  :config

  ;; Karthik Chikmagalur @karthink's init:
  ;; https://github.com/karthink/.emacs.d/blob/c717161d7653a4615c1f0a6b22cbc2994b145cd2/init.el
  (defun kt/avy-isearch (&optional arg)
    "Goto isearch candidate in this window with hints."
    (interactive "P")
    (let ((avy-all-windows)
          (current-prefix-arg (if arg 4)))
      (call-interactively 'avy-isearch)))

  :bind
  ("C-'" . 'avy-goto-char-timer)
  ("M-'" . 'avy-goto-line)
  (:map isearch-mode-map
        ("C-'" . kt/avy-isearch)))
(with-eval-after-load "xah-fly-keys"
  (with-eval-after-load "avy"
    (define-key 'xah-fly-leader-key-map (kbd "'") #'avy-goto-char-timer)))

Writing

UTF-8 Mode

(setq system-time-locale "en_US.utf8")

(prefer-coding-system       'utf-8)
(set-default-coding-systems 'utf-8)
(set-terminal-coding-system 'utf-8)
(set-keyboard-coding-system 'utf-8)
(set-language-environment   'utf-8)

Markdown

Wiki link usage:

  • C-c C-s w: add/modify wiki link at point
  • C-c C-o: open thing at point, also wiki link
(use-package markdown-mode
  :ensure t
  :mode (("README\\.md\\'" . gfm-mode)
         ("\\.txt\\'" . markdown-mode)
         ("\\.md\\'" . markdown-mode)
         ("\\.markdown\\'" . markdown-mode))
  :config
  (defun ct/markdown-insert-comment ()
    "Insert Markdown comments for <!--ct-->!"
    (interactive)
    (if (use-region-p)
        (progn ; active region
          (let (($p1 (region-beginning))
                ($p2 (region-end)))
            (goto-char $p2)
            (insert "-->")
            (goto-char $p1)
            (insert "<!--ct: ")
            (goto-char (+ $p2 8))))
      (progn ; no selection
        (let (($p1 (point)))
          (insert "-->")
          (goto-char $p1)
          (insert "<!--ct: ")
          (goto-char (+ $p1 8))))
      ) ; if
    ) ; defun
  (defun ct/markdown-insert-comment--folie ()
    "Insert Markdown comments for <!--FF-->!"
    (interactive)
    (if (use-region-p)
        (progn ; active region
          (let (($p1 (region-beginning))
                ($p2 (region-end)))
            (goto-char $p2)
            (insert "-->")
            (goto-char $p1)
            (insert "<!--FF: ")
            (goto-char (+ $p2 8))))
      (progn ; no selection
        (let (($p1 (point)))
          (insert "-->")
          (goto-char $p1)
          (insert "<!--FF: ")
          (goto-char (+ $p1 8))))))
  (defun ct/markdown-insert-comment--maybe-comma ()
    "Insert Markdown comments for <!--FF-->!"
    (interactive)
    (insert "<!--ct: \",\"?-->"))
  (define-key markdown-mode-map (kbd "C-c C-t C-t") #'ct/markdown-insert-comment)
  (define-key markdown-mode-map (kbd "C-c C-t C-f") #'ct/markdown-insert-comment--folie)
  (define-key markdown-mode-map (kbd "C-c C-t C-,") #'ct/markdown-insert-comment--maybe-comma)

  (setq markdown-hide-markup t)
  (setq markdown-asymmetric-header t)
  (setq markdown-italic-underscore t)
  (setq markdown-enable-wiki-links t)
  (setq markdown-wiki-link-alias-first t) ;; [[alias first|pagename_second]]
  )
(add-hook 'markdown-mode-hook #'display-line-numbers-mode)
(add-hook 'markdown-mode-hook #'visual-line-mode)

Writeroom modes

centered-cursor-mode (ccm)

Overscroll in both directions to keep the cursor centered at all times.x

(use-package centered-cursor-mode
  :config
  ;; ccm-scroll-down conflicts with my paste key
  (unbind-key (kbd "M-v") 'ccm-map)
  :init
  (setq ccm-recenter-at-end-of-file t))

topspace adds padding to the top to start at the center, too

(use-package topspace
  :ensure t)

olivetti-mode: just limit the window size

C-c { { { { and C-c } } } } allow setting the width on the fly. <3

(use-package olivetti
  :ensure t
  :defer t
  :after org
  :delight ;; Don't show in modeline; it's obvious when the mode is on.
  :interpreter ("olivetti" . olivetti-mode)
  :config
  (setq olivetti-body-width 0.65
        olivetti-minimum-body-width 80
        olivetti-recall-visual-line-mode-entry-state t)

  (defun ct/olivetti-set-width (w)
    "Increase the column width to accommodate the 24px frame border (inner padding)"
    (olivetti-set-width (+ 2 w)))

  (defun ct/olivetti-org-mode ()
    (olivetti-mode 1)
    (ct/olivetti-set-width ct/org-width))
  (defun ct/olivetti-org-agenda-mode ()
    (olivetti-mode 1)
    (ct/olivetti-set-width ct/org-agenda-width))

  :hook ((message-mode . olivetti-mode)
         (org-agenda-finalize . ct/olivetti-org-agenda-mode)
         (org-mode . ct/olivetti-org-mode))
  )

focus-mode can dim non-active sentences to help with focus

(use-package focus
  :config
  (set-face-attribute 'focus-unfocused nil
                      :foreground "azure3"))

Amazing Combination Writeroom mode

(use-package emacs
  :demand
  :after (:all olivetti centered-cursor-mode topspace)
  :config
  (defun ct/writeroom-mode (&optional enable)
    "Toggles olivetti and centered-cursor-mode on/off."
    (interactive)
    (display-line-numbers-mode -1) ;; no need to re-enable, just toggle off
    (let* ((is-in-writeroom-mode (or olivetti-mode centered-cursor-mode))
           (enable (or enable (not is-in-writeroom-mode)))
           ($value (if enable +1 -1)))
      (message (if enable "yes" "no"))
      (olivetti-mode $value)
      (topspace-mode $value)
      (centered-cursor-mode $value))))

“Adaptive line prefixes” (for indentation in lists)

(use-package adaptive-wrap
  :defer t
  :ensure t
  :hook (markdown-mode . adaptive-wrap-prefix-mode))

Highlight TODO in files

(use-package hl-todo
  :ensure t
  :config
  (setq hl-todo-keyword-faces
        '(("TODO"  . modus-themes-intense-yellow)
          ("FIXME" . modus-themes-intense-red)
          ("DEBUG" . modus-themes-intense-neutral)))
  :bind
  (:map hl-todo-mode-map
        ;; The prefix `C-c C-t` is used for comment insertion in texts, too
        ("C-c C-t p" . hl-todo-previous)
        ("C-c C-t n" . hl-todo-next)))

LaTeX

Make the built-in tex-mode and latex-mode suck less

The default compilation command began to prepend \\nonstop\\input some time in late 2020, and that breaks file names with spaces in my shell somehow.

(use-package tex-mode
  :defer t
  :config
  (setq tex-start-commands nil))

Use AucTeX instead

The interactive shell of tex-mode is annoying, especially when I compile mostly invoices where I change numbers and don’t produce errors I would want to react to. AucTeX gets out of my way, prints the result in the minibuffer, and it compiles filenames with spaces in the default settings.

Enabling AucTeX automatically associates .tex and other file extensions with this mode, so tex-mode is obsolete by now.

(use-package auctex
  :defer t
  :ensure t)

subword-mode to delete and jump to CamelCasedWord boundaries

Use subword-mode to make e.g. M-f move forward up to the next camel-cased part of a variable instead of the next word boundary. camelCase

(use-package emacs
  :delight subword-mode
  :hook (after-init . global-subword-mode))

flymake-languagetool: built-in checker, languagetool.org support with corrections

(use-package flymake-languagetool
  :ensure t
  :config
  (setq flymake-languagetool-server-jar nil; "/opt/homebrew/Cellar/languagetool/6.1/libexec/languagetool-server.jar"
        ;; homebrew server connects, but also dies
        flymake-languagetool-url "http://localhost:8081")
  :hook ((markdown-mode   . flymake-languagetool-load))
  :bind
  (:map flymake-mode-map
        ("s-." . flymake-languagetool-correct-dwim)))

Programming

Indentation guides

(use-package highlight-indent-guides
  :config
  (setq highlight-indent-guides-method 'bitmap))

prog-mode hooks

Enable visual line mode and showing parentheses in all programming modes.

(use-package emacs
  :after highlight-indent-guides
  :hook ((prog-mode . visual-line-mode)
         (prog-mode . show-paren-mode)
         (prog-mode . display-line-numbers-mode)
         (prog-mode . highlight-indent-guides-mode)
         ;; Treat camel-case as words
         (prog-mode . subword-mode)))

Delete trailing whitespace

(defun ct/delete-trailing-whitespace-when-writing-file ()
  (add-to-list 'write-file-functions 'delete-trailing-whitespace))
(add-hook 'prog-mode-hook #'ct/delete-trailing-whitespace-when-writing-file)

flycheck: Compiler checks, warnings, and linting while typing

When something appears to be wrong, check the current flycheck configuration:

  • C-c ! v
  • M-x flycheck-verify-setup
(use-package flycheck
  :ensure t
  :hook (prog-mode . flycheck-mode))

Compilation

;; Scroll to the bottom of the compilation buffer:
                                        ; (setq compilation-scroll-output t)

;; Scroll to the bottom OR the first error in the compilation buffer
(setq compilation-scroll-output 'first-error)
  1. Keep compilation window small,
  2. Dedicate compilation window to its buffer to not accidentally open other buffers in it.

This was annoying because it opens the compilation buffer in the first place on screen, even though it wouldn’t need to. Affected tangling the org init file and also ag searches.

(setq ct/compilation-window-height 10)

(defun ct/create-proper-compilation-window ()
  "Setup the *compilation* window with custom settings."
  (when (not (get-buffer-window "*compilation*"))
    (save-selected-window
      (save-excursion
        (let* ((w (split-window-vertically))
               (h (window-height w)))
          (select-window w)
          (switch-to-buffer "*compilation*")

          ;; Reduce window height
          (shrink-window (- h ct/compilation-window-height))

          ;; Prevent other buffers from displaying inside
          (set-window-dedicated-p w t)
          )))))
;; (add-hook 'compilation-mode-hook #'ct/create-proper-compilation-window)

Kill compilation buffer with the q key

(defun ct/quit-and-kill-auxiliary-windows ()
  "Kill compilation buffer and its window on quitting"
  (local-set-key (kbd "q") #'kill-buffer-and-window))
(add-hook 'special-mode-hook #'ct/quit-and-kill-auxiliary-windows)
(add-hook 'compilation-mode-hook #'ct/quit-and-kill-auxiliary-windows)

Display compilation buffer’s ^L (form feed) as a line

Used in ancient outputs as a page break character: https://www.gnu.org/software/emacs/manual/html_node/emacs/Pages.html

Can be used as section separators – but I want these to display as lines in GUI emacs, then.

  • page-break-lines inserts unicode characters; gets line width wrong in collapsed org buffer, though
  • form-feed-mode draws a line that also works for collapsed, truncated org buffers
(use-package form-feed
  :ensure
  :delight
  :config
  (custom-set-variables
   '(form-feed-include-modes '(prog-mode text-mode help-mode compilation-mode org-mode)))
  :init
  (global-form-feed-mode +1))

Recognize ANSI colors in compilation output

(require 'ansi-color)
(defun colorize-compilation-buffer ()
  (let ((inhibit-read-only t))
    (ansi-color-apply-on-region (point-min) (point-max))))
(add-hook 'compilation-filter-hook #'colorize-compilation-buffer)

A project-compile command that takes the command as parameter

The default project-compile command always prompts for input interactively. For some modes, or even projects, I want to bind this to a default command.

(defun ct/project-compile (command)
  "Run `compile' non-interactively in the project root with `command'."
  (let ((default-directory (project-root (project-current t)))
        (compilation-buffer-name-function
         (or project-compilation-buffer-name-function
             compilation-buffer-name-function)))
    (compile command)))

Save all project buffers quickly and silently

In principle, saving silently can work by using (save-some-buffers t) to save all modified buffers without asking, or by temporarily overriding compilation-ask-about-save:

(let ((compilation-ask-about-save nil))
  (compile command here))

But the following function only auto-saved project-related, file-visiting buffers. It will still ask for buffers modified elsewhere, which I prefer:

(defun ct/project-save-all-buffers (&optional arg proj)
  "Save all file-visiting buffers in PROJ, without asking if ARG is non-nil.

Falls back to `project-current' if PROJ is not specified."
  (let* ((proj (or proj (project-current)))
         (buffers (project-buffers (project-current))))
    (dolist (buf buffers)
      ;; Act on base buffer of indirect buffers, if needed.
      (with-current-buffer (or (buffer-base-buffer buf) buf)
        (when (and (buffer-file-name buf)      ;; Ignore all non-file-visiting buffers.
                   (file-writable-p            ;;
                    (buffer-file-name buf)) ;; Ignore readonly buffers.
                   (buffer-modified-p buf))    ;; Ignore all unchanged buffers.
          (let ((buffer-save-without-query t))  ; Save silently.
            (save-buffer arg)))))))

Can also use save-some-buffers, supplying a predicate to decide. See how magit-save-repository-buffers limits the scope to files withing the repository:

(save-some-buffers
 arg (lambda ()
       (and buffer-file-name
            ;; - Check whether refreshing is disabled.
            (not magit-inhibit-refresh-save)
            ;; - Check whether the visited file is either on the
            ;;   same remote as the repository, or both are on
            ;;   the local system.
            (equal (file-remote-p buffer-file-name) remote)
            ;; Delayed checks that are more expensive for remote
            ;; repositories, due to the required network access.
            ;; - Check whether the file is inside the repository.
            (equal (magit-rev-parse-safe "--show-toplevel") topdir)
            ;; - Check whether the file is actually writable.
            (file-writable-p buffer-file-name))))

Ruby

(use-package ruby-mode
  :hook
  (ruby-mode . tree-sitter)
  (ruby-mode . tree-sitter-hl-mode))
(use-package rvm
  :ensure t)

Recognize ERB template files: http://web-mode.org/

(add-to-list 'auto-mode-alist '("\\.erb\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.rhtml\\'" . web-mode))
(use-package ruby-electric
  :delight ruby-electric-mode
  :config
  (add-hook 'ruby-mode-hook #'ruby-electric-mode))
(use-package robe
  :ensure t
  :config
  (defun ct/company-robe-backend ()
    (set (make-local-variable 'company-backends)
         '((company-dabbrev-code company-robe company-capf))))
  (add-hook 'ruby-mode-hook #'ct/company-robe-backend)
  (add-hook 'ruby-mode-hook #'robe-mode))

Python

(setq python-indent-offset 4)

Completions

(use-package company-anaconda  ;; Python
  :after company
  :config
  (defun ct/company-anaconda-backend ()
    (set (make-local-variable 'company-backends)
         '((company-dabbrev-code company-anaconda company-capf))))
  (add-hook 'python-mode-hook #'ct/company-anaconda-backend))

web-mode in general

(use-package web-mode
  :ensure t
  :commands web-mode
  :mode
  (("\\.html\\'"        . web-mode)
   ("\\.phtml\\'"       . web-mode)
   ("\\.tpl\\.php\\'"   . web-mode)
   ("\\.html\\.twig\\'" . web-mode))

  :config
  (setq web-mode-attr-indent-offset 2
        web-mode-code-indent-offset 2
        web-mode-css-indent-offset 2
        web-mode-indent-style 2
        web-mode-markup-indent-offset 2  ;; markup is HTML
        web-mode-sql-indent-offset 2

        ;; Draw vertical column highlighter between opening/closing block
        web-mode-enable-current-element-highlight t
        web-mode-enable-current-column-highlight t)

  ;; Set PHP as the embedded language for phtml/tpl.php files.
  (add-to-list 'web-mode-engines-alist
               '(("php" . "\\.phtml\\'")
                 ("php" . "\\.tpl\\.php\\'")))

  (defun ct/web-mode-init ()
    (setq web-mode-style-padding 2)
    (emmet-mode)
    ;; Manually override the supported company backenda
    (set (make-local-variable 'company-backends)
         '(company-css company-web-html company-yasnippet company-files company-php company-dabbrev-code company-capf company-files))
    ;; Use emacs browser to browse local documentation:
    (setq-local browse-url-browser-function #'eww-browse-url))
  (defun ct/web-mode-before-autocomplete-hook ()
    (let ((web-mode-cur-language (web-mode-language-at-pos)))
      (if (string= web-mode-cur-language "php")
          (yas-activate-extra-mode 'php-mode)
        (yas-deactivate-extra-mode 'php-mode))
      (if (string= web-mode-cur-language "css")
          (setq emmet-use-css-transform t)
        (setq emmet-use-css-transform nil))))
  :hook
  ;;  (web-mode . lsp)
  (web-mode . ct/web-mode-init)
  (web-mode-before-auto-complete . ct/web-move-before-autocomplete-hook)

  :bind
  (:map web-mode-map
        ;; HTML element selection comes closest to paragraph selection
        ("M-h" . web-mode-element-select))
  )

HTML

HTML Entity picker

Based on the code by @emacs_gifs from https://emacsgifs.github.io/html-entities-helper, but the scraped entities and the helper function to select an entity are put into the private namespace of the ct/html-insert-entity function.

I saved this in an other file because the many entities slowed down editing this org file a lot.

See my blog post: https://christiantietze.de/posts/2020/07/emacs-insert-html-entity

(load-file "~/.emacs.d/src/html-insert-entity.el")

Emmet: expansion mode and HTML actions

Emmet’s expansion is triggered via <C-J> by default.

In TextMate, you could tab between edit points, e.g. from list item to list item. Here, it’s:

  • <C-M-left> is “Previous Edit Point” (M-x emmet-prev-edit-point)
  • <C-M-right> is “Next Edit Point” (M-x emmet-next-edit-point)
  • <C-c C-c w> is used to apply the expansion from the minibuffer around the current selection.

See the website for more details. Emmet can do a lot more stuff.

(use-package emmet-mode
  :delight emmet-mode
  :ensure t
  :config
  (add-hook 'web-mode-hook  #'emmet-mode)
  (add-hook 'sgml-mode-hook #'emmet-mode) ;; Auto-start on any markup modes
  (add-hook 'css-mode-hook  #'emmet-mode) ;; enable Emmet's css abbreviation.
  )

CSS

Always just two spaces for CSS/SCSS, thank you.

(add-hook 'css-mode-hook #'ct/two-space-indentation)
(add-hook 'scss-mode-hook #'ct/two-space-indentation)
(setq css-indent-offset 2)

JavaScript

;; js-mode does not use tab-width
(setq js-indent-level 2)
(use-package js2-mode)

nvm (node version manager) awareness in Emacs

Used in JS or node-based projects to specify the version of node we’re using. This is not intended to just use the “default” of nvm. You don’t need nvm for that, just the shell environment that resolved which node.

(use-package nvm
  :defer)

JavaScript/node REPL via comint

(use-package js-comint
  :config
  (define-key js-mode-map [remap eval-last-sexp] #'js-comint-send-last-sexp))

Use NVM when active.

(with-eval-after-load 'nvm
  (with-eval-after-load 'js-comint
    (js-do-use-nvm)))

PHP

  • Plain php-mode is fine, but phps-mode implements the PHP parser in Elisp, so it works without any reference to the PHP runtime/CLI tools. You can use phps-mode over TRAMP as well because of this.
  • phps-mode is faster but has the annoying habit of opening error buffers in my neotree buffer or a window split.
  • <C-c C-f> displays documentation for symbol at point; change browse-url-browser-function to display it inside emacs.

Consider adding this file extension lookup to enable any of the PHP modes. Use =web-mode= for PHP files instead because that’s my main use case. Enable PHP modes for Laravel projects and the like where needed. This way, I neither spawn an LSP session nor run into ac-php errors for completions.

:mode ("\\.php\\'" "\\.phtml\\'")
(use-package phps-mode
  :after flycheck
  :ensure t
  :hook
  (phps-mode . ct/phps-mode-hook)
  (phps-mode . tree-sitter-mode)
  (phps-mode . tree-sitter-hl-mode)
  :config
  (defun ct/phps-mode-hook ()
    (setq-local tab-width 2))

  (phps-mode-flycheck-setup)
  (setq phps-mode-async-process t)
  (setq phps-mode-async-process-using-async-el t))
(with-eval-after-load 'phps-mode
  (with-eval-after-load 'tree-sitter-langs
    (add-to-list 'tree-sitter-major-mode-language-alist '(phps-mode . php))))
(with-eval-after-load 'phps-mode
  (with-eval-after-load 'eglot
    (add-to-list 'eglot-server-programs '(phps-mode . ("intelephense" "--stdio")))))
(use-package php-mode
  :ensure t
  :config
  (defun ct/use-eww-as-local-browser ()
    "Use eww as  browser to browse links, e.g. local documentation"
    (setq-local browse-url-browser-function #'eww-browse-url))
  (add-hook 'php-mode-hook #'ct/use-eww-as-local-browser)

  ;; We should use web-mode for mixed PHP/HTML; php-mode is for pure PHP
  (setq php-mode-template-compatibility nil))

Auto-completion in PHP

company-php, which confusingly is part of the ac-php project, requires a TAGS database file in the project root, unless you have a .projectile file or vendor/autoload.php.

cd /project/to/poject/root
echo ".ac-php-conf.json" >> .gitignore
touch .ac-php-conf.json

Then you have to run M-x ac-php-remake-tags to remake the TAGS database.

(use-package company-php
  :after (company php-mode)
  :config
  (defun ct/company-php-mode-hook ()
    (ac-php-core-eldoc-setup)
    (set (make-local-variable 'company-backends)
	     '((;;company-ac-php-backend  ;; can't get this to work
            company-dabbrev-code)
		   company-capf company-files))
    (local-set-key (kbd "M-.") #'ac-php-find-symbol-at-point))
  :hook
  (php-mode . ct/company-php-mode-hook)
  :bind
  (:map php-mode-map
        ;; Jump to definition (optional)
        ("M-]" . ac-php-find-symbol-at-point)
        ;; Return back (optional)
        ("M-[" . ac-php-location-stack-back))
  )

Swift

company-sourcekit requires sourcekittendaemon in PATH and that tool hasn’t been updated sicne Swift 4.2 in late 2018. So we’re stuck with LSP.

Swift Package Compilation shortcuts

(with-eval-after-load 'swift-mode
  (defun ct/swift-build-command (&optional arg)
    "Builds the Swift package in the current project directory."
    (interactive "p")
    (ct/project-save-all-buffers t)
    (ct/project-compile "swift build"))
  (defun ct/swift-test-command (&optional arg)
    (interactive "p")
    (ct/project-save-all-buffers t)
    (ct/project-compile "swift test"))
  (define-key swift-mode-map (kbd "s-u") #'ct/swift-test-command)
  (define-key swift-mode-map (kbd "s-b") #'ct/swift-build-command))

Magit

(use-package magit
  :defer t
  :ensure
  :config
  (setq magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)
  (setq magit-diff-refine-hunk t)

  ;; Sort branches by modification date (creation date of commits)
  (setq magit-list-refs-sortby "-creatordate")

  ;; Display (rather wide) fringes in Magit buffers to show the collapsible sections
  (defun ct/magit-window-config ()
    "Used in `window-configuration-change-hook' to configure fringes for Magit."
    (set-window-fringes nil 20 0))
  (defun ct/magit-mode-hook ()
    "Custom `magit-mode' behaviours."
    ;; Install the fringe setter using buffer-local (!) mode hooks.
    (add-hook 'window-configuration-change-hook
              'ct/magit-window-config
              nil :local)
    ;; Show magit stuff in smaller font; since there's no magit-base-face, cannot set :height uniformly
    (text-scale-decrease 1)
    (subword-mode)
    (ct/disable-local-line-spacing))

  (add-hook 'magit-mode-hook #'ct/magit-mode-hook)

  ;; I'm using `hl-line-mode' and `lin' to highlight the current line in Magit.
  ;; UI lines (i.e. non-code) should also be variable pitch. Use :family, not :font,
  ;; to allow the height to be inherited.
  (let ((sans-serif-family (face-attribute 'variable-pitch :family)))
    (set-face-attribute 'magit-diff-file-heading nil :family sans-serif-family :weight 'normal :bold nil)
    (set-face-attribute 'magit-diff-file-heading-highlight nil :family sans-serif-family :weight 'normal :bold nil)
    (set-face-attribute 'magit-section-child-count nil :family sans-serif-family :weight 'normal :bold nil)

    (set-face-attribute 'magit-section-heading nil :family sans-serif-family :bold t)
    (set-face-attribute 'magit-section-highlight nil :family sans-serif-family :bold t))

  ;; To not override nested faces when highlighting, disable section highlight completely:
  ;; https://github.com/magit/magit/issues/4599#issuecomment-1050204282
  (setq magit-section-highlight-hook nil)

  ;; Show absolute dates in the log margin.
  (setq magit-log-margin '(t "%Y-%m-%d %H:%M" magit-log-margin-width t 18))

  ;; Do not add <C-x g>, <C-x M-g> and <C-c M-g> by default; I'll add these.
  (setq magit-define-global-key-bindings nil)

  ;; Was 'magit-copy-buffer-revision but 99% of the time I want to close a buffer.
  (define-key magit-mode-map (kbd "M-w") #'kill-buffer-and-window)
  ;; Dired-like key binding to hide log details (author and date)
  (define-key magit-log-mode-map (kbd "(") #'magit-toggle-margin)

  (global-set-key (kbd "C-x g") #'magit-status)
  (global-set-key (kbd "C-x M-g") #'magit-dispatch)
  (global-set-key (kbd "C-c g") #'magit-file-dispatch))

Show TODOs in magit status buffers automatically

(use-package magit-todos
  :after magit
  :ensure
  :defer
  :custom
  (magit-todos-exclude-globs '(".git/" "Carthage/"))
  :hook (magit-status-mode . magit-todos-mode))

C-u RET visits the file of the TODO in the system default editor thanks to consult.el’s helper that I’m also using via Embark:

(with-eval-after-load 'magit-todos
  (with-eval-after-load 'consult
    (defun ct/magit-todos-open-item-default-editor (item)
      (let* ((filename (string-trim-right (magit-todos-item-filename item)))
             (path (expand-file-name filename)))
        (message (concat "Opening externally: '" filename "'"))
        (consult-file-externally path)))

    (cl-defun ct/magit-todos-jump-to-item (arg &key peek externally (item (oref (magit-current-section) value)))
      (interactive "P")
      (if (or externally arg)
          (ct/magit-todos-open-item-default-editor item)
        (magit-todos-jump-to-item :peek peek :item item)))

    (define-key magit-todos-item-section-map [remap magit-visit-thing] #'ct/magit-todos-jump-to-item)))

forge connects Magit to GitHub, GitLab, etc.

(use-package forge
  :after magit
  :config
  ;; Was 'magit-copy-buffer-revision but 99% of the time I want to close a buffer.
  (unbind-key "M-w" forge-topic-mode-map))

PICO-8 console programming

(use-package lua-mode)
(use-package pico8-mode
  :after lua-mode
  :load-path "~/.emacs.d/src/pico8-mode"
  :config
  (add-to-list 'auto-mode-alist '("\\.p8\\'" . pico8-mode)))

Diffing and merge conflict resolution

Resolving merge conflicts from magit starts in smerge-mode. The prefix is C-x ^; I’d rather use n and p directly to go from one conflict to another.

(with-eval-after-load 'smerge-mode
  (define-key smerge-mode-map (kbd "C-p") #'smerge-prev)
  (define-key smerge-mode-map (kbd "C-n") #'smerge-next)
  (define-key smerge-basic-map (kbd "<down>") #'smerge-keep-lower)
  (define-key smerge-basic-map (kbd "<up>") #'smerge-keep-upper))

Tree-Sitter syntax highlighting

(use-package tree-sitter
  :defer t)
(use-package tree-sitter-langs
  :after tree-sitter
  :defer t)

repeat-mode helps to stay in the context of a complex keymap so you only repeat the last letters of the map

(use-package emacs
  :hook (after-init . repeat-mode)
  :config
  ;; https://karthinks.com/software/it-bears-repeating/
  (defun ct/repeatize (keymap)
    "Add `repeat-mode' support to a KEYMAP."
    (map-keymap
     (lambda (_key cmd)
       (when (symbolp cmd)
         (put cmd 'repeat-map keymap)))
     (symbol-value keymap)))

  (with-eval-after-load 'smerge-mode
    (ct/repeatize 'smerge-basic-map)))

Favor which-key output over default repeat-echo-function

via https://karthinks.com/software/it-bears-repeating/

(with-eval-after-load 'repeat-mode
  (with-eval-after-load 'which-key
    ;; Disable the built-in repeat-mode hinting
    (custom-set-variables '(repeat-echo-function #'ignore))

    ;; Spawn or hide a which-key popup
    (advice-add 'repeat-post-hook :after
                (defun repeat-help--which-key-popup ()
                  (if-let ((cmd (or this-command real-this-command))
                           (keymap (or repeat-map
                                       (repeat--command-property 'repeat-map))))
                      (run-at-time
                       0 nil
                       (lambda ()
                         (which-key--create-buffer-and-show
                          nil (symbol-value keymap))))
                    (which-key--hide-popup))))))

Elisp

Try:

Mouse input

Mouse selection in the terminal

(use-package emacs
  :if (not (window-system))
  :init
  (require 'mouse)
  (xterm-mouse-mode t)
  (defun track-mouse (e))
  (setq mouse-sel-mode t))

Contextual menu on right-click

Customizations by Philip K used to highlight symbol at point of the mouse click.

(use-package emacs
  :config
  (defun highlight-symbol-at-mouse (e)
    "Highlight symbol at mouse click E."
    (interactive "e")
    (save-excursion
      (mouse-set-point e)
      (highlight-symbol-at-point)))
  (defun context-menu-highlight-symbol (menu click)
    "Populate MENU with command to search online."
    (save-excursion
      (mouse-set-point click)
      (when (symbol-at-point)
        (define-key-after menu [highlight-search-separator] menu-bar-separator)
        (define-key-after menu [highlight-search-mouse]
          '(menu-item "Highlight Symbol" highlight-symbol-at-mouse
                      :help "Highlight symbol at point"))))
    menu)

  (defun magit-current-section-menu-label ()
    "Menu item label for section at point."
    (pcase (magit-diff-scope)
      ('hunk     "Hunk")
      ('hunks    "Hunks")
      ('file     "File")
      ('files    "Files")
      ('module   "Module")
      ('region   "Region")))
  (defun magit-context-menu (menu click)
    "Populate MENU with commands that perform actions on magit sections at point."
    (save-excursion
      (mouse-set-point click)
      ;; Ignore log and commit buffers, only apply to status.
      ;; `magit-section-match' without 2nd parameter uses section at point automatically.
      (when (magit-section-match 'status magit-root-section)
        (when-let ((section-label (magit-current-section-menu-label)))
          (define-key-after menu [magit-separator] menu-bar-separator)

          (when (eq 'staged (magit-diff-type))
            (define-key-after menu [magit-unstage-thing-at-mouse]
              `(menu-item (concat "Unstage " ,section-label)
                          magit-unstage
                          :help "Unstage thing at point")))
          (when (member (magit-diff-type) '(unstaged untracked))
            (define-key-after menu [magit-stage-thing-at-mouse]
              `(menu-item (concat "Stage " ,section-label)
                          magit-stage
                          :help "Stage thing at point")))

          (when (magit-section-match '(magit-module-section
                                       magit-file-section
                                       magit-hunk-section))
            (define-key-after menu [magit-discard-thing-at-mouse]
              `(menu-item (concat "Discard " ,section-label)
                          magit-discard
                          :help "Discard thing at point"))))))
    menu)

  (setq-default context-menu-functions
                '(context-menu-ffap
                  magit-context-menu
                  context-menu-highlight-symbol
                  occur-context-menu
                  context-menu-region
                  context-menu-undo))
  :hook
  (after-init . context-menu-mode))

Shift-click to select up to point

Unbind shift-click opening the appearance menu, of all things.

(global-set-key (kbd "S-<down-mouse-1>") #'mouse-set-mark)  ;; was: mouse-appearance-menu
(global-set-key (kbd "A-<down-mouse-1>") #'mouse-drag-region-rectangle)

Start Server

(use-package edit-server
  :if window-system
  :ensure t
  :init
  (add-hook 'after-init-hook 'server-start t)
  (add-hook 'after-init-hook 'edit-server-start t))

Replacing yes-or-no with y-or-n

Some commands are destructive and warrant a longer “yes” reply by the user. But some, like killing all buffers in a project, don’t. Shorten these to “y”.

(defun yes-or-no-p->-y-or-n-p (orig-fun &rest r)
  (cl-letf (((symbol-function 'yes-or-no-p) #'y-or-n-p))
    (apply orig-fun r)))

(advice-add 'project-kill-buffers :around #'yes-or-no-p->-y-or-n-p)

Email

My setup relies on OfflineIMAP to sync the Maildir-formatted mail into ~/.Mail, which is then indexed by notmuch using a cron job.

*/5 * * * * /usr/local/bin/notmuch new >/dev/null 2>&1

Email Composition

I want to see signatures by default, because GitHub issue links are hidden away in them:

(setq notmuch-wash-signature-lines-max 0)
(setq message-default-mail-headers "Cc: \nBcc: \n"
      message-kill-buffer-on-exit t
      user-mail-address "me@christiantietze.de"
      user-full-name "Christian Tietze"
      ct/user-sender (concat user-full-name " <" user-mail-address ">"))

I don’t want to auto-fill (insert hard line breaks) when writing email. But I do want to see a visual guide.

;; Turn of autofill when composing.
(add-hook 'message-mode-hook #'turn-off-auto-fill)
(add-hook 'message-mode-hook #'display-fill-column-indicator-mode)
(with-eval-after-load 'corfu
  (defun ct/turn-off-corfu-mode ()
    (corfu-mode -1))
  (add-hook 'message-mode-hook #'ct/turn-off-corfu-mode))

Warn before sending email with empty subjects.

(defun ct/confirm-empty-subject ()
  "Allow to quit when current message subject is empty."
  (or (message-field-value "Subject")
      (yes-or-no-p "Really send without Subject? ")
      (keyboard-quit)))

(add-hook 'message-send-hook #'ct/confirm-empty-subject)

Downloading attachments

When viewing/reading email in a notmuch message buffer, the dot . is a prefix key to a key map full of attachment handling functions, like . s to save attachment and . v to view.

(defun ct/set-downloads-dir ()
  (setq-local default-directory "~/Downloads"))
(add-hook 'notmuch-show-hook #'ct/set-downloads-dir)

Attaching files from dired

Use C-c RET C-a to attach marked files in dired to the current composition window. Despite the name, also works without Gnus.

Confer https://emacs.stackexchange.com/a/33463/18986 for other approaches.

(add-hook 'dired-mode-hook #'turn-on-gnus-dired-mode)

SMTP/Outgoing mail

Make sure to have this set in custom.el because that’s the only place the setting will work:

'(epg-gpg-program "/opt/homebrew/bin/gpg")
;;(epa-file-enable)

;;;;
;;;; Disabled since homebrew install of v27.x -- apparently, the package is not included
;;;;
;; (use-package starttls
;;   :ensure t
;;   :config
;;   (setq starttls-use-gnutls t
;;         starttls-gnutls-program "gnutls-cli"
;;         starttls-extra-arguments '("--insecure")
;;         auth-sources '("~/.authinfo.gpg")))

;; SMTP outgoing email
(setq message-send-mail-function 'smtpmail-send-it
      smtpmail-stream-type 'starttls
      smtpmail-default-smtp-server "smtp.fastmail.com"
      smtpmail-smtp-server "smtp.fastmail.com"
      smtpmail-smtp-service 587)

(require 'epa-file)
;; Let Emacs handle passphrase queries through the minibuffer:
(setf epa-pinentry-mode 'loopback)

;;(custom-set-variables '(epg-gpg-program  "/opt/homebrew/bin/gpg"))

SMTP Queuing: sending for later when offline

$ mu mkdir ~/.Mail/queue $ touch ~/.Mail/queue/.noindex

(setq smtpmail-queue-mail nil ;t  ;; start in queuing mode
      smtpmail-queue-dir   "~/.Mail/queue/cur")

IMAP/Incoming email

(setq message-directory "~/.Mail/")

notmuch

Notmuch maintains a database of indexed mail only. The maildir has to be fetched by e.g. offlineimap first; my notmuch new’s pre-new hook executes an offlineimap fetch.

The notmuch configuration files are:

  • .notmuch-config
  • pre-new hook
  • post-new hook
    • feed.db which contains lines of notmuch search queries to identify newsletters
(add-to-list 'load-path "/opt/homebrew/share/emacs/site-lisp/notmuch")
(require 'notmuch)
(global-set-key (kbd "<f10>") #'notmuch)
(global-set-key (kbd "C-x m") #'notmuch-mua-new-mail)

;; After refreshing a buffer with `g', the selection is not where it seems. hl-line is off
(defun ct/notmuch--reset-selection ()
  "Refresh line highlight when refreshing search buffers"
  (hl-line-mode -1)
  (forward-char +1)
  (hl-line-mode +1)
  (backward-char -1))
(advice-add #'notmuch-search-refresh-view :after #'ct/notmuch--reset-selection)

Eagerly load to further separate configurations into more org babel code blocks:

(require 'notmuch)

;; Buffer local nowadays
(setq-default notmuch-search-oldest-first nil)

(setq notmuch-draft-folder "Drafts"
      notmuch-fcc-dirs '(("me@christiantietze.de" . "Sent"))
      notmuch-show-all-tags-list t)

;; Always reply from my main address
(defadvice notmuch-mua-reply (around notmuch-fix-sender)
  (let ((sender ct/user-sender))
    ad-do-it))
(ad-activate 'notmuch-mua-reply)

;; Directories to access the sender databases:
(setq
 notmuch-mail-dir "~/.Mail/me@christiantietze.de-fastmail/"
 notmuch-hooks-dir (expand-file-name ".notmuch/hooks" notmuch-mail-dir))

Indent wrapping/multi-line subject lines

(defun ct/notmuch-set-wrap-prefix ()
  (setq-local wrap-prefix "                                          "))
(add-hook 'notmuch-search-hook #'ct/notmuch-set-wrap-prefix)
(add-hook 'notmuch-tree-hook #'ct/notmuch-set-wrap-prefix)

Key bindings

  • In message view, hit <e> for notmuch-show-resume-message to continue a draft.

Shared tagging keys via notmuch-tagging-keys

(defvar ct/notmuch-trashing-tags '("+trashing" "-inbox" "-new" "-feed")
  "List of tag changes to apply to a amessage or a thread when it is trashed.")

(setq notmuch-archive-tags '("-inbox" "-unread" "+archived")
      notmuch-message-replied-tags '("+replied" "-inbox")
      notmuch-show-mark-read-tags '("-inbox" "-unread" "+touched"))

(setq notmuch-tagging-keys
      '(("a"
         notmuch-archive-tags
         "Archive")
        ("u"
         notmuch-show-mark-read-tags
         "Mark read")
        ("f"
         ("+flagged")
         "Flag")
        ("s"
         ("+spam" "-inbox")
         "Mark as spam")
        ("d"
         ct/notmuch-trashing-tags
         "Trash")
        ("i"
         ("+killing")
         "Kill (delete)")))

notmuch-show-mode (single message)

;; Bindings in `notmuch-show-mode' (single message)
(define-key notmuch-show-mode-map (kbd "r")
  'notmuch-show-reply)
(define-key notmuch-show-mode-map (kbd "R")
  'notmuch-show-reply-sender)
(define-key notmuch-show-mode-map (kbd "TAB")
  'notmuch-show-toggle-message)
(define-key notmuch-show-mode-map (kbd "d")
  (lambda ()
    "Toggle single message to be trashed"
    (interactive)
    (if (member "trashing" (notmuch-show-get-tags))
        (notmuch-show-tag (list "-trashing"))
      (notmuch-show-tag ct/notmuch-trashing-tags))))

notmuch-search-mode (search results, tagged messages)

;; Bindings in `notmuch-search-mode' (search results/tagged messages list)
(define-key notmuch-search-mode-map (kbd "r")
  'notmuch-search-reply-to-thread)
(define-key notmuch-search-mode-map (kbd "F")
  'vedang/notmuch-move-sender-to-feed)
(define-key notmuch-search-mode-map (kbd "R")
  'notmuch-search-reply-to-thread-sender)
(define-key notmuch-search-mode-map (kbd "d")
  (lambda ()
    "Mark single message to be trashed"
    (interactive)
    (if (member "trashing" (notmuch-tree-get-tags))
        (notmuch-search-tag '("-trashing"))
      (notmuch-search-tag ct/notmuch-trashing-tags))))

notmuch-tree-mode (tree view of current results)

;; Bindings in `notmuch-tree-mode' (tree view of the current results)
(define-key notmuch-tree-mode-map (kbd "d")
  (lambda ()
    "Toggle whole thread to be trashed (or untrash thread if current message is to be trashed)"
    (interactive)
    (if (member "trashing" (notmuch-tree-get-tags))
        (notmuch-tree-tag-thread '("-trashing"))
      (notmuch-tree-tag-thread ct/notmuch-trashing-tags))))

Using notmuch instead of plain compose-mail

Need a macro for this to execute the body block lazily. Copied from save-window-excursion.

(defmacro ct/with-notmuch-as-compose-mail (&rest body)
  "Overrides `compose-mail' with `notmuch-mua-mail' for the duration of BODY."
  `(progn
     (require 'notmuch-mua)
     (advice-add 'compose-mail :override #'notmuch-mua-mail)
     (unwind-protect (progn ,@body)
       (advice-remove 'compose-mail #'notmuch-mua-mail))))

notmuch “Hello” screen customizations

(setq notmuch-hello-thousands-separator ".")
(setq notmuch-saved-searches
      '((:key "i"
              :name "Unscreened"
              :query "tag:inbox")
        (:name "Timing inbox"
               :query "tag:timing not (tag:trashed or tag:trashing or tag:sent or tag:archived)")
        (:name "Mind Tap inbox"
               :query "tag:mindtap not (tag:trashed or tag:trashing or tag:sent or tag:archived)")
        (:name "Starface inbox"
               :query "tag:starface not (tag:trashed or tag;trashing or tag:sent or tag:archived)")
        (:name "The Archive inbox"
               :query "tag:thearchiveapp not (tag:trashed or tag:trashing or tag:sent or tag:archived)")
        (:name "TableFlip inbox"
               :query "tag:tableflipapp not (tag:trashed or tag:trashing or tag:sent or tag:archived)")
        (:name "WordCounter inbox"
               :query "tag:wordcounterapp not (tag:trashed or tag:trashing or tag:sent or tag:archived)")
        (:name "Open Source inbox"
               :query "tag:opensource not (tag:trashed or tag:trashing or tag:sent or tag:archived)")
        (:key "F"
              :name "flagged"
              :query "tag:flagged and not (tag:trashed or tag:trashing or tag:sent)"
              :search-type 'tree)
        (:key "f"
              :name "feed"
              :query "tag:feed and not (tag:trashed or tag:trashing)"
              :search-type 'tree)
        (:key "p"
              :name "papertrail"
              :query "tag:papertrail and not (tag:trashed or tag:trashing)")
        (:key "S"
              :name "@Sascha"
              :query "from:/sascha.?fast@/ and not (tag:trashed or tag:trashing)"
              :search-type tree)
        (:key "u"
              :name "unread"
              :query "tag:unread" )
        (:key "d"
              :name "drafts"
              :query "tag:draft")
        (:key "t"
              :name "sent"
              :query "tag:sent")
        (:key "a"
              :name "all mail"
              :query "*")))

@vedang’s “Hey!” workflow customizations

The “Hey!” workflow implementation comes from this Gist by @vedang: https://gist.github.com/vedang/26a94c459c46e45bc3a9ec935457c80f

;; Helper functions
(defun vedang/notmuch-search-get-from ()
  "A helper function to find the email address for the given email. Assumes `notmuch-search-mode.`"
  (let ((notmuch-addr-sexp (car ; was `first` in the original
                            (notmuch-call-notmuch-sexp "address"
                                                       "--format=sexp"
                                                       "--format-version=1"
                                                       "--output=sender"
                                                       (notmuch-search-find-thread-id)))))
    (plist-get notmuch-addr-sexp :name-addr)))
(defun vedang/notmuch-tree-get-from ()
  "A helper function to find the email address for the given email. Assumes `notmuch-tree-mode'."
  (plist-get (notmuch-tree-get-prop :headers) :From))

(defun vedang/notmuch-get-from ()
  "Find the From email address for the email at point."
  (car (notmuch-clean-address (cond
                               ((eq major-mode 'notmuch-show-mode)
                                (notmuch-show-get-from))
                               ((eq major-mode 'notmuch-tree-mode)
                                (vedang/notmuch-tree-get-from))
                               ((eq major-mode 'notmuch-search-mode)
                                (vedang/notmuch-search-get-from))))))

(defun vedang/notmuch-search-by-from (&optional no-display)
  "Show all emails sent from the sender of the current thread.
NO-DISPLAY is sent forward to `notmuch-search'."
  (interactive)
  (notmuch-search (concat "from:" (vedang/notmuch-get-from))
                  notmuch-search-oldest-first
                  nil
                  nil
                  no-display))
(defun vedang/notmuch-tag-by-from (tag-changes &optional beg end refresh)
  "Apply TAG-CHANGES to all emails from the sender of the current thread.
BEG and END provide the region, but are ignored. They are defined
since `notmuch-search-interactive-tag-changes' returns them. If
REFRESH is true, refresh the buffer from which we started the
search."
  (interactive (notmuch-search-interactive-tag-changes))
  (let ((this-buf (current-buffer)))
    (vedang/notmuch-search-by-from t)
    ;; This is a dirty hack since I can't find a way to run a
    ;; temporary hook on `notmuch-search' completion. So instead of
    ;; waiting on the search to complete in the background and then
    ;; making tag-changes on it, I will just sleep for a short amount
    ;; of time. This is generally good enough and works, but is not
    ;; guaranteed to work every time. I'm fine with this.
    (sleep-for 0.5)
    (notmuch-search-tag-all tag-changes)
    (when refresh
      (set-buffer this-buf)
      (notmuch-refresh-this-buffer))))

(defun vedang/notmuch-add-addr-to-db (nmaddr nmdbfile)
  "Add the email address NMADDR to the db-file NMDBFILE with a `from:` prefix."
  (append-to-file (format "+feed -- from:%s\n" nmaddr) nil nmdbfile))

(defun vedang/notmuch-move-sender-to-feed ()
  "For the email at point, move the sender of that email to the feed.
This means:
1. All new email should go to the feed and skip the inbox altogether.
2. All existing email should be updated with the tag =feed=.
3. All existing email should be removed from the inbox."
  (interactive)
  (let ((sender (vedang/notmuch-get-from)))
    (vedang/notmuch-add-addr-to-db sender
                                   (format "%s/feed.db" notmuch-hooks-dir))
    (vedang/notmuch-tag-by-from '("+feed" "+archived" "-inbox"))
    (message (concat "Added " sender " to feed.db"))))

Reply later to email with org-capture

Add a capture template:

(with-eval-after-load 'notmuch
  (with-eval-after-load 'org-capture
    (push '("r" "Respond to email"
            entry (file (ct/file-join ct/org-gtd-directory "tickler.org"))
            "* TODO Respond to %:from on %:subject  :email: \nSCHEDULED: %t\n%U\n%a\n"
            :clock-in t
            :clock-resume t
            :immediate-finish t)
          org-capture-templates)))

Programmatically invoke the capture template. Use the same key mnemonic!

(defun ct/notmuch-reply-later ()
  "Capture this email for replying later."
  (interactive)

  (org-capture nil "r")

  ;; The rest of this function is just a nice message in the modeline.
  (let* ((email-subject (format "%s..."
                                (substring (notmuch-show-get-subject) 0 15)))
         (email-from (format "%s..."
                             (substring (notmuch-show-get-from) 0 15)))
         (email-string (format "%s (From: %s)" email-subject email-from)))
    (message "Noted! Reply Later: %s" email-string)))

(define-key notmuch-show-mode-map (kbd "C") #'ct/notmuch-reply-later)

IRC/Chat

ERC

(use-package erc
  :config
  (setq erc-user-full-name "Christian Tietze"
        erc-nick "ctietze"
        erc-kill-buffer-on-part t
        erc-track-shorten-start 8)
  (defun ct/erc-connect-to-sourcehut ()
    (interactive)
    (erc-tls :server "chat.sr.ht"
             :port 6697
             :nick "ctietze"
             :password (ct/get-keychain-password "emacs-circe-sourcehut"))))

Circe

(use-package circe
  :config
  (setq circe-network-options
        `(("chat.sr.ht"
           :tls t
           :port 6697
           :nick "ctietze"
           :sasl-username "ctietze/irc.libera.chat"
           :sasl-password ,(ct/get-keychain-password "emacs-circe-sourcehut")
           :channels ("#soju")))))

Limit IRC buffer width to readable center column:

(with-eval-after-load 'circe
  (with-eval-after-load 'olivetti
    (defun ct/circe-olivetti-mode ()
      "Set up olivetti for IRC buffers."
      (setq-local olivetti-body-width 82)
      (olivetti-mode))
    (add-hook 'circe-channel-mode-hook #'ct/circe-olivetti-mode)
    (add-hook 'circe-server-mode-hook #'ct/circe-olivetti-mode)))

Slow down Company completions a bit so that I can type in the chat without every other word being completed into a user name or IRC command:

(with-eval-after-load 'circe
  (with-eval-after-load 'company
    (defun ct/slow-down-company ()
      (setq-local company-idle-delay 0.8))
    (add-hook 'crce-channel-mode-hook #'ct/slow-down-company)))

Mastodon

(use-package mastodon
  :ensure
  :defer
  :bind
  (:map mastodon-mode-map
        ("g" . mastodon-tl--update)
        ;; see org-capture-templates addition
        ;; ("o" . (lambda () (interactive) (org-capture nil "m")))
        ("@" . my-mastodon-mentions))
  :commands (mastodon-http--api mastodon-http--post mastodon-mode mastodon-http--get-search-json)

  :config
  (setq mastodon-instance-url "https://mastodon.social"
        mastodon-active-user "ctietze"))
(with-eval-after-load 'mastodon
  (with-eval-after-load 'discover
    (require 'mastodon-discover)
    (mastodon-discover)))

Web browsing

Elpher: Gopher and Gemini client

(use-package elpher
  :defer t
  :config
  (defun elpher:eww-browse-url (original url &optional new-window)
    "Handle gemini links."
    (cond ((string-match-p "\\`\\(gemini\\|gopher\\)://" url)
	       (require 'elpher)
	       (elpher-go url))
	      (t (funcall original url new-window))))
  (advice-add 'eww-browse-url :around 'elpher:eww-browse-url))

xwidget-webkit displays an actual WebKit browser in a buffer

(use-package xwidget
  :defer t
  :if (memq window-system '(mac ns x))
  :bind
  (:map xwidget-webkit-mode-map
        ("<swipe-right>" . xwidget-webkit-forward)
        ("<swipe-left>" . xwidget-webkit-back)))

Current time

(defun display-current-time ()
  (interactive)
  (message (format-time-string "%Y-%m-%d %H:%M:%S")))

Ledger & Accounting & Bookkeeping

(use-package ledger-mode)
(use-package company-ledger
  :after (ledger-mode company))

Startup & Teardown

Show Agenda

(use-package emacs
  :after (modus-themes org)
  :init
  (delete-other-windows) ;; Ensure only 1 window is used
  (add-hook 'after-init-hook (lambda () (org-agenda nil "d"))))

Confirm before closing

(setq confirm-kill-emacs 'y-or-n-p)

Web dev helpers

Copy current directory in website project

For my website projects at ~/Sites/project.com I sometimes need to copuy the project-relative path to assemble relative links.

(defun ct/next-subdir (dir)
  "Returns what's left after trying to trim until the next \"/\".

Will work with leading \"/\", too:

(ct/next-subdir (ct/next-subdir \"/var/tmp/foo\")) will return \"foo\"."
  (let ((pos  (string-match "/" dir))
        (next (string-match "/" dir 1)))
    (if (eq 0 pos)
        (if next
            (substring dir next)
          dir)
      (if pos
          (substring dir next)
        dir))))
(defun ct/sites-relative-path ()
  (let* ((sites-dir (expand-file-name "~/Sites/"))
         (site (ct/next-subdir (substring buffer-file-name (string-match "/" buffer-file-name (length sites-dir)))))
         (path (concat (file-name-sans-extension site) "/")))
    path))
(defun ct/copy-site-path ()
  (interactive)
  (let ((path (ct/sites-relative-path)))
    (kill-new path)
    (message "Added \"%s\" to kill ring" path)))

Create new post and file in project

(with-eval-after-load 'project
  (defun ct/project-create-post (title filename)
    "Add a new post called TITLE in the current project's root directory,
or `default-directory' if no project is recognized."
    (interactive
     (if-let* ((title (read-string "New post's title: "))
               (filename-suggestion (concat (ct/slug title) ".md"))
               (filename (read-string "Filename: " filename-suggestion)))
         (list title filename)))
    (require 'project)
    (ct/create-post title filename
		            (project-root (project-current t)))))

(defun ct/create-post (title filename &optional directory)
  "Visit post called TITLE at FILENAME inside DIRECTORY.

A blog-appropriate subdirectory inside DIRECTORY is used -- if DIRECTORY is nil,
falls back to `default-directory'."
  (unless directory
    (setq directory default-directory))
  (let* ((subdir (concat "content/posts/" (format-time-string "%Y/%m") "/"))
	     (default-directory (concat directory subdir))
	     (path (concat default-directory filename)))
    (find-file path)
    (kill-new title)
    (message "Added %s to kill ring; TODO add to title template!")))

Convert a string into a URL slug for filenames

Adapted from org-roam, changing the _ to - in slugs:

(defun ct/slug (title)
  "Return the slug of TITLE."
  (let ((slug-trim-chars '(;; Combining Diacritical Marks https://www.unicode.org/charts/PDF/U0300.pdf
                           768 ; U+0300 COMBINING GRAVE ACCENT
                           769 ; U+0301 COMBINING ACUTE ACCENT
                           770 ; U+0302 COMBINING CIRCUMFLEX ACCENT
                           771 ; U+0303 COMBINING TILDE
                           772 ; U+0304 COMBINING MACRON
                           774 ; U+0306 COMBINING BREVE
                           775 ; U+0307 COMBINING DOT ABOVE
                           776 ; U+0308 COMBINING DIAERESIS
                           777 ; U+0309 COMBINING HOOK ABOVE
                           778 ; U+030A COMBINING RING ABOVE
                           779 ; U+030B COMBINING DOUBLE ACUTE ACCENT
                           780 ; U+030C COMBINING CARON
                           795 ; U+031B COMBINING HORN
                           803 ; U+0323 COMBINING DOT BELOW
                           804 ; U+0324 COMBINING DIAERESIS BELOW
                           805 ; U+0325 COMBINING RING BELOW
                           807 ; U+0327 COMBINING CEDILLA
                           813 ; U+032D COMBINING CIRCUMFLEX ACCENT BELOW
                           814 ; U+032E COMBINING BREVE BELOW
                           816 ; U+0330 COMBINING TILDE BELOW
                           817 ; U+0331 COMBINING MACRON BELOW
                           )))
    (cl-flet* ((nonspacing-mark-p (char) (memq char slug-trim-chars))
               (strip-nonspacing-marks (s) (ucs-normalize-NFC-string
                                            (apply #'string
                                                   (seq-remove #'nonspacing-mark-p
                                                               (ucs-normalize-NFD-string s)))))
               (cl-replace (title pair) (replace-regexp-in-string (car pair) (cdr pair) title)))
      (let* ((pairs `(("[^[:alnum:][:digit:]]" . "-") ;; convert anything not alphanumeric
                      ("__*" . "_")                   ;; remove sequential underscores
                      ("--*" . "-")                   ;; remove sequential dashes
                      ("^-+" . "")                    ;; remove starting dashes
                      ("-+$" . "")))                  ;; remove ending dashes
             (slug (-reduce-from #'cl-replace (strip-nonspacing-marks title) pairs)))
        (downcase slug)))))

christiantietze.de Header Form

(defun ct/ct-insert-header (title)
  (interactive "sTitle: ")
  (insert
   (format template
           title
           (format-time-string "%Y-%m-%d %H:%M:%S +0100"))))

zettelkasten.de Header Form

(defun ct/zk-insert-header (title author)
  (interactive
   (list (read-string "Title: ")
         (completing-read "Author: " '("christian" "sascha") nil t)))
  (insert
   (format template
           title
           (format-time-string "%Y-%m-%d %H:%M:%S +0100")
           author)))

Insert updated_at into YAML frontmatter

(defun ct/yaml/update-post ()
  "Insert or update an `updated_at' in the YAML header with current ISO time."
  (interactive)
  (save-excursion
    (goto-char (point-min))
    ;; Find the YAML frontmatter block boundaries, ignoring (optional) beginning of the block.
    (when (looking-at-p "^---") (forward-line +1))
    (let ((target-pos (point))
          (timestamp (format-time-string "%Y-%m-%d %H:%M:%S +0100"))
          (yaml-end-pos (search-forward-regexp "^---" nil t)))
      ;; Return after search-forward moved point.
      (goto-char target-pos)
      (if (not yaml-end-pos)
          ;; No YAML block, yet; insert a new one
          (insert (format "updated_at: %s\n---\n" timestamp))
        ;; If YAML block is found, update or insert the date entry
        (progn
          (if (search-forward-regexp "^updated_at:" yaml-end-pos t)
              (delete-region (line-beginning-position) (line-end-position))
            (progn
              (goto-char yaml-end-pos)
              (beginning-of-line)
              (newline)
              (backward-char)))
          (insert (format "updated_at: %s" timestamp)))))))

Insert blog tags based on YAML frontmatter

(use-package emacs
  :config
  (defun ct/all-tag-lines ()
    "Extract the array contents of YAML lines for `tags: [...]'."
    (let ((project-dir (cdr (project-current)))
          (regex "^tags:\\s\\[\\s*([a-zA-Z0-9 -_]+)\\s*\\]"))
      (shell-command-to-string
       ;; (concat "ag -t -C0 --nobreak --nomultiline --nonumbers --noheading --nofilename -- " (shell-quote-argument regex) " " project-dir)
       (concat "rg -C0 --no-filename --no-heading --replace \"\\$1\" -- " (shell-quote-argument regex) " " project-dir))))

  (defun ct/all-tags ()
    "Return a list of unique tags across all articles."
    (delete-dups
     (flatten-tree
      (mapcar (lambda (s) (split-string s "[, ]+" t " "))
              (split-string (ct/all-tag-lines) "\n" nil " ")))))

  (defun ct/insert-project-tag ()
    "Select and insert a tag from YAML frontmatter tags in the project."
    (interactive)
    (insert (completing-read "Tag: " (ct/all-tags))))

  (define-key project-prefix-map (kbd "t") #'ct/insert-project-tag))

Recorded Macros

(use-package emacs
  :config
  (fset 'ct/setup-tabs
        (kmacro-lambda-form [home ?  ?t ?r ?M ?a ?i ?l return f10 ?  ?t ?n ?\C-c ?a ?d ?  ?t ?n ?  ?f ?t ?i ?m ?i ?n ?g return ?  ?t ?n] 0 "%d")))

Dir Local Settings

(dir-locals-set-class-variables 'schreibprojekt-dir-settings
                                '((markdown-mode . ((eval . (ct/writeroom-mode 1))
                                                    (eval . (display-line-numbers-mode -1))
                                                    (eval . (hl-todo-mode 1))
                                                    ;; Languagetool integration
                                                    (eval . (flymake-mode))))))

(dir-locals-set-directory-class "/Users/ctm/Dropbox/Shared/Sascha/Online Course" 'schreibprojekt-dir-settings)
(dir-locals-set-directory-class "/Users/ctm/Dropbox/Shared/Sascha/Gegenlesen" 'schreibprojekt-dir-settings)
(dir-locals-set-directory-class "/Users/ctm/Sites/christiantietze.de/content" 'schreibprojekt-dir-settings)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment