Updated: {{{export-date}}}
- Git repository
- https://github.com/sfromm/emacs.d
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))
;; 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/>.
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.
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
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.
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.
- Sacha Chua’s emacs configuration
- John Wiegley
- Protesilaos Stavrou: dotemacs
- Bernt Hansen (norang.ca) Org Mode (Organize Your Life in Plain Text)
- Emacs Starter Kit
- Grant Rettke: lolsmacs, and previously ALEC
- John Kitchin: emacs configuration
- Howard Abrams: Emacs and dot files
- Bastien Guerry
- Lars Tveito
- Steve Purcell
- Making Emacs work for me
- Julien Danmmjou
- Joe Di Castro
- Ryan Rix: Complete Computing Environment (formerly Hardcore Freestyle Emacs org source)
- Abelardo Jara-Berrocal
- Lee Hinman’s Emacs Operating System (see https://github.com/dakrone/eos)
- Karl Voit’s dot-emacs
- Damien Cassou
- Henrik Lissner Doom Emacs
- Ambrevar
- DJCB
- Luca Cambiaghi
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)
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)
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)
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))
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 withuse-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 letuse-package
handle it. See discussion at end of section on :after keyword. - Pull in
org
andorg-contrib
as early as possible to make sureload-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
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))))
(provide 'init-elpa)
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 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>>
(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)))
Create a customization group for variables specific to this configuration.
(defgroup forge nil
"Forge custom settings."
:group 'environment)
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))
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:
- https://www.wisdomandwonder.com/wp-content/uploads/2014/03/C3F.html
- https://stackoverflow.com/questions/1229142/how-can-i-save-my-mini-buffer-history-in-emacs
- https://www.gnu.org/software/emacs/manual/html_node/emacs/Saving-Emacs-Sessions.html
(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))
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
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))))))
Before going to much further, go ahead and load any site configuration.
(forge/load-directory-modules forge-site-dir)
The following are a couple elements that are only loaded if on MacOS or on Linux.
;;; 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))))
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))
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)))
(provide 'init-core)
(require 'init-appearance)
;;; init-appearance.el --- Init appearance pieces -*- lexical-binding: t -*-
<<license>>
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:
(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.")
(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 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 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)
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";
}
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)
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)
(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 - 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)))
(provide 'init-appearance)
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>>
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)
(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))
)
;;; init-ui-completion.el --- Init UI Completion elements -*- lexical-binding: t -*-
;; UI Completion elements
<<license>>
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))
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))
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))
(provide 'init-ui-completion)
;;; init-navigation.el --- Init navigation elements -*- lexical-binding: t -*-
;; Navigation elements
<<license>>
Emacs view-mode.
Key | Command | Description |
---|---|---|
q | view-quit | Disable view mode and switch back to previous buffer |
e | view-exit | Disable 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))))
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)))
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 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))
(provide 'init-navigation)
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.
Key | Command | Description |
---|---|---|
C-x t b | switch-to-buffer-other-tab | Open a buffer in a new tab |
C-x t d | dired-other-tab | Open a directory in a new tab |
C-x t f | find-file-other-tab | Open a file in a new tab |
C-x t 0 | close-tab | Close current tab |
C-x t 1 | close-tab-other | Close all other tabs |
C-x t 2 | tab-new | Open 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)
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 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))
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)))
(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)
(provide 'init-ui)
;;; 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>>
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)
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)))
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:
- https://github.com/purcell/page-break-lines
- http://endlessparentheses.com/improving-page-navigation.html
- https://ericjmritz.wordpress.com/2015/08/29/using-page-breaks-in-gnu-emacs/
(use-package page-break-lines
:diminish page-break-lines-mode
:hook
(emacs-lisp-mode . page-break-lines-mode))
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 to quickly expand the selected region. Use C-=
to do so.
(use-package expand-region
:bind ("C-=" . er/expand-region))
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))
;; 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-indent-guides provides a handy visual cue for indentation.
(use-package highlight-indent-guides
:custom (highlight-indent-guides-method 'character))
(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 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))
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)))
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 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))))
(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)))
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))
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))
(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 (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:
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\\'")
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))
(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)))
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))
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))
ESS is a package that supports various statistical analysis programs.
(use-package ess)
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))
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.
Command | Description |
---|---|
C-c C-c | runs the query under the cursor, tries to pretty-print the response (if possible) |
C-c C-r | same, but doesn’t do anything with the response, just shows the buffer |
C-c C-v | same as `C-c C-c`, but doesn’t switch focus to other window |
C-c C-p | jump to the previous query |
C-c C-n | jump to the next query |
C-c C-. | mark the query under the cursor |
C-c C-u | copy query under the cursor as a curl command |
(use-package restclient
:mode ("\\.http\\'" . restclient-mode))
graphql is a mode to edit GraphQL schema and queries.
(use-package graphql-mode
:mode ("\\.graphql\\'" . graphql-mode))
This is really predominantly for syntax highlighting.
(use-package php-mode
:mode "\\.php\\'")
(use-package csv-mode)
JSON is one data-serialization format.
(use-package json-mode)
YAML is another data-serialization format.
(use-package yaml-mode
:config
(setq yaml-indent-offset 2))
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)))
(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))
(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))
(use-package yang-mode)
(use-package nftables-mode)
(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 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))
(provide 'init-editing-lang)
(provide 'init-editing)
(require 'init-chat)
Pull in notifications
and tls
for all the pieces below.
(require 'notifications)
(require 'tls)
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)
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:
- https://github.com/yuya373/emacs-slack#how-to-get-token
- http://endlessparentheses.com/keep-your-slack-distractions-under-control-with-emacs.html?source=rss
(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))
(provide 'init-chat)
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"))
(with-eval-after-load 'dired-aux
(add-to-list 'dired-compress-file-suffixes '("\\.zip\\'" ".zip" "unzip")))
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-usage is a handy utility to explore disk utilization. On MacOS, this will require GNU coreutils.
(use-package disk-usage)
(provide 'init-dired)
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)
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:
- Fetching email
- Composing & sending email
- Reading and organizing email
(require 'init-email)
;;; init-email.el --- Init email management -*- lexical-binding: t -*-
<<license>>
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.
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:
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
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).
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)))
Sometimes it is handy to quote text when drafting an email and boxquote does this well.
(use-package boxquote)
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)))
(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)
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)))
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"))
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:
- Run the pre hook.
- Notmuch then indexes your email.
- 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
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))
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)))))
(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))))
(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)
(provide 'init-email)
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 withC-c C-x C-c
. In column view,g
will refresh andq
will exit. You can also optionally also define different columns in an outline’sPROPERTIES
.
: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
, useC-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))))
(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:")))
(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))
See: https://github.com/yjwen/org-reveal/
(use-package ox-reveal)
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))
(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)))
- Org Export to Twitter Bootstrap
- Uses htmlize to fontify some sections.
- Org Export to Tufte
- You will need the CSS and fonts from tufte-css.
- Org Export to Reveal JS
;;(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)
(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)))
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"))
(provide 'init-org)
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))
(provide 'init-elfeed)
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 aneshell
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:
- Eshell manual
- Mastering Emacs: Mastering Eshell by Mickey Petersen
- Eschewing Zshell for Emacs Shell by Howard Abrams
(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)
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 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)))
(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)
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)
rg.el is a frontend to ripgrep.
(use-package rg)
(use-package gist
:custom (gist-view-gist t))
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:
Keybinding | Command |
---|---|
n | Go to next item |
p | Go to previous item |
g, u | Update current timeline |
M-n | Go to next “interesting” actionable thing |
M-p | Go to previous “interesting” actionable thing |
F | Open federated timeline |
H | Open home timeline |
L | Open local timeline |
N | Open notifications timeline |
T | Open thread for toot under point |
A | Open author profile under point |
P | Open 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"))
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")
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))))
(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"))
Lastly, this returns the currently playing song in iTunes.
Useful resources:
- https://apple.stackexchange.com/questions/297240/getting-the-file-path-of-a-currently-playing-itunes-track-with-applescript
- https://alvinalexander.com/blog/post/mac-os-x/applescript-concatenate-strings
(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)))
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)))
;; 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)))
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))))))
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))))
Try to make it easier to use emacsclient
as the $EDITOR
of child processes using with-editor.
(use-package with-editor)
(provide 'init-utils)
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)