1412 lines
67 KiB
Org Mode
1412 lines
67 KiB
Org Mode
#+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 one .
|
||
;;
|
||
;;; Code:
|
||
#+end_src
|
||
* Basic Configuration
|
||
I begin with configuration of Emacs that isn’t /package-specific/. For instance, I hate a fat-finger that stop Emacs:
|
||
#+begin_src emacs-lisp
|
||
(setq confirm-kill-emacs 'yes-or-no-p)
|
||
#+end_src
|
||
|
||
New way to display line-numbers. I set mine to =relative= so that I can jump up and down by that value. Set this to =nil= to turn off, or =t= to be absolute.
|
||
#+begin_src emacs-lisp
|
||
(setq display-line-numbers t
|
||
display-line-numbers-type 'relative)
|
||
#+end_src
|
||
|
||
I like the rendering to 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)
|
||
#+end_src
|
||
|
||
Changes and settings I like 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
|
||
|
||
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. Also, as [[https://www.bytedude.com/gpg-in-emacs/][bytedude]] mentions, I need to se the =epa-pineentry-mode= to =loopback= to actually get a prompt for the password, instead of an error.
|
||
#+begin_src emacs-lisp
|
||
(use-package epa-file
|
||
:config
|
||
(defvar epa-pinentry-mode)
|
||
(setq epa-file-select-keys nil
|
||
epa-pinentry-mode 'loopback
|
||
auth-sources '("~/.authinfo.gpg")
|
||
auth-source-cache-expiry nil))
|
||
#+end_src
|
||
|
||
Unicode ellispis are nicer than three dots:
|
||
#+begin_src emacs-lisp
|
||
(setq truncate-string-ellipsis "…")
|
||
#+end_src
|
||
More settings:
|
||
|
||
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
|
||
|
||
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
|
||
* Basic Configuration
|
||
** Initial Settings and UI
|
||
Let's turn off the menu and other settings:
|
||
#+begin_src emacs-lisp
|
||
(when (display-graphic-p)
|
||
(context-menu-mode 1)
|
||
(tool-bar-mode -1)
|
||
(scroll-bar-mode -1)
|
||
(horizontal-scroll-bar-mode -1)
|
||
(setq visible-bell 1
|
||
frame-inhibit-implied-resize 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
|
||
** File Access
|
||
*** Remote Files
|
||
To speed up TRAMP access, let’s 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. Let’s 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 don’t want trailing spaces or any tabs to appear, so let’s 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 aren’t.
|
||
|
||
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= doesn’t 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))))
|
||
:init
|
||
(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, I’ll 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
|
||
(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 '(projectile-find-file . file))
|
||
(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
|
||
Why would I ever quit Emacs with a simple keybinding? Let’s override it:
|
||
#+begin_src emacs-lisp
|
||
(global-set-key (kbd "s-q") 'bury-buffer)
|
||
#+end_src
|
||
** Undo
|
||
I mean, I /always/ use ~C-/~ for [[help:undo][undo]] (and ~C-?~ for [[help:undo-redo][redo]]), but when I’m on the Mac, I need to cover my bases.
|
||
|
||
Why use [[https://gitlab.com/ideasman42/emacs-undo-fu][undo-fu]] instead of the built-in undo functionality? Well, there isn’t much to the project (that’s a good thing), but It basically doesn’t /cycle/ around the redo, which annoying.
|
||
|
||
#+begin_src emacs-lisp
|
||
(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
|
||
** On the Subject of Being Evil
|
||
I’m currently using the [[https://github.com/emacs-evil/evil][Extensible VI Layer]] for Emacs, aka /evil/. This configuration happens in [[file:ha-evil.org][ha-evil.org]], but because much of my configuration requires access to =ha-leader=, =general=, and other features, we need to include it here:
|
||
#+begin_src emacs-lisp
|
||
(ha-hamacs-load "ha-evil")
|
||
#+end_src
|
||
** Additional Global Packages
|
||
The following defines my use of the Emacs completion system. I’ve 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 I’m not sure if I can split them
|
||
- IDEs overuse the ~TAB~ binding, and I should re-think the bindings.
|
||
|
||
*** Auto Completion
|
||
I don’t find the Emacs completion system obvious, with different interfaces, some distinct, some connected. Here’s 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 let’s 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 don’t 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 I’m in Evil’s =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 let’s 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
|
||
* 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
|
||
(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 Doom’s [[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
|
||
*** Visual Replace with Visual 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
|
||
|
||
Let’s 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
|
||
I’m not thinking of ideas of what would be useful, e.g. ~v~ to highlight from cursor to target, etc.
|
||
*** Link Hint, the Link Jumper
|
||
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)
|
||
("s-y" . link-hint-copy-link)
|
||
:general
|
||
(:states 'normal
|
||
"gl" '("open link" . link-hint-open-link)
|
||
"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)
|
||
(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
|
||
* Working Layout
|
||
While editing any file on disk is easy enough, I like the mental context switch associated with a full-screen window frame showing all the buffers of a /project task/ (often a direct link to a repository project, but not always).
|
||
** Projects
|
||
While I don't /need/ all the features that [[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)
|
||
(projectile-project-root-functions '(projectile-root-bottom-up))
|
||
|
||
:config
|
||
(ha-leader
|
||
"p" '(:ignore t :which-key "projects")
|
||
"p W" '("initialize workspace" . ha-workspace-initialize)
|
||
"p n" '("new project space" . ha-project-persp)
|
||
"p !" '("run cmd in project root" . projectile-run-shell-command-in-root)
|
||
"p &" '("async cmd in project root" . projectile-run-async-shell-command-in-root)
|
||
"p a" '("add new project" . projectile-add-known-project)
|
||
"p b" '("switch to project buffer" . projectile-switch-to-buffer)
|
||
"p C" '("compile in project" . projectile-compile-project)
|
||
"p c" '("recompile" . recompile)
|
||
"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/said that I shouldsaid that I shouldsaid that I should 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) (projectile-project-name)) "*" ""))
|
||
(ha--persp-label (1+ num) (cdr names)))))
|
||
|
||
(defun ha-persp-labels ()
|
||
"Return a string of numbered elements from a list of names."
|
||
(ha--persp-label 1 (sort (hash-table-keys (perspectives-hash)) 's-less?)))
|
||
#+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 "
|
||
("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
|
||
|
||
I have no idea why this binding doesn’t work /within/ the =use-package= declaration, but oh well…
|
||
#+begin_src emacs-lisp
|
||
(ha-leader "TAB" '("workspaces" . hydra-workspace-leader/body))
|
||
#+end_src
|
||
|
||
The /special/ perspective is a nice shortcut to the one I use the most:
|
||
#+begin_src emacs-lisp
|
||
(defun ha-switch-to-special ()
|
||
"Change to the projects perspective."
|
||
(interactive)
|
||
(persp-switch "projects"))
|
||
#+end_src
|
||
*** 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."
|
||
(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. Projectile keeps state of a "project" based on the current file loaded, so we /combine/ the two projects by first choosing from a list of /known projects/ and then creating a perspective based on the name. To pin the perspective to a project, we load a file from it, e.g. Like a README or something.
|
||
|
||
#+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 Dired."
|
||
(interactive (list (projectile-completing-read "Project: " projectile-known-projects)))
|
||
(when (f-directory-p project)
|
||
(unless name
|
||
(setq name (f-filename project)))
|
||
(persp-switch name)
|
||
|
||
;; Unclear if the following is actually necessary.
|
||
(ignore-errors
|
||
(projectile-add-known-project root)
|
||
(let ((projectile-switch-project-action nil))
|
||
(projectile-switch-project-by-name root)))
|
||
|
||
;; To pin a project in projectile to the perspective, we need to load a file
|
||
;; from that project. The README will do, or at least, the dired of it.
|
||
(let ((recent-files (thread-last recentf-list
|
||
(--filter (s-starts-with? project it))
|
||
(-take 3)))
|
||
(readme-org (f-join project "README.org"))
|
||
(readme-org (f-join project "README.md"))
|
||
(readme-md (f-join project "README.rst")))
|
||
(cond
|
||
(files (ha--project-show-files project files))
|
||
(recent-files (ha--project-show-files project recent-files))
|
||
((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.
|
||
* Applications
|
||
Can we 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
|
||
;; See https://github.com/magit/magit/wiki/Emacsclient for why we need to set:
|
||
:custom (with-editor-emacsclient-executable "emacsclient")
|
||
|
||
: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 u" '("Git Update" . vc-update)
|
||
|
||
"g g" '("Magit status" . magit-status)
|
||
"g s" '("Magit status here" . magit-status-here)
|
||
"g D" '("Magit file delete" . magit-file-delete)
|
||
"g B" '("Magit blame" . magit-blame-addition)
|
||
"g C" '("Magit clone" . magit-clone)
|
||
"g F" '("Magit fetch" . magit-fetch)
|
||
"g L" '("Magit buffer log" . magit-log-buffer-file)
|
||
"g R" '("Revert file" . magit-file-checkout)
|
||
"g S" '("Git stage file" . magit-stage-file)
|
||
"g U" '("Git unstage file" . magit-unstage-file)
|
||
|
||
"g f" '(:ignore t :which-key "find")
|
||
"g f f" '("Find file" . magit-find-file)
|
||
"g f g" '("Find gitconfig file" . magit-find-git-config-file)
|
||
"g f c" '("Find commit" . magit-show-commit)
|
||
|
||
"g l" '(:ignore t :which-key "list")
|
||
"g l r" '("List repositories" . magit-list-repositories)
|
||
"g l s" '("List submodules" . magit-list-submodules)
|
||
|
||
"g o" '(:ignore t :which-key "open")
|
||
|
||
"g c" '(:ignore t :which-key "create")
|
||
"g c R" '("Initialize repo" . magit-init)
|
||
"g c C" '("Clone repo" . magit-clone)
|
||
"g c c" '("Commit" . magit-commit-create)
|
||
"g c f" '("Fixup" . magit-commit-fixup)
|
||
"g c b" '("Branch" . magit-branch-and-checkout)))
|
||
#+end_src
|
||
*** Git Gutter
|
||
The [[https://github.com/syohex/emacs-git-gutter-fringe][git-gutter-fringe]] project displays markings in the fringe (extreme left margin) to show modified and uncommitted lines. This project builds on [[https://github.com/emacsorphanage/git-gutter][git-gutter]] project to provide movement between /hunks/:
|
||
#+begin_src emacs-lisp
|
||
(use-package git-gutter-fringe
|
||
:custom
|
||
;; To have both flymake and git-gutter work, we put
|
||
;; git-gutter on the right side:
|
||
(git-gutter-fr:side 'right-fringe)
|
||
(left-fringe-width 15)
|
||
(right-fringe-width 10)
|
||
|
||
:config
|
||
(set-face-foreground 'git-gutter-fr:modified "yellow")
|
||
(set-face-foreground 'git-gutter-fr:added "green")
|
||
(set-face-foreground 'git-gutter-fr:deleted "red")
|
||
|
||
(global-git-gutter-mode)
|
||
|
||
(ha-leader
|
||
"g n" '("next hunk" . git-gutter:next-hunk)
|
||
"g p" '("previous hunk" . git-gutter:previous-hunk)
|
||
"g e" '("end of hunk" . git-gutter:end-of-hunk)
|
||
"g r" '("revert hunk" . git-gutter:revert-hunk)
|
||
"g s" '("stage hunk" . git-gutter:stage-hunk)))
|
||
#+end_src
|
||
*** Git Delta
|
||
The [[https://scripter.co/using-git-delta-with-magit][magit-delta]] project uses [[https://github.com/dandavison/delta][git-delta]] for colorized diffs.
|
||
#+begin_src emacs-lisp
|
||
(use-package magit-delta
|
||
:ensure t
|
||
:hook (magit-mode . magit-delta-mode))
|
||
#+end_src
|
||
I also need to append the following to my [[file:~/.gitconfig][~/.gitconfig]] file:
|
||
#+begin_src conf
|
||
[delta]
|
||
minus-style = normal "#8f0001"
|
||
minus-non-emph-style = normal "#8f0001"
|
||
minus-emph-style = normal bold "#d01011"
|
||
minus-empty-line-marker-style = normal "#8f0001"
|
||
zero-style = syntax
|
||
plus-style = syntax "#006800"
|
||
plus-non-emph-style = syntax "#006800"
|
||
plus-emph-style = syntax "#009000"
|
||
plus-empty-line-marker-style = normal "#006800"
|
||
#+end_src
|
||
*** Git with Difftastic
|
||
I’m stealing the code for this section from [[https://tsdh.org/posts/2022-08-01-difftastic-diffing-with-magit.html][this essay]] by Tassilo Horn, and in fact, I’m going to lift a lot of his explanation too, as I may need to remind myself how this works. The idea is based on using Wilfred’s excellent [[https://github.com/Wilfred/difftastic][difftastic]] tool to do a structural/syntax comparison of code changes in git. To begin, install the binary:
|
||
#+begin_src sh
|
||
brew install difftastic # and the equivalent on Linux
|
||
#+end_src
|
||
Next, we can do this, to use this as a diff tool for everything.
|
||
#+begin_src emacs-lisp
|
||
(setenv "GIT_EXTERNAL_DIFF" "difft")
|
||
#+end_src
|
||
But perhaps integrating it into Magit and selectively calling it (as it is slow). Tassilo suggests making the call to =difft= optional by first creating a helper function to set the =GIT_EXTERNAL_DIFF= to =difft=:
|
||
#+begin_src emacs-lisp
|
||
(defun th/magit--with-difftastic (buffer command)
|
||
"Run COMMAND with GIT_EXTERNAL_DIFF=difft then show result in BUFFER."
|
||
(let ((process-environment
|
||
(cons (concat "GIT_EXTERNAL_DIFF=difft --width="
|
||
(number-to-string (frame-width)))
|
||
process-environment)))
|
||
;; Clear the result buffer (we might regenerate a diff, e.g., for
|
||
;; the current changes in our working directory).
|
||
(with-current-buffer buffer
|
||
(setq buffer-read-only nil)
|
||
(erase-buffer))
|
||
;; Now spawn a process calling the git COMMAND.
|
||
(make-process
|
||
:name (buffer-name buffer)
|
||
:buffer buffer
|
||
:command command
|
||
;; Don't query for running processes when emacs is quit.
|
||
:noquery t
|
||
;; Show the result buffer once the process has finished.
|
||
:sentinel (lambda (proc event)
|
||
(when (eq (process-status proc) 'exit)
|
||
(with-current-buffer (process-buffer proc)
|
||
(goto-char (point-min))
|
||
(ansi-color-apply-on-region (point-min) (point-max))
|
||
(setq buffer-read-only t)
|
||
(view-mode)
|
||
(end-of-line)
|
||
;; difftastic diffs are usually 2-column side-by-side,
|
||
;; so ensure our window is wide enough.
|
||
(let ((width (current-column)))
|
||
(while (zerop (forward-line 1))
|
||
(end-of-line)
|
||
(setq width (max (current-column) width)))
|
||
;; Add column size of fringes
|
||
(setq width (+ width
|
||
(fringe-columns 'left)
|
||
(fringe-columns 'right)))
|
||
(goto-char (point-min))
|
||
(pop-to-buffer
|
||
(current-buffer)
|
||
`(;; If the buffer is that wide that splitting the frame in
|
||
;; two side-by-side windows would result in less than
|
||
;; 80 columns left, ensure it's shown at the bottom.
|
||
,(when (> 80 (- (frame-width) width))
|
||
#'display-buffer-at-bottom)
|
||
(window-width . ,(min width (frame-width))))))))))))
|
||
#+end_src
|
||
The crucial parts of this helper function are that we "wash" the result using =ansi-color-apply-on-region= so that the function can transform the difftastic highlighting using shell escape codes to Emacs faces. Also, note the need to possibly change the width, as difftastic makes a side-by-side comparison.
|
||
|
||
The functions below depend on [[help:magit-thing-at-point][magit-thing-at-point]], and this depends on the [[https://sr.ht/~pkal/compat/][compat]] library, so let’s grab that stuff:
|
||
#+begin_src emacs-lisp :tangle no
|
||
(use-package compat
|
||
:straight (:host github :repo "emacs-straight/compat"))
|
||
|
||
(use-package magit-section
|
||
:commands magit-thing-at-point)
|
||
#+end_src
|
||
Next, let's define our first command basically doing a =git show= for some revision which defaults to the commit or branch at point or queries the user if there's none.
|
||
#+begin_src emacs-lisp
|
||
(defun th/magit-show-with-difftastic (rev)
|
||
"Show the result of \"git show REV\" with GIT_EXTERNAL_DIFF=difft."
|
||
(interactive
|
||
(list (or
|
||
;; Use if given the REV variable:
|
||
(when (boundp 'rev) rev)
|
||
;; If not invoked with prefix arg, try to guess the REV from
|
||
;; point's position.
|
||
(and (not current-prefix-arg)
|
||
(or (magit-thing-at-point 'git-revision t)
|
||
(magit-branch-or-commit-at-point)))
|
||
;; Otherwise, query the user.
|
||
(magit-read-branch-or-commit "Revision"))))
|
||
(if (not rev)
|
||
(error "No revision specified")
|
||
(th/magit--with-difftastic
|
||
(get-buffer-create (concat "*git show difftastic " rev "*"))
|
||
(list "git" "--no-pager" "show" "--ext-diff" rev))))
|
||
#+end_src
|
||
And here the second command which basically does a =git diff=. It tries to guess what one wants to diff, e.g., when point is on the Staged changes section in a magit buffer, it will run =git diff --cached= to show a diff of all staged changes. If it can not guess the context, it'll query the user for a range or commit for diffing.
|
||
#+begin_src emacs-lisp
|
||
(defun th/magit-diff-with-difftastic (arg)
|
||
"Show the result of \"git diff ARG\" with GIT_EXTERNAL_DIFF=difft."
|
||
(interactive
|
||
(list (or
|
||
;; Use If RANGE is given, just use it.
|
||
(when (boundp 'range) range)
|
||
;; If prefix arg is given, query the user.
|
||
(and current-prefix-arg
|
||
(magit-diff-read-range-or-commit "Range"))
|
||
;; Otherwise, auto-guess based on position of point, e.g., based on
|
||
;; if we are in the Staged or Unstaged section.
|
||
(pcase (magit-diff--dwim)
|
||
('unmerged (error "unmerged is not yet implemented"))
|
||
('unstaged nil)
|
||
('staged "--cached")
|
||
(`(stash . ,value) (error "stash is not yet implemented"))
|
||
(`(commit . ,value) (format "%s^..%s" value value))
|
||
((and range (pred stringp)) range)
|
||
(_ (magit-diff-read-range-or-commit "Range/Commit"))))))
|
||
(let ((name (concat "*git diff difftastic"
|
||
(if arg (concat " " arg) "")
|
||
"*")))
|
||
(th/magit--with-difftastic
|
||
(get-buffer-create name)
|
||
`("git" "--no-pager" "diff" "--ext-diff" ,@(when arg (list arg))))))
|
||
#+end_src
|
||
|
||
What's left is integrating the new show and diff commands in Magit. For that purpose, Tasillo created a new transient prefix for all personal commands. Intriguing, but I have a hack that I can use on a leader:
|
||
#+begin_src emacs-lisp
|
||
(defun ha-difftastic-here ()
|
||
(interactive)
|
||
(call-interactively
|
||
(if (eq major-mode 'magit-log-mode)
|
||
'th/magit-show-with-difftastic
|
||
'th/magit-diff-with-difftastic)))
|
||
|
||
(ha-leader "g d" '("difftastic" . ha-difftastic-here))
|
||
#+end_src
|
||
*** Time Machine
|
||
The [[https://github.com/emacsmirror/git-timemachine][git-timemachine]] project visually shows how a code file changes with each iteration:
|
||
#+begin_src emacs-lisp
|
||
(use-package git-timemachine
|
||
:config
|
||
(ha-leader "g t" '("git timemachine" . git-timemachine)))
|
||
#+end_src
|
||
*** Gist
|
||
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 :tangle no
|
||
(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
|
||
|
||
The gist project depends on the [[https://github.com/sigma/gh.el][gh library]]. There seems to be a problem with it.
|
||
#+begin_src emacs-lisp :tangle no
|
||
(use-package gh
|
||
:straight (:host github :repo "sigma/gh.el"))
|
||
#+end_src
|
||
|
||
*** Forge
|
||
Let's extend Magit with [[https://github.com/magit/forge][Magit Forge]] for working with Github and Gitlab:
|
||
#+begin_src emacs-lisp :tangle no
|
||
(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
|
||
|
||
Every /so often/, pop over to the following URLs and generate a new token where the *Note* is =forge=, and then copy that into the [[file:~/.authinfo.gpg][~/.authinfo.gpg]]:
|
||
- [[https://gitlab.com/-/user_settings/personal_access_tokens][Gitlab]]
|
||
- [[https://github.com/settings/tokens][Github]]
|
||
and make sure this works:
|
||
|
||
#+begin_src emacs-lisp :tangle no :results replace
|
||
(ghub-request "GET" "/user" nil
|
||
:forge 'github
|
||
:host "api.github.com"
|
||
:username "howardabrams"
|
||
:auth 'forge)
|
||
#+end_src
|
||
*** Pushing is Bad
|
||
Pushing directly to the upstream branch is /bad form/, as one should create a pull request, etc. To prevent an accidental push, we /double-check/ first:
|
||
|
||
#+begin_src emacs-lisp
|
||
(define-advice magit-push-current-to-upstream (:before (args) query-yes-or-no)
|
||
"Prompt for confirmation before permitting a push to upstream."
|
||
(when-let ((branch (magit-get-current-branch)))
|
||
(unless (yes-or-no-p (format "Push %s branch upstream to %s? "
|
||
branch
|
||
(or (magit-get-upstream-branch branch)
|
||
(magit-get "branch" branch "remote"))))
|
||
(user-error "Push to upstream aborted by user"))))
|
||
#+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
|
||
(ha-leader "a b" '("eww browser" . eww))
|
||
|
||
:general
|
||
(:states 'normal :keymaps 'eww-mode-map
|
||
"B" 'eww-list-bookmarks
|
||
"Y" 'eww-copy-page-url
|
||
"H" 'eww-back-url
|
||
"L" 'eww-forward-url
|
||
"u" 'eww-top-url
|
||
"p" 'eww-previous-url
|
||
"n" 'eww-next-url
|
||
"q" 'bury-buffer)
|
||
(:states 'normal :keymaps 'eww-buffers-mode-map
|
||
"q" 'bury-buffer))
|
||
#+end_src
|
||
|
||
This function allows Imenu to offer HTML headings in EWW buffers, helpful for navigating long, technical documents.
|
||
#+begin_src emacs-lisp
|
||
(use-package eww
|
||
:config
|
||
(defun unpackaged/imenu-eww-headings ()
|
||
"Return alist of HTML headings in current EWW buffer for Imenu.
|
||
Suitable for `imenu-create-index-function'."
|
||
(let ((faces '(shr-h1 shr-h2 shr-h3 shr-h4 shr-h5 shr-h6 shr-heading)))
|
||
(save-excursion
|
||
(save-restriction
|
||
(widen)
|
||
(goto-char (point-min))
|
||
(cl-loop for next-pos = (next-single-property-change (point) 'face)
|
||
while next-pos
|
||
do (goto-char next-pos)
|
||
for face = (get-text-property (point) 'face)
|
||
when (cl-typecase face
|
||
(list (cl-intersection face faces))
|
||
(symbol (member face faces)))
|
||
collect (cons (buffer-substring (point-at-bol) (point-at-eol)) (point))
|
||
and do (forward-line 1))))))
|
||
:hook (eww-mode .
|
||
(lambda ()
|
||
(setq-local imenu-create-index-function #'unpackaged/imenu-eww-headings))))
|
||
#+end_src
|
||
*** Get Pocket
|
||
The [[https://github.com/alphapapa/pocket-reader.el][pocket-reader]] project connects to the [[https://getpocket.com/en/][Get Pocket]] service.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package pocket-reader
|
||
:init
|
||
(setq org-web-tools-pandoc-sleep-time 1)
|
||
:config
|
||
(ha-leader "o p" '("get pocket" . pocket-reader))
|
||
|
||
;; Instead of jumping into Emacs mode to get the `pocket-mode-map',
|
||
;; we add the keybindings to the normal mode that makes sense.
|
||
:general
|
||
(:states 'normal :keymaps 'pocket-reader-mode-map
|
||
"RET" 'pocket-reader-open-url
|
||
"TAB" 'pocket-reader-pop-to-url
|
||
|
||
"*" 'pocket-reader-toggle-favorite
|
||
"B" 'pocket-reader-open-in-external-browser
|
||
"D" 'pocket-reader-delete
|
||
"E" 'pocket-reader-excerpt-all
|
||
"F" 'pocket-reader-show-unread-favorites
|
||
"M" 'pocket-reader-mark-all
|
||
"R" 'pocket-reader-random-item
|
||
"S" 'tabulated-list-sort
|
||
"a" 'pocket-reader-toggle-archived
|
||
"c" 'pocket-reader-copy-url
|
||
"d" 'pocket-reader
|
||
"e" 'pocket-reader-excerpt
|
||
"f" 'pocket-reader-toggle-favorite
|
||
"l" 'pocket-reader-limit
|
||
"m" 'pocket-reader-toggle-mark
|
||
"o" 'pocket-reader-more
|
||
"q" 'quit-window
|
||
"s" 'pocket-reader-search
|
||
"u" 'pocket-reader-unmark-all
|
||
"t a" 'pocket-reader-add-tags
|
||
"t r" 'pocket-reader-remove-tags
|
||
"t s" 'pocket-reader-tag-search
|
||
"t t" 'pocket-reader-set-tags
|
||
|
||
"g s" 'pocket-reader-resort
|
||
"g r" 'pocket-reader-refresh))
|
||
#+end_src
|
||
|
||
Use these special keywords when searching:
|
||
|
||
- =:*=, =:favorite= Return favorited items.
|
||
- =:archive= Return archived items.
|
||
- =:unread= Return unread items (default).
|
||
- =:all= Return all items.
|
||
- =:COUNT= Return at most /COUNT/ (a number) items. This limit persists until you start a new search.
|
||
- =:t:TAG=, =t:TAG= Return items with /TAG/ (you can search for one tag at a time, a limitation of the Pocket API).
|
||
*** External Browsing
|
||
Browsing on a work laptop is a bit different. According to [[http://ergoemacs.org/emacs/emacs_set_default_browser.html][this page]], I can set a /default browser/ for different URLs, which is great, as I can launch my browser for personal browsing, or another browser for work access, or even EWW. To make this clear, I'm using the abstraction associated with [[https://github.com/rolandwalker/osx-browse][osx-browse]]:
|
||
#+begin_src emacs-lisp
|
||
(use-package osx-browse
|
||
:init
|
||
(setq browse-url-handlers
|
||
'(("docs\\.google\\.com" . osx-browse-url-personal)
|
||
("grafana.com" . osx-browse-url-personal)
|
||
("dndbeyond.com" . osx-browse-url-personal)
|
||
("tabletopaudio.com" . osx-browse-url-personal)
|
||
("youtu.be" . osx-browse-url-personal)
|
||
("youtube.com" . osx-browse-url-personal)
|
||
("." . eww-browse-url)))
|
||
|
||
:config
|
||
(defun osx-browse-url-personal (url &optional new-window browser focus)
|
||
"Open URL in Firefox for my personal surfing.
|
||
The parameters, URL, NEW-WINDOW, and FOCUS are as documented in
|
||
the function, `osx-browse-url'."
|
||
(interactive (osx-browse-interactive-form))
|
||
(cl-callf or browser "org.mozilla.Firefox")
|
||
(osx-browse-url url new-window browser focus)))
|
||
#+end_src
|
||
** Dirvish
|
||
The [[https://github.com/alexluigit/dirvish][dirvish]] project aims to be a better =dired=. And since the =major-mode= is still =dired-mode=, the decades of finger memory isn’t lost. For people starting to use =dired=, most commands are pretty straight-forward (and Prot did a pretty good [[https://www.youtube.com/watch?v=5dlydii7tAU][introduction]] to it), but to remind myself, keep in mind:
|
||
|
||
- ~%~ :: will /mark/ a bunch of files based on a regular expression
|
||
- ~m~ :: marks a single file
|
||
- ~d~ :: marks a file to delete, type ~x~ to follow-through on all files marked for deletion.
|
||
- ~u~ :: un-mark a file, or type ~!~ to un-mark all
|
||
- ~t~ :: to toggle the marked files. Keep files with =xyz= extension? Mark those with ~%~, and then ~t~ toggle.
|
||
- ~C~ :: copy the current file or all marked files
|
||
- ~R~ :: rename/move the current file or all marked files
|
||
- ~M~ :: change the mode (=chmod=) of current or marked files, accepts symbols, like =a+x=
|
||
|
||
Note that =dired= has /two marks/ … one is a general mark, and the other is specifically a mark of files to delete.
|
||
|
||
Dirvish does require the following supporting programs, but I’ve already got those puppies installed:
|
||
#+begin_src sh
|
||
brew install coreutils fd poppler ffmpegthumbnailer mediainfo imagemagick
|
||
#+end_src
|
||
|
||
I’m beginning with dirvish to use the [[https://github.com/alexluigit/dirvish/blob/main/docs/CUSTOMIZING.org][sample configuration]] and change it:
|
||
#+begin_src emacs-lisp
|
||
(use-package dirvish
|
||
:straight (:host github :repo "alexluigit/dirvish")
|
||
:init
|
||
(dirvish-override-dired-mode)
|
||
|
||
:custom
|
||
(dirvish-quick-access-entries
|
||
'(("h" "~/" "Home")
|
||
("p" "~/personal" "Personal")
|
||
("p" "~/projects" "Projects")
|
||
("t" "~/technical" "Technical")
|
||
("w" "~/website" "Website")
|
||
("d" "~/Downloads/" "Downloads")))
|
||
|
||
:config
|
||
;; This setting is like `treemacs-follow-mode' where the buffer
|
||
;; changes based on the current file. Not sure if I want this:
|
||
;; (dirvish-side-follow-mode)
|
||
|
||
(setq dirvish-mode-line-format
|
||
'(:left (sort symlink) :right (omit yank index)))
|
||
(setq dirvish-attributes
|
||
'(all-the-icons file-time file-size collapse subtree-state vc-state git-msg))
|
||
|
||
(setq delete-by-moving-to-trash t
|
||
dired-auto-revert-buffer t)
|
||
|
||
;; With `ls' as an alias, and `gls' available on _some_ of my systems, I dont:
|
||
;; (setq insert-directory-program "gls")
|
||
;; And instead use Emacs' built-in directory lister:
|
||
(setq insert-directory-program nil)
|
||
(setq ls-lisp-use-insert-directory-program nil)
|
||
(require 'ls-lisp)
|
||
(setq dired-listing-switches
|
||
"-l --almost-all --human-readable --group-directories-first --no-group")
|
||
|
||
(set-face-attribute 'dirvish-hl-line nil :background "darkmagenta"))
|
||
#+end_src
|
||
While in =dirvish-mode=, we can rebind some keys:
|
||
#+begin_src emacs-lisp
|
||
(use-package dirvish
|
||
:bind
|
||
(:map dirvish-mode-map ; Dirvish inherits `dired-mode-map'
|
||
("a" . dirvish-quick-access)
|
||
("f" . dirvish-file-info-menu)
|
||
("y" . dirvish-yank-menu)
|
||
("N" . dirvish-narrow)
|
||
("^" . dirvish-history-last)
|
||
("h" . dirvish-history-jump) ; remapped `describe-mode'
|
||
("q" . dirvish-quit)
|
||
("s" . dirvish-quicksort) ; remapped `dired-sort-toggle-or-edit'
|
||
("v" . dirvish-vc-menu) ; remapped `dired-view-file'
|
||
("TAB" . dirvish-subtree-toggle)
|
||
("M-f" . dirvish-history-go-forward)
|
||
("M-b" . dirvish-history-go-backward)
|
||
("M-l" . dirvish-ls-switches-menu)
|
||
("M-m" . dirvish-mark-menu)
|
||
("M-t" . dirvish-layout-toggle)
|
||
("M-s" . dirvish-setup-menu)
|
||
("M-e" . dirvish-emerge-menu)
|
||
("M-j" . dirvish-fd-jump)))
|
||
#+end_src
|
||
** ediff
|
||
Love me ediff, but with monitors that are wider than they are tall, let’s put the diffs side-by-side:
|
||
#+begin_src emacs-lisp
|
||
(setq ediff-split-window-function 'split-window-horizontally)
|
||
#+end_src
|
||
Frames, er, windows, are actually annoying for me, as Emacs is always in full-screen mode.
|
||
#+begin_src emacs-lisp
|
||
(setq ediff-window-setup-function 'ediff-setup-windows-plain)
|
||
#+end_src
|
||
When =ediff= is finished, it leaves the windows /borked/. This is annoying, but according to [[http://yummymelon.com/devnull/surprise-and-emacs-defaults.html][this essay]], we can fix it:
|
||
#+begin_src emacs-lisp
|
||
(defvar my-ediff-last-windows nil
|
||
"Session for storing window configuration before calling `ediff'.")
|
||
|
||
(defun my-store-pre-ediff-winconfig ()
|
||
"Store `current-window-configuration' in variable `my-ediff-last-windows'."
|
||
(setq my-ediff-last-windows (current-window-configuration)))
|
||
|
||
(defun my-restore-pre-ediff-winconfig ()
|
||
"Restore window configuration to stored value in `my-ediff-last-windows'."
|
||
(set-window-configuration my-ediff-last-windows))
|
||
|
||
(add-hook 'ediff-before-setup-hook #'my-store-pre-ediff-winconfig)
|
||
(add-hook 'ediff-quit-hook #'my-restore-pre-ediff-winconfig)
|
||
#+end_src
|
||
** Annotations
|
||
Let's try [[https://github.com/bastibe/annotate.el][annotate-mode]], which allows you to drop "notes" and then move to them (yes, serious overlap with bookmarks, which we will return to).
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package annotate
|
||
:config
|
||
(ha-leader
|
||
"t A" '("annotations" . annotate-mode)
|
||
|
||
"n" '(:ignore t :which-key "notes")
|
||
"n a" '("toggle mode" . annotate-mode)
|
||
"n n" '("annotate" . annotate-annotate)
|
||
"n d" '("delete" . annotate-delete)
|
||
"n s" '("summary" . annotate-show-annotation-summary)
|
||
"n j" '("next" . annotate-goto-next-annotation)
|
||
"n k" '("prev" . annotate-goto-previous-annotation)
|
||
|
||
;; If a shift binding isn't set, it defaults to non-shift version
|
||
;; Use SPC N N to jump to the next error:
|
||
"n N" '("next error" . flycheck-next-error)))
|
||
#+end_src
|
||
Keep the annotations simple, almost /tag-like/, and then the summary allows you to display them.
|
||
** Keepass
|
||
Use the [[https://github.com/ifosch/keepass-mode][keepass-mode]] to view a /read-only/ version of my Keepass file in Emacs:
|
||
#+begin_src emacs-lisp
|
||
(use-package keepass-mode)
|
||
#+end_src
|
||
When having your point on a key entry, you can copy fields to kill-ring using:
|
||
- ~u~ :: URL
|
||
- ~b~ :: user name
|
||
- ~c~ :: password
|
||
|
||
** Demo It
|
||
Making demonstrations /within/ Emacs with my [[https://github.com/howardabrams/demo-it][demo-it]] project. While on MELPA, I want to use my own cloned version to make sure I can keep debugging it.
|
||
#+begin_src emacs-lisp
|
||
(use-package demo-it
|
||
:straight (:local-repo "~/other/demo-it")
|
||
;; :straight (:host github :repo "howardabrams/demo-it")
|
||
:commands (demo-it-create demo-it-start))
|
||
#+end_src
|
||
** PDF Viewing
|
||
Why not [[https://github.com/politza/pdf-tools][view PDF files]] better? If you have standard build tools installed on your system, run [[help:pdf-tools-install][pdf-tools-install]], as this command will an =epdfinfo= program to PDF displays.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package pdf-tools
|
||
:mode ("\\.pdf\\'" . pdf-view-mode)
|
||
:init
|
||
(if (ha-running-on-macos?)
|
||
(setq pdf-info-epdfinfo-program "/opt/homebrew/bin/epdfinfo")
|
||
(setq pdf-info-epdfinfo-program "/usr/local/bin/epdfinfo"))
|
||
:general
|
||
(:states 'normal :keymaps 'pdf-view-mode-map
|
||
"gp" 'pdf-view-goto-page
|
||
">" 'doc-view-fit-window-to-page))
|
||
#+end_src
|
||
|
||
Make sure the [[help:pdf-info-check-epdfinfo][pdf-info-check-epdfinfo]] function works.
|
||
* 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
|