#+TITLE: General Emacs Configuration #+AUTHOR: Howard X. Abrams #+EMAIL: howard.abrams@gmail.com #+DATE: 2020-09-10 #+FILETAGS: :emacs: A literate programming file for configuring Emacs. #+BEGIN_SRC emacs-lisp :exports none ;;; ha-config.org --- A literate programming file for configuring Emacs. -*- lexical-binding: t; -*- ;; ;; Copyright (C) 2020-2021 Howard X. Abrams ;; ;; Author: Howard X. Abrams ;; Maintainer: Howard X. Abrams ;; Created: September 10, 2020 ;; ;; This file is not part of GNU Emacs. ;; ;; *NB:* Do not edit this file. Instead, edit the original literate file at: ;; ~/other/hamacs/ha-config.org ;; Using `find-file-at-point', and tangle the file to recreate this one . ;; ;;; Code: #+END_SRC * Introduction The following are general variable settings, and doesn't really belong to any specific purpose. #+BEGIN_SRC emacs-lisp (setq user-full-name "Howard X. Abrams" user-mail-address "howard.abrams@gmail.com") #+END_SRC New way to display line-numbers. I set mine to =relative= so that I can easily jump up and down by that value. Set this to =nil= to turn off, or =t= to be absolute. #+BEGIN_SRC emacs-lisp (setq display-line-numbers t) (setq display-line-numbers-type 'relative) #+END_SRC As [[https://tecosaur.github.io/emacs-config/config.html][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. #+BEGIN_SRC emacs-lisp (setq auth-sources '("~/.authinfo.gpg") auth-source-cache-expiry nil) #+END_SRC More settings: #+BEGIN_SRC emacs-lisp (setq truncate-string-ellipsis "…" ; Unicode ellispis are nicer than "..." auto-save-default t) #+END_SRC And some Mac-specific settings: #+BEGIN_SRC emacs-lisp (when (equal system-type 'darwin) (setq mac-option-modifier 'meta) (setq mac-command-modifier 'super) (add-to-list 'default-frame-alist '(ns-transparent-titlebar . t)) (add-to-list 'default-frame-alist '(ns-appearance . dark))) #+END_SRC Finally, we need to make sure that the =/usr/local/bin= path is available to Emacs. Normally, we get this value from =PATH= environment variable, but I have one Emacs installation that refuses to, so... here we are. #+BEGIN_SRC emacs-lisp (add-to-list 'exec-path "/usr/local/bin/") #+END_SRC ** Customization Section While I would rather program my configurations, sometimes the Emacs menu system is “good enough”, but I want it in its own file: #+BEGIN_SRC emacs-lisp (setq custom-file (expand-file-name "custom.el" user-emacs-directory)) (when (file-exists-p custom-file) (load custom-file)) #+END_SRC * Support Packages ** Piper Rewriting my shell scripts in Emacs Lisp uses my [[https://gitlab.com/howardabrams/emacs-piper][emacs-piper project]], and this code spills into my configuration code, so let's load it now: #+BEGIN_SRC emacs-lisp (use-package piper :straight nil :load-path "~/other/emacs-piper/" :commands shell-command-to-list ; I use this function quite a bit :bind (:map evil-normal-state-map ("|" . piper-user-interface))) #+END_SRC ** Yet Another Snippet System (YASnippets) Using [[https://github.com/joaotavora/yasnippet][yasnippet]] to convert templates into text: #+BEGIN_SRC emacs-lisp (use-package yasnippet :config (add-to-list 'yas-snippet-dirs "~/.emacs.d/snippets") (yas-global-mode +1)) #+END_SRC ** Request System The above code (and other stuff) needs the [[https://github.com/tkf/emacs-request][request]] package: #+BEGIN_SRC emacs-lisp (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)))))) #+END_SRC *** Dad Jokes! The /critical part/ here, is the [[https://icanhazdadjoke.com/][Dad Joke]] function, which is just a =curl= call: #+BEGIN_SRC sh curl -sH "Accept: text/plain" https://icanhazdadjoke.com/ #+END_SRC For this, I use the =request= package, which is /asynchronous/ #+BEGIN_SRC emacs-lisp #+END_SRC * Configuration Changes ** Initial Settings and UI Let's turn off the menu and other things: #+BEGIN_SRC emacs-lisp (tool-bar-mode -1) (scroll-bar-mode -1) (horizontal-scroll-bar-mode -1) (setq visible-bell 1) #+END_SRC I dislike forgetting to trim trailing white-space: #+BEGIN_SRC emacs-lisp (add-hook 'before-save-hook 'delete-trailing-whitespace) #+END_SRC I like being able to enable local variables in =.dir-local.el= files: #+BEGIN_SRC emacs-lisp (setq enable-local-variables t) #+END_SRC ** 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 [[https://github.com/minad/vertico][vertico]] package puts the completing read in a vertical format, and seems to fit the bill. It seems to be similar to [[https://github.com/raxod502/selectrum#vertico][Selectrum]], and I'll use it (at least for a while), however, I may be jumping between the two. #+BEGIN_SRC emacs-lisp (use-package vertico :config (vertico-mode)) #+END_SRC My only issue with using Vertico with =find-file= is that I really like having the Return key insert the directory at point, and not open =dired=. Seems like this is addressed with this extension /installed with Vertico/: #+BEGIN_SRC emacs-lisp (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)) #+END_SRC *** Selectrum While I've been /dabbling/ in some of the alternates for =completing-read=, after watching [[https://youtu.be/lfgQC540sNM][Rari Comninos' overview]], I decided to try [[https://github.com/raxod502/selectrum][selectrum]] for better narrowing and selecting (instead of Ivy) and [[https://github.com/raxod502/prescient.el][prescient]] to order the selection from history. #+BEGIN_SRC emacs-lisp :tangle no (use-package selectrum :config ;; Optional performance optimization by highlighting only the visible candidates. (setq selectrum-highlight-candidates-function #'orderless-highlight-matches orderless-skip-highlighting (lambda () selectrum-is-active)) (selectrum-mode +1)) (use-package selectrum-prescient :init (setq selectrum-prescient-enable-filtering nil ; Use prescient on top of orderless selectrum-prescient-enable-sorting t) :config (selectrum-prescient-mode +1) (prescient-persist-mode +1)) #+END_SRC Keybindings: - ~RET~ :: Select the candidate (obviously), but if directory, opens =dired= - ~M-# RET~ :: Select =#= candidate (where # is a number 0-9) - ~C-j~ :: Submit what you've typed (even if it would select something else) - ~TAB~ :: Move into a directory (for =find-file=) - ~M-w~ :: Copy the candidate to the kill ring (clipboard) - ~,~ :: Select multiple candidates - ~M-BKSP~ :: To go up a directory - ~M-p~ / ~M-n~ / ~M-r~ :: Select/Search the selection history Wouldn't it be swell if we could quickly select one of the items visually shown. #+BEGIN_SRC emacs-lisp :tangle no (define-key selectrum-minibuffer-map (kbd "C-l") 'selectrum-quick-select) #+END_SRC *** Orderless While the space can be use to separate words (acting a bit like a =.*= regular expression), the [[https://github.com/oantolin/orderless][orderless]] project allows those words to be in any order. #+BEGIN_SRC emacs-lisp (use-package orderless :init (setq completion-styles '(substring orderless) completion-category-defaults nil completion-category-overrides '((file (styles partial-completion))))) #+END_SRC *Note:* Multiple files can be opened at once with =find-file= if you enter a wildcard. We may also give the =initials= completion style a try. *** Savehist Persist history over Emacs restarts using the built-in [[https://www.emacswiki.org/emacs/SaveHist][savehist]] project. Since both Vertico and Selectrum sorts by history position, this should make the choice /smarter/ with time. #+BEGIN_SRC emacs-lisp (use-package savehist :init (savehist-mode)) #+END_SRC *** Marginalia The [[https://github.com/minad/marginalia][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. #+BEGIN_SRC emacs-lisp ;; Enable richer annotations using the Marginalia package (use-package marginalia :init (setq marginalia-annotators-heavy t) :config (marginalia-mode)) #+END_SRC * Key Bindings To begin my binding changes, let's turn on [[https://github.com/justbur/emacs-which-key][which-key]]: #+BEGIN_SRC emacs-lisp (use-package which-key :init (setq which-key-popup-type 'minibuffer) :config (which-key-mode)) #+END_SRC ** 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]] #+BEGIN_SRC emacs-lisp (use-package evil :init (setq evil-disable-insert-state-bindings t evil-want-keybinding nil evil-want-integration t evil-escape-key-sequence "fd" evil-escape-unordered-key-sequence t) :config (add-to-list 'evil-normal-state-modes 'shell-mode) (add-to-list 'evil-emacs-state-modes 'term-mode) (add-to-list 'evil-emacs-state-modes 'elfeed-search-mode) (add-to-list 'evil-emacs-state-modes 'elfeed-show-mode) ;; Use escape to get out of visual mode, eh? (evil-define-key 'visual global-map (kbd "v") 'er/expand-region) (evil-mode)) #+END_SRC Using the key-chord project allows me to make Escape be on two key combo presses on both sides of my keyboard: #+BEGIN_SRC emacs-lisp (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)) #+END_SRC ** Evil Collection Dropping into Emacs state is better than pure Evil state for applications, however, [[https://github.com/emacs-evil/evil-collection][the evil-collection package]] creates a hybrid between the two, that I like. #+BEGIN_SRC emacs-lisp (use-package evil-collection :after evil :config (evil-collection-init)) #+END_SRC Do I want to specify the list of modes to change for =evil-collection-init=, e.g. #+BEGIN_SRC emacs-lisp :tangle no :eval no '(eww magit dired notmuch term wdired) #+END_SRC ** 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 [[https://github.com/noctuid/general.el][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. #+BEGIN_SRC emacs-lisp (use-package general :config (general-evil-setup t) (general-create-definer ha-leader :keymaps 'normal :prefix "SPC" :non-normal-prefix "M-SPC" :global-prefix "")) #+END_SRC *** Top-Level Operations Let's try this out with #+BEGIN_SRC emacs-lisp (ha-leader "SPC" '("M-x" . execute-extended-command) "." '("repeat" . repeat) "X" 'org-capture "L" 'org-store-link "RET" 'bookmark-jump "m" '(:ignore t :which-key "mode")) #+END_SRC And ways to stop the system: #+BEGIN_SRC emacs-lisp (ha-leader "q" '(:ignore t :which-key "quit/session") "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)) #+END_SRC *** File Operations Obviously, =find-file= is still my bread and butter, but I do like getting information about the file associated with the buffer. For instance, the file path: #+BEGIN_SRC emacs-lisp (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. If ROOT is given, they 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"))) #+END_SRC With these helper functions in place, I can create a leader collection for file-related functions: #+BEGIN_SRC emacs-lisp (ha-leader "f" '(:ignore t :which-key "files") "f f" '("load" . find-file) "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)) #+END_SRC *** 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: #+BEGIN_SRC emacs-lisp (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)))) #+END_SRC And the collection of useful operations: #+BEGIN_SRC emacs-lisp (ha-leader "b" '(:ignore t :which-key "buffers") "b b" '("switch" . persp-switch-to-buffer) "b B" '("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)) #+END_SRC *** Toggle Switches The goal here is toggle switches and other miscellaneous settings. #+BEGIN_SRC emacs-lisp (ha-leader "t" '(:ignore t :which-key "toggles") "t l" '("line numbers" . display-line-numbers-mode) "t r" '("relative lines" . ha-toggle-relative-line-numbers)) #+END_SRC Really? We can't automatically toggle between relative and absolute line numbers? #+BEGIN_SRC emacs-lisp (defun ha-toggle-relative-line-numbers () (interactive) (if (eq display-line-numbers 'relative) (setq display-line-numbers t) (setq display-line-numbers 'relative))) #+END_SRC *** Window Operations While it comes with Emacs, I use [[https://www.emacswiki.org/emacs/WinnerMode][winner-mode]] to undo window-related changes: #+BEGIN_SRC emacs-lisp (use-package winner :custom (winner-dont-bind-my-keys t) :config (winner-mode +1)) #+END_SRC Use the [[https://github.com/abo-abo/ace-window][ace-window]] project to jump to any window you see: #+BEGIN_SRC emacs-lisp (use-package ace-window) #+END_SRC This package, bound to ~SPC w w~, also allows operations specified before choosing the window: - ~x~ - delete window - ~m~ - swap windows - ~M~ - move window - ~c~ - copy window - ~j~ - select buffer - ~n~ - select the previous window - ~u~ - select buffer in the other window - ~c~ - split window fairly, either vertically or horizontally - ~v~ - split window vertically - ~b~ - split window horizontally - ~o~ - maximize current window - ~?~ - show these command bindings To jump to a window even quicker, use the [[https://github.com/deb0ch/emacs-winum][winum package]]: #+BEGIN_SRC emacs-lisp (use-package winum :config (winum-mode +1)) #+END_SRC The ~0~ key/window should be always associated with a project-specific tree window: #+BEGIN_SRC emacs-lisp (add-to-list 'winum-assign-functions (lambda () (when (string-match-p (buffer-name) ".*\\*NeoTree\\*.*") 10))) #+END_SRC Let's try this out with a Hydra since some commands (enlarge window), I want to repeatedly call. It also allows me to organize the helper text. #+BEGIN_SRC emacs-lisp (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+ _k_: down _m_: maximize _<_: narrower _r_: redo _h_: left _s_: h-split _e_: balanced _R_: redo+ _l_: right _v_: v-split _o_: choose by number (also 0-9) " ("w" ace-window) ("c" other-window) ("=" balance-windows) ("m" delete-other-windows) ("d" delete-window) ("D" ace-delete-window) ("z" ace-window-swap) ("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) ("s" evil-window-split) ("v" evil-window-vsplit) ("+" 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) ("0" winum-select-window-0-or-10) ("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) ;; 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)) #+END_SRC *** Search Operations Ways to search for information goes under the ~s~ key. This primarily depends on the [[https://github.com/dajva/rg.el][rg]] package, which builds on the internal =grep= system. #+BEGIN_SRC emacs-lisp (use-package rg :config (ha-leader "s" '(:ignore t :which-key "search") "s p" 'projectile-ripgrep)) #+END_SRC *** Text Operations Stealing much of this from Spacemacs. #+BEGIN_SRC emacs-lisp (ha-leader "x" '(:ignore t :which-key "text") "x q" '("fill paragraph" . fill-paragraph)) #+END_SRC *** Help Operations While the ~C-h~ is easy enough, I am now in the habit of typing ~SPC h~ instead. #+BEGIN_SRC emacs-lisp (ha-leader "h" '(:ignore t :which-key "help") "h f" '("function" . describe-function) "h v" '("variable" . describe-variable) "h k" '("key binding" . describe-key) "h B" '("embark" . embark-bindings)) #+END_SRC *** Consult Enhancements The [[https://github.com/minad/consult][consult]] package is a replacement for selecting buffers and other /speciality functions/, similar to the [[https://oremacs.com/2015/04/09/counsel-completion/][Ivy's counsel completion]] project. I think I may be adding it sparingly, as personally, I read files and buffers based on the selected /project/. The pattern is to add the /consult/ functions to my standard general leader organization, but they will all end with ~TAB~ (unique, easy and consistent). #+BEGIN_SRC emacs-lisp (use-package consult :config (ha-leader "b TAB" '("consult buffer" . consult-buffer) "b S-TAB" '("consult buffer in window" . consult-buffer-other-window) "s TAB" '("consult search" . consult-ripgrep) "f TAB" '("consult file" . consult-file))) #+END_SRC *** Embark The [[https://github.com/oantolin/embark/][embark]] project offers /actions/ on /targets/, however, I'm primarily thinking of acting on selected items in the minibuffer, however, they actually act anywhere. Consequently, I need an easy-to-use keybinding that doesn't conflict. Hey, that is what the Super key is for, right? #+BEGIN_SRC emacs-lisp (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)) #+END_SRC Consult users will also want the embark-consult package. #+BEGIN_SRC emacs-lisp (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)) #+END_SRC ** Evil Snipe Doom introduced me to [[https://github.com/hlissner/evil-snipe][evil-snipe]] which is similar to =f= and =t=, but does two characters, and can, when configured, search more than the current line: #+BEGIN_SRC emacs-lisp (use-package evil-snipe :init (setq evil-snipe-scope 'visible) :config (evil-define-key '(normal motion operator visual) "s" #'evil-snipe-s "S" #'evil-snipe-S) (evil-snipe-mode +1)) #+END_SRC It highlights all potential matches, use ~;~ to skip to the next match, and ~,~ to jump back. ** Evil Surround I like both [[https://github.com/emacs-evil/evil-surround][evil-surround]] and Henrik's [[https://github.com/hlissner/evil-snipe][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 calls surround. As an original Emacs person, I am not bound by that key history, but I do need them consistent: #+BEGIN_SRC emacs-lisp (dolist (state '(normal motion operator visual)) (evil-define-key state evil-surround-mode-map "z" 'evil-surround-edit) (evil-define-key state evil-surround-mode-map "Z" 'evil-Surround-edit)) #+END_SRC ** Jump, Jump, Jump! While I grew up on =Control S=, I am liking the /mental model/ associated with the [[https://github.com/abo-abo/avy][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. #+BEGIN_SRC emacs-lisp (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)) :bind ("" . avy-goto-char-timer)) #+END_SRC *Note:* The links should be shorter near the point as opposed to starting from the top of the window. ** Miscellaneous Keys I really appreciated the [[https://github.com/benma/visual-regexp.el][visual-regexp package]]: #+BEGIN_SRC emacs-lisp (use-package visual-regexp :bind (("C-c r" . vr/replace) ("C-c q" . vr/query-replace))) #+END_SRC * 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 really don't /need/ all the features that [[https://github.com/bbatsov/projectile][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 [[https://cestlaz.github.io/post/using-emacs-79-project/][this essay]] for details on what I mean as an alternative). #+BEGIN_SRC emacs-lisp (use-package projectile :custom (projectile-sort-order 'recentf) :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))) #+END_SRC ** 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 [[https://github.com/nex3/perspective-el][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: #+BEGIN_SRC emacs-lisp (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" ; Shame that the following doesn't work: num ; (propertize (number-to-string num) :foreground "#00a0") (car names)) ; Nor does surrounding the number with underbars. (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?))) #+END_SRC Build the hydra as well as configure the =perspective= project. #+BEGIN_SRC emacs-lisp (use-package perspective :custom (persp-modestring-short t) (persp-sort 'name) (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 " ("TAB" persp-switch) ("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) ("]" 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) ("q" nil) ("C-g" nil)) (ha-leader "TAB" '("workspaces" . hydra-workspace-leader/body))) #+END_SRC *** Predefined Workspaces First step is to get rid of the /recent/ feature, as I don't really use that. #+BEGIN_SRC emacs-lisp :tangle no (recentf-mode -1) (remove-hook 'kill-emacs-hook 'recentf-cleanup) (remove-hook 'kill-emacs-hook 'save-place-kill-emacs-hook) (remove-hook 'kill-emacs-hook 'savehist-autosave) #+END_SRC 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. #+BEGIN_SRC emacs-lisp (defvar ha-workspace-projects-personal '(("projects" "~/projects" ("breathe.org" "tasks.org")) ("personal" "~/personal" ("general.org")) ("technical" "~/technical" ("ansible.org")) ("hamacs" "~/other/hamacs" ("README.org" "ha-config.org")) ("rpg" "~/Dropbox/org/rpg" ("workdavians-dragon-heist.org" "dragon-heist.org")) ("dm-work" "~/Dropbox/org/rpg-dm" ("README-mythic.org" "rpgdm.el"))) "List of default projects with a name.") #+END_SRC Given a list of information about project-workspaces, can we just create them all? #+BEGIN_SRC emacs-lisp (defun ha-persp-exists? (name) "Return non-nill is a perspective of NAME has been created." (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))))) #+END_SRC 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 just need to load a file from it, e.g. Like a README or something. #+BEGIN_SRC emacs-lisp (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 a 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. (if files (ha--project-show-files project files) (if-let ((readme (f-join project "README*"))) (find-file readme t) (dired project))))) #+END_SRC Displaying a few files? Well, when /starting/ I am only showing one or two files (maybe three), so we will split the window horizontally for each file. #+BEGIN_SRC emacs-lisp (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)))) #+END_SRC * Applications Can we really call these /applications/? ** Magit Can not live without [[https://magit.vc/][Magit]], a Git porcelain for Emacs. I stole the bulk of this work from Doom Emacs. #+BEGIN_SRC emacs-lisp (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 R" '("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))) #+END_SRC Let's extend Magit with [[https://github.com/magit/forge][Magit Forge]] for working with Github and Gitlab: #+BEGIN_SRC emacs-lisp (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))) #+END_SRC The [[https://github.com/emacsmirror/git-timemachine][git-timemachine]] project is cool: #+BEGIN_SRC emacs-lisp (use-package git-timemachine :config (ha-leader "g t" '("git timemachine" . git-timemachine))) #+END_SRC Using the [[https://github.com/emacsmirror/gist][gist package]] to write code snippets on [[https://gist.github.com/][Github]] seems like it can be useful, but I'm not sure how often. #+BEGIN_SRC emacs-lisp (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 #+END_SRC ** Web Browsing *** EWW Web pages look pretty good with EWW, but I'm having difficulty getting it to render a web search from DuckDuck. #+BEGIN_SRC emacs-lisp (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 (define-key eww-mode-map (kbd "L") #'eww-list-bookmarks) (define-key eww-buffers-mode-map (kbd "q") #'eww-bookmark-kill) (define-key eww-bookmark-mode-map (kbd "q") #'eww-bookmark-kill) (use-package ace-link :config (ace-link-setup-default))) #+END_SRC ** Feed Reader Let's get our feeds from a collection of org mode files. By default, Doom configures =rmh-elfeed-org-files= to [[file:~/Dropbox/org/elfeed.org][elfeed.org]] in =org-directory=, so that will be fine. By setting this variable, we configure elfeed to use elfeed: #+BEGIN_SRC emacs-lisp (setq rmh-elfeed-org-files (list (f-join hamacs-source-dir "my-feeds.org"))) #+END_SRC While I would like to share the /status/ of my reads, so ... #+BEGIN_SRC emacs-lisp (use-package elfeed :config (setq elfeed-db-directory "~/dropbox/.elfeed/") (evil-define-key 'normal elfeed-show-mode-map (kbd "q") 'delete-window) (evil-define-key 'normal elfeed-search-mode-map (kbd "r") 'ha/elfeed-tag-unread) (evil-define-key 'normal elfeed-search-mode-map (kbd "R") 'elfeed-search-update--force) (use-package elfeed-org :config (elfeed-org))) (defun ha/elfeed-tag-unread () (interactive) (elfeed-search-untag-all 'unread) (elfeed-search-update)) #+END_SRC According to Ben Maughan and [[http://pragmaticemacs.com/emacs/to-eww-or-not-to-eww/][this Pragmatic Emacs essay]], we could easily browse an article in the GUI browser instead of EWW with capital B: #+BEGIN_SRC emacs-lisp (defun bjm/elfeed-show-visit-gui () "Wrapper for elfeed-show-visit to use gui browser instead of eww" (interactive) (let ((browse-url-generic-program "/usr/bin/open")) (elfeed-show-visit t))) (define-key elfeed-show-mode-map (kbd "B") 'bjm/elfeed-show-visit-gui) #+END_SRC ** VTerm I'm not giving up on Eshell, but I am playing around with [[https://github.com/akermu/emacs-libvterm][vterm]], and it is pretty good, but I use it primarily as a more reliable approach to [[file:ha-remoting.org][a remote shell]]. VTerm has an issue (at least for me) with M-Backspace not deleting the previous word, and yeah, I want to make sure that both keystrokes do the same thing. #+BEGIN_SRC emacs-lisp (use-package vterm :init (setq vterm-shell "/usr/local/bin/fish") ;; Granted, I seldom pop out to the shell except during code demonstrations, ;; but I like how C-p/C-n jumps up to each prompt entry using this setting ;; that works with my prompt: (setq vterm-use-vterm-prompt-detection-method nil term-prompt-regexp "^.* $ ") :config (dolist (k '("" "")) (define-key vterm-mode-map (kbd k) (lambda () (interactive) (vterm-send-key (kbd "C-w"))))) (advice-add 'vterm-copy-mode :after 'evil-normal-state)) #+END_SRC The advantage of running terminals in Emacs is the ability to copy text without a mouse. For that, hit ~C-c C-t~ to enter a special copy-mode. If I go into this mode, I might as well also go into normal mode to move the cursor. *Note:* To exit the copy-mode (and copy the selected text to the clipboard), hit ~Return~. Hrm. Seems that I might want a function to copy the output of the last command to a register, or even an org-capture... ** Fish Shell #+BEGIN_SRC emacs-lisp (use-package fish-mode :mode ("\\.fish" . fish-mode) :hook (fish-mode . (lambda () (add-hook 'before-save-hook 'fish_indent-before-save)))) #+END_SRC * Technical Artifacts :noexport: Let's provide a name so that the file can be required: #+BEGIN_SRC emacs-lisp :exports none (provide 'ha-config) ;;; ha-config.el ends here #+END_SRC Before you can build this on a new system, make sure that you put the cursor over any of these properties, and hit: ~C-c C-c~ #+DESCRIPTION: A literate programming file for configuring Emacs. #+PROPERTY: header-args:sh :tangle no #+PROPERTY: header-args:emacs-lisp :tangle yes #+PROPERTY: header-args :results none :eval no-export :comments no #+OPTIONS: num:nil toc:nil todo:nil tasks:nil tags:nil date:nil #+OPTIONS: skip:nil author:nil email:nil creator:nil timestamp:nil #+INFOJS_OPT: view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js