hamacs/ha-config.org
Howard Abrams add010fd44 Label the g commands for which-key
Cleaned up the commands that follow on the g key with labels, and I
got rid of commands that I don't think is useful.

This required changes through the repo, but things are looking good.

I also changed the behavior of `e` and `E`, and `w` and `W` based on
how I think that should be.
2023-06-15 15:48:49 -07:00

116 KiB
Raw Blame History

General Emacs Configuration

A literate programming file for configuring Emacs.

Basic Configuration

I begin with configuration of Emacs that isnt package-specific. For instance, I hate a fat-finger that stop Emacs:

  (setq confirm-kill-emacs 'yes-or-no-p)

New way to display line-numbers. I set mine to relative so that I can jump up and down by that value. Set this to nil to turn off, or t to be absolute.

  (setq display-line-numbers t
        display-line-numbers-type 'relative)

I like the rendering to curved quotes using text-quoting-style, because it improves the readability of documentation strings in the Help buffer and whatnot.

  (setq text-quoting-style 'curve)

Changes and settings I like introduced in Emacs 28:

  (setq use-short-answers t
        describe-bindings-outline t
        completions-detailed t)

In Emacs version 28, we can hide commands in M-x which do not apply to the current mode.

  (setq read-extended-command-predicate
        #'command-completion-default-include-p)

As tec wrote, I want to use ~/.authsource.gpg as I dont want to accidentaly purge this file cleaning ~/.emacs.d, and let's cache as much as possible, as my home machine is pretty safe, and my laptop is shutdown a lot. Also, as bytedude mentions, I need to se the epa-pineentry-mode to loopback to actually get a prompt for the password, instead of an error.

  (use-package epa-file
    :config
    (defvar epa-pinentry-mode)
    (setq epa-file-select-keys nil
          epa-pinentry-mode 'loopback
          auth-sources '("~/.authinfo.gpg")
          auth-source-cache-expiry nil))

Unicode ellispis are nicer than three dots:

  (setq truncate-string-ellipsis "…")

More settings:

When I get an error, I need a stack trace to figure out the problem. Yeah, when I stop fiddling with Emacs, this should go off:

  (setq debug-on-error t)

And some Mac-specific settings:

  (when (ha-running-on-macos?)
    (setq mac-option-modifier 'meta
          mac-command-modifier 'super)
    (add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))
    (add-to-list 'default-frame-alist '(ns-appearance . dark)))

Support Packages

Configuration Changes

Initial Settings and UI

Let's turn off the menu and other settings:

  (when (display-graphic-p)
    (tool-bar-mode -1)
    (scroll-bar-mode -1)
    (horizontal-scroll-bar-mode -1)
    (setq visible-bell 1
          frame-inhibit-implied-resize t))

I like being able to enable local variables in .dir-local.el files:

  (setq enable-local-variables t)

File Access

Remote Files

To speed up TRAMP access, lets disabled lock files, you know, the ones that have the # surrounding characters:

  (setq remote-file-name-inhibit-locks t)

What do I think about remote-file-name-inhibit-auto-save-visited?

During remote access, TRAMP can slow down performing Git operations. Lets turn that off as well:

  (defun turn-off-vc-for-remote-files ()
    "Disable"
    (when (file-remote-p (buffer-file-name))
      (setq-local vc-handled-backends nil)))

  (add-hook 'find-file-hook 'turn-off-vc-for-remote-files)

Changes on Save

Always spaces and never tabs. Note that we use setq-default since indent-tabs-mode is a buffer-local variable, meaning using setq, sets it for that buffer file. We want this globally the default:

  (setq-default indent-tabs-mode nil)

When I push changes to my files to Gerrit and other code review, I dont want trailing spaces or any tabs to appear, so lets fix all files when I save them:

  (defun ha-cleanup-buffer-file ()
    "Cleanup a file, often done before a file save."
    (interactive)
    (ignore-errors
      (unless (or (equal major-mode 'makefile-mode)
                  (equal major-mode 'makefile-bsdmake-mode))
        (untabify (point-min) (point-max)))
      (delete-trailing-whitespace)))

  (add-hook 'before-save-hook #'ha-cleanup-buffer-file)

Recent Files

The recentf feature has been in Emacs for a long time, but it has a problem with Tramp, as we need to turn off the cleanup feature that attempts to stat all the files and remove them from the recent accessed list if they are readable. The requires recentf to open up a remote files which blocks Emacs at the most inopportune times… like when trying to reboot the machine.

  (use-package recentf
    :straight (:type built-in)
    :config
    (setq recentf-auto-cleanup 'never) ;; disable before we start recentf!
    (recentf-mode 1))

File Backups

While I use git as much as I can, sometimes Emacs built-in file backup and versioning feature has saved me for files that arent.

As Phil Jackson mentioned, Emacs has a lot of variations to its file backup strategy, and either change the backup-directory-alist to put individual file backups elsewhere, e.g.

  (setq backup-directory-alist `(("." . ,(concat user-emacs-directory "backups"))))

Or leave them in the current directory, but create an alias so ls doesnt display them, e.g.

  alias ls="ls --color=auto --hide='*~'"

I'm leaving them side-by-side, but I am keeping some extra copies:

  (setq create-lockfiles nil   ; Having .# files around ain't helpful
        auto-save-default t
        delete-old-versions t
        kept-new-versions 6
        kept-old-versions 2
        version-control t)

The version-control variable affect backups (not some sort of global VC setting), this makes numeric backups.

Auto Save of Files

Save the file whenever I move away from Emacs (see this essay):

  (defun save-all-buffers ()
    "Saves all buffers, because, why not?"
    (interactive)
    (save-some-buffers t))

  (add-hook 'focus-out-hook 'save-all-buffers)

Completing Read User Interface

After using Ivy, I am going the route of a completing-read interface that extends the original Emacs API, as opposed to implementing backend-engines or complete replacements.

Vertico

The vertico package puts the completing read in a vertical format, and like Selectrum, it extends Emacs built-in functionality, instead of adding a new process. This means all these projects work together.

  (use-package vertico
    :config (vertico-mode))

My issue with Vertico is when calling find-file, the Return key opens dired, instead of inserting the directory at point. This package addresses this:

  (use-package vertico-directory
    :straight (el-patch :files ("~/.emacs.d/straight/repos/vertico/extensions/vertico-directory.el"))
    ;; More convenient directory navigation commands
    :bind (:map vertico-map
                ("RET" . vertico-directory-enter)
                ; ("DEL" . vertico-directory-delete-word)
                ("M-RET" . minibuffer-force-complete-and-exit)
                ("M-TAB" . minibuffer-complete))
    ;; Tidy shadowed file names
    :hook (rfn-eshadow-update-overlay . vertico-directory-tidy))

Hotfuzz

This fuzzy completion style is like the built-in flex style, but has a better scoring algorithm, non-greedy and ranks completions that match at word; path component; or camelCase boundaries higher.

  (use-package hotfuzz)

While flexible at matching, you have to get the order correct. For instance, alireg matches with align-regexp, but regali does not, so we will use hotfuzz for scoring, and not use this as a completion-project (see the fussy project below).

Orderless

While the space can be use to separate words (acting a bit like a .* regular expression), the orderless project allows those words to be in any order.

  (use-package orderless
    :commands (orderless-filter)
    :custom
    (completion-styles '(orderless basic))
    (completion-ignore-case t)
    (completion-category-defaults nil)
    (completion-category-overrides '((file (styles partial-completion)))))

Note: Open more than one file at once with find-file with a wildcard. We may also give the initials completion style a try.

Fussy Filtering and Matching

The fussy project is a fuzzy pattern matching extension for the normal completing-read interface. By default, it uses flx, but we can specify other sorting and filtering algorithms.

How does it compare? Once upon a time, I enjoyed typing plp for package-list-packages, and when I switched to orderless, I would need to put a space between the words. While I will continue to play with the different mechanism, Ill combine hotfuzz and orderless.

  (use-package fussy
    :straight (:host github :repo "jojojames/fussy")
    :config
    (push 'fussy completion-styles)
    (setq completion-category-defaults nil
          completion-category-overrides nil
          fussy-filter-fn 'fussy-filter-orderless-flex
          fussy-score-fn 'fussy-hotfuzz-score))

Savehist

Persist history over Emacs restarts using the built-in savehist project. Since both Vertico and Selectrum sorts by history position, this should make the choice smarter with time.

  (use-package savehist
    :init
    (savehist-mode))

Marginalia

The marginalia package gives a preview of M-x functions with a one line description, extra information when selecting files, etc. Nice enhancement without learning any new keybindings.

  ;; Enable richer annotations using the Marginalia package
  (use-package marginalia
    :init
    (setq marginalia-annotators-heavy t)
    :config
    (marginalia-mode))

Key Bindings

To begin my binding changes, let's turn on which-key:

  (use-package which-key
    :init   (setq which-key-popup-type 'minibuffer)
    :config (which-key-mode))

Why would I ever quit Emacs with a simple keybinding? Lets override it:

  (global-set-key (kbd "s-q") 'bury-buffer)

Undo

I mean, I always use C-/ for undo (and C-? for redo), but when Im on the Mac, I need to cover my bases.

Why use undo-fu instead of the built-in undo functionality? Well, there isnt much to the project (thats a good thing), but It basically doesnt cycle around the redo, which annoying.

  (use-package undo-fu
    :config
    (global-set-key [remap undo] 'undo-fu-only-undo)
    (global-set-key [remap undo-redo] 'undo-fu-only-redo)
    (global-unset-key (kbd "s-z"))
    (global-set-key (kbd "s-z")   'undo-fu-only-undo)
    (global-set-key (kbd "s-S-z") 'undo-fu-only-redo))

Evil-Specific Keybindings

Can we change Evil at this point? Some tips:

  (use-package evil
    :init
    (setq evil-undo-system 'undo-fu
          evil-auto-indent t
          evil-respect-visual-line-mode t
          evil-want-fine-undo t         ; Be more like Emacs
          evil-disable-insert-state-bindings t
          evil-want-keybinding nil
          evil-want-integration t
          evil-want-C-u-scroll nil
          evil-want-C-i-jump nil
          evil-escape-key-sequence "jk"
          evil-escape-unordered-key-sequence t))

The Escape key act like C-g and always go back to normal mode?

  (use-package evil
    :config
    (global-set-key (kbd "<escape>") 'keyboard-escape-quit)
    (evil-mode))

Even with the /git/howard/hamacs/src/commit/add010fd445417e559719ada5e22e732d67f3ae7/Evil%20Collection, some modes should be Emacs:

  (use-package evil
    :config
    (dolist (mode '(custom-mode
                    eshell-mode
                    git-rebase-mode
                    erc-mode
                    circe-server-mode
                    circe-chat-mode
                    circe-query-mode
                    vterm-mode))
      (add-to-list 'evil-emacs-state-modes mode)))

Im not a long term VI user, and I generally like easy keys, e.g. w, have larger jumps, and harder keys, e.g. W (shifted), have smaller, fine-grained jumps. So I am switching these around:

  (use-package evil
    :config
    (require 'evil-commands)
    (evil-define-key '(normal visual motion operator) 'global
      "w" 'evil-forward-WORD-begin
      "W" 'evil-forward-word-begin
      "e" 'evil-forward-WORD-end
      "E" 'evil-forward-word-end)

    ;; The `b' key seems to need its own configuration setting:
    (evil-define-key '(normal visual motion operator) 'global
      "b" 'evil-backward-WORD-begin)
    (evil-define-key '(normal visual motion operator) 'global
      "B" 'evil-backward-word-begin)
    ;; Note that evil-backward-word-end is on the `g e':

    ;; Not a long-term VI user, so let's Emacsify some other keybindings:
    (evil-define-key '(normal visual motion operator) 'global
      (kbd "C-b") 'scroll-up-command
      (kbd "C-f") 'scroll-down-command
      (kbd "C-p") 'previous-line
      (kbd "C-n") 'next-line
      ;; I have better window control:
      (kbd "C-w") 'sp-kill-region))

Testing:

  • word-subword-subword
  • word_subword_subword

This clever hack from Manuel Uberti got me finding these useful bindings:

g ;
goto-last-change
g ,
goto-last-change-reverse

Keybindings I would like to use more:

*
jumps to the next instance of the word under point
#
jumps to the previous instance of the word under point

While Im pretty good with the VIM keybindings, I would like to play around with the text objects and how it compares to others (including the surround), for instance:

diw
deletes a word, but can be anywhere in it, while de deletes to the end of the word.
daw
deletes a word, plus the surrounding space, but not punctuation.
xis
changes a sentence, and if i is a, it gets rid of the surrounding whitespace as well. Probably das and cis.
xip
changes a paragraph.
xio
changes a symbol, which can change for each mode, but works with snake_case and other larger-than-word variables.
?

Surrounding punctuation, like quotes, parenthesis, brackets, etc. also work, so ci) changes all the parameters to a function call, for instance

a”
a double quoted string
i”
inner double quoted string
a'
a single quoted string
i'
inner single quoted string
a`
a back quoted string
i`
inner back quoted string

Note: The x in the above examples are d for delete, v for select, y for copying and c for changing.

What text objects are known?

w
word
s
sentence
p
paragraph
l
lines, with the Text Object Line package
o
symbol, like a variable
a string, surround by quotes, also ` for backticks
)
parenthesis, also } and ], see g
x
within a brace, paren, etc., with the my extensions below, see b and f for similar functionality.
f
a defun, or code block, see definition here.
i
indention area, for YAML and Python, with the evil-indent-plus package
t
an HTML tag
c
for comments
u
for URLs
a
function arguments (probably a lot like symbol, o) with the evil-args extension (that Im not bothering with)

Evil Text Object Line

Delete a line, d d is in basic VI. Since some commands use text objects, and the basic text object doesnt include lines, the evil-textobj-line project adds that:

  (use-package evil-textobj-line)

Now v i l and v a l works as youd expect, but does this improve on S-v?

Text Objects based on Indentation

The evil-indent-plus project creates text objects based on the indentation level, similar to how the b works with “blocks” of code.

  (use-package evil-indent-plus)

This can be handy for Python, YAML, and lists in org files. Note that i works for the current indent, but k includes one line above and j includes one line above and below.

Arguments as Text Objects

The evil-args projects creates text objects for symbols, but with trailing , or other syntax.

  (use-package evil-args
    :config
    ;; bind evil-args text objects
    (define-key evil-inner-text-objects-map "a" 'evil-inner-arg)
    (define-key evil-outer-text-objects-map "a" 'evil-outer-arg)

    ;; bind evil-forward/backward-args
    (define-key evil-normal-state-map "L" 'evil-forward-arg)
    (define-key evil-normal-state-map "H" 'evil-backward-arg)
    (define-key evil-motion-state-map "L" 'evil-forward-arg)
    (define-key evil-motion-state-map "H" 'evil-backward-arg)

    ;; bind evil-jump-out-args
    (define-key evil-normal-state-map "K" 'evil-jump-out-args))

For a function, like this Python example, with the cursor on b:

  def foobar(a, b, c):
    return a + b + c

Typing d a a will delete the argument leaving:

  def foobar(a, c):
    return a + b + c

Better Parenthesis with Text Object

I took the following clever idea and code from this essay from Chen Bin for creating a xig to grab code within any grouping characters, like parens, braces and brackets. For instance, dig cuts the content inside brackets, etc. First, we need a function to do the work (I changed the original from my- to ha- so that it is easier for me to distinguish functions from my configuration):

  (defun ha-evil-paren-range (count beg end type inclusive)
    "Get minimum range of paren text object.
  COUNT, BEG, END, TYPE is used.  If INCLUSIVE is t, the text object is inclusive."
    (let* ((parens '("()" "[]" "{}" "<>"))
           range
           found-range)
      (dolist (p parens)
        (condition-case nil
            (setq range (evil-select-paren (aref p 0) (aref p 1) beg end type count inclusive))
          (error nil))
        (when range
          (cond
           (found-range
            (when (< (- (nth 1 range) (nth 0 range))
                     (- (nth 1 found-range) (nth 0 found-range)))
              (setf (nth 0 found-range) (nth 0 range))
              (setf (nth 1 found-range) (nth 1 range))))
           (t
            (setq found-range range)))))
      found-range))

Extend the text object to call this function for both inner and outer:

  (evil-define-text-object ha-evil-a-paren (count &optional beg end type)
    "Select a paren."
    :extend-selection t
    (ha-evil-paren-range count beg end type t))

  (evil-define-text-object ha-evil-inner-paren (count &optional beg end type)
    "Select 'inner' paren."
    :extend-selection nil
    (ha-evil-paren-range count beg end type nil))

And the keybindings:

  (define-key evil-inner-text-objects-map "x" #'ha-evil-inner-paren)
  (define-key evil-outer-text-objects-map "x" #'ha-evil-a-paren)

Text Object for Functions

While Emacs has the ability to recognize functions, the Evil text object does not. But text objects have both an inner and outer form, and what does that mean for a function? The inner will be the function itself and the outer (like words) would be the surrounding non-function stuff … in other words, the distance between the next functions.

  (defun ha-evil-defun-range (count beg end type inclusive)
    "Get minimum range of `defun` as a text object.
  COUNT, is the number of _following_ defuns to count. BEG, END,
  TYPE are not used. If INCLUSIVE is t, the text object is
  inclusive acquiring the areas between the surrounding defuns."
    (let ((start (save-excursion
                   (beginning-of-defun)
                   (when inclusive
                     (beginning-of-defun)
                     (end-of-defun))
                   (point)))
          (end (save-excursion
                 (end-of-defun count)
                 (when inclusive
                   (end-of-defun)
                   (beginning-of-defun))
                 (point))))
      (list start end)))

Extend the text object to call this function for both inner and outer:

  (evil-define-text-object ha-evil-a-defun (count &optional beg end type)
    "Select a defun and surrounding non-defun content."
    :extend-selection t
    (ha-evil-defun-range count beg end type t))

  (evil-define-text-object ha-evil-inner-defun (count &optional beg end type)
    "Select 'inner' (actual) defun."
    :extend-selection nil
    (ha-evil-defun-range count beg end type nil))

And the keybindings:

  (define-key evil-inner-text-objects-map "d" #'ha-evil-inner-defun)
  (define-key evil-outer-text-objects-map "d" #'ha-evil-a-defun)

Why not use f? Im reserving the f for a tree-sitter version that is not always available for all modes… yet.

Key Chord

Using the key-chord project allows me to make Escape be on two key combo presses on both sides of my keyboard:

  (use-package key-chord
    :config
    (key-chord-mode t)
    (key-chord-define-global "fd" 'evil-normal-state)
    (key-chord-define-global "jk" 'evil-normal-state)
    (key-chord-define-global "JK" 'evil-normal-state))

General Leader Key Sequences

The one thing that both Spacemacs and Doom taught me, is how much I like the key sequences that begin with a leader key. In both of those systems, the key sequences begin in the normal state with a space key. This means, while typing in insert state, I have to escape to normal state and then hit the space.

I'm not trying an experiment where specially-placed function keys on my fancy ergodox keyboard can kick these off using General Leader project. Essentially, I want a set of leader keys for Evil's normal state as well as a global leader in all modes.

  (use-package general
    :custom
    (general-use-package-emit-autoloads t)

    :config
    (general-evil-setup t)

    (general-create-definer ha-leader
      :states '(normal visual motion)
      :keymaps 'override
      :prefix "SPC"
      :non-normal-prefix "M-SPC"
      :global-prefix "<f13>")

    (general-create-definer ha-local-leader
      :states '(normal visual motion)
      :prefix ","
      :global-prefix "<f17>"
      :non-normal-prefix "S-SPC"))

Relabel the G Keys

Cant remember all the shortcuts on the g key, and which-key displays the entire function, so lets re-add those keybindings, but with labels. The g is extemely convenient, yet I realize that I will never use some of the default keybindings (like g m to go to the middle of the line? Too imprecise). So I am also going to delete some of them.

  (use-package evil
    :general
    (:states '(normal visual motion operator)
             ;; These go into operator mode, so the key sequence, g U i o
             ;; upper cases the symbol at point:
             "g u" '("downcase" . evil-downcase)
             "g U" '("upcase" . evil-case)
             "g ~" '("invert case" . evil-invert-case)

             ;; Use this ALL the time:
             "g ;" '("last change" . evil-goto-last-change)
             "g d" '("goto def" . evil-goto-definition)
             "g i" '("resume insert" . evil-insert-resume)
             "g v" '("resume visual" . evil-visual-restore)

             "g g" '("goto first line" . evil-goto-first-line)
             "g f" '("find file" . find-file-at-point)
             "g q" '("fill para" . fill-paragraph)
             "g Q" '("unfill para" . unfill-paragraph)

             "g e" '("← WORD end" . evil-backward-WORD-end) ; like b
             "g E" '("← word end" . evil-backward-word-end) ; like B
             "g w" '("→ WORD end" . evil-forward-WORD-end)
             "g W" '("→ word end" . evil-forward-word-end)

             ;; Not sure how to use these two as they need text objs
             "g n" '("next match" , evil-next-match)
             "g N" '("prev match" , evil-previous-match)

             "g P" '("paste after" . evil-paste-before-cursor-after)

             ;; Let's clean out keybindings already in normal mode
             ;; without the initial g:
             "g #" nil   ; evil-search-unbounded-word-backward
             "g *" nil   ; evil-search-unbounded-word-forward
             "g ^" nil   ; evil-first-non-blank
             "g $" nil   ; evil-end-of-line
             "g _" nil   ; evil-last-non-blank ... eh
             "g 0" nil   ; evil-beginning-of-line
             "g &" nil   ; evil-ex-repeat-global-substitute
             "g 8" nil   ; what-cursor-position
             "g F" nil   ; evil-find-file-at-point-with-line
             "g J" nil   ; evil-join-whitespace
             "g I" nil   ; evil-insert-0-line ... just use I
             "g m" nil   ; evil-middle-of-visual-line
             "g M" nil   ; evil-percentage-of-line ... middle?
             "g T" nil   ; tab-bar-switch-to-prev-tab
             "g t" nil   ; tab-bar-switch-to-next-tab

             "g j" nil   ; This will be a major-mode-specific keybinding
             "g k" nil

             (kbd "g <up>") nil
             (kbd "g <down>") nil
             (kbd "g <left>") nil
             (kbd "g <right>") nil
             (kbd "g <home>") nil
             (kbd "g <end>") nil))

Top-Level Operations

Let's try this general "space" prefix by defining some top-level operations, including hitting space twice to bring up the M-x collection of functions:

  (ha-leader
    "SPC" '("M-x" . execute-extended-command)
    "."   '("repeat" . repeat)
    "!"   '("shell command" . shell-command)
    "|"   'piper
    "X"   '("org capture" . org-capture)
    "L"   '("store org link" . org-store-link)
    "RET" 'bookmark-jump
    "a"  '(:ignore t :which-key "apps")
    "o"  '(:ignore t :which-key "org/open")
    "o i" 'imenu
    "m"   '(:ignore t :which-key "mode")
    "u"   'universal-argument)

And ways to stop the system:

  (ha-leader
    "q"  '(:ignore t :which-key "quit/session")
    "q b" '("bury buffer" . bury-buffer)
    "q w" '("close window" . delete-window)
    "q K" '("kill emacs (and dæmon)" . save-buffers-kill-emacs)
    "q q" '("quit emacs" . save-buffers-kill-terminal)
    "q Q" '("quit without saving" . evil-quit-all-with-error-code))

File Operations

While find-file is still my bread and butter, I like getting information about the file associated with the buffer. For instance, the file path:

  (defun ha-relative-filepath (filepath)
    "Return the FILEPATH without the HOME directory and typical filing locations.
  The expectation is that this will return a filepath with the proejct name."
    (let* ((home-re (rx (literal (getenv "HOME")) "/"))
           (work-re (rx (regexp home-re)
                        (or "work" "other" "projects") ; Typical organization locations
                        "/"
                        (optional (or "4" "5" "xway") "/") ; Sub-organization locations
                        )))
      (cond
       ((string-match work-re filepath) (substring filepath (match-end 0)))
       ((string-match home-re filepath) (substring filepath (match-end 0)))
       (t filepath))))

  (defun ha-yank-buffer-path (&optional root)
    "Copy the file path of the buffer relative to my 'work' directory, ROOT."
    (interactive)
    (if-let (filename (buffer-file-name (buffer-base-buffer)))
        (message "Copied path to clipboard: %s"
                 (kill-new (abbreviate-file-name
                            (if root
                                (file-relative-name filename root)
                              (ha-relative-filepath filename)))))
      (error "Couldn't find filename in current buffer")))

  (defun ha-yank-project-buffer-path (&optional root)
    "Copy the file path of the buffer relative to the file's project.
  When given ROOT, this copies the filepath relative to that."
    (interactive)
    (if-let (filename (buffer-file-name (buffer-base-buffer)))
        (message "Copied path to clipboard: %s"
                 (kill-new
                  (f-relative filename (or root (projectile-project-root filename)))))
      (error "Couldn't find filename in current buffer")))

This simple function allows me to load a project-specific file in a numbered window, based on winum:

  (defun find-file-in-window (win)
    "Change the buffer in a particular window number."
    (interactive)
    (if (windowp win)
        (aw-switch-to-window win)
      (winum-select-window-by-number win))
    (consult-projectile-find-file))

With these helper functions in place, I can create a leader collection for file-related functions:

  (ha-leader
    "f"  '(:ignore t :which-key "files")
    "f a" '("load any" . find-file)
    "f f" '("load" . consult-projectile-find-file)
    "f F" '("load new window" . find-file-other-window)
    "f l" '("locate" . locate)
    "f s" '("save" . save-buffer)
    "f S" '("save as" . write-buffer)
    "f r" '("recent" . recentf-open-files)
    "f c" '("copy" . copy-file)
    "f R" '("rename" . rename-file)
    "f D" '("delete" . delete-file)
    "f y" '("yank path" . ha-yank-buffer-path)
    "f Y" '("yank path from project" . ha-yank-project-buffer-path)
    "f d" '("dired" . dirvish)

    "f 1" '("load win-1" . ha-find-file-window-1)
    "f 2" '("load win-2" . ha-find-file-window-2)
    "f 3" '("load win-3" . ha-find-file-window-3)
    "f 4" '("load win-4" . ha-find-file-window-4)
    "f 5" '("load win-5" . ha-find-file-window-5)
    "f 6" '("load win-6" . ha-find-file-window-6)
    "f 7" '("load win-7" . ha-find-file-window-7)
    "f 8" '("load win-8" . ha-find-file-window-8)
    "f 9" '("load win-9" . ha-find-file-window-9))

On Unix systems, the locate command is faster than find when searching the whole system, since it uses a pre-computed database, and find is faster if you need to search a specific directory instead of the whole system. On the Mac, we need to change the locate command:

  (when (equal system-type 'darwin)
    (setq locate-command "mdfind"))

The advantage of mdfind is that is searches for filename and its contents of your search string.

Trying the spotlight project, as it has a slick interface for selecting files:

  (use-package spotlight
    :config (ha-leader "f /" '("search files" . spotlight)))

Buffer Operations

This section groups buffer-related operations under the "SPC b" sequence.

Putting the entire visible contents of the buffer on the clipboard is often useful:

  (defun ha-yank-buffer-contents ()
    "Copy narrowed contents of the buffer to the clipboard."
    (interactive)
    (kill-new (buffer-substring-no-properties
               (point-min) (point-max))))

This simple function allows me to switch to a buffer in a numbered window, based on winum:

  (defun switch-buffer-in-window (win)
    "Change the buffer in a particular window number."
    (interactive)
    (if (windowp win)
        (aw-switch-to-window win)
      (winum-select-window-by-number win))
    (consult-project-buffer))

And the collection of useful operations:

  (ha-leader
    "b"  '(:ignore t :which-key "buffers")
    "b B" '("switch" . persp-switch-to-buffer)
    "b o" '("switch" . switch-to-buffer-other-window)
    "b O" '("other" . projectile-switch-buffer-to-other-window)
    "b i" '("ibuffer" . ibuffer)
    "b I" '("ibuffer" . ibuffer-other-window)
    "b k" '("persp remove" . persp-remove-buffer)
    "b N" '("new" . evil-buffer-new)
    "b d" '("delete" . persp-kill-buffer*)
    "b r" '("revert" . revert-buffer)
    "b s" '("save" . save-buffer)
    "b S" '("save all" . evil-write-all)
    "b n" '("next" . next-buffer)
    "b p" '("previous" . previous-buffer)
    "b y" '("copy contents" . ha-yank-buffer-contents)
    "b z" '("bury" . bury-buffer)
    "b Z" '("unbury" . unbury-buffer)

    "b 1" '("load win-1" . (lambda () (interactive) (switch-buffer-in-window 1)))
    "b 2" '("load win-2" . (lambda () (interactive) (switch-buffer-in-window 2)))
    "b 3" '("load win-3" . (lambda () (interactive) (switch-buffer-in-window 3)))
    "b 4" '("load win-4" . (lambda () (interactive) (switch-buffer-in-window 4)))
    "b 5" '("load win-5" . (lambda () (interactive) (switch-buffer-in-window 5)))
    "b 6" '("load win-6" . (lambda () (interactive) (switch-buffer-in-window 6)))
    "b 7" '("load win-7" . (lambda () (interactive) (switch-buffer-in-window 7)))
    "b 8" '("load win-8" . (lambda () (interactive) (switch-buffer-in-window 8)))
    "b 9" '("load win-9" . (lambda () (interactive) (switch-buffer-in-window 9)))

    ;; And double up on the bookmarks:
    "b m" '("set bookmark" . bookmark-set)
    "b M" '("delete mark" . bookmark-delete))

Toggle Switches

The goal here is toggle switches and other miscellaneous settings.

  (ha-leader
    "t"   '(:ignore t :which-key "toggles")
    "t a" '("abbrev"         . abbrev-mode)
    "t d" '("debug"          . toggle-debug-on-error)
    "t F" '("show functions" . which-function-mode)
    "t f" '("auto-fill"      . auto-fill-mode)
    "t l" '("line numbers"   . display-line-numbers-mode)
    "t R" '("read only"      . read-only-mode)
    "t t" '("truncate"       . toggle-truncate-lines)
    "t v" '("visual"         . visual-line-mode)
    "t w" '("whitespace"     . whitespace-mode))
Line Numbers

Since we can't automatically toggle between relative and absolute line numbers, we create this function:

  (defun ha-toggle-relative-line-numbers ()
    (interactive)
    (if (eq display-line-numbers 'relative)
        (setq display-line-numbers t)
      (setq display-line-numbers 'relative)))

Add it to the toggle menu:

  (ha-leader
    "t r" '("relative lines" . ha-toggle-relative-line-numbers))
Narrowing

I like the focus the Narrowing features offer, but what a dwim aspect:

  (defun ha-narrow-dwim ()
    "Narrow to region or org-tree or widen if already narrowed."
    (interactive)
    (cond
     ((buffer-narrowed-p) (widen))
     ((region-active-p)  (narrow-to-region (region-beginning) (region-end)))
     ((and (fboundp 'logos-focus-mode)
           (seq-contains local-minor-modes 'logos-focus-mode 'eq))
      (logos-narrow-dwim))
     ((eq major-mode 'org-mode) (org-narrow-to-subtree))
     (t  (narrow-to-defun))))

And put it on the toggle menu:

  (ha-leader "t n" '("narrow" . ha-narrow-dwim))

Window Operations

While it comes with Emacs, I use winner-mode to undo window-related changes:

  (use-package winner
    :custom
    (winner-dont-bind-my-keys t)
    :config
    (winner-mode +1))
Ace Window

Use the ace-window project to jump to any window you see.

Often transient buffers show in other windows, obscuring my carefully crafted display. Instead of jumping into a window, typing q (to either call quit-buffer) if available, or bury-buffer otherwise. This function hooks to ace-window

  (defun ha-quit-buffer (window)
    "Quit or bury buffer in a given WINDOW."
    (interactive)
    (aw-switch-to-window window)
    (unwind-protect
        (condition-case nil
            (quit-buffer)
          (error
           (bury-buffer))))
    (aw-flip-window))

Since I use numbers for the window, I can make the commands more mnemonic, and add my own:

  (use-package ace-window
    :init
    (setq aw-dispatch-alist
          '((?d aw-delete-window "Delete Window")
            (?m aw-swap-window "Swap Windows")
            (?M aw-move-window "Move Window")
            (?c aw-copy-window "Copy Window")
            (?b switch-buffer-in-window "Select Buffer")
            (?f find-file-in-window "Find File")
            (?n aw-flip-window)
            (?c aw-split-window-fair "Split Fair Window")
            (?s aw-split-window-vert "Split Vert Window")
            (?v aw-split-window-horz "Split Horz Window")
            (?o delete-other-windows "Delete Other Windows")
            (?q ha-quit-buffer "Quit Buffer")
            (?w aw-execute-command-other-window "Execute Command")
            (?? aw-show-dispatch-help)))

    :bind ("s-w" . ace-window))

Keep in mind, these shortcuts work with more than two windows open. For instance, SPC w w d 3 closes the "3" window.

Transpose Windows

My office at work has a monitor oriented vertically, and to move an Emacs with “three columned format” to a “stacked format” I use the transpose-frame package:

  (use-package transpose-frame)
Winum

To jump to a window even quicker, use the winum package:

  (use-package winum
    :bind (("s-1" . winum-select-window-1)
           ("s-2" . winum-select-window-2)
           ("s-3" . winum-select-window-3)
           ("s-4" . winum-select-window-4)
           ("s-5" . winum-select-window-5)
           ("s-6" . winum-select-window-6)
           ("s-7" . winum-select-window-7)
           ("s-8" . winum-select-window-8)
           ("s-9" . winum-select-window-9)))

This is nice since the window numbers are always present on a Doom modeline, but they sometime order the window numbers differently than ace-window.

The 0 key/window should be always associated with a project-specific tree window of dired (or Dirvish):

  (use-package winum
    :config
    (winum-mode +1)
    (add-to-list 'winum-assign-functions
                 (lambda () (when (eq major-mode 'dired-mode) 10))))

Id like to have dirvish show in Window 0:

  (defun dirvish-show-or-switch ()
    "As it says on the tin. Show or start Dirvish.
  If `divish' is showing, that is, is window 0 is showing,
  switch to it, otherwise, start 'er up."
    (interactive)
    (if (seq-contains (winum--available-numbers) 0)
        (winum-select-window-0-or-10)
      (dirvish-side (projectile-project-root))))

And lets bind Command-0 to select the window that shows dirvish, or open drvish:

  (use-package winum
    :bind ("s-0" . dirvish-show-or-switch))

Let's try this out with a Hydra since some I can repeat some commands (e.g. enlarge window). It also allows me to organize the helper text.

  (use-package hydra
    :config
    (defhydra hydra-window-resize (:color blue :hint nil) "
  _w_: select _m_: move/swap _u_: undo  _^_: taller (t)  _+_: text larger
  _j_: go up  _d_: delete    _U_: undo+ _v_: shorter (T) _-_: text smaller
  _k_: down   _e_: balance   _r_: redo  _>_: wider       _F_: font larger
  _h_: left   _n_: v-split   _R_: redo+ _<_: narrower    _f_: font smaller
  _l_: right  _s_: split   _o_: only this window     _c_: choose (also 1-9)"
      ("w" ace-window)
      ("c" other-window                 :color pink) ; change window
      ("o" delete-other-windows)          ; “Only” this window
      ("d" delete-window)     ("x" delete-window)

      ;; Ace Windows ... select the window to affect:
      ("m" ace-swap-window)
      ("D" ace-delete-window)
      ("O" ace-delete-other-windows)

      ("u" winner-undo)
      ("U" winner-undo                 :color pink)
      ("C-r" winner-redo)
      ("r" winner-redo)
      ("R" winner-redo                 :color pink)

      ("J" evil-window-down            :color pink)
      ("K" evil-window-up              :color pink)
      ("H" evil-window-left            :color pink)
      ("L" evil-window-right           :color pink)

      ("j" evil-window-down)
      ("k" evil-window-up)
      ("h" evil-window-left)
      ("l" evil-window-right)

      ("x" transpose-frame)
      ("s" hydra-window-split/body)
      ("n" hydra-window-split/body)

      ("F" font-size-increase          :color pink)
      ("f" font-size-decrease          :color pink)
      ("+" text-scale-increase         :color pink)
      ("=" text-scale-increase         :color pink)
      ("-" text-scale-decrease         :color pink)
      ("^" evil-window-increase-height :color pink)
      ("v" evil-window-decrease-height :color pink)
      ("t" evil-window-increase-height :color pink)
      ("T" evil-window-decrease-height :color pink)
      (">" evil-window-increase-width  :color pink)
      ("<" evil-window-decrease-width  :color pink)
      ("." evil-window-increase-width  :color pink)
      ("," evil-window-decrease-width  :color pink)
      ("e" balance-windows)

      ("1" winum-select-window-1)
      ("2" winum-select-window-2)
      ("3" winum-select-window-3)
      ("4" winum-select-window-4)
      ("5" winum-select-window-5)
      ("6" winum-select-window-6)
      ("7" winum-select-window-7)
      ("8" winum-select-window-8)
      ("9" winum-select-window-9)
      ("0" dirvish-dwim)

      ;; Extra bindings:
      ("q" nil :color blue)))

  (ha-leader "w" '("windows" . hydra-window-resize/body))
Window Splitting

When I split a window, I have a following intentions:

  • Split and open a file from the prespective/project in the new window
  • Split and change to a buffer from the prespective in the new window
  • Split and move focus to the new window … you know, to await a new command

And when creating new windows, why isn't the new window selected? Also, when I create a new window, I typically want a different buffer or file shown.

  (defun ha-new-window (side file-or-buffer)
    (pcase side
      (:left  (split-window-horizontally))
      (:right (split-window-horizontally)
              (other-window 1))
      (:above (split-window-vertically))
      (:below (split-window-vertically)
              (other-window 1)))
    (pcase file-or-buffer
      (:file   (call-interactively 'consult-projectile-find-file))
      (:buffer (call-interactively 'consult-projectile-switch-to-buffer))
      (:term   (ha-shell (projectile-project-root)))))

Shame that hydra doesnt have an ignore-case feature.

  (use-package hydra
    :config
    (defhydra hydra-window-split (:color blue :hint nil)
      ("s" hydra-window-split-below/body "below")
      ("j" hydra-window-split-below/body "below")
      ("k" hydra-window-split-above/body "above")
      ("h" hydra-window-split-left/body "left")
      ("l" hydra-window-split-right/body "right")
      ("n" hydra-window-split-right/body "right"))

    (defhydra hydra-window-split-above (:color blue :hint nil)
      ("b" (lambda () (interactive) (ha-new-window :above :buffer)) "switch buffer")
      ("f" (lambda () (interactive) (ha-new-window :above :file))   "load file")
      ("t" (lambda () (interactive) (ha-new-window :above :term))   "terminal")
      ("k" split-window-below                                  "split window"))

    (defhydra hydra-window-split-below (:color blue :hint nil)
      ("b" (lambda () (interactive) (ha-new-window :below :buffer))        "switch buffer")
      ("f" (lambda () (interactive) (ha-new-window :below :file))          "load file    ")
      ("t" (lambda () (interactive) (ha-new-window :below :term))          "terminal")
      ("j" (lambda () (interactive) (split-window-below) (other-window 1)) "split window ")
      ("s" (lambda () (interactive) (split-window-below) (other-window 1)) "split window "))

    (defhydra hydra-window-split-right (:color blue :hint nil)
      ("b" (lambda () (interactive) (ha-new-window :right :buffer))        "switch buffer")
      ("f" (lambda () (interactive) (ha-new-window :right :file))          "load file")
      ("t" (lambda () (interactive) (ha-new-window :right :term))          "terminal")
      ("l" (lambda () (interactive) (split-window-right) (other-window 1)) "split window ")
      ("n" (lambda () (interactive) (split-window-right) (other-window 1)) "split window "))

    (defhydra hydra-window-split-left (:color blue :hint nil)
      ("b" (lambda () (interactive) (ha-new-window :left :buffer))         "switch buffer")
      ("f" (lambda () (interactive) (ha-new-window :left :file))           "load file    ")
      ("t" (lambda () (interactive) (ha-new-window :left :term))           "terminal")
      ("h" split-window-right                                         "split window")))

This means that, without thinking, the following just works:

SPC w s s s
creates a window directly below this.
SPC w n n n
creates a window directly to the right.

But, more importantly, the prefix w s gives me more precision to view what I need.

Search Operations

Ways to search for information goes under the s key. The venerable sage has always been grep, but we now have new-comers, like ripgrep, which are really fast.

ripgrep

Install the rg package, which builds on the internal grep system, and creates a *rg* window with compilation mode, so C-j and C-k will move and show the results by loading those files.

  (use-package rg
    :config
    ;; Make an interesting Magit-like menu of options, which I don't use much:
    (rg-enable-default-bindings (kbd "M-R"))

    ;; Old habits die hard ...
    (define-key global-map [remap xref-find-references] 'rg-dwim)

    (ha-leader
      "s"  '(:ignore t :which-key "search")
      "s q" '("close" . ha-rg-close-results-buffer)
      "s r" '("dwim" . rg-dwim)
      "s s" '("search" . rg)
      "s S" '("literal" . rg-literal)
      "s p" '("project" . rg-project) ; or projectile-ripgrep
      "s d" '("directory" . rg-dwim-project-dir)
      "s f" '("file only" . rg-dwim-current-file)
      "s j" '("next results" . ha-rg-go-next-results)
      "s k" '("prev results" . ha-rg-go-previous-results)
      "s b" '("results buffer" . ha-rg-go-results-buffer))

    (defun ha-rg-close-results-buffer ()
      "Close to the `*rg*' buffer that `rg' creates."
      (interactive)
      (kill-buffer "*rg*"))

    (defun ha-rg-go-results-buffer ()
      "Pop to the `*rg*' buffer that `rg' creates."
      (interactive)
      (pop-to-buffer "*rg*"))

    (defun ha-rg-go-next-results ()
      "Bring the next file results into view."
      (interactive)
      (ha-rg-go-results-buffer)
      (next-error-no-select)
      (compile-goto-error))

    (defun ha-rg-go-previous-results ()
      "Bring the previous file results into view."
      (interactive)
      (ha-rg-go-results-buffer)
      (previous-error-no-select)
      (compile-goto-error)))

Note we bind the key M-R to the rg-menu, which is a Magit-like interface to ripgrep.

I dont understand the bug associated with the :general extension to use-package, but it works, but stops everything else from working, so pulling it out into its own use-package section addresses that issue:

  (use-package rg
    :general (:states 'normal "gS" 'rg-dwim))
wgrep

The wgrep package integrates with ripgrep. Typically, you hit i to automatically go into wgrep-mode and edit away, but since I typically want to edit everything at the same time, I have a toggle that should work as well:

  (use-package wgrep
    :after rg
    :commands wgrep-rg-setup
    :hook (rg-mode-hook . wgrep-rg-setup)
    :config
    (ha-leader
      :keymaps 'rg-mode-map  ; Actually, `i' works!
      "s w" '("wgrep-mode" . wgrep-change-to-wgrep-mode)
      "t w" '("wgrep-mode" . wgrep-change-to-wgrep-mode)))

Text Operations

Stealing much of this from Spacemacs.

  (ha-leader
    "x"  '(:ignore t :which-key "text")
    "x a" '("align"            . align-regexp)
    "x q" '("fill paragraph"   . fill-paragraph)
    "x p" '("unfill paragraph" . unfill-paragraph))

Unfilling a paragraph joins all the lines in a paragraph into a single line. Taken from here … I use this all the time:

  (defun unfill-paragraph ()
    "Convert a multi-line paragraph into a single line of text."
    (interactive)
    (let ((fill-column (point-max)))
      (fill-paragraph nil)))

Help Operations

While the C-h is easy enough, I am now in the habit of typing SPC h instead. Since I tweaked the help menu, I craft my own menu:

  (ha-leader
    "h"  '(:ignore t :which-key "help")
    "h ." '("cursor position"  . what-cursor-position)
    "h a" '("apropos"          . apropos-command)
    "h c" '("elisp cheatsheet" . shortdoc-display-group)
    "h e" '("errors"           . view-echo-area-messages)
    "h f" '("function"         . helpful-callable)
    "h F" '("font"             . describe-font)
    "h =" '("face"             . describe-face)
    "h k" '("key binding"      . helpful-key)
    "h K" '("key map"          . describe-keymap)
    "h m" '("mode"             . describe-mode)
    "h o" '("symbol"           . describe-symbol)
    "h p" '("package"          . describe-package)
    "h s" '("info symbol"      . info-lookup-symbol)
    "h v" '("variable"         . helpful-variable)
    "h i" '("info"             . info)
    "h I" '("info manual"      . info-display-manual)
    "h j" '("info jump"        . info-apropos)

    "h E" '("emacs info"       . (lambda () (interactive) (info "emacs")))
    "h L" '("emacs-lisp"       . (lambda () (interactive) (info "elisp")))
    "h O" '("org info"         . (lambda () (interactive) (info "org")))
    ;; Since I do a lot of literate programming, I appreciate a quick
    ;; jump directly into the Info manual...
    "h B" '("org babel"        . (lambda () (interactive)
                                   (org-info-open "org#Working with Source Code" nil))))

Remember these keys in the Help buffer:

s
view source of the function
i
view info manual of the function

Let's make Info behave a little more VI-like:

  (use-package info
    :straight (:type built-in)
    :general
    (:states 'normal :keymaps 'Info-mode-map
             "B" 'Info-bookmark-jump
             "Y" 'org-store-link
             "H" 'Info-history-back
             "L" 'Info-history-forward
             "u" 'Info-up
             "U" 'Info-directory
             "T" 'Info-top-node
             "p" 'Info-backward-node
             "n" 'Info-forward-node))    ; Old habit die hard

Consult

The consult project aims to use libraries like Vertico to enhance specific, built-in, Emacs functions. I appreciate this project that when selecting an element in the minibuffer, it displays what you are looking at… for instance, it previews a buffer before choosing it. Unlike Vertico and Orderless, you need to bind keys to its special functions (or rebind existing keys that do something similar).

  (use-package consult
    :after general
    ;; Enable automatic preview at point in the *Completions* buffer. This is
    ;; relevant when you use the default completion UI.
    :hook (completion-list-mode . consult-preview-at-point-mode)

    :init
    ;; Use Consult to select xref locations with preview
    (setq xref-show-xrefs-function #'consult-xref
          xref-show-definitions-function #'consult-xref)

    (ha-leader
      "RET" '("bookmark" . consult-bookmark)
      "o i" '("imenu" . consult-imenu)
      "x y" '("preview yank" . consult-yank-pop))

    :bind ("s-v" . consult-yank-pop)

    :general
    (:states 'normal
             "gp" '("preview paste" . 'consult-yank-pop)
             "gs" '("go to line" . 'consult-line)))

Consult for Projects

One of the reasons that Consult hasnt been too important to me, is that I often narrow my searching based on projectile. The consult-projectile can help with this.

  (use-package consult-projectile
    :after consult general
    :straight (:host gitlab :repo "OlMon/consult-projectile" :branch "master")
    :config
    (ha-leader
      "p ." '("switch to..."     . consult-projectile)
      "b b" '("switch buffer"    . consult-projectile-switch-to-buffer)
      "p p" '("switch project"   . consult-projectile-switch-project)
      "p f" '("find file"        . consult-projectile-find-file)
      "p r" '("find recent file" . consult-projectile-recentf)))

The advantage of persp-switch-to-buffer over consult-projectile-switch-to-buffer is that is shows non-file buffers.

Embark

The embark project offers actions on targets. I'm primarily thinking of acting on selected items in the minibuffer, but these commands act anywhere. I need an easy-to-use keybinding that doesn't conflict. Hey, that is what the Super key is for, right?

  (use-package embark
    :bind
    (("s-;" . embark-act)               ; Work in minibuffer and elsewhere
     ("s-/" . embark-dwim))

    :init
    ;; Optionally replace the key help with a completing-read interface
    (setq prefix-help-command #'embark-prefix-help-command)

    :config
    (ha-leader "h K" '("keybindings" . embark-bindings)))

According to this essay, Embark cooperates well with the Marginalia and Consult packages. Neither of those packages is a dependency of Embark, but Embark supplies a hook for Consult where Consult previews can be done from Embark Collect buffers:

  (use-package embark-consult
    :after (embark consult)
    :demand t ; only necessary if you have the hook below
    ;; if you want to have consult previews as you move around an
    ;; auto-updating embark collect buffer
    :hook
    (embark-collect-mode . consult-preview-at-point-mode))

According to the Embark-Consult page:

Users of the popular which-key package may prefer to use the embark-which-key-indicator from the Embark wiki. Just copy its definition from the wiki into your configuration and customize the embark-indicators user option to exclude the mixed and verbose indicators and to include embark-which-key-indicator.

In other words, typing s-; to call Embark, specifies the options in a buffer, but the following code puts them in a smaller configuration directly above the selections.

  (defun embark-which-key-indicator ()
    "An embark indicator that displays keymaps using which-key.
  The which-key help message will show the type and value of the
  current target followed by an ellipsis if there are further
  targets."
    (lambda (&optional keymap targets prefix)
      (if (null keymap)
          (which-key--hide-popup-ignore-command)
        (which-key--show-keymap
         (if (eq (plist-get (car targets) :type) 'embark-become)
             "Become"
           (format "Act on %s '%s'%s"
                   (plist-get (car targets) :type)
                   (embark--truncate-target (plist-get (car targets) :target))
                   (if (cdr targets) "…" "")))
         (if prefix
             (pcase (lookup-key keymap prefix 'accept-default)
               ((and (pred keymapp) km) km)
               (_ (key-binding prefix 'accept-default)))
           keymap)
         nil nil t (lambda (binding)
                     (not (string-suffix-p "-argument" (cdr binding))))))))

  (setq embark-indicators
        '(embark-which-key-indicator
          embark-highlight-indicator
          embark-isearch-highlight-indicator))

  (defun embark-hide-which-key-indicator (fn &rest args)
    "Hide the which-key indicator immediately when using the completing-read prompter."
    (which-key--hide-popup-ignore-command)
    (let ((embark-indicators
           (remq #'embark-which-key-indicator embark-indicators)))
      (apply fn args)))

  (advice-add #'embark-completing-read-prompter
              :around #'embark-hide-which-key-indicator)

Evil Extensions

Evil Exchange

I often use the Emacs commands, M-t and whatnot to exchange words and whatnot, but this requires a drop out of normal state mode. The evil-exchange project attempts to do something similar, but in a VI-way, and the objects do not need to be adjacent.

  (use-package evil-exchange
    :init
    (setq evil-exchange-key (kbd "gx")
          evil-exchange-cancel-key (kbd "gX"))

    :general (:states 'normal
                      "g x" '("exchange" . 'evil-exchange)
                      "g X" '("cancel exchange" . 'evil-exchange-cancel)

                      ;; What about a "normal mode" binding to regular emacs transpose?
                      "z w" '("transpose words" . transpose-words)
                      "z x" '("transpose sexps" . transpose-sexps)
                      "z k" '("transpose lines" . transpose-lines))

    :config (evil-exchange-install))

Lets explain how this works as the documentation assumes some previous knowledge. If you had a sentence:

The ball was blue and the boy was red.

Move the point to the word, red, and type g x i w (anywhere since we are using the inner text object). Next, jump to the word blue, and type the sequence, g x i w again, and you have:

The ball was blue and the boy was red.

The idea is that you can exchange anything. The g x marks something (like what we would normally do in visual mode), and then by marking something else with a g x sequence, it swaps them.

Notice that you can swap:

gx i w
words, W words with dashes, or o for programming symbols (like variables)
gx i s
sentences
gx i p
paragraphs
gx i x
programming s-expressions between parens, braces, etc.
gx i l
lines, with the line-based text object project installed

Evil Lion

The evil-lion package is a wrapper around Emacs align function. Just a little easier to use. Primary sequence is g a i p = to align along all the equal characters in the paragraph (block), or g a i b RET to use a built in rule to align (see below), or g a i b / to specify a regular expression, similar to align-regexp.

  (use-package evil-lion
    :after evil
    :general
    (:states '(normal visual)
             "g a" '("lion ←" . evil-lion-left)
             "g A" '("lion →" . evil-lion-right)))

Lion sounds like align … get it?

Where I like to align, is on variable assignments, e.g.

  (let ((foobar        "Something something")
        (a             42)
        (very-long-var "odd string"))
    ;;
    )

If you press RETURN for the character to align, evil-lion package simply calls the built-in align function. This function chooses a regular expression based on a list of rules, and aligning Lisp variables requires a complicated regular expression. Extend align-rules-list:

  (use-package align
    :straight (:type built-in)
    :config
    (add-to-list 'align-rules-list
                 `("lisp-assignments"
                   (regexp . ,(rx (group (one-or-more space))
                                  (or
                                   (seq "\"" (zero-or-more any) "\"")
                                   (one-or-more (not space)))
                                  (one-or-more ")") (zero-or-more space) eol))
                   (group . 1)
                   (modes . align-lisp-modes))))

Evil Commentary

The evil-commentary is a VI-like way of commenting text. Yeah, I typically type M-; to call Emacs originally functionality, but in this case, g c c comments out a line(s), and g c comments text objects and whatnot. For instance, g c $ comments to the end of the line.

  (use-package evil-commentary
    :config (evil-commentary-mode)

    :general
    (:states '(normal visual motion operator)
             "g c" '("comments" . evil-commentary)
             "g y" '("yank comment" . evil-commentary-yank)))

Evil Collection

Dropping into Emacs state is better than pure Evil state for applications, however, the evil-collection package creates a hybrid between the two, that I like.

  (use-package evil-collection
    :after evil
    :config
    (evil-collection-init))

Do I want to specify the list of modes to change for evil-collection-init, e.g.

  '(eww magit dired notmuch term wdired)

Evil Owl

Not sure what is in a register? Have it show you when you hit or @ with evil-owl:

  (use-package posframe)

  (use-package evil-owl
    :after posframe
    :config
    (setq evil-owl-display-method 'posframe
          evil-owl-extra-posframe-args '(:width 50 :height 20 :background-color "#444")
          evil-owl-max-string-length 50)
    (evil-owl-mode))

Evil Snipe

Doom introduced me to evil-snipe, like f and t, but with two characters, and can, when configured, search more than the current line. My issue is that /git/howard/hamacs/src/commit/add010fd445417e559719ada5e22e732d67f3ae7/Evil%20Surround uses the same keybindings. Since surround doesnt work in normal and visual states, well bind snipe only for those:

  (use-package evil-snipe
    :after evil
    :init
    (setq evil-snipe-scope 'visible)

    :general
    (:states '(normal visual)
             "s" 'evil-snipe-s
             "S" 'evil-snipe-S)
    :config
    (evil-snipe-mode +1))

It highlights all potential matches, use ; to skip to the next match, and , to jump back.

Evil Surround

I like both evil-surround and Henrik's evil-snipe, but they both start with s, and conflict, and getting them to work together means I have to remember when does s call sniper and when it calls surround. As an original Emacs person, I am not bound by that key history, but I do need them consistent, so Im choosing the s to be surround.

  (use-package evil-surround
    :config
    (defun evil-surround-elisp ()
      (push '(?\` . ("`" . "'")) evil-surround-pairs-alist))
    (defun evil-surround-org ()
      (push '(?\" . ("“" . "”")) evil-surround-pairs-alist)
      (push '(?\' . ("" . "")) evil-surround-pairs-alist)
      (push '(?b . ("*" . "*")) evil-surround-pairs-alist)
      (push '(?* . ("*" . "*")) evil-surround-pairs-alist)
      (push '(?i . ("/" . "/")) evil-surround-pairs-alist)
      (push '(?/ . ("/" . "/")) evil-surround-pairs-alist)
      (push '(?= . ("=" . "=")) evil-surround-pairs-alist)
      (push '(?~ . ("~" . "~")) evil-surround-pairs-alist))

    (global-evil-surround-mode 1)

    :hook
    (org-mode . evil-surround-org)
    (emacs-lisp-mode . evil-surround-elisp))

Notes:

cs'"
to convert surrounding single quote string to double quotes.
ds"
to delete the surrounding double quotes.
yse"
puts single quotes around the next word.
ysiw'
puts single quotes around the word, no matter the points position.
yS$<p>
surrouds the line with HTML <p> tag (with extra carriage returns).
ysiw'
puts single quotes around the word, no matter the points position.
(
puts spaces inside the surrounding parens, but ) doesn't. Same with [ and ].

Additional Global Packages

The following defines my use of the Emacs completion system. Ive decided my rules will be:

  • Nothing should automatically appear; that is annoying and distracting.
  • Spelling in org files (abbrev or hippie expander) and code completion are separate, but Im not sure if I can split them
  • IDEs overuse the TAB binding, and I should re-think the bindings.

Auto Completion

I dont find the Emacs completion system obvious, with different interfaces, some distinct, some connected. Heres the summary as I understand:

indent-for-tab-command, which we can call: └─ completion-at-point, which calls: └─ completion-at-point-functions (capf), which can call: └─ hippie and dabbrev functions

In org-mode, TAB calls org-cycle, which, in the context of typing text, calls the binding for TAB, which is the indent-for-tab-command. If the line is indented, I can complete the word:

  (setq tab-always-indent 'complete
        tab-first-completion 'word-or-paren
        completion-cycle-threshold nil)

Note that no matter the setting for tab-first-completion, hitting TAB twice, results in completion.

This calls completion-at-point. This code (from mini-buffer) doubles with the other completing processes (like completing-read) and presents choices based on a series of functions (see this essay for details). This will call into the CAPF function list (see the variable, completion-at-point-functions and the Cape section for details).

Hippie Expand

The venerable hippie-expand function does a better job than the default, dabbrev-expand, so lets swap it out (see this essay by Mickey Petersen) with its default key of M-/ (easy to type on the laptop) as well as C-Tab (easier on mechanical keyboards):

  (global-set-key [remap dabbrev-expand] 'hippie-expand)
  (global-set-key (kbd "M-<tab>") 'completion-at-point)

Details on its job? We need to update its list of expanders. I dont care much for try-expand-line, so that is not on the list.

  (setq hippie-expand-try-functions-list
        '(try-complete-file-name-partially   ; complete filenames, start with /
          try-complete-file-name
          yas-hippie-try-expand              ; expand matching snippets
          try-expand-all-abbrevs
          try-expand-list                    ; help when args repeated another's args
          try-expand-dabbrev
          try-expand-dabbrev-all-buffers
          try-expand-whole-kill              ; grab text from the kill ring
          try-expand-dabbrev-from-kill       ; as above
          try-complete-lisp-symbol-partially
          try-complete-lisp-symbol))

In the shell, IDEs and other systems, the key binding is typically TAB. In modes other than org-mode, TAB re-indents the line with indent-for-tab-command, but I find that I want that feature when Im in Evils normal state and hit the = key, so changing this sounds good. But why not have both?

  (advice-add #'indent-for-tab-command :after #'hippie-expand)

Corfu

The default completion system either inserts the first option directly in the text (without cycling, so lets hope it gets it right the first time), or presents choices in another buffer (who wants to hop to it to select an expansion).

After using company for my completion back-end, I switch to corfu as it works with the variable-spaced font of my org files (also see this essay for my initial motivation).

  (use-package corfu
    :custom
    (corfu-cycle t)
    (corfu-separator ?\s)
    :init
    (global-corfu-mode))

Yet Another Snippet System (YASnippets)

Using yasnippet to convert templates into text:

  (use-package yasnippet
    :config
    (add-to-list 'yas-snippet-dirs
                 (expand-file-name "snippets" user-emacs-directory))
    (yas-global-mode +1))

Check out the documentation for writing them.

Since I have troubles installing Dooms collection of snippets, lets use the yasnippet-snippets package:

  (use-package yasnippet-snippets)

Auto Insert Templates

The auto-insert feature is a wee bit complicated. All I want is to associate a filename regular expression with a YASnippet template. I'm stealing some ideas from Henrik Lissner's set-file-template! macro, but simpler?

  (use-package autoinsert
    :init
    (setq auto-insert-directory (expand-file-name "templates" user-emacs-directory))
    ;; Don't prompt before insertion:
    (setq auto-insert-query nil)

    (add-hook 'find-file-hook 'auto-insert)
    (auto-insert-mode t))

Since auto insertion requires entering data for particular fields, and for that Yasnippet is better, so in this case, we combine them:

  (defun ha-autoinsert-yas-expand()
    "Replace text in yasnippet template."
    (yas-expand-snippet (buffer-string) (point-min) (point-max)))

And since I'll be associating snippets with new files all over my configuration, let's make a helper function:

  (defun ha-auto-insert-file (filename-re snippet-name)
    "Autofill file buffer matching FILENAME-RE regular expression.
    The contents inserted from the YAS SNIPPET-NAME."
    ;; The define-auto-insert takes a regular expression and an ACTION:
    ;; ACTION may also be a vector containing successive single actions.
    (define-auto-insert filename-re
      (vector snippet-name 'ha-autoinsert-yas-expand)))

As an example of its use, any Org files loaded in this project should insert my config file:

  (ha-auto-insert-file (rx "hamacs/" (one-or-more any) ".org" eol) "hamacs-config")

Visual Replace with Visual Regular Expressions

I appreciated the visual-regexp package to see what you want to change before executing the replace.

  (use-package visual-regexp
    :bind (("C-c r" . vr/replace)
           ("C-c q" . vr/query-replace))
    :general (:states 'normal "g r" '("replace" . vr/replace))
    :config (ha-leader
              "r" '("replace" . vr/replace)
              "R" '("query replace" . vr/query-replace)))

Jump with Avy

While I grew up on Control S, I am liking the mental model associated with the avy project that allows a jump among matches across all visible windows. I use the F18 key on my keyboard that should be easy to use, but g o seems obvious.

  (use-package avy
    :init
    (setq avy-all-windows t
          avy-single-candidate-jump t
          avy-orders-alist
          '((avy-goto-char . avy-order-closest)
            (avy-goto-word-0 . avy-order-closest)))

    :config (ha-leader "j" '("jump" . avy-goto-char-timer))

    :general
    (:states 'normal "go" '("avy goto" . avy-goto-char-timer))

    :bind ("<f18>" . avy-goto-char-timer))

Note: The links should be shorter near the point as opposed to starting from the top of the window.

If you hit the following keys before you select a target, you get a special action:

n
copies the matching target word

Link Hint, the Link Jumper

The Goto Address mode (see this online link) turns URLs into clickable links. Nice feature and built into Emacs, but it requires using the mouse or moving to the URL and hitting Return (if you like this idea, check out Álvaro Ramírez's configuration for this).

I appreciated ace-links idea for hyperlinks on Org, EWW and Info pages, as it allowed you to jump to a URL from any location on the screen. The link-hint project does this, but works with more types of files and links:

  (use-package link-hint
    :bind
    ("s-o" . link-hint-open-link)
    ("C-c l o" . link-hint-open-link)
    ("C-c l c" . link-hint-copy-link)
    :general
    (:states 'normal
             "gl" '("open link" . link-hint-open-link)
             "gL" '("copy link" . link-hint-copy-link)
             "g C-l" '("open link→window" . link-hint-open-link-ace-window))
    (:states 'normal :keymaps 'eww-mode-map
             "o" 'link-hint-open-link)
    (:states 'normal :keymaps 'Info-mode-map
             "o" 'link-hint-open-link))

Can I open a link in another window? The idea with this is that I can select a link, and with multiple windows open, I can specify where the *eww* window should show the link. If only two windows, then the new EWW buffer shows in the other one.

  (defun link-hint-open-link-ace-window ()
    (interactive)
    (link-hint-copy-link)
    (aw-select "Open EWW Link" #'link-hint-open-link-window))

  (defun link-hint-open-link-window (window)
    (aw-switch-to-window window)
    (eww (current-kill 0)))

Expand Region

Magnar Sveen's expand-region project allows me to hit v in visual mode, and have the selection grow by syntactical units.

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

    :general
    ;; Use escape to get out of visual mode, but hitting v again expands the selection.
    (:states 'visual
             "v" 'er/expand-region
             "V" 'er/contract-region
             "-" 'er/contract-region))

Working Layout

While editing any file on disk is easy enough, I like the mental context switch associated with a full-screen window frame showing all the buffers of a project task (often a direct link to a repository project, but not always).

Projects

While I don't need all the features that projectile provides, it has all the features I do need, and is easy enough to install. I am referring to the fact that I could use the built-in project.el system (see this essay for details on what I mean as an alternative).

  (use-package projectile
    :custom
    (projectile-sort-order 'recentf)
    (projectile-project-root-functions '(projectile-root-bottom-up))

    :config
    (ha-leader
      "p"  '(:ignore t :which-key "projects")
      "p W" '("initialize workspace" . ha-workspace-initialize)
      "p n" '("new project space" . ha-project-persp)
      "p !" '("run cmd in project root" . projectile-run-shell-command-in-root)
      "p &" '("async cmd in project root" . projectile-run-async-shell-command-in-root)
      "p a" '("add new project" . projectile-add-known-project)
      "p b" '("switch to project buffer" . projectile-switch-to-buffer)
      "p C" '("compile in project" . projectile-compile-project)
      "p c" '("recompile" . recompile)
      "p d" '("remove known project" . projectile-remove-known-project)
      "p e" '("edit project .dir-locals" . projectile-edit-dir-locals)
      "p f" '("find file in project" . projectile-find-file)
      "p g" '("configure project" . projectile-configure-project)
      "p i" '("invalidate project cache" . projectile-invalidate-cache)
      "p k" '("kill project buffers" . projectile-kill-buffers)
      "p o" '("find other file" . projectile-find-other-file)
      "p p" '("switch project" . projectile-switch-project)
      "p r" '("find recent project files" . projectile-recentf)
      "p R" '("run project" . projectile-run-project)
      "p S" '("save project files" . projectile-save-project-buffers)
      "p T" '("test project" . projectile-test-project)))

Workspaces

A workspace (at least to me) requires a quick jump to a collection of buffer windows organized around a project or task. For this, I'm basing my work on the perspective.el project.

I build a Hydra to dynamically list the current projects as well as select the project. To do this, we need a way to generate a string of the perspectives in alphabetical order:

  (defun ha--persp-label (num names)
    "Return string of numbered elements. NUM is the starting
  number and NAMES is a list of strings."
    (when names
      (concat
       (format "  %d: %s%s"    ; Shame that the following doesn't work:
               num             ; (propertize (number-to-string num) :foreground "#00a0")
               (car names)     ; Nor does surrounding the number with underbars.
               (if (equal (car names) (projectile-project-name)) "*" ""))
       (ha--persp-label (1+ num) (cdr names)))))

  (defun ha-persp-labels ()
    "Return a string of numbered elements from a list of names."
    (ha--persp-label 1 (sort (hash-table-keys (perspectives-hash)) 's-less?)))

Build the hydra as well as configure the perspective project.

  (use-package perspective
    :custom
    (persp-modestring-short t)
    (persp-show-modestring t)

    :config
    (setq persp-suppress-no-prefix-key-warning t)

    (persp-mode +1)

    (defhydra hydra-workspace-leader (:color blue :hint nil) "
    Workspaces- %s(ha-persp-labels)
    _n_: new project  _r_: rename    _a_: add buffer     _l_: load worksp
    _]_: next worksp  _d_: delete    _b_: goto buffer    _s_: save worksp
    _[_: previous     _W_: init all  _k_: remove buffer  _`_: to last worksp "
      ("TAB" persp-switch-quick)
      ("RET" persp-switch)
      ("`" persp-switch-last)
      ("1" (persp-switch-by-number 1))
      ("2" (persp-switch-by-number 2))
      ("3" (persp-switch-by-number 3))
      ("4" (persp-switch-by-number 4))
      ("5" (persp-switch-by-number 5))
      ("6" (persp-switch-by-number 6))
      ("7" (persp-switch-by-number 7))
      ("8" (persp-switch-by-number 8))
      ("9" (persp-switch-by-number 9))
      ("0" (persp-switch-by-number 0))
      ("n" ha-project-persp)
      ("N" ha-new-persp)
      ("]" persp-next :color pink)
      ("[" persp-prev :color pink)
      ("d" persp-kill)
      ("W" ha-workspace-initialize)
      ("a" persp-add-buffer)
      ("b" persp-switch-to-buffer)
      ("k" persp-remove-buffer)
      ("K" persp-kill-buffer)
      ("m" persp-merge)
      ("u" persp-unmerge)
      ("i" persp-import)
      ("r" persp-rename)
      ("s" persp-state-save)
      ("l" persp-state-load)
      ("w" ha-switch-to-special)  ; The most special perspective
      ("q" nil)
      ("C-g" nil)))

I have no idea why this binding doesnt work within the use-package declaration, but oh well…

  (ha-leader "TAB" '("workspaces" . hydra-workspace-leader/body))

The special perspective is a nice shortcut to the one I use the most:

  (defun ha-switch-to-special ()
    "Change to the projects perspective."
    (interactive)
    (persp-switch "projects"))

Predefined Workspaces

Let's describe a list of startup project workspaces. This way, I don't need the clutter of the recent state, but also get back to a state of mental normality. Granted, this list is essentially a list of projects that I'm currently developing, so I expect this to change often.

  (defvar ha-workspace-projects-personal nil "List of default projects with a name.")

  (add-to-list 'ha-workspace-projects-personal
               '("projects" "~/projects" ("breathe.org" "tasks.org")))
  (add-to-list 'ha-workspace-projects-personal
               '("personal" "~/personal" ("general.org")))
  (add-to-list 'ha-workspace-projects-personal
               '("technical" "~/technical" ("ansible.org")))
  (add-to-list 'ha-workspace-projects-personal
               '("hamacs" "~/other/hamacs" ("README.org" "ha-config.org")))

Given a list of information about project-workspaces, can we create them all?

  (defun ha-persp-exists? (name)
    "Return non-nill if a perspective of NAME exists."
    (when (fboundp 'perspectives-hash)
      (seq-contains (hash-table-keys (perspectives-hash)) name)))

  (defun ha-workspace-initialize (&optional projects)
    "Precreate workspace projects from a PROJECTS list.
    Each entry in the list is a list containing:
      - name (as a string)
      - project root directory
      - a optional list of files to display"
    (interactive)
    (unless projects
      (setq projects ha-workspace-projects-personal))

    (dolist (project projects)
      (-let (((name root files) project))
        (unless (ha-persp-exists? name)
          (message "Creating workspace: %s (from %s)" name root)
          (ha-project-persp root name files))))
    (persp-switch "main"))

Often, but not always, I want a perspective based on an actual Git repository, e.g. a project. Projectile keeps state of a "project" based on the current file loaded, so we combine the two projects by first choosing from a list of known projects and then creating a perspective based on the name. To pin the perspective to a project, we load a file from it, e.g. Like a README or something.

  (defun ha-project-persp (project &optional name files)
    "Create a new perspective, and then switch to the PROJECT using projectile.
  If NAME is not given, then figure it out based on the name of the
  PROJECT. If FILES aren't specified, then see if there is a
  README. Otherwise, pull up Dired."
    (interactive (list (projectile-completing-read "Project: " projectile-known-projects)))
    (when (f-directory-p project)
      (unless name
        (setq name (f-filename project)))
      (persp-switch name)

      ;; Unclear if the following is actually necessary.
      (ignore-errors
        (projectile-add-known-project root)
        (let ((projectile-switch-project-action nil))
          (projectile-switch-project-by-name root)))

      ;; To pin a project in projectile to the perspective, we need to load a file
      ;; from that project. The README will do, or at least, the dired of it.
      (let ((recent-files (thread-last recentf-list
                                       (--filter (s-starts-with? project it))
                                       (-take 3)))
            (readme-org (f-join project "README.org"))
            (readme-md  (f-join project "README.md")))
        (cond
         (files                  (ha--project-show-files project files))
         (recent-files           (ha--project-show-files project recent-files))
         ((f-exists? readme-org) (find-file readme-org))
         ((f-exists? readme-md)  (find-file readme-md))
         (t                      (dirvish project))))))

When starting a new perspective, and I specify more than one file, this function splits the window horizontally for each file.

  (defun ha--project-show-files (root files)
    "Display a list of FILES in a project ROOT directory.
  Each file gets its own window (so don't make the list of files
  long)."
    (when files
      (let ((default-directory root)
            (file (car files))
            (more (cdr files)))
        (message "Loading files from %s ... %s and %s" root file more)
        (when (f-exists? file)
          (find-file file))
        (when more
          (split-window-horizontally)
          (ha--project-show-files root more)))))

The persp-switch allows me to select or create a new project, but what if we insisted on a new workspace?

  (defun ha-new-persp (name)
    (interactive "sNew Workspace: ")
    (persp-switch name)
    (cond
     ((s-ends-with? "mail" name) (notmuch))
     ((s-starts-with? "twit" name) (twit))))

Once we create the new perspective workspace, if it matches a particular name, I pretty much know what function I would like to call.

Applications

Can we call these applications?

Magit

Can not live without Magit, a Git porcelain for Emacs. I stole the bulk of this work from Doom Emacs.

  (use-package magit
    ;; See https://github.com/magit/magit/wiki/Emacsclient for why we need to set:
    :custom (with-editor-emacsclient-executable "/usr/local/bin/emacsclient")

    :config
    ;; The following code re-instates my General Leader key in Magit.
    (general-unbind magit-mode-map "SPC")

    (ha-leader
      "g" '(:ignore t :which-key "git")
      "g /" '("Magit dispatch"             . magit-dispatch)
      "g ." '("Magit file dispatch"        . magit-file-dispatch)
      "g b" '("Magit switch branch"        . magit-branch-checkout)
      "g u" '("Git Update"                 . vc-update)

      "g g" '("Magit status"               . magit-status)
      "g s" '("Magit status here"          . magit-status-here)
      "g D" '("Magit file delete"          . magit-file-delete)
      "g B" '("Magit blame"                . magit-blame-addition)
      "g C" '("Magit clone"                . magit-clone)
      "g F" '("Magit fetch"                . magit-fetch)
      "g L" '("Magit buffer log"           . magit-log-buffer-file)
      "g R" '("Revert file"                . magit-file-checkout)
      "g S" '("Git stage file"             . magit-stage-file)
      "g U" '("Git unstage file"           . magit-unstage-file)

      "g f" '(:ignore t :which-key "find")
      "g f f"  '("Find file"               . magit-find-file)
      "g f g"  '("Find gitconfig file"     . magit-find-git-config-file)
      "g f c"  '("Find commit"             . magit-show-commit)

      "g l" '(:ignore t :which-key "list")
      "g l r" '("List repositories"        . magit-list-repositories)
      "g l s" '("List submodules"          . magit-list-submodules)

      "g o" '(:ignore t :which-key "open")

      "g c" '(:ignore t :which-key "create")
      "g c R" '("Initialize repo"          . magit-init)
      "g c C" '("Clone repo"               . magit-clone)
      "g c c" '("Commit"                   . magit-commit-create)
      "g c f" '("Fixup"                    . magit-commit-fixup)
      "g c b" '("Branch"                   . magit-branch-and-checkout)))

Git Gutter

The git-gutter-fringe project displays markings in the fringe (extreme left margin) to show modified and uncommitted lines. This project builds on git-gutter project to provide movement between hunks:

  (use-package git-gutter-fringe
    :custom
    ;; To have both flymake and git-gutter work, we put
    ;; git-gutter on the right side:
    (git-gutter-fr:side 'right-fringe)
    (left-fringe-width 15)
    (right-fringe-width 10)

    :config
    (set-face-foreground 'git-gutter-fr:modified "yellow")
    (set-face-foreground 'git-gutter-fr:added    "green")
    (set-face-foreground 'git-gutter-fr:deleted  "red")

    (global-git-gutter-mode)

    (ha-leader
      "g n" '("next hunk"     . git-gutter:next-hunk)
      "g p" '("previous hunk" . git-gutter:previous-hunk)
      "g e" '("end of hunk"   . git-gutter:end-of-hunk)
      "g r" '("revert hunk"   . git-gutter:revert-hunk)
      "g s" '("stage hunk"    . git-gutter:stage-hunk)))

Git Delta

The magit-delta project uses git-delta for colorized diffs.

  (use-package magit-delta
    :ensure t
    :hook (magit-mode . magit-delta-mode))

I also need to append the following to my ~/.gitconfig file:

  [delta]
  minus-style                   = normal "#8f0001"
  minus-non-emph-style          = normal "#8f0001"
  minus-emph-style              = normal bold "#d01011"
  minus-empty-line-marker-style = normal "#8f0001"
  zero-style                    = syntax
  plus-style                    = syntax "#006800"
  plus-non-emph-style           = syntax "#006800"
  plus-emph-style               = syntax "#009000"
  plus-empty-line-marker-style  = normal "#006800"

Git with Difftastic

Im stealing the code for this section from this essay by Tassilo Horn, and in fact, Im going to lift a lot of his explanation too, as I may need to remind myself how this works. The idea is based on using Wilfreds excellent difftastic tool to do a structural/syntax comparison of code changes in git. To begin, install the binary:

  brew install difftastic # and the equivalent on Linux

Next, we can do this, to use this as a diff tool for everything.

  (setenv "GIT_EXTERNAL_DIFF" "difft")

But perhaps integrating it into Magit and selectively calling it (as it is slow). Tassilo suggests making the call to difft optional by first creating a helper function to set the GIT_EXTERNAL_DIFF to difft:

  (defun th/magit--with-difftastic (buffer command)
    "Run COMMAND with GIT_EXTERNAL_DIFF=difft then show result in BUFFER."
    (let ((process-environment
           (cons (concat "GIT_EXTERNAL_DIFF=difft --width="
                         (number-to-string (frame-width)))
                 process-environment)))
      ;; Clear the result buffer (we might regenerate a diff, e.g., for
      ;; the current changes in our working directory).
      (with-current-buffer buffer
        (setq buffer-read-only nil)
        (erase-buffer))
      ;; Now spawn a process calling the git COMMAND.
      (make-process
       :name (buffer-name buffer)
       :buffer buffer
       :command command
       ;; Don't query for running processes when emacs is quit.
       :noquery t
       ;; Show the result buffer once the process has finished.
       :sentinel (lambda (proc event)
                   (when (eq (process-status proc) 'exit)
                     (with-current-buffer (process-buffer proc)
                       (goto-char (point-min))
                       (ansi-color-apply-on-region (point-min) (point-max))
                       (setq buffer-read-only t)
                       (view-mode)
                       (end-of-line)
                       ;; difftastic diffs are usually 2-column side-by-side,
                       ;; so ensure our window is wide enough.
                       (let ((width (current-column)))
                         (while (zerop (forward-line 1))
                           (end-of-line)
                           (setq width (max (current-column) width)))
                         ;; Add column size of fringes
                         (setq width (+ width
                                        (fringe-columns 'left)
                                        (fringe-columns 'right)))
                         (goto-char (point-min))
                         (pop-to-buffer
                          (current-buffer)
                          `(;; If the buffer is that wide that splitting the frame in
                            ;; two side-by-side windows would result in less than
                            ;; 80 columns left, ensure it's shown at the bottom.
                            ,(when (> 80 (- (frame-width) width))
                               #'display-buffer-at-bottom)
                            (window-width . ,(min width (frame-width))))))))))))

The crucial parts of this helper function are that we "wash" the result using ansi-color-apply-on-region so that the function can transform the difftastic highlighting using shell escape codes to Emacs faces. Also, note the need to possibly change the width, as difftastic makes a side-by-side comparison.

The functions below depend on magit-thing-at-point, and this depends on the compat library, so lets grab that stuff:

  (use-package compat
    :straight (:host github :repo "emacs-straight/compat"))

  (use-package magit-section
    :commands magit-thing-at-point)

Next, let's define our first command basically doing a git show for some revision which defaults to the commit or branch at point or queries the user if there's none.

  (defun th/magit-show-with-difftastic (rev)
    "Show the result of \"git show REV\" with GIT_EXTERNAL_DIFF=difft."
    (interactive
     (list (or
            ;; Use if given the REV variable:
            (when (boundp 'rev) rev)
            ;; If not invoked with prefix arg, try to guess the REV from
            ;; point's position.
            (and (not current-prefix-arg)
                 (or (magit-thing-at-point 'git-revision t)
                     (magit-branch-or-commit-at-point)))
            ;; Otherwise, query the user.
            (magit-read-branch-or-commit "Revision"))))
    (if (not rev)
        (error "No revision specified")
      (th/magit--with-difftastic
       (get-buffer-create (concat "*git show difftastic " rev "*"))
       (list "git" "--no-pager" "show" "--ext-diff" rev))))

And here the second command which basically does a git diff. It tries to guess what one wants to diff, e.g., when point is on the Staged changes section in a magit buffer, it will run git diff --cached to show a diff of all staged changes. If it can not guess the context, it'll query the user for a range or commit for diffing.

  (defun th/magit-diff-with-difftastic (arg)
    "Show the result of \"git diff ARG\" with GIT_EXTERNAL_DIFF=difft."
    (interactive
     (list (or
            ;; Use If RANGE is given, just use it.
            (when (boundp 'range) range)
            ;; If prefix arg is given, query the user.
            (and current-prefix-arg
                 (magit-diff-read-range-or-commit "Range"))
            ;; Otherwise, auto-guess based on position of point, e.g., based on
            ;; if we are in the Staged or Unstaged section.
            (pcase (magit-diff--dwim)
              ('unmerged (error "unmerged is not yet implemented"))
              ('unstaged nil)
              ('staged "--cached")
              (`(stash . ,value) (error "stash is not yet implemented"))
              (`(commit . ,value) (format "%s^..%s" value value))
              ((and range (pred stringp)) range)
              (_ (magit-diff-read-range-or-commit "Range/Commit"))))))
    (let ((name (concat "*git diff difftastic"
                        (if arg (concat " " arg) "")
                        "*")))
      (th/magit--with-difftastic
       (get-buffer-create name)
       `("git" "--no-pager" "diff" "--ext-diff" ,@(when arg (list arg))))))

What's left is integrating the new show and diff commands in Magit. For that purpose, Tasillo created a new transient prefix for all personal commands. Intriguing, but I have a hack that I can use on a leader:

  (defun ha-difftastic-here ()
    (interactive)
    (call-interactively
     (if (eq major-mode 'magit-log-mode)
         'th/magit-show-with-difftastic
       'th/magit-diff-with-difftastic)))

  (ha-leader "g d" '("difftastic" . ha-difftastic-here))

Time Machine

The git-timemachine project visually shows how a code file changes with each iteration:

  (use-package git-timemachine
    :config
    (ha-leader "g t" '("git timemachine" . git-timemachine)))

Gist

Using the gist package to write code snippets on Github seems like it can be useful, but I'm not sure how often.

  (use-package gist
    :config
    (ha-leader
      "g G" '(:ignore t :which-key "gists")
      "g l g" '("gists"          . gist-list)
      "g G l" '("list"           . gist-list)                     ; Lists your gists in a new buffer.
      "g G r" '("region"         . gist-region)                   ; Copies Gist URL into the kill ring.
      "g G R" '("private region" . gist-region-private)           ; Explicitly create a private gist.
      "g G b" '("buffer"         . gist-buffer)                   ; Copies Gist URL into the kill ring.
      "g G B" '("private buffer" . gist-buffer-private)           ; Explicitly create a private gist.
      "g c g" '("gist"           . gist-region-or-buffer)         ; Post either the current region, or buffer
      "g c G" '("private gist"   . gist-region-or-buffer-private))) ; create private gist from region or buffer

The gist project depends on the gh library. There seems to be a problem with it.

  (use-package gh
    :straight (:host github :repo "sigma/gh.el"))

Forge

Let's extend Magit with Magit Forge for working with Github and Gitlab:

  (use-package forge
    :after magit
    :config
    (ha-leader
      "g '"   '("Forge dispatch"           . forge-dispatch)
      "g f i" '("Find issue"               . forge-visit-issue)
      "g f p" '("Find pull request"        . forge-visit-pullreq)

      "g l i" '("List issues"              . forge-list-issues)
      "g l p" '("List pull requests"       . forge-list-pullreqs)
      "g l n" '("List notifications"       . forge-list-notifications)

      "g o r" '("Browse remote"            . forge-browse-remote)
      "g o c" '("Browse commit"            . forge-browse-commit)
      "g o i" '("Browse an issue"          . forge-browse-issue)
      "g o p" '("Browse a pull request"    . forge-browse-pullreq)
      "g o i" '("Browse issues"            . forge-browse-issues)
      "g o P" '("Browse pull requests"     . forge-browse-pullreqs)

      "g c i" '("Issue"                    . forge-create-issue)
      "g c p" '("Pull request"             . forge-create-pullreq)))

Every so often, pop over to the following URLs and generate a new token where the Note is forge, and then copy that into the ~/.authinfo.gpg:

  (ghub-request "GET" "/user" nil
                :forge 'github
                :host "api.github.com"
                :username "howardabrams"
                :auth 'forge)

Pushing is Bad

Pushing directly to the upstream branch is bad form, as one should create a pull request, etc. To prevent an accidental push, we double-check first:

  (define-advice magit-push-current-to-upstream (:before (args) query-yes-or-no)
    "Prompt for confirmation before permitting a push to upstream."
    (when-let ((branch (magit-get-current-branch)))
      (unless (yes-or-no-p (format "Push %s branch upstream to %s? "
                                   branch
                                   (or (magit-get-upstream-branch branch)
                                       (magit-get "branch" branch "remote"))))
        (user-error "Push to upstream aborted by user"))))

Web Browsing

EWW

Web pages look pretty good with EWW, but I'm having difficulty getting it to render a web search from DuckDuck.

  (use-package eww
    :init
    (setq browse-url-browser-function 'eww-browse-url
          browse-url-secondary-browser-function 'browse-url-default-browser
          eww-browse-url-new-window-is-tab nil
          shr-use-colors nil
          shr-use-fonts t     ; I go back and forth on this one
          ;; shr-discard-aria-hidden t
          shr-bullet "• "
          shr-inhibit-images nil  ; Gotta see the images?
          ;; shr-blocked-images '(svg)
          ;; shr-folding-mode nil
          url-privacy-level '(email))

    :config
    (ha-leader "a b" '("eww browser" . eww))

    :general
    (:states 'normal :keymaps 'eww-mode-map
             "B" 'eww-list-bookmarks
             "Y" 'eww-copy-page-url
             "H" 'eww-back-url
             "L" 'eww-forward-url
             "u" 'eww-top-url
             "p" 'eww-previous-url
             "n" 'eww-next-url
             "q" 'bury-buffer)
    (:states 'normal :keymaps 'eww-buffers-mode-map
             "q" 'bury-buffer))

This function allows Imenu to offer HTML headings in EWW buffers, helpful for navigating long, technical documents.

  (use-package eww
    :config
    (defun unpackaged/imenu-eww-headings ()
      "Return alist of HTML headings in current EWW buffer for Imenu.
  Suitable for `imenu-create-index-function'."
      (let ((faces '(shr-h1 shr-h2 shr-h3 shr-h4 shr-h5 shr-h6 shr-heading)))
        (save-excursion
          (save-restriction
            (widen)
            (goto-char (point-min))
            (cl-loop for next-pos = (next-single-property-change (point) 'face)
                     while next-pos
                     do (goto-char next-pos)
                     for face = (get-text-property (point) 'face)
                     when (cl-typecase face
                            (list (cl-intersection face faces))
                            (symbol (member face faces)))
                     collect (cons (buffer-substring (point-at-bol) (point-at-eol)) (point))
                     and do (forward-line 1))))))
    :hook (eww-mode .
                    (lambda ()
                      (setq-local imenu-create-index-function #'unpackaged/imenu-eww-headings))))

Get Pocket

The pocket-reader project connects to the Get Pocket service.

  (use-package pocket-reader
    :init
    (setq org-web-tools-pandoc-sleep-time 1)
    :config
    (ha-leader "o p" '("get pocket" . pocket-reader))

    ;; Instead of jumping into Emacs mode to get the `pocket-mode-map',
    ;; we add the keybindings to the normal mode that makes sense.
    :general
    (:states 'normal :keymaps 'pocket-reader-mode-map
             "RET" 'pocket-reader-open-url
             "TAB" 'pocket-reader-pop-to-url

             "*" 'pocket-reader-toggle-favorite
             "B" 'pocket-reader-open-in-external-browser
             "D" 'pocket-reader-delete
             "E" 'pocket-reader-excerpt-all
             "F" 'pocket-reader-show-unread-favorites
             "M" 'pocket-reader-mark-all
             "R" 'pocket-reader-random-item
             "S" 'tabulated-list-sort
             "a" 'pocket-reader-toggle-archived
             "c" 'pocket-reader-copy-url
             "d" 'pocket-reader
             "e" 'pocket-reader-excerpt
             "f" 'pocket-reader-toggle-favorite
             "l" 'pocket-reader-limit
             "m" 'pocket-reader-toggle-mark
             "o" 'pocket-reader-more
             "q" 'quit-window
             "s" 'pocket-reader-search
             "u" 'pocket-reader-unmark-all
             "t a" 'pocket-reader-add-tags
             "t r" 'pocket-reader-remove-tags
             "t s" 'pocket-reader-tag-search
             "t t" 'pocket-reader-set-tags

             "g s" 'pocket-reader-resort
             "g r" 'pocket-reader-refresh))

Use these special keywords when searching:

  • :*, :favorite Return favorited items.
  • :archive Return archived items.
  • :unread Return unread items (default).
  • :all Return all items.
  • :COUNT Return at most COUNT (a number) items. This limit persists until you start a new search.
  • :t:TAG, t:TAG Return items with TAG (you can search for one tag at a time, a limitation of the Pocket API).

External Browsing

Browsing on a work laptop is a bit different. According to this page, I can set a default browser for different URLs, which is great, as I can launch my browser for personal browsing, or another browser for work access, or even EWW. To make this clear, I'm using the abstraction associated with osx-browse:

  (use-package osx-browse
    :init
    (setq browse-url-handlers
          '(("docs\\.google\\.com" . osx-browse-url-personal)
            ("grafana.com"         . osx-browse-url-personal)
            ("dndbeyond.com"       . osx-browse-url-personal)
            ("tabletopaudio.com"   . osx-browse-url-personal)
            ("youtu.be"            . osx-browse-url-personal)
            ("youtube.com"         . osx-browse-url-personal)
            ("."                   . eww-browse-url)))

    :config
    (defun osx-browse-url-personal (url &optional new-window browser focus)
      "Open URL in Firefox for my personal surfing.
  The parameters, URL, NEW-WINDOW, and FOCUS are as documented in
  the function, `osx-browse-url'."
      (interactive (osx-browse-interactive-form))
      (cl-callf or browser "org.mozilla.Firefox")
      (osx-browse-url url new-window browser focus)))

Dirvish

The dirvish project aims to be a better dired. And since the major-mode is still dired-mode, the decades of finger memory isnt lost. For people starting to use dired, most commands are pretty straight-forward (and Prot did a pretty good introduction to it), but to remind myself, keep in mind:

%
will mark a bunch of files based on a regular expression
m
marks a single file
d
marks a file to delete, type x to follow-through on all files marked for deletion.
u
un-mark a file, or type ! to un-mark all
t
to toggle the marked files. Keep files with xyz extension? Mark those with %, and then t toggle.
C
copy the current file or all marked files
R
rename/move the current file or all marked files
M
change the mode (chmod) of current or marked files, accepts symbols, like a+x

Note that dired has two marks … one is a general mark, and the other is specifically a mark of files to delete.

Dirvish does require the following supporting programs, but Ive already got those puppies installed:

  brew install coreutils fd poppler ffmpegthumbnailer mediainfo imagemagick

Im beginning with dirvish to use the sample configuration and change it:

  (use-package dirvish
    :straight (:host github :repo "alexluigit/dirvish")
    :init
    (dirvish-override-dired-mode)

    :custom
    (dirvish-quick-access-entries
     '(("h" "~/"           "Home")
       ("p" "~/personal"   "Personal")
       ("p" "~/projects"   "Projects")
       ("t" "~/technical"  "Technical")
       ("w" "~/website"    "Website")
       ("d" "~/Downloads/" "Downloads")))

    :config
    ;; This setting is like `treemacs-follow-mode' where the buffer
    ;; changes based on the current file. Not sure if I want this:
    ;; (dirvish-side-follow-mode)

    (setq dirvish-mode-line-format
          '(:left (sort symlink) :right (omit yank index)))
    (setq dirvish-attributes
          '(all-the-icons file-time file-size collapse subtree-state vc-state git-msg))
    (setq delete-by-moving-to-trash t)

    ;; With `ls' as an alias, and `gls' available on _some_ of my systems, I dont:
    ;;       (setq insert-directory-program "gls")
    ;; And instead use Emacs' built-in directory lister:
    (setq insert-directory-program nil)
    (setq ls-lisp-use-insert-directory-program nil)
    (require 'ls-lisp)
    (setq dired-listing-switches
          "-l --almost-all --human-readable --group-directories-first --no-group")

    (set-face-attribute 'dirvish-hl-line nil :background "darkmagenta"))

While in dirvish-mode, we can rebind some keys:

  (use-package dirvish
    :bind
    (:map dirvish-mode-map ; Dirvish inherits `dired-mode-map'
     ("a"   . dirvish-quick-access)
     ("f"   . dirvish-file-info-menu)
     ("y"   . dirvish-yank-menu)
     ("N"   . dirvish-narrow)
     ("^"   . dirvish-history-last)
     ("h"   . dirvish-history-jump) ; remapped `describe-mode'
     ("q"   . dirvish-quit)
     ("s"   . dirvish-quicksort)    ; remapped `dired-sort-toggle-or-edit'
     ("v"   . dirvish-vc-menu)      ; remapped `dired-view-file'
     ("TAB" . dirvish-subtree-toggle)
     ("M-f" . dirvish-history-go-forward)
     ("M-b" . dirvish-history-go-backward)
     ("M-l" . dirvish-ls-switches-menu)
     ("M-m" . dirvish-mark-menu)
     ("M-t" . dirvish-layout-toggle)
     ("M-s" . dirvish-setup-menu)
     ("M-e" . dirvish-emerge-menu)
     ("M-j" . dirvish-fd-jump)))

Annotations

Let's try annotate-mode, which allows you to drop "notes" and then move to them (yes, serious overlap with bookmarks, which we will return to).

  (use-package annotate
    :config
    (ha-leader
      "t A" '("annotations" . annotate-mode)

      "n"   '(:ignore t :which-key "notes")
      "n a" '("toggle mode" . annotate-mode)
      "n n" '("annotate"    . annotate-annotate)
      "n d" '("delete"      . annotate-delete)
      "n s" '("summary"     . annotate-show-annotation-summary)
      "n j" '("next"        . annotate-goto-next-annotation)
      "n k" '("prev"        . annotate-goto-previous-annotation)

      ;; If a shift binding isn't set, it defaults to non-shift version
      ;; Use SPC N N to jump to the next error:
      "n N" '("next error"  . flycheck-next-error)))

Keep the annotations simple, almost tag-like, and then the summary allows you to display them.

Keepass

Use the keepass-mode to view a read-only version of my Keepass file in Emacs:

  (use-package keepass-mode)

When having your point on a key entry, you can copy fields to kill-ring using:

u
URL
b
user name
c
password

Demo It

Making demonstrations within Emacs with my demo-it project. While on MELPA, I want to use my own cloned version to make sure I can keep debugging it.

  (use-package demo-it
    :straight (:local-repo "~/other/demo-it")
    ;; :straight (:host github :repo "howardabrams/demo-it")
    :commands (demo-it-create demo-it-start))

PDF Viewing

Why not view PDF files better? To do this, first install the following on a Mac:

  brew install poppler automake

Instead run pdf-tools-install, as this command will do the above for the system.

Lets install the Emacs connection to the pdfinfo program:

  (use-package pdf-tools
    :mode ("\\.pdf\\'" . pdf-view-mode)
    :init
    (setq pdf-info-epdfinfo-program "/usr/local/bin/epdfinfo")
    :general
    (:states 'normal :keymaps 'pdf-view-mode-map
             "gp" 'pdf-view-goto-page
             ">"  'doc-view-fit-window-to-page))

Make sure the pdf-info-check-epdfinfo function works.