/emacs.d

emacs configuration

Primary LanguageEmacs LispOtherNOASSERTION

Emacs configuration file

Updated: {{{export-date}}}

Overview

Official links to this document

Git repository
https://github.com/sfromm/emacs.d

What is this?

The following document contains my configuration for Emacs. This is based on the idea of literate programming, where code and commentary are embedded in the same document. The benefit of this approach is you get all the benefits of Org-mode with Babel to lay out and document the code.

I try to use setq or :custom in use-package for much of my configuration. For those that parts that need to remain private, I will use customize or pull the configuration in via a separate lisp file.

This configuration used to be in straight lisp files, with various modules for different sets of functionality. And before that, it was an Org file. I came back to laying this out with Org because I ultimately think that there is more benefit with to tangling from an Org file because you have a lot more tools to document and describe what the intent is and what some of the more important pieces are.

This configuration is built to immediately tangle early-init.el and init.el whenever I save the file. For reference, it is possible parse the configuration without tangling it with something like the following:

(require 'org)
(org-babel-load-file
 (expand-file-name "emacs.org"
                   user-emacs-directory))

COPYING

;; Copyright (C) 2021, 2022 by Stephen Fromm

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

Installation

First things first. Clone the git repository:

$ git clone https://github.com/sfromm/emacs.d ~/.emacs.d

If you do not already have Emacs installed, you can use the make install target. On MacOSX, this will use Homebrew to install Emacs.

On the first time you run this configuration, it will go install several packages. This may take awhile, so please be patient. You can also run the target make bootstrap command to install the main packages needed.

Configuration

There are a couple directories you use to store any personal or site-specific configuration. Everything in forge-personal-dir is ignored by git.

forge-site-dir
~/.emacs.d/site-lisp
forge-personal-dir
~/.emacs.d/user

Updates

You can keep up to date by using the make update target. This will do a few things:

  • Do a git pull to pull in the latest changes to emacs-forge.
  • Run the function forge/update-packages to update the installed Emacs packages.

Inspiration

There are many elements of this configuration that are drawn from other users’ configurations online. Below is a list, in no particular order, of some of those configurations that caught my eye.

Early initialization

This uses package-quickstart to help with loading packages. See the commit for more information. To go with this, it is important to precompute the autoload file using (package-quickstart-refresh). The quickstart file is relocated to .emacs.d/var.
;;; early-init.el --- Early Init File -*- lexical-binding: t -*-
<<license>>

;; native compilation
(when (and (fboundp 'native-comp-available-p) (native-comp-available-p))
  (if (fboundp 'startup-redirect-eln-cache)
      (startup-redirect-eln-cache (convert-standard-filename (expand-file-name "var/eln-cache/" user-emacs-directory)))
    (add-to-list 'native-comp-eln-load-path (convert-standard-filename (expand-file-name "var/eln-cache/" user-emacs-directory))))
  (setq native-comp-deferred-compilation nil)
  (setq native-comp-async-report-warnings-errors 'silent))

;; package and package-quickstart
;; In Emacs 27+, package initialization occurs before `user-init-file' is
;; loaded, but after `early-init-file'.
(setq package-quickstart nil                   ;; nope
      package-enable-at-startup nil            ;; nope part deux
      package-quickstart-file (locate-user-emacs-file "var/package-quickstart.el"))

(add-to-list 'load-path (concat user-emacs-directory "lisp"))

;; help startup and garbage collection
(setq warning-suppress-log-types '((comp) (bytecomp)))
(setq byte-compile-warnings '(not obsolete))
(setq gc-cons-threshold most-positive-fixnum)  ;; Set garbage collection to highest threshold
(setq message-log-max 16384)                   ;; Turn up logging settings

;; Set default coding system to UTF-8
(set-language-environment "UTF-8")

file-name-handler-alist is consulted on every require, load, and so on. Borrowing from doom-emacs, I set this to nil temporarily during startup so as to achieve a minor speed up.

(unless (daemonp)
  (defvar init-file-name-handler-alist file-name-handler-alist)
  ;; Crank garbage collection to 11 for initialization.
  ;; Reset after init
  (setq file-name-handler-alist nil))

(defun init-reset-file-handler-alist ()
  "Reset `file-handler-alist' to initial value after startup."
  (setq file-name-handler-alist init-file-name-handler-alist))

(defun init-reset-garbage-collection ()
  "Reset garbage collection settings after startup."
  (setq gc-cons-threshold 16777216 ;; 16mb
        gc-cons-percentage 0.1
        message-log-max 1024))

(defun init-reset-startup-settings ()
  (init-reset-file-handler-alist)
  (init-reset-garbage-collection))

(add-hook 'emacs-startup-hook #'init-reset-startup-settings)

(provide 'early-init)

Startup

Do some housekeeping to measure start up time. See this post on using lexical binding.

;;; init.el --- Init File -*- lexical-binding: t -*-

<<license>>

(message "Loading up Emacs...")
(defvar init-core-start-time (current-time))

(defun init-report-startup-time ()
  "Report startup time."
  (interactive)
  (message "Emacs is ready, finished loading after %.03fs."
           (float-time (time-subtract after-init-time before-init-time))))

(add-hook 'after-init-hook #'init-report-startup-time)

Package Management

Set up package archives

This defines what package archives are used and then initializes package.

(require 'init-elpa)
;;; init-elpa.el --- Init ELPA and package management -*- lexical-binding: t -*-
<<license>>


(setq package-archives '(("gnu" . "https://elpa.gnu.org/packages/")
                         ("nongnu" . "https://elpa.nongnu.org/nongnu/")
                         ("melpa" . "https://melpa.org/packages/")))
(package-initialize)

Helper functions for managing packages

Sometimes installing a new package will bork packages. This will recompile the .el files.

(defun my-package-install (package)
  "Install PACKAGE if not yet installed."
  (unless (fboundp 'package-installed-p)
    (package-initialize))
  (unless (package-installed-p package)
    (message "%s" "Refreshing package database...")
    (package-refresh-contents)
    (message "%s" " done.")
    (package-install package)
    (message "Installed package %s." package)
    (delete-other-windows)))

(defun my-package-upgrade-packages ()
  "Upgrade all installed packages."
  (interactive)
  (save-window-excursion
    (package-refresh-contents)
    (package-list-packages t)
    (package-menu-mark-upgrades)
    (package-menu-execute 'noquery)
    (message "Packages updated.")))

;; Via spacemacs/core/core-funcs.el
;; https://github.com/syl20bnr/spacemacs/blob/c7a103a772d808101d7635ec10f292ab9202d9ee/core/core-funcs.el
(defun my-recompile-elpa ()
  "Recompile packages in elpa directory.  Useful if you switch Emacs versions."
  (interactive)
  (byte-recompile-directory package-user-dir nil t))

;; Via https://emacsredux.com/blog/2020/09/12/reinstalling-emacs-packages/
(defun my-reinstall-package (pkg)
  (interactive (list (intern (completing-read "Reinstall package: " (mapcar #'car package-alist)))))
  (unload-feature pkg)
  (package-reinstall pkg)
  (require pkg))

Set up use-package and other critical packages

I find the use-package macro makes a configuration easier to read and more declarative. This installs the package, if not already present, and then sets it up.

Additional notes:

  • Try out use-package-compute-statistics to compute statistics concerned use-package declarations. You can view the report with use-package-report.
  • There is an option to always defer loading of packages with use-package-always-defer. I do not have this enabled because I have some times found it more tricky to get a package to load rather than defer manually or let use-package handle it. See discussion at end of section on :after keyword.
  • Pull in org and org-contrib as early as possible to make sure load-path is set correctly.

(defvar init-core-packages '(use-package diminish org org-contrib)
  "A list of core packages that will be automatically installed.")

(defun init-install-core-packages ()
  "Install core packages to bootstrap Emacs environment."
  (interactive)
  (dolist (package init-core-packages)
    (progn (my-package-install package))))

(init-install-core-packages)

;; https://github.com/jwiegley/use-package
(eval-when-compile
  (require 'diminish)
  (require 'use-package)
  (require 'use-package-ensure))

(setq use-package-verbose t
      use-package-compute-statistics t       ;; compute stats
      use-package-always-ensure t
      use-package-minimum-reported-time 0.1) ;; carp if it takes awhile to load a package

VC Install

Since Emacs 29.1, there is built-in support for installing packages from a VC backend. Instead of pulling in Quelpa, I’d like to use the built-in functionality. Make it look like (quelpa) as much as possible.

(cl-defun init-vc-install (&key (fetcher "github") repo name rev backend)
  "Install a package from a remote.
This is meant to be a thin wrapper around `package-vc-install'.  Takes
the following arguments:

- FETCHER the remote where to get the package (e.g. \"gitlab\").
  Defaults to \"github\".
- REPO is the name of the repository (e.g. \"sfromm/ip-query\").
- NAME, REV, and BACKEND are passed to `package-vc-install'."
  (interactive)
  (let* ((url (format "https://www.%s.com/%s" fetcher repo))
         (iname (when name (intern name)))
         (pkg (or iname (intern (file-name-base repo)))))
    (unless (package-installed-p pkg)
      (package-vc-install url iname rev backend))))

Postamble


(provide 'init-elpa)

Core

Define paths

The following is meant to help keep ~.emacs.d/ tidy. The idea is not original and basically comes from no-littering. Perhaps in the future I will look to pulling this package in.

(defvar forge-site-dir (expand-file-name "site-lisp/" user-emacs-directory)
  "Path to user's site configuration.")

(defvar forge-personal-dir (expand-file-name "user/" user-emacs-directory)
  "Path to user's personal configuration.")

(defvar forge-themes-dir (expand-file-name "themes/" user-emacs-directory)
  "Path to user themes.")

(defvar forge-state-dir (expand-file-name "var/" user-emacs-directory)
  "Path to Emacs' persistent data files.")

(defvar forge-backup-dir (expand-file-name "backup/" forge-state-dir)
  "Path to Emacs' backup and autosave files.")

(defvar forge-log-dir (expand-file-name "log/" forge-state-dir)
  "Path to Emacs packages' log files.")

<<forge-vars>>

Load user custom file

;; Load custom and then do basic initialization.
(setq custom-file (expand-file-name "custom.el" forge-personal-dir))
(when (file-exists-p custom-file)
  (load custom-file))

<<custom>>

Platform helpers

(require 'init-core)
;;; init-core.el --- Init Core -*- lexical-binding: t -*-
<<license>>


;;; Platform specific details.
(defun forge/system-type-darwin-p ()
  "Return non-nil if system is Darwin/MacOS."
  (string-equal system-type "darwin"))

(defun forge/system-type-windows-p ()
  "Return non-nil if system is Windows."
  (string-equal system-type "windows-nt"))

(defun forge/system-type-linux-p ()
  "Return non-nil if system is GNU/Linux."
  (string-equal system-type "gnu/linux"))

(defun forge/native-comp-p ()
  "Return non-nil native compilation is available."
  (if (fboundp 'native-comp-available-p) (native-comp-available-p)))

Customization group

Create a customization group for variables specific to this configuration.

(defgroup forge nil
  "Forge custom settings."
  :group 'environment)

Configure basic behavior

Configure Emacs to use the above defined paths. Also configure some basic behavior, and load custom.el file, used by customize.


(defun init-mkdirs-user-emacs-directory ()
  "Create emacs.d directories environment."
  (dolist (dir (list forge-site-dir forge-personal-dir forge-state-dir forge-backup-dir forge-log-dir))
    (unless (file-directory-p dir)
      (make-directory dir t))))

(defun init-clean-user-emacs-directory ()
  "Set appropriate paths to keep `user-emacs-directory' clean."
  (interactive)
  (with-no-warnings
    (setq gamegrid-user-score-file-directory (expand-file-name "games" forge-state-dir)
          async-byte-compile-log-file (expand-file-name "async-bytecomp.log" forge-state-dir))
    (setopt bookmark-default-file (expand-file-name "bookmarks" forge-state-dir))
    (setopt calc-settings-file (expand-file-name "calc-settings.el" forge-state-dir))
    (setopt transient-history-file (expand-file-name "transient/history.el" forge-state-dir))
    (setopt transient-levels-file (expand-file-name "transient/levels.el" forge-personal-dir))
    (setopt transient-values-file (expand-file-name "transient/values.el" forge-personal-dir))
    (setopt message-auto-save-directory (expand-file-name "messages" forge-state-dir))
    (setopt package-quickstart-file (expand-file-name "package-quickstart.el" forge-state-dir))
    (setopt project-list-file (expand-file-name "project-list.el" forge-state-dir))
    (setopt tramp-auto-save-directory (expand-file-name "tramp/auto-save" forge-state-dir))
    (setopt tramp-persistency-file-name (expand-file-name "tramp/persistency.el" forge-state-dir))
    (setopt url-cache-directory (expand-file-name "url/cache/" forge-state-dir))
    (setopt url-configuration-directory (expand-file-name "url/configuration/" forge-state-dir))))

(init-mkdirs-user-emacs-directory)
(init-clean-user-emacs-directory)

(when (forge/native-comp-p)
  (setq native-comp-deferred-compilation nil
        native-comp-async-report-warnings-errors 'silent
        package-native-compile nil))

(add-to-list 'load-path forge-site-dir)
(add-to-list 'custom-theme-load-path forge-themes-dir)

(setopt inhibit-splash-screen t)
(setopt initial-major-mode 'org-mode)
(setopt initial-scratch-message
        (concat
         "#+TITLE: Scratch Buffer\n\n"
         "* Welcome to Emacs\n"
         "[[file:~/.emacs.d/emacs.org][emacs.org]]\n"
         "#+begin_src emacs-lisp\n"
         "#+end_src"))

(setopt visible-bell t)                 ;; set a visible bell ...
(setopt ring-bell-function #'ignore)    ;; and squash the audio bell
(setopt load-prefer-newer t)            ;; always load the newer version of a file
(setopt large-file-warning-threshold 50000000) ;; warn when opening files bigger than 50MB

(when (display-graphic-p)
  (when (forge/system-type-darwin-p)
    (setopt frame-resize-pixelwise t))  ;; allow frame resizing by pixels, instead of character dimensions
  (line-number-mode t)                ;; show line number in modeline
  (column-number-mode t)              ;; show column number in modeline
  (size-indication-mode t)            ;; show buffer size in modeline
  (tool-bar-mode -1)                  ;; disable toolbar
  (scroll-bar-mode -1)                ;; disable scroll bar
  (display-battery-mode))

Minibuffer history

This keeps a record of all commands and history entered in the minibuffer. This then allows the completion frameworks to become even more useful.

Resources:


(require 'savehist)
(with-eval-after-load 'savehist
  (setq savehist-file (expand-file-name "savehist" forge-state-dir)
        savehist-save-minibuffer-history 1
        savehist-additional-variables '(kill-ring search-ring regexp-search-ring))
        history-length 1000
        history-delete-duplicates t
  (add-hook 'after-init-hook #'savehist-mode))

Scrolling

This is an attempt to make scrolling a bit smoother and friendlier.


(when (fboundp 'pixel-scroll-precision-mode)
  (pixel-scroll-precision-mode))

(setopt mouse-wheel-follow-mouse 't)         ;; scroll window under mouse

Module loading and site configuration

Helpers to load modules from a path or directory

The following is meant to load any configuration in forge-site-dir or forge-personal-dir.


(defun forge/message-module-load (mod time)
  "Log message on how long it took to load module MOD from TIME."
  (message "Loaded %s (%0.2fs)" mod (float-time (time-subtract (current-time) time))))

(defun forge/load-directory-modules (path)
  "Load Lisp files in PATH directory."
  (let ((t1 (current-time)))
    (when (file-exists-p path)
      (message "Loading lisp files in %s..." path)
      (mapc 'load (directory-files path 't "^[^#\.].*el$"))
      (forge/message-module-load path t1))))

(defun forge/load-modules (&rest modules)
  "Load forge modules MODULES."
  (interactive)
  (dolist (module (cons '() modules ))
    (when module
      (let ((t1 (current-time)))
        (unless (featurep module)
          (require module nil t)
          (forge/message-module-load module t1))))))

Site configuration

Before going to much further, go ahead and load any site configuration.

(forge/load-directory-modules forge-site-dir)

Platform-dependent configuration

The following are a couple elements that are only loaded if on MacOS or on Linux.

Set up PATH


;;; exec-path-from-shell
;;; Set exec-path based on shell PATH.
;;; Some platforms, such as MacOSX, do not get this done correctly.
(use-package exec-path-from-shell
  :config
  (when (memq window-system '(mac ns x))
    (exec-path-from-shell-initialize)))

(when (forge/system-type-darwin-p)
  (dolist (path (list "/usr/local/bin" (expand-file-name "~/bin")))
    (progn
      (add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))
      (setenv "PATH" (concat path ":" (getenv "PATH")))
      (add-to-list 'exec-path path))))

Linux DBUS

This only has the helper function to check for network connectivity.


;; dbus is a linux thing -- only load on that platform
(when (eq system-type 'gnu/linux)
  (use-package dbus))

Check for network connectivity

Helper to test if there is network connectivity. This is MacOS specific.


(defun macos-networksetup-status ()
  "Run networksetup to get network status."
  (let* ((output (shell-command-to-string "networksetup -listnetworkserviceorder | grep 'Hardware Port'"))
         (netsetup (split-string output "\n")))
    (catch 'found
      (dolist (elt netsetup)
        (when (> (length elt) 0)
          (let* ((netifseq (string-match "Device: \\([a-z0-9]+\\))" elt))
                 (netif (match-string 1 elt)))
            (when (string-match "status: active" (shell-command-to-string (concat "ifconfig " netif " | grep status")))
              (throw 'found netif))))))))

(defun linux-networkmanager-status ()
  "Query NetworkManager for network status."
  (let ((nm-service "org.freedesktop.NetworkManager")
        (nm-path "/org/freedesktop/NetworkManager")
        (nm-interface "org.freedesktop.NetworkManager")
        (nm-state-connected-global 70))
    (eq nm-state-connected-global
        (dbus-get-property :system nm-service nm-path nm-interface "State"))))

(defun forge/network-online-p ()
  "Check if we have a working network connection"
  (interactive)
  (when (forge/system-type-linux-p)
    (linux-networkmanager-status))
  (when (forge/system-type-darwin-p)
    (macos-networksetup-status)))

Postamble


(provide 'init-core)

Appearance

Preamble

(require 'init-appearance)
;;; init-appearance.el --- Init appearance pieces -*- lexical-binding: t -*-
<<license>>

Fonts

First, let’s create a way to customize the font used and size. This defines the monospace font, the size for this font, a variable pitch font, and how much to scale the variable pitch font compared to monospace. Lastly, this will define fonts to use for different unicode situations.

Resources:

Define font variables


(defcustom forge-font "JetBrains Mono"
  "Preferred default font."
  :type 'string
  :group 'forge)

(defcustom forge-font-size 12
  "Preferred font size."
  :type 'integer
  :group 'forge)

(defcustom forge-variable-pitch-font "Fira Sans"
  "Preferred variable pitch font."
  :type 'string
  :group 'forge)

(defcustom forge-variable-pitch-scale 1.1
  "Preferred variable pitch font."
  :type 'decimal
  :group 'forge)

(defcustom forge-unicode-font "Fira Sans"
  "Preferred Unicode font.  This takes precedence over `forge-unicode-extra-fonts'."
  :type 'string
  :group 'forge)

(defvar forge-unicode-extra-fonts
  (list "all-the-icons"
        "FontAwesome"
        "github-octicons"
        "Weather Icons")
  "List of extra Unicode fonts.")

Helper functions for configuring fonts

(defun my-font-name-and-size ()
  "Compute and return font name and size string."
  (interactive)
  (let* ((size (number-to-string forge-font-size))
         (name (concat forge-font "-" size)))
    (if (interactive-p) (message "Font: %s" name))
    name))

(defun font-ok-p ()
  "Is configured font valid?"
  (interactive)
  (member forge-font (font-family-list)))

(defun font-size-increase ()
  "Increase font size."
  (interactive)
  (setq forge-font-size (+ forge-font-size 1))
  (my-font-update))

(defun font-size-decrease ()
  "Decrease font size."
  (interactive)
  (setq forge-font-size (- forge-font-size 1))
  (my-font-update))

(defun my-font-update ()
  "Update font configuration."
  (interactive)
  (when (font-ok-p)
    (progn
      (message "Font: %s" (my-font-name-and-size))
      ;; (set-frame-font forge-font)
      (set-face-attribute 'default nil :family forge-font :weight 'semi-light :height (* forge-font-size 10))
      (set-face-attribute 'fixed-pitch nil :family forge-font :height 1.0)
      (when forge-variable-pitch-font
        (set-face-attribute 'variable-pitch nil :family forge-variable-pitch-font :height forge-variable-pitch-scale))
      (when (fboundp 'set-fontset-font) ;; from doom-emacs
        (dolist (font (append (list forge-unicode-font) forge-unicode-extra-fonts))
          (set-fontset-font t 'unicode (font-spec :family font) nil 'prepend))))))

(my-font-update)

All the icons

all-the-icons comes with various icons and characters to help prettify Emacs modes.

(use-package all-the-icons)

(use-package all-the-icons-dired
  :hook (dired-mode . all-the-icons-dired-mode))

Emojis

Emojis and emoticons.

(defun forge/emoji-shrug () "Shrug emoji." (interactive) (insert "¯\\_(ツ)_/¯"))
(defun forge/emoji-glare () "Glare emoji." (interactive) (insert "ಠ_ಠ"))
(defun forge/emoji-table-flip () "Table fip emoji." (interactive) (insert "(╯°□°)╯︵ ┻━┻"))

(use-package emojify
  :init (setq emojify-emojis-dir (expand-file-name "emojis" forge-state-dir)))

Rainbow mode to visualize color codes.

(use-package rainbow-mode)

Choosing a Font

The following has been borrowed from Protesilaos Stavrou and from Howard Abrams and should help pick a font that is readable.

| Similarities | Regular                    |
|--------------+----------------------------|
| ()[]{}<>«»‹› | ABCDEFGHIJKLMNOPQRSTUVWXYZ |
| 6bB8&        | abcdefghijklmnopqrstuvwxyz |
| 0ODdoaoOQGC  | 0123456789                 |
| I1tilIJL     | ~!@#$%^&*+                 |
| !¡ij         | `'"‘’“”.,;:…               |
| 5$§SsS5      | ()[]{}—-_=<>/\             |
| 17ZzZ2       | ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ   |
| 9gqpG6       | αβγδεζηθικλμνξοπρστυφχψω   |
| hnmMN        |                            |
| uvvwWuuwvy   |                            |
| x×X          |                            |
| .,·°%        |                            |
| ¡!¿?         |                            |
| :;           |                            |
| `''"‘’“”     |                            |
| —-~≈=≠+*_    |                            |
| …⋯           |                            |
| ...          |                            |

The following is from Hack’s website:

//  The four boxing wizards jump
#include <stdio.h> // <= quickly.
int main(int argc, char **argv) {
  long il1[]={1-2/3.4,5+6==7/8};
  int OxFaced=0xBAD||"[{(CQUINE";
  unsigned O0,l1,Z2,S5,G6,B8__XY;
  printf("@$Hamburgefo%c`",'\n');
  return ~7&8^9?0:l1|!"j->k+=*w";
}

Themes

This will install a decent variety of themes to start off with something that looks good. It will also define a default theme to start with.


(defcustom forge-theme 'modus-operandi
  "Preferred graphics theme."
  :type 'symbol
  :group 'forge)

;; https://github.com/hlissner/emacs-solaire-mode
;; Encouraged by doom-themes
(use-package solaire-mode
  :disabled t)

;; https://github.com/hlissner/emacs-doom-themes
(use-package doom-themes
  :config
  (doom-themes-org-config))

;; https://github.com/dracula/emacs
(use-package dracula-theme)

;; https://github.com/fniessen/emacs-leuven-theme
(use-package leuven-theme)

;; https://github.com/cpaulik/emacs-material-theme
(use-package material-theme)

;; https://gitlab.com/protesilaos/ef-themes
(use-package ef-themes
  :custom
  (ef-themes-mixed-fonts t)
  (ef-themes-variable-pitch-ui t))

;; https://gitlab.com/protesilaos/modus-themes
(use-package modus-themes
  :custom
  (modus-themes-mixed-fonts t)
  (modus-themes-variable-pitch-ui t))

;; https://github.com/rougier/nano-theme
(use-package nano-theme)

;; https://github.com/kunalb/poet
(use-package poet-theme)

;; https://github.com/bbatsov/solarized-emacs
(use-package solarized-theme
  :custom
  (solarized-use-variable-pitch t)
  (solarized-scale-org-headlines t))

;; https://github.com/purcell/color-theme-sanityinc-tomorrow
(use-package color-theme-sanityinc-tomorrow)

;; https://github.com/ianpan870102/tron-legacy-emacs-theme
(use-package tron-legacy-theme
  :custom
  (tron-legacy-theme-vivid-cursor t)
  (tron-legacy-theme-softer-bg t))

;; https://github.com/bbatsov/zenburn-emacs
(use-package zenburn-theme
  :custom
  (zenburn-use-variable-pitch t)
  (zenburn-scale-org-headlines t))

(defun my-load-theme ()
  "Load configured forge theme."
  (when (boundp 'forge-theme)
    (load-theme forge-theme t)))

(add-hook 'after-init-hook #'my-load-theme)

Modeline

At this point, I’ve been fairly happy with using doom-modeline. I try to keep it simple.


;; https://github.com/seagle0128/doom-modeline
(use-package doom-modeline
  :custom
  (doom-modeline-github nil "Disable github integration")
  (doom-modeline-buffer-file-name-style 'buffer-name)
  (doom-modeline-lsp nil "Disable integration with lsp")
  (doom-modeline-workspace-name t)
  :hook
  (doom-modeline-mode . column-number-mode)
  (doom-modeline-mode . size-indication-mode)
  (after-init . doom-modeline-mode))

(use-package nyan-mode)

Look and feel


(defun forge/setup-ui-in-daemon (frame)
  "Reload the UI in a daemon frame FRAME."
  (when (or (daemonp) (not (display-graphic-p)))
    (with-selected-frame frame
      (run-with-timer 0.1 nil #'my-font-update))))

(when (daemonp)
  (add-hook 'after-make-frame-functions #'forge/setup-ui-in-daemon))

(add-hook 'after-init-hook #'my-font-update)

LIN

LIN - a utility to enhance the built-in hl-line-mode so that it better matches the selection color in MacOS or other platforms. See the manual.

(use-package lin
  :config
  (setq lin-face 'lin-mac-override-fg)
  (dolist (hook '(dired-mode-hook
                  elfeed-search-mode-hook
                  log-view-mode-hook
                  magit-log-mode-hook
                  notmuch-search-mode-hook
                  notmuch-tree-mode-hook
                  occur-mode-hook
                  org-agenda-mode-hook
                  package-menu-mode-hook))
    (add-hook hook #'lin-mode)))

Postamble

(provide 'init-appearance)

User interface elements

This section configures those pieces of Emacs where I can affect how I interact with it. This covers a lot of ground.

(require 'init-ui-completion)

(require 'init-navigation)

(require 'init-ui)
;;; init-ui.el --- Init UI elements -*- lexical-binding: t -*-
;; UI here is rather broadly defined ... not just the classical one.
<<license>>

Keybindings and keymaps

Personal keymap

This is my personal keymap that then hooks into different commands, hydras, or other pieces.


(define-prefix-command 'forge-mkhome-map)
(define-key forge-mkhome-map (kbd "g") 'forge-mkhome-update)
(define-key forge-mkhome-map (kbd "w") 'forge-mkhome-www)
(define-key forge-mkhome-map (kbd "s") 'forge-mkhome-src)

(define-prefix-command 'forge-map)
(define-key forge-map (kbd "w") 'forge/window/body)
(define-key forge-map (kbd "m") 'notmuch-cycle-notmuch-buffers)
(define-key forge-map (kbd "h") 'forge-mkhome-map)
(define-key forge-map (kbd "f") 'elfeed)
(define-key forge-map (kbd "j") 'forge/jabber-start-or-switch)
(define-key forge-map (kbd "g") 'magit-status)
(define-key forge-map (kbd "s") 'eshell-here)
(define-key forge-map (kbd "S") 'forge/slack/body)
(define-key forge-map (kbd "t") 'org-pomodoro)
(define-key forge-map (kbd "p") 'package-list-packages)
(define-key forge-map (kbd "u") 'browse-url-at-point)
(define-key forge-map (kbd "V") 'view-mode)
(define-key forge-map (kbd "F") 'forge-focus)
(global-set-key (kbd "C-z") 'forge-map)

Hydra


(use-package hydra
  :demand t
  :config
  (defhydra forge/navigate (:foreign-keys run)
    "[Navigate] or q to exit."
    ("a" beginning-of-line)
    ("e" end-of-line)
    ("l" forward-char)
    ("h" backward-char)
    ("n" next-line)
    ("j" next-line)
    ("p" previous-line)
    ("k" previous-line)
    ("d" View-scroll-half-page-forward)
    ("u" View-scroll-half-page-backward)
    ("SPC" scroll-up-command)
    ("S-SPC" scroll-down-command)
    ("[" backward-page)
    ("]" forward-page)
    ("{" backward-paragraph)
    ("}" forward-paragraph)
    ("<" beginning-of-buffer)
    (">" end-of-buffer)
    ("." end-of-buffer)
    ("C-'" nil)
    ("q" nil :exit t))

  (defhydra forge/window ()
    ("a" ace-window "Ace Window" :exit t)
    ("t" transpose-frame "Transpose" :exit t)
    ("o" ace-delete-other-windows "Delete other windows " :exit t)
    ("s" ace-swap-window "Swap window" :exit t)
    ("d" ace-delete-window "Delete window" :exit t)
    ("b" consult-buffer "Switch" :exit t)
    ("g" golden-ratio "Golden ratio" :exit t)
    ("v" (lambda ()
           (interactive)
           (split-window-right)
           (windmove-right)) "Split Vert")
    ("x" (lambda ()
           (interactive)
           (split-window-below)
           (windmove-down)) "Split Horz")
    ("m" consult-bookmark "Bookmark" :exit t)
    ("q" nil))

  (defhydra forge/music-mpd-hydra ()
    "MPD Actions"
    ("p" mingus-toggle "Play/Pause")
    ("/" mingus-search "Search" :exit t)
    ("c" (message "Currently Playing: %s" (shell-command-to-string "mpc status")) "Currently Playing")
    ("m" mingus "Mingus" :exit t)
    ("<" (progn
           (mingus-prev)
           (message "Currently Playing: %s" (shell-command-to-string "mpc status"))) "Previous")
    (">" (progn
           (mingus-next)
           (message "Currently Playing: %s" (shell-command-to-string "mpc status"))) "Next")
    ("+" (dotimes (i 5) (mingus-vol-up)) "Louder")
    ("-" (dotimes (i 5) (mingus-vol-down)) "Quieter")
    ("q" nil "Quit"))

  (defhydra forge/music-emms-hydra ()
    "EMMS Actions"
    ("SPC" emms-pause "Play/Pause")
    ("s" emms-stop "Stop")
    ("c" emms-show "Currently Playing")
    ("m" emms "EMMS")
    ("S" emms-streams "EMMS Streams")
    ("<" emms-previous "Previous")
    (">" emms-next "Next")
    ("+" emms-volume-raise "Louder")
    ("-" emms-volume-lower "Quieter")
    ("C" emms-playlist-clear "Clear")
    ("q" nil "Quit"))

  (defhydra forge/slack (:color blue)
    ("s" slack-start "Start")
    ("i" slack-im-select "IM")
    ("g" slack-group-select "Group")
    ("c" slack-channel-select "Channel")
    ("d" slack-ws-close "Disconnect")
    ("q" nil))

  )

Narrowing & Completion

;;; init-ui-completion.el --- Init UI Completion elements -*- lexical-binding: t -*-
;; UI Completion elements
<<license>>

Show key bindings

which-key will display key bindings following your currently entered incomplete command in a popup. For example, you can type C-h and it will show available completions.


(use-package which-key
  :custom (which-key-idle-delay 1.5)
  :diminish
  :commands which-key-mode
  :config (which-key-mode))

Vertico, Orderless, & Consult

The nice thing about completion frameworks is that they support interactively narrowing a set of results. I typically look for something that, from my perspective, integrates well with Emacs.


;;; Completion
;; https://github.com/minad/vertico
(use-package vertico
  :hook (after-init . vertico-mode))

;; https://github.com/oantolin/orderless
(use-package orderless
  :custom
  (completion-styles '(orderless basic)))

;; https://github.com/minad/consult
(use-package consult
  :bind
  ;; M-g go-to map
  (("M-g g" . consult-goto-line)
   ("M-g h" . consult-org-heading)
   ("M-g i" . consult-imenu)
   ;; M-s search map
   ("M-s l" . consult-line)
   ("M-s L" . consult-line-multi)
   ("M-s g" . consult-grep)
   ("M-s G" . consult-git-grep)
   ("M-s O" . consult-outline)
   ("M-s r" . consult-ripgrep)
   ("M-y" . consult-yank-pop)
   ([remap switch-to-buffer] . consult-buffer)
   ([remap switch-to-buffer-other-window] . consult-buffer-other-window)
   ([remap switch-to-buffer-other-frame] . consult-buffer-other-frame)
   ("C-c f" . consult-find))
  :config
  (setq consult-narrow-key "<"
        consult-project-root-function
        (lambda ()
          (when-let (project (project-current))
            (car (project-roots project))))))

;; https://github.com/minad/marginalia
(use-package marginalia
  :bind (:map minibuffer-local-map
              ("C-M-a" . marginalia-cycle))
  :custom
  (marginalia-annotators '(marginalia-annotators-heavy marginalia-annotators-light nil))
  :hook (after-init . vertico-mode))

Completion at point

Corfu (COmpletion in Region FUnction) is an in-buffer completion with a small completion popup.


(use-package corfu
  :custom
  (corfu-separator ?\s)
  :init
  (global-corfu-mode)
  :bind
  (:map corfu-map
        ("SPC" . corfu-insert-separator)
        ("C-n" . corfu-next)
        ("C-p" . corfu-previous)))

(use-package corfu-popupinfo
  :ensure nil
  :after corfu
  :hook (corfu-mode . corfu-popupinfo-mode)
  :custom
  (corfu-popupinfo-delay '(0.25 . 0.1))
  (corfu-popupinfo-hide nil)
  :config
  (corfu-popupinfo-mode))

Pretty icons for corfu using kind-icon.

(use-package kind-icon
  :after corfu
  :if (display-graphic-p)
  :custom
  (kind-icon-default-face 'corfu-default) ; to compute blended backgrounds correctly
  :config
  (add-to-list 'corfu-margin-formatters #'kind-icon-margin-formatter))

(use-package emacs
  :init
  (setq tab-always-indent 'complete)
  (setq completion-cycle-threshold 3))

Completion postamble

(provide 'init-ui-completion)

Navigation

;;; init-navigation.el --- Init navigation elements -*- lexical-binding: t -*-
;; Navigation elements
<<license>>

View Mode Navigation

Emacs view-mode.

KeyCommandDescription
qview-quitDisable view mode and switch back to previous buffer
eview-exitDisable view mode and keep current buffer
(use-package view
  :bind
  (:map view-mode-map
        ("h" . backward-char)
        ("l" . forward-char)
        ("j" . next-line)
        ("k" . previous-line))
  :hook
  (view-mode . hl-line-mode)
  :config
  (cond ((derived-mode-p 'org-mode)
         (keymap-set view-mode-map "p" #'org-previous-visible-heading)
         (keymap-set view-mode-map "n" #'org-next-visible-heading))
        ((derived-mode-p 'elfeed-show-mode)
         (keymap-set view-mode-map "p" #'backward-paragraph)
         (keymap-set view-mode-map "n" #'forward-paragraph))
        ((derived-mode-p 'makefile-mode)
         (keymap-set view-mode-map "p" #'makefile-previous-dependency)
         (keymap-set view-mode-map "n" #'makefile-next-dependency))
        ((derived-mode-p 'emacs-lisp-mode)
         (keymap-set view-mode-map "p" #'backward-sexp)
         (keymap-set view-mode-map "n" #'forward-sexp))
        (t
         (define-key view-mode-map (kbd "p") 'scroll-down-command)
         (define-key view-mode-map (kbd "n") 'scroll-up-command))))

Navigation between windows: windmove and ace-window

windmove makes it really easy to navigate between windows. The keys below are based on vim and intended to keep my hand on the home row.

ace-window makes it easy to select between multiple windows and is intended to be as simple as other-window.

;;; windmove
(use-package windmove
  :bind
  (("s-l" . windmove-right)
   ("s-h" . windmove-left)
   ("s-k" . windmove-up)
   ("s-j" . windmove-down))
  :custom (windmove-wrap-around t)
  :config (windmove-default-keybindings 'super))

;;; ace-window
(use-package ace-window
  :bind
  (([remap other-window] . ace-window)))

Navigation between definitions with dumb-jump

dumb-jump is a utility to jump to a definition. dumb-jump hooks into the xref backend to perform a search. This can be invoked with M-..

(use-package dumb-jump
  :commands (xref-find-definitions)
  :config
  (add-hook 'xref-backend-functions #'dumb-jump-xref-activate)
  ;; this requires at least xref-1.1.0, which comes with emacs-28.1 or newer
  (when (version<= "28.1" emacs-version)
    (setq xref-show-definitions-function #'xref-show-definitions-completing-read)))

Avy

avy is an Emacs package for jumping to visible text using a char-based decision tree.

(use-package avy
  :bind ("C-;" . avy-goto-char-timer)
  :custom (avy-case-fold-search t)
  :functions (avy-setup-default)
  :config (avy-setup-default))

Navigation postamble

(provide 'init-navigation)

Buffer & window management

Tab bar

The Emacs tab bar provides a way to have virtual workspaces inside a single Emacs frame. Each tab can have its own window arrangement. In that way, it is similar to eyebrowse but is something built-in. The table below describes common shortcuts and commands. for int

Separately, I use the function prot-tab--tab-bar-tabs from Prot’s prot-tab.el as part of a DWIM function to switch tabs.

KeyCommandDescription
C-x t bswitch-to-buffer-other-tabOpen a buffer in a new tab
C-x t ddired-other-tabOpen a directory in a new tab
C-x t ffind-file-other-tabOpen a file in a new tab
C-x t 0close-tabClose current tab
C-x t 1close-tab-otherClose all other tabs
C-x t 2tab-newOpen current buffer in new tab

(when (require 'tab-bar nil 'noerror)
  (tab-bar-mode)
  (setq tab-bar-close-tab-select 'recent
        tab-bar-close-button-show nil
        tab-bar-new-tab-choice 'ibuffer
        tab-bar-new-tab-to 'right
        tab-bar-position nil
        tab-bar-select-tab-modifiers '(super meta)
        tab-bar-tab-hints t
        tab-bar-show 1))

;; https://gitlab.com/protesilaos/dotfiles/-/blob/master/emacs/.emacs.d/prot-lisp/prot-tab.el
(defun prot-tab--tab-bar-tabs ()
  "Return a list of `tab-bar' tabs, minus the current one."
  (mapcar (lambda (tab)
            (alist-get 'name tab))
          (tab-bar--tabs-recent)))

(defun forge/switch-tab-dwim ()
  "Do-What-I-Mean (DWIM) switch to other tab.
This will create a new tab if no tabs exist, switch
to the other tab if there are only 2 tabs, and finally
prompt for what tab to switch to."
  (interactive)
  (let ((tabs (prot-tab--tab-bar-tabs)))
    (cond ((eq tabs nil)
           (tab-new))
          ((eq (length tabs) 1)
           (tab-next))
          (t
           (call-interactively #'tab-bar-select-tab-by-name)))))

(global-set-key (kbd "C-x t t") #'forge/switch-tab-dwim)

Window management

The forge/window hydra defined above uses functions from ace-window and transpose-frame. Go ahead and pull those in for the helper functions. I

(use-package transpose-frame)

Golden ratio for window sizes

golden-ratio mode will use the golden ratio for how to size windows. This can either be done as a one-off command with golden-ratio or a mode with golden-ratio-mode where Emacs will always keep the active window as the larger window.

This tries to specify what modes to exclude from golden-ratio for window sizing.

(use-package golden-ratio
  :hook
  (ediff-before-setup-windows . (lambda () (golden-ratio-mode -1)))
  :config
  (setq golden-ratio-exclude-modes '(messages-buffer-mode
                                     fundamental-mode
                                     ediff-mode
                                     calendar-mode
                                     calc-mode
                                     calc-trail-mode
                                     magit-popup-mode))
  (add-to-list 'golden-ratio-extra-commands 'ace-window))

Keep buffer names unique

Uniquify helps make it easier to navigate between buffers that have identically named files.

(with-eval-after-load 'uniquify
  (setq uniquify-buffer-name-style 'forward
        uniquify-separator "/"
        uniquify-ignore-buffers-re "^\\*"
        uniquify-after-kill-buffer-p t))

The next is olivetti, which cleans up the window so that one can focus more on the window contents.

(use-package olivetti
  :custom
  (olivetti-hide-mode-line t)
  (olivetti-body-width 80)
  :commands olivetti-mode
  :preface
  (defun forge-focus ()
    "Enable features to focus."
    (interactive)
    (olivetti-mode)))

ibuffer

(require 'init-ibuffer)
;;; init-ibuffer.el --- Init ibuffer -*- lexical-binding: t -*-
<<license>>


(use-package ibuffer
  :bind ("C-x C-b" . ibuffer)
  :preface
  (defun init-ibuffer-filters ()
    (ibuffer-switch-to-saved-filter-groups "default"))
  :hook
  (ibuffer-mode . init-ibuffer-filters)
  :custom
  (ibuffer-show-empty-filter-groups nil)
  (ibuffer-saved-filter-groups
   '(("default"
      ("Notmuch"
       (or
        (name . "\\*notmuch-")
        (mode . notmuch-search-mode)
        (mode . notmuch-show-mode)
        (mode . notmuch-message-mode)
        (mode . message-mode)))
      ("Org"
       (or
        (name . "^\\*Calendar\\*$")
        (name . "^\\*Org Agenda")
        (name . "^ \\*Agenda")
        (name . "^diary$")
        (mode . org-mode)))
      ("RANCID"
       (or
        (filename . "-rancid/")
        (mode . eos-mode)))
      ("DNS"
       (or
        (mode . dns-mode)
        (filename . "dns-zones/")))
      ("Lisp"
       (mode . emacs-lisp-mode))
      ("Dired" (mode . dired-mode))
      ("SSH"   (filename . "^/ssh:*"))
      ("Docker"
       (or
        (mode . dockerfile-mode)
        (mode . docker-compose-mode)))
      ("Magit"
       (or
        (mode . magit-status-mode)
        (mode . magit-log-mode)
        (name . "\\*magit")
        (name . "magit-")
        (name . "git-monitor")))
      ("Slack"
       (or
        (name . "^\\*Slack Log")
        (name . "^\\*Slack Event")
        (mode . slack-message-buffer-mode)
        (mode . slack-mode)))
      ("Commands"
       (or
        (mode . shell-mode)
        (mode . eshell-mode)
        (mode . term-mode)
        (mode . compilation-mode)))
      ("Emacs"
       (or
        (filename . ".emacs.d/emacs.org")
        (name . "^\\*scratch\\*$")
        (name . "^\\*Messages\\*$")
        (name . "^\\*\\(Customize\\|Help\\)")
        (name . "\\*\\(Echo\\|Minibuf\\)")))))))

(use-package ibuffer-vc
  :after (ibuffer vc)
  :bind (:map ibuffer-mode-map
              ("/ V" . ibuffer-vc-set-filter-groups-by-vc-root)))

(provide 'init-ibuffer)

Postamble

(provide 'init-ui)

Editing

General

;;; init-editing.el --- Init general editing support -*- lexical-binding: t -*-
<<license>>
(require 'init-editing)

First off, I set up some basics for all editing. This will configure Emacs to show matching parentheses. Secondly, it disables using tabs and requires a final newline in the file.


(show-paren-mode)
(add-hook 'text-mode-hook 'visual-line-mode)
(setq-default indent-tabs-mode nil
              fill-column 80
              require-final-newline t)

This will join the next line with the current line and is inspired from the J command in Vim.

(defun forge/join-next-line ()
  "Join the next line with the current line."
  (interactive)
  (join-line -1))

(global-set-key (kbd "M-j") 'forge/join-next-line)

On endlessparentheses.com, the author Artur has an article that discusses how to fill or unfill a paragraph with one command. This is super useful to either re-fill a paragraph or to unfill as needed.

(defun endless/fill-or-unfill ()
  "Like `fill-paragraph', but unfill if used twice."
  (interactive)
  (let ((fill-column
         (if (eq last-command 'endless/fill-or-unfill)
             (progn (setq this-command nil)
                    (point-max))
           fill-column)))
    (call-interactively #'fill-paragraph)))

(global-set-key [remap fill-paragraph] #'endless/fill-or-unfill)

Defined further down, pull in support for various languages.

(require 'init-editing-lang)
;;; init-editing-lang.el --- Init support for various languages -*- lexical-binding: t -*-
<<license>>

Saving files and hooks

This will save all buffers that are associated with a file. Further, if focus switches away from Emacs, save all files.

(defun forge/save-all ()
  "Save any file-related buffers."
  (interactive)
  (message "Saving buffers at %s" (format-time-string "%Y-%m-%dT%T"))
  (save-some-buffers t))

;; If focus switches away, save all files.
(when (version<= "24.4" emacs-version)
  (add-hook 'focus-out-hook 'forge/save-all))

Make all files scripts executable when saving them.

(add-hook 'after-save-hook 'executable-make-buffer-file-executable-if-script-p)

Backups and Auto-Save

The goal here is to keep any backup files and save history related items located under ~/.emacs.d/- and not scattered across the filesystem. Make backups regularly.


(setopt version-control t)        ;; number each backup file
(setopt backup-by-copying t)      ;; instead of renaming current file
(setopt delete-old-versions t)    ;; clean up after oneself
(setopt kept-new-versions 5)      ;; Number of newer versions to keep.
(setopt kept-old-versions 5)      ;; Number of older versions to keep.
(setopt trash-directory "~/.Trash")
(setq backup-directory-alist (list (cons "." forge-backup-dir))
      tramp-backup-directory-alist backup-directory-alist)

;; Turn on auto-save, so we have a fallback in case of crashes or lost data.
;; Use `recover-file' or `recover-session' to recover them.
(setopt auto-save-default t)
(setopt auto-save-timeout 120)
(setopt auto-save-interval 64)
(setq auto-save-include-big-deletions t ;; don't auto-disable auto-save after deleting large chunks
      auto-save-list-file-prefix (expand-file-name "autosave/" forge-state-dir)
      ;; handle tramp paths differently than local ones, borrowed from doom
      auto-save-file-name-transforms
      (list (list "\\`/[^/]*:\\([^/]*/\\)*\\([^/]*\\)\\'"
                  (concat auto-save-list-file-prefix "tramp-\\2") t)
            (list ".*" auto-save-list-file-prefix t)))

Page breaks

Using page breaks have a couple benefits. It can provide a nice visual separation between sections. It also provides a way to navigate between sections with C-x [ and C-x ] to move backward and forward within a file.

Resources:


(use-package page-break-lines
  :diminish page-break-lines-mode
  :hook
  (emacs-lisp-mode . page-break-lines-mode))

Snippets

Yasnippet is a template system for Emacs. You type in an abbreviation and yasnippet will automatically expand it into function templates.

The default for yas-snippet-dirs is ~/.emacs.d/snippets/. I add to that another path that is ~/.emacs.d/user/snippets.


(use-package yasnippet
  :diminish yasnippet-minor-mode
  :init
  (yas-global-mode 1)
  :config
  (add-to-list 'yas-snippet-dirs (expand-file-name "snippets" forge-personal-dir))
  (add-hook 'term-mode-hook (lambda () "Disable yasnippet in terminal" (setq yas-dont-activate t))))

Expand region semantically with expand-region

expand-region to quickly expand the selected region. Use C-= to do so.


(use-package expand-region
  :bind ("C-=" . er/expand-region))

Display line numbers

Display line numbers any time in a programming-related buffer.


;; display-line-numbers-mode
(when (fboundp 'display-line-numbers-mode)
  (let ((linum-hooks '(csv-mode-hook prog-mode-hook yaml-mode-hook yaml-ts-mode-hook)))
    (mapc (lambda (hook) (add-hook hook 'display-line-numbers-mode)) linum-hooks))
  (setopt display-line-numbers-width 3))

Highlight current line


;; hl-line-mode
(let ((hl-line-hooks '(csv-mode-hook dired-mode-hook fle-mode-hook prog-mode-hook yaml-mode-hook yaml-ts-mode-hook)))
  (mapc (lambda (hook) (add-hook hook 'hl-line-mode)) hl-line-hooks))

Highlight indentation

highlight-indent-guides provides a handy visual cue for indentation.


(use-package highlight-indent-guides
  :custom (highlight-indent-guides-method 'character))

Highlight whitespace

(defun my-whitespace-visualize ()
  "Enable whitespace visualizations."
  (setq highlight-tabs t)
  (setq show-trailing-whitespace t))

(let ((ws-visualize-hooks '(csv-mode-hook json-mode-hook prog-mode-hook yaml-mode-hook)))
  (mapc (lambda (hook) (add-hook hook 'my-whitespace-visualize)) ws-visualize-hooks))

Delete trailing whitespace

Delete trailing whitespace when saving files.


;; delete-trailing-whitespace
(let ((hooks '(csv-mode-hook json-mode-hook prog-mode-hook yaml-mode-hook)))
  (mapc (lambda (hook) (add-hook hook 'delete-trailing-whitespace)) hooks))

Hide Show / Folding

Hideshow minor mode allows you to selectively display portions of a document. This includes things like folding XML sections.


(use-package hideshow
  :diminish hs-minor-mode
  :hook ((prog-mode) . hs-minor-mode)
  :bind (("C-c h" . hs-toggle-hiding)))

Finding recent files

recentf will show recently opened files. The state file is saved in forge-state-dir.


(use-package recentf
  :bind ("<f7>" . consult-recent-file)
  :custom
  (recentf-save-file (expand-file-name "recentf" forge-state-dir))
  (recentf-max-menu-items 500)
  (recentf-exclude '("COMMIT_MSG" "COMMIT_EDITMSG" "/tmp" "/ssh:"))
  :init
  (recentf-mode 1))

DOS to Unix

DOS and Unix have different encoding systems. This helps make any necessary conversions if files are coming from a DOS or Window system. Also consider using set-buffer-file-coding-system (C-x RET f) to undecided-dos or undecided-unix

See also: https://www.emacswiki.org/emacs/DosToUnix


(defun dos2unix (buffer)
  "Do replacement of ^M characters with newlines in BUFFER."
  ;; This is basically: "M-% C-q C-m RET C-q C-j RET"
  (interactive "*b")
  (save-excursion
    (goto-char (point-min))
    (while (search-forward (string ?\C-m) nil t)
      (replace-match (string ?\C-j) nil t))))

Very large files

(use-package vlf-setup
  :ensure vlf
  :init (require 'vlf-setup))

(defun ffap-vlf ()
  "Find file at point with VLF."
  (interactive)
  (let ((file (ffap-file-at-point)))
    (unless (file-exists-p file)
      (error "File does not exist: %s" file))
    (vlf file)))

Managing diffs between files or previous versions of a file

Ediff

Ediff is a comprehensive interface to diff and patch utilities.


(use-package ediff
  :init
  (setq ediff-split-window-function 'split-window-horizontally
        ediff-window-setup-function 'ediff-setup-windows-plain))

Show indicator for uncommitted differences

diff-hl will highlight uncommitted changes on the left side of the window and allows you to jump between changes and revert them.


(use-package diff-hl
  :commands (diff-hl-mode diff-hl-dired-mode)
  :hook (magit-post-refresh . diff-hl-magit-post-refresh))

Treesitter

(defun my-treesitter-setup ()
  "Set up treesitter for use in environment."
  (interactive)
  (when (treesit-available-p)
    (setq treesit-language-source-alist
          '((bash "https://github.com/tree-sitter/tree-sitter-bash")
            ;; (cmake "https://github.com/uyha/tree-sitter-cmake")
            ;; (css "https://github.com/tree-sitter/tree-sitter-css")
            (elisp "https://github.com/Wilfred/tree-sitter-elisp")
            ;; (go "https://github.com/tree-sitter/tree-sitter-go")
            ;; (html "https://github.com/tree-sitter/tree-sitter-html")
            ;; (javascript "https://github.com/tree-sitter/tree-sitter-javascript" "master" "src")
            (json "https://github.com/tree-sitter/tree-sitter-json")
            (make "https://github.com/alemuller/tree-sitter-make")
            (markdown "https://github.com/ikatyang/tree-sitter-markdown")
            (python "https://github.com/tree-sitter/tree-sitter-python")
            ;; (toml "https://github.com/tree-sitter/tree-sitter-toml")
            ;; (tsx "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src")
            ;; (typescript "https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src")
            (yaml "https://github.com/ikatyang/tree-sitter-yaml")))
    (mapc #'treesit-install-language-grammar (mapcar #'car treesit-language-source-alist))))

(use-package emacs
  :config
  (setq major-mode-remap-alist
        '((bash-mode . bash-ts-mode)
          (json-mode . json-ts-mode)
          ;; (markdown-mode . markdown-ts-mode)
          (python-mode . python-ts-mode)
          (yaml-mode . yaml-ts-mode)))
  :hook
  ((prog-mode . electric-pair-mode)))

Eglot

Eglot (manual) is an Emacs package that is a LSP client. It is now integrated with Emacs-29. Alternative manual link.

I find that eglot-server-programs is already defined by eglot and the mode one is interested is most likely defined.

(use-package eglot
  :ensure nil
  :commands (eglot eglot-ensure)
  :custom
  (eglot-send-changes-idle-time 0.1)
  (eglot-auto-shudown t)
  (eglot-extend-to-xref t)
  :config
  ;; (setq-default eglot-workspace-configuration
  ;;               '((:pylsp . (:configurationSources ["flake8"]
  ;;                                                  :plugins (
  ;;                                                            :pycodestyle (:enabled nil)
  ;;                                                            :mccabe (:enabled nil)
  ;;                                                            :flake8 (:enabled t))))))
  ;; (add-to-list 'eglot-server-programs
  ;;              '(python-mode . ("pylsp"))
  ;;              '(yaml-mode . ("yaml-language-server")))
  (fset #'jsonrpc--log-event #'ignore)  ; don't log every event
  :hook
  (python-mode . eglot-ensure)
  (yaml-mode . eglot-ensure))

The next steps are to install the language servers for those of interest. There is a list of officially supported languages. To install the packages, the following is tailored for MacOS and Homebrew.

pipx install "python-lsp-server[all]"
brew install ansible-language-server
brew install bash-language-server
brew install dockerfile-language-server
brew install yaml-language-server

Other resources:

Docker

dockerfile-mode and docker-compose-mode for editing Dockerfiles and docker-compose files.


(use-package dockerfile-mode
  :mode ("Dockerfile\\'" . dockerfile-mode))

(use-package docker-compose-mode
  :mode "docker-compose.*\.yml\\'")

Lisp

Go ahead and set up modes to help with editing Lisp files.


(use-package aggressive-indent
  :hook (emacs-lisp-mode . aggressive-indent-mode))

(with-eval-after-load 'lisp-mode
  (setq lisp-indent-offset nil))

(use-package eldoc
  :diminish eldoc-mode
  :hook
  (emacs-lisp-mode . eldoc-mode)
  (lisp-interaction-mode . eldoc-mode)
  :config
  (setq eldoc-idle-delay 0.3))

Markdown

markdown-mode


(use-package markdown-mode
  :commands (markdown-mode gfm-mode)
  :mode (("README\\.md\\'" . gfm-mode)
         ("\\.md\\'" . markdown-mode)
         ("\\.markdown\\'" . markdown-mode))
  :custom
  (markdown-command "pandoc -f markdown_github+smart")
  :preface
  (defun orgtbl-to-gfm (table params)
    "Convert the Orgtbl mode TABLE to GitHub Flavored Markdown."
    (let* ((alignment (mapconcat (lambda (x) (if x "|--:" "|---"))
                                 org-table-last-alignment ""))
           (params2
            (list
             :splice t
             :hline (concat alignment "|")
             :lstart "| " :lend " |" :sep " | ")))
      (orgtbl-to-generic table (org-combine-plists params2 params))))

  (defun forge/insert-org-to-md-table (table-name)
    "Helper function to create markdown and orgtbl boilerplate."
    (interactive "*sEnter table name: ")
    (insert "<!---
#+ORGTBL: SEND " table-name " orgtbl-to-gfm

-->
<!--- BEGIN RECEIVE ORGTBL " table-name " -->
<!--- END RECEIVE ORGTBL " table-name " -->")
    (previous-line)
    (previous-line)
    (previous-line)))

Python

Use anaconda-mode for navigation, completion, and documentation lookup.

Set python-shell-interpreter to python3, if found in PATH, and if python-shell-interpreter is the default value python.


(use-package python
  :interpreter ("python" . python-mode)
  :config
  ;; set python-shell-interpreter to python3
  (when (and (executable-find "python3")
             (string= python-shell-interpreter "python"))
    (setq python-shell-interpreter "python3"))
  (setq-default python-indent-offset 4))

Go-lang

I pull in this package for the formatting and font-lock features.


(use-package go-mode
  :mode "\\.go\\'"
  :config
  (add-hook 'before-save-hook #'gofmt-before-save))

R & ESS

ESS is a package that supports various statistical analysis programs.


(use-package ess)

Web-related

web-mode

web-mode is a mode for editing web templates, including HTML, CSS, Jinja, and other files.


(use-package web-mode
  :mode ("\\.html\\'" "\\.j2\\'")
  :init
  (setq web-mode-enable-auto-indentation nil ;; temporary for now.
        web-mode-css-indent-offset 2
        web-mode-markup-indent-offset 2
        web-mode-code-indent-offset 2))

REST client

restclient enables Emacs to act as a REST client to query web services. This will go into restclient-mode if the file ends with .http.

See also Emacs Rocks: restclient-mode episode.

CommandDescription
C-c C-cruns the query under the cursor, tries to pretty-print the response (if possible)
C-c C-rsame, but doesn’t do anything with the response, just shows the buffer
C-c C-vsame as `C-c C-c`, but doesn’t switch focus to other window
C-c C-pjump to the previous query
C-c C-njump to the next query
C-c C-.mark the query under the cursor
C-c C-ucopy query under the cursor as a curl command
(use-package restclient
  :mode ("\\.http\\'" . restclient-mode))

GraphQL

graphql is a mode to edit GraphQL schema and queries.

(use-package graphql-mode
  :mode ("\\.graphql\\'" . graphql-mode))

PHP

This is really predominantly for syntax highlighting.

(use-package php-mode
  :mode "\\.php\\'")

Data serialization formats

CSV


(use-package csv-mode)

JSON

JSON is one data-serialization format.

(use-package json-mode)

YAML

YAML is another data-serialization format.

(use-package yaml-mode
  :config
  (setq yaml-indent-offset 2))

XML

Just enough to help recognize XML tags and then later fold xml with hs-toggle-folding. This configures nXML mode for viewing and editing XML documents.

(with-eval-after-load 'nxml
  (defalias 'xml-mode 'nxml-mode)
  (autoload 'sgml-skip-tag-forward "sgml-mode")
  (add-to-list 'hs-special-modes-alist
               '(nxml-mode
                 "<!--\\|<[^/>]*[^/]>"
                 "-->\\|</[^/>]*[^/]>"
                 "<!--"
                 sgml-skip-tag-forward
                 nil)))

Network device configurations

Junos


(with-eval-after-load 'junos-mode
  (add-to-list 'magic-mode-alist '("!RANCID-CONTENT-TYPE: fujitsu_1finity" . junos-mode))
  (add-to-list 'magic-mode-alist '("!RANCID-CONTENT-TYPE: juniper" . junos-mode))
  (add-to-list 'magic-mode-alist '("!RANCID-CONTENT-TYPE: junos" . junos-mode))
  (setq-local c-basic-offset 4))

EOS

(use-package eos-mode
  :ensure nil ;; install via package-vc-install
  :init (init-vc-install :fetcher "github" :repo "sfromm/eos-mode")
  :commands (eos-mode)
  :magic ("!RANCID-CONTENT-TYPE: arista" . eos-mode)
  :hook (eos-mode . highlight-indent-guides-mode))

Yang

(use-package yang-mode)

Nftables

(use-package nftables-mode)

FLE - Fast Log Entry


(use-package fle-mode
  :ensure nil
  :init (init-vc-install :fetcher "github" :repo "sfromm/fle-mode")
  :commands (fle-mode)
  :magic ("mycall " . fle-mode)
  :config
  (setq-local tab-width 8))

Ledger

Ledger and beancount are CLI applications for double-entry accounting system. This configures the modes (ledger and beancount) that go with them.


(use-package ledger-mode
  :commands ledger-mode)

(use-package beancount
  :ensure nil
  :init (init-vc-install :fetcher "github" :repo "beancount/beancount-mode")
  :mode ("\\.beancount\\'" . beancount-mode)
  :hook (beancount-mode . outline-minor-mode))

Postamble


(provide 'init-editing-lang)

(provide 'init-editing)

Applications

Chat

(require 'init-chat)

Pull in notifications and tls for all the pieces below.

(require 'notifications)
(require 'tls)

IRC

IRC configuration. As a default, use user-login-name as the default for erc-nick and erc-user-full-name.

;;; init-chat.el --- Init chat functionality -*- lexical-binding: t -*-
<<license>>


(with-eval-after-load 'erc
  (defun sf/erc-connect ()
    "Connect to IRC via ERC"
    (interactive)
    (when (y-or-n-p "Connect to libera? ")
      (erc-tls :server "irc.libera.chat" :port 6697))
    (when (y-or-n-p "Connect to bitlbee? ")
      (progn
        (use-package bitlbee :demand t)
        (bitlbee-start)
        (sleep-for 2)
        (erc :server "localhost" :port 6667))))

  (setq erc-nick user-login-name
        erc-server "irc.libera.chat"
        erc-away-nickname (concat erc-nick "|afk")
        erc-user-full-name erc-nick
        erc-fill-column 100
        erc-fill-static-center 16
        erc-fill-function 'erc-fill-static
        erc-modules '(autojoin autoaway button completion fill irccontrols
                               list log match menu move-to-prompt netsplit
                               networks notifications readonly ring
                               services smiley spelling stamp track))

  (erc-services-mode t)

  ;; use customize for `erc-keywords', and `erc-auto-join-channels-alist'
  (setq erc-prompt (lambda () (concat "[" (buffer-name) "]"))
        erc-insert-timestamp-function 'erc-insert-timestamp-left
        erc-timestamp-format "%H:%M:%S "
        erc-kill-buffer-on-part t         ;; kill buffer after channel /part
        erc-kill-server-buffer-on-quit t  ;; kill buffer for server messages after /quit
        erc-auto-discard-away t           ;; autoaway
        erc-autoaway-use-emacs-idle t
        ;; logging
        erc-generate-log-file-name-function 'erc-generate-log-file-name-with-date
        erc-log-channels-directory (expand-file-name "erc" forge-log-dir)
        erc-log-insert-log-on-open nil
        erc-prompt-for-nickserv-password nil
        erc-track-exclude-types '("JOIN" "NICK" "PART" "QUIT" "MODE"
                                  "324" "329" "333" "353" "477")
        erc-save-buffer-on-part t))

The following is a helper function to auto identify when using BitlBee.

(defun bitlbee-netrc-identify ()
  "Auto-identify for Bitlbee channels using authinfo or netrc.

   The entries that we look for in netrc or authinfo files have
   their 'port' set to 'bitlbee', their 'login' or 'user' set to
   the current nickname and 'server' set to the current IRC
   server's name.  A sample value that works for authenticating
   as user 'keramida' on server 'localhost' is:

   machine localhost port bitlbee login keramida password supersecret"
  (interactive)
  (when (string= (buffer-name) "&bitlbee")
    (let* ((secret (plist-get (nth 0 (auth-source-search :max 1
                                                         :host erc-server
                                                         :user (erc-current-nick)
                                                         :port "bitlbee"))
                              :secret))
           (password (if (functionp secret)
                         (funcall secret)
                       secret)))
      (erc-message "PRIVMSG" (concat (erc-default-target) " " "identify" " " password) nil))))
;; Enable the netrc authentication function for &biblbee channels.
(add-hook 'erc-join-hook 'bitlbee-netrc-identify)

Slack

There is a Slack client for Emacs available. Set up is a bit convoluted. I sometimes use this and sometimes use the official client. Follow the setup instructions for getting the client id, token, and so on.

Helpful resources:


(defvar forge-slack-client-id nil
  "Slack Client ID.")

(defvar forge-slack-client-token nil
  "Slack client token.")

(use-package slack
  :commands (slack-start)
  :bind (:map slack-mode-map
              ("C-c C-e" . slack-message-edit)
              ("C-c C-k" . slack-channel-leave)
              ("@" . slack-message-embed-mention)
              ("#" . slack-message-embed-channel))
  :init
  (setq slack-buffer-emojify t
        slack-prefer-current-team t))

Postamble

(provide 'init-chat)

Directory browser and management

Using Dired

The following is to configure Dired. There is some effort here to Dired to reuse the same window & buffer.

https://writequit.org/denver-emacs/presentations/2016-05-24-elpy-and-dired.html#orgheadline11

(require 'init-dired)
;;; init-dired.el --- Init dired integration -*- lexical-binding: t -*-
<<license>>


(with-eval-after-load 'dired
  (diminish 'dired-omit-mode)

  (define-key global-map (kbd "C-c d") 'dired-jump)
  (define-key dired-mode-map (kbd "RET") 'dired-find-alternate-file)
  (define-key dired-mode-map (kbd "Y") 'forge/dired-rsync)
  (define-key dired-mode-map (kbd "^") 'forge/dired-up)

  (defun forge/dired-mode-hook ()
    "Set misc settings in dired mode."
    (setq-local truncate-lines t))

  (defun forge/dired-up ()
    "Move up a directory without opening a new buffer."
    (interactive)
    (find-alternate-file ".."))

  (put 'dired-find-alternate-file 'disabled nil)
  (when (forge/system-type-darwin-p)
    (setq dired-use-ls-dired nil))

  (setq dired-dwim-target t
        dired-ls-F-marks-symlinks t
        dired-listing-switches "-laFh1v --group-directories-first"
        ;; -F (classify), -h (human readable), -1 (one file per line), -v (version sorting)
        dired-recursive-copies 'always
        dired-recursive-deletes 'top
        global-auto-revert-non-file-buffers t) ;; auto refresh dired buffers

  ;; This requires installing coreutils via homebrew
  (when (executable-find "gls")
    (setq insert-directory-program "gls"
          dired-use-ls-dired t)))

(setq auto-revert-verbose nil)

Pull in async for asyncrhonous operations in dired.

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

Extra features for dired with dired-x

(with-eval-after-load 'dired-x
  (setq dired-bind-jump nil
        dired-guess-shell-alist-user (list '("\\.\\(mkv\\|mpe?g\\|avi\\|mp3\\|mp4\\|ogm\\|webm\\)$" "mpv")
                                           '("\\.\\(docx?\\|xlsx?\\|kmz\\)$" "open")
                                           '("\\.pdf$" "open")))
  (global-unset-key (kbd "C-x C-j"))
  (add-to-list 'dired-omit-extensions ".DS_Store"))

More features for dired with dired-aux

(with-eval-after-load 'dired-aux
  (add-to-list 'dired-compress-file-suffixes '("\\.zip\\'" ".zip" "unzip")))

Dired & Rsync

The following helper uses rsync to make file transfers quicker. This comes from:

https://github.com/tmtxt/tmtxt-dired-async/pull/6/files

(defun forge/maybe-convert-directory-to-rsync-target (directory)
  "Adapt dired target DIRECTORY in case it is a remote target.

If directory starts with /scp: or /ssh: it is probably a tramp
target and should be converted to rsync-compatible destination
string, else we do (shell-quote-argument (expand-file-name
directory)) as is required for normal local targets acquired with
read-file-name and dired-dwim-target-directory."
  (if (or (string-prefix-p "/scp:" directory)
	  (string-prefix-p "/ssh:" directory))
      ;; - throw out the initial "/scp:" or "/ssh:"
      ;; - replace spaces with escaped spaces
      ;; - surround the whole thing with quotes
      ;; TODO: double-check that target ends with "/""
      ;; which in the case of DWIM is what we want
      (prin1-to-string
       (replace-regexp-in-string "[[:space:]]" "\\\\\\&"
	                         (substring directory 5)))
    (shell-quote-argument (expand-file-name directory))))

(defun forge/dired-rsync (dest)
  (interactive
   (list
    (expand-file-name (read-file-name "Rsync to:" (dired-dwim-target-directory)))))
  ;; store all selected files into "files" list
  (let ((files (dired-get-marked-files nil current-prefix-arg))
        ;; the rsync command
        (forge/rsync-command "rsync -arvz --progress "))
    ;; add all selected file names as arguments
    ;; to the rsync command
    (dolist (file files)
      (setq forge/rsync-command (concat forge/rsync-command (shell-quote-argument file) " ")))
    ;; append the destination
    (setq forge/rsync-command (concat forge/rsync-command (forge/maybe-convert-directory-to-rsync-target dest)))
    ;; run the async shell command
    (async-shell-command forge/rsync-command "*rsync*")
    ;; finally, switch to that window
    (other-window 1)))

Resolve Syncthing file conflicts with emacs-conflict

See https://www.reddit.com/r/emacs/comments/bqqqra/quickly_find_syncthing_conflicts_and_resolve_them/

;;(use-package emacs-conflict :commands (emacs-conflict-show-conflicts-dired))

Disk utilization

disk-usage is a handy utility to explore disk utilization. On MacOS, this will require GNU coreutils.

(use-package disk-usage)

Postamble

(provide 'init-dired)

Git

I use git for lots of things, but essentially to version control all kinds of documents. magit is a wonderful frontend to git. Other comments:

  • Pull in git-timemachine as a way to browse history in a git repository.
  • I no longer use git-annex. Here for reference.
(require 'init-git)
;;; init-git.el --- Init git integration -*- lexical-binding: t -*-
<<license>>


(use-package magit
  :commands magit-status
  :bind ("C-x g" . magit-status)
  :custom
  (magit-push-always-verify nil)
  :init
  (setq magit-last-seen-setup-instructions "1.4.0"))

(use-package git-timemachine
  :bind ("C-x v t" . git-timemachine-toggle)
  :commands git-timemachine)

(provide 'init-git)

Mail

There are several mail clients for Emacs, including the built-in ones Rmail and Gnus. I’ve used both mu4e and notmuch. I think both have their pros and cons. In the end, I settled on notmuch.

There are basically three parts to my email configuration:

  1. Fetching email
  2. Composing & sending email
  3. Reading and organizing email
(require 'init-email)
;;; init-email.el --- Init email management -*- lexical-binding: t -*-
<<license>>

Fetching email

I use a couple tools to fetch my email, depending on who the service provider is. For those providers where I can use IMAP, I use mbsync, aka isync, to sync mail to my workstation. While you can use mbsync to pull mail from Gmail, I use lieer to pull and push email & labels from a Gmail account. I recommend reviewing the instructions for more on how to setup and configure lieer.

I provide an example configuration for mbsync to use with Gmail below.

Example mbsyncrc configuration

A couple notes about this configuration:

  • It uses a utility I have to query auth-source from the CLI to look up credentials.
  • It stores mail in ~/.mail.
  • Recent releases of mbsync use the terminology Far/Near instead of Master/Slave.
Create Both
Expunge Both
SyncState *

IMAPAccount Provider1
Host mail.example.com
User myusername
PassCmd "~/bin/auth-source-query.py myusername mail.example.com 993"
SSLType IMAPS
CertificateFile ~/.mail/.certs.pem

IMAPStore provider1-remote
Account Provider1

MaildirStore provider1-local
Path ~/.mail/Provider1/
Inbox ~/.mail/Provider1/INBOX
Flatten .

Channel provider1
Far :provider1-remote:
Near :provider1-local:

Example lieer configuration

Actually, there isn’t much to configure. Again, review the instructions. At a high level, this is what I did.

git clone https://github.com/gauteh/lieer.git ~/src/lieer
pip3 install -r ~/src/lieer/requirements.txt
pushd ~/bin
ln -s ~/src/lieer/gmi gmi
popd

Example mbsyncrc with Gmail

The Arch Wiki has some nice information on configuring Isync with Gmail. One approach is to, by default, exclude all of the special [Gmail] folders and then include Drafts, Sent Mail, Trash, and Starred.

The following is the configuration I had when I used mbsync to pull from Gmail. The reason I pulled out the individual channels is because I didn’t want [Gmail] in the path name and this provided a way to manage that. For example, [Gmail]/All Mail as the remote folder would become Archive on my workstation.

I’ll finally note that you may need to create an application-specific password for Gmail. Follow Google’s instructions for creating the password and then store that in your password database.

IMAPAccount Gmail
Host imap.gmail.com
User myusername@gmail.com
PassCmd "~/bin/auth-source-query.py myusername imap.gmail.com 993"
SSLType IMAPS
CertificateFile ~/.mail/.certs.pem

IMAPStore gmail-remote
Account Gmail

MaildirStore gmail-local
Path ~/.mail/Gmail/
Inbox ~/.mail/Gmail/INBOX

Channel gmail-inbox
Far :gmail-remote:INBOX
Near :gmail-local:INBOX
MaxMessages 5000

Channel gmail-archive
Far :gmail-remote:"[Gmail]/All Mail"
Near :gmail-local:Archive

Channel gmail-drafts
Far :gmail-remote:"[Gmail]/Drafts"
Near :gmail-local:Drafts

Channel gmail-sent
Far :gmail-remote:"[Gmail]/Sent Mail"
Near :gmail-local:Sent

Channel gmail-trash
Far :gmail-remote:"[Gmail]/Trash"
Near :gmail-local:Trash
MaxMessages 5000

Channel gmail-flagged
Far :gmail-remote:"[Gmail]/Starred"
Near :gmail-local:flagged

Group Gmail
Channel gmail-inbox
Channel gmail-archive
Channel gmail-drafts
Channel gmail-sent
Channel gmail-trash
Channel gmail-flagged

The other thing of interest is that I used MaxMessages to limit how much to keep locally. The man page says:

Sets the maximum number of messages to keep in each Slave mailbox. This is useful for mailboxes where you keep a complete archive on the server, but want to mirror only the last messages (for instance, for mailing lists).

Composing email

message-mode is the Emacs message composition mode.

Notes:

  • message-forward-as-mime defaults to nil. If set to t, Emacs will forward messages as rfc822 attachments. When nil, it forwards the message directly inline.
  • Add function forge/mail-toggle-forward-mime to toggle whether to forward inline or as MIME.

(require 'message)
(with-eval-after-load 'message
  (defun forge/mail-toggle-forward-mime ()
    "Toggle whether to forward as MIME or inline."
    (interactive)
    (if (bound-and-true-p message-forward-as-mime)
        (setq message-forward-as-mime nil)
      (setq message-forward-as-mime t)))
  (setq mail-from-style 'angles
        message-kill-buffer-on-exit t
        message-forward-as-mime t
        message-citation-line-format "On %a, %Y-%m-%d at %T %z, %N wrote:"
        message-citation-line-function (quote message-insert-formatted-citation-line)
        message-make-forward-subject-function (quote message-forward-subject-fwd)
        message-signature t
        message-signature-file "~/.signature"
        message-sendmail-envelope-from 'header
        message-send-mail-function 'message-send-mail-with-sendmail)
  (add-hook 'message-mode-hook #'footnote-mode)
  (add-hook 'message-mode-hook #'turn-on-flyspell)
  (add-hook 'message-mode-hook #'yas-minor-mode)
  (add-hook 'message-mode-hook #'turn-on-auto-fill
            (lambda ()
              (turn-on-auto-fill)
              (setq fill-column 72)
              (setq mail-header-separator ""))))

(require 'mm-decode)
(with-eval-after-load 'mm-decode
  (setq mm-text-html-renderer 'shr
        mm-inline-large-images nil
        mm-inline-text-html-with-images nil
        mm-discouraged-alternatives '("text/html" "text/richtext")))

gnus-alias provides a mechanism to switch identities when composing email with message-mode. I use customize to set gnus-alias-identity-alist

(use-package gnus-alias
  :custom
  (gnus-alias-default-identity "work")
  :hook
  (message-setup . gnus-alias-determine-identity))

Here is an example of how gnu-alias-identity-alist should look. The extra headers field can be useful if you need to add headers on the outgoing email.

(setq gnus-alias-identity-alist
      '(("personal" ;; name
         nil        ;; nil
         "My Name <example@example.com>"  ;; email address
         nil        ;; organization header
         nil        ;; extra headers
         nil        ;; body text
         "~/.signature"))) ;; signature or signature file

gnus-dired is an option for attaching files to email.

(add-hook 'dired-mode #'turn-on-gnus-dired-mode)
(with-eval-after-load
    (setq gnus-dired-mail-mode 'notmuch-user-agent))

Sometimes it is convenient to open a new frame and compose an email there.

(defun forge/mail-toggle-compose-new-frame ()
  "Toggle whether to compose email in new frame."
  (interactive)
  (let ((frame "same"))
    (if (boundp 'notmuch-mua-compose-in)
        (if (eq notmuch-mua-compose-in 'current-window)
            (progn (setq frame "new") (setq notmuch-mua-compose-in 'new-frame))
          (setq notmuch-mua-compose-in 'current-window))
      (if mu4e-compose-in-new-frame
          (setq mu4e-compose-in-new-frame nil)
        (progn (setq mu4e-compose-in-new-frame t) (setq frame "new"))))
    (message "Compose mail in %s frame" frame)))

Boxquote

Sometimes it is handy to quote text when drafting an email and boxquote does this well.

(use-package boxquote)

Functions to facilitate abuse reports

Among my responsibilities is forwarding abuse reports from other service providers to the appropriate contact to handle. These just make that process a little simpler.

(defun forge/mail-forward-complaint (template)
  "Forward an abuse complaint using TEMPLATE."
  (interactive)
  (if (boundp 'notmuch-mua-compose-in) (notmuch-show-forward-message) (mu4e-compose 'forward))
  (message-goto-body)
  (yas-expand-snippet (yas-lookup-snippet template))
  (message-add-header (concat "Cc: " forge-mail-abuse-poc))
  (message-goto-to))

(defun forge/mail-forward-abuse-complaint ()
  "Forward an abuse complaint to responsible party."
  (interactive)
  (forge/mail-forward-complaint "abuse-template"))

(defun forge/mail-forward-infringement-complaint ()
  "Forward a infringement complaint to responsible party."
  (interactive)
  (forge/mail-forward-complaint "infringement-template"))

(defun forge/mail-forward-spam-complaint ()
  "Forward a spam complaint to responsible party."
  (interactive)
  (forge/mail-forward-complaint "spam-template"))

(defun forge/mail-forward-compromised-complaint ()
  "Forward a compromised account report to responsible party."
  (interactive)
  (forge/mail-forward-complaint "compromise-template"))

(defun forge/mail-reply-to-abuse ()
  "Set Reply-To header to Abuse POC."
  (interactive)
  (message-add-header (concat "Reply-To: " forge-mail-abuse-poc)))

(defun forge/mail-reply-to-noc ()
  "Set Reply-To header to NOC POC."
  (interactive)
  (message-add-header (concat "Reply-To: " forge-mail-noc-poc)))

(defun forge/mail-org-notes ()
  "Send org notes as an email."
  (interactive)
  (when (eq major-mode 'org-mode)
    (org-mime-org-subtree-htmlize)
    (message-goto-to)))

Email hydra

(defhydra forge/hydra-email (:color blue)
  "

  _A_ Forward Abuse report  _S_  Forward Spam report  _N_  Toggle compose New frame
  _I_ Forward Infringement  _C_  Comporomised report  _W_  Save all attachments
  _sd_ Search last day      _sw_ Search last week     _sm_ Search last month
  _ra_ Reply to Abuse POC   _rn_ Reply to NOC POC     _O_ Email Org notes
  "
  ("A" forge/mail-forward-abuse-complaint)
  ("ra" forge/mail-reply-to-abuse)
  ("rn" forge/mail-reply-to-noc)
  ("sd" notmuch-search-last-day)
  ("sw" notmuch-search-last-week)
  ("sm" notmuch-search-last-month)
  ("O" forge/mail-org-notes)
  ("I" forge/mail-forward-infringement-complaint)
  ("S" forge/mail-forward-spam-complaint)
  ("C" forge/mail-forward-compromised-complaint)
  ("W" forge/notmuch-save-all-attachments)
  ("N" forge/mail-toggle-compose-new-frame))

(global-set-key (kbd "C-c m") 'forge/hydra-email/body)

Sending email

Historically, I used smtpmail to send email. If you are online most, if not all, the time, this works well enough. smtpmail will block Emacs while it is going through the SMTP process, but this is usually very brief. Another alternative is to use msmtp to mail queuing if offline. You need to make sure that auth-source has credentials to perform SMTP AUTH.

In my case, I wrote my own utility to perform the SMTP exchange. This is because some mail providers will require XOAUTH2 authentication and the native tools were not in a place to support this.

(with-eval-after-load 'sendmail
  (setq mail-specify-envelope-from t
        mail-envelope-from 'header
        sendmail-program (executable-find "sendmail.py")))

Here is an example of my deprecated smtpmail configuration.

(use-package smtpmail
  :disabled t
  :config
  (setq smtpmail-stream-type 'ssl
        smtpmail-default-smtp-server forge-smtp-server-work
        smtpmail-smtp-server forge-smtp-server-work
        smtpmail-smtp-service 465
        smtpmail-smtp-user forge-smtp-user-work
        smtpmail-queue-dir (expand-file-name "queue" forge-state-dir)))

Signing and Encryption

This configures Emacs Message Mode security settings. The key points are to always encrypt to self and to list the OpenPGP keys that I will sign with.

  • C-c C-m s s Sign current message with S/MIME.
  • C-c C-m s o Sign current message with PGP.
  • C-c C-m s p Sign current message with PGP/MIME.
  • C-c RET C-c Encrypt current message.
  • C-c C-m c s Encrypt current message using S/MIME.
  • C-c C-m c o Encrypt current message using PGP.
  • C-c C-m c p Encrypt current message using PGP/MIME.
(with-eval-after-load 'mml-sec
  (setq mml-secure-openpgp-encrypt-to-self t
        mml-secure-openpgp-sign-with-sender t
        mml-secure-smime-encrypt-to-self t
        mml-secure-smime-sign-with-sender t)
  (add-to-list 'mml-secure-openpgp-signers "A852499F"))

Notmuch base configuration

Before I get into how I have Emacs configured to work with notmuch, the following gives an idea of how I configure notmuch itself. You will want to run notmuch setup first, which will create ~/.notmuch-config.

The important part from my configuration is the option new.tags which is the default tag applied to all messages incorporated by notmuch new.

#	tags	A list (separated by ';') of the tags that will be
#		added to all messages incorporated by "notmuch new".
[new]
tags=new

I then have two hooks configured for notmuch. The general flow when you run notmuch new is:

  1. Run the pre hook.
  2. Notmuch then indexes your email.
  3. Run the post hook.

The following are just examples of hooks. The first is the pre hook to fetch email.

# The pre-new hook
echo -e "---\nStart $(date)."
mbsync -a
exit 0

The next hook to run is the post hook, which will tag my email.

# The post-new hook
notmuch tag --batch <<EOF
+inbox                  -- folder:Work/INBOX or folder:Personal/INBOX
-inbox                  -- not ( folder:Personal/INBOX or folder:Work/INBOX )
+sent                   -- folder:Personal/Sent or folder:Work/Sent
+bulk                   -- folder:Work/Spam or folder:Work/incoming
-bulk                   -- not ( folder:Work/Spam or folder:Work/incoming or folder:Work/Trash )
+trash -archive -inbox  -- folder:Personal/Trash or folder:Work/Trash
#
+invite                 -- mimetype:text/calendar
#
-new -- tag:new
EOF

echo -e "Done at $(date)."
exit

Notmuch reading and organizing email

The following is then the configuration that integrates Emacs with Notmuch.

  • Update on [2022-06-24 Fri 09:45]
    Remove ability to mark whole buffer as read. Too easy to accidentally invoke.
(use-package notmuch
  :ensure nil
  :commands (notmuch)
  :custom
  (notmuch-search-oldest-first nil)
  (notmuch-hello-thousands-separator ",")
  (notmuch-crypto-process-mime t)
  (notmuch-crypto-gpg-program (executable-find "gpg"))
  (notmuch-show-part-button-default-action 'notmuch-show-view-part)

  :preface
  (defmacro forge-notmuch-show-tag (tags)
    "Macro to take list of tags and apply to query."
    `(progn
       (notmuch-show-add-tag ,tags)
       (unless (notmuch-show-next-open-message)
         (notmuch-show-next-thread t))))

  (defmacro forge-notmuch-search-tag (tags)
    "Macro to take list of tags and apply to query."
    `(progn
       (notmuch-search-tag ,tags)
       (notmuch-search-next-thread)))

  (defmacro forge-notmuch-show-toggle-tag (tag)
    "Macro to toggle presence of tag for query."
    `(progn
       (if (member ,tag (notmuch-show-get-tags))
           (notmuch-show-remove-tag (list (concat "-" ,tag)))
         (notmuch-show-add-tag (list (concat "+" ,tag))))))

  (defmacro forge-notmuch-search-toggle-tag (tag)
    "Macro to toggle presence of tag for query."
    `(progn
       (if (member ,tag (notmuch-search-get-tags))
           (notmuch-search-tag (list (concat "-" ,tag)))
         (notmuch-search-tag (list (concat "+" ,tag))))))

  (defun notmuch-search-attachment (ext)
    "Search for attachments with extension EXT.

You can provide a space-delimited list of extensions to search for.
Will open a notmuch search buffer of the search results."
    (interactive "sExtension: ")
    (notmuch-search
     (mapconcat 'identity
                (mapcar (lambda (arg) (concat "attachment:" arg)) (split-string ext)) " or ")))

  (defun notmuch-search-recent (period &optional query)
    "Search for recent mail for time period PERIOD.

Prompts for  QUERY and this will amend the search to
limit it to the provided time PERIOD.
Will open a notmuch search buffer of the search results."
    (let* ((query (or query (notmuch-read-query "Query: "))))
      (notmuch-search (concat "date:" period ".. AND " query))))

  (defun notmuch-search-last-day (&optional query)
    "Search recent mail for prompted query.

Search notmuch for QUERY and this will amend the search to
limit it to the last day.
Will open a notmuch search buffer of the search results."
    (interactive)
    (notmuch-search-recent "1d" query))

  (defun notmuch-search-last-week (&optional query)
    "Search recent mail for prompted query.

Search notmuch for QUERY and this will amend the search to
limit it to the last 7 days.
Will open a notmuch search buffer of the search results."
    (interactive)
    (notmuch-search-recent "7d" query))

  (defun notmuch-search-last-month (&optional query)
    "Search recent mail for prompted query.

Search notmuch for QUERY and this will amend the search to
limit it to the last month.
Will open a notmuch search buffer of the search results."
    (interactive)
    (notmuch-search-recent "1M" query))

  :bind
  (:map notmuch-show-mode-map
        ("g" . notmuch-refresh-this-buffer)
        ("y" . notmuch-show-archive-message-then-next-or-next-thread)
        ("Y" . notmuch-show-archive-thread-then-next)
        ("d" . (lambda ()
                 "mark message for trash"
                 (interactive)
                 (forge-notmuch-show-tag (list "+trash" "-inbox" "-unread" "-archive"))))
        ("I" . (lambda ()
                 "mark message for inbox and delete trash, if present."
                 (interactive)
                 (forge-notmuch-show-tag (list "-trash" "+inbox"))))
        ("J" . (lambda ()
                 "mark message as junk"
                 (interactive)
                 (forge-notmuch-show-tag (list "+bulk" "+trash" "-inbox" "-unread" "-archive"))))
        ("F" . (lambda ()
                 "toggle message as flagged"
                 (interactive)
                 (forge-notmuch-show-toggle-tag "flagged")))
        ("M" . (lambda ()
                 "toggle message as muted"
                 (interactive)
                 (forge-notmuch-show-toggle-tag "mute")))
        ("b" . (lambda (&optional address)
                 "Bounce the current message"
                 (interactive "sBounce to: ")
                 (notmuch-show-view-raw-message)
                 (message-resend address)))
        ("S-SPC" . notmuch-show-rewind))
  (:map notmuch-search-mode-map
        ("g" . notmuch-refresh-this-buffer)
        ("y" . notmuch-search-archive-thread)
        ("Y" . notmuch-search-archive-thread)
        ("d" . (lambda ()
                 "mark thread for trash"
                 (interactive)
                 (forge-notmuch-search-tag (list "+trash" "-inbox" "-unread" "-archive"))))
        ("I" . (lambda ()
                 "mark message for inbox and delete trash tag, if present."
                 (interactive)
                 (forge-notmuch-search-tag (list "-trash" "+inbox"))))
        ("J" . (lambda ()
                 "mark thread as junk"
                 (interactive)
                 (forge-notmuch-search-tag (list "+bulk" "+trash" "-inbox" "-unread" "-archive"))))
        ("F" . (lambda ()
                 "toggle thread as flagged"
                 (interactive)
                 (forge-notmuch-search-toggle-tag "flagged")))
        ("M" . (lambda ()
                 "toggle thread as muted"
                 (interactive)
                 (forge-notmuch-search-toggle-tag "mute")))
        ("S-SPC" . notmuch-search-scroll-down))
  (:map notmuch-show-part-map
        ("c" . forge/notmuch-show-calendar-invite))

  :config
  (add-hook 'notmuch-show-hook '(lambda () (setq show-trailing-whitespace nil)))
  (setq notmuch-archive-tags '("-unread" "-inbox" "-trash" "-bulk" "-spam")
        notmuch-address-save-filename "~/annex/var/notmuch/contacts"
        notmuch-saved-searches '(( :name "📥 Inbox"
                                   :key "i"
                                   :query "tag:inbox")
                                 ( :name "🚩 Flagged"
                                   :key "f"
                                   :query "tag:flagged or tag:important")
                                 ( :name "📅 Today"
                                   :key "t"
                                   :query "date:24h.. and ( tag:inbox or tag:unread )")
                                 ( :name "💬 Unread"
                                   :key "u"
                                   :query "tag:unread")
                                 ( :name "Sent"
                                   :key "s"
                                   :query "tag:sent")
                                 ( :name "3 days"
                                   :key "3"
                                   :query "date:3d..  and ( tag:inbox or tag:unread )")
                                 ( :name "Last 7 days"
                                   :key "7"
                                   :query "date:7d..  and ( tag:inbox or tag:unread )")
                                 ( :name "Last 30 days"
                                   :key "m"
                                   :query "date:1M..1d and ( tag:inbox or tag:unread )")
                                 ( :name "Old messages"
                                   :key "o"
                                   :query "date:..1M and ( tag:inbox or tag:bulk or tag:unread ) ")
                                 ( :name "Attachments"
                                   :key "A"
                                   :query "tag:attachment")
                                 ( :name "Bulk"
                                   :key "B"
                                   :query "tag:unread and ( tag:bulk or tag:spam )")
                                 ( :name "Meeting Invites"
                                   :key "c"
                                   :query "mimetype:text/calendar"))))

Luminance is something that helps with legibility when reading HTML emails on an either light or dark background.

(defun forge/twiddle-luminance (value)
  "Twiddle the luminance value to VALUE."
  (interactive "nLuminance: ")
  (message "Current luminance level: %s" shr-color-visible-luminance-min)
  (setq shr-color-visible-luminance-min value))

Attachments

The following helpers, one for notmuch and a legacy one for mu4e, will save all attachments to a directory.

(defcustom forge-attachment-dir
  (expand-file-name "Downloads" "~/")
  "Directory to save attachments from email."
  :group 'forge
  :type 'string)

(defun forge/mu4e-save-all-attachments (&optional msg)
  "Save all attachments in MSG to attachment directory.
The sub-directory in `forge-attachment-dir' is derived from the subject of the email message."
  (interactive)
  (let* ((msg (or msg (mu4e-message-at-point)))
         (subject (message-wash-subject (mu4e-message-field msg :subject)))
         (attachdir (concat forge-attachment-dir "/" subject))
         (count (hash-table-count mu4e~view-attach-map)))
    (if (> count 0)
        (progn
          (mkdir attachdir t)
          (dolist (num (number-sequence 1 count))
            (let* ((att (mu4e~view-get-attach msg num))
                   (fname (plist-get att :name))
                   (index (plist-get att :index))
                   fpath)
              (setq fpath (expand-file-name (concat attachdir "/" fname)))
              (mu4e~proc-extract
               'save (mu4e-message-field msg :docid)
               index mu4e-decryption-policy fpath))))
      (message "Nothing to extract"))))

;; This is derived in part from notmuch-show-save-attachments
;; but calls mm-save-part-to-file instead so as to save files without prompting.
(defun forge/notmuch-save-all-attachments ()
  "Save all attachments in MSG to attachment directory."
  (interactive)
  (let* ((subject (message-wash-subject (notmuch-show-get-subject)))
         (attachdir (concat (file-name-as-directory forge-attachment-dir) subject)))
    (with-current-notmuch-show-message
     (let ((mm-handle (mm-dissect-buffer)))
       (message "%s" subject)
       (mkdir attachdir t)
       (notmuch-foreach-mime-part
        (lambda (p)
          (let ((disposition (mm-handle-disposition p)))
            (and (listp disposition)
                 (or (equal (car disposition) "attachment")
                     (and (equal (car disposition) "inline")
                          (assq 'filename disposition)))
                 (mm-save-part-to-file
                  p (concat (file-name-as-directory attachdir) (cdr (assq 'filename disposition)))))))
        mm-handle)))))

Deprecated functions

(defun forge/mail-add-calendar-invite (handle &optional prompt)
  "Open calendar ICS part in Calendar."
  (ignore prompt)
  (mm-with-unibyte-buffer
    (mm-insert-part handle)
    (mm-add-meta-html-tag handle)
    (let ((path (expand-file-name "~/Download/invite.ics")))
      (mm-write-region (point-min) (point-max) path nil nil nil 'binary t)
      (start-process "add-calendar-invite" nil "/usr/bin/open" "-a" "/Applications/Microsoft Outlook.app" path))))

(defun forge/notmuch-show-calendar-invite ()
  "Save ics MIME part."
  (interactive)
  (notmuch-show-apply-to-current-part-handle #'forge/mail-add-calendar-invite))

(defun forge/mail-open-html ()
  "Open HTML part in browser."
  (interactive)
  (with-current-notmuch-show-message
      (let ((mm-handle (mm-dissect-buffer)))
        (notmuch-foreach-mime-part
         (lambda (p)
           (if (string-equal (mm-handle-media-type p) "text/html")
               (mm-display-part p "open")))
         ;;             (notmuch-show-view-part)))
         ;;             (notmuch-show-apply-to-current-part-handle #'mm-display-part)))
         mm-handle))))

Email keymap

(define-prefix-command 'my-mail-search-map)
(define-key my-mail-search-map (kbd "s") 'notmuch-search)
(define-key my-mail-search-map (kbd "r") 'notmuch-search-last-week)
(define-key my-mail-search-map (kbd "m") 'notmuch-search-last-month)
(define-key my-mail-search-map (kbd "A") 'notmuch-search-attachment)

(define-prefix-command 'my-mail-map)
(define-key my-mail-map (kbd "N") 'forge/mail-toggle-compose-new-frame)
(define-key my-mail-map (kbd "W") 'forge/notmuch-save-all-attachments)
(define-key my-mail-map (kbd "ra") 'forge/mail-reply-to-abuse)
(define-key my-mail-map (kbd "rn") 'forge/mail-reply-to-noc)
(define-key my-mail-map (kbd "s") 'my-mail-search-map)
(global-set-key (kbd "C-c m") 'my-mail-map)

Cleanup

(provide 'init-email)

Org Mode

Org

I pull in Org with all of the contributed packages via the org-plus-contrib package via the Org ELPA package repository.

Miscellaneous notes:

  • Column View is a way to view the outline tree in a column view. This configures the default column view using org-columns-default-format. You can then turn on column view with C-c C-x C-c. In column view, g will refresh and q will exit. You can also optionally also define different columns in an outline’s PROPERTIES.
:PROPERTIES:
:COLUMNS: %25ITEM %TAGS %PRIORITY %TODO
:END:
  • Initial visibility can be managed using the org-startup-folded variable and on a per-file basis with #+STARTUP: in the file. Allowed values are overview, content, showall, and showeverything. I’ve modified the default from showeverything to content so that it shows all headings when first visiting a file.
  • org-tags-exclude-from-inheritance to enumerate tags that should not be inherited. See also org-crypt.
  • Use org-reverse-note-order to refile items to the top of a document.
  • For org-timer-set-timer, use C-c-C-x ;. The timer is set to 25 minutes. This has taken the place of org-pomodoro. See Org timers.
(require 'init-org)
;;; init-org.el --- Init orgmode -*- lexical-binding: t -*-
<<license>>


(use-package org
  :preface
  (defun my-org-mode-hook ()
    "Turn on settings for org-mode."
    (interactive)
    (when (fboundp 'turn-off-auto-fill)
      (turn-off-auto-fill))
    (when (fboundp 'turn-on-flyspell)
      (turn-on-flyspell)))

  (defun init-my-org-agenda ()
    "Initialze org-agenda configuration."
    (interactive)
    (setq org-agenda-skip-scheduled-if-deadline-is-shown t
          org-agenda-sticky t
          org-agenda-hide-tags-regexp "."
          org-agenda-restore-windows-after-quit t
          org-agenda-window-setup 'current-window
          org-agenda-compact-blocks nil
          org-agenda-files
          (list (concat org-directory "/inbox.org")
                (concat org-directory "/agenda.org")
                (concat org-directory "/journal.org")
                (concat org-directory "/work.org")
                (concat org-directory "/personal.org"))
          org-agenda-prefix-format
          '((agenda . " %i %-12:c%?-12t% s")
            (todo   . " %i %-12:c")
            (tags   . " %i %-12:c")
            (search . " %i %-12:c")))
    ;; There's a lot to org-agenda-custom-commands
    ;; For type:
    ;;   type     The command type, any of the following symbols:
    ;;     agenda      The daily/weekly agenda.
    ;;     todo        Entries with a specific TODO keyword, in all agenda files.
    ;;     search      Entries containing search words entry or headline.
    ;;     tags        Tags/Property/TODO match in all agenda files.
    ;;     tags-todo   Tags/P/T match in all agenda files, TODO entries only.
    ;;     todo-tree   Sparse tree of specific TODO keyword in *current* file.
    ;;     tags-tree   Sparse tree with all tags matches in *current* file.
    ;;     occur-tree  Occur sparse tree for *current* file.
    (setq org-agenda-custom-commands
          '(("2" "Next two weeks"
             ((agenda ""
                      ((org-agenda-start-on-weekday nil)
                       ;; (org-agenda-start-day "+1d")
                       (org-agenda-span 14)
                       (org-deadline-warning-days 0)
                       (org-agenda-block-separator nil)
                       (org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))
                       (org-agenda-overriding-header "\n📅 Next 14 days\n")))))
            ("g" "Overview"
             ((agenda ""
                      ((org-agenda-overriding-header "🕐 Today\n")
                       (org-agenda-span 1)
                       (org-deadline-warning-days 0)
                       (org-agenda-day-face-function (lambda (date) 'org-agenda-date))
                       (org-agenda-block-separator nil)))
              (agenda ""
                      ((org-agenda-start-on-weekday nil)
                       (org-agenda-start-day "+1d")
                       (org-agenda-span 'week)
                       (org-deadline-warning-days 0)
                       (org-agenda-block-separator nil)
                       (org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))
                       (org-agenda-overriding-header "\n📅 Next 7 days\n")))
              (tags-todo "inbox"
                         ((org-agenda-prefix-format "  %?-12t% s")
                          (org-agenda-block-separator nil)
                          (org-agenda-overriding-header "\n📥 Inbox\n")))
              (agenda ""
                      ((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-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))
                       (org-agenda-overriding-header "\n🞜 Upcoming deadlines (+14d)\n")))
              (todo "NEXT"
                    ((org-agenda-skip-function '(org-agenda-skip-entry-if 'deadline))
                     (org-agenda-prefix-format "  %i %-12:c [%e] ")
                     (org-agenda-block-separator nil)
                     (org-agenda-overriding-header "\nNext\n")))
              (todo "WAITING"
                    ((org-agenda-skip-function '(org-agenda-skip-entry-if 'deadline))
                     (org-agenda-prefix-format "  %i %-12:c [%e] ")
                     (org-agenda-block-separator nil)
                     (org-agenda-overriding-header "\n💤 Waiting\n")))
              (todo "PROJECT"
                    ((org-agenda-skip-function '(org-agenda-skip-entry-if 'deadline))
                     (org-agenda-prefix-format "  %i %-12:c [%e] ")
                     (org-agenda-block-separator nil)
                     (org-agenda-overriding-header "\n🚧 Projects\n")))
              (tags-todo "SOMEDAY"
                         ((org-agenda-skip-function '(org-agenda-skip-entry-if 'deadline))
                          (org-agenda-prefix-format "  %i %-12:c [%e] ")
                          (org-agenda-block-separator nil)
                          (org-agenda-overriding-header "\n💤 Someday\n")))
              (tags "CLOSED>=\"<-10d>\""
                    ((org-agenda-overriding-header "\n✓ Completed last 10 days\n"))))))))

  (defun init-my-org-capture-templates ()
    "Set up org-capture-templates."
    (interactive)
    ;; For template expansion,
    ;; see https://orgmode.org/manual/Template-expansion.html#Template-expansion
    (setq org-capture-templates
          `(("i" "Inbox" entry
             (file "inbox.org")
             "* TODO %? \n:PROPERTIES:\n:CAPTURED:  %U\n:END:\nReference: %a\n")
            ("c" "Calendar invite" entry
             (file+headline "agenda.org" "Future")
             (function notmuch-calendar-capture-event)
             :prepend t)
            ("l" "Log" entry
             (file+olp+datetree "journal.org")
             "* %U - %?\n")
            ("n" "Meeting notes" entry
             (file+olp+datetree "journal.org")
             "* Notes - %a \n:PROPERTIES:\n:CAPTURED:  %U\n:END:\n%U\nAttendees:\n\nAgenda:\n\nDiscussion:\n"
             :clock-in t
             :clock-resume t)
            ("j" "Journal" entry
             (file+olp+datetree "journal.org")
             "* %?\n%U\n"
             :clock-in t
             :clock-resume t)
            ("b" "Bookmark" entry
             (file+headline "notebook.org" "Unfiled")
             "* %^L %^g \n:PROPERTIES:\n:CAPTURED: %U\n:END:\n\n"
             :prepend t)

            ("r" "Reference")
            ("rm" "Music" entry
             (file+olp+datetree "journal.org")
             "* %(forge/capture-current-song) :music:\n%U\n")
            ("rr" "Reference" entry
             (file+olp+datetree "articles.org")
             "* %a %?\n:PROPERTIES:\n:CAPTURED:  %U\n:END:\n"
             :prepend t)
            ("rw" "Web Page" entry
             (file+olp+datetree "articles.org")
             (function my-org-clip-web-page)
             :prepend t)
            ("rf" "Elfeed/News Article" entry
             (file+olp+datetree "articles.org")
             "* %a %? :%(forge/elfeed-get-entry-tags):ARTICLE:\n:PROPERTIES:\n:CAPTURED:  %U\n:END:\n"
             :prepend t)
            ("rt" "Twitter Post" entry
             (file+olp+datetree "articles.org")
             "* %a %? :TWITTER:\n:PROPERTIES:\n:CAPTURED:  %U\n:END:\n"
             :prepend t))))

  (defun my-org-goto-journal-entry (date)
    "Go to specified journal entry for DATE and narrow to subtree."
    (interactive (list (org-read-date)))
    (let* ((date-list (org-date-to-gregorian date))
           (year (number-to-string (nth 2 date-list))))
      (find-file (expand-file-name "journal.org" org-directory))
      (widen)
      (org-datetree-find-date-create date-list)))

  (defun my-org-goto-relative-journal-entry (offset)
    "Go to a relative date's journal entry in the datetree and narrow to subtree."
    (interactive "Enter date offset (e.g., +3, fri, +2tue): ")
    (let ((date (org-read-date nil nil offset)))
      (my-org-goto-journal-entry date)))

  (defun my-org-goto-yesterday-journal-entry ()
    "Go to yesterday's journal entry in the datetree and narrow to subtree."
    (interactive)
    (my-org-goto-relative-journal-entry "-1"))

  (defun my-org-goto-tomorrow-journal-entry ()
    "Go to tomorrow's journal entry in the datetree and narrow to subtree."
    (interactive)
    (my-org-goto-relative-journal-entry "+1"))

  (defun my-org-init-hook ()
    "Set up defaults after org.el has been loaded."
    (init-my-org-agenda)
    (init-my-org-capture-templates))

  (defun my-org-fixed-font-faces ()
    "Keep the following with fixed-pitch fonts."
    (interactive)
    (set-face-attribute 'org-table nil :inherit 'fixed-pitch)
    (set-face-attribute 'org-code nil :inherit 'fixed-pitch)
    (set-face-attribute 'org-block nil :inherit 'fixed-pitch))

  (defun my-tangle-org-mode-on-save ()
    "Tangle org-mode file when saving."
    (when (string= (message "%s" major-mode) "org-mode")
      (org-babel-tangle)))


  (defun my-org-set-property (property value)
    "Set arbitrary PROPERTY to VALUE for current heading."
    (org-back-to-heading)
    (when (not (org-element-property :CREATED (org-element-at-point)))
      (org-set-property property value)))

  (defun my-org-set-uuid ()
    "Set ID property for current headline."
    (interactive)
    (my-org-set-property "ID" (org-id-uuid)))

  (defun my-org-set-created ()
    "Set CREATED property for current headline."
    (interactive)
    (my-org-set-property "CREATED" (with-temp-buffer (org-insert-time-stamp (current-time) t t))))

  (defun my-org-timer-clock-in ()
    "Clock in when starting a org-timer."
    (if (eq major-mode 'org-agenda-mode)
        (call-interactively 'org-agenda-clock-in)
      (call-interactively 'org-clock-in)))

  (defun my-org-timer-clock-out ()
    "Clock in when starting a org-timer."
    (if (eq major-mode 'org-agenda-mode)
        (call-interactively 'org-agenda-clock-out)
      (call-interactively 'org-clock-out)))

  (defun my-org-set-properties ()
    "Set stock org properties for current headline."
    (interactive)
    (my-org-set-uuid)
    (my-org-set-created))

  (defun my-org-clip-web-page ()
    "Clip web page for org capture."
    (interactive)
    (when (derived-mode-p 'eww-mode)
      (require 'ol-eww)
      (org-eww-copy-for-org-mode)
      (concat
       "* %a %? :ARTICLE:
  :PROPERTIES:
  :CREATED:  %U
  :URL:      " (eww-current-url) "
  :END:\n\n" (car kill-ring))))


  :hook
  ((org-mode . my-org-mode-hook)
   (org-load . my-org-init-hook)
   (after-save . my-tangle-org-mode-on-save)
   (org-timer-set . my-org-timer-clock-in)
   (org-timer-done . my-org-timer-clock-out)
   (org-timer-stop . my-org-timer-clock-out)
   (org-mode . variable-pitch-mode))

  :custom
  (org-attach-id-dir "~/annex/org/data/")
  (org-directory "~/forge")
  (org-attach-method 'mv)
  (org-babel-python-command "python3")
  (org-catch-invisible-edits 'smart)
  (org-clock-display-default-range 'thisweek)
  (org-clock-in-resume t)
  (org-clock-out-remove-zero-time-clocks t)
  (org-clock-persist t)
  (org-clock-sound (expand-file-name "drip.ogg" "~/annex/Music/"))
  (org-confirm-babel-evaluate nil)
  (org-default-notes-file (expand-file-name "journal.org" org-directory))
  (org-ellipsis "")
  (org-export-allow-bind-keywords t)
  (org-export-backends '(ascii html icalendar latex md))
  (org-export-coding-system 'utf-8)
  (org-html-checkbox-type 'html)
  (org-list-allow-alphabetical t)
  (org-log-done t)
  (org-log-reschedule "note")
  (org-log-into-drawer t)
  (org-outline-path-complete-in-steps nil)
  (org-refile-use-outline-path 'file)
  (org-refile-targets '(("inbox.org" :maxlevel . 2)
                        ("agenda.org" :maxlevel . 3)
                        ("articles.org" :maxlevel . 3)
                        ("notebook.org" :maxlevel . 5)
                        ("work.org" :maxlevel . 5)
                        ("personal.org" :maxlevel . 5)
                        (nil :maxlevel . 2)))
  (org-reverse-note-order t)
  (org-src-fontify-natively t)
  (org-startup-indented t)
  (org-startup-folded 'content)
  (org-timer-default-timer 25)
  (org-list-demote-modify-bullet '(("+" . "-") ("-" . "+") ("*" . "+") ("1." . "a.")))

  :bind
  (("<f8>" . org-cycle-agenda-files)
   ("<f12>" . org-agenda)
   ("C-c l" . org-store-link)
   ("C-c c" . org-capture)
   ("C-c a" . org-agenda)
   ("C-c b" . org-switchb))
  (:map org-mode-map
        ("M-q" . endless/fill-or-unfill)
        ("RET" . org-return))

  :init
  (setq org-file-apps
        '((auto-mode . emacs)
          ("\\.doc\\'" . "open %s")
          ("\\.docx\\'" . "open %s")
          ("\\.xlsx\\'" . "open %s")
          ("\\.pptx\\'" . "open %s")
          ("\\.pdf\\'" . default)))
  (setq org-structure-template-alist
        '(("a" . "export ascii")
          ("c" . "center")
          ("C" . "comment")
          ("e" . "example")
          ("E" . "export")
          ("h" . "export html")
          ("l" . "src emacs-lisp")
          ("m" . "export md")
          ("p" . "src python")
          ("q" . "quote")
          ("s" . "src")
          ("v" . "verse")
          ("y" . "src yaml")))

  ;; Workflow states
  ;; https://orgmode.org/manual/Workflow-states.html#Workflow-states
  (setq org-todo-keywords '((sequence "TODO(t)" "NEXT(n)" "WAITING(w)" "SOMEDAY(m)" "|" "DONE(d)" "DELEGATED(l)" "CANCELLED(c)")
                            (sequence "PROJECT" "|" "DONE(d)")
                            (sequence "|" "MEETING" "REFERENCE(r)")))
  ;; List of tags that should never be inherited.
  (setq org-tags-exclude-from-inheritance '("crypt"))
  ;;
  (setq org-columns-default-format "%50ITEM(Task) %2PRIORITY %10Effort(Effort){:} %10CLOCKSUM"
        org-modules '(org-id ol-eww ol-docview ol-info ol-irc org-habit)
        org-src-preserve-indentation t
        org-src-window-setup 'current-window                    ;; use current window when editing a source block
        org-cycle-separator-lines 2                             ;; leave this many empty lines in collapsed view
        org-table-export-default-format "orgtbl-to-csv"         ;; export tables as CSV instead of tab-delineated
        org-publish-project-alist '(("public"
                                     :base-directory "~/forge"
                                     :publishing-directory "~/Documents")))

  )



(with-eval-after-load 'org
  ;; (forge/org-fixed-font-faces)
  ;; (org-load-modules-maybe t)
  (org-babel-do-load-languages 'org-babel-load-languages '((ditaa . t)
                                                           (dot . t)
                                                           (emacs-lisp . t)
                                                           (org . t)
                                                           (perl . t)
                                                           (python . t)
                                                           (ruby . t)
                                                           (shell . t)
                                                           (calc . t))))

Org Contacts, Notmuch and mail

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

org-mime allows one to use Org formatting for email.

(use-package org-mime
  :config
  (add-hook 'message-mode-hook
            (lambda ()
              (local-set-key "\C-c\M-o" 'org-mime-htmlize)))
  (add-hook 'org-mode-hook
            (lambda ()
              (local-set-key "\C-c\M-o" 'org-mime-org-buffer-htmlize)))
  :init
  (setq org-mime-export-options '(:section-numbers nil :with-author nil :with-toc nil)))
(use-package org-contacts
  :after org
  :config
  (setq org-contacts-files (list  "~/forge/contacts.org"))
  (add-to-list 'org-capture-templates
               '("C" "Contacts" entry
                 (file "~/forge/contacts.org")
                 "* %(org-contacts-template-name)\n:PROPERTIES:\n:EMAIL: %(org-contacts-template-email)\n:PHONE:\n:ADDRESS:\n:BIRTHDAY:\n:END:")))

Presentations with org-tree-slide

(use-package org-tree-slide
  :bind (:map org-tree-slide-mode-map
              ("<f8>" . org-tree-slide-mode)
              ("<f9>" . org-tree-slide-move-previous-tree)
              ("<f10>" . org-tree-slide-move-next-tree)
              ("C-,"  . org-tree-slide-move-previous-tree)
              ("C-." . org-tree-slide-move-next-tree))
  :init
  (setq org-tree-slide-skip-outline-level 4))

Presentations with org-reveal

See: https://github.com/yjwen/org-reveal/

(use-package ox-reveal)

Org Crypt

Org Crypt encrypts the text of an entry, but not the headline or properties associated with the entry.

  • org-crypt-key Either the Key ID or set to nil to use symmetric encryption.
  • Use org-encrypt-entry on a headline to encrypt.
  • Similarly, use org-decrypt-entry on a headline to decrypt.
  • Excluding the crypt tag from inheritance prevents already encrypted text from being encrypted again.
(with-eval-after-load 'org-crypt
  (org-crypt-use-before-save-magic)
  (setq org-crypt-disable-auto-save t
        org-crypt-key user-full-name))

Org ID

(with-eval-after-load 'org-id
  (setq org-id-method 'uuid
        org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id
        org-id-locations-file (expand-file-name "org/id-locations.el" forge-state-dir)))

Odds and ends

;;(use-package ol-git-link :straight (org-contrib :includes ol-git-link))

(use-package ol-eww
  :ensure nil
  :after org)

;; support links to manual pages
(use-package ol-man
  :ensure nil
  :after org)

(use-package ox-md
  :ensure nil
  :after org)

(use-package org-bullets
  :hook (org-mode . org-bullets-mode))

(use-package htmlize)

;; (use-package ox-twbs
;;   :commands (org-twbs-export-to-html
;;              org-twbs-export-as-html
;;              org-twbs-convert-region-to-html))

;; (use-package ox-reveal
;;   :after org-compat
;;   :custom (org-reveal-note-key-char nil))

;; (use-package ox-tufte :after org)

Helper functions

(defun forge/capture-current-song ()
  "Capture the current song details."
  (let ((itunes-song (my-get-current-song-itunes))
        (mpd-song (when (fboundp 'forge/get-current-song-mpd) (forge/get-current-song-mpd)))
        (song-info nil))
    (setq song-info (if itunes-song itunes-song mpd-song))
    (concat (car song-info) ", \"" (car (cdr song-info)) "\"")))

(defun my-org-set-lastupdated ()
  "Set LASTUPDATED property to today."
  (interactive)
  (org-set-property "LASTUPDATED" (format-time-string (org-time-stamp-format nil t))))

(defun forge/org-table-export (name)
  "Search for table named `NAME` and export"
  (interactive "sTable: ")
  (outline-show-all)
  (push-mark)
  (goto-char (point-min))
  (let ((case-fold-search t))
    (if (search-forward-regexp (concat "#\\+NAME: +" name) nil t)
        (progn
          (forward-line)
          (org-table-export (format "%s.csv" name) "orgtbl-to-csv"))))
  (pop-mark))

;; via https://vxlabs.com/2018/10/29/importing-orgmode-notes-into-apple-notes/
(defun forge/org-html-publish-to-html-for-apple-notes (plist filename pub-dir)
  "Convert exported files to format that plays nicely with Apple Notes. Takes PLIST, FILENAME, and PUB-DIR."
  ;; https://orgmode.org/manual/HTML-preamble-and-postamble.html
  ;; disable author + date + validate link at end of HTML exports
  ;;(setq org-html-postamble nil)

  (let* ((org-html-with-latex 'imagemagick)
         (outfile
          (org-publish-org-to 'html filename
                              (concat "." (or (plist-get plist :html-extension)
                                              org-html-extension
                                              "html"))
                              plist pub-dir)))
    ;; 1. apple notes handles <p> paras badly, so we have to replace all blank
    ;;    lines (which the orgmode export accurately leaves for us) with
    ;;    <br /> tags to get apple notes to actually render blank lines between
    ;;    paragraphs
    ;; 2. remove large h1 with title, as apple notes already adds <title> as
    ;; the note title
    (shell-command (format "sed -i \"\" -e 's/^$/<br \\/>/' -e 's/<h1 class=\"title\">.*<\\/h1>$//' %s" outfile)) outfile))

(defun forge/tangle-file (file)
  "Given an 'org-mode' FILE, tangle the source code."
  (interactive "fOrg File: ")
  (find-file file)
  (org-babel-tangle)
  (kill-buffer))

(defun forge/tangle-files (path &optional full)
  "Tangle files in PATH (directory), FULL for absolute paths.
Example: (forge/tangle-files \"~/.emacs.d/*.org\")."
  (interactive)
  (mapc 'forge/tangle-file (forge/get-files path full)))

(defun forge/get-files (path &optional full)
  "Return list of files in directory PATH that match glob pattern, FULL for absolute paths."
  (directory-files (file-name-directory path)
                   full
                   (eshell-glob-regexp (file-name-nondirectory path))))

(defun my/migrate-datetree-entry ()
  "Take an org entry from a datetree outline and migrate to an org-journal file.

The general intent behind this function is that it will migrate the current heading
and advance to the next heading.  One can then bind it to a macro for the repetition piece.
It will not remove entries from the source org file."
  (interactive)
  (org-beginning-of-line)
  (let* ((heading (nth 4 (org-heading-components)))
         (tags (nth 5 (org-heading-components)))
         (year (format-time-string "%Y" (apply 'encode-time (org-parse-time-string heading))))
         (time (format-time-string "%H:%M" (apply 'encode-time (org-parse-time-string heading))))
         (day (format-time-string "%A, %d %B %Y" (apply 'encode-time (org-parse-time-string heading))))
         (subject (when (string-match "\] *\\(.*\\)" heading) (match-string 1 heading)))
         (day-heading (format "* %s" day))
         (jrnl-heading (format "** %s %s   %s" time subject (or tags ""))))
    (org-copy-subtree)
    (with-current-buffer year
      (my/migrate-datetree-goto-heading day-heading)
      (message "%s" jrnl-heading)
      (insert (format "%s\n" jrnl-heading))
      (org-paste-subtree)
      (kill-line 1))
    (forward-line)
    ;; go to next datetree heading
    (re-search-forward "^\\*\\*\\*\\* \\[" nil t)))

(defun my/migrate-journal-entry ()
  "Migrate an org-journal entry."
  (interactive)
  (org-beginning-of-line)
  (if (= 1 (nth 1 (org-heading-components)))
      (org-next-visible-heading 1)
    (let* ((heading (nth 4 (org-heading-components)))
           (time (when (string-match "\\(..:..\\)" heading) (match-string 1 heading))))
      (push-mark)
      (outline-up-heading 1)
      (let* ((date (org-entry-get (point) "CREATED"))
             (date-heading (nth 4 (org-heading-components)))
             (weekday (when (string-match "\\(.*\\)," date-heading) (match-string 1 date-heading)))
             (shortday (when (string-match "\\(...\\)" weekday) (match-string 1 weekday)))
             (month (when (string-match ", [0-9]+ \\(.*\\) " date-heading) (match-string 1 date-heading)))
             (year (when (string-match "\\(....\\)" date) (match-string 1 date)))
             (monthint (when (string-match "....\\(..\\).." date) (match-string 1 date)))
             (dayint (when (string-match "......\\(..\\)" date) (match-string 1 date)))
             (month-heading (format "** %s-%s %s" year monthint month))
             (day-heading (format "*** %s-%s-%s %s" year monthint dayint weekday))
             (org-ts (format "[%s-%s-%s %s %s]" year monthint dayint shortday time)))
        (pop-global-mark)
        (org-copy-subtree)
        (with-current-buffer "journal.org"
          (my/migrate-datetree-goto-heading month-heading)
          (message "month heading %s" month-heading)
          (my/migrate-datetree-goto-heading day-heading)
          (message "day heading %s" day-heading)
          (org-paste-subtree 4)
          (forward-line)
          (insert (format "%s\n" org-ts)))
        (org-next-visible-heading 1)))))

(defun my/migrate-datetree-goto-heading (heading)
  "Go to day heading HEADING in org-journal file.  Create if it doesn't exist."
  (interactive)
  (goto-char (point-min))
  (unless (search-forward heading nil t)
    (progn (goto-char (point-max))
           (insert (format "%s\n" heading))
           (goto-char (point-min))))
  (search-forward heading nil t)
  (goto-char (point-max)))

Org Journal

I used to use org-journal for journaling, but now just use Org’s datetree model.

(use-package org-journal
  :preface
  (defun org-journal-file-header-func (time)
  "Custom function to create journal header."
  (concat
    (pcase org-journal-file-type
      (`daily "#+TITLE: Daily Journal\n#+STARTUP: showeverything")
      (`weekly "#+TITLE: Weekly Journal\n#+STARTUP: folded")
      (`monthly "#+TITLE: Monthly Journal\n#+STARTUP: folded")
      (`yearly "#+TITLE: Yearly Journal\n#+STARTUP: folded"))))

  (defun org-journal-find-location ()
    "Open today's journal file."
    ;; For integration with org-capture ...
    ;; Open today's journal, but specify a non-nil prefix argument in order to
    ;; inhibit inserting the heading; org-capture will insert the heading.
    ;; This should also get org-mode to the right place to add a heading at the correct depth
    (org-journal-new-entry t)
    (unless (eq org-journal-file-type 'daily)
      (org-narrow-to-subtree))
    (goto-char (point-max)))
  :init
  (setq org-journal-prefix-key "C-c j ")
  :config
  (setq org-journal-dir (expand-file-name "journal" org-directory)
        org-journal-file-header 'org-journal-file-header-func
        org-journal-file-type 'yearly
        org-journal-created-property-timestamp-format "%Y-%m-%d"
        org-journal-file-format "%Y.org"
        org-journal-date-format "%Y-%m-%d %A"))

Postamble

(provide 'init-org)

Password management

This configures a couple things: auth-source and password store. First, on MacOS, specify the pass to gpg and the type of pinentry. I use a couple tools to get passwords from password-store. The first is pass which provides a UI overview of the password store. I also use ivy-pass as a quick way to copy passwords to the clipboard.

(require 'init-pass)
;;; init-pass.el --- Init password management -*- lexical-binding: t -*-
<<license>>


(when (forge/system-type-darwin-p)
  (setopt epg-gpg-program (executable-find "gpg"))
  (setopt epg-pinentry-mode 'loopback))

(use-package auth-source)

(use-package auth-source-pass
  :after auth-source
  :init
  (setq auth-sources '(password-store "~/.authinfo.gpg")))

;; https://github.com/ccrusius/auth-source-xoauth2
(use-package auth-source-xoauth2
  :after auth-source)

;;
(use-package pass
  :config
  (advice-add 'pass-quit :after 'forge/delete-window))

;; https://github.com/ecraven/ivy-pass
(use-package ivy-pass
  :bind
  ("C-c p" . ivy-pass))

(provide 'init-pass)

RSS reader Elfeed

Now, let’s configure Elfeed. This sets up several keybindings to make it easy to navigate a news feed. It also sets a timer elfeed-update-timer to fetch articles every 2 hours.

(require 'init-elfeed)
;;; init-elfeed.el --- Init Elfeed -*- lexical-binding: t -*-
<<license>>


(use-package elfeed
  :commands (elfeed)
  :bind
  (:map elfeed-search-mode-map
        ("j" . next-line)
        ("k" . previous-line)
        ("d" . elfeed-search-youtube-dl)
        ("f" . forge/elfeed-search-toggle-starred)
        ("o" . elfeed-search-mpv)
        ("J" . elfeed-unjam)
        ("S" . forge/elfeed-search-save-db)
        ("R" . forge/elfeed-search-mark-all-read)
        ("F" . forge/elfeed-search-starred)
        ("U" . forge/elfeed-search-unread)
        ("<" . forge/elfeed-search-first-article)
        (">" . forge/elfeed-search-last-article)
        :map elfeed-show-mode-map
        ("j" . elfeed-show-next)
        ("k" . elfeed-show-prev)
        ("d" . elfeed-show-youtube-dl)
        ("e" . elfeed-show-open-eww)
        ("f" . forge/elfeed-show-toggle-starred)
        ("o" . elfeed-show-mpv))
  :preface
  (defun forge/elfeed-load-db ()
    "Wrapper to load elfeed database from disk when running elfeed."
    (elfeed-db-load))

  (advice-add 'elfeed :before #'forge/elfeed-load-db)

  (defun forge/elfeed-stop-timer ()
    "Cancel elfeed-update-timer."
    (interactive)
    (when elfeed-update-timer (cancel-timer elfeed-update-timer)))

  (defun forge/elfeed-start-timer ()
    "Start elfeed-update-timer."
    (interactive)
    (setq elfeed-update-timer (run-at-time 180 (* 120 60) 'forge/elfeed-update)))

  :config
  (defun elfeed-search-mpv ()
    "Play the current entry with mpv"
    (interactive)
    (message "url %s" (elfeed-entry-link (car (elfeed-search-selected))))
    (start-process "*elfeed-mpv*" nil "mpv" (elfeed-entry-link (car (elfeed-search-selected)))))

  (defun elfeed-show-mpv ()
    "Play the current entry with mpv"
    (interactive)
    (start-process "*elfeed-mpv*" nil "mpv" (elfeed-entry-link elfeed-show-entry)))

  (defun elfeed--youtube-dl (entry)
    "Download a video for ENTRY via youtube-dl."
    (if (null (youtube-dl (elfeed-entry-link entry)
                          :title (elfeed-entry-title entry)
                          ;; elfeed-feed-author will return a list of plist that will look like ((:name "HappyBlog" :uri "https://example.com/happyblog"))
                          :directory (concat youtube-dl-directory "/" (plist-get (car (elfeed-feed-author (elfeed-entry-feed entry))) :name))))
        (message "Entry is not a youtube link")
      (message "Downloading %s" (elfeed-entry-title entry))))

  ;; from skeeto
  ;; https://github.com/skeeto/.emacs.d/blob/master/etc/feed-setup.el
  (defun elfeed-search-youtube-dl ()
    "Download the current entry/entries with youtube-dl"
    (interactive)
    (let ((entries (elfeed-search-selected)))
      (dolist (entry entries)
        (elfeed--youtube-dl entry)
        (elfeed-untag entry 'unread)
        (elfeed-search-update-entry entry)
        (unless (use-region-p) (forward-line)))))

  ;; from skeeto
  ;; https://github.com/skeeto/.emacs.d/blob/master/etc/feed-setup.el
  (defun elfeed-show-youtube-dl ()
    "Download the current entry with youtube-dl"
    (interactive)
    (elfeed--youtube-dl elfeed-show-entry))

  (defun forge/elfeed-entry-tags ()
    "Return entry tags as a string."
    (interactive)
    (let ((entry))
      (if (eq major-mode 'elfeed-show-mode)
          (setq entry elfeed-show-entry)
        (setq entry (car (elfeed-search-selected))))
      (upcase (mapconcat #'symbol-name (elfeed-entry-tags entry) ":"))))

  (defun forge/elfeed-get-entry-tags ()
    "hello"
    (interactive)
    (with-current-buffer "*elfeed-entry*"
      (forge/elfeed-entry-tags)))

  (defun elfeed-show-open-eww ()
    "Open the current entry with eww."
    (interactive)
    (eww (elfeed-entry-link elfeed-show-entry))
    (add-hook 'eww-after-render-hook 'eww-readable nil t))

  (defun forge/elfeed-search-starred ()
    "Show starred elfeed articles"
    (interactive)
    (elfeed-search-set-filter "+starred"))

  (defun forge/elfeed-search-unread ()
    "Show elfeed articles tagged with unread"
    (interactive)
    (elfeed-search-set-filter "@6-months-ago +unread"))

  (defun forge/elfeed-search-save-db ()
    "Save elfeed database to disk."
    (interactive)
    (elfeed-db-save)
    (message "elfeed db saved."))

  ;; from manuel uberti
  ;; https://manuel-uberti.github.io/emacs/2017/08/01/elfeed/
  (defun forge/elfeed-search-mark-all-read ()
    "Mark all articles as read."
    (interactive)
    (call-interactively 'mark-whole-buffer)
    (elfeed-search-untag-all-unread))

  (defalias 'forge/elfeed-search-toggle-starred (elfeed-expose #'elfeed-search-toggle-all 'starred))

  (defun forge/elfeed-show-toggle-starred ()
    "Toggle starred tag for elfeed article"
    (interactive)
    (forge/elfeed-show-toggle-tag 'starred))

  (defun forge/elfeed-show-toggle-tag (tag)
    "Toggle tag for elfeed article."
    (interactive)
    (if (elfeed-tagged-p tag elfeed-show-entry)
        (elfeed-show-untag tag)
      (elfeed-show-tag tag)))

  (defun forge/elfeed-update ()
    "Update elfeed database."
    (message "Updating elfeed articles...")
    (elfeed-update)
    (elfeed-db-save))

  (defun forge/elfeed-search-first-article ()
    "Go to first message in search."
    (interactive)
    (goto-char (point-min)))

  (defun forge/elfeed-search-last-article ()
    "Go to last message in search."
    (interactive)
    (goto-char (point-max)))

  (defface elfeed-search-starred-title-face '((t :foreground "#f77"))
    "Marks a starred Elfeed entry.")
  (push '(starred elfeed-search-starred-title-face) elfeed-search-face-alist)

  ;; Lastly, the following will helps with downloading videos from Youtube when they
  ;; are part of a RSS feed.
  (with-eval-after-load 'youtube-dl
    (setq youtube-dl-directory "~/annex/Video/youtube"))

  (elfeed-org)
  (setq url-queue-timeout 30
        elfeed-db-directory (expand-file-name "elfeed" (concat (getenv "HOME") "/annex/var"))))
        ;; create timer to update elfeed
        ;; elfeed-update-timer (run-at-time 180 (* 120 60) 'forge/elfeed-update)))

Elfeed feed organizer with elfeed-org

Normally, one would configure the feeds to subscribe to with the variable elfeed-feeds. This package enables one to use an Org mode file to manage one’s list of feeds. I find this approach preferable.

(use-package elfeed-org
  :after (:all org elfeed)
  :commands (elfeed-org)
  :config
  (setq rmh-elfeed-org-files (list (expand-file-name "elfeed.org" org-directory)))
  (elfeed-org))

Cleanup

(provide 'init-elfeed)

EShell Terminal

I predominantly use eshell for terminal like needs. There’s always ansi-term if for some reason eshell isn’t a good fit, but I find I rarely use it nowadays. If I need a real terminal, I will probably use a proper terminal application.

I also, on occasion, have a need to use serial console access. The way to access this is via serial-term. If using something like ansi-term or serial-term, keep in mind the following keybindings. You probably normally want to be in terminal character mode.

  • C-c C-j / term-line-mode Terminal line mode.
  • C-c C-k / term-char-mode Terminal character mode.

Some notes:

  • One of the helpers here is eshell-here, which will open an eshell buffer in the directory where the current file is located.
  • When closing an eshell buffer, it will delete the window if it is not the only one in the frame.

Resources:

(require 'init-term)
;;; init-term.el --- Init Shell -*- lexical-binding: t -*-
<<license>>


(with-eval-after-load 'em-unix
  (unintern 'eshell/su nil)
  (unintern 'eshell/sudo nil))

(defun eshell-here ()
  "Opens up a new shell in the directory associated with the current buffer's file."
  (interactive)
  (let* ((parent (if (buffer-file-name) (file-name-directory (buffer-file-name)) (getenv "HOME")))
         (height (/ (window-total-height) 3))
         (name (car (last (split-string parent "/" t)))))
    (split-window-vertically (- height))
    (other-window 1)
    (eshell "new")
    (rename-buffer (concat "*eshell: " name "*"))
    (insert (concat "ls"))
    (eshell-send-input)))

(define-key global-map (kbd "C-!") 'eshell-here)

(with-eval-after-load 'eshell

  (defun my-tramp-loaded-p ()
    "Return t if tramp is loaded and nil otherwise."
    (fboundp 'tramp-tramp-file-p))

  (defun my-eshell-tramp-path-p ()
    "Return t if path has tramp file syntax."
    (when (my-tramp-loaded-p)
      (tramp-tramp-file-p default-directory)))

  (defun my-eshell-prompt-user ()
    "Return username on current system for use in eshell prompt."
    (if (my-eshell-tramp-path-p)
        (or (tramp-file-name-user (tramp-dissect-file-name default-directory)) (getenv "USER"))
      (user-login-name)))

  (defun my-eshell-prompt-host ()
    "Return hostname of current system for use in eshell prompt."
    (if (my-eshell-tramp-path-p)
        (tramp-file-name-host (tramp-dissect-file-name default-directory))
      (system-name)))

  (defun my-eshell-prompt-pwd ()
    "Return PWD on current system for use in eshell prompt."
    (abbreviate-file-name
     (if (my-eshell-tramp-path-p)
         (nth 6 (tramp-dissect-file-name default-directory))
       (eshell/pwd))))

  (defun my-eshell-default-prompt ()
    "Generate prompt string for eshell.  Use for `eshell-prompt-function'."
    (let ((user (my-eshell-prompt-user))
          (host (my-eshell-prompt-host))
          (now (format-time-string "%b %d %H:%M" (current-time)))
          (pwd (my-eshell-prompt-pwd)))
      (concat
       "┌─[" user "" host " " pwd "]─[" now "]\n"
       "└─>"
       (propertize " λ" 'face (if (zerop eshell-last-command-status) 'success 'error))
       " ")))

  (setenv "TERM" "xterm-256color")
  (setq explicit-shell-file-name "/bin/bash") ;; this is from term.el
  (advice-add 'eshell-life-is-too-much :after 'forge/delete-window)
  (setq tramp-default-method "ssh"
        eshell-directory-name (expand-file-name "eshell" forge-state-dir)
        eshell-visual-commands '("less" "tmux" "htop" "top" "docker" "nethack")
        eshell-visual-subcommands '(("git" "log" "diff" "show"))
        eshell-prompt-regexp "^[^#\nλ]*[#$λ] "
        eshell-prompt-function #'my-eshell-default-prompt)
  (add-hook 'eshell-mode-hook (lambda ()
                                (eshell/alias "q" "exit")
                                (eshell/alias "l" "ls -al")
                                (eshell/alias "ll" "ls -al")
                                (eshell/alias "e" "find-file \$1")
                                (eshell/alias "ff" "find-file \$1")
                                (eshell/alias "vi" "find-file \$1")
                                (eshell/alias "d" "dired \$1")
                                (eshell/alias "ee" "find-file-other-window \$1")
                                (eshell/alias "gd" "magit-diff-unstaged")
                                (eshell/alias "gds" "magit-diff-staged")
                                (eshell/alias "gst" "magit-status"))))

The following is a relic from when I used ansi-term. Switch to an ansi-term buffer if it exists; otherwise, create one and switch to it.

(defun my-terminal ()
  "Switch to terminal; launch if non-existent."
  (interactive)
  (if (get-buffer "*ansi-term*")
    (switch-to-buffer "*ansi-term*")
    (ansi-term "/bin/bash"))
  (get-buffer-process "*ansi-term*"))
(provide 'init-term)

Music

Mingus

I don’t use MPD much anymore. I used to use Mingus as an Emacs interface to MPD.

(use-package mingus
  :disabled t
  :preface
  (defun forge/get-current-song-mpd ()
    "Get the current song playing via MPD."
    (interactive)
    (let ((conn (mpd-conn-new "localhost" 6600))
          (cursong nil))
      (condition-case nil
          (setq cursong (split-string (plist-get (mpd-get-current-song conn) 'Title) " - "))
        (error nil))
      cursong)))

EMMS

EMMS is another Emacs interface to playing multimedia. I occasionally use this. The docs are here.

(use-package emms
  :disabled t
  :custom
  (emms-directory (expand-file-name "emms" forge-state-dir))
  (emms-source-file-default-directory (expand-file-name "~/annex/Audio"))
  :config
  (emms-all)
  (emms-history-load)
  (setq emms-player-list (list emms-player-mpv)
        emms-stream-info-backend 'mplayer
        emms-source-file-directory-tree-function 'emms-source-file-directory-tree-find
        emms-browser-covers 'emms-browser-cache-thumbnail)
  (add-to-list 'emms-player-mpv-parameters "--no-audio-display")
  (add-to-list 'emms-info-functions 'emms-info-cueinfo)
  (if (executable-find "emms-print-metadata")
      (progn
        (require 'emms-info-libtag)
        (add-to-list 'emms-info-functions 'emms-info-libtag)
        (delete 'emms-info-ogginfo emms-info-functions)
        (delete 'emms-info-mp3info emms-info-functions))
    (add-to-list 'emms-info-functions 'emms-info-ogginfo)
    (add-to-list 'emms-info-functions 'emms-info-mp3info)))

Miscellaneous Utilities

The following are miscellaneous utilities that are small but useful.
(require 'init-utils)

Epub reader with Nov

;;; init-utils.el --- Init various utilities -*- lexical-binding: t -*-
<<license>>


(use-package nov
  :mode ("\\.epub\\'" . nov-mode)
  :init
  (setq nov-save-place-file (expand-file-name "nov-places" forge-state-dir)))
(use-package lorem-ipsum)

Network related, ping and traceroute

A place to configure miscellaneous settings for network utilities.


(use-package net-utils
  :commands (ping traceroute)
  :config
  (setq ping-program-options (list "-c" "5"))
  (setq traceroute-program-options (list "-I" "-m" "30" "-w" "1")))

Pull in ip-query for functions to query the IP to ASN Mapping Service provided by Team Cymru.

(use-package ip-query
  :ensure nil
  :init (init-vc-install :fetcher "github" :repo "sfromm/ip-query")
  :commands (ip-query ip-query-asn))

dig-extended is meant to be a wrapper around dig, using the Emacs advice facility.

(defun dig-extended (fn &optional
                        domain query-type query-class query-option dig-option server)
  "Wrapper for `dig'.
Query for DNS records for DOMAIN of QUERY-TYPE."
  (message "domain: '%s'" domain)
  (unless domain
    (setq domain (read-string "Host: ")))
  (unless query-type
    (setq query-type (completing-read "Type: " '("A" "SOA" "NS" "TXT" "CNAME" "PTR"))))
  (funcall fn domain query-type query-class query-option dig-option server))

(advice-add 'dig :around #'dig-extended)

Search / Ripgrep

rg.el is a frontend to ripgrep.


(use-package rg)
(use-package gist
  :custom (gist-view-gist t))

Mastodon

https://codeberg.org/martianh/mastodon.el

The following variables are set with customize:

  • mastodon-instance-url
  • mastodon-active-user

(use-package mastodon
  :custom
  (mastodon-client--token-file (expand-file-name "mastodon/mastodon.plstore" forge-state-dir))
  :config
  (mastodon-discover))

Keybindings of note:

KeybindingCommand
nGo to next item
pGo to previous item
g, uUpdate current timeline
M-nGo to next “interesting” actionable thing
M-pGo to previous “interesting” actionable thing
FOpen federated timeline
HOpen home timeline
LOpen local timeline
NOpen notifications timeline
TOpen thread for toot under point
AOpen author profile under point
POpen profile of user under point

Weather with Wttrin

(use-package wttrin
  :ensure nil
  :init (init-vc-install :fetcher "github" :repo "sfromm/emacs-wttrin")
  :commands (wttrin)
  :custom
  (wttrin-default-cities '("Eugene" "Portland" "Sonoma" "Kapolei" "New Orleans"))
  (wttrin-language "en-US"))

World Clock

Because sometimes I work with people in more than one timezone. M-x world-clock to the rescue.

(setq zoneinfo-style-world-list ; M-x shell RET timedatectl list-timezones or M-x dired RET /usr/share/zoneinfo
      '(("America/Los_Angeles" "Los Angeles")
        ("America/Denver" "Denver")
        ("America/Chicago" "Chicago")
        ("America/New_York" "New York")
        ("Canada/Atlantic" "Canada/Atlantic")
        ("UTC" "UTC")
        ("Europe/London" "London")
        ("Europe/Lisbon" "Lisbon")
        ("Europe/Brussels" "Barcelona • Paris • Brussels • Berlin")
        ("Europe/Athens" "Athens • Cairo • Kyiv")
        ("Asia/Tel_Aviv" "Tel Aviv")
        ("Asia/Kolkata" "Kolkata")
        ("Asia/Shanghai" "Beijing • Shanghai")
        ("Asia/Seoul" "Seoul")
        ("Asia/Tokyo" "Tokyo")
        ("Asia/Vladivostok" "Vladivostok")
        ("Australia/Brisbane" "Brisbane")
        ("Australia/Sydney" "Sydney")
        ("Pacific/Auckland" "Auckland")
        ("Pacific/Honolulu" "Honolulu")))
(setq world-clock-list t
      world-clock-buffer-name "*world-clock*"
      world-clock-time-format "%R %Z (%z)  %A %d %B")

VPN helpers

Helpers to connect and disconnect from a VPN session.


(defvar forge/vpn-config ""
  "Name of the OpenVPN VPN configuration to use.")

(when (forge/system-type-darwin-p)
  (defun vpn-connect ()
    "Connect to VPN configuration CFG.
Assumes you are are on MacOS and using Wireguard to connect."
    (interactive)
    (require 'em-glob)
    (let ((cfg (completing-read "Config: "
                                (mapcar #'file-name-sans-extension
                                        (directory-files "~/annex/etc" nil (eshell-glob-regexp "wg*conf"))))))
      (setq forge/vpn-config cfg)
      (when (forge/system-type-darwin-p)
        (shell-command (concat "scutil --nc start " cfg)))))

  (defun vpn-disconnect ()
    "Disconnect VPN configuration CFG.
Assumes you are are on MacOS and using Wireguard to connect."
    (interactive)
    (when (forge/system-type-darwin-p)
      (shell-command (concat "scutil --nc stop " forge/vpn-config))))

  (defun openvpn-connect ()
    "Connect to OpenVPN configuration CFG.
Assumes you are on MacOS and using Tunnelblick to connect."
    (interactive)
    (require 'em-glob)
    (let ((cfg (completing-read "Config: "
                                (mapcar #'file-name-sans-extension
                                        (directory-files "~/annex/etc" nil (eshell-glob-regexp "*ovpn"))))))
      (setq forge/vpn-config cfg)
      (when (forge/system-type-darwin-p)
        (let ((osatmpl ""))
          (setq osatmpl (concat "tell application \"/Applications/Tunnelblick.app\"\n"
                                "    connect \"" cfg "\"\n"
                                "end tell"))
          (do-applescript osatmpl)))))

  (defun openvpn-disconnect ()
    "Disconnect from VPN.
Assumes you are on MacOS and using Tunnelblick to manage your VPN."
    (interactive)
    (let ((osatmpl ""))
      (setq osatmpl (concat "tell application \"/Applications/Tunnelblick.app\"\n"
                            "    disconnect \"" forge/vpn-config "\"\n"
                            "end tell"))
      (do-applescript osatmpl))))

Home Makefile helpers

(defmacro forge-mkhome-target (target)
  "Macro to run mkhome makefile TARGET."
  `(with-temp-buffer
     (progn
       (cd (getenv "HOME"))
       (compile (mapconcat 'shell-quote-argument (list "make" "-f" "Makefile.mkhome" ,target) " ")))))

(defun forge-mkhome-update ()
  "Run mkhome git."
  (interactive)
  (forge-mkhome-target "update"))

(defun forge-mkhome-www ()
  "Run mkhome www."
  (interactive)
  (forge-mkhome-target "www"))

(defun forge-mkhome-src ()
  "Run mkhome src."
  (interactive)
  (forge-mkhome-target "src"))

Get currently playing song

Lastly, this returns the currently playing song in iTunes.

Useful resources:


(when (forge/system-type-darwin-p)
  (defun my-get-current-song-itunes ()
    "Get current song playing via itunes."
    (let ((osa-tmpl "")
          (cursong nil))
      (setq osa-tmpl "tell application \"Music\"
	if player state is not stopped then
		set ct to (properties of current track)
		set this_song to \"\"
		if (class of ct is URL track) and (get current stream title) is not missing value then
			set this_song to (get current stream title)
		else
			set this_song to artist in ct & \" - \" & name in ct
		end if
		this_song
	end if
end tell")
      (condition-case nil
          (setq cursong (split-string (do-applescript osa-tmpl) " - "))
        (error nil))
      cursong)))

Bookmark opener

Inspired and borrowed from Alvaro Ramirez:

(defun my-org-web-bookmarks (path)
  "Return all HTTP links from an org-file at PATH."
  (with-temp-buffer
    (let (links)
      (insert-file-contents path)
      (org-mode)
      (org-element-map (org-element-parse-buffer) 'link
        (lambda (link)
          (let* ((raw-link (org-element-property :raw-link link))
                 (content (org-element-contents link))
                 (title (substring-no-properties (or (seq-first content) raw-link))))
            (when (string-prefix-p "http" raw-link)
              (push (concat title "\n" (propertize raw-link 'face 'whitespace-space) "\n")
                    links))))
        nil nil 'link)
      (seq-sort 'string-greaterp links))))

(defun my-open-browser-bookmark ()
  "Send a bookmark to the browser from the bookmark file."
  (interactive)
  (browse-url
   (seq-elt
    (split-string
     (completing-read "Open: " (my-org-web-bookmarks (expand-file-name "notebook.org" org-directory))) "\n") 1)))

Helper functions

Feature related helpers


;; What follows are various helper functions that are used
;; either interactively or in other parts of the configuration.

(defun my-reload-emacs-configuration ()
  "Reload emacs configuration."
  (interactive)
  (load-file (expand-file-name "init.el" user-emacs-directory)))

(defun forge/turn-off-delete-trailing-whitespace ()
  "Turn off `delete-trailing-whitespace' when saving files."
  (remove-hook 'before-save-hook 'delete-trailing-whitespace t))

;; Via jwiegley
;; https://github.com/jwiegley/dot-emacs/blob/master/init.el
(defun lookup-password (host user port)
  "Look up password for HOST, USER, and PORT."
  (require 'auth-source)
  (require 'auth-source-pass)
  (let ((auth (auth-source-search :host host :user user :port port)))
    (if auth
        (let ((secretf (plist-get (car auth) :secret)))
          (if secretf
              (funcall secretf)
            (error "Auth entry for %s@%s:%s has no secret!"
                   user host port)))
      (error "No auth entry found for %s@%s:%s" user host port))))

;; Via https://emacs.stackexchange.com/questions/8104/is-there-a-mode-to-automatically-update-copyright-years-in-files
(defun forge/enable-copyright-update ()
  "Update copyright year when saving a file."
  (when (fboundp 'copyright-update)
    (setq copyright-names-regexp "Free Software")
    (add-hook 'before-save-hook #'copyright-update)))

;; Delete window if not the only one.
(defun forge/delete-window ()
  "Delete window if it is not the only one."
  (when (not (one-window-p))
    (delete-window)))

(defun forge/transparency (value)
  "Set the transparency of the frame window with VALUE 0=transparent/100=opaque."
  (interactive "nTransparency Value 0 - 100 opaque:")
  (set-frame-parameter (selected-frame) 'alpha value))

(defun forge/untabify-buffer ()
  "Remove tab characters from buffer."
  (interactive)
  (untabify (point-min) (point-max)))

Opening files helpers

Heavily borrowed from … somewhere.

(defun sudo-file-path (file)
  "Return path for FILE with sudo access."
  (let ((host (or (file-remote-p file 'host) "localhost")))
    (concat "/" (when (file-remote-p file)
                  (concat (file-remote-p file 'method) ":"
                          (if-let (user (file-remote-p file 'user))
                              (concat user "@" host) host)
                          "|"))
            "sudo:root@" host
            ":" (or (file-remote-p file 'localname)
                    file))))

(defun sudo-find-file (file)
  "Open FILE as root."
  (interactive "FOpen file as root: ")
  (find-file (sudo-file-path file)))

(defun sudo-this-file ()
  "Open current file as root."
  (interactive)
  (find-file
   (sudo-file-path
    (or buffer-file-name
        (or buffer-file-name
            (when (or (derived-mode-p 'dired-mode)
                      (derived-mode-p 'wdired-mode))
              default-directory))))))

Miscellaneous functions

The following functions are not so much tied to features. They are just various helpers.

  • Remember c=f𝛌
  • The next are helpers around converting wavelength to frequency and vice-versa, mostly in the context of DWDM.
(defconst speed_of_light 299792458 "Speed of light, m/s.")

(defun wavelength-to-frequency (wavelength)
  "Convert a wavelength to frequency."
  (interactive "nWavelength: ")
  (message "Frequency: %0.2f" (/ (/ speed_of_light wavelength) 1000)))

(defun frequency-to-wavelength (frequency)
  "Convert a frequency to wavelength (nm)."
  (interactive "nFrequency: ")
  (message "Wavelength: %0.4f" (/ (/ speed_of_light frequency) 1000)))

(defun forge/date-today ()
  "Insert friendly date string for today."
  (interactive)
  (insert (format-time-string "%B %d, %Y" (current-time))))

with-editor

Try to make it easier to use emacsclient as the $EDITOR of child processes using with-editor.

(use-package with-editor)

(provide 'init-utils)

Cleanup

The goal here is to load any personal lisp files in forge-personal-dir path, and then finally to report how long it took to load the configuration.

(forge/load-directory-modules forge-personal-dir)

Resources

Emacs Calc