hamacs/ha-config.org

871 lines
41 KiB
Org Mode
Raw Normal View History

2023-12-03 18:57:36 +00:00
#+title: General Emacs Configuration
#+author: Howard X. Abrams
#+date: 2020-09-10
#+tags: emacs
A literate programming file for configuring Emacs.
#+begin_src emacs-lisp :exports none
;;; ha-config --- Emacs configuration. -*- lexical-binding: t; -*-
;;
;; © 2020-2023 Howard X. Abrams
;; Licensed under a Creative Commons Attribution 4.0 International License.
;; See http://creativecommons.org/licenses/by/4.0/
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
;; 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.
;;
;;; Code:
#+end_src
* Basic Configuration
I begin configuration of Emacs that isnt /package-specific/. For instance, I hate to fat-finger a single letter that could stop Emacs:
#+begin_src emacs-lisp
(setq confirm-kill-emacs 'yes-or-no-p)
#+end_src
Emacs has some new code to display line-numbers:
#+begin_src emacs-lisp
(setq display-line-numbers t
display-line-numbers-type 'visual)
#+end_src
I like the rendering of curved quotes using [[help:text-quoting-style][text-quoting-style]], because it improves the readability of documentation strings in the =Help= buffer and whatnot.
#+begin_src emacs-lisp
(setq text-quoting-style 'curve
truncate-string-ellipsis "…")
#+end_src
When typing prose in Org documents, I need to [[file:ha-org-word-processor.org::*Typographic Quotes][do something else]] for rounded quotes and ellipsis.
Changes and settings I like introduced that were introduced in Emacs 28:
#+begin_src emacs-lisp
(setq use-short-answers t
describe-bindings-outline t
completions-detailed t)
#+end_src
In Emacs version 28, we can hide commands in ~M-x~ which do not apply to the current mode.
#+begin_src emacs-lisp
(setq read-extended-command-predicate
#'command-completion-default-include-p)
#+end_src
Before that, we used ~M-X~ (capital ~X~).
When I get an error, I need a stack trace to figure out the problem. Yeah, when I stop fiddling with Emacs, this should go off:
#+begin_src emacs-lisp
(setq debug-on-error t)
#+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
And some Mac-specific settings:
#+begin_src emacs-lisp
(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)))
#+end_src
** Emacs Everywhere
After reading [[https://irreal.org/blog/?p=12139][Jon Sanders essay]] as well as [[https://mbork.pl/2024-04-27_Emacs_everywhere][Marcin Borkowski's essay]], I decided to try out Tecosaurs [[https://github.com/tecosaur/emacs-everywhere][Emacs Everywhere]] approach:
#+begin_src emacs-lisp
(use-package emacs-everywhere
:straight (:host github :repo "tecosaur/emacs-everywhere"))
#+end_src
This package is /called outside of Emacs/, so I bound a keybinding to iCanHazShortcut:
#+begin_src sh
emacsclient --socket-name personal --eval "(emacs-everywhere)"
#+end_src
When you type ~C-c C-c~ to close a window, it /doesnt always/ paste back into the original window, but the text is saved to the clipboard, a quick paste works. And now, I dont scream when I need to use those Electron apps, like Slack and Discord.
** File Access
*** Remote Files
To speed up TRAMP access, lets disabled lock files, you know, the ones that have the =#= surrounding characters:
#+begin_src emacs-lisp
(setq remote-file-name-inhibit-locks t)
#+end_src
What do I think about [[elisp:(describe-variable 'remote-file-name-inhibit-auto-save-visited)][remote-file-name-inhibit-auto-save-visited]]?
During remote access, TRAMP can slow down performing Git operations. Lets turn that off as well:
#+begin_src emacs-lisp
(defun turn-off-vc-for-remote-files ()
"Disable"
(when (file-remote-p (buffer-file-name))
(setq-local vc-handled-backends nil)))
(add-hook 'find-file-hook 'turn-off-vc-for-remote-files)
#+end_src
*** Changes on Save
Always spaces and never tabs. Note that we use =setq-default= since [[elisp:(describe-variable 'indent-tabs-mode)][indent-tabs-mode]] is a /buffer-local/ variable, meaning using =setq=, sets it for /that buffer file/. We want this globally the default:
#+begin_src emacs-lisp
(setq-default indent-tabs-mode nil)
#+end_src
When I push changes to my files to Gerrit and other code review, I dont want trailing spaces or any tabs to appear, so lets fix all files when I [[elisp:(describe-variable 'before-save-hook)][save them]]:
#+begin_src emacs-lisp
(defun ha-cleanup-buffer-file ()
"Cleanup a file, often done before a file save."
(interactive)
(ignore-errors
(unless (or (equal major-mode 'makefile-mode)
(equal major-mode 'makefile-bsdmake-mode))
(untabify (point-min) (point-max)))
(delete-trailing-whitespace)))
(add-hook 'before-save-hook #'ha-cleanup-buffer-file)
#+end_src
*** Recent Files
The [[https://www.emacswiki.org/emacs/RecentFiles][recentf]] feature has been in Emacs for a long time, but it has a problem with Tramp, as we need to turn off the cleanup feature that attempts to =stat= all the files and remove them from the =recent= accessed list if they are readable. The requires recentf to open up a remote files which blocks Emacs at the most inopportune times… like when trying to reboot the machine.
#+begin_src emacs-lisp
(use-package recentf
:straight (:type built-in)
:config
(setq recentf-auto-cleanup 'never) ;; disable before we start recentf!
(recentf-mode 1))
#+end_src
*** File Backups
While I use git as much as I can, sometimes Emacs built-in file backup and versioning feature has saved me for files that arent.
As [[https://philjackson.github.io//emacs/backups/2022/01/31/keeping-backups-of-every-edited-file/][Phil Jackson]] mentioned, Emacs has a lot of variations to its file backup strategy, and either change the [[help:backup-directory-alist][backup-directory-alist]] to put individual file backups elsewhere, e.g.
#+begin_src emacs-lisp
(setq backup-directory-alist `(("." . ,(concat user-emacs-directory "backups"))))
#+end_src
Or leave them in the current directory, but create an alias so =ls= doesnt display them, e.g.
#+begin_src sh
alias ls="ls --color=auto --hide='*~'"
#+end_src
I'm leaving them side-by-side, but I am keeping some extra copies:
#+begin_src emacs-lisp
(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)
#+end_src
The [[help:version-control][version-control]] variable affect backups (not some sort of global VC setting), this makes numeric backups.
*** Auto Save of Files
Save the file whenever I move away from Emacs (see [[https://irreal.org/blog/?p=10314][this essay]]):
#+begin_src emacs-lisp
(defun save-all-buffers ()
"Saves all buffers, because, why not?"
(interactive)
(save-some-buffers t))
(add-hook 'focus-out-hook 'save-all-buffers)
#+end_src
*** Download Files via URL
Might be nice to have a =url-download= function that just grabs a file from a website without fuss (or other dependencies). Easy enough to prototype, but dealing with errors are another thing …
#+begin_src emacs-lisp
(defun url-download (url dest)
"Download the file as URL and save in file, DEST.
Note that this doesn't do any error checking ATM."
(interactive "sURL: \nDDestination: ")
(let* ((url-parts (url-generic-parse-url url))
(url-path (url-filename url-parts))
(filename (file-name-nondirectory url-path))
(target (if (file-directory-p dest)
(file-name-concat dest filename)
dest))
(callback (lambda (status destination)
(unwind-protect
(pcase status
(`(:error . ,_)
(message "Error downloading %s: %s" url (plist-get status :error)))
(_ (progn
;; (switch-to-buffer (current-buffer))
(delete-region (point-min) (1+ url-http-end-of-headers))
(write-file destination)
(kill-buffer)
(when (called-interactively-p 'any)
(kill-new destination)))))))))
(message "Retrieving %s into %s" url target)
(url-retrieve url callback (list target))))
#+end_src
This function can be called interactively with a URL and a directory (and it attempts to create the name of the destination file based on the latter-part of the URL), or called programmatically, like:
#+begin_src emacs-lisp :tangle no
(url-download "https://www.emacswiki.org/emacs/download/bookmark+.el"
"~/Downloads/bookmark-plus.el")
#+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 like [[https://github.com/raxod502/selectrum#vertico][Selectrum]], it extends Emacs built-in functionality, instead of adding a new process. This means all these projects work together.
#+begin_src emacs-lisp
(use-package vertico
:config (vertico-mode))
#+end_src
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:
#+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
*** 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.
#+begin_src emacs-lisp
(use-package hotfuzz)
#+end_src
While flexible at matching, you have to get the /order/ correct. For instance, ~alireg~ matches with [[help:align-regexp][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 [[https://github.com/oantolin/orderless][orderless]] project allows those words to be in any order.
#+begin_src emacs-lisp
(use-package orderless
:commands (orderless-filter)
:custom
(completion-ignore-case t)
(completion-category-defaults nil)
(completion-category-overrides '((file (styles partial-completion))))
2024-02-26 21:25:33 +00:00
:init
2024-02-26 21:25:33 +00:00
(defvar orderless-skip-highlighting nil
"Not sure why this is being accessed.")
(push 'orderless completion-styles))
#+end_src
*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 [[https://github.com/jojojames/fussy][fussy]] project is a fuzzy pattern matching extension for the normal [[help:completing-read][completing-read]] interface. By default, it uses [[https://github.com/lewang/flx][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 [[https://github.com/oantolin/orderless][orderless]], I would need to put a space between the words. While I will continue to play with the different mechanism, Ill combine =hotfuzz= and =orderless=.
#+begin_src emacs-lisp
(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))
#+end_src
*** 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
2022-10-21 03:50:40 +00:00
(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
(add-to-list 'marginalia-command-categories '(project-find-file . file))
(marginalia-mode))
#+end_src
* Key Bindings
The [[https://github.com/justbur/emacs-which-key][which-key]] project shows a menu of available key-bindings based on what you have already typed. For instance, if you remember that Org Goto function (like most Org-related functions) began with ~C-c~, after typing that sequence, all possible keybindings and their functions are shown. Useful for discovering new features.
#+begin_src emacs-lisp
2022-10-21 03:50:40 +00:00
(use-package which-key
:init (setq which-key-popup-type 'minibuffer)
:config (which-key-mode))
#+end_src
Why would I ever quit Emacs with a simple keybinding on a Mac? Lets override it:
#+begin_src emacs-lisp
(global-set-key (kbd "s-q") 'bury-buffer)
#+end_src
** Undo
The [[https://gitlab.com/ideasman42/emacs-undo-fu][undo-fu]] isnt much to the project (thats a good thing), but It doesnt /cycle/ around the redo ring, which can be annoying.
2022-03-25 18:01:04 +00:00
#+begin_src emacs-lisp
2022-10-21 03:50:40 +00:00
(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))
#+end_src
While I usually use ~C-/~ for [[help:undo][undo]] (and ~C-?~ for [[help:undo-redo][redo]]), when Im on the Mac, I need to cover my bases.
** Leader Sequences
Pressing the ~SPACE~ can activate a /leader key sequence/ I define in my [[file:ha-leader.org][ha-leader]] file.
#+begin_src emacs-lisp
(ha-hamacs-load "ha-general.org")
#+end_src
This extends the =use-package= to include a =:general= keybinding section.
Since I seldom remember keybindings, or even function names, for major-modes, I pull them all together into a nice table using the [[https://github.com/jerrypnz/major-mode-hydra.el][Major Mode Hydra]] project:
#+begin_src emacs-lisp
(use-package major-mode-hydra
:config
(global-set-key (kbd "s-,") #'major-mode-hydra))
#+end_src
** Additional Global Packages
The following defines my use of the Emacs completion system. Ive decided my /rules/ will be:
- Nothing should automatically appear; that is annoying and distracting.
- Spelling in org files (abbrev or hippie expander) and code completion are separate, but Im not sure if I can split them
- IDEs overuse the ~TAB~ binding, and I should re-think the bindings.
I dont find the Emacs completion system obvious, with different interfaces, some distinct, some connected. Heres the summary as I understand:
#+begin_verse
=indent-for-tab-command=, which /we can/ call:
└─ =completion-at-point=, which calls:
└─ =completion-at-point-functions= (capf), which can call:
└─ hippie and dabbrev functions
#+end_verse
In =org-mode=, ~TAB~ calls [[help:org-cycle][org-cycle]], which, in the context of typing text, calls the binding for ~TAB~, which is the [[help:indent-for-tab-command][indent-for-tab-command]]. If the line is /indented/, I can complete the word:
#+begin_src emacs-lisp
(setq tab-always-indent 'complete
tab-first-completion 'word-or-paren
completion-cycle-threshold nil)
#+end_src
Note that no matter the setting for =tab-first-completion=, hitting ~TAB~ twice, results in completion.
This calls [[help:completion-at-point][completion-at-point]]. This code (from mini-buffer) doubles with the other [[Vertico][completing processes]] (like [[help:completing-read][completing-read]]) and presents choices based on a series of functions (see [[https://with-emacs.com/posts/tutorials/customize-completion-at-point/][this essay]] for details). This will call into the CAPF function list (see the variable, =completion-at-point-functions= and the [[file:ha-programming.org::*Cape][Cape]] section for details).
*** Hippie Expand
The venerable [[help:hippie-expand][hippie-expand]] function does a better job than the default, [[help:dabbrev-expand][dabbrev-expand]], so lets swap it out (see this [[https://www.masteringemacs.org/article/text-expansion-hippie-expand][essay]] by Mickey Petersen) with its default key of ~M-/~ (easy to type on the laptop) as well as ~C-Tab~ (easier on mechanical keyboards):
#+begin_src emacs-lisp
(global-set-key [remap dabbrev-expand] 'hippie-expand)
(global-set-key (kbd "M-<tab>") 'completion-at-point)
#+end_src
Details on its job? We need to update its [[help:hippie-expand-try-functions-list][list of expanders]]. I dont care much for [[help:try-expand-line][try-expand-line]], so that is not on the list.
#+begin_src emacs-lisp
(setq hippie-expand-try-functions-list
'(try-complete-file-name-partially ; complete filenames, start with /
try-complete-file-name
yas-hippie-try-expand ; expand matching snippets
try-expand-all-abbrevs
try-expand-list ; help when args repeated another's args
try-expand-dabbrev
try-expand-dabbrev-all-buffers
try-expand-whole-kill ; grab text from the kill ring
try-expand-dabbrev-from-kill ; as above
try-complete-lisp-symbol-partially
try-complete-lisp-symbol))
#+end_src
In the shell, IDEs and other systems, the key binding is typically ~TAB~. In modes other than =org-mode=, ~TAB~ re-indents the line with [[help:indent-for-tab-command][indent-for-tab-command]], but I find that I want that feature when Im in Evils =normal state= and hit the ~=~ key, so changing this sounds good. But why not /have both/?
#+begin_src emacs-lisp :tangle no
(advice-add #'indent-for-tab-command :after #'hippie-expand)
#+end_src
*** Corfu
The default completion system either inserts the first option directly in the text (without cycling, so lets hope it gets it right the first time), or presents choices in another buffer (who wants to hop to it to select an expansion).
After using [[http://company-mode.github.io/][company]] for my completion back-end, I switch to [[https://github.com/minad/corfu][corfu]] as it works with the variable-spaced font of my org files (also see [[https://takeonrules.com/2022/01/17/switching-from-company-to-corfu-for-emacs-completion/][this essay]] for my initial motivation).
#+begin_src emacs-lisp
(use-package corfu
:custom
(corfu-cycle t)
(corfu-separator ?\s)
:init
(global-corfu-mode))
#+end_src
*** Snippets
Using [[https://github.com/joaotavora/yasnippet][yasnippet]] to expand templates into text:
#+begin_src emacs-lisp
(use-package yasnippet
:config
(add-to-list 'yas-snippet-dirs
(expand-file-name "snippets" user-emacs-directory))
(yas-global-mode +1))
#+end_src
Check out [[http://joaotavora.github.io/yasnippet/][the documentation]] for writing them.
Since I have troubles installing Dooms [[https://github.com/hlissner/doom-snippets][collection of snippets]], lets use the [[http://github.com/AndreaCrotti/yasnippet-snippets][yasnippet-snippets]] package:
#+begin_src emacs-lisp
(use-package yasnippet-snippets)
#+end_src
*** Auto Insert Templates
The [[https://www.emacswiki.org/emacs/AutoInsertMode][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 [[https://github.com/hlissner/doom-emacs/blob/develop/modules/editor/file-templates/autoload.el][set-file-template!]] macro, but simpler?
#+begin_src emacs-lisp
(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))
#+end_src
Since auto insertion requires entering data for particular fields, and for that Yasnippet is better, so in this case, we combine them:
#+begin_src emacs-lisp
(defun ha-autoinsert-yas-expand()
"Replace text in yasnippet template."
(let ((orig-mode major-mode)
(auto-insert-query nil)
(yas-indent-line nil))
(yas/minor-mode 1)
(when (fboundp 'evil-insert-state)
(evil-insert-state))
(yas-expand-snippet (buffer-string) (point-min) (point-max))))
#+end_src
And since I'll be associating snippets with new files all over my configuration, let's make a helper function:
#+begin_src emacs-lisp
(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)))
#+end_src
As an example of its use, any Org files loaded in /this project/ should insert my config file:
#+begin_src emacs-lisp
(ha-auto-insert-file (rx "hamacs/" (one-or-more any) ".org" eol) "hamacs-config")
(ha-auto-insert-file (rx ".dir-locals.el") "dir-locals.el")
#+end_src
** Additional Global Packages
*** Visual Replacing Regular Expressions
I appreciated the [[https://github.com/benma/visual-regexp.el][visual-regexp package]] to see what you want to change /before/ executing the replace.
#+begin_src emacs-lisp
(use-package visual-regexp
:bind (("C-c r" . vr/replace)
("C-c q" . vr/query-replace))
:general (:states 'normal "g r" '("replace" . vr/replace))
:config (ha-leader
"r" '("replace" . vr/replace)
"R" '("query replace" . vr/query-replace)))
#+end_src
For all other functions that use regular expressions, many call the function, =read-regexp=, and thought it would be helpful if I could type =rx:…= and allow me to take advantage of the =rx= macro.
#+begin_src emacs-lisp
(defun read-regexp-with-rx (input)
"Advice for `read-regexp' to allow specifying `rx' expressions.
If INPUT starts with rx: then the rest of the input is given to
the `rx' macro, and function returns that regular expression.
Otherwise, return INPUT."
(if (string-match (rx bos "rx:" (zero-or-more space)
(group (one-or-more any)))
input)
(let* ((rx-input (match-string 1 input))
(rx-expr (format "(rx %s)" rx-input)))
(message "%s and %s" rx-input rx-expr)
(eval (read rx-expr)))
input))
#+end_src
Lets right a little test case to make sure it works:
#+begin_src emacs-lisp :tangle no
(ert-deftest read-regexp-with-rx-test ()
(should (equal (read-regexp-with-rx "foo|bar") "foo|bar"))
(should (equal (read-regexp-with-rx "rx:\"foobar\"") "foobar"))
(should (equal (read-regexp-with-rx "rx:bol (zero-or-more space) eol") "^[[:space:]]*$")))
#+end_src
Now we just need to filter the results from the built-in Emacs function:
#+begin_src emacs-lisp
(advice-add 'read-regexp :filter-return 'read-regexp-with-rx)
#+end_src
*** Jump with Avy
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, but ~g o~ seems obvious.
#+begin_src emacs-lisp
(use-package avy
:init
(setq avy-all-windows t
avy-single-candidate-jump nil ; May want to yank the candidate
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" . avy-goto-char-timer)
"s" '("avy word" . avy-goto-subword-1))
:bind ("<f18>" . avy-goto-char-timer)
("s-g" . avy-goto-char-timer)
("s-;" . avy-next)
("s-a" . avy-prev))
#+end_src
*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 special actions (check out this [[https://karthinks.com/software/avy-can-do-anything/][great essay]] about this understated feature):
- ~n~ :: copies the matching target word, well, from the target to the end of the word, so match at the beginning.
- ~x~ :: =kill-word= … which puts it in the kill-ring to be pasted later.
- ~X~ :: =kill-stay= … kills the target, but leaves the cursor in the current place.
- ~t~ :: =teleport= … bring the word at the target to the current point … great in the shell.
- ~m~ :: =mark= … select the word at target
- ~y~ :: =yank= … puts any word on the screen on the clipbard.
- ~Y~ :: =yank-line= … puts the entire target line on the clipboard.
- ~i~ :: =ispell= … fix spelling from a distance.
- ~z~ :: =zap-to-char= … kill from current point to the target
Im not thinking of ideas of what would be useful, e.g. ~v~ to highlight from cursor to target, etc.
*** Link Hint, the Link Jumper
2022-12-21 19:28:17 +00:00
The [[info:emacs#Goto Address mode][Goto Address]] mode (see this [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Goto-Address-mode.html][online link]]) turns URLs into clickable links. Nice feature and built into Emacs, but it requires using the mouse or moving to the URL and hitting ~Return~ (if you like this idea, check out [[https://xenodium.com/actionable-urls-in-emacs-buffers/][Álvaro Ramírez's configuration]] for this).
I appreciated [[https://github.com/abo-abo/ace-link][ace-link]]s idea for hyperlinks on Org, EWW and Info pages, as it allowed you to jump to a URL from any location on the screen. The [[https://github.com/noctuid/link-hint.el][link-hint]] project does this, but works with more types of files and links:
#+begin_src emacs-lisp
(use-package link-hint
:bind
("s-o" . link-hint-open-link)
2023-06-16 05:48:14 +00:00
("s-y" . link-hint-copy-link)
:general
(:states 'normal
"gl" '("open link" . link-hint-open-link)
2023-06-16 05:48:14 +00:00
"gL" '("open link→window" . link-hint-open-link-ace-window)
"gm" '("copy link" . 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))
#+end_src
Can I open a link in another window? The idea with this is that I can select a link, and with multiple windows open, I can specify where the =*eww*= window should show the link. If only two windows, then the new EWW buffer shows in the /other/ one.
#+begin_src emacs-lisp
(defun link-hint-open-link-ace-window ()
(interactive)
(link-hint-copy-link)
2023-06-16 05:48:14 +00:00
(ace-select-window)
(eww (current-kill 0)))
#+end_src
*** Expand Region
Magnar Sveen's [[https://github.com/magnars/expand-region.el][expand-region]] project allows me to hit ~v~ in =visual= mode, and have the selection grow by syntactical units.
#+begin_src emacs-lisp
(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
"V" 'er/contract-region
"-" 'er/contract-region))
#+end_src
2024-04-12 21:55:21 +00:00
*** iSearch
The built-in =isearch= is fantastically simple and useful, but the [[https://github.com/kickingvegas/cc-isearch-menu][cc-isearch-menu]] helps expose some /buried/ features.
#+begin_src emacs-lisp
(use-package cc-isearch-menu
:straight (:host github :repo "kickingvegas/cc-isearch-menu")
:bind (:map isearch-mode-map ("s-g" . cc-isearch-menu-transient)))
#+end_src
The idea, is that you can start a search with ~C-s~ (or even ~s-f~ … er, ~Command-f~ on the Mac), and type some letters. Hitting ~C-s~ goes to the next occurrence of what youve typed, but if you hit ~Command-g~, a menu appears allowing you to pull in the rest of the word or symbol you are looking at, or edit it completely.
* 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
Since I wasnt using all the features that [[https://github.com/bbatsov/projectile][projectile]] provides, I have switched to the built-in =project= functions.
#+begin_src emacs-lisp
(use-package emacs
:config
(ha-leader
2022-10-21 03:50:40 +00:00
"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" . project-shell-command)
"p &" '("run cmd async" . project-async-shell-command)
"p a" '("add new project" . project-remember-projects-under)
"p d" '("remove known project" . project-forget-project)
"p k" '("kill project buffers" . project-kill-buffers)
"p p" '("switch project" . project-switch-project)
"p f" '("find file" . project-find-file)
"p F" '("find file o/win" . project-find-file-other-window)
"p b" '("switch to project buffer" . project-switch-to-buffer)
"p C" '("compile in project" . compile-project)
"p c" '("recompile" . recompile)
"p e" '("project shell" . project-eshell)
"p s" '("project shell" . project-shell)))
#+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%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) (persp-name (persp-curr))) "*" ""))
(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-show-modestring t)
:config
(setq persp-suppress-no-prefix-key-warning t)
(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 "
2021-11-18 17:46:10 +00:00
("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)
("d" persp-kill)
("W" ha-workspace-initialize)
("a" persp-add-buffer)
("b" persp-switch-to-buffer)
("k" persp-remove-buffer)
("K" persp-kill-buffer)
("m" persp-merge)
("u" persp-unmerge)
("i" persp-import)
("r" persp-rename)
("s" persp-state-save)
("l" persp-state-load)
("w" ha-switch-to-special) ; The most special perspective
("q" nil)
("C-g" nil)))
#+end_src
Lets give it a binding:
#+begin_src emacs-lisp
2022-10-21 03:50:40 +00:00
(ha-leader "TAB" '("workspaces" . hydra-workspace-leader/body))
#+end_src
When called, it /can/ look like:
[[file:screenshots/projects-hydra.png]]
The /special/ perspective is a nice shortcut to the one I use the most:
#+begin_src emacs-lisp
2022-10-21 03:50:40 +00:00
(defun ha-switch-to-special ()
"Change to the projects perspective."
(interactive)
(persp-switch "projects"))
#+end_src
I often want a workspace dedicated to an /application/, so this function:
#+begin_src emacs-lisp
(defun ha-app-perspective (name func)
(lambda ()
(interactive)
(let ((already-started? (seq-contains-p (persp-names) name 'equal)))
(persp-switch name)
(unless already-started?
(call-interactively func)))))
#+end_src
And I can then use it like:
#+begin_src emacs-lisp :tangle no
(ha-leader "a x" `("to foobar" . ,(ha-app-perspective "foobar" #'foobar)))
#+end_src
2022-03-09 04:43:25 +00:00
*** 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.
#+begin_src emacs-lisp
(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")))
#+end_src
Given a list of information about project-workspaces, can we create them all?
#+begin_src emacs-lisp
(defun ha-persp-exists? (name)
"Return non-nill if a perspective of NAME exists."
2022-03-09 04:43:25 +00:00
(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))))
(persp-switch "main"))
#+end_src
Often, but not always, I want a perspective based on an actual Git repository, e.g. a project. Emacs calls these transients.
#+begin_src emacs-lisp
(defun ha-project-persp (project &optional name files)
"Create a new perspective, and then switch to the PROJECT.
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
2022-02-11 07:16:24 +00:00
README. Otherwise, pull up Dired."
(interactive (list (completing-read "Project: "
(project-known-project-roots))))
(when (f-directory-p project)
(unless name
(setq name (f-filename project)))
(persp-switch name)
;; Unclear if the following is actually necessary.
(ignore-errors
(project-remember-project root)
(project-switch-project root))
(let ((recent-files (thread-last recentf-list
(--filter (s-starts-with? project it))
(-take 3)))
(readme-org (f-join project "README.org"))
2023-08-04 15:29:54 +00:00
(readme-org (f-join project "README.md"))
(readme-md (f-join project "README.rst")))
2022-02-04 22:38:56 +00:00
(cond
(files (ha--project-show-files project files))
(recent-files (ha--project-show-files project recent-files))
2022-02-04 22:38:56 +00:00
((f-exists? readme-org) (find-file readme-org))
((f-exists? readme-md) (find-file readme-md))
(t (dirvish project))))))
#+end_src
When starting a new perspective, and I specify more than one file, this function splits 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)."
(when files
(let ((default-directory root)
(file (car files))
(more (cdr files)))
(message "Loading files from %s ... %s and %s" root file more)
(when (f-exists? file)
(find-file file))
(when more
(split-window-horizontally)
(ha--project-show-files root more)))))
#+end_src
The =persp-switch= allows me to select or create a new project, but what if we insisted on a new workspace?
#+begin_src emacs-lisp
(defun ha-new-persp (name)
(interactive "sNew Workspace: ")
(persp-switch name)
(cond
((s-ends-with? "mail" name) (notmuch))
((s-starts-with? "twit" name) (twit))))
#+end_src
Once we create the new perspective workspace, if it matches a particular name, I pretty much know what function I would like to call.
* Pretty Good Encryption
For details on using GnuPG in Emacs, see Mickey Petersens [[https://www.masteringemacs.org/article/keeping-secrets-in-emacs-gnupg-auth-sources][GnuPG Essay]].
On Linux, GPG is pretty straight-forward, but on the Mac, I often have troubles doing:
#+begin_src sh
brew install gpg
#+end_src
Next, on every reboot, start the agent:
#+begin_src sh
/opt/homebrew/bin/gpg-agent --daemon
#+end_src
Also, as [[https://www.bytedude.com/gpg-in-emacs/][bytedude]] mentions, I need to use the =epa-pineentry-mode= to =loopback= to actually get a prompt for the password, instead of an error. Also 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
(use-package epa-file
:straight (:type built-in)
:custom
(epg-debug t)
(auth-source-debug t)
;; Since I normally want symmetric encryption, and don't want
;; to use the "key selection":
(epa-file-select-keys 'symmetric-only)
;; Make sure we prompt in the minibuffer for the password:
(epg-pinentry-mode 'loopback)
;; I trust my Emacs session, so I don't bother expiring my pass:
(auth-source-cache-expiry nil))
#+end_src
Need to make sure that Emacs will handle the prompts, and turn it on:
#+begin_src emacs-lisp
(use-package epa-file
:config
(setenv "GPG_AGENT_INFO" nil)
(epa-file-enable))
#+end_src
* Technical Artifacts :noexport:
Let's provide a name so we can =require= this file:
#+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:t todo:nil tasks:nil tags:nil date:nil
#+options: skip:nil author:nil email:nil creator:nil timestamp:nil
#+infojs_opt: view:nil toc:t ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js