#+TITLE: Leader Key Sequences #+AUTHOR: Howard X. Abrams #+DATE: 2024-01-31 #+FILETAGS: :emacs: A literate programming file for defining leaders with general #+begin_src emacs-lisp :exports none ;;; ha-leader --- defining leaders with general -*- lexical-binding: t; -*- ;; ;; © 2024 Howard X. Abrams ;; This work is licensed under a Creative Commons Attribution 4.0 International License. ;; See http://creativecommons.org/licenses/by/4.0/ ;; ;; Author: Howard X. Abrams ;; Maintainer: Howard X. Abrams ;; Created: January 31, 2024 ;; ;; While obvious, GNU Emacs does not include this file or project. ;; ;; *NB:* Do not edit this file. Instead, edit the original literate file at: ;; /Users/howard.abrams/other/hamacs/ha-leader.org ;; And tangle the file to recreate this one. ;; ;;; Code: #+end_src * Introduction 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 (setq general-use-package-emit-autoloads t) (general-evil-setup t) (general-create-definer ha-leader :states '(normal visual motion) :keymaps 'override :prefix "SPC" :non-normal-prefix "M-SPC" :prefix "") (general-create-definer ha-local-leader :states '(normal visual motion) :global-prefix "") (general-nmap "SPC m" (general-simulate-key "," :which-key "major mode"))) #+end_src ** Relabel the G Keys Can’t remember all the shortcuts on the ~g~ key, and =which-key= displays the entire function, so let’s /re-add/ those keybindings, but with labels. The ~g~ is extemely convenient, yet I realize that I will never use some of the default keybindings (like ~g m~ to go to the middle of the line? Too imprecise). So I am also going to delete some of them. #+begin_src emacs-lisp (use-package evil :general (:states '(normal visual motion operator) ;; These go into operator mode, so the key sequence, g U i o ;; upper cases the symbol at point: "g u" '("downcase" . evil-downcase) "g U" '("upcase" . evil-upcase) "g ~" '("invert case" . evil-invert-case) ;; Use this ALL the time: "g ;" '("last change →" . evil-goto-last-change) "g :" '("last change ←" . evil-goto-last-change-reverse) "g d" '("goto def" . evil-goto-definition) "g i" '("resume insert" . evil-insert-resume) "g v" '("resume visual" . evil-visual-restore) "g g" '("goto first line" . evil-goto-first-line) "g f" '("find file" . find-file-at-point) "g e" '("← WORD end" . evil-backward-WORD-end) ; like b "g E" '("← word end" . evil-backward-word-end) ; like B "g w" '("→ WORD end" . evil-forward-WORD-end) "g W" '("→ word end" . evil-forward-word-end) ;; Not sure how to use these two as they need text objs "g n" '("next match" , evil-next-match) "g N" '("prev match" , evil-previous-match) "g P" '("paste after" . evil-paste-before-cursor-after) ;; Let's clean out keybindings already in normal mode ;; without the initial g: "g #" nil ; evil-search-unbounded-word-backward "g *" nil ; evil-search-unbounded-word-forward "g ^" nil ; evil-first-non-blank "g $" nil ; evil-end-of-line "g _" nil ; evil-last-non-blank ... eh "g 0" nil ; evil-beginning-of-line "g &" nil ; evil-ex-repeat-global-substitute "g 8" nil ; what-cursor-position "g F" nil ; evil-find-file-at-point-with-line "g J" nil ; evil-join-whitespace "g I" nil ; evil-insert-0-line ... just use I "g m" nil ; evil-middle-of-visual-line "g M" nil ; evil-percentage-of-line ... middle? "g T" nil ; tab-bar-switch-to-prev-tab "g t" nil ; tab-bar-switch-to-next-tab "g j" nil ; This will be a major-mode-specific keybinding "g k" nil (kbd "g C-]") nil (kbd "g ") nil (kbd "g ") nil (kbd "g ") nil (kbd "g ") nil (kbd "g ") nil (kbd "g ") nil)) #+end_src While we are at it, let’s readd, and relabel the ~z~ command functions: #+begin_src emacs-lisp (use-package evil :general (:states '(normal visual motion operator) "z q" '("fill para" . fill-paragraph) "z Q" '("unfill para" . unfill-paragraph) "z p" '("unfill para" . unfill-paragraph) "z m" '("scroll to center" . evil-scroll-line-to-center) "z t" '("scroll to top" . evil-scroll-line-to-top) "z b" '("scroll to bottom" . evil-scroll-line-to-bottom) (kbd "z ") '("scroll left" . evil-scroll-column-left) (kbd "z ") '("scroll right" . evil-scroll-column-right) "z a" '("toggle fold" . evil-toggle-fold) "z f" '("close fold" . evil-close-fold) "z o" '("open fold" . evil-open-fold) "z F" '("close all folds" . evil-close-folds) "z O" '("open all folds" . evil-open-folds) ;; Open a fold at point recursively? Never see a need: ;; Since I have overridden z-l and whatnot, why have z-h? "z e" nil ; evil-scroll-end-column "z h" nil ; evil-scroll-column-left "z l" nil ; evil-scroll-column-right "z r" nil "z s" nil ; evil-scroll-start-column "z ^" nil ; evil-scroll-top-line-to-bottom "z +" nil ; evil-scroll-bottom-line-to-top "z -" nil ; evil-scroll-line-to-bottom-first-non-blank "z ." nil ; evil-scroll-line-to-center-first-non-blank (kbd "z RET") nil ; evil-scroll-line-to-top (kbd "z ") nil)) ; evil-scroll-line-to-top #+end_src ** 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: #+begin_src emacs-lisp (ha-leader "SPC" '("M-x" . execute-extended-command) "" '(keyboard-escape-quit :which-key t) "." '("repeat" . repeat) "!" '("shell command" . shell-command) "|" 'piper "X" '("org capture" . org-capture) "L" '("store org link" . org-store-link) "RET" 'bookmark-jump "a" '(:ignore t :which-key "apps") "a " '(keyboard-escape-quit :which-key t) "m" '(:ignore t :which-key "mode") "m " '(keyboard-escape-quit :which-key t) "o" '(:ignore t :which-key "org/open") "o " '(keyboard-escape-quit :which-key t) "o i" 'imenu "u" 'universal-argument) #+end_src And ways to stop the system: #+begin_src emacs-lisp (ha-leader "q" '(:ignore t :which-key "quit/session") "q " '(keyboard-escape-quit :which-key t) "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)) #+end_src And ways to load my tangled org-files: #+begin_src emacs-lisp (ha-leader "h h" '(:ignore t :which-key "hamacs") "h h " '(keyboard-escape-quit :which-key t) "h h f" '("features" . ha-hamacs-features) "h h e" '("edit" . ha-hamacs-find-file) "h h h" '("reload" . ha-hamacs-load) "h h a" '("reload all" . ha-hamacs-reload-all)) #+end_src ** 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: #+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. When given ROOT, this copies the filepath relative to that." (interactive) (if-let* ((filename (buffer-file-name (buffer-base-buffer))) (relative (f-relative filename (or nil (project-root (project-current)))))) (progn (kill-new relative) (message "Copied path to clipboard: %s" relative)) (message "Couldn't find filename in current buffer"))) #+end_src This simple function allows me to load a project-specific file in a numbered window, based on winum: #+begin_src emacs-lisp (defun find-file-in-window (win) "Change the buffer in a particular window number." (interactive) (if (windowp win) (aw-switch-to-window win) (winum-select-window-by-number win)) (project-find-file)) #+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 " '(keyboard-escape-quit :which-key t) "f a" '("load any" . find-file) "f f" '("load" . project-find-file) "f F" '("load new window" . find-file-other-window) "f l" '("locate" . locate) "f s" '("save" . save-buffer) "f S" '("save as" . write-buffer) "f r" '("recent" . recentf-open-files) "f c" '("copy" . copy-file) "f R" '("rename" . rename-file) "f D" '("delete" . delete-file) "f y" '("yank path" . ha-yank-buffer-path) "f Y" '("yank path from project" . ha-yank-project-buffer-path) "f d" '("dired" . dirvish) "f 1" '("load win-1" . ha-find-file-window-1) "f 2" '("load win-2" . ha-find-file-window-2) "f 3" '("load win-3" . ha-find-file-window-3) "f 4" '("load win-4" . ha-find-file-window-4) "f 5" '("load win-5" . ha-find-file-window-5) "f 6" '("load win-6" . ha-find-file-window-6) "f 7" '("load win-7" . ha-find-file-window-7) "f 8" '("load win-8" . ha-find-file-window-8) "f 9" '("load win-9" . ha-find-file-window-9)) #+end_src On Unix systems, the =locate= command is faster than =find= when searching the whole system, since it uses a pre-computed database, and =find= is faster if you need to search a specific directory instead of the whole system. On the Mac, we need to change the =locate= command: #+begin_src emacs-lisp (when (ha-running-on-macos?) (setq locate-command "mdfind")) #+end_src The advantage of =mdfind= is that is searches for filename /and/ its contents of your search string. Trying the [[https://github.com/benmaughan/spotlight.el][spotlight]] project, as it has a slick interface for selecting files: #+begin_src emacs-lisp (use-package spotlight :config (ha-leader "f /" '("search files" . spotlight))) #+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 This simple function allows me to switch to a buffer in a numbered window, based on winum: #+begin_src emacs-lisp (defun switch-buffer-in-window (win) "Change the buffer in a particular window number." (interactive) (if (windowp win) (aw-switch-to-window win) (winum-select-window-by-number win)) (consult-project-buffer)) #+end_src And the collection of useful operations: #+begin_src emacs-lisp (ha-leader "b" '(:ignore t :which-key "buffers") "b " '(keyboard-escape-quit :which-key t) "b B" '("switch" . project-switch-to-buffer) "b o" '("other" . project-switch-buffer-to-other-window) "b O" '("switch" . switch-to-buffer-other-window) "b i" '("ibuffer" . ibuffer) "b I" '("ibuffer" . ibuffer-other-window) "b k" '("persp remove" . persp-remove-buffer) "b N" '("new" . evil-buffer-new) "b d" '("delete" . persp-kill-buffer*) "b r" '("revert" . revert-buffer) "b s" '("save" . save-buffer) "b S" '("save all" . evil-write-all) "b n" '("next" . next-buffer) "b p" '("previous" . previous-buffer) "b y" '("copy contents" . ha-yank-buffer-contents) "b z" '("bury" . bury-buffer) "b Z" '("unbury" . unbury-buffer) "b 1" '("load win-1" . (lambda () (interactive) (switch-buffer-in-window 1))) "b 2" '("load win-2" . (lambda () (interactive) (switch-buffer-in-window 2))) "b 3" '("load win-3" . (lambda () (interactive) (switch-buffer-in-window 3))) "b 4" '("load win-4" . (lambda () (interactive) (switch-buffer-in-window 4))) "b 5" '("load win-5" . (lambda () (interactive) (switch-buffer-in-window 5))) "b 6" '("load win-6" . (lambda () (interactive) (switch-buffer-in-window 6))) "b 7" '("load win-7" . (lambda () (interactive) (switch-buffer-in-window 7))) "b 8" '("load win-8" . (lambda () (interactive) (switch-buffer-in-window 8))) "b 9" '("load win-9" . (lambda () (interactive) (switch-buffer-in-window 9)))) #+end_src ** Bookmarks I like the idea of dropping returnable bookmarks, however, the built-in behavior doesn’t honor either /projects/ or /perspectives/, but I use [[https://codeberg.org/ideasman42/emacs-bookmark-in-project][bookmark-in-project]] package to make a =project=-specific bookmarks and use that to jump to only bookmarks in the current project. #+begin_src emacs-lisp (use-package bookmark-in-project :config (ha-leader ;; Set or delete a bookmark associated with project: "b m" '("set proj mark" . bookmark-in-project-toggle) "b M" '("set global mark" . bookmark-set) "b X" '("delete mark" . bookmark-delete) "b g" '("goto proj mark" . bookmark-in-project-jump) "b " '("next mark" . bookmark-in-project-jump-next) "b " '("next mark" . bookmark-in-project-jump-previous))) #+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 " '(keyboard-escape-quit :which-key t) "t a" '("abbrev" . abbrev-mode) "t d" '("debug" . toggle-debug-on-error) "t F" '("show functions" . which-function-mode) "t f" '("auto-fill" . auto-fill-mode) "t o" '("overwrite" . overwrite-mode) "t l" '("line numbers" . display-line-numbers-mode) "t R" '("read only" . read-only-mode) "t t" '("truncate" . toggle-truncate-lines) "t v" '("visual" . visual-line-mode) "t w" '("whitespace" . whitespace-mode)) #+end_src *** Line Numbers Since we can't automatically toggle between relative and absolute line numbers, we create this function: #+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 Add it to the toggle menu: #+begin_src emacs-lisp (ha-leader "t r" '("relative lines" . ha-toggle-relative-line-numbers)) #+end_src *** Narrowing I like the focus the [[info:emacs#Narrowing][Narrowing features]] offer, but what a /dwim/ aspect: #+begin_src emacs-lisp (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)))) #+end_src And put it on the toggle menu: #+begin_src emacs-lisp (ha-leader "t n" '("narrow" . ha-narrow-dwim)) #+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 *** Ace Window Use the [[https://github.com/abo-abo/ace-window][ace-window]] project to jump to any window you see. Often transient buffers show in other windows, obscuring my carefully crafted display. Instead of jumping into a window, typing ~q~ (to either call [[help:quit-buffer][quit-buffer]]) if available, or [[help:bury-buffer][bury-buffer]] otherwise. This function hooks to =ace-window= #+begin_src emacs-lisp (defun ha-quit-buffer (window) "Quit or bury buffer in a given WINDOW." (interactive) (aw-switch-to-window window) (unwind-protect (condition-case nil (quit-buffer) (error (bury-buffer)))) (aw-flip-window)) #+end_src Since I use numbers for the window, I can make the commands more mnemonic, and add my own: #+begin_src emacs-lisp (use-package ace-window :init (setq aw-dispatch-alist '((?d aw-delete-window "Delete Window") (?m aw-swap-window "Swap Windows") (?M aw-move-window "Move Window") (?c aw-copy-window "Copy Window") (?b switch-buffer-in-window "Select Buffer") (?f find-file-in-window "Find File") (?n aw-flip-window) (?c aw-split-window-fair "Split Fair Window") (?s aw-split-window-vert "Split Vert Window") (?v aw-split-window-horz "Split Horz Window") (?o delete-other-windows "Delete Other Windows") (?q ha-quit-buffer "Quit Buffer") (?w aw-execute-command-other-window "Execute Command") (?? aw-show-dispatch-help))) :bind ("s-w" . ace-window)) #+end_src Keep in mind, these shortcuts work with more than two windows open. For instance, ~SPC w w d 3~ closes the "3" window. *** Transpose Windows My office at work has a monitor oriented vertically, and to move an Emacs with “three columned format” to a “stacked format” I use the [[https://www.emacswiki.org/emacs/TransposeFrame][transpose-frame]] package: #+begin_src emacs-lisp (use-package transpose-frame) #+end_src *** Winum To jump to a window even quicker, use the [[https://github.com/deb0ch/emacs-winum][winum package]]: #+begin_src emacs-lisp (use-package winum :bind (("s-1" . winum-select-window-1) ("s-2" . winum-select-window-2) ("s-3" . winum-select-window-3) ("s-4" . winum-select-window-4) ("s-5" . winum-select-window-5) ("s-6" . winum-select-window-6) ("s-7" . winum-select-window-7) ("s-8" . winum-select-window-8) ("s-9" . winum-select-window-9))) #+end_src This is nice since the window numbers are always present on a Doom modeline, but they sometime order the window numbers /differently/ than =ace-window=. The ~0~ key/window should be always associated with a project-specific tree window of =dired= (or [[Dirvish][Dirvish]]): #+begin_src emacs-lisp (use-package winum :config (winum-mode +1) (add-to-list 'winum-assign-functions (lambda () (when (eq major-mode 'dired-mode) 10)))) #+end_src I’d like to have dirvish show in Window 0: #+begin_src emacs-lisp (defun dirvish-show-or-switch () "As it says on the tin. Show or start Dirvish. If `divish' is showing, that is, is window 0 is showing, switch to it, otherwise, start 'er up." (interactive) (if (seq-contains (winum--available-numbers) 0) (winum-select-window-0-or-10) (dirvish-side (project-root (project-current))))) #+end_src And let’s bind Command-0 to select the window that shows dirvish, or open drvish: #+begin_src emacs-lisp (use-package winum :bind ("s-0" . dirvish-show-or-switch)) #+end_src 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. #+begin_src emacs-lisp (use-package hydra :config (defhydra hydra-window-resize (:color blue :hint nil) " _w_: select _m_: move/swap _u_: undo _^_: taller (t) _+_: text larger _j_: go up _d_: delete _U_: undo+ _v_: shorter (T) _-_: text smaller _k_: down _e_: balance _r_: redo _>_: wider _F_: font larger _h_: left _n_: v-split _R_: redo+ _<_: narrower _f_: font smaller _l_: right _s_: split _o_: only this window _c_: choose (also 1-9)" ("w" ace-window) ("c" other-window :color pink) ; change window ("o" delete-other-windows) ; “Only” this window ("d" delete-window) ("x" delete-window) ;; Ace Windows ... select the window to affect: ("m" ace-swap-window) ("D" ace-delete-window) ("O" ace-delete-other-windows) ("u" winner-undo) ("U" winner-undo :color pink) ("C-r" winner-redo) ("r" winner-redo) ("R" winner-redo :color pink) ("J" evil-window-down :color pink) ("K" evil-window-up :color pink) ("H" evil-window-left :color pink) ("L" evil-window-right :color pink) ("j" evil-window-down) ("k" evil-window-up) ("h" evil-window-left) ("l" evil-window-right) ("x" transpose-frame) ("s" hydra-window-split/body) ("n" hydra-window-split/body) ("F" font-size-increase :color pink) ("f" font-size-decrease :color pink) ("+" text-scale-increase :color pink) ("=" text-scale-increase :color pink) ("-" text-scale-decrease :color pink) ("^" evil-window-increase-height :color pink) ("v" evil-window-decrease-height :color pink) ("t" evil-window-increase-height :color pink) ("T" evil-window-decrease-height :color pink) (">" evil-window-increase-width :color pink) ("<" evil-window-decrease-width :color pink) ("." evil-window-increase-width :color pink) ("," evil-window-decrease-width :color pink) ("e" balance-windows) ("1" winum-select-window-1) ("2" winum-select-window-2) ("3" winum-select-window-3) ("4" winum-select-window-4) ("5" winum-select-window-5) ("6" winum-select-window-6) ("7" winum-select-window-7) ("8" winum-select-window-8) ("9" winum-select-window-9) ("0" dirvish-dwim) ;; Extra bindings: ("q" nil :color blue))) (ha-leader "w" '("windows" . hydra-window-resize/body)) #+end_src *** Window Splitting When I split a window, I have a following intentions: - Split and open a file from the prespective/project in the new window - Split and change to a buffer from the prespective in the new window - Split and move focus to the new window … you know, to await a new command And when creating new windows, why isn't the new window selected? Also, when I create a new window, I typically want a different buffer or file shown. #+begin_src emacs-lisp (defun ha-new-window (side file-or-buffer) (pcase side (:left (split-window-horizontally)) (:right (split-window-horizontally) (other-window 1)) (:above (split-window-vertically)) (:below (split-window-vertically) (other-window 1))) (pcase file-or-buffer (:file (call-interactively 'project-find-file)) (:buffer (call-interactively 'project-switch-to-buffer)) (:term (ha-shell (project-root (project-current)))))) #+end_src Shame that hydra doesn’t have an /ignore-case/ feature. #+begin_src emacs-lisp (use-package hydra :config (defhydra hydra-window-split (:color blue :hint nil) ("s" hydra-window-split-below/body "below") ("j" hydra-window-split-below/body "below") ("k" hydra-window-split-above/body "above") ("h" hydra-window-split-left/body "left") ("l" hydra-window-split-right/body "right") ("n" hydra-window-split-right/body "right")) (defhydra hydra-window-split-above (:color blue :hint nil) ("b" (lambda () (interactive) (ha-new-window :above :buffer)) "switch buffer") ("f" (lambda () (interactive) (ha-new-window :above :file)) "load file") ("t" (lambda () (interactive) (ha-new-window :above :term)) "terminal") ("k" split-window-below "split window")) (defhydra hydra-window-split-below (:color blue :hint nil) ("b" (lambda () (interactive) (ha-new-window :below :buffer)) "switch buffer") ("f" (lambda () (interactive) (ha-new-window :below :file)) "load file ") ("t" (lambda () (interactive) (ha-new-window :below :term)) "terminal") ("j" (lambda () (interactive) (split-window-below) (other-window 1)) "split window ") ("s" (lambda () (interactive) (split-window-below) (other-window 1)) "split window ")) (defhydra hydra-window-split-right (:color blue :hint nil) ("b" (lambda () (interactive) (ha-new-window :right :buffer)) "switch buffer") ("f" (lambda () (interactive) (ha-new-window :right :file)) "load file") ("t" (lambda () (interactive) (ha-new-window :right :term)) "terminal") ("l" (lambda () (interactive) (split-window-right) (other-window 1)) "split window ") ("n" (lambda () (interactive) (split-window-right) (other-window 1)) "split window ")) (defhydra hydra-window-split-left (:color blue :hint nil) ("b" (lambda () (interactive) (ha-new-window :left :buffer)) "switch buffer") ("f" (lambda () (interactive) (ha-new-window :left :file)) "load file ") ("t" (lambda () (interactive) (ha-new-window :left :term)) "terminal") ("h" split-window-right "split window"))) #+end_src This means that, without thinking, the following just works: - ~SPC w s s s~ :: creates a window directly below this. - ~SPC w n n n~ :: creates a window directly to the right. But, more importantly, the prefix ~w s~ gives me more precision to view what I need. ** Search Operations Ways to search for information goes under the ~s~ key. The venerable sage has always been =grep=, but we now have new-comers, like [[https://github.com/BurntSushi/ripgrep][ripgrep]], which are really fast. *** ripgrep Install the [[https://github.com/dajva/rg.el][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. #+begin_src emacs-lisp (use-package rg :config ;; Make an interesting Magit-like menu of options, which I don't use much: (rg-enable-default-bindings (kbd "M-R")) ;; Old habits die hard ... (define-key global-map [remap xref-find-references] 'rg-dwim) (ha-leader "s" '(:ignore t :which-key "search") "s " '(keyboard-escape-quit :which-key t) "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) "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))) #+end_src Note we bind the key ~M-R~ to the [[help:rg-menu][rg-menu]], which is a Magit-like interface to =ripgrep=. I don’t understand the bug associated with the =:general= extension to =use-package=, but it /works/, but stops everything else from working, so pulling it out into its own =use-package= section addresses that issue: #+begin_src emacs-lisp (use-package rg :general (:states 'normal "gS" 'rg-dwim)) #+end_src *** wgrep The [[https://github.com/mhayashi1120/Emacs-wgrep][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: #+begin_src emacs-lisp (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))) #+end_src ** Text Operations Stealing much of this from Spacemacs. #+begin_src emacs-lisp (ha-leader "x" '(:ignore t :which-key "text") "x " '(keyboard-escape-quit :which-key t) "x a" '("align" . align-regexp) "x q" '("fill paragraph" . fill-paragraph) "x p" '("unfill paragraph" . unfill-paragraph)) #+end_src Unfilling a paragraph joins all the lines in a paragraph into a single line. Taken [[http://www.emacswiki.org/UnfillParagraph][from here]] … I use this all the time: #+begin_src emacs-lisp (defun unfill-paragraph () "Convert a multi-line paragraph into a single line of text." (interactive) (let ((fill-column (point-max))) (fill-paragraph nil))) #+end_src ** 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: #+begin_src emacs-lisp (ha-leader "h" '(:ignore t :which-key "help") "h " '(keyboard-escape-quit :which-key t) "h ." '("cursor position" . what-cursor-position) "h a" '("apropos" . apropos-command) "h c" '("elisp cheatsheet" . shortdoc-display-group) "h e" '("errors" . view-echo-area-messages) "h f" '("function" . helpful-callable) "h F" '("font" . describe-font) "h =" '("face" . describe-face) "h k" '("key binding" . helpful-key) "h K" '("key map" . describe-keymap) "h m" '("mode" . describe-mode) "h o" '("symbol" . describe-symbol) "h p" '("package" . describe-package) "h s" '("info symbol" . info-lookup-symbol) "h v" '("variable" . helpful-variable) "h i" '("info" . info) "h I" '("info manual" . info-display-manual) "h j" '("info jump" . info-apropos) "h E" '("emacs info" . (lambda () (interactive) (info "emacs"))) "h L" '("emacs-lisp" . (lambda () (interactive) (info "elisp"))) "h O" '("org info" . (lambda () (interactive) (info "org"))) ;; Since I do a lot of literate programming, I appreciate a quick ;; jump directly into the Info manual... "h B" '("org babel" . (lambda () (interactive) (org-info-open "org#Working with Source Code" nil)))) #+end_src 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: #+begin_src emacs-lisp (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 #+end_src ** Consult The [[https://github.com/minad/consult][consult project]] aims to use libraries like [[*Vertico][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). #+begin_src emacs-lisp (use-package consult :after general ;; Enable automatic preview at point in the *Completions* buffer. This is ;; relevant when you use the default completion UI. :hook (completion-list-mode . consult-preview-at-point-mode) :init ;; Use Consult to select xref locations with preview (setq xref-show-xrefs-function #'consult-xref xref-show-definitions-function #'consult-xref) (ha-leader "RET" '("bookmark" . consult-bookmark) "o i" '("imenu" . consult-imenu) "x y" '("preview yank" . consult-yank-pop)) :bind ("s-v" . consult-yank-pop) :general (:states 'normal "gp" '("preview paste" . 'consult-yank-pop) "gs" '("go to line" . 'consult-line))) #+end_src ** Embark The [[https://github.com/oantolin/embark/][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? #+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) :config (ha-leader "h K" '("keybindings" . embark-bindings))) #+end_src In [[https://karthinks.com/software/fifteen-ways-to-use-embark/][15 Ways to Use Embark]], Karthik Chikmagalur suggests a nifty macro for integrating Embark with [[Ace Window][Ace Window]]: #+begin_src emacs-lisp (use-package embark :after ace-window :config (defmacro my/embark-ace-action (fn) `(defun ,(intern (concat "my/embark-ace-" (symbol-name fn))) () (interactive) (with-demoted-errors "%s" (require 'ace-window) (let ((aw-dispatch-always t)) (aw-switch-to-window (aw-select nil)) (call-interactively (symbol-function ',fn)))))) (defmacro my/embark-split-action (fn split-type) `(defun ,(intern (concat "my/embark-" (symbol-name fn) "-" (car (last (split-string (symbol-name split-type) "-"))))) () (interactive) (funcall #',split-type) (call-interactively #',fn))) ;; Use the macros to define some helper functions: (my/embark-ace-action find-file) ; --> my/embark-ace-find-file (my/embark-ace-action switch-to-buffer) ; --> my/embark-ace-switch-to-buffer (my/embark-ace-action bookmark-jump) ; --> my/embark-ace-bookmark-jump (my/embark-split-action find-file split-window-below) ; --> my/embark-find-file-below (my/embark-split-action find-file split-window-right) ; --> my/embark-find-file-right (my/embark-split-action switch-to-buffer split-window-below) ; --> my/embark-switch-to-buffer-below (my/embark-split-action switch-to-buffer split-window-right) ; --> my/embark-switch-to-buffer-right (my/embark-split-action bookmark-jump split-window-below) ; --> my/embark-bookmark-jump-below (my/embark-split-action bookmark-jump split-window-right)) ; --> my/embark-bookmark-jump-right #+end_src We can rebind the various =embark-xyz-map= with calls to our macroized functions: #+begin_src emacs-lisp (use-package embark :bind (:map embark-file-map ("y" . embark-copy-as-kill) ("Y" . embark-save-relative-path) ("W" . nil) ("w" . my/embark-ace-find-file) ("2" . my/embark-find-file-below) ("3" . my/embark-find-file-right) :map embark-buffer-map ("y" . embark-copy-as-kill) ("w" . my/embark-ace-switch-to-buffer) ("2" . my/embark-switch-to-buffer-below) ("3" . my/embark-switch-to-buffer-right) :map embark-file-map ("y" . embark-copy-as-kill) ("w" . my/embark-ace-bookmark-jump) ("2" . my/embark-bookmark-jump-below) ("3" . my/embark-bookmark-jump-right))) #+end_src According to [[https://elpa.gnu.org/packages/embark-consult.html#orgc76b5de][this essay]], Embark cooperates well with the [[https://github.com/minad/marginalia][Marginalia]] and [[https://github.com/minad/consult][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: #+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 According to the [[https://elpa.gnu.org/packages/embark-consult.html][Embark-Consult page]]: #+begin_quote Users of the popular [[https://github.com/justbur/emacs-which-key][which-key]] package may prefer to use the =embark-which-key-indicator= from the [[https://github.com/oantolin/embark/wiki/Additional-Configuration#use-which-key-like-a-key-menu-prompt][Embark wiki]]. Just copy its definition from the wiki into your configuration and customize the =embark-indicators= user option to exclude the mixed and verbose indicators and to include =embark-which-key-indicator=. #+end_quote 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. #+begin_src emacs-lisp (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) #+end_src * Technical Artifacts :noexport: Let's =provide= a name so we can =require= this file: #+begin_src emacs-lisp :exports none (provide 'ha-leader) ;;; ha-leader.el ends here #+end_src #+DESCRIPTION: defining leaders with general #+PROPERTY: header-args:sh :tangle no #+PROPERTY: header-args:emacs-lisp :tangle yes #+PROPERTY: header-args :results none :eval no-export :comments no mkdirp yes #+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