Applications

Table of Contents

A literate programming file configuring critical applications.

Can we call the following applications? I guess.

Git and 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 "emacsclient")

  :config
  ;; See https://takeonrules.com/2024/03/01/quality-of-life-improvement-for-entering-and-exiting-magit/
  (setq magit-display-buffer-function
      #'magit-display-buffer-fullframe-status-v1)
  (setq magit-bury-buffer-function
      #'magit-restore-window-configuration)

  ;; 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)

    "g <escape>" '(keyboard-escape-quit :which-key t)
    "g C-g" '(keyboard-escape-quit :which-key t))

  (general-nmap "<escape>" #'transient-quit-one))

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))

This requires installing an executable. For instance, on my Mac:

brew install git-delta

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

I’m stealing the code for this section from this essay by Tassilo Horn, and in fact, I’m 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 Wilfred’s 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 let’s 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"))))

Github Search?

Wanna see an example of how other’s use a particular function?

(defun my-github-search(&optional search)
  (interactive (list (read-string "Search: " (thing-at-point 'symbol))))
  (let* ((language (cond ((eq major-mode 'python-mode) "Python")
                         ((eq major-mode 'emacs-lisp-mode) "Emacs Lisp")
                         ((eq major-mode 'yaml-mode) "Ansible")
                         (t "Text")))
         (url (format "https://github.com/search/?q=\"%s\"+language:\"%s\"&type=Code" (url-hexify-string search)
                      language)))
    (browse-url url)))

ediff

Love me ediff, but with monitors that are wider than they are tall, let’s put the diffs side-by-side:

(setq ediff-split-window-function 'split-window-horizontally)

Frames, er, windows, are actually annoying for me, as Emacs is always in full-screen mode.

(setq ediff-window-setup-function 'ediff-setup-windows-plain)

When ediff is finished, it leaves the windows borked. This is annoying, but according to this essay, we can fix it:

(defvar my-ediff-last-windows nil
  "Session for storing window configuration before calling `ediff'.")

(defun my-store-pre-ediff-winconfig ()
  "Store `current-window-configuration' in variable `my-ediff-last-windows'."
  (setq my-ediff-last-windows (current-window-configuration)))

(defun my-restore-pre-ediff-winconfig ()
  "Restore window configuration to stored value in `my-ediff-last-windows'."
  (set-window-configuration my-ediff-last-windows))

(add-hook 'ediff-before-setup-hook #'my-store-pre-ediff-winconfig)
(add-hook 'ediff-quit-hook #'my-restore-pre-ediff-winconfig)

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))

  (defun ha-eww-save-off-window (name)
    (interactive (list (read-string "Name: " (plist-get eww-data :title))))
    (rename-buffer (format "*eww: %s*" name) t))

  (defun ha-eww-better-scroll (prefix)
    (interactive "^p")
    (forward-paragraph prefix)
    ;; (recenter) ... if you want the cursor in the center,
    ;; otherwise, this puts the paragraph at the top of window:
    (recenter-top-bottom 0))

  (major-mode-hydra-define eww-mode nil
    ("Browser"
     (("G" eww-browse "Browse")
      ("B" eww-list-bookmarks "Bookmarks")
      ("q" bury-buffer "Quit"))
     "History"
     (("l" eww-back-url     "Back" :color pink)
      ("r" eww-forward-url  "Forward" :color pink)
      ("H" eww-list-histories "History"))
     "Current Page"
     (("b" eww-add-bookmark "Bookmark")
      ("g" link-hint-open-link "Jump Link")
      ("d" eww-download "Download"))
     "Render Page"
     (("e" eww-browse-with-external-browser "Open in Firefox")
      ("R" eww-readable "Reader Mode")
      ("y" eww-copy-page-url "Copy URL"))
     "Navigation"
     (("u" eww-top-url "Site Top")
      ("n" eww-next-url "Next Page" :color pink)
      ("p" eww-previous-url "Previous" :color pink))
     "Toggles"
     (("c" eww-toggle-colors "Colors")
      ("i" eww-toggle-images "Images")
      ("f" eww-toggle-fonts "Fonts"))
     "Misc"
     (("s" ha-eww-save-off-window "Rename")
      ("S" eww-switch-to-buffer "Switch to")
      ("-" eww-write-bookmarks "Save Bookmarks")
      ("M" eww-read-bookmarks "Load Bookmarks"))))

  :general
  (:states 'normal :keymaps 'eww-mode-map
           "q" 'bury-buffer
           "J" 'ha-eww-better-scroll)
  (: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))))

SHRFace

Make my EWW browsers look like an Org file with the shrface project.

(use-package shrface
  :straight (:host github :repo "chenyanming/shrface")
  :config
  (shrface-basic)
  ;; (shrface-trial)
  ;; (shrface-default-keybindings) ; setup default keybindings
  (setq shrface-href-versatile t)

  (major-mode-hydra-define+ eww-mode nil
    ("Headlines"
     (("j" shrface-next-headline "Next Heading" :color pink)
      ("k" shrface-previous-headline "Previous" :color pink)
      ("J" shrface-headline-consult "Goto Heading")))))

The following connection to EWW throws errors now. Hrm.

(use-package eww
  :after shrface
  :hook (eww-after-render #'shrface-mode))

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)))

Dired

Allow me a confession. When renaming a file or flipping an executable bit, I don’t pull up dired as a first thought. But I feel like I should, as can do a lot of things quicker than pulling up a shell. Especially when working with multiple files. Most commands are somewhat straight-forward (and Prot did a pretty good introduction to it), but to remind myself, keep in mind it has two actions … mark one or more files to do something, or flag one or more files to delete them. Why two? Dunno. Especially since they act the same. For instance:

  1. Mark a few files with m, and then type D to delete them, or …
  2. Flag a few files with d, and then type x to delete them.

Seems the same to me. Especially since you can type u to unmark or unflag.

Few other commands to note:

m
marks a single file
%
will mark a bunch of files based on a regular expression
u
un-mark a file, or type U 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
D
delete 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

Couple useful settings:

(setq delete-by-moving-to-trash t
      dired-auto-revert-buffer t
      dired-vc-rename-file t)  ; Why not mention to git when renaming?

My ls is an often alias and GNU’s ls, labeled gls on my Mac, isn’t consistent between Mac and Linux, so I don’t do:

(setq insert-directory-program "gls")

Instead I use Emacs’ built-in directory lister (which accepts the standard, dired-listing-switches to customize the output):

(use-package ls-lisp
  :straight (:type built-in)
  :config
  (setq ls-lisp-use-insert-directory-program nil
        dired-listing-switches
        "-l --almost-all --human-readable --group-directories-first --no-group"))

And this article by Mickey Petersen convinced me to turn on the built-in dired-x (just have to tell straight that knowledge):

(use-package dired-x
  :straight (:type built-in))

The advantage of dired-x is the ability to have shell command guessing when selecting one or more files, and running a shell command on them with ! or &.

Dirvish

The dirvish project aims to make a prettier dired. And since the major-mode is still dired-mode, the decades of finger memory isn’t lost. Dirvish does require the following supporting programs, but I’ve already got those puppies installed:

brew install coreutils fd poppler ffmpegthumbnailer mediainfo imagemagick

I’m 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")
     ("e" "~/.emacs.d/" "Emacs user directory")
     ("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))

  (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'
   (","   . dirvish-dispatch)
   ("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)))

My Dired Interface

Because I can’t remember all the cool things dired can do, I put together a helper/cheatsheet. Typing , brings up a menu of possibilities (for others, I recommend Casual Dired):

(use-package major-mode-hydra
  :config
  (major-mode-hydra-define dired-mode (:quit-key "q")
    ("File"
     (("C" dired-do-copy "Copy")
      ("D" dired-do-delete "Delete")
      ("S" dired-do-symlink "Symlink")
      ("w" dired-copy-filename-as-kill "Copy name")
      ("!" dired-do-shell-command "Shell")
      ("&" dired-do-async-shell-command "Shell &"))   ; Really?
     "Change"
     (("R" dired-do-rename "Rename")
      ("M" dired-do-chmod "Mode")
      ("O" dired-do-chown "Owner")
      ("G" dired-do-chgrp "Group")
      ("T" dired-do-touch "Mod time"))
     "Directory"
     (("+" dired-create-directory "New")
      ("i" dired-insert-subdir "Insert subdir" :color pink)
      ("I" dired-hide-subdir "Hide subdir" :color pink)
      ("g" revert-buffer  "Refresh" :color pink)
      ("E" wdired-change-to-wdired-mode "Edit (wdired)"))
     "Mark"
     (("m" dired-mark "Mark" :color pink)
      ("u" dired-unmark "Unmark" :color pink)
      ("U" dired-unmark-all-marks "Unmark all" :color pink)
      ("t" dired-toggle-marks "Toggle marks" :color pink)
      ("~" dired-flag-backup-files "Mark backups" :color pink)
      ("r" hydra-dired-regexp-mark/body "Regexp »"))
     "Navigation"
     (("^" dired-up-directory "Up Directory")
      ("j" dired-next-line "Next File" :color pink)
      ("k" dired-previous-line "Previous File" :color pink)
      ("J" dired-next-subdir "Next subdir" :color pink)
      ("K" dired-previous-subdir "Previous subdir" :color pink))
     "Misc"
     (("x" hydra-dired-utils/body "Utils »")
      ("o" hydra-dired-toggles/body "Toggles »")
      ("a" dirvish-quick-access "Quick Access"))))

  ;; And some more hydras for the sub-menus:
  (pretty-hydra-define hydra-dired-regexp-mark (:color blue :hint nil)
    ("Mark files with regexp..."
     (("m" dired-mark-files-regexp "matching filenames")
      ("g" dired-mark-files-containing-regexp "containing text")
      ("d" dired-flag-files-regexp "to delete")
      ("c" dired-do-copy-regexp "to copy")
      ("r" dired-do-rename-regexp "to rename"))))

  (pretty-hydra-define hydra-dired-toggles (:color blue)
    ("Dired Toggles"
     (("d" dired-hide-details-mode "File details")
      ("h" dired-do-kill-lines "Hide marked")
      ("o" dired-omit-mode "Hide (omit) some?")
      ("T" image-dired "Image thumbnails"))))

  (pretty-hydra-define hydra-dired-utils (:color blue :hint nil)
    ("Files"
     (("f" dired-do-find-marked-files "open marked")
      ("z" dired-do-compress "(un)compress marked"))
     "Rename"
     (("u" dired-upcase "upcase")
      ("d" dired-downcase "downcase"))
     "Search"
     (("g" dired-do-find-regexp "grep marked")
      ("s" dired-do-isearch "isearch marked")) ; Maybe C-s ... even on top?
     "Replace"
     (("r" dired-do-find-regexp-and-replace "find/replace marked")
      ("R" dired-do-query-replace-regexp "query find/replace")))))

Notice E to turn on wdired, which brings dired to a whole new level.

I do want to change a couple of bindings, as j to pull up a completing-read interface for files, and then move the cursor to the on selected (why not just search) and k for hiding marked files, aren’t very useful, compared to the finger memory I now have for using those two keys to move up and down lines.

(define-key dired-mode-map (kbd "j") 'evil-next-line)
(define-key dired-mode-map (kbd "k") 'evil-previous-line)
(define-key dired-mode-map (kbd "/") 'isearch-forward)
(define-key dired-mode-map (kbd "n") 'evil-search-next)
(define-key dired-mode-map (kbd ",") 'major-mode-hydras/dired-mode/body)

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

PDF Viewing

Why not view PDF files better? If you have standard build tools installed on your system, run pdf-tools-install, as this command will an epdfinfo program to PDF displays.

(use-package pdf-tools
  :mode ("\\.pdf\\'" . pdf-view-mode)
  :init
  (setq pdf-info-epdfinfo-program
        (if (file-exists-p "/opt/homebrew")
            "/opt/homebrew/bin/epdfinfo"
          "/usr/local/bin/epdfinfo")

        ;; Match my theme:
        pdf-view-midnight-colors '("#c5c8c6" . "#1d1f21"))

  :general
  (:states 'normal :keymaps 'pdf-view-mode-map
           ;; Since the keys don't make sense when reading:
           "J" 'pdf-view-scroll-up-or-next-page
           "K" 'pdf-view-scroll-down-or-previous-page
           "gp" 'pdf-view-goto-page
           ">"  'doc-view-fit-window-to-page))

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

The package adds the following keybindings:

z d
Dark mode … indispensable, see also z m
C-j / C-k
next and previous pages
j / k
up and down the page
h / l
scroll the page left and right
= / -
enlarge and shrink the page
o
Table of contents (if available)

I’d like write notes in org files that link to the PDFs (and maybe visa versa), using the org-noter package:

(use-package org-noter
  :config
  (major-mode-hydra-define org-noter-doc-mode-map nil
    ("Notes"
     (("i" org-noter-insert-note "insert note")
      ("s" org-noter-sync-current-note "sync note")
      ("n" org-noter-sync-next-note "next note" :color pink)
      ("p" org-noter-sync-prev-note "previous note" :color pink)))))

To use, open a header in an org doc, and run M-x org-noter (SPC o N) and select the PDF. The org-noter function can be called in the PDF doc as well. In Emacs state, type i to insert a note as a header, or in Normal state, type , i.