Using the org-mac-link and some custom functions, I can quickly get information from external programs into my org files.
73 KiB
General Emacs Configuration
A literate programming file for configuring Emacs.
Basic Configuration
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)
As Phil Jackson mentioned, Emacs has a lot of 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"))))
Oh, and let’s see if I will use the recentf
feature more:
(recentf-mode 1)
Or leave them in the current directory, but create an alias so ls
doesn’t 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.
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)
As tec wrote, I want to use ~/.authsource.gpg
as I don’t 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))
More settings:
(setq truncate-string-ellipsis "…" ; Unicode ellispis are nicer than "..."
debug-on-error t)
The venerable hippie-expand function does a better job than the default, dabbrev-expand, so let’s swap it out (see this essay by Mickey Petersen):
(global-set-key [remap dabbrev-expand] 'hippie-expand)
Details? Check out its list of expanders.
Let’s bind TAB
instead of the default M-/
. By default, TAB
re-indents the line, but I find that I want that feature when I’m in Evil’s 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)
Now while we’re typing along, we can hit the TAB
key after partially typing a word to have it completed.
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)
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
Piper
Rewriting my shell scripts in Emacs Lisp uses my emacs-piper project, and this code spills into my configuration code, so let's load it now:
(use-package piper
:straight (:type git :protocol ssh :host gitlab :repo "howardabrams/emacs-piper")
:commands piper shell-command-to-list ; I use this function often
:bind (:map evil-normal-state-map
("C-M-|" . piper)
("C-|" . piper-user-interface)))
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.
Seems the best collection of snippets is what Henrik Lissner has made for Doom (otherwise, we should use yasnippet-snippets package):
(use-package doom-snippets
:after yasnippet
:straight (:type git :protocol ssh :host github :repo "doomemacs/snippets")
:config
(add-to-list 'yas-snippet-dirs (thread-last user-emacs-directory
(expand-file-name "straight")
(expand-file-name "repos")
(expand-file-name "doom-snippets"))))
Note: Including his snippets also includes some helper functions and other features.
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")
Request System
The above code needs the request package:
(use-package request
:init
(defvar ha-dad-joke nil "Holds the latest dad joke.")
:config
(defun ha-dad-joke ()
"Display a random dad joke."
(interactive)
(message (ha--dad-joke)))
(defun ha--dad-joke ()
"Return string containing a dad joke from www.icanhazdadjoke.com."
(setq ha-dad-joke nil) ; Clear out old joke
(ha--dad-joke-request)
(ha--dad-joke-wait))
(defun ha--dad-joke-wait ()
(while (not ha-dad-joke)
(sit-for 1))
(unless ha-dad-joke
(ha--dad-joke-wait))
ha-dad-joke)
(defun ha--dad-joke-request ()
(request "https://icanhazdadjoke.com"
:sync t
:complete (cl-function
(lambda (&key data &allow-other-keys)
(setq ha-dad-joke data))))))
Dad Jokes!
The critical part here, is the Dad Joke function, a curl
call to a web service:
curl -sH "Accept: text/plain" https://icanhazdadjoke.com/
For this, I use the request
package, which is asynchronous
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))
I dislike forgetting to trim trailing white-space:
(add-hook 'before-save-hook 'delete-trailing-whitespace)
I like being able to enable local variables in .dir-local.el
files:
(setq enable-local-variables t)
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, I’ll 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? Let’s override it:
(global-set-key (kbd "s-q") 'bury-buffer)
Oh, and let’s not close the frame, but instead, the window:
(global-set-key (kbd "s-w") 'delete-window)
Undo
I mean, I always use C-/
for undo (and C-?
for redo), but when I’m on the Mac, I need to cover my bases.
Why use undo-fu instead of the built-in undo functionality? Well, there isn’t much to the project (that’s a good thing), but It basically doesn’t 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:
- https://github.com/noctuid/evil-guide
- https://nathantypanski.com/blog/2014-08-03-a-vim-like-emacs-config.html
- Evil insert state is really Emacs? Real answer to that is to set evil-disable-insert-state-bindings
(use-package evil
:init
(setq evil-undo-system 'undo-fu
evil-want-fine-undo t ; Be more like Emacs
evil-disable-insert-state-bindings t
evil-want-keybinding nil
evil-want-integration t
evil-escape-key-sequence "jk"
evil-escape-unordered-key-sequence t)
:config
;; Now that `evil-disable-insert-state-bindings' works to use Emacs
;; keybindings in Evil's insert mode, we no longer need this code:
;; (setq evil-insert-state-map (make-sparse-keymap))
;; (define-key evil-insert-state-map (kbd "<escape>") 'evil-normal-state)
;; In insert mode, type C-o to execute a single Evil command:
(define-key evil-insert-state-map (kbd "C-o") 'evil-execute-in-normal-state)
(evil-mode))
While I’m 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.
-
xi,w
- changes a word that is snake or camel-cased, as in programming languages. The
x
can bec
to change,d
to delete,y
to copy, etc. -
xis
- changes a sentence, and if
i
isa
, it gets rid of the surrounding whitespace as well. Probablydas
andcis
. -
xip
- changes a paragraph.
- ?
- Surrounding punctuation, like quotes, parenthesis, brackets, etc. Also work, so
ci)
changes all the parameters to a function call.
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 "SPC m"
:global-prefix "<f17>"
:non-normal-prefix "S-SPC"))
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
"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")))
Perhaps my OCD is out-of-control, but I want to load a file in another window, but want to control which window.
(defmacro ha-create-find-file-window (winum)
(let ((func-name (intern (format "ha-find-file-window-%s" winum)))
(call-func (intern (format "winum-select-window-%s" winum))))
`(defun ,func-name ()
"Call `find-file' in the particular `winum' window."
(interactive)
(,call-func)
(call-interactively 'find-file))))
(dolist (winum (number-sequence 1 9))
(ha-create-find-file-window winum))
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 f" '("load" . find-file)
"f F" '("load new window" . find-file-other-window)
"f s" '("save" . save-buffer)
"f S" '("save as" . write-buffer)
"f SPC" '("project" . projectile-find-file)
"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" . dired)
"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))
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))))
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)
;; 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" '("auto-fill" . auto-fill-mode)
"t l" '("line numbers" . display-line-numbers-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))
Use the ace-window project to jump to any window you see:
(use-package ace-window)
This package, bound to SPC w w
, also allows operations specified before choosing the window:
x
- delete windowm
- swap windowsM
- move windowc
- copy windowj
- select buffern
- select the previous windowu
- select buffer in the other windowc
- split window, either vertically or horizontallyv
- split window verticallyb
- split window horizontallyo
- maximize current window?
- show these command bindings
Keep in mind, these shortcuts work with more than two windows open. For instance, SPC w w x 3
closes the "3" window.
To jump to a window even quicker, use the winum package:
(use-package winum
:config
(winum-mode +1))
And when creating new windows, why isn't the new window selected?
(defun jump-to-new-window (&rest _arg)
"Advice function to jump to newly spawned window."
(other-window 1))
(dolist (command '(split-window-below split-window-right
evil-window-split evil-window-vsplit))
(advice-add command :after #'jump-to-new-window))
This is nice since the window numbers are always present on a Doom modeline, but they order the window numbers differently than ace-window
. Let's see which I end up liking better.
The 0
key/window should be always associated with a project-specific tree window:
(add-to-list 'winum-assign-functions
(lambda ()
(when (string-match-p (buffer-name) ".*\\*NeoTree\\*.*") 10)))
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 _n_: new _^_: taller (t) _z_: Swap _+_: text larger
_c_: cycle _d_: delete _V_: shorter (T) _u_: undo _-_: text smaller
_j_: go up _=_: balance _>_: wider _U_: undo+ _F_: font larger
_k_: down _m_: maximize _<_: narrower _r_: redo _f_: font smaller
_h_: left _s_: h-split _e_: balanced _R_: redo+ _0_: toggle neotree
_l_: right _v_: v-split _o_: choose by number (also 1-9)
"
("w" ace-window)
("c" other-window)
("=" balance-windows)
("m" delete-other-windows)
("d" delete-window)
("D" ace-delete-window)
("z" ace-swap-window)
("u" winner-undo)
("U" winner-undo :color pink)
("C-r" winner-redo)
("r" winner-redo)
("R" winner-redo :color pink)
("n" evil-window-new)
("j" evil-window-up)
("k" evil-window-down)
("h" evil-window-left)
("l" evil-window-right)
("o" other-window)
("s" evil-window-split)
("v" evil-window-vsplit)
("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)
("e" balance-windows)
("o" winum-select-window-by-number)
("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" neotree-toggle)
;; Extra bindings:
("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)
("q" nil :color blue)))
(ha-leader "w" '("windows" . hydra-window-resize/body))
Search Operations
Ways to search for information goes under the s
key. This primarily depends on 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
:init ; I sometimes call `grep`:
; (grep-apply-setting 'grep-command "rg -n -H --no-heading -e ")
:config
(rg-enable-default-bindings (kbd "M-R"))
(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)))
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 a" '("apropos" . apropos-command)
"h c" '("elisp cheatsheet" . shortdoc-display-group)
"h e" '("errors" . view-echo-area-messages)
"h E" '("emacs-lisp" . (lambda () (interactive) (info "elisp")))
"h f" '("function" . describe-function)
"h F" '("font" . describe-font)
"h =" '("face" . describe-face)
"h k" '("key binding" . describe-key)
"h K" '("key map" . describe-keymap)
"h m" '("mode" . describe-mode)
"h p" '("package" . describe-package)
"h s" '("symbol" . info-lookup-symbol)
"h v" '("variable" . describe-variable)
"h i" '("info" . info)
"h I" '("info manual" . info-display-manual)
"h j" '("info jump" . info-apropos))
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" 'consult-yank-pop
"gs" 'consult-line))
Consult for Projects
One of the reasons that Consult hasn’t 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 (consult-projectile :type git :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 theembark-indicators
user option to exclude the mixed and verbose indicators and to includeembark-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.
(use-package evil-exchange
;; While I normally just use `link-hint', the gx keybinding is used by evil-exchange:
:general (:states 'normal "gz" 'browse-url-at-point)
:config (evil-exchange-install))
Let’s explain how this works as the documentation assumes some previous knowledge. If you had a sentence:
The ball was red and the boy was blue.
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.
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
takes text objects and whatnot. For instance, g c $
comments to the end of the line.
(use-package evil-commentary
:config (evil-commentary-mode))
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 evil-owl
: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 which is similar to f
and t
, but does two characters, and can, when configured, search more than the current line. My issue is that /git/howard/hamacs/src/commit/1395f70a1161bee71b41565057983f7173e492ca/Evil%20Surround uses the same keybindings.
(use-package evil-snipe
:after evil
:init
(setq evil-snipe-scope 'visible)
:general
(:states '(normal motion operator 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, however, 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 I’m choosing the s
to be surround.
(use-package evil-surround
:after evil-snipe
:config
(push '(?\" . ("“" . "”")) evil-surround-pairs-alist)
(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)
:general
(:states 'operator :keymaps 'evil-surround-mode-map
"z" 'evil-surround-edit
"Z" 'evil-Surround-edit)
:hook (text-mode . evil-surround-mode)) ; Don't globally use it on Magit, et. al
Notes:
-
cz'"
- to convert surrounding single quote string to double quotes.
-
dz"
- to delete the surrounding double quotes.
-
yze"
- puts single quotes around the next word.
-
yziw'
- puts single quotes around the word, no matter the points position.
-
yZ$<p>
- surrouds the line with HTML
<p>
tag (with extra carriage returns). -
(
- puts spaces inside the surrounding parens, but
)
doesn't. Same with[
and]
.
Additional Global Packages
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 "gR" '("replace" . vr/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-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
I originally appreciated ace-link to work with hyperlinks on Org, EWW and Info pages, but the link-hint project works with more types of 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" 'link-hint-open-link
"gL" 'link-hint-copy-link)
(:states 'normal :keymaps 'eww-mode-map
"o" 'link-hint-open-link)
(:states 'normal :keymaps 'Info-mode-map
"o" 'link-hint-open-link))
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))
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" '("repeat last command" . projectile-repeat-last-command)
"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
(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)
("r" persp-rename)
("d" persp-kill)
("W" ha-workspace-initialize)
("a" persp-add-buffer)
("b" persp-switch-to-buffer)
("k" persp-remove-buffer)
("K" persp-kill-buffer)
("s" persp-state-save)
("l" persp-state-load)
("w" ha-switch-to-special) ; The most special perspective
("q" nil)
("C-g" nil))
:bind ("C-<tab>" . hydra-workspace-leader/body))
I have no idea why this binding doesn’t 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)))))
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 ((readme-org (f-join project "README.org"))
(readme-md (f-join project "README.md")))
(cond
(files (ha--project-show-files project files))
((f-exists? readme-org) (find-file readme-org))
((f-exists? readme-md) (find-file readme-md))
(t (dired 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)."
(message "Loading files from %s ... %s" root files)
(let* ((file (car files))
(more (cdr files))
(filename (format "%s/%s" root file)))
(find-file filename)
(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
: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 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" . vc-revert)
"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)))
The git-timemachine project is cool:
(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 (:type git :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:
and make sure this works:
(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).
Neotree
I primarily use Neotree when I am screen-sharing my Emacs session with collegues as it shows a project like an IDE.
(use-package neotree
:general ; evil-collection forgot a couple:
(:states 'normal :keymaps 'neotree-mode-map
"TAB" 'neotree-enter
"SPC" 'neotree-quick-look
"RET" 'neotree-enter
"H" 'neotree-hidden-file-toggle))
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 (:type git :protocol ssh :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.
Let’s 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.