A literate programming file configuring critical applications.
Can we call the following applications? I guess.
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))
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)))
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"
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))
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)))
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"))
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 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"))))
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)))
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 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))))
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))
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).
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)))
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:
- Mark a few files with
m
, and then typeD
to delete them, or … - Flag a few files with
d
, and then typex
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 thent
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, likea+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 &
.
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)))
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)
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.
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
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 evil-collection 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
.
Let’s provide a name so that the file can be required: