GNU Emacs configuration
An advanced literate configuration that produces modular code
Last revised and exported on 2024-12-18 09:58:56 +0200 with a word count of 106378.
This is my literate Emacs configuration file. It is a combination of prose and code. You can either read this page or check my dotfiles to find everything related to my Emacs setup.
What you are now reading is not a common literate configuration of
Emacs. In most such cases, you have a generic init.el
with a call to
the org-babel-load-file
function that gets an Org document as its
value. That method works but is very slow, because we have to load Org
before starting Emacs (and Org loads a bunch of other things we do not
need at such an early stage).
Whereas this Org document serves as (i) a single point of entry to my Emacs setup and (ii) the origin of all of my Emacs configurations. While I am defining everything in a single Org file, I am not actually starting Emacs by reading this file. Rather, I am instructing Org to put the code blocks defined herein in standalone files, organised by scope. The end result is something where you cannot tell whether a literate program was executed or not.
This is the beauty of it. I can keep editing a single file as the “source of truth”, though I can still handle each of the files individually (e.g. someone wants to see how I do a specific thing, so I share only that file as an email attachment—no need to send over this massive document).
When I want to modify my Emacs setup, I edit this file and then
evaluate the following code block or do C-c C-v C-t
. All files will
be updated accordingly.
(org-babel-tangle)
Note that I always build Emacs from source because I maintain lots of packages and need to be on the bleeding edge (Details of my Emacs build). This means that my code may not necessarily work with your version of Emacs.
- Website: https://protesilaos.com/emacs/dotemacs
- Git repositories:
- Video demo: https://protesilaos.com/codelog/2023-12-18-emacs-org-advanced-literate-conf/
- Backronyms: Do Observe, Transpose, Examine, or Mirror All Configurations, Stranger (dotemacs); Dotfiles Operate Transparently For the Included Linux and Emacs Setups (dotfiles).
Table of Contents
- 1. Details of my Emacs build
- 2. Anatomy of my Emacs configuration
- 3. The early initialisation of Emacs (
early-init.el
)- 3.1. The
early-init.el
macro to run code only in a Desktop Environment - 3.2. The
early-init.el
code to set frame parameters - 3.3. The
early-init.el
basic frame settings - 3.4. The
early-init.el
tweaks to startup time and garbage collection - 3.5. The
early-init.el
initialises the package cache - 3.6. The
early-init.el
defines general theme-related functions - 3.7. The
early-init.el
takes care to avoid the initial flash of light - 3.8. The
early-init.el
gives a name to the default frame
- 3.1. The
- 4. The main initialisation of Emacs (
init.el
)- 4.1. The
init.el
user options- 4.1.1. The
init.el
user option to load a theme family - 4.1.2. The
init.el
user option to load a minibuffer user interface - 4.1.3. The
init.el
user option to load extras for minibuffer completion - 4.1.4. The
init.el
user option to load treesitter extras - 4.1.5. The
init.el
user option to enablewhich-key
- 4.1.6. The
init.el
user option to load icons (nerd-icons.el
)
- 4.1.1. The
- 4.2. The
init.el
basic configurations to disable backups and lockfiles - 4.3. The
init.el
tweaks to make native compilation silent - 4.4. The
init.el
setting to sendcustom-file
to oblivion - 4.5. The
init.el
settings for multilingual editing (input methods) - 4.6. The
init.el
settings to enable certain commands and disable others - 4.7. The
init.el
setting to always start with the*scratch*
buffer - 4.8. The
init.el
arrangements for my own modules and custom libraries - 4.9. The
init.el
settings for packages (package.el
) - 4.10. The
init.el
option to declare all themes as safe - 4.11. The
init.el
macro to do nothing with Elisp code (prot-emacs-comment
) - 4.12. The
init.el
macro to bind keys (prot-emacs-keybind
) - 4.13. The
init.el
macro to define abbreviations (prot-emacs-abbrev
) - 4.14. The
init.el
addition of highlighting for my macros - 4.15. The
init.el
final part to load the individual modules
- 4.1. The
- 5. The modules of my Emacs configuration
- 5.1. The
prot-emacs-theme.el
module- 5.1.1. The
prot-emacs-theme.el
section to load a theme (prot-emacs-load-theme-family
) - 5.1.2. The
prot-emacs-theme.el
section forpulsar
- 5.1.3. The
prot-emacs-theme.el
section forlin
- 5.1.4. The
prot-emacs-theme.el
section forspacious-padding
- 5.1.5. The
prot-emacs-theme.el
section forrainbow-mode
- 5.1.6. The
prot-emacs-theme.el
section forcursory
- 5.1.7. The
prot-emacs-theme.el
section fortheme-buffet
- 5.1.8. The
prot-emacs-theme.el
section aboutfontaine
- 5.1.9. The
prot-emacs-theme.el
section aboutshow-font
- 5.1.10. The
prot-emacs-theme.el
section aboutvariable-pitch-mode
and font resizing - 5.1.11. The
prot-emacs-theme.el
call toprovide
- 5.1.1. The
- 5.2. The
prot-emacs-essentials.el
module- 5.2.1. The
prot-emacs-essentials.el
block with basic configurations - 5.2.2. The
prot-emacs-essentials.el
section aboutprot-common.el
(custom basic functions) - 5.2.3. The
prot-emacs-essentials.el
section aboutprot-simple.el
(custom basic commands) - 5.2.4. The
prot-emacs-essentials.el
section aboutprot-scratch.el
(scratch buffer per major mode) - 5.2.5. The
prot-emacs-essentials.el
section aboutprot-pair.el
(insert character pairs) - 5.2.6. The
prot-emacs-essentials.el
section for comments - 5.2.7. The
prot-emacs-essentials.el
section aboutprot-prefix.el
(prefix nested keymaps) - 5.2.8. The
prot-emacs-essentials.el
configuration to track recently visited files - 5.2.9. The
prot-emacs-essentials.el
mouse configurations - 5.2.10. The
prot-emacs-essentials.el
settings forrepeat-mode
- 5.2.11. The
prot-emacs-essentials.el
settings for bookmarks - 5.2.12. The
prot-emacs-essentials.el
settings for registers - 5.2.13. The
prot-emacs-essentials.el
settings for auto revert - 5.2.14. The
prot-emacs-essentials.el
section fordelete-selection-mode
- 5.2.15. The
prot-emacs-essentials.el
settings for tooltips - 5.2.16. The
prot-emacs-essentials.el
configurations for the date and time (display-time-mode
) - 5.2.17. The
prot-emacs-essentials.el
settings for theworld-clock
- 5.2.18. The
prot-emacs-essentials.el
settings for manpages - 5.2.19. The
prot-emacs-essentials.el
settings forproced
- 5.2.20. The
prot-emacs-essentials.el
arrangement to run Emacs as a server - 5.2.21. The
prot-emacs-essentials.el
section aboutsubstitute
- 5.2.22. The
prot-emacs-essentials.el
section aboutgoto-chg
(go to change) - 5.2.23. The
prot-emacs-essentials.el
section aboutexpreg
(tree-sitter mark syntactically) - 5.2.24. The
prot-emacs-essentials.el
section aboutvundo
(visualise undo steps) - 5.2.25. The
prot-emacs-essentials.el
section abouttmr
(set timers) - 5.2.26. The
prot-emacs-essentials.el
section aboutpassword-store
- 5.2.27. The
prot-emacs-essentials.el
section aboutshell
- 5.2.28. The
prot-emacs-essentials.el
section about the laptop-specific settings - 5.2.29. The
prot-emacs-essentials.el
call toprovide
- 5.2.1. The
- 5.3. The
prot-emacs-modeline.el
module - 5.4. The
prot-emacs-completion.el
module- 5.4.1. The
prot-emacs-completion.el
settings for completion styles - 5.4.2. The
prot-emacs-completion.el
for theorderless
completion style - 5.4.3. The
prot-emacs-completion.el
settings to ignore letter casing - 5.4.4. The
prot-emacs-completion.el
settings for recursive minibuffers - 5.4.5. The
prot-emacs-completion.el
settings for default values - 5.4.6. The
prot-emacs-completion.el
settings for common interactions - 5.4.7. The
prot-emacs-completion.el
generic minibuffer UI settings - 5.4.8. The
prot-emacs-completion.el
settings for saving the history (savehist-mode
) - 5.4.9. The
prot-emacs-completion.el
settings for dynamic text expansion (dabbrev
) - 5.4.10. The
prot-emacs-completion.el
settings for static text expansion (abbrev
) - 5.4.11. The
prot-emacs-completion.el
for in-buffer completion popup (corfu
) - 5.4.12. The
prot-emacs-completion.el
settings forconsult
- 5.4.13. The
prot-emacs-completion.el
section aboutembark
- 5.4.14. The
prot-emacs-completion.el
section to configure completion annotations (marginalia
) - 5.4.15. The
prot-emacs-completion.el
setting to load a minibuffer UI submodule
- 5.4.1. The
- 5.5. The
prot-emacs-search.el
module- 5.5.1. The
prot-emacs-search.el
on isearch lax space - 5.5.2. The
prot-emacs-search.el
settings for isearch highlighting - 5.5.3. The
prot-emacs-search.el
on isearch match counter - 5.5.4. The
prot-emacs-search.el
tweaks to the isearch motion behaviour - 5.5.5. The
prot-emacs-search.el
tweaks for the occur buffer - 5.5.6. The
prot-emacs-search.el
modified isearch and occur key bindings - 5.5.7. The
prot-emacs-search.el
extras provided by theprot-search.el
library - 5.5.8. The
prot-emacs-search.el
tweaks toxref
,re-builder
andgrep
- 5.5.9. The
prot-emacs-search.el
setup for editable grep buffers (grep-edit-mode
orwgrep
) - 5.5.10. The
prot-emacs-search.el
call toprovide
- 5.5.1. The
- 5.6. The
prot-emacs-dired.el
module- 5.6.1. The
prot-emacs-dired.el
settings for common operations - 5.6.2. The
prot-emacs-dired.el
switches forls
(how files are listed) - 5.6.3. The
prot-emacs-dired.el
setting for dual-pane Dired - 5.6.4. The
prot-emacs-dired.el
settings to open files externally - 5.6.5. The
prot-emacs-dired.el
miscellaneous tweaks - 5.6.6. The
prot-emacs-dired.el
section about various conveniences - 5.6.7. The
prot-emacs-dired.el
section about my extras (prot-dired.el
) - 5.6.8. The
prot-emacs-dired.el
section aboutdired-subtree
- 5.6.9. The
prot-emacs-dired.el
section aboutwdired
(writable Dired) - 5.6.10. The
prot-emacs-dired.el
section aboutimage-dired
- 5.6.11. The
prot-emacs-dired.el
section aboutdired-preview
- 5.6.12. The
prot-emacs-dired.el
section about multimedia previews (ready-player
) - 5.6.13. The
prot-emacs-dired.el
section abouttrashed.el
- 5.6.14. The
prot-emacs-dired.el
section aboutmandoura
(mpv
media player) - 5.6.15. The
prot-emacs-dired.el
call toprovide
- 5.6.1. The
- 5.7. The
prot-emacs-window.el
module- 5.7.1. The
prot-emacs-window.el
section about running commands in popup frames - 5.7.2. The
prot-emacs-window.el
section about uniquifying buffer names - 5.7.3. The
prot-emacs-window.el
section about line highlighting (hl-line-mode
) - 5.7.4. The
prot-emacs-window.el
section about negative space highlighting (whitespace-mode
) - 5.7.5. The
prot-emacs-window.el
section about line numbers (display-line-numbers-mode
) - 5.7.6. The
prot-emacs-window.el
rules for displaying buffers (display-buffer-alist
) - 5.7.7. The
prot-emacs-window.el
setting to enablevisual-line-mode
in some contexts - 5.7.8. The
prot-emacs-window.el
settings to truncate some buffers silently - 5.7.9. The
prot-emacs-window.el
section key bindings - 5.7.10. The
prot-emacs-window.el
section aboutbeframe
- 5.7.11. The
prot-emacs-window.el
configuration ofundelete-frame-mode
andwinner-mode
- 5.7.12. The
prot-emacs-window.el
keys for window motions (windmove
) - 5.7.13. The
prot-emacs-window.el
use of contextual header line (breadcrumb
) - 5.7.14. The
prot-emacs-window.el
provide
form
- 5.7.1. The
- 5.8. The
prot-emacs-git.el
module- 5.8.1. The
prot-emacs-git.el
section about ediff - 5.8.2. The
prot-emacs-git.el
section aboutproject.el
- 5.8.3. The
prot-emacs-git.el
section aboutdiff-mode
- 5.8.4. The
prot-emacs-git.el
section aboutvc.el
and related - 5.8.5. The
prot-emacs-git.el
section aboutagitate
- 5.8.6. The
prot-emacs-git.el
section aboutmagit
(great Git client) - 5.8.7. The
prot-emacs-git.el
call toprovide
- 5.8.1. The
- 5.9. The
prot-emacs-org.el
module- 5.9.1. The
prot-emacs-org.el
section on thecalendar
- 5.9.2. The
prot-emacs-org.el
section about appointment reminders (appt.el
) - 5.9.3. The
prot-emacs-org.el
section with basic Org settings - 5.9.4. The
prot-emacs-org.el
Org to-do and refile settings - 5.9.5. The
prot-emacs-org.el
Org heading tags - 5.9.6. The
prot-emacs-org.el
Org time/state logging - 5.9.7. The
prot-emacs-org.el
Org link settings - 5.9.8. The
prot-emacs-org.el
Org code block settings - 5.9.9. The
prot-emacs-org.el
Org export settings - 5.9.10. The
prot-emacs-org.el
Org capture templates (org-capture
) - 5.9.11. The
prot-emacs-org.el
Org agenda settings - 5.9.12. The
prot-emacs-org.el
call toprovide
- 5.9.1. The
- 5.10. The
prot-emacs-langs.el
module- 5.10.1. The
prot-emacs-langs.el
settings for TAB - 5.10.2. The
prot-emacs-langs.el
settings for “electric” behaviour - 5.10.3. The
prot-emacs-langs.el
settingsshow-paren-mode
- 5.10.4. The
prot-emacs-langs.el
settings for plain text (no double spaces,auto-fill-mode
) - 5.10.5. The
prot-emacs-langs.el
settings for common file types - 5.10.6. The
prot-emacs-langs.el
settings foreldoc
- 5.10.7. The
prot-emacs-langs.el
settings foreglot
(LSP client) - 5.10.8. The
prot-emacs-langs.el
settings for very long lines - 5.10.9. The
prot-emacs-langs.el
settings formarkdown-mode
- 5.10.10. The
prot-emacs-langs.el
settings forcsv-mode
- 5.10.11. The
prot-emacs-langs.el
settings forsxhkdrc-mode
- 5.10.12. The
prot-emacs-langs.el
settings for spell checking - 5.10.13. The
prot-emacs-langs.el
settings for code linting (flymake
) - 5.10.14. The
prot-emacs-langs.el
settings foroutline-minor-mode
- 5.10.15. The
prot-emacs-langs.el
settings fordictionary
- 5.10.16. The
prot-emacs-langs.el
settings foraltcaps
(alternating letter casing) - 5.10.17. The
prot-emacs-langs.el
settings fordenote
(notes and file-naming) - 5.10.18. The
prot-emacs-langs.el
settings forlogos
(writing extras and buffer navigation)
- 5.10.1. The
- 5.11. The
prot-emacs-email.el
module- 5.11.1. The
prot-emacs-email.el
basic settings (includingauthinfo
) - 5.11.2. The
prot-emacs-email.el
message composition and encryption settings (message.el
) - 5.11.3. The
prot-emacs-email.el
integration with Dired for email attachments (gnus-dired-mode
) - 5.11.4. The
prot-emacs-email.el
settings forsendmail
- 5.11.5. The
prot-emacs-email.el
loading of the email client and call toprovide
- 5.11.6. The
prot-emacs-email.el
submodule fornotmuch
(prot-emacs-notmuch.el
)- 5.11.6.1. The
prot-emacs-notmuch.el
section about the account settings - 5.11.6.2. The
prot-emacs-notmuch.el
section about the general user interface - 5.11.6.3. The
prot-emacs-notmuch.el
section about the presentation of search buffers - 5.11.6.4. The
prot-emacs-notmuch.el
section about tag settings - 5.11.6.5. The
prot-emacs-notmuch.el
section about email composition settings - 5.11.6.6. The
prot-emacs-notmuch.el
section about reading messages - 5.11.6.7. The
prot-emacs-notmuch.el
section about hooks and key bindings - 5.11.6.8. The
prot-emacs-notmuch.el
custom extensions (perprot-notmuch.el
) - 5.11.6.9. The
prot-emacs-notmuch.el
glue code fororg-capture
(ol-notmuch.el
) - 5.11.6.10. The
prot-emacs-notmuch.el
section about thenotmuch-indicator
- 5.11.6.11. The
prot-emacs-notmuch.el
call toprovide
- 5.11.6.1. The
- 5.11.7. The deprecated
prot-emacs-mail.el
submodule formu4e
(prot-emacs-mu4e.el
) - 5.11.8. The deprecated
prot-emacs-mail.el
submodule for Gnus (prot-emacs-gnus.el
) - 5.11.9. Overview of my email setup (
mbsync
,msmtp
, mail indexer, and MUA)
- 5.11.1. The
- 5.12. TODO The
prot-emacs-web.el
module- 5.12.1. TODO The
prot-emacs-web.el
settings about following links (browse-url
) - 5.12.2. TODO The
prot-emacs-web.el
settings about buttonising links (goto-addr
) - 5.12.3. TODO The
prot-emacs-web.el
settings about the Simple HTML Renderer (shr
) - 5.12.4. TODO The
prot-emacs-web.el
settings about browser cookies - 5.12.5. TODO The
prot-emacs-web.el
settings about the web browser (eww
) - 5.12.6. TODO The
prot-emacs-web.el
extras foreww
(prot-eww.el
) - 5.12.7. TODO The
prot-emacs-web.el
RSS/Atom reader (elfeed
) - 5.12.8. TODO The
prot-emacs-web.el
settings for the IRC client
- 5.12.1. TODO The
- 5.13. The
prot-emacs-which-key.el
module - 5.14. The
prot-emacs-icons.el
module (nerd-icons
for various packages)
- 5.1. The
- 6. The custom libraries of my configuration
- 6.1. The
prot-abbrev.el
library - 6.2. The
prot-comment.el
library - 6.3. The
prot-common.el
library - 6.4. The
prot-dired.el
library - 6.5. The
prot-elfeed.el
library - 6.6. The
prot-embark.el
library - 6.7. The
prot-eww.el
library - 6.8. The
prot-marginalia.el
library - 6.9. The
prot-modeline.el
library - 6.10. The
prot-notmuch.el
library - 6.11. The
prot-orderless.el
library - 6.12. The
prot-org.el
library - 6.13. The
prot-pair.el
library - 6.14. The
prot-prefix.el
library - 6.15. The
prot-project.el
library - 6.16. The
prot-scratch.el
library - 6.17. The
prot-search.el
library - 6.18. The
prot-shell.el
library - 6.19. The
prot-simple.el
library - 6.20. The
prot-spell.el
library - 6.21. The
prot-vertico.el
library - 6.22. The
prot-window.el
library
- 6.1. The
- 7. Frequently Asked Questions (FAQ)
- 7.1. Why many modules instead of one init.el?
- 7.2. Why use Org when you can have an outline in Elisp?
- 7.3. Why do you use multiple
setq
instead of one? - 7.4. Why don’t you remap keys?
- 7.5. Why not use Org block arguments in the properties drawer?
- 7.6. What hardware and software do you use?
- 7.7. What is your desktop setup?
1. Details of my Emacs build
I track the emacs.git trunk, as I am the maintainer of several Emacs packages and a contributor to Emacs core. Here are my current settings (2024-12-18 09:58:56 +0200):
system-configuration-options
--prefix=/usr/local --with-x-toolkit=gtk3 --disable-gc-mark-trace --with-native-compilation=aot --without-gif --without-tiff --without-selinux --without-xinput2 --without-gpm --without-compress-install --without-xft --with-cairo --with-harfbuzz --with-tree-sitter=ifavailable --without-gsettings --without-gconf
Users of Arch Linux can refer to this PKGBUILD
I maintain for my
purposes:
- Git repositories:
- Backronym for “PKGBUILD … of Emacs”: Package Knowhow Germane to Building Unapologetically Individuated Local Design … of Emacs.
2. Anatomy of my Emacs configuration
[ Also read: Why many modules instead of one init.el? and Why use Org when you can have an outline in Elisp? ]
What you are now reading is the prot-emacs.org
file. It is the
document that generates—and thus controls—every other file that
underpins my Emacs configuration.
This Org file is not loaded directly. Its sole purpose is to produce the files that do the actual work. These files are organised by their purpose and function:
- The standard
early-init.el
- It includes optimisations for starting up Emacs and sets some basics in place, such as to avoid the flash of light when starting Emacs while in a dark environment.
- The standard
init.el
- It contains foundational blocks of my system, i.e. Lisp macros I define, and loads the individual configuration modules.
- The
prot-emacs-modules
directory - It includes all my
configuration modules. Each module is about a specific type of
functionality, such a
prot-emacs-theme.el
for themes andprot-emacs-essentials.el
for basic tools. These configuration modules tweak packages and are not meant to define extra functionality. - The
prot-lisp
directory - Here are the custom libraries I maintain as part of my Emacs setup. They are written in accordance with the best practices for packaging Emacs Lisp, though are only meant to be used as part of my setup. As such, they are not necessarily up to par with the public-facing packages I maintain for Emacs: https://protesilaos.com/emacs.
- The
prot-emacs-pre-custom
file - It is evaluated before the modules are loaded. It is intended for users of my configuration who want to make use of the options I provide (The init.el user options).
- The
prot-emacs-post-custom
file - Like the above, this file is meant for users of my setup. It is evaluated after the rest of my setup is loaded. Users can include whatever code they want in this file to either override existing functionality of define new one.
- The
prot-emacs.org
file - The source of what you are currently reading.
Here is a schematic representation of this directory structure (files shown here may not reflect the latest state of the project):
~/Git/Projects/dotfiles/emacs/.emacs.d $ tree -aF ./ ├── early-init.el ├── init.el ├── prot-emacs-modules/ │ ├── prot-emacs-completion.el │ ├── prot-emacs-dired.el │ ├── prot-emacs-ef-themes.el │ ├── prot-emacs-email.el │ ├── prot-emacs-essentials.el │ ├── prot-emacs-git.el │ ├── prot-emacs-gnus.el │ ├── prot-emacs-icons.el │ ├── prot-emacs-langs.el │ ├── prot-emacs-mct.el │ ├── prot-emacs-modeline.el │ ├── prot-emacs-modus-themes.el │ ├── prot-emacs-mu4e.el │ ├── prot-emacs-notmuch.el │ ├── prot-emacs-org.el │ ├── prot-emacs-search.el │ ├── prot-emacs-standard-themes.el │ ├── prot-emacs-theme.el │ ├── prot-emacs-vertico.el │ ├── prot-emacs-web.el │ ├── prot-emacs-which-key.el │ └── prot-emacs-window.el ├── prot-emacs.org ├── prot-emacs-post-custom.el ├── prot-emacs-pre-custom.el └── prot-lisp/ ├── prot-abbrev.el ├── prot-coach.el ├── prot-comment.el ├── prot-common.el ├── prot-dired.el ├── prot-elfeed.el ├── prot-embark.el ├── prot-eww.el ├── prot-marginalia.el ├── prot-modeline.el ├── prot-notmuch.el ├── prot-orderless.el ├── prot-org.el ├── prot-pair.el ├── prot-prefix.el ├── prot-project.el ├── prot-scratch.el ├── prot-search.el ├── prot-shell.el ├── prot-simple.el ├── prot-spell.el ├── prot-vertico.el └── prot-window.el 3 directories, 50 files
The reason I have this modular setup is because it is easier to debug
it but also to share individual snippets with others. The
prot-emacs.org
file is not a hindrance in this regard: it provides
an additional way of sharing my work in the form of this consolidated
view you are now seeing.
When I want to make a change to my Emacs setup, I do the edits in this
prot-emacs.org
and then type C-c C-v C-t
(M-x org-babel-tangle
)
to propagate the changes to the relevant files.
On a new computer, I put all my Emacs files where they are meant to
be (inside the ~/.emacs.d
directory) with this command, which uses
the stow
system package (all my dotfiles are stowed in place with
this program):
~/Git/Projects/dotfiles $ stow -t $HOME emacs
If I ever add/remove files, I do this instead:
~/Git/Projects/dotfiles $ stow -t $HOME -R emacs
I have built my setup from scratch and am observing best practices
with regard to how Emacs expects things to run. I do not use the Emacs
daemon, as I have encountered instabilities with it. Instead, I run a
single instance of Emacs and then configure it to act as the server.
This means that I can still connect to the running session via
emacsclient
, which is useful when I want to evaluate Elisp code from
outside of Emacs (e.g. with my delight.sh
shell script that switches
the entire “environment” theme of my tiling window manager or desktop
environment—see my dotfiles for the technicalities).
3. The early initialisation of Emacs (early-init.el
)
This is the first file that Emacs reads when starting up. It should
contain code that does not depend on any package or the proportions of
the Emacs frame. In general, this early initialisation file is meant
to set up a few basic things before Emacs produces the initial frame
by delegating to the init.el
(Anatomy of my Emacs configuration).
3.1. The early-init.el
macro to run code only in a Desktop Environment
There are a few parts of my setup where I need to run code based on whether I am using a regular desktop environment. This is not the norm, as I default to a tiling window manager (check my dotfiles for their specifics). What I have here is a macro which I can then use to wrap any code that should only be evaluated when I am not in one of my tiling window managers.
(defvar prot-emacs-tiling-window-manager-regexp "bspwm\\|herbstluftwm\\|i3" "Regular expression to tiling window managers. See definition of `prot-emacs-with-desktop-session'.") (defmacro prot-emacs-with-desktop-session (&rest body) "Expand BODY if desktop session is not a tiling window manager. See `prot-emacs-tiling-window-manager-regexp' for what constitutes a matching tiling window manager." (declare (indent 0)) `(when-let* ((session (getenv "DESKTOP_SESSION")) ((not (string-match-p session prot-emacs-tiling-window-manager-regexp)))) ,@body))
3.2. The early-init.el
code to set frame parameters
Here I am setting parameters for the size of the Emacs frame: the
first as well as any future one. In a tiling window manager, these
parameters are not relevant, since all windows are forcibly made to
fit into rectangles (tiles) that fill up the entire screen. So I use
the prot-emacs-with-desktop-session
macro that I described above to
set these parameters only when I am in a regular desktop environment
(The early-init.el macro to run code only in a Desktop Environment).
The initial-frame-alist
is about the first frame that is produced
when starting Emacs. The default-frame-alist
is for all frames after
that.
(defun prot-emacs-add-to-list (list element) "Add to symbol of LIST the given ELEMENT. Simplified version of `add-to-list'." (set list (cons element (symbol-value list)))) (prot-emacs-with-desktop-session (mapc (lambda (var) (prot-emacs-add-to-list var '(width . (text-pixels . 800))) (prot-emacs-add-to-list var '(height . (text-pixels . 900))) (prot-emacs-add-to-list var '(scroll-bar-width . 10))) '(default-frame-alist initial-frame-alist)))
3.3. The early-init.el
basic frame settings
These are some general settings for frames and the basics of the
toolkit. In short, I want to keep things minimal. Notice the
frame-resize-pixelwise
and frame-inhibit-implied-resize
: by
default Emacs will resize the frame if you adjust the font size, which
I never want.
(setq frame-resize-pixelwise t frame-inhibit-implied-resize t frame-title-format '("%b") ring-bell-function 'ignore use-dialog-box t ; only for mouse events, which I seldom use use-file-dialog nil use-short-answers t inhibit-splash-screen t inhibit-startup-screen t inhibit-x-resources t inhibit-startup-echo-area-message user-login-name ; read the docstring inhibit-startup-buffer-menu t) ;; I do not use those graphical elements by default, but I do enable ;; them from time-to-time for testing purposes or to demonstrate ;; something. NEVER tell a beginner to disable any of these. They ;; are helpful. (menu-bar-mode -1) (scroll-bar-mode -1) (tool-bar-mode -1)
3.4. The early-init.el
tweaks to startup time and garbage collection
I do not have a deep understanding of “garbage collection”, though I have learnt through trial and error that I can maximise the threshold during startup to make Emacs boot a bit faster. What I am doing here is to arrange for the relevant values to be set to very high values during startup and then be brought down to something more practical once Emacs is done loading.
;; Temporarily increase the garbage collection threshold. These ;; changes help shave off about half a second of startup time. The ;; `most-positive-fixnum' is DANGEROUS AS A PERMANENT VALUE. See the ;; `emacs-startup-hook' a few lines below for what I actually use. (setq gc-cons-threshold most-positive-fixnum gc-cons-percentage 0.5) ;; Same idea as above for the `file-name-handler-alist' and the ;; `vc-handled-backends' with regard to startup speed optimisation. ;; Here I am storing the default value with the intent of restoring it ;; via the `emacs-startup-hook'. (defvar prot-emacs--file-name-handler-alist file-name-handler-alist) (defvar prot-emacs--vc-handled-backends vc-handled-backends) (setq file-name-handler-alist nil vc-handled-backends nil) (add-hook 'emacs-startup-hook (lambda () (setq gc-cons-threshold (* 1000 1000 8) gc-cons-percentage 0.1 file-name-handler-alist prot-emacs--file-name-handler-alist vc-handled-backends prot-emacs--vc-handled-backends)))
3.5. The early-init.el
initialises the package cache
I use the standard package.el
to manage my Emacs packages. It works
for me and I never had a need for more (The init.el settings for packages (package.el
)).
If I have to tinker with a package’s source code, I use Git
ordinarily—no need for a package manager to also be a development
tool.
;; Initialise installed packages at this early stage, by using the ;; available cache. I had tried a setup with this set to nil in the ;; early-init.el, but (i) it ended up being slower and (ii) various ;; package commands, like `describe-package', did not have an index of ;; packages to work with, requiring a `package-refresh-contents'. (setq package-enable-at-startup t)
3.6. The early-init.el
defines general theme-related functions
Here I am defining helper functions that check what theme I should be using. I then rely on these functions to load a light or dark theme accordingly. This is done in the sections about themes:
- The
prot-emacs-modus-themes.el
module - The
prot-emacs-ef-themes.el
module - The
prot-emacs-standard-themes.el
module
;;;; General theme code (defun prot-emacs-theme-gsettings-dark-p () "Return non-nil if gsettings (GNOME) has a dark theme. Return nil if the DESKTOP_SESSION is either bspwm or herbstluftwm, per the configuration of my dotfiles. Also check the `delight.sh' shell script." (prot-emacs-with-desktop-session (string-match-p "dark" (shell-command-to-string "gsettings get org.gnome.desktop.interface color-scheme")))) (defun prot-emacs-theme-twm-dark-p () "Return non-nil if my custom setup has a dark theme. I place a file in ~/.config/prot-xtwm-active-theme which contains a single word describing my system-wide theme. This is part of my dotfiles. Check my `delight.sh' shell script for more." (when-let* ((file "~/.config/prot-xtwm-active-theme") ((file-exists-p file))) (string-match-p "dark" (with-temp-buffer (insert-file-contents file) (buffer-string))))) (defun prot-emacs-theme-environment-dark-p () "Return non-nil if environment theme is dark." (or (prot-emacs-theme-twm-dark-p) (prot-emacs-theme-gsettings-dark-p))) (defun prot-emacs-re-enable-frame-theme (_frame) "Re-enable active theme, if any, upon FRAME creation. Add this to `after-make-frame-functions' so that new frames do not retain the generic background set by the function `prot-emacs-avoid-initial-flash-of-light'." (when-let* ((theme (car custom-enabled-themes))) (enable-theme theme)))
3.7. The early-init.el
takes care to avoid the initial flash of light
Since I put in the effort to define the above theme-related functions, I can now benefit by having Emacs set an appropriate set of basic colour values at startup to eliminate the flash of light it normally displays (The early-init.el defines general theme-related functions). By default, Emacs loads a light theme, but this is terrible if I am in a fairly dark environment. Whereas my arrangement here makes sure that Emacs uses a black background if the environment is dark.
Note that in the snippet below I hardcode the black colour (#000000
)
to avoid any extra calculations at this early stage. Otherwise, I
would have to check which theme will be loaded and then set its
background here. That would be too slow for what we need in the early
initialisation file, thus defeating the purpose of not having a flash
of light at startup.
;; NOTE 2023-02-05: The reason the following works is because (i) the ;; `mode-line-format' is specified again and (ii) the ;; `prot-emacs-theme-gsettings-dark-p' will load a dark theme. (defun prot-emacs-avoid-initial-flash-of-light () "Avoid flash of light when starting Emacs, if needed. New frames are instructed to call `prot-emacs-re-enable-frame-theme'." (when (prot-emacs-theme-environment-dark-p) (setq mode-line-format nil) (set-face-attribute 'default nil :background "#000000" :foreground "#ffffff") (set-face-attribute 'mode-line nil :background "#000000" :foreground "#ffffff" :box 'unspecified) (add-hook 'after-make-frame-functions #'prot-emacs-re-enable-frame-theme))) (prot-emacs-avoid-initial-flash-of-light)
3.8. The early-init.el
gives a name to the default frame
Finally, I like to call my default frame home
. This is because I use
my beframe
package to group the list of buffers on a per-frame basis
(The prot-emacs-window.el
section about beframe
). The multi-frame
arrangement is the best thing I ever did to boost my productivity:
bonus points when used in tandem with a tiling window manager.
Naming frames allows you to select them using completion. Emacs can do
this (M-x select-frame-by-name
), though it is not always reliable as
it depends on the window manager (it works fine on GNOME, from what I
can tell). For minimalist window managers on Linux, something like the
rofi
program can select system windows based on their name.
(add-hook 'after-init-hook (lambda () (set-frame-name "home")))
4. The main initialisation of Emacs (init.el
)
This is where I define the Lisp macros used in my setup and load all the invidiual modules.
4.1. The init.el
user options
I define several user options for my Emacs. These are useful to me if I need to quickly test some aspect of my setup, though I provide them mostly for those who use my files as a basis for their configuration.
All user options must be set in a prot-emacs-pre-custom.el
file in
the same directory as the init.el
(Anatomy of my Emacs configuration).
;; For those who use my dotfiles and need an easy way to write their ;; own extras on top of what I already load: search below for the files ;; prot-emacs-pre-custom.el and prot-emacs-post-custom.el (defgroup prot-emacs nil "User options for my dotemacs. These produce the expected results only when set in a file called prot-emacs-pre-custom.el. This file must be in the same directory as the init.el." :group 'file)
4.1.1. The init.el
user option to load a theme family
I am the developer/maintainer of three distinct theme packages. You can read more about them (and see pictures) on their respective web pages:
- https://protesilaos.com/emacs/modus-themes
- https://protesilaos.com/emacs/ef-themes
- https://protesilaos.com/emacs/standard-themes
In short:
- Modus themes
- They conform with the highest accessibility standard
for colour contrast (WCAG AAA). They are elegant and designed with
attention to detail. I consider the
modus-operandi
andmodus-vivendi
themes to be the standard of what a default accessible theme should be like. - Ef themes
- Highly legible (WCAG AA or WCAG AAA) and more colourful than the Modus themes. The collection of palettes is also wider to match a broad variety of preferences.
- Standard themes
- A re-imagination of the default Emacs looks. They bring consistency and customisability to those who like how Emacs is out-of-the-box.
Here we specify which module to load at startup. Remember to read how these options come into effect (The init.el user options).
(defcustom prot-emacs-load-theme-family 'modus "Set of themes to load. Valid values are the symbols `ef', `modus', and `standard', which reference the `ef-themes', `modus-themes', and `standard-themes', respectively. A nil value does not load any of the above (use Emacs without a theme). This user option must be set in the `prot-emacs-pre-custom.el' file. If that file exists in the Emacs directory, it is loaded before all other modules of my setup." :group 'prot-emacs :type '(choice :tag "Set of themes to load" :value modus (const :tag "The `ef-themes' module" ef) (const :tag "The `modus-themes' module" modus) (const :tag "The `standard-themes' module" standard) (const :tag "Do not load a theme module" nil)))
4.1.2. The init.el
user option to load a minibuffer user interface
I normally use vertico
with my own custom extensions to it (The prot-vertico.el
library),
though I also maintain the mct
package for those who want to use the
built-in completion framework with a few extras for better movement
and the like. I think mct
will eventually be replaced by built-in
facilities, given there are developments on that front for Emacs 30.
It still has its place in the meantime.
Here we specify which module to load at startup. Remember to read how these options come into effect (The init.el user options).
(defcustom prot-emacs-completion-ui 'vertico "Choose minibuffer completion UI between `mct' or `vertico'. If the value is nil, the default completion user interface is used. On Emacs 30, this is close the experience with `mct'. This user option must be set in the `prot-emacs-pre-custom.el' file. If that file exists in the Emacs directory, it is loaded before all other modules of my setup." :group 'prot-emacs :type '(choice :tag "Minibuffer user interface" (const :tag "Default user interface" nil) (const :tag "The `mct' module" mct) (const :tag "The `vertico' module" vertico)))
4.1.3. The init.el
user option to load extras for minibuffer completion
I normally load some packages that enhance the experience with the minibuffer. The upside is that we get more power out of Emacs. The downside is that they have a learning curve. Users who do not need these features can set the option to nil.
Remember to read how these options come into effect (The init.el user options).
(defcustom prot-emacs-completion-extras t "When non-nil load extras for minibuffer completion. These include packages such as `consult' and `embark'." :group 'prot-emacs :type 'boolean)
4.1.4. The init.el
user option to load treesitter extras
I configure Emacs to support the tree-sitter
program, though I do
not use anything that leverages it. I either code in Emacs Lisp or
write prose. This user option is in place for those who want to use my
configuration as a basis for their own setup.
Remember to read how these options come into effect (The init.el user options).
(defcustom prot-emacs-treesitter-extras t "When non-nil load extras for tree-sitter integration These include packages such as `expreg' and generally anything that adds functionality on top of what the major mode provides." :group 'prot-emacs :type 'boolean)
4.1.5. The init.el
user option to enable which-key
The which-key
package provides hints for keys that complete the
currently incomplete sequence. Here we determine whether to load the
module or not. I personally never rely on which-key
even if I enable
its mode. If I ever need to review which key bindings are available I
will either type C-h
to complete a key sequence (produces a Help
buffer with relevant keys) or I will do C-h m
(M-x describe-mode
to get information about the current major mode).
Remember to read how these options come into effect (The init.el user options).
Also check the prot-emacs-which-key.el
module.
(defcustom prot-emacs-load-which-key nil "When non-nil, display key binding hints after a short delay. This user option must be set in the `prot-emacs-pre-custom.el' file. If that file exists in the Emacs directory, it is loaded before all other modules of my setup." :group 'prot-emacs :type 'boolean)
4.1.6. The init.el
user option to load icons (nerd-icons.el
)
Here we check whether to load decorative icons in a number of places,
such as Dired buffers and the completion user interface (where
relevant). My setup does not try to install the font files: the user
must do this manually with M-x nerd-icons-install-fonts
.
Remember to read how these options come into effect (The init.el user options).
Also check the prot-emacs-icons.el
module.
(defcustom prot-emacs-load-icons nil "When non-nil, enable iconography in various contexts. This installs and uses the `nerd-icons' package and its variants. NOTE that you still need to invoke `nerd-icons-install-fonts' manually to first get the icon files. This user option must be set in the `prot-emacs-pre-custom.el' file. If that file exists in the Emacs directory, it is loaded before all other modules of my setup." :group 'prot-emacs :type 'boolean)
4.2. The init.el
basic configurations to disable backups and lockfiles
By default, Emacs tries to lock down files so that they are not modified by other programs. It also keeps backups. These are features I do not need because (i) if I am ever modifying my files externally, then I know what I am doing and (ii) all the files I care about are either under version control or backed up to a flash drive.
(setq make-backup-files nil) (setq backup-inhibited nil) ; Not sure if needed, given `make-backup-files' (setq create-lockfiles nil)
4.3. The init.el
tweaks to make native compilation silent
The --with-native-compilation=yes
build option of Emacs is very
nice: it enables the “native compilation” of Emacs Lisp, translating
it down to machine code. However, the default setting for reporting
errors is set to a verbose value which, in my coaching experience,
confuses users: it produces warnings for compilation issues that only
the developer of the given package needs to deal with. These include
innocuous facts like docstrings being wider than a certain character
count. To make things even worse, the buffer that shows these warnings
uses the stop sign character, resulting in a long list of lines with
red spots everywhere, as if we have totally broken Emacs.
;; Make native compilation silent and prune its cache. (when (native-comp-available-p) (setq native-comp-async-report-warnings-errors 'silent) ; Emacs 28 with native compilation (setq native-compile-prune-cache t)) ; Emacs 29
4.4. The init.el
setting to send custom-file
to oblivion
By default, Emacs writes persistent customisations to the end of the
user’s init file. These are encapsulated in a “custom” block. Emacs
writes those whenever the user does something with M-x customize
or
related.
I personally prefer writing the actual Elisp over using the interface
of M-x customize
. I also want my init file to only ever contain what
I wrote and to never—ever!—evaluate code I have not called myself.
As such, I want to set the custom-file
variable to tell Emacs to
write its persistent variables to that file instead of my init.el
.
Though since I have no intent to ever use this file, I choose to make
it disposable by placing it inside of the /tmp
directory (this is
what the make-temp-file
function does). So when I close the
computer, the file is gone. Finally, I do not invoke the load
function because I will never rely on the custom-file
. I would
prefer to just have an option to avoid the Custom infrastructure
altogether, but this is not possible. So here we are…
;; Disable the damn thing by making it disposable. (setq custom-file (make-temp-file "emacs-custom-"))
4.5. The init.el
settings for multilingual editing (input methods)
This sets up Emacs for me to be able to type in Greek while still using Emacs key bindings involving modifier keys (I don’t get this if I switch keyboard layouts at the system level).
Watch my video about multilingual editing: https://protesilaos.com/codelog/2023-12-12-emacs-multilingual-editing/.
(setq default-input-method "greek") ; also check "greek-postfix" (setq default-transient-input-method "greek")
4.6. The init.el
settings to enable certain commands and disable others
Some Emacs commands are disabled by default. This means that Emacs will produce a warning when we try to invoke them and will ask us for confirmation. Here I define which commands I want to have enabled and then put some on the disabled list.
;; Enable these (mapc (lambda (command) (put command 'disabled nil)) '(list-timers narrow-to-region narrow-to-page upcase-region downcase-region)) ;; And disable these (mapc (lambda (command) (put command 'disabled t)) '(eshell project-eshell overwrite-mode iconify-frame diary))
4.7. The init.el
setting to always start with the *scratch*
buffer
I like starting with a scratch buffer. I know that a lot of users specify a dashboard or an Org agenda view, but I prefer to keep things generic in this regard.
(setq initial-buffer-choice t) (setq initial-major-mode 'lisp-interaction-mode) (setq initial-scratch-message (format ";; This is `%s'. Use `%s' to evaluate and print results.\n\n" 'lisp-interaction-mode (propertize (substitute-command-keys "\\<lisp-interaction-mode-map>\\[eval-print-last-sexp]") 'face 'help-key-binding)))
4.8. The init.el
arrangements for my own modules and custom libraries
I use a literate configuration as the “source of truth” for my Emacs
configuration. What I do is to specify everything in one file and
provide instructions for where things should go. The end product
consists of a large set of files, encompassing the early-init.el
(The early initialisation of Emacs (early-init.el
)), the init.el
(The main initialisation of Emacs (init.el
)), the modules of my
init, and the custom libraries I wrote.
In the code snippet further below, I add two directories to the
load-path
. Concretely, any Emacs Lisp file inside these directories
is thus declared to Emacs and we can load it properly. Here is what
these two directories are about:
- The
prot-emacs-modules
directory This is where I store all the individual components of my Emacs setup. When I run Emacs, the directory is a subdirectory of
~/.emacs.d/
. All files are prefixed withprot-emacs-
, followed by a word that broadly describes their scope of application, such asprot-emacs-font
,prot-emacs-window
…Each module consists of ordinary Elisp and a final call to
provide
the set of configurations as a feature that can then be loaded viarequire
from theinit.el
. What Emacs calls a “feature” is, in essence, a variable whose value is the entirety of the file that has aprovide
call in it. Features are symbols that are named after the file name minus its file type extension:prot-emacs-theme
is the feature provided byprot-emacs-theme.el
.Modules are intended only for configuration purposes. They do not define any major variables/functions, unless those are too small/specific to be extracted into their own library.
- The
prot-lisp
directory As with the aforementioned modules, this directory is a subdirectory of
~/.emacs.d/
. This is where I keep all my custom code that individual modules configure. The contents of this directory can be understood as fully fledged “packages” and, in fact, many of my actual packages started out asprot-lisp
experiments.Each file is written in accordance with the conventions on Emacs packaging, even though they are only intended for use in my setup and are not polished to the level of my actual public-facing packages (meaning the ones listed here: https://protesilaos.com/emacs).
All this may not matter to you if you are reading either the
prot-emacs.org
file or its web page version. Still, this arrangement
gives me maximum flexbility, as I can still share my code the way it
would look. Plus, if I ever decide to stop using the literate config,
I can simply stop editing it and perfom the edits directly in the
files that are already placed where I need them to be.
(mapc (lambda (string) (add-to-list 'load-path (locate-user-emacs-file string))) '("prot-lisp" "prot-emacs-modules"))
4.9. The init.el
settings for packages (package.el
)
The package.el
is built into Emacs and is perfectly fine for my
use-case. We do not need to load it explicitly, as it will be called
by use-package
when it needs it. Since the introduction of the
early-init.el
file, we also do not need to initialise the packages
at this point: we activate the cache instead (The early-init.el
initialises the package cache).
With regard to the settings here, make sure to read my article about package archives, pinning packages, and setting priorities: https://protesilaos.com/codelog/2022-05-13-emacs-elpa-devel/.
;;;; Packages (setq package-vc-register-as-project nil) ; Emacs 30 (add-hook 'package-menu-mode-hook #'hl-line-mode) ;; Also read: <https://protesilaos.com/codelog/2022-05-13-emacs-elpa-devel/> (setq package-archives '(("gnu-elpa" . "https://elpa.gnu.org/packages/") ("gnu-elpa-devel" . "https://elpa.gnu.org/devel/") ("nongnu" . "https://elpa.nongnu.org/nongnu/") ("melpa" . "https://melpa.org/packages/"))) ;; Highest number gets priority (what is not mentioned has priority 0) (setq package-archive-priorities '(("gnu-elpa" . 3) ("melpa" . 2) ("nongnu" . 1))) ;; NOTE 2023-08-21: I build Emacs from source, so I always get the ;; latest version of built-in packages. However, this is a good ;; solution to set to non-nil if I ever switch to a stable release. (setq package-install-upgrade-built-in nil)
I want to use my own packages from the GNU-devel ELPA. I am thus
pinning them to that archive by setting the value of
package-pinned-packages
. This way, I get to run the latest version
while also making sure the actual package works properly. All other
packages will rely on package-archive-priorities
.
(defvar prot-emacs-my-packages '(agitate altcaps beframe cursory denote dired-preview ef-themes fontaine lin logos mct modus-themes notmuch-indicator pulsar show-font spacious-padding standard-themes substitute sxhkdrc-mode theme-buffet tmr) "List of symbols representing the packages I develop/maintain.") ;; Also read: <https://protesilaos.com/codelog/2022-05-13-emacs-elpa-devel/> (setq package-pinned-packages `(,@(mapcar (lambda (package) (cons package "gnu-elpa-devel")) prot-emacs-my-packages)))
4.10. The init.el
option to declare all themes as safe
When loading a theme, Emacs will produce a warning explaining how
themes are ordinary Elisp and thus can run harmful code. I understand
why this message is there, but I do not need to be reminded about it.
Setting this to non-nil saves me from the occasional warning if I
ever run load-theme
without a NO-CONFIRM
argument (like this:
(load-theme 'modus-operandi :no-confirm)
).
[ Note that Emacs considers the built-in themes “safe”. This includes my
modus-themes
. ]
(setq custom-safe-themes t)
4.11. The init.el
macro to do nothing with Elisp code (prot-emacs-comment
)
This is something I learnt while studying Clojure: a comment
macro
that wraps some code, effectively commenting it out, while keeping
indentation and syntax highlighting intact.
What I have here is technically not commenting out the code, because the expansion of the macro is nil, not the actual code with comments around it.
(defmacro prot-emacs-comment (&rest body) "Do nothing with BODY and return nil, with no side effects." (declare (indent defun)) nil)
The above is an example. What I actually use is the following. It
behaves the same as above, except when it reads a plist of the form
(:eval t)
. The idea is for me to quickly activate something I want
to test by passing that to the macro. So here we have it:
(defmacro prot-emacs-comment (&rest body) "Determine what to do with BODY. If BODY contains an unquoted plist of the form (:eval t) then return BODY inside a `progn'. Otherwise, do nothing with BODY and return nil, with no side effects." (declare (indent defun)) (let ((eval)) (dolist (element body) (when-let* (((plistp element)) (key (car element)) ((eq key :eval)) (val (cadr element))) (setq eval val body (delq element body)))) (when eval `(progn ,@body))))
And here is a function I might develop further to quickly insert
prot-emacs-comment
(though another is needed to also remove it and
then a Do-What-I-Mean wrapper to switch between the two):
;; Sample use of `prot-emacs-comment'. The function ;; `prot-emacs-insert-comment-macro' is never evaluated. (prot-emacs-comment (defun prot-emacs-insert-comment-macro (beg end) "Wrap region between BEG and END in `prot-emacs-comment'." (interactive "r") (if (region-active-p) (let ((text (buffer-substring beg end))) (delete-region beg end) (insert (format "(prot-emacs-comment\n%s)" text)) (indent-region beg end)) (user-error "No active region; will not insert `prot-emacs-comment' here"))))
4.12. The init.el
macro to bind keys (prot-emacs-keybind
)
[ Watch: define prefix/leader key (nested key maps) (2024-01-29). ]
This Lisp macro does not try to be too smart. It simply reduces the
typing we have to do to define key bindings. As with the underlying
define-key
function, it can bind a key sequence to a command, a nil
value, or even a keymap. The constraint it imposes is that the
arguments supplied to it as an even number and the odd ones are key
bindings (strings that can be passed to the kbd
function). This
means that it does not try to cover the case of [remap COMMAND]
(I
am not a fan of it because the code alone does not tell us which key
we end up using (Why don’t you remap keys?)).
(defmacro prot-emacs-keybind (keymap &rest definitions) "Expand key binding DEFINITIONS for the given KEYMAP. DEFINITIONS is a sequence of string and command pairs." (declare (indent 1)) (unless (zerop (% (length definitions) 2)) (error "Uneven number of key+command pairs")) (let ((keys (seq-filter #'stringp definitions)) ;; We do accept nil as a definition: it unsets the given key. (commands (seq-remove #'stringp definitions))) `(when-let* (((keymapp ,keymap)) (map ,keymap)) ,@(mapcar (lambda (pair) (let* ((key (car pair)) (command (cdr pair))) (unless (and (null key) (null command)) `(define-key map (kbd ,key) ,command)))) (cl-mapcar #'cons keys commands))))) ;; Sample of `prot-emacs-keybind' ;; (prot-emacs-keybind global-map ;; "C-z" nil ;; "C-x b" #'switch-to-buffer ;; "C-x C-c" nil ;; ;; Notice the -map as I am binding keymap here, not a command: ;; "C-c b" beframe-prefix-map ;; "C-x k" #'kill-buffer)
4.13. The init.el
macro to define abbreviations (prot-emacs-abbrev
)
[ Watch: abbreviations with abbrev-mode (quick text expansion) (2024-02-03). ]
This is the same idea as prot-emacs-keybind
, adjusted to work with
the define-abbrev
function (The init.el macro to bind keys (prot-emacs-keybind
)).
I probably do not need this, as I only write a small number of
abbreviations. Though it is good to practice some programming.
(defmacro prot-emacs-abbrev (table &rest definitions) "Expand abbrev DEFINITIONS for the given TABLE. DEFINITIONS is a sequence of (i) string pairs mapping the abbreviation to its expansion or (ii) a string and symbol pair making an abbreviation to a function." (declare (indent 1)) (unless (zerop (% (length definitions) 2)) (error "Uneven number of key+command pairs")) `(if (abbrev-table-p ,table) (progn ,@(mapcar (lambda (pair) (let ((abbrev (nth 0 pair)) (expansion (nth 1 pair))) (if (stringp expansion) `(define-abbrev ,table ,abbrev ,expansion) `(define-abbrev ,table ,abbrev "" ,expansion)))) (seq-split definitions 2))) (error "%s is not an abbrev table" ,table)))
4.14. The init.el
addition of highlighting for my macros
In the previous sections, I define a few Lisp macros that I use throughout my setup. The following makes these known to Emacs and specifies how they should be colourised.
(defvar prot-emacs-package-form-regexp "^(\\(prot-emacs-keybind\\|prot-emacs-abbrev\\) +'?\\([0-9a-zA-Z-]+\\)" "Regexp to add packages to `lisp-imenu-generic-expression'.") (eval-after-load 'lisp-mode `(add-to-list 'lisp-imenu-generic-expression (list "Packages" ,prot-emacs-package-form-regexp 2))) (defconst prot-emacs-font-lock-keywords '(("(\\(prot-emacs-\\(keybind\\|abbrev\\)\\)\\_>[ \t']*\\(\\(\\sw\\|\\s_\\)+\\)?" (3 font-lock-variable-name-face nil t)) ("(\\(prot-emacs-comment\\)\\_>[ \t']*" (1 font-lock-preprocessor-face nil t)))) (font-lock-add-keywords 'emacs-lisp-mode prot-emacs-font-lock-keywords)
4.15. The init.el
final part to load the individual modules
My configuration is split into several modules (The init.el arrangements for my own modules and custom libraries). This makes it easier for me to share parts of my code but also to review it as code without delving into a large Org file (though the one Org file has its advantages, which is why you are reading this).
All I do here is load the modules. Note that some of these are subject
to user options (The init.el user options). Those who need to set
those options must have the prot-emacs-pre-custom.el
file in place,
as I have already explained in the section about these user options.
The individual modules are documented in a section of their own under the modules of my Emacs configuration.
;; For those who use my dotfiles and need an easy way to write their ;; own extras on top of what I already load. The file must exist at ;; ~/.emacs.d/prot-emacs-pre-custom.el ;; ;; The purpose of this file is for the user to define their ;; preferences BEFORE loading any of the modules. (load (locate-user-emacs-file "prot-emacs-pre-custom.el") :no-error :no-message) (require 'prot-emacs-theme) (require 'prot-emacs-essentials) (require 'prot-emacs-modeline) (require 'prot-emacs-completion) (require 'prot-emacs-search) (require 'prot-emacs-dired) (require 'prot-emacs-window) (require 'prot-emacs-git) (require 'prot-emacs-org) (require 'prot-emacs-langs) (require 'prot-emacs-email) (require 'prot-emacs-web) (when prot-emacs-load-which-key (require 'prot-emacs-which-key)) (when prot-emacs-load-icons (require 'prot-emacs-icons)) ;; For those who use my dotfiles and need an easy way to write their ;; own extras on top of what I already load. The file must exist at ;; ~/.emacs.d/prot-emacs-post-custom.el ;; ;; The purpose of the "post customisations" is to make tweaks to what ;; I already define, such as to change the default theme. See above ;; for the `prot-emacs-pre-custom.el' to make changes BEFORE loading ;; any of my other configurations. (load (locate-user-emacs-file "prot-emacs-post-custom.el") :no-error :no-message)
5. The modules of my Emacs configuration
In my init.el
I have a section where I add my modules to the
load-path
so that Emacs can run their code (The init.el arrangements for my own modules and custom libraries).
The subheadings of this chapter define modules, each of which is
loaded at the end of my init.el
(The init.el final part to load the individual modules).
5.1. The prot-emacs-theme.el
module
In this module I define everything broadly related to the aesthetics of Emacs.
5.1.1. The prot-emacs-theme.el
section to load a theme (prot-emacs-load-theme-family
)
We start by loading one of my themes (The init.el option to load a theme family). These are actually defined in modules of their own, though this is the only place where they are used.
;;; Theme setup and related ;;;; Load the desired theme module ;; These all reference my packages: `modus-themes', `ef-themes', ;; `standard-themes'. (when prot-emacs-load-theme-family (require (pcase prot-emacs-load-theme-family ('ef 'prot-emacs-ef-themes) ('modus 'prot-emacs-modus-themes) ('standard 'prot-emacs-standard-themes))))
5.1.1.1. The prot-emacs-modus-themes.el
module
This is one of the components of the prot-emacs-theme.el
module
(The prot-emacs-theme.el
section to load a theme (prot-emacs-load-theme-family
)).
It defines some theme settings and also includes code I use when I
need to test things.
The modus-themes
are highly accessible themes, conforming with the
highest standard for colour contrast between background and foreground
values (WCAG AAA). They also are optimised for users with red-green or
blue-yellow colour deficiency.
The themes are very customisable and provide support for a wide range of packages. Their manual is detailed so that new users can get started, while it also provides custom code for all sorts of more advanced customisations.
Since August 2020, the original Modus themes (modus-operandi
,
modus-vivendi
) are built into Emacs version 28 or higher. Emacs 28
ships with modus-themes
version 1.6.0
. Emacs 29 includes version
3.0.0
. Emacs 30 provides a newer, refactored version that
thoroughly refashions how the themes are implemented and customized.
Such major versions are not backward-compatible due to the limited
resources at my disposal to support multiple versions of Emacs and of
the themes across the years.
- Package name (GNU ELPA):
modus-themes
- Official manual: https://protesilaos.com/emacs/modus-themes
- Change log: https://protesilaos.com/emacs/modus-themes-changelog
- Colour palette: https://protesilaos.com/emacs/modus-themes-colors
- Sample pictures: https://protesilaos.com/emacs/modus-themes-pictures
- Git repositories:
- Backronym: My Old Display Unexpectedly Sharpened … themes
Note that the prot-emacs-comment
is there for my testing purposes
(The init.el
macro to do nothing with Elisp code (prot-emacs-comment
)).
;;; The Modus themes ;; The themes are highly customisable. Read the manual: ;; <https://protesilaos.com/emacs/modus-themes>. (use-package modus-themes :ensure t :demand t :bind (("<f5>" . modus-themes-toggle) ("C-<f5>" . modus-themes-select) ("M-<f5>" . modus-themes-rotate)) :config (setq modus-themes-custom-auto-reload nil modus-themes-to-toggle '(modus-operandi modus-vivendi) ;; modus-themes-to-toggle '(modus-operandi-tinted modus-vivendi-tinted) ;; modus-themes-to-toggle '(modus-operandi-deuteranopia modus-vivendi-deuteranopia) ;; modus-themes-to-toggle '(modus-operandi-tritanopia modus-vivendi-tritanopia) modus-themes-to-rotate modus-themes-items modus-themes-mixed-fonts t modus-themes-variable-pitch-ui t modus-themes-italic-constructs t modus-themes-bold-constructs nil modus-themes-completions '((t . (extrabold))) modus-themes-prompts '(extrabold) modus-themes-headings '((agenda-structure . (variable-pitch light 2.2)) (agenda-date . (variable-pitch regular 1.3)) (t . (regular 1.15)))) (setq modus-themes-common-palette-overrides nil) (if (prot-emacs-theme-environment-dark-p) (modus-themes-load-theme (cadr modus-themes-to-toggle)) (modus-themes-load-theme (car modus-themes-to-toggle)))) ;; NOTE: For testing purposes (prot-emacs-comment (:eval nil) (progn (mapc #'disable-theme custom-enabled-themes) (add-to-list 'load-path "/home/prot/Git/Projects/modus-themes/") (require 'modus-themes) (setq modus-themes-custom-auto-reload nil modus-themes-to-toggle '(modus-operandi modus-vivendi) ;; modus-themes-to-toggle '(modus-operandi-tinted modus-vivendi-tinted) ;; modus-themes-to-toggle '(modus-operandi-deuteranopia modus-vivendi-deuteranopia) ;; modus-themes-to-toggle '(modus-operandi-tritanopia modus-vivendi-tritanopia) modus-themes-mixed-fonts t modus-themes-variable-pitch-ui nil modus-themes-italic-constructs t modus-themes-bold-constructs t modus-themes-completions '((t . (extrabold))) modus-themes-prompts nil modus-themes-headings '((agenda-structure . (variable-pitch light 2.2)) (agenda-date . (variable-pitch regular 1.3)) (t . (regular 1.15)))) ;; (setq modus-themes-common-palette-overrides nil) (setq modus-themes-common-palette-overrides `((fringe unspecified) ;; (bg-mode-line-active bg-lavender) ;; (border-mode-line-active unspecified) ;; (border-mode-line-inactive unspecified) (bg-line-number-active bg-hl-line) (bg-line-number-inactive unspecified) (fg-line-number-active fg-main) ;; ,@modus-themes-preset-overrides-warmer )) ;; ;; For testing purposes I only want the overrides for those two ;; ;; Modus themes. The rest have their own styles already. ;; ;; (let ((overrides '((cursor blue-intense) ;; (keybind green-cooler) ;; (comment red-faint) ;; (bg-paren-match unspecified) ;; (fg-paren-match magenta-intense) ;; (underline-paren-match magenta-intense)))) ;; (setq modus-operandi-palette-overrides overrides ;; modus-vivendi-palette-overrides overrides)) ;; ;; Make the active mode line have a pseudo 3D effect (this assumes ;; ;; you are using the default mode line and not an extra package). ;; (custom-set-faces ;; '(mode-line ((t :box (:style unspecified))))) (if (prot-emacs-theme-environment-dark-p) (modus-themes-load-theme (cadr modus-themes-to-toggle)) (modus-themes-load-theme (car modus-themes-to-toggle))) ;; Also check `modus-themes-select'. To list the palette's colours, ;; use `modus-themes-list-colors', `modus-themes-list-colors-current'. (define-key global-map (kbd "<f5>") #'modus-themes-toggle))) (provide 'prot-emacs-modus-themes)
5.1.1.2. The prot-emacs-ef-themes.el
module
This is one of the components of the prot-emacs-theme.el
module
(The prot-emacs-theme.el
section to load a theme (prot-emacs-load-theme-family
)).
It defines some theme settings and also includes code I use when I
need to test things.
The ef-themes
are a collection of light and dark themes for GNU
Emacs that provide colourful (“pretty”) yet legible options for users
who want something with a bit more flair than the modus-themes
(also
designed by me).
- Package name (GNU ELPA):
ef-themes
- Official manual: https://protesilaos.com/emacs/ef-themes
- Change log: https://protesilaos.com/emacs/ef-themes-changelog
- Sample pictures: https://protesilaos.com/emacs/ef-themes-pictures
- Git repositories:
- Backronym: Eclectic Fashion in Themes Hides Exaggerated Markings, Embellishments, and Sparkles.
Note that the prot-emacs-comment
is there for my testing purposes
(The init.el
macro to do nothing with Elisp code (prot-emacs-comment
)).
;;; The Ef (εὖ) themes ;; The themes are customisable. Read the manual: ;; <https://protesilaos.com/emacs/ef-themes>. (use-package ef-themes :ensure t :demand t :bind (("<f5>" . ef-themes-rotate) ("C-<f5>" . ef-themes-select)) :config (setq ef-themes-variable-pitch-ui t ef-themes-mixed-fonts t ef-themes-rotate ef-themes-items ef-themes-headings ; read the manual's entry of the doc string '((0 . (variable-pitch light 1.9)) (1 . (variable-pitch light 1.8)) (2 . (variable-pitch regular 1.7)) (3 . (variable-pitch regular 1.6)) (4 . (variable-pitch regular 1.5)) (5 . (variable-pitch 1.4)) ; absence of weight means `bold' (6 . (variable-pitch 1.3)) (7 . (variable-pitch 1.2)) (agenda-date . (semilight 1.5)) (agenda-structure . (variable-pitch light 1.9)) (t . (variable-pitch 1.1)))) ;; The `ef-themes' provide lots of themes. I want to pick one at ;; random when I start Emacs: the `ef-themes-load-random' does just ;; that (it can be called interactively as well). I just check with ;; my desktop environment to determine if the choice should be about ;; a light or a dark theme. Those functions are in my init.el. (if (prot-emacs-theme-environment-dark-p) (ef-themes-load-random 'dark) (ef-themes-load-random 'light))) ;; NOTE: For testing purposes (prot-emacs-comment (:eval nil) (progn (mapc #'disable-theme custom-enabled-themes) (add-to-list 'load-path "/home/prot/Git/Projects/ef-themes/") (require 'ef-themes) (load-theme 'ef-arbutus t t) (load-theme 'ef-autumn t t) (load-theme 'ef-bio t t) (load-theme 'ef-cherie t t) (load-theme 'ef-cyprus t t) (load-theme 'ef-dark t t) (load-theme 'ef-day t t) (load-theme 'ef-deuteranopia-dark t t) (load-theme 'ef-deuteranopia-light t t) (load-theme 'ef-duo-dark t t) (load-theme 'ef-duo-light t t) (load-theme 'ef-eagle t t) (load-theme 'ef-frost t t) (load-theme 'ef-kassio t t) (load-theme 'ef-light t t) (load-theme 'ef-melissa-dark t t) (load-theme 'ef-melissa-light t t) (load-theme 'ef-night t t) (load-theme 'ef-owl t t) (load-theme 'ef-rosa t t) (load-theme 'ef-spring t t) (load-theme 'ef-summer t t) (load-theme 'ef-symbiosis t t) (load-theme 'ef-trio-dark t t) (load-theme 'ef-trio-light t t) (load-theme 'ef-tritanopia-dark t t) (load-theme 'ef-tritanopia-light t t) (load-theme 'ef-winter t t) (setq ef-themes-headings ; read the manual's entry or the doc string '((0 . (variable-pitch light 1.9)) (1 . (variable-pitch light 1.8)) (2 . (variable-pitch light 1.7)) (3 . (variable-pitch semilight 1.6)) (4 . (variable-pitch semilight 1.5)) (5 . (variable-pitch regular 1.4)) (6 . (variable-pitch regular 1.3)) (7 . (variable-pitch regular 1.2)) ; absence of weight means `bold' (agenda-date . (semilight 1.5)) (agenda-structure . (variable-pitch light 1.9)) (t . (variable-pitch regular 1.1)))) ;; They are nil by default... (setq ef-themes-mixed-fonts t ef-themes-variable-pitch-ui nil) (mapcar (lambda (theme) (add-to-list 'custom-theme-load-path (concat "/home/prot/Git/Projects/ef-themes/" (symbol-name theme) "-theme.el"))) (ef-themes--list-enabled-themes)) (if (prot-emacs-theme-environment-dark-p) (ef-themes-load-random 'dark) (ef-themes-load-random 'light)) (define-key global-map (kbd "<f5>") #'ef-themes-rotate) (define-key global-map (kbd "C-<f5>") #'ef-themes-select))) (provide 'prot-emacs-ef-themes)
5.1.1.3. The prot-emacs-standard-themes.el
module
This is one of the components of the prot-emacs-theme.el
module
(The prot-emacs-theme.el
section to load a theme (prot-emacs-load-theme-family
)).
It defines some theme settings and also includes code I use when I
need to test things.
The standard-themes
are a collection of light and dark themes for
GNU Emacs. The standard-light
and standard-dark
emulate the
out-of-the-box looks of Emacs (which technically do NOT constitute a
theme) while bringing to them thematic consistency, customizability,
and extensibility. Other themes are stylistic variations of those.
In practice, the Standard themes take the default style of the font-lock and Org faces, complement it with a wider and harmonious colour palette, address many inconsistencies, and apply established semantic patterns across all interfaces by supporting a large number of packages.
Note that the prot-emacs-comment
is there for my testing purposes
(The init.el
macro to do nothing with Elisp code (prot-emacs-comment
)).
;;; The Standard themes ;; The themes are customisable. Read the manual: ;; <https://protesilaos.com/emacs/standard-themes>. (use-package standard-themes :ensure t :demand t :bind (("<f5>" . standard-themes-toggle) ("M-<f5>" . standard-themes-rotate)) :config (setq standard-themes-bold-constructs t standard-themes-italic-constructs t standard-themes-mixed-fonts t standard-themes-variable-pitch-ui t ;; more complex alist to set weight, height, and optional ;; `variable-pitch' per heading level (t is for any level not ;; specified) standard-themes-headings '((0 . (variable-pitch light 1.9)) (1 . (variable-pitch light 1.8)) (2 . (variable-pitch light 1.7)) (3 . (variable-pitch semilight 1.6)) (4 . (variable-pitch semilight 1.5)) (5 . (variable-pitch 1.4)) (6 . (variable-pitch 1.3)) (7 . (variable-pitch 1.2)) (agenda-date . (1.3)) (agenda-structure . (variable-pitch light 1.8)) (t . (variable-pitch 1.1)))) ;; Load a theme that is consistent with my session's theme. Those ;; functions are defined in my init.el. (standard-themes-load-theme (if (prot-emacs-theme-environment-dark-p) 'standard-dark 'standard-light))) ;; NOTE: For testing purposes (prot-emacs-comment (:eval nil) (progn (mapc #'disable-theme custom-enabled-themes) (add-to-list 'load-path "/home/prot/Git/Projects/standard-themes/") (require 'standard-themes) (load-theme 'standard-dark t t) (load-theme 'standard-light t t) (load-theme 'standard-dark-tinted t t) (load-theme 'standard-light-tinted t t) (setq standard-themes-bold-constructs t standard-themes-italic-constructs t standard-themes-disable-other-themes t standard-themes-mixed-fonts t standard-themes-variable-pitch-ui t ;; more complex alist to set weight, height, and optional ;; `variable-pitch' per heading level (t is for any level not ;; specified) standard-themes-headings '((0 . (variable-pitch light 1.9)) (1 . (variable-pitch light 1.8)) (2 . (variable-pitch light 1.7)) (3 . (variable-pitch semilight 1.6)) (4 . (variable-pitch semilight 1.5)) (5 . (variable-pitch 1.4)) (6 . (variable-pitch 1.3)) (7 . (variable-pitch 1.2)) (agenda-date . (1.3)) (agenda-structure . (variable-pitch light 1.8)) (t . (variable-pitch 1.1)))) (mapcar (lambda (theme) (add-to-list 'custom-theme-load-path (concat "/home/prot/Git/Projects/standard-themes/" (symbol-name theme) "-theme.el"))) (standard-themes--list-enabled-themes)) (standard-themes-load-theme (if (prot-emacs-theme-environment-dark-p) 'standard-dark 'standard-light)) (define-key global-map (kbd "<f5>") #'standard-themes-toggle) (define-key global-map (kbd "M-<f5>") #'standard-themes-rotate))) (provide 'prot-emacs-standard-themes)
5.1.2. The prot-emacs-theme.el
section for pulsar
This is a small package of mine that temporarily highlights the
current line after a given function is invoked. The affected functions
are defined in the user option pulsar-pulse-functions
. What Pulsar
does is set up an advice so that those functions run a hook after they
are called. The pulse effect is added there (pulsar-after-function-hook
).
- Package name (GNU ELPA):
pulsar
- Official manual: https://protesilaos.com/emacs/pulsar
- Change log: https://protesilaos.com/emacs/pulsar-changelog
- Git repositories:
- Backronym: Pulsar Unquestionably Luminates, Strictly Absent the Radiation
;;;; Pulsar ;; Read the pulsar manual: <https://protesilaos.com/emacs/pulsar>. (use-package pulsar :ensure t :config (setopt pulsar-pulse t pulsar-delay 0.055 pulsar-iterations 10 pulsar-face 'pulsar-green pulsar-highlight-face 'pulsar-magenta) (pulsar-global-mode 1) :hook ;; There are convenience functions/commands which pulse the line using ;; a specific colour: `pulsar-pulse-line-red' is one of them. ((next-error . (pulsar-pulse-line-red pulsar-recenter-top pulsar-reveal-entry)) (minibuffer-setup . pulsar-pulse-line-red)) :bind ;; pulsar does not define any key bindings. This is just my personal ;; preference. Remember to read the manual on the matter. Evaluate: ;; ;; (info "(elisp) Key Binding Conventions") (("C-x l" . pulsar-pulse-line) ; override `count-lines-page' ("C-x L" . pulsar-highlight-dwim))) ; or use `pulsar-highlight-line'
5.1.3. The prot-emacs-theme.el
section for lin
My lin
package is a stylistic enhancement for Emacs’ built-in
hl-line-mode
. It remaps the hl-line
face (or equivalent)
buffer-locally to a style that is optimal for major modes where line
selection is the primary mode of interaction.
The idea is that hl-line-mode
cannot work equally well for contexts
with competing priorities: (i) line selection, or (ii) simple line
highlight. In the former case, the current line needs to be made
prominent because it carries a specific meaning of some significance
in the given context: the user has to select a line. Whereas in the
latter case, the primary mode of interaction does not revolve around
the line highlight itself: it may be because the focus is on editing
text or reading through the buffer’s contents, so the current line
highlight is more of a reminder of the point’s location on the
vertical axis.
- Package name (GNU ELPA):
lin
- Official manual: https://protesilaos.com/emacs/lin
- Change log: https://protesilaos.com/emacs/lin-changelog
- Git repositories:
- GitHub: https://github.com/protesilaos/lin
- GitLab: https://gitlab.com/protesilaos/lin
- Backronym: LIN Is Noticeable
;;;; Lin ;; Read the lin manual: <https://protesilaos.com/emacs/lin>. (use-package lin :ensure t :hook (after-init . lin-global-mode) ; applies to all `lin-mode-hooks' :config ;; You can use this to live update the face: ;; ;; (customize-set-variable 'lin-face 'lin-green) ;; ;; Or `setopt' on Emacs 29: (setopt lin-face 'lin-yellow) ;; ;; I still prefer `setq' for consistency. (setq lin-face 'lin-cyan))
5.1.4. The prot-emacs-theme.el
section for spacious-padding
This package provides a global minor mode to increase the
spacing/padding of Emacs windows and frames. The idea is to make
editing and reading feel more comfortable. Enable the mode with M-x
spacious-padding-mode
. Adjust the exact spacing values by modifying
the user option spacious-padding-widths
.
Inspiration for this package comes from Nicolas Rougier’s impressive designs
and Daniel Mendler’s org-modern
package.
- Package name (GNU ELPA):
spacious-padding
- Official manual: https://protesilaos.com/emacs/spacious-padding
- Git repositories:
- Sample images:
- Backronyms: Space Perception Adjusted Consistently Impacts Overall Usability State … padding; Spacious … Precise Adjustments to Desktop Divider Internals Neatly Generated.
I also take care to make it work with my customised mode line
(The prot-emacs-modeline.el
module).
;;;; Increase padding of windows/frames ;; Yet another one of my packages: ;; <https://protesilaos.com/codelog/2023-06-03-emacs-spacious-padding/>. (use-package spacious-padding :ensure t :if (display-graphic-p) :hook (after-init . spacious-padding-mode) :bind ("<f8>" . spacious-padding-mode) :init ;; These are the defaults, but I keep it here for visiibility. (setq spacious-padding-widths '( :internal-border-width 30 :header-line-width 4 :mode-line-width 6 :tab-width 4 :right-divider-width 30 :scroll-bar-width 8 :left-fringe-width 20 :right-fringe-width 20)) ;; (setq spacious-padding-subtle-mode-line ;; `( :mode-line-active ,(if (or (eq prot-emacs-load-theme-family 'modus) ;; (eq prot-emacs-load-theme-family 'standard)) ;; 'default ;; 'help-key-binding) ;; :mode-line-inactive window-divider)) ;; Read the doc string of `spacious-padding-subtle-mode-line' as ;; it is very flexible. (setq spacious-padding-subtle-mode-line nil))
5.1.5. The prot-emacs-theme.el
section for rainbow-mode
This package produces an in-buffer preview of a colour value. I use
those while developing my themes, hence the prot/rainbow-mode-in-themes
to activate rainbow-mode
if I am editing a theme file.
;;;; Rainbow mode for colour previewing (rainbow-mode.el) (use-package rainbow-mode :ensure t :init (setq rainbow-ansi-colors nil) (setq rainbow-x-colors nil) (defun prot/rainbow-mode-in-themes () (when-let* ((file (buffer-file-name)) ((derived-mode-p 'emacs-lisp-mode)) ((string-match-p "-theme" file))) (rainbow-mode 1))) :bind ( :map ctl-x-x-map ("c" . rainbow-mode)) ; C-x x c :hook (emacs-lisp-mode . prot/rainbow-mode-in-themes))
5.1.6. The prot-emacs-theme.el
section for cursory
My cursory
package provides a thin wrapper around built-in variables
that affect the style of the Emacs cursor on graphical terminals. The
intent is to allow the user to define preset configurations such as
“block with slow blinking” or “bar with fast blinking” and set them on
demand. The use-case for such presets is to adapt to evolving
interface requirements and concomitant levels of expected comfort,
such as in the difference between writing and reading.
- Package name (GNU ELPA):
cursory
- Official manual: https://protesilaos.com/emacs/cursory
- Change log: https://protesilaos.com/emacs/cursory-changelog
- Git repositories:
- Backronym: Cursor Usability Requires Styles Objectively Rated Yearlong
;;; Cursor appearance (cursory) ;; Read the manual: <https://protesilaos.com/emacs/cursory>. (use-package cursory :ensure t :demand t :if (display-graphic-p) :config (setq cursory-presets '((box :blink-cursor-interval 1.2) (box-no-blink :blink-cursor-mode -1) (bar :cursor-type (bar . 2) :blink-cursor-interval 0.8) (bar-no-other-window :inherit bar :cursor-in-non-selected-windows nil) (bar-no-blink :cursor-type (bar . 2) :blink-cursor-mode -1) (underscore :cursor-type (hbar . 3) :blink-cursor-interval 0.3 :blink-cursor-blinks 50) (underscore-no-other-window :inherit underscore :cursor-in-non-selected-windows nil) (underscore-thick :cursor-type (hbar . 8) :blink-cursor-interval 0.3 :blink-cursor-blinks 50 :cursor-in-non-selected-windows (hbar . 3)) (underscore-thick-no-blink :blink-cursor-mode -1 :cursor-type (hbar . 8) :cursor-in-non-selected-windows (hbar . 3)) (t ; the default values :cursor-type box :cursor-in-non-selected-windows hollow :blink-cursor-mode 1 :blink-cursor-blinks 10 :blink-cursor-interval 0.2 :blink-cursor-delay 0.2))) ;; I am using the default values of `cursory-latest-state-file'. ;; Set last preset or fall back to desired style from `cursory-presets'. (cursory-set-preset (or (cursory-restore-latest-preset) 'box)) (cursory-mode 1) :bind ;; We have to use the "point" mnemonic, because C-c c is often the ;; suggested binding for `org-capture' and is the one I use as well. ("C-c p" . cursory-set-preset))
5.1.7. The prot-emacs-theme.el
section for theme-buffet
The theme-buffet
package arranges to automatically change themes
during specific times of the day or at fixed intervals. The collection
of themes is customisable, with the default options covering the
built-in Emacs themes as well as my modus-themes
and ef-themes
.
Bruno Boal is the lead developer and I am a co-maintainer.
- Package name (GNU ELPA):
theme-buffet
- Git repo on SourceHut: https://git.sr.ht/~bboal/theme-buffet
- Mirrors:
- GitHub: https://github.com/BBoal/theme-buffet
- Codeberg: https://codeberg.org/BBoal/theme-buffet
- Mirrors:
- Mailing list: https://lists.sr.ht/~bboal/general-issues
- Backronym: Themes Harmoniously Exchanged Mid Evening Beget Understandable Feelings of Fascination, Excitement, and Thrill.
;;;; Theme buffet (use-package theme-buffet :ensure t :after (:any modus-themes ef-themes) :defer 1 :config (let ((modus-themes-p (featurep 'modus-themes)) (ef-themes-p (featurep 'ef-themes))) (setq theme-buffet-menu 'end-user) (setq theme-buffet-end-user (cond ((and modus-themes-p ef-themes-p) '( :night (modus-vivendi ef-dark ef-winter ef-autumn ef-night ef-duo-dark ef-symbiosis) :morning (modus-operandi ef-light ef-cyprus ef-spring ef-frost ef-duo-light) :afternoon (modus-operandi-tinted ef-arbutus ef-day ef-kassio ef-summer ef-elea-light ef-maris-light ef-melissa-light ef-trio-light ef-reverie) :evening (modus-vivendi-tinted ef-rosa ef-elea-dark ef-maris-dark ef-melissa-dark ef-trio-dark ef-dream))) (ef-themes-p '( :night (ef-dark ef-winter ef-autumn ef-night ef-duo-dark ef-symbiosis ef-owl) :morning (ef-light ef-cyprus ef-spring ef-frost ef-duo-light ef-eagle) :afternoon (ef-arbutus ef-day ef-kassio ef-summer ef-elea-light ef-maris-light ef-melissa-light ef-trio-light ef-reverie) :evening (ef-rosa ef-elea-dark ef-maris-dark ef-melissa-dark ef-trio-dark ef-dream))) (modus-themes-p '( :night (modus-vivendi modus-vivendi-tinted modus-vivendi-tritanopia modus-vivendi-deuteranopia) :morning (modus-operandi modus-operandi-tinted modus-operandi-tritanopia modus-operandi-deuteranopia) :afternoon (modus-operandi modus-operandi-tinted modus-operandi-tritanopia modus-operandi-deuteranopia) :evening (modus-vivendi modus-vivendi-tinted modus-vivendi-tritanopia modus-vivendi-deuteranopia))))) (when (or modus-themes-p ef-themes-p) (theme-buffet-timer-hours 1))))
5.1.8. The prot-emacs-theme.el
section about fontaine
[ Watch: Customise Emacs fonts (2024-01-16) ]
My fontaine
package allows the user to define detailed font
configurations and set them on demand. For example, one can have a
regular-editing
preset and another for presentation-mode
(these
are arbitrary, user-defined symbols): the former uses small fonts
which are optimised for writing, while the latter applies typefaces
that are pleasant to read at comfortable point sizes.
- Package name (GNU ELPA):
fontaine
- Official manual: https://protesilaos.com/emacs/fontaine
- Change log: https://protesilaos.com/emacs/fontaine-changelog
- Git repositories:
- Backronym: Fonts, Ornaments, and Neat Typography Are Irrelevant in Non-graphical Emacs
You will notice that all my fonts are Iosevka Comfy variants. I explain what this is about (Information about my Iosevka Comfy fonts).
Another section defines some complementary functionality
(The prot-emacs-theme.el
section about variable-pitch-mode
and font resizing).
Also check my show-font
package (The prot-emacs-theme.el
section about show-font
).
;;;; Fontaine (font configurations) ;; Read the manual: <https://protesilaos.com/emacs/fontaine> (use-package fontaine :ensure t :if (display-graphic-p) :hook ;; Persist the latest font preset when closing/starting Emacs and ;; while switching between themes. ((after-init . fontaine-mode) (after-init . (lambda () ;; Set last preset or fall back to desired style from `fontaine-presets'. (fontaine-set-preset (or (fontaine-restore-latest-preset) 'regular))))) :bind (("C-c f" . fontaine-set-preset) ("C-c F" . fontaine-toggle-preset)) :config ;; This is defined in Emacs C code: it belongs to font settings. (setq x-underline-at-descent-line nil) ;; And this is for Emacs28. (setq-default text-scale-remap-header-line t) ;; This is the default value. Just including it here for ;; completeness. (setq fontaine-latest-state-file (locate-user-emacs-file "fontaine-latest-state.eld")) (setq fontaine-presets '((small :default-height 80) (regular) ; like this it uses all the fallback values and is named `regular' (medium :default-weight semilight :default-height 115 :bold-weight extrabold) (large :inherit medium :default-height 150) (live-stream :default-family "Iosevka Comfy Wide Motion" :default-height 150 :default-weight medium :fixed-pitch-family "Iosevka Comfy Wide Motion" :variable-pitch-family "Iosevka Comfy Wide Duo" :bold-weight extrabold) (presentation :default-height 180) (jumbo :default-height 260) (t ;; I keep all properties for didactic purposes, but most can be ;; omitted. See the fontaine manual for the technicalities: ;; <https://protesilaos.com/emacs/fontaine>. :default-family "Iosevka Comfy" :default-weight regular :default-slant normal :default-width normal :default-height 100 :fixed-pitch-family "Iosevka Comfy" :fixed-pitch-weight nil :fixed-pitch-slant nil :fixed-pitch-width nil :fixed-pitch-height 1.0 :fixed-pitch-serif-family nil :fixed-pitch-serif-weight nil :fixed-pitch-serif-slant nil :fixed-pitch-serif-width nil :fixed-pitch-serif-height 1.0 :variable-pitch-family "Iosevka Comfy Motion Duo" :variable-pitch-weight nil :variable-pitch-slant nil :variable-pitch-width nil :variable-pitch-height 1.0 :mode-line-active-family nil :mode-line-active-weight nil :mode-line-active-slant nil :mode-line-active-width nil :mode-line-active-height 1.0 :mode-line-inactive-family nil :mode-line-inactive-weight nil :mode-line-inactive-slant nil :mode-line-inactive-width nil :mode-line-inactive-height 1.0 :header-line-family nil :header-line-weight nil :header-line-slant nil :header-line-width nil :header-line-height 1.0 :line-number-family nil :line-number-weight nil :line-number-slant nil :line-number-width nil :line-number-height 1.0 :tab-bar-family nil :tab-bar-weight nil :tab-bar-slant nil :tab-bar-width nil :tab-bar-height 1.0 :tab-line-family nil :tab-line-weight nil :tab-line-slant nil :tab-line-width nil :tab-line-height 1.0 :bold-family nil :bold-slant nil :bold-weight bold :bold-width nil :bold-height 1.0 :italic-family nil :italic-weight nil :italic-slant italic :italic-width nil :italic-height 1.0 :line-spacing nil))) (with-eval-after-load 'pulsar (add-hook 'fontaine-set-preset-hook #'pulsar-pulse-line)))
5.1.9. The prot-emacs-theme.el
section about show-font
This is yet another package of mine. It lets you preview a font inside of Emacs. It does so in three ways:
- Prompt for a font on the system and display it in a buffer.
- List all known fonts in a buffer, with a short preview for each.
- Provide a major mode to preview a font whose file is among the installed ones.
Check out its sources:
- Package name (GNU ELPA):
show-font
- Official manual: https://protesilaos.com/emacs/show-font
- Change log: https://protesilaos.com/emacs/show-font-changelog
- Git repository: https://github.com/protesilaos/show-font
- Sample pictures: https://protesilaos.com/codelog/2024-09-10-emacs-show-font-0-1-0/
- Backronym: Show How Outlines Will Feature Only in Non-TTY.
To actually set fonts, I use my fontaine
package (The prot-emacs-theme.el
section about fontaine
).
;;;; Show Font (preview fonts) ;; Read the manual: <https://protesilaos.com/emacs/show-font> (use-package show-font :ensure t :if (display-graphic-p) :commands (show-font-select-preview show-font-list) :config ;; These are the defaults, but I keep them here for easier access. (setq show-font-pangram 'prot) (setq show-font-character-sample " ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 !@#$¢%^&*~| `'\"‘’“”.,;: ()[]{}—-_+=<> ()[]{}<>«»‹› 6bB8&0ODdoa 1tiIlL|\/ !ij c¢ 5$Ss 7Z2z 9gqp nmMNNMW uvvwWuuw x×X .,·°;:¡!¿?`'‘’ ÄAÃÀ TODO "))
5.1.10. The prot-emacs-theme.el
section about variable-pitch-mode
and font resizing
[ Watch: Customise Emacs fonts (2024-01-16) ]
The built-in variable-pitch-mode
makes the current buffer use a
proportionately spaced font. In technical terms, it remaps the
default
face to variable-pitch
, so whatever applies to the latter
takes effect over the former. I take care of their respective font
families in my fontaine
setup (The prot-emacs-theme.el
section about fontaine
).
I want to activate variable-pitch-mode
in all buffers where I
normally focus on prose. The exact mode hooks are specified in the
variable prot/enable-variable-pitch-in-hooks
. Exceptions to these
are major modes that I do not consider related to prose (and which in
my opinion should not be derived from text-mode
): these are excluded
in the function prot/enable-variable-pitch
.
Then I also arrange the key bindings that resize text on-the-fly. I want those to have a global effect, meaning that they affect all buffers and the minibuffer, instead of the default behaviour to only adjust the current buffer’s text size (Why don’t you remap keys?).
;;;;; `variable-pitch-mode' setup (use-package face-remap :ensure nil :functions prot/enable-variable-pitch :bind ( :map ctl-x-x-map ("v" . variable-pitch-mode)) :hook ((text-mode notmuch-show-mode elfeed-show-mode) . prot/enable-variable-pitch) :config ;; NOTE 2022-11-20: This may not cover every case, though it works ;; fine in my workflow. I am still undecided by EWW. (defun prot/enable-variable-pitch () (unless (derived-mode-p 'mhtml-mode 'nxml-mode 'yaml-mode) (variable-pitch-mode 1))) ;;;;; Resize keys with global effect :bind ;; Emacs 29 introduces commands that resize the font across all ;; buffers (including the minibuffer), which is what I want, as ;; opposed to doing it only in the current buffer. The keys are the ;; same as the defaults. (("C-x C-=" . global-text-scale-adjust) ("C-x C-+" . global-text-scale-adjust) ("C-x C-0" . global-text-scale-adjust)))
5.1.10.1. Information about my Iosevka Comfy fonts
Iosevka Comfy is a customised build of the Iosevka typeface, with a consistent rounded style and overrides for almost all individual glyphs in both roman (upright) and italic (slanted) variants. Many font families are available, covering a broad range of typographic weights. The README file in the git repository covers all the technicalities.
Family | Shapes | Spacing | Style | Ligatures |
---|---|---|---|---|
Iosevka Comfy | Sans | Compact | Monospaced | Yes |
Iosevka Comfy Fixed | Sans | Compact | Monospaced | No |
Iosevka Comfy Duo | Sans | Compact | Duospaced | Yes |
Iosevka Comfy Motion | Slab | Compact | Monospaced | Yes |
Iosevka Comfy Motion Fixed | Slab | Compact | Monospaced | No |
Iosevka Comfy Motion Duo | Slab | Compact | Duospaced | Yes |
Iosevka Comfy Wide | Sans | Wide | Monospaced | Yes |
Iosevka Comfy Wide Fixed | Sans | Wide | Monospaced | No |
Iosevka Comfy Wide Duo | Sans | Wide | Duospaced | Yes |
Iosevka Comfy Wide Motion | Slab | Wide | Monospaced | Yes |
Iosevka Comfy Wide Motion Fixed | Slab | Wide | Monospaced | No |
Iosevka Comfy Wide Motion Duo | Slab | Wide | Duospaced | Yes |
- Git repositories:
- Sample pictures: https://protesilaos.com/emacs/iosevka-comfy-pictures
- Backronym: Iosevka … Could Only Modify a Font, Yes
5.1.11. The prot-emacs-theme.el
call to provide
Finally, we provide
the module. This is the mirror function of
require
(The init.el final part to load the individual modules).
(provide 'prot-emacs-theme)
5.2. The prot-emacs-essentials.el
module
This module load basic configurations that apply to most facets of Emacs. Many of my own custom libraries are loaded here (The init.el arrangements for my own modules and custom libraries).
5.2.1. The prot-emacs-essentials.el
block with basic configurations
Better use C-h v
(M-x describe-variable
) to learn about each of
these variables. Since I am here, I also unbind or repurpose some of the default
key bindings.
;;; Essential configurations (use-package emacs :ensure nil :demand t :config ;;;; General settings and common custom functions (prot-simple.el) (setq blink-matching-paren nil) (setq delete-pair-blink-delay 0.1) ; Emacs28 -- see `prot-simple-delete-pair-dwim' (setq delete-pair-push-mark t) ; Emacs 31 (setq help-window-select t) (setq next-error-recenter '(4)) ; center of the window (setq find-library-include-other-files nil) ; Emacs 29 (setq remote-file-name-inhibit-delete-by-moving-to-trash t) ; Emacs 30 (setq remote-file-name-inhibit-auto-save t) ; Emacs 30 (setq tramp-connection-timeout (* 60 10)) ; seconds (setq save-interprogram-paste-before-kill t) (setq mode-require-final-newline 'visit-save) (setq-default truncate-partial-width-windows nil) (setq eval-expression-print-length nil) (setq kill-do-not-save-duplicates t) (setq duplicate-line-final-position -1 ; both are Emacs 29 duplicate-region-final-position -1) (setq scroll-error-top-bottom t) (setq echo-keystrokes-help nil) ; Emacs 30 (setq epa-keys-select-method 'minibuffer) ; Emacs 30 ;; Keys I unbind here are either to avoid accidents or to bind them ;; elsewhere later in the configuration. :bind ( :map global-map ("<f2>" . toggle-input-method) ; F2 overrides that two-column gimmick. Sorry, but no! ("<insert>" . nil) ("<menu>" . nil) ("C-x C-d" . nil) ; never use it ("C-x C-v" . nil) ; never use it ("C-z" . nil) ; I have a window manager, thanks! ("C-x C-z" . nil) ; same idea as above ("C-x C-c" . nil) ; avoid accidentally exiting Emacs ("C-x C-c C-c" . save-buffers-kill-emacs) ; more cumbersome, less error-prone ("C-x C-r" . restart-emacs) ; override `find-file-read-only' ("C-h h" . nil) ; Never show that "hello" file ("M-`" . nil) ("M-o" . delete-blank-lines) ; alias for C-x C-o ("M-SPC" . cycle-spacing) ("M-z" . zap-up-to-char) ; NOT `zap-to-char' ("M-c" . capitalize-dwim) ("M-l" . downcase-dwim) ; "lower" case ("M-u" . upcase-dwim) ("M-=" . count-words) ("C-x O" . next-multiframe-window) ("C-h K" . describe-keymap) ; overrides `Info-goto-emacs-key-command-node' ("C-h u" . apropos-user-option) ("C-h F" . apropos-function) ; lower case is `describe-function' ("C-h V" . apropos-variable) ; lower case is `describe-variable' ("C-h L" . apropos-library) ; lower case is `view-lossage' ("C-h c" . describe-char) ; overrides `describe-key-briefly' :map prog-mode-map ("C-M-d" . up-list) ; confusing name for what looks like "down" to me ("<C-M-backspace>" . backward-kill-sexp) ;; Keymap for buffers (Emacs28) :map ctl-x-x-map ("f" . follow-mode) ; override `font-lock-update' ("r" . rename-uniquely) ("l" . visual-line-mode)))
5.2.2. The prot-emacs-essentials.el
section about prot-common.el
(custom basic functions)
The prot-common.el
library contains custom snippets that I use in
various other parts of my setup, notably my custom libraries
(“packages”). All I do here is load it, so that others will have it
available by the time they need it.
Since the prot-common.el
defines some basic functions that can be
used outside the narrow confines of my custom code, I set them up
here:
- Truncate lines by default in a number of places and do not produce
a message about the fact. Note that the function used to achieve
this, i.e.
prot-common-truncate-lines-silently
, may also be set up elsewhere and described in that context. Here I only cover the basic parent modes. - Make
M-x
not produce any message about how such and such command can also be called via this or that key binding. If I am doing something withM-x
instead of a key binding, I have a good reason for it and do not want the extra noise.
(use-package prot-common :ensure nil :functions (prot-common-truncate-lines-silently) :hook ((fundamental-mode text-mode prog-mode dired-mode) . prot-common-truncate-lines-silently) :config ;; NEVER tell me which key can call a command that I specifically ;; invoked with M-x: I have a good reason to use it that way. (advice-add #'execute-extended-command--describe-binding-msg :override #'prot-common-ignore))
5.2.3. The prot-emacs-essentials.el
section about prot-simple.el
(custom basic commands)
The prot-simple.el
library is done in the same spirit as the
built-in simple.el
: it is a file with a collection of little
commands that are useful for everyday tasks. I bind these commands to
keys.
The prot-simple-display-unsaved-buffers-on-exit
produces a list of
unsaved, file-visiting buffers before closing Emacs. I am doing this
using the advice mechanism to redefine the behaviour of the original
command (save-buffers-kill-emacs
), since I cannot find a way to do
this via some hook.
(use-package prot-simple :ensure nil :demand t :config (setq prot-simple-date-specifier "%F") (setq prot-simple-time-specifier "%R %z") (advice-add #'save-buffers-kill-emacs :before #'prot-simple-display-unsaved-buffers-on-exit) (with-eval-after-load 'pulsar (add-hook 'prot-simple-file-to-register-jump-hook #'pulsar-recenter-center) (add-hook 'prot-simple-file-to-register-jump-hook #'pulsar-reveal-entry)) :bind ( ("ESC ESC" . prot-simple-keyboard-quit-dwim) ("C-g" . prot-simple-keyboard-quit-dwim) ("C-M-SPC" . prot-simple-mark-sexp) ; will be overriden by `expreg' if tree-sitter is available ;; Commands for lines ("M-k" . prot-simple-kill-line-backward) ("M-j" . delete-indentation) ("M-w" . prot-simple-kill-ring-save) ("C-S-d" . prot-simple-duplicate-line-or-region) ("C-S-w" . prot-simple-copy-line) ("C-S-y" . prot-simple-yank-replace-line-or-region) ("C-v" . prot-simple-multi-line-below) ; overrides `scroll-up-command' ("<next>" . prot-simple-multi-line-below) ; overrides `scroll-up-command' ("M-v" . prot-simple-multi-line-above) ; overrides `scroll-down-command' ("<prior>" . prot-simple-multi-line-above) ; overrides `scroll-down-command' ("<C-return>" . prot-simple-new-line-below) ("<C-S-return>" . prot-simple-new-line-above) ("C-x x a" . prot-simple-auto-fill-visual-line-mode) ; auto-fill/visual-line toggle ;; Commands for text insertion or manipulation ("C-=" . prot-simple-insert-date) ("C-<" . prot-simple-escape-url-dwim) ;; "C->" prot-simple-insert-line-prefix-dwim ("M-Z" . prot-simple-zap-to-char-backward) ;; Commands for object transposition ("C-S-p" . prot-simple-move-above-dwim) ("C-S-n" . prot-simple-move-below-dwim) ("C-t" . prot-simple-transpose-chars) ("C-x C-t" . prot-simple-transpose-lines) ("C-S-t" . prot-simple-transpose-paragraphs) ("C-x M-t" . prot-simple-transpose-sentences) ("C-M-t" . prot-simple-transpose-sexps) ("M-t" . prot-simple-transpose-words) ;; Commands for paragraphs ("M-Q" . prot-simple-unfill-region-or-paragraph) ;; Commands for windows and pages ("C-x o" . prot-simple-other-window) ("C-x n k" . prot-simple-delete-page-delimiters) ("C-x M-r" . prot-simple-swap-window-buffers) ;; Commands for buffers ("<C-f2>" . prot-simple-rename-file-and-buffer) ("C-x k" . prot-simple-kill-buffer-current) ("C-x K" . kill-buffer) ; leaving this here to contrast with the above ("M-s b" . prot-simple-buffers-major-mode) ("M-s v" . prot-simple-buffers-vc-root) ;; Commands for files ("C-x r ." . prot-simple-file-to-register)))
5.2.4. The prot-emacs-essentials.el
section about prot-scratch.el
(scratch buffer per major mode)
The prot-scratch.el
library provides the means to create a scratch
buffer for a given major mode. It has the option to set a default
major mode to use. It can also copy the active region into the scratch
buffer. Read the doc string of the command prot-scratch-buffer
.
;;;; Scratch buffers per major mode (prot-scratch.el) (use-package prot-scratch :ensure nil :bind ("C-c s" . prot-scratch-buffer) :config (setq prot-scratch-default-mode 'text-mode))
5.2.5. The prot-emacs-essentials.el
section about prot-pair.el
(insert character pairs)
The prot-pair.el
library defines a mechanism to insert character
pairs around the symbol at point or the active region. There is a user
option called prot-pair-pairs
, which specifies which characters form
pairs. This can also run a function to set a pair according to some
context, environment, or any such condition. I use it to insert
quotation marks specific to natural languages I have set up.
;;;; Insert character pairs (prot-pair.el) (use-package prot-pair :ensure nil :bind (("C-'" . prot-pair-insert) ("M-'" . prot-pair-insert) ("M-\\" . prot-pair-delete)))
5.2.6. The prot-emacs-essentials.el
section for comments
The prot-comment.el
library expands the built-in commenting
facilities with what makes sense to me. The prot-comment
command is
like the built-in comment-dwim
, but toggles linewise commenting
instead of appending to them by default. While the prot-comment-timestamp-keyword
prompts for a keyword among prot-comment-keywords
and formats it as
a comment with a timestamp next to it. The format of the latter is
controlled by the user option prot-comment-timestamp-format-concise
or prot-comment-timestamp-format-verbose
(the verbose is set when
the command is called with a prefix argument (C-u
by default)).
A big part of writing code is the ability to quickly insert comments.
I have a custom library that builds on what Emacs makes available by
default (The prot-comment.el
library). While I use this section of
the module to set my desired configurations.
;;;; Comments (prot-comment.el) (use-package prot-comment :ensure nil :init (setq comment-empty-lines t) (setq comment-fill-column nil) (setq comment-multi-line t) (setq comment-style 'multi-line) (setq-default comment-column 0) (setq prot-comment-comment-keywords '("TODO" "NOTE" "XXX" "REVIEW" "FIXME")) (setq prot-comment-timestamp-format-concise "%F") (setq prot-comment-timestamp-format-verbose "%F %T %z") :bind (("C-;" . prot-comment) ("C-x C-;" . prot-comment-timestamp-keyword)))
5.2.7. The prot-emacs-essentials.el
section about prot-prefix.el
(prefix nested keymaps)
[ Watch: define prefix/leader key (nested key maps) (2024-01-29). ]
The prot-prefix.el
library defines a set of keymaps with commonly
used commands and puts them behind a prefix map. The idea is to hit a
series of keys to get to the desired command. Keymaps are organised
thematically and rely on strong mnemonics, such as b
for buffers,
w
for windows, and so on. The which-key
package is a nice addition
for this purpose, as it visualises incomplete key bindings after a
configurable amount of time (The prot-emacs-which-key.el
module).
;;;; Prefix keymap (prot-prefix.el) (use-package prot-prefix :ensure nil :bind-keymap (("<insert>" . prot-prefix) ("C-z" . prot-prefix)))
5.2.8. The prot-emacs-essentials.el
configuration to track recently visited files
Emacs can keep track of recently visited files. Then we can revisit
them with the command recent-open
, which provides minibuffer
completion (The prot-emacs-completion.el
module).
Recent files are also available in the consult-buffer
interface,
which makes it a one-stop-shop for opening buffers, recent files, or
bookmarks (The prot-emacs-completion.el
settings for consult
).
This can be better than having to remember if something is a buffer or
is stored by bookmarks/recentf. Same idea for using one command
instead of three (or more).
I generally do not rely on recentf-mode
, as most of my work is done
in projects, which I switch to directly. Though I sometimes need to
revisit a file that I do not need to keep track of.
(use-package recentf :ensure nil :hook (after-init . recentf-mode) :config (setq recentf-max-saved-items 100) (setq recentf-max-menu-items 25) ; I don't use the `menu-bar-mode', but this is good to know (setq recentf-save-file-modes nil) (setq recentf-keep nil) (setq recentf-auto-cleanup nil) (setq recentf-initialize-file-name-history nil) (setq recentf-filename-handlers nil) (setq recentf-show-file-shortcuts-flag nil))
5.2.9. The prot-emacs-essentials.el
mouse configurations
I do use the mouse on occasion. More so when I am doing a video demonstration.
;;;; Mouse and mouse wheel behaviour (use-package mouse :ensure nil :hook (after-init . mouse-wheel-mode) :config ;; Some of these variables are defined in places other than ;; mouse.el, but this is fine. (setq mouse-autoselect-window t) ; complements the auto-selection of my tiling window manager ;; In Emacs 27+, use Control + mouse wheel to scale text. (setq mouse-wheel-scroll-amount '(1 ((shift) . 5) ((meta) . 0.5) ((control) . text-scale)) mouse-drag-copy-region nil make-pointer-invisible t mouse-wheel-progressive-speed t mouse-wheel-follow-mouse t) ;; Scrolling behaviour (setq-default scroll-preserve-screen-position t scroll-conservatively 1 ; affects `scroll-step' scroll-margin 0 next-screen-context-lines 0))
5.2.10. The prot-emacs-essentials.el
settings for repeat-mode
The repeat-mode
is designed to find when a “repeatable” command is
called and arrange so that it can be called again with single key
press. A case in point is the other-window
command, bound to C-x o
by default. With repeat-mode
enabled, we can type C-x o
to invoke
the command and then type o
to call it again. So C-x o o o
runs
other-window
three times. This is quite convenient.
I think repeatable commands are not easy to define because (i) we have
to put a property to their symbol and (ii) have them in a keymap that
repeat-mode
knows about. The defvar-keymap
of Emacs 29 makes this
a bit easier for users, though it still is a rather advanced feature.
In most cases, just know that repeat-mode
is nice to have, though
you can probably use Emacs just fine without it.
;;;; Repeatable key chords (repeat-mode) (use-package repeat :ensure nil :hook (after-init . repeat-mode) :config (setq repeat-on-final-keystroke t repeat-exit-timeout 5 repeat-exit-key "<escape>" repeat-keep-prefix nil repeat-check-key t repeat-echo-function 'ignore ;; Technically, this is not in repeal.el, though it is the ;; same idea. set-mark-command-repeat-pop t))
5.2.11. The prot-emacs-essentials.el
settings for bookmarks
Bookmarks are compartments that store arbitrary information about a
file or buffer. The records are used to recreate that file/buffer
inside of Emacs. Put differently, we can easily jump back to a file or
directory (or anything that has a bookmark recorder+handler, really).
Use the bookmark-set
command (C-x r m
by default) to record a
bookmark and then visit one of your bookmarks with bookmark-jump
(C-x r b
by default).
Also see the prot-emacs-essentials.el
settings for registers.
;;;; Built-in bookmarking framework (bookmark.el) (use-package bookmark :ensure nil :commands (bookmark-set bookmark-jump bookmark-bmenu-list) :hook (bookmark-bmenu-mode . hl-line-mode) :config (setq bookmark-use-annotations nil) (setq bookmark-automatically-show-annotations nil) (setq bookmark-fringe-mark nil) ; Emacs 29 to hide bookmark fringe icon ;; Write changes to the bookmark file as soon as 1 modification is ;; made (addition or deletion). Otherwise Emacs will only save the ;; bookmarks when it closes, which may never happen properly ;; (e.g. power failure). (setq bookmark-save-flag 1))
5.2.12. The prot-emacs-essentials.el
settings for registers
[ Watch: Mark and register basics (2023-06-28). ]
Much like bookmarks, registers store data that we can reinstate
quickly (The prot-emacs-essentials.el
settings for bookmarks). A
common use-case is to write some text to a register and then insert
that text by calling the given register. This is much better than
relying on the kill-ring
, because registers are meant to be
overwritten by the user, whereas the kill-ring
accumulates lots of
text that we do not necessarily need.
To me, registers are essential for keyboard macros. By default,
registers do not persist between Emacs sessions, though I do need to
re-use them from time to time, hence the arrangement to record them
with savehist-mode
(The prot-emacs-completion.el
settings for saving the history (savehist-mode
)).
;;;; Registers (register.el) (use-package register :ensure nil :defer t ; its commands are autoloaded, so this will be loaded then :config (setq register-preview-delay 0.8 register-preview-function #'register-preview-default) (with-eval-after-load 'savehist (add-to-list 'savehist-additional-variables 'register-alist)))
5.2.13. The prot-emacs-essentials.el
settings for auto revert
The “auto revert” facility makes Emacs update the contents of a saved
buffer when its underlying file is changed externally. This can happen,
for example, when a git pull
modifies the file we are already
displaying in a buffer. Emacs thus automatically reverts the buffer to
reflect the new file contents.
;;;; Auto revert mode (use-package autorevert :ensure nil :hook (after-init . global-auto-revert-mode) :config (setq auto-revert-verbose t))
5.2.14. The prot-emacs-essentials.el
section for delete-selection-mode
Every graphical application I have ever used will delete the selected
text upon the insertion of new text. Emacs does not do this by
default. With delete-selection-mode
we get it.
;;;; Delete selection (use-package delsel :ensure nil :hook (after-init . delete-selection-mode))
5.2.15. The prot-emacs-essentials.el
settings for tooltips
With these settings in place, Emacs will use its own faces and frame infrastructure to display tooltips. I prefer it this way because then we can benefit from the text properties that can be added to these messages (e.g. a different colour or a slant).
;;;; Tooltips (tooltip-mode) (use-package tooltip :ensure nil :hook (after-init . tooltip-mode) :config (setq tooltip-delay 0.5 tooltip-short-delay 0.5 x-gtk-use-system-tooltips t tooltip-frame-parameters '((name . "tooltip") (internal-border-width . 10) (border-width . 0) (no-special-glyphs . t))))
5.2.16. The prot-emacs-essentials.el
configurations for the date and time (display-time-mode
)
I like to display the current date and time on the mode line(The prot-emacs-modeline.el
module).
This is what display-time-mode
does. Note that my custom modeline
shows the time only in the active/selected window. Otherwise, the
default is to show the time on all mode lines, which is annoying.
The time.el
library which provides the display-time-mode
also
defines functions to get the load average and check a directory for
new emails. I have no use for the load avergae, while emails counters
are best handled by my notmuch-indicator
package
(The prot-emacs-email.el
submodule for notmuch
(prot-emacs-notmuch.el
)).
;;;; Display current time (use-package time :ensure nil :hook (after-init . display-time-mode) :config (setq display-time-format " %a %e %b, %H:%M ") ;;;; Covered by `display-time-format' ;; (setq display-time-24hr-format t) ;; (setq display-time-day-and-date t) (setq display-time-interval 60) (setq display-time-default-load-average nil) ;; NOTE 2022-09-21: For all those, I have implemented my own solution ;; that also shows the number of new items, although it depends on ;; notmuch: the `notmuch-indicator' package. (setq display-time-mail-directory nil) (setq display-time-mail-function nil) (setq display-time-use-mail-icon nil) (setq display-time-mail-string nil) (setq display-time-mail-face nil) ;; I don't need the load average and the mail indicator, so let this ;; be simple: (setq display-time-string-forms '((propertize (format-time-string display-time-format now) 'face 'display-time-date-and-time 'help-echo (format-time-string "%a %b %e, %Y" now)) " ")))
5.2.17. The prot-emacs-essentials.el
settings for the world-clock
I communicate with people from across the globe. Knowing their local
time is of paramount importance. With M-x world-clock
we get a
buffer with all cities and concomitant time zones specified in
zoneinfo-style-world-list
. The contents are displayed according to
the world-clock-time-format
. Note that I control the placement of
these and many other buffers by configuring the display-buffer-alist
(The prot-emacs-window.el
module).
;;;; World clock (M-x world-clock) (use-package time :ensure nil :commands (world-clock) :config (setq display-time-world-list t) (setq zoneinfo-style-world-list ; M-x shell RET timedatectl list-timezones '(("America/Los_Angeles" "Los Angeles") ("America/Vancouver" "Vancouver") ("Canada/Pacific" "Canada/Pacific") ("America/Chicago" "Chicago") ("Brazil/Acre" "Rio Branco") ("America/Toronto" "Toronto") ("America/New_York" "New York") ("Canada/Atlantic" "Canada/Atlantic") ("Brazil/East" "Brasília") ("UTC" "UTC") ("Europe/Lisbon" "Lisbon") ("Europe/Brussels" "Brussels") ("Europe/Athens" "Athens") ("Asia/Riyadh" "Riyadh") ("Asia/Tehran" "Tehran") ("Asia/Tbilisi" "Tbilisi") ("Asia/Yekaterinburg" "Yekaterinburg") ("Asia/Kolkata" "Kolkata") ("Asia/Singapore" "Singapore") ("Asia/Shanghai" "Shanghai") ("Asia/Seoul" "Seoul") ("Asia/Tokyo" "Tokyo") ("Asia/Vladivostok" "Vladivostok") ("Australia/Brisbane" "Brisbane") ("Australia/Sydney" "Sydney") ("Pacific/Auckland" "Auckland"))) ;; All of the following variables are for Emacs 28 (setq world-clock-list t) (setq world-clock-time-format "%R %z (%Z) %A %d %B") (setq world-clock-buffer-name "*world-clock*") ; Placement handled by `display-buffer-alist' (setq world-clock-timer-enable t) (setq world-clock-timer-second 60))
5.2.18. The prot-emacs-essentials.el
settings for manpages
Most buffers conform with rules we define in the display-buffer-alist
(The prot-emacs-window.el
module). However, M-x man
does not do
this because it has its own behaviour. At least, it is customisable.
The Man-notify-method
is a very old option, according to what the
Help buffer is telling me (check its documentation with C-h v
or
M-x describe-variable
), so I suspect this was never updated to
conform with the newer display-buffer-alist
…
;;;; `man' (manpages) (use-package man :ensure nil :commands (man) :config (setq Man-notify-method 'pushy)) ; does not obey `display-buffer-alist'
5.2.19. The prot-emacs-essentials.el
settings for proced
The M-x proced
command produces a listing of all running processes
on the system. This is like the top
program on the command-line.
While inside the *Proced*
buffer, type C-h m
(M-x describe-mode
)
to learn about keys/commands you can use therein. Personally, I mostly
use this interface to check if some process is doing more than it should.
;;;; `proced' (process monitor, similar to `top') (use-package proced :ensure nil :commands (proced) :config (setq proced-auto-update-flag 'visible) ; Emacs 30 supports more the `visible' value (setq proced-enable-color-flag t) ; Emacs 29 (setq proced-auto-update-interval 5) (setq proced-descend t) (setq proced-filter 'user))
5.2.20. The prot-emacs-essentials.el
arrangement to run Emacs as a server
The “server” is functionally like the daemon, except it is run by the
first Emacs frame we launch. With a running server, we can connect to
it through a new emacsclient
call. This is useful if we want to
launch new frames that share resources with the existing running
process. Though the emacsclient
can be used to simply evaluate code
outside of an Emacs frame (e.g. to load a new theme, as I do with my
delight.sh
shell script as part of my dotfiles).
When we close the last frame, the server is terminated. Whereas the daemon remains active even if all Emacs frames are closed. For me, the server is easier to work with, as I know for sure when its life cycle starts and ends. Beside that, I did give the daemon an honest try. Emacs would crash whenever I would encounter an error in some Lisp evaluation. Whereas the server works just fine.
Note that I only ever server-start
if there is no server running.
One is enough.
;;;; Emacs server (allow emacsclient to connect to running session) (use-package server :ensure nil :defer 1 :config (setq server-client-instructions nil) (unless (server-running-p) (server-start)))
5.2.21. The prot-emacs-essentials.el
section about substitute
I use substitute
to efficiently replace targets in the buffer or
context. The substitute
package provides a set of commands that
perform text replacement (i) throughout the buffer, (ii) limited to
the current definition (per `narrow-to-defun`), (iii) from point to
the end of the buffer, and (iv) from point to the beginning of the
buffer.
These substitutions are meant to be as quick as possible and to not
move the point. As such, they differ from the standard query-replace
(which I still use where relevant). The provided commands prompt for
substitute text and perform the substitution outright, without moving
the point. Better check the video I did on the matter:
https://protesilaos.com/codelog/2023-01-16-emacs-substitute-package-demo/.
- Package name (GNU ELPA):
substitute
- Official manual: https://protesilaos.com/emacs/substitute
- Git repositories:
- Backronym: Some Utilities Built to Substitute Targets Independent of Their Utterances, Thoroughly and Easily.
;;; Substitute ;; Another package of mine... Video demo: ;; <https://protesilaos.com/codelog/2023-01-16-emacs-substitute-package-demo/>. (use-package substitute :ensure t :defer 1 ;; Produce a message after the substitution that reports on what ;; happened. It is a single line, like "Substituted `TARGET' with ;; `SUBSTITUTE' N times across the buffer. :hook (substitute-post-replace . substitute-report-operation) :commands (substitute-target-below-point ; Forward motion like isearch (C-s) substitute-target-above-point ; Backward motion like isearch (C-r) substitute-target-in-defun ; inside of the current definition substitute-target-in-buffer) ; throughout the buffer :config ;; Set this to non-nil to highlight all occurrences of the current ;; target. (setopt substitute-highlight t) ;; Set this to t if you want to always treat the letter casing ;; literally. Otherwise each command accepts a `C-u' prefix ;; argument to do this on-demand. (setq substitute-fixed-letter-case nil) ;; C-c s is occupied by `prot-scratch-buffer'. (define-key global-map (kbd "C-c r") #'substitute-prefix-map))
5.2.22. The prot-emacs-essentials.el
section about goto-chg
(go to change)
The goto-chg
package, authored by David Andersson and maintained by
Vasilij Schneidermann, moves the cursor to the point where the last
change happened. Calling the command again cycles to the point before
that and so on. Simple and super effective.
(use-package goto-chg :ensure t :bind (("C-(" . goto-last-change) ("C-)" . goto-last-change-reverse)))
5.2.23. The prot-emacs-essentials.el
section about expreg
(tree-sitter mark syntactically)
The expreg
package by Yuan Fu (aka casouri) uses the tree-sitter
framework to incrementally expand the region from the smallest to the
largest syntactic unit in the given context. This is a powerful
feature, though it (i) requires Emacs to be built with tree-sitter
support and (ii) for the user to be running a major mode that is
designed for tree-sitter (Lisp seems to work regardless).
The package offers the expreg-expand
and expreg-contract
commands.
I believe I have never used the latter. I find it easier to just abort
and start again than to have a special key for the rare scenario where
I widened the selection more than I should. For that case, we can
always exchange point and mark. I have explained this in my video
about mark and register basics:
https://protesilaos.com/codelog/2023-06-28-emacs-mark-register-basics/.
If tree-sitter functionality is not available, then the C-M-SPC
binding is taken by prot-simple-mark-sexp
which is similar in spirit
(The prot-emacs-essentials.el
section about prot-simple.el
(custom basic commands)).
For me, expreg
is mostly an investment into the future, as I am
monitoring developments on the tree-sitter front. My setup provides a
user option to not load such tree-sitter extras (The init.el
user option to load treesitter extras).
Note that in the code block below I define two small commands. Custom code belong in libraries, though not in cases where it is ad-hoc like this (The custom libraries of my configuration).
;;; Mark syntactic constructs efficiently if tree-sitter is available (expreg) (when (and (treesit-available-p) prot-emacs-treesitter-extras) (use-package expreg :ensure t :functions (prot/expreg-expand prot/expreg-expand-dwim) ;; There is also an `expreg-contract' command, though I have no use for it. :bind ("C-M-SPC" . prot/expreg-expand-dwim) ; overrides `mark-sexp' :config (defun prot/expreg-expand (n) "Expand to N syntactic units, defaulting to 1 if none is provided interactively." (interactive "p") (dotimes (_ n) (expreg-expand))) (defun prot/expreg-expand-dwim () "Do-What-I-Mean `expreg-expand' to start with symbol or word. If over a real symbol, mark that directly, else start with a word. Fall back to regular `expreg-expand'." (interactive) (let ((symbol (bounds-of-thing-at-point 'symbol))) (cond ((equal (bounds-of-thing-at-point 'word) symbol) (prot/expreg-expand 1)) (symbol (prot/expreg-expand 2)) (t (expreg-expand)))))))
5.2.24. The prot-emacs-essentials.el
section about vundo
(visualise undo steps)
The vundo
package by Yuan Fu (aka “casouri”) builds on top of the
standard undo
infrastructure to provide a tree view of the undo
steps. It is an essential complement to what is otherwise a powerful
mechanism.
I personally like minimalist interfaces by default, meaning that I
prefer nothing to pop up unless it is necessary. To this end, my
command prot/vundo-if-repeat-undo
produces a visualisation of the
undo steps only after I repeat the undo
command. The assumption is
that if I am repeating, I am already interested in something further
back in history, at which point having a representation of it is
helpful. I am implementing this using the advice mechanism, so that
(i) the command calls the original function if needed, and (ii) I can
extend the functionality to many functions without needing to rebind
any keys.
To make this feel natural, I bind keys in the vundo-mode-map
that
are consistent with the defaults for undo
and undo-redo
. This way,
I can keep operating on the buffer without switching contexts. The
visualisation is a nice extra (The init.el
macro to bind keys (prot-emacs-keybind
)).
I did contribute to the vundo
package a while ago to define the
vundo-after-undo-functions
. This was an abnormal hook that we could
employ for advanced uses, such as to display a diff with the relevant
changes (per https://github.com/casouri/vundo/pull/74). It seems
that the source code of the package now has the diffing functionality
built-in, though the hook I introduced is no longer there. That is
unfortunate, because we could use it for other things as well, such as
to pulse the region.
;;; Visualise undo ring (`vundo') (use-package vundo :ensure t :if (display-graphic-p) :defer 1 :bind ( :map vundo-mode-map ("C-/" . vundo-backward) ("C-?" . vundo-forward) ("u" . vundo-backward) ("U" . vundo-forward) ("g" . vundo-goto-last-saved) ("." . vundo-goto-last-saved) ("h" . vundo-backward) ("j" . vundo-next) ("k" . vundo-previous) ("l" . vundo-forward)) :config (setq vundo-glyph-alist vundo-unicode-symbols) (defvar prot/vundo-undo-functions '(undo undo-only undo-redo) "List of undo functions to check if we need to visualise the undo ring.") (defvar prot/vundo-undo-command #'undo "Command to call if we are not going to visualise the undo ring.") (defun prot/vundo-if-repeat-undo (&rest args) "Use `vundo' if the last command is among `prot/vundo-undo-functions'. In other words, start visualising the undo ring if we are going to be cycling through the edits." (interactive) (if (and (member last-command prot/vundo-undo-functions) (not undo-in-region)) (call-interactively 'vundo) (apply args))) (mapc (lambda (fn) (advice-add fn :around #'prot/vundo-if-repeat-undo)) prot/vundo-undo-functions) (with-eval-after-load 'pulsar (add-hook 'vundo-post-exit-hook #'pulsar-pulse-line-green)))
5.2.25. The prot-emacs-essentials.el
section about tmr
(set timers)
This is a package that I wrote and for which I received substantial
code contributions from Damien Cassou and Daniel Mendler. With tmr
we set timers using a convenient notation. The point of entry is the
tmr
command (or tmr-with-description
if you want to describe what
the timer is about).
Set a timer by specifying one of these:
Input | Meaning |
---|---|
5 | 5 Minutes from now |
5m | Same as abovre |
1h | 1 hour from now |
06:35 | From now until 06:35 |
To view the running timers in a tabulated list, invoke the command
tmr-tabulated-view
. From there, type C-h m
(or M-x describe-mode
)
to learn about all the available commands and their respective key bindings.
- Package name (GNU ELPA):
tmr
- Official manual: https://protesilaos.com/emacs/tmr
- Change log: https://protesilaos.com/emacs/tmr-changelog
- Git repositories:
- GitHub: https://github.com/protesilaos/tmr
- GitLab: https://gitlab.com/protesilaos/tmr
- Backronym: TMR May Ring; Timer Must Run
;;; TMR May Ring (tmr is used to set timers) ;; Read the manual: <https://protesilaos.com/emacs/tmr>. (use-package tmr :ensure t :bind (("C-c t t" . tmr) ("C-c t T" . tmr-with-description) ("C-c t l" . tmr-tabulated-view) ; "list timers" mnemonic ("C-c t c" . tmr-clone) ("C-c t k" . tmr-cancel) ("C-c t s" . tmr-reschedule) ("C-c t e" . tmr-edit-description) ("C-c t r" . tmr-remove) ("C-c t R" . tmr-remove-finished)) :config (setq tmr-sound-file "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga" tmr-notification-urgency 'normal tmr-description-list 'tmr-description-history))
5.2.26. The prot-emacs-essentials.el
section about password-store
The pass
program (aka password-store
) is a Unix-style password
manager. Each password entry is its own encrypted file, stored in a
local directory (~/.password-store
, by default). Encryption is done
with GPG, though I believe Age can also be used. It can generate
strong passwords and allows us to quickly retrieve the password
associated with a given file.
The Emacs interface makes it easy to access passowrds with M-x pass
.
The resulting buffer shows a tree representation of the ~/.password-store
and provides an overview of the available key bindings. From here we
can write a new entry, generate a password for an existing one, and so
on.
My most common interaction with it is via the command password-store-copy
which uses minibuffer completion to match an entry and get the
password from it. On this note, I have a convention of naming password
files based on their scope, like username@website
. This makes it
easy to retrieve what I need.
;;; Pass interface (password-store) (use-package password-store :ensure t ;; Mnemonic is the root of the "code" word (κώδικας). But also to add ;; the password to the kill-ring. Other options are already taken. :bind ("C-c k" . password-store-copy) :config (setq password-store-time-before-clipboard-restore 30)) (use-package pass :ensure t :commands (pass))
5.2.27. The prot-emacs-essentials.el
section about shell
Before using Emacs, I did not have a clear idea of what the
distinction between a “shell” and a “terminal” is. But I quickly
learnt that a terminal (“terminal emulator”) is an application that
provides a text-centric interface and handles all the technicalities
of presenting text accordingly. Whereas the “shell” (or “command-line
shell”) is the program that runs inside the terminal whose job is to
interpret the user’s input and communicate with the computer.
Something like xterm
or gnome-terminal
is a terminal. While the
likes of bash
, zsh
, and fish
are shells.
In Emacs we can have both. Emacs can run a process that constitutes a
fully fledged terminal emulator, such as with the vterm
package, or
it can provide the interface necessary for a mere shell to handle the
command-line interactivity.
A terminal emulator is only needed if we use programs that require
Terminal User Interface (TUI) capabilities, such as htop
. I do not
run any of those, in large part because Emacs has better or equally
capable alternatives like M-x proced
to do what htop
does in the
terminal (The prot-emacs-essentials.el
settings for proced
).
With the TUI out of the way, we can have M-x shell
run a native Unix
shell for us. Mine is bash
because I am a simpleton, but also
because my prot-shell.el
provides a few niceties that improve the
user experience (The prot-shell.el
library).
I run a shell to do things like interface with my system’s package
manager or run a program with some flags. M-x shell
is more than
enough for this purpose. To make it a bit easier to work with multiple
shells that need to be named after the directory they are in, I use
the command prot-shell
: it not only uses a unique and informative
buffer name, but it also keeps track of cd
commands to update the
buffer name accordingly.
Note that there also exists a shell implemented in Emacs Lisp. It is
called eshell
. Unlike shell
, it does not read the ~/.bashrc
and
is its own little Emacs-only thing with its own command-line syntax.
In short, it is “okay” in a vacuum but I have no use for it beside
tinkering with Elisp, while I prefer to have a reliable ~/.bashrc
at
all times. As such, the eshell
command is on the list of disabled
commands (The init.el
settings to enable certain commands and disable others).
My prot-shell-mode
defines a few extra key bindings (per the
prot-shell-mode-map
) and also implements a bookmark handler for
shell buffers (The prot-emacs-essentials.el
settings for bookmarks).
The bookmarking functionality is a wonderful extra, as it leverages
Emacs’ TRAMP infrastructure to re-establish the connection to the
given host. For example, if I do M-x find-file
and then input
/sudo::/usr/share/
to go to /usr/share/
with sudo
privileges,
then I can open a shell there and bookmark it. When I jump back to the
bookmark, Emacs will automatically handle the sudo
part while taking
me to that shell in its directory. Relevant note from my source code:
;; NOTE 2023-08-18: I sent this to the Emacs maintainers as a patch ;; (bug#65039). I received approval to proceed with the change, but I ;; did not do it because a user reported an issue with SSH (TRAMP). I ;; do not have access to SSH and am not familiar with such workflows. ;; If/when that changes, I will try again. In the meantime, this is ;; good code and it works for me.
Now the actual configurations:
;;; Shell (M-x shell) (use-package shell :ensure nil :bind ( :map shell-mode-map ("C-c C-k" . comint-clear-buffer) ("C-c C-w" . comint-write-output)) :config ;; Check my .bashrc which handles `comint-terminfo-terminal': ;; ;; # Default pager. The check for the terminal is useful for Emacs with ;; # M-x shell (which is how I usually interact with bash these days). ;; # ;; # The COLORTERM is documented in (info "(emacs) General Variables"). ;; # I found the reference to `dumb-emacs-ansi' in (info "(emacs) ;; # Connection Variables"). ;; if [ "$TERM" = "dumb" ] && [ "$INSIDE_EMACS" ] || [ "$TERM" = "dumb-emacs-ansi" ] && [ "$INSIDE_EMACS" ] ;; then ;; export PAGER="cat" ;; alias less="cat" ;; export TERM=dumb-emacs-ansi ;; export COLORTERM=1 ;; else ;; # Quit once you try to scroll past the end of the file. ;; export PAGER="less --quit-at-eof" ;; fi (setq shell-command-prompt-show-cwd t) ; Emacs 27.1 (setq ansi-color-for-comint-mode t) (setq shell-input-autoexpand 'input) (setq shell-highlight-undef-enable t) ; Emacs 29.1 (setq shell-has-auto-cd nil) ; Emacs 29.1 (setq shell-get-old-input-include-continuation-lines t) ; Emacs 30.1 (setq shell-kill-buffer-on-exit t) ; Emacs 29.1 (setq shell-completion-fignore '("~" "#" "%")) (setq-default comint-scroll-to-bottom-on-input t) (setq-default comint-scroll-to-bottom-on-output nil) (setq-default comint-input-autoexpand 'input) (setq comint-prompt-read-only t) (setq comint-buffer-maximum-size 9999) (setq comint-completion-autolist t) (setq comint-input-ignoredups t) (setq tramp-default-remote-shell "/bin/bash") (setq shell-font-lock-keywords '(("[ \t]\\([+-][^ \t\n]+\\)" 1 font-lock-builtin-face) ("^[^ \t\n]+:.*" . font-lock-string-face) ("^\\[[1-9][0-9]*\\]" . font-lock-constant-face))) ;; Support for OS-specific escape sequences such as what `ls ;; --hyperlink' uses. I normally don't use those, but I am checking ;; this to see if there are any obvious advantages/disadvantages. (add-hook 'comint-output-filter-functions 'comint-osc-process-output)) (use-package prot-shell :ensure nil :bind (("<f1>" . prot-shell)) ; I don't use F1 for help commands :hook (shell-mode . prot-shell-mode))
5.2.28. The prot-emacs-essentials.el
section about the laptop-specific settings
Due to my limited electricity setup, I spend about half my computer time on the laptop (What hardware and software do you use?). It does not add to the load that my solar-powered batteries have to handle and I can thus keep the lights on until ~21:00.
When I am working on my laptop, I want to make a few tweaks to the
default dimensions of Emacs frames (The early-init.el
code to set frame parameters).
I also arrange for the battery indicator to be displayed on the mode
line.
;;; Laptop settings (unless (directory-empty-p "/sys/class/power_supply/") (add-to-list 'default-frame-alist '(width . (text-pixels . 800))) (add-to-list 'default-frame-alist '(height . (text-pixels . 600))) (use-package battery :ensure nil :hook (after-init . display-battery-mode) :config ;;;; Show battery status on the mode line (battery.el) (setq battery-mode-line-format (cond ((eq battery-status-function #'battery-linux-proc-acpi) "⏻%b%p%%,%d°C ") (battery-status-function "⏻%b%p%% ")))))
5.2.29. The prot-emacs-essentials.el
call to provide
Finally, we provide
the module. This is the mirror function of
require
(The init.el final part to load the individual modules).
(provide 'prot-emacs-essentials)
5.3. The prot-emacs-modeline.el
module
[ Watch: write a custom mode line (2023-07-29) and customise mode line colours (2024-01-13). ]
I use a custom mode line that is close in spirit to the default one.
The main difference is that I design most of the components to only
appear in the focused window. This way, I get a minimal view while
also avoid the repetition of global indicators, such as for the time
(The prot-emacs-essentials.el
configurations for the date and time (display-time-mode
))
or the notmuch-indicator
(The prot-emacs-notmuch.el
section about the notmuch-indicator
).
Most items that go into the mode-line-format
are defined in
the prot-modeline.el
library.
Notice the use of setq-default
. This is like setq
but sets the
default value of variable that normally are buffer-local. You will
only find a few cases where this is needed.
In this section I also take care to provide integration with my
spacious-padding
package (The prot-emacs-theme.el
section for spacious-padding
).
It adds, among others, a box effect to mode line constructs.
To make the faces of prot-modeline.el
look right in this scenario, I
add a box to them as well. They then adopt whatever padding is there.
The with-eval-after-load
pattern is how to evaluate some code
as soon as the given package/feature is loaded.
;;; Mode line (use-package prot-modeline :ensure nil :config (setq mode-line-compact nil) ; Emacs 28 (setq mode-line-right-align-edge 'right-margin) ; Emacs 30 (setq-default mode-line-format '("%e" prot-modeline-kbd-macro prot-modeline-narrow prot-modeline-buffer-status prot-modeline-window-dedicated-status prot-modeline-input-method " " prot-modeline-buffer-identification " " prot-modeline-major-mode prot-modeline-process " " prot-modeline-vc-branch " " prot-modeline-eglot " " prot-modeline-flymake " " mode-line-format-right-align ; Emacs 30 prot-modeline-notmuch-indicator " " prot-modeline-misc-info)) (with-eval-after-load 'spacious-padding (defun prot/modeline-spacious-indicators () "Set box attribute to `'prot-modeline-indicator-button' if spacious-padding is enabled." (if (bound-and-true-p spacious-padding-mode) (set-face-attribute 'prot-modeline-indicator-button nil :box t) (set-face-attribute 'prot-modeline-indicator-button nil :box 'unspecified))) ;; Run it at startup and then afterwards whenever ;; `spacious-padding-mode' is toggled on/off. (prot/modeline-spacious-indicators) (add-hook 'spacious-padding-mode-hook #'prot/modeline-spacious-indicators)))
5.3.1. The prot-emacs-modeline.el
section about keycast
This is a helpful package by Jonas Bernoulli that echoes the key presses and corresponding commands on the mode line, tab bar, header line, or a special buffer.
I usually enable keycast-mode-line-mode
when I do a presentation. It
shows an indicator on the focused mode line.
Note that the value of keycast-mode-line-insert-after
only works
with my customised mode line (The prot-emacs-modeline.el
module).
;;; Keycast mode (use-package keycast :ensure t :after prot-modeline :commands (keycast-mode-line-mode keycast-header-line-mode keycast-tab-bar-mode keycast-log-mode) :init (setq keycast-mode-line-format "%2s%k%c%R") (setq keycast-mode-line-insert-after 'prot-modeline-vc-branch) (setq keycast-mode-line-window-predicate 'mode-line-window-selected-p) (setq keycast-mode-line-remove-tail-elements nil) :config (dolist (input '(self-insert-command org-self-insert-command)) (add-to-list 'keycast-substitute-alist `(,input "." "Typing…"))) (dolist (event '( mouse-event-p mouse-movement-p mwheel-scroll handle-select-window mouse-set-point mouse-drag-region)) (add-to-list 'keycast-substitute-alist `(,event nil))))
5.3.2. The prot-emacs-essentials.el
call to provide
Finally, we provide
the module. This is the mirror function of
require
(The init.el final part to load the individual modules).
(provide 'prot-emacs-modeline)
5.4. The prot-emacs-completion.el
module
The term “completion” describes a process where user input is assisted
by pattern matching algorithms to type out incomplete terms. The most
basic way of this model of interaction is what we get in a
command-line prompt, where we can hit TAB
to expand the word before
point to something the program already knows about (e.g ema
followed
by TAB
may complete to emacs
).
In Emacs, completion encompasses user interfaces that show the available candidates (the likely options) right away, as well as provide more advanced capabilities for storing the history of previous inputs, displaying helpful annotations next to each candidate, and “completion styles” to control how user input is matched to candidates. Because we use the minibuffer for most common interactions, completion is an integral part of any setup.
5.4.1. The prot-emacs-completion.el
settings for completion styles
The completion-styles
are pattern matching algorithms. They
interpret user input and match candidates accordingly.
- emacs22
- Prefix completion that only operates on the text before
point. If we are in
prefix|suffix
, with|
representing the cursor, it will consider everything that expandsprefix
and then add back to it thesuffix
. - basic
- Prefix completion that also accounts for the text after
point. Using the above example, this one will consider patterns that
match all of
emacs22
as well as anything that completessuffix
. - partial-completion
- This is used for file navigation. Instead of
typing out a full path like
~/.local/share/fonts
, we do~/.l/s/f
or variants thereof to make the matches unique such as~/.l/sh/fon
. It is a joy to navigate the file system in this way. - substring
- Matches the given sequence of characters literally
regardless of where it is in a word. So
pro
will matchprofessional
as well asreproduce
. - flex
- Completion of an in-order subset of characters. It does not
matter where the charactes are in the word, so long as they are
encountered in the given order. The input
lad
will thus matchlist-faces-display
as well aspulsar-highlight-dwim
. - initials
- Completion of acronyms and initialisms. Typing
lfd
will thus matchlist-faces-display
. This completion style can also be used for file system navigation, though I prefer to only havepartial-completion
handle that task. - orderless
- This is the only completion style I use which is not
built into Emacs and which I tweak further in a separate section
(The
prot-emacs-completion.el
for theorderless
completion style). It matches patterns out-of-order. Patterns are typically words separated by spaces, though they can also be regular expressions, and even styles that are the same as the aforementionedflex
andinitials
.
Now that you know about the completion styles I use, take a look at
the value of my completion-styles
. You will notice that orderless
,
which is the most powerful/flexible is placed last. I do this because
Emacs tries the styles in the given order from left to right, moving
the next one until it finds a match. As such, I usually want to start
with tight matches (e.g. li-fa-di
for list-faces-display
) and only
widen the scope of the search as I need to. This is easy to do because
none of the built-in completion styles parses the empty space, so as
soon as I type a space after some characters I am using orderless
.
Notice that this is not all, as we still have to consider what happens
when the minibuffer prompt we are using defines a specific completion
category whose pattern matching styles differ from what we have in the
completion-styles
. To that end, we also set up a more fine-grained
set of completion styles on a per-category basis using overrides.
The completion-styles
is the fallback option in case there is no
provision for the given completion category. The completion category
is a piece of metadata that is associated with the completion table we
are matching against while using the minibuffer. For example, the
find-file
command has the file
category, while the
switch-to-buffer
command uses the buffer
category. The defaults
for those are specified in the variable
completion-category-defaults
, while overrides for them can be set in
the completion-category-overrides
.
While we can override only the categories we care about, the presence
of those completion-category-defaults
will surprise us in some cases
because we will not be using what we specified in the completion-styles
.
As such, I set completion-category-defaults
to nil, to always fall
back to my preferred completion-styles
and then I further configure
overrides where those make sense to me.
We can opt for per-category styles by configuring the user option
completion-category-overrides
. Notice, for example, how I arrange
for partial-completion
to be set only for the file
completion
category, as I only ever need it there. Also bear in mind what I
described above about why orderless
is placed last on the list:
Emacs uses the completion styles from left to right until something
matches the given input. So I do not need to have partial-completion
first as basic
will never match something like ~/.l/s/fo
for
~/.local/share/fonts
.
;;; General minibuffer settings (use-package minibuffer :ensure nil :config ;;;; Completion styles (setq completion-styles '(basic substring initials flex orderless)) ; also see `completion-category-overrides' (setq completion-pcm-leading-wildcard t) ; Emacs 31: make `partial-completion' behave like `substring' ;; Reset all the per-category defaults so that (i) we use the ;; standard `completion-styles' and (ii) can specify our own styles ;; in the `completion-category-overrides' without having to ;; explicitly override everything. (setq completion-category-defaults nil) ;; A non-exhaustve list of known completion categories: ;; ;; - `bookmark' ;; - `buffer' ;; - `charset' ;; - `coding-system' ;; - `color' ;; - `command' (e.g. `M-x') ;; - `customize-group' ;; - `environment-variable' ;; - `expression' ;; - `face' ;; - `file' ;; - `function' (the `describe-function' command bound to `C-h f') ;; - `info-menu' ;; - `imenu' ;; - `input-method' ;; - `kill-ring' ;; - `library' ;; - `minor-mode' ;; - `multi-category' ;; - `package' ;; - `project-file' ;; - `symbol' (the `describe-symbol' command bound to `C-h o') ;; - `theme' ;; - `unicode-name' (the `insert-char' command bound to `C-x 8 RET') ;; - `variable' (the `describe-variable' command bound to `C-h v') ;; - `consult-grep' ;; - `consult-isearch' ;; - `consult-kmacro' ;; - `consult-location' ;; - `embark-keybinding' ;; (setq completion-category-overrides ;; NOTE 2021-10-25: I am adding `basic' because it works better as a ;; default for some contexts. Read: ;; <https://debbugs.gnu.org/cgi/bugreport.cgi?bug=50387>. ;; ;; `partial-completion' is a killer app for files, because it ;; can expand ~/.l/s/fo to ~/.local/share/fonts. ;; ;; If `basic' cannot match my current input, Emacs tries the ;; next completion style in the given order. In other words, ;; `orderless' kicks in as soon as I input a space or one of its ;; style dispatcher characters. '((file (styles . (basic partial-completion orderless))) (bookmark (styles . (basic substring))) (library (styles . (basic substring))) (embark-keybinding (styles . (basic substring))) (imenu (styles . (basic substring orderless))) (consult-location (styles . (basic substring orderless))) (kill-ring (styles . (emacs22 orderless))) (eglot (styles . (emacs22 substring orderless))))))
5.4.2. The prot-emacs-completion.el
for the orderless
completion style
The orderless
package by Omar Antolín Camarena provides one of the
completion styles that I use (The prot-emacs-completion.el
settings for completion styles).
It is a powerful pattern matching algorithm that parses user input and
interprets it out-of-order, so that in pa
will cover insert-pair
as well as package-install
. Components of the search are
space-separated, by default, though we can modify the user option
orderless-component-separator
to have something else (but I cannot
think of a better value). In the section about completion styles, I
explain how I use orderless
and why its power does not result in
lots of false positives.
With orderless
we can also define so-called “style dispatchers”.
These are characters attached to the input which instruct orderless
to use a specific pattern for that component. My prot-orderless.el
defines such style dispatchers as postfix operators: they are added to
the end of the input (The prot-orderless.el
library). The equals
sign takes the input literally, the dot interprets it as a file type
extension, while the tilde means to match the input either at the
beginning or the end. Granted, these are overkill most of the time. It
is easier to just continue typing to narrow the list of candidates.
Still, they do have their uses.
;;; Orderless completion style (and prot-orderless.el) (use-package orderless :ensure t :demand t :after minibuffer :config ;; Remember to check my `completion-styles' and the ;; `completion-category-overrides'. (setq orderless-matching-styles '(orderless-prefixes orderless-regexp)) ;; SPC should never complete: use it for `orderless' groups. ;; The `?' is a regexp construct. :bind ( :map minibuffer-local-completion-map ("SPC" . nil) ("?" . nil))) (use-package prot-orderless :ensure nil :config (setq orderless-style-dispatchers '(prot-orderless-literal prot-orderless-file-ext prot-orderless-beg-or-end)))
5.4.3. The prot-emacs-completion.el
settings to ignore letter casing
I never really need to match letters case-sensitively in the minibuffer. Let’s have everything ignore casing by default.
[ In some Elisp that I write there is a let
binding for
case-fold-search
to make the search case-sensitive. But those are
the exceptions. ]
(setq completion-ignore-case t) (setq read-buffer-completion-ignore-case t) (setq-default case-fold-search t) ; For general regexp (setq read-file-name-completion-ignore-case t)
5.4.4. The prot-emacs-completion.el
settings for recursive minibuffers
“Recursive minibuffers” are of those advanced features that you don’t need frequently, but when you do, it is an excellent addition to your workflow. The concept describes the use of a minibuffer while another minibuffer is already open.
The need to have multiple (i.e. “recursive”) minibuffers arises when
you initiate a command, such as M-x
followed by some incomplete
command where you remember that you forgot to perform another command
before confirming the first one. I mostly use this as a combination of
M-x
(execute-extended-command
) and M-:
(eval-expression
).
The read-minibuffer-restore-windows
restores the window layout that
was in place when the minibuffer recursion started. I personally do
not want that: just leave me where I am.
The minibuffer-depth-indicate-mode
shows a number next to the
minibuffer prompt, indicating the level of depth in the recursion,
starting with 2
.
(use-package mb-depth :ensure nil :hook (after-init . minibuffer-depth-indicate-mode) :config (setq read-minibuffer-restore-windows nil) ; Emacs 28 (setq enable-recursive-minibuffers t))
5.4.5. The prot-emacs-completion.el
settings for default values
Minibuffer prompts often have a default value. This is used when the
user types RET
without inputting anything. The out-of-the-box
behaviour of Emacs is to append informative text to the prompt like
(default some-default-value)
. With the tweak to minibuffer-default-prompt-format
we get a more compact style of [some-default-value]
, which looks
better to me.
The minibuffer-electric-default-mode
displays the default value next
to the prompt only if RET
will actually use the default in that
situation. This means that while you start typing in the minibuffer,
the [some-default-value]
indicator disappears, since it is no longer
applicable. Without this mode, the indicator stays there at all times,
which can be confusing or distracting.
(use-package minibuf-eldef :ensure nil :hook (after-init . minibuffer-electric-default-mode) :config (setq minibuffer-default-prompt-format " [%s]")) ; Emacs 29
5.4.6. The prot-emacs-completion.el
settings for common interactions
Here I combine several small tweaks to improve the overall minibuffer experience.
- The need to
resize-mini-windows
arises on some occasions where Emacs has to show text spanning multiple lines in the “mini windows”. A common scenario for me is in Org mode buffers where I set theTODO
keyword of a task withC-c C-t
(M-x org-todo
) and have this as my setting:(setq org-use-fast-todo-selection 'expert)
Otherwise, this is not an issue anyway and I may also like other options fororg-use-fast-todo-selection
. - The
read-answer-short
is complementary touse-short-answers
. This is about providing the shorter version to some confirmation prompt, such asy
instead ofyes
. - The
echo-keystrokes
is set to a low value to show in the echo area the incomplete key sequence I have just typed. This is especially helpful for demonstration purposes but also to double check that I did not mistype something (I cannot touch-type, so this happens a lot). The
minibuffer-prompt-properties
and advice tocompleting-read-multiple
make it so that (i) the minibuffer prompt is not accessible with regular motions to avoid mistakes and (ii) prompts that complete multiple targets show an indicator about this fact. With regard to the latter in particular, we have prompts like that of Org to set tags for a heading (withC-c C-q
elseM-x org-set-tags-command
) where more than one candidate can be provided using completion, provided each candidate is separated by thecrm-separator
(a comma by default, though Org uses:
in that scenario).Remember that when using completion in the minibuffer, you can hit
TAB
to expand the selected choice without exiting with it. For cases when multiple candidates can be selected, you select the candidate,TAB
, then input thecrm-separator
, and repeat until you are done selecting at which point you typeRET
.- Finally the
file-name-shadow-mode
is a neat little feature to remove the “shadowed” part of a file prompt while using something likeC-x C-f
(M-x find-file
). File name shadowing happens when we invokefind-file
and instead of first deleting the contents of the minibuffer, we start typing out the file system path we wish to visit. For example, I am in~/Git/Projects/
and type directly after it something like~/.local/share/fonts/
, so Emacs displays~/Git/Projects/~/.local/share/fonts/
with the original part greyed out. Withfile-name-shadow-mode
the “shadowed” part is removed altogether. This is especially nice when combined with the completion style calledpartial-completion
(Theprot-emacs-completion.el
settings for completion styles).
(use-package rfn-eshadow :ensure nil :hook (minibuffer-setup . cursor-intangible-mode) :config ;; Not everything here comes from rfn-eshadow.el, but this is fine. (setq resize-mini-windows t) (setq read-answer-short t) ; also check `use-short-answers' for Emacs28 (setq echo-keystrokes 0.25) (setq kill-ring-max 60) ; Keep it small ;; Do not allow the cursor to move inside the minibuffer prompt. I ;; got this from the documentation of Daniel Mendler's Vertico ;; package: <https://github.com/minad/vertico>. (setq minibuffer-prompt-properties '(read-only t cursor-intangible t face minibuffer-prompt)) ;; MCT has a variant of this built-in. (unless (eq prot-emacs-completion-ui 'mct) ;; Add prompt indicator to `completing-read-multiple'. We display ;; [`completing-read-multiple': <separator>], e.g., ;; [`completing-read-multiple': ,] if the separator is a comma. This ;; is adapted from the README of the `vertico' package by Daniel ;; Mendler. I made some small tweaks to propertize the segments of ;; the prompt. (defun crm-indicator (args) (cons (format "[`completing-read-multiple': %s] %s" (propertize (replace-regexp-in-string "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" "" crm-separator) 'face 'error) (car args)) (cdr args))) (advice-add #'completing-read-multiple :filter-args #'crm-indicator)) (file-name-shadow-mode 1))
5.4.7. The prot-emacs-completion.el
generic minibuffer UI settings
These are some settings for the default completion user interface.
These do not come into effect unless prot-emacs-completion-ui
is set
to a nil
value or when we are not using any package for in-buffer
completion (such as the corfu
package).
- The
init.el
option to load a minibuffer user interface - The
prot-emacs-completion.el
submodule forvertico
(prot-emacs-vertico.el
) - The
prot-emacs-completion.el
submodule formct
(prot-emacs-mct.el
)
(use-package minibuffer :ensure nil :demand t :config (setq completions-format 'one-column) (setq completion-show-help nil) (setq completion-auto-help 'always) (setq completion-auto-select nil) (setq completions-detailed t) (setq completion-show-inline-help nil) (setq completions-max-height 6) (setq completions-header-format (propertize "%s candidates:\n" 'face 'bold-italic)) (setq completions-highlight-face 'completions-highlight) (setq minibuffer-completion-auto-choose t) (setq minibuffer-visible-completions t) ; Emacs 30 (setq completions-sort 'historical) (unless prot-emacs-completion-ui (prot-emacs-keybind minibuffer-local-completion-map "<up>" #'minibuffer-previous-line-completion "<down>" #'minibuffer-next-line-completion) (add-hook 'completion-list-mode-hook #'prot-common-truncate-lines-silently)))
5.4.8. The prot-emacs-completion.el
settings for saving the history (savehist-mode
)
Minibuffer prompts can have their own history. When they do not, they
share a common history of user inputs. Emacs keeps track of that
history in the current session, but loses it as soon as we close it.
With savehist-mode
enabled, all minibuffer histories are written to
a file and are restored when we start Emacs again.
Histories are useful in two ways:
- Recent choices appear at the top, so we can find them more easily.
- The
M-p
(previous-history-element
) andM-n
(next-history-element
) commands in the minibuffer will be useful right away upon restoring Emacs (all my packages make good use of minibuffer histories per prompt, soM-p
andM-n
only show relevant values).
Since we are already recording minibuffer histories, we can instruct
savehist-mode
to also keep track of additional variables and restore
them next time we use Emacs. Hence savehist-additional-variables
. I
do this in a few of places:
- The
prot-emacs-completion.el
for in-buffer completion popup and preview (corfu
) - The
prot-emacs-essentials.el
settings for registers - The
prot-shell.el
library
Note that the user option history-length
applies to each individual
history variable: it is not about all histories combined.
Overall, I am happy with this feature and benefit from it on a daily basis.
;;;; `savehist' (minibuffer and related histories) (use-package savehist :ensure nil :hook (after-init . savehist-mode) :config (setq savehist-file (locate-user-emacs-file "savehist")) (setq history-length 100) (setq history-delete-duplicates t) (setq savehist-save-minibuffer-history t) (add-to-list 'savehist-additional-variables 'kill-ring))
5.4.9. The prot-emacs-completion.el
settings for dynamic text expansion (dabbrev
)
The built-in dabbrev
package provides a text completion method that
reads the contents of a buffer and expands the text before the cursor
to match possible candidates. This is done with M-/
(dabbrev-expand
)
which is what I use most of the time to perform in-buffer completions.
I like dabbrev
because it is minimal. It does not produce any popup
or affect the window layout and so it is keeping me focused on what I
am doing. I wish it had a behaviour where we could initiate it and at
any point demand a fully fledged minibuffer presentation of what it is
trying to match, instead of cycling through the candidates with
repeated M-/
. Granted, I normally do not cycle in that way, as I
typically type out enough to get an exact match or be one M-/
away
from it.
Apart from the dabbrev-expand
command, we have dabbrev-completion
.
I do not use it because it does not feel natural while typing to stop,
check the minibuffer for some text, select it, and go back to typing.
Perhaps this is because I have a style of writing without
interruptions and without going back to immediately edit what I wrote
(unless I am doing a demonstration, where the viewer needs to follow
along).
The term “dabbrev” stands for “dynamic abbreviation”. Emacs also has
static, user-defined abbreviations (The prot-emacs-completion.el
settings for static text expansion (abbrev
)).
(use-package dabbrev :ensure nil :commands (dabbrev-expand dabbrev-completion) :config ;;;; `dabbrev' (dynamic word completion (dynamic abbreviations)) (setq dabbrev-abbrev-char-regexp "\\sw\\|\\s_") (setq dabbrev-abbrev-skip-leading-regexp "[$*/=~']") (setq dabbrev-backward-only nil) (setq dabbrev-case-distinction 'case-replace) (setq dabbrev-case-fold-search nil) (setq dabbrev-case-replace 'case-replace) (setq dabbrev-check-other-buffers t) (setq dabbrev-eliminate-newlines t) (setq dabbrev-upcase-means-case-search t) (setq dabbrev-ignored-buffer-modes '(archive-mode image-mode docview-mode pdf-view-mode)))
5.4.10. The prot-emacs-completion.el
settings for static text expansion (abbrev
)
[ Watch: abbreviations with abbrev-mode (quick text expansion) (2024-02-03). ]
Unlike “dynamic abbreviations” that depend on the text already
available in a buffer, we can define abbreviations that always expand
to what we have specified (The prot-emacs-completion.el
settings for dynamic text expansion (dabbrev
)).
Abbreviations, else the abbrev
mechanism, are strings of characters
that when typed out are replaced by another string. For example, if I
want to type in my website’s URL, I insert meweb
and continue
typing. Emacs will expand that word into https://protesilaos.com
.
Unless you are documenting what your abbreviation does or have abbrevs
that are easy to mistype, you will never need to tell Emacs not to
expand the given input. Note that the command unexpand-abbrev
is
there if you need it. I had to use it in the above paragraph to first
type out the abbrev I used as an example.
Similarly, we can expand an abbrev anywhere with the command
expand-abbrev
, though we normally do not have to do this because it
happens automatically as we type.
Emacs is smart about how we define and use abbrevs. Each major mode
has its own abbrev table, to which we add our definitions. When we are
in a buffer that has that major mode, we gain access to the relevant
abbreviations. A global-abbrev-table
is also available. In case of a
conflict between a major-mode-specific table and the global one, the
former takes precedence. Furthermore, abbrev tables conform with the
same inheritence principle as major modes at-large, meaning that
something like Org mode will inherit the text-mode-abbrev-table
because org-mode
is derived from text-mode
. Abbrev tables are thus
consistent with how hooks and keymaps work in terms of precedence and
inheritence.
As you will notice below, I use the prot-emacs-abbrev
macro to make
it easier to define these (The init.el
macro to define abbreviations (prot-emacs-abbrev
)).
Most of my abbrevs are simple letter casing tweaks. Though keep in
mind that the underlying mechanism is powerful and can evaluate
arbitrary Elisp code (I played around with it, though I don’t use
templates to have a real need for it—but it works).
;;;; `abbrev' (Abbreviations, else Abbrevs) (use-package abbrev :ensure nil ;; message-mode derives from text-mode, so we don't need a separate ;; hook for it. :hook ((text-mode prog-mode git-commit-mode) . abbrev-mode) :config (setq only-global-abbrevs nil) (prot-emacs-abbrev global-abbrev-table "meweb" "https://protesilaos.com" "megit" "https://github.com/protesilaos" "mehub" "https://github.com/protesilaos" "meclone" "git@github.com/protesilaos/" "melab" "https://gitlab.com/protesilaos" "medrive" "hyper://5cr7mxac8o8aymun698736tayrh1h4kbqf359cfk57swjke716gy/" ";web" "https://protesilaos.com" ";git" "https://github.com/protesilaos" ";hub" "https://github.com/protesilaos" ";clone" "git@github.com/protesilaos/" ";lab" "https://gitlab.com/protesilaos" ";drive" "hyper://5cr7mxac8o8aymun698736tayrh1h4kbqf359cfk57swjke716gy/") (prot-emacs-abbrev text-mode-abbrev-table "asciidoc" "AsciiDoc" "auctex" "AUCTeX" "cafe" "café" "cliche" "cliché" "clojurescript" "ClojureScript" "emacsconf" "EmacsConf" "github" "GitHub" "gitlab" "GitLab" "javascript" "JavaScript" "latex" "LaTeX" "libreplanet" "LibrePlanet" "linkedin" "LinkedIn" "paypal" "PayPal" "sourcehut" "SourceHut" "texmacs" "TeXmacs" "typescript" "TypeScript" "visavis" "vis-à-vis" "deja" "déjà" "youtube" "YouTube" ";up" "🙃" ";uni" "🦄" ";laugh" "🤣" ";smile" "😀" ";sun" "☀️") ;; Allow abbrevs with a prefix colon, semicolon, or underscore. I demonstrated ;; this here: <https://protesilaos.com/codelog/2024-02-03-emacs-abbrev-mode/>. (abbrev-table-put global-abbrev-table :regexp "\\(?:^\\|[\t\s]+\\)\\(?1:[:;_].*\\|.*\\)") (with-eval-after-load 'text-mode (abbrev-table-put text-mode-abbrev-table :regexp "\\(?:^\\|[\t\s]+\\)\\(?1:[:;_].*\\|.*\\)")) (with-eval-after-load 'org (prot-emacs-abbrev org-mode-abbrev-table ";dev" "}") (abbrev-table-put org-mode-abbrev-table :regexp "\\(?:^\\|[\t\s]+\\)\\(?1:[:;_].*\\|.*\\)")) (with-eval-after-load 'message (prot-emacs-abbrev message-mode-abbrev-table "bestregards" "Best regards,\nProtesilaos (or simply \"Prot\")" "allthebest" "All the best,\nProtesilaos (or simply \"Prot\")" "niceday" "Have a nice day,\nProtesilaos (or simply \"Prot\")" "abest" "All the best,\nProt" "bregards" "Best regards,\nProt" "nday" "Have a nice day,\nProt" "nosrht" "P.S. I am phasing out SourceHut: <https://protesilaos.com/codelog/2024-01-27-sourcehut-no-more/>. Development continues on GitHub with GitLab as a mirror.")) ;; The `prot-emacs-abbrev' macro, which simplifies how we use ;; `define-abbrev', does not only expand a static text. It can take ;; a pair of string and function to trigger the latter when the ;; former is inserted. Think of it like the basis of a simplistic ;; templating system. (require 'prot-abbrev) (prot-emacs-abbrev global-abbrev-table "metime" #'prot-abbrev-current-time "medate" #'prot-abbrev-current-date "mejitsi" #'prot-abbrev-jitsi-link ";time" #'prot-abbrev-current-time ";date" #'prot-abbrev-current-date ";jitsi" #'prot-abbrev-jitsi-link) (prot-emacs-abbrev text-mode-abbrev-table ";update" #'prot-abbrev-update-html) ;; Because the *scratch* buffer is produced before we load this, we ;; have to explicitly activate the mode there. (when-let* ((scratch (get-buffer "*scratch*"))) (with-current-buffer scratch (abbrev-mode 1))) ;; By default, abbrev asks for confirmation on whether to use ;; `abbrev-file-name' to save abbrevations. I do not need that, nor ;; do I want it. (remove-hook 'save-some-buffers-functions #'abbrev--possibly-save))
5.4.11. The prot-emacs-completion.el
for in-buffer completion popup (corfu
)
I generally do not rely on in-buffer text completion. I feel it slows
me down and distracts me. When I do, however, need to rely on it, I
have the corfu
package by Daniel Mendler: it handles the task
splendidly as it works with Emacs’ underlying infrastructure for
completion-at-point-functions
.
Completion is triggered with the TAB
key, which produces a popup
where the cursor is. The companion corfu-popupinfo-mode
will show a
secondary documentation popup if we move over a candidate but do not
do anything with it.
Also see the prot-emacs-completion.el
settings for dynamic text expansion (dabbrev
).
;;; Corfu (in-buffer completion popup) (use-package corfu :ensure t :if (display-graphic-p) :hook (after-init . global-corfu-mode) ;; I also have (setq tab-always-indent 'complete) for TAB to complete ;; when it does not need to perform an indentation change. :bind (:map corfu-map ("<tab>" . corfu-complete)) :config (setq corfu-preview-current nil) (setq corfu-min-width 20) (setq corfu-popupinfo-delay '(1.25 . 0.5)) (corfu-popupinfo-mode 1) ; shows documentation after `corfu-popupinfo-delay' ;; Sort by input history (no need to modify `corfu-sort-function'). (with-eval-after-load 'savehist (corfu-history-mode 1) (add-to-list 'savehist-additional-variables 'corfu-history)))
5.4.12. The prot-emacs-completion.el
settings for consult
[ This feature is subject to The init.el
user option to load extras for minibuffer completion. ]
consult
is another wonderful package by Daniel Mendler. It provides
a number of commands that turbocharge the minibuffer with advanced
capabilities for filtering, asynchronous input, and previewing of the
current candidate’s context.
- A case where filtering is in use is the
consult-buffer
command, which many users have as a drop-in replacement to the genericC-x b
(M-x switch-to-buffer
). It is a one-stop-shop for buffers, recently visited files (ifrecentf-mode
is used—I don’t), bookmarks (Theprot-emacs-essentials.el
settings for bookmarks), and, in principle, anything else that defines a source for this interface. To filter those source, we can type at the empty minibufferb SPC
, which will insert a filter specific to buffers. Delete back to remove the[Buffer]
filter and insert another filter. Available filters are displayed by typing?
at the prompt (I define it this way to call the commandconsult-narrow-help
). Every multi-source command fromconsult
relies on this paradigm. - Asynchronous input pertains to the intersection between Emacs and
external search programs. A case in point is
consult-grep
, which calls the system’sgrep
program. The prompt distinguishes between what is sent to the external program and what is only shown to Emacs by wrapping the former inside of#
. So the input#prot-#completion
will sendprot-
to thegrep
program and then usecompletion
inside of the minibuffer to perform the subsequent pattern-matching (e.g. with help fromorderless
(Theprot-emacs-completion.el
for theorderless
completion style). The part that is sent to the external program does not block Emacs. It is handled asynchronously, so everything stays responsive. - As for previewing,
consult
commands show the context of the current match and update the window as we move between completion candidates in the minibuffer. For example, theconsult-line
command performs an in-buffer search and lets us move between matches in the minibuffer while seeing in the window above what the surrounding text looks like. This is an excellent feature when we are trying to find something and do not quite remember all the search terms to narrow down to it simply by typing at the minibuffer prompt.
Also check: The prot-emacs-search.el
module.
;;; Enhanced minibuffer commands (consult.el) (when prot-emacs-completion-extras (use-package consult :ensure t :hook (completion-list-mode . consult-preview-at-point-mode) :bind ( :map global-map ("M-g M-g" . consult-goto-line) ("M-K" . consult-keep-lines) ; M-S-k is similar to M-S-5 (M-%) ("M-F" . consult-focus-lines) ; same principle ("M-s M-b" . consult-buffer) ("M-s M-f" . consult-find) ("M-s M-g" . consult-grep) ("M-s M-h" . consult-history) ("M-s M-i" . consult-imenu) ("M-s M-l" . consult-line) ("M-s M-m" . consult-mark) ("M-s M-y" . consult-yank-pop) ("M-s M-s" . consult-outline) :map consult-narrow-map ("?" . consult-narrow-help)) :config (setq consult-line-numbers-widen t) ;; (setq completion-in-region-function #'consult-completion-in-region) (setq consult-async-min-input 3) (setq consult-async-input-debounce 0.5) (setq consult-async-input-throttle 0.8) (setq consult-narrow-key nil) (setq consult-find-args (concat "find . -not ( " "-path */.git* -prune " "-or -path */.cache* -prune )")) (setq consult-preview-key 'any) (setq consult-project-function nil) ; always work from the current directory (use `cd' to switch directory) (add-to-list 'consult-mode-histories '(vc-git-log-edit-mode . log-edit-comment-ring)) (require 'consult-imenu) ; the `imenu' extension is in its own file (with-eval-after-load 'pulsar ;; see my `pulsar' package: <https://protesilaos.com/emacs/pulsar> (setq consult-after-jump-hook nil) ; reset it to avoid conflicts with my function (dolist (fn '(pulsar-recenter-top pulsar-reveal-entry)) (add-hook 'consult-after-jump-hook fn)))))
5.4.13. The prot-emacs-completion.el
section about embark
[ This feature is subject to The init.el
user option to load extras for minibuffer completion. ]
The embark
package by Omar Antolín Camarena provides a mechanism to
perform relevant actions in the given context. What constitutes “the
given context” depends on where the cursor is, such as if it is at the
end of a symbolic expression in Lisp code or inside the minibuffer.
The single point of entry is the embark-act
command or variants like
embark-dwim
.
With embark-act
we gain access to a customisable list of commands
for the given context. If we are over a Lisp symbol, one possible
action is to describe it (i.e. produce documentation about it). If we
are browsing files in the minibuffer, possible actions include file
operations such as to delete or rename the file. And so on for
everything.
The embark-dwim
command always performs the default action for the
given context. It is like invoking embark-act
and then typing the
RET
key.
A killer feature of embark
is the concepts of “collect” and
“export”. These are used in the minibuffer to produce a dedicated
buffer that contains all the completion candidates. For example, if we
are reading documentation about embark-
and have 10 items there, we
can “collect” the results in their own buffer and then navigate it as
if it were the minibuffer: RET
will perform the action that the
actual minibuffer would have carried out (to show documentation, in
this case). Similarly, the export mechanism takes the completion
candidates out of the minibuffer, though it also puts them in a major
mode that is appropriate for them. Files, for instance, will be placed
in a Dired buffer (The prot-emacs-dired.el
module).
Depending on the configurations about the “indicator”, the embark-act
command will display an informative buffer with keys and their
corresponding commands. We can control its placement the same way we
do with all well-behaved buffers (The prot-emacs-window.el
rules for displaying buffers (display-buffer-alist
)).
One downside of embark
is that it is hard to know what the context
is. I have had this experience myself several times, where I though I
was targeting the URL at point while the actions were about Org source
blocks, headings, and whatnot. Embark is probably correct in such a
case, though I cannot make my brain think the way it expects.
Another downside, which is also true for which-key
(The prot-emacs-which-key.el
module),
is the sheer number of options for each context. I feel that the
defaults should be more conservative, to have 3-4 actions per context
to make it easier to find stuff. Those who need more, can add them.
Documentation can also be provided to that end. Adding commands to
such a list is not a trivial task, because the user must modify
keymaps and thus understand the relevant concepts. Sure, we can all
learn, but this is not your usual setq
tweak.
All things considered, I do not recommend embark
to new users as I
know for a fact that people have trouble using it effectively. Power
users can benefit from it, though you will notice in the following
code block and in prot-embark.el
how even power users need to put in
some work (The prot-embark.el
library). Whether it is worth it or
not depends on one’s use-case.
Karthik Chikmagalur has an excellently written and presented essay on
Fifteen ways to use Embark. If you plan on becoming an embark
power
user, this will help you. Quote from Karthik:
Despite what these examples suggest, I estimate that I use less than a third of what Embark provides. Even so, in allowing me to change or chain actions at any time, it lets me pilot Emacs by the seat of my pants. A second, unforeseen benefit is that it makes commands and listings that I would never use available in a frictionless way: commands like
transpose-regions
andapply-macro-to-region-lines
, or customdired
,ibuffer
andpackage-menu
listings that are interactively inaccessible otherwise. The ability to quickly whip up such buffers makes knowing how to use dired or ibuffer pay off several fold. In composing such features seamlessly with minibuffer interaction or with text-regions, Embark acts as a lever to amplify the power of Emacs’ myriad built in commands and libraries.
I have been using Emacs since 2019 and not once did I have a use for
transpose-regions
or apply-macro-to-region-lines
. To run a
keyboard macro in a region, narrow to it, and call the kmacro with a
0
argument (run until you encounter an error or the end of the
buffer). Why remember such a specialised function when the general
pattern is getting the job done? And why be presented with the option
of that same function every single time you embark-act
on a region?
Personally, I would be content with a package that does the equivalent of “collect” and “export”. The rest is about organising keybindings and how you approach a given task (workflow as opposed to core functionality).
;;; Extended minibuffer actions and more (embark.el and prot-embark.el) (when prot-emacs-completion-extras (use-package embark :ensure t :defer 1 :config (setq embark-confirm-act-all nil) ;; The prot-embark.el has an advice to further simplify the ;; minimal indicator. It shows cycling, which I never want to see ;; or do. (setq embark-mixed-indicator-both nil) (setq embark-mixed-indicator-delay 1.0) (setq embark-indicators '(embark-mixed-indicator embark-highlight-indicator)) (setq embark-verbose-indicator-nested nil) ; I think I don't have them, but I do not want them either (setq embark-verbose-indicator-buffer-sections '(bindings)) (setq embark-verbose-indicator-excluded-actions '(embark-cycle embark-act-all embark-collect embark-export embark-insert)) ;; I never cycle and want to disable the damn thing. Normally, a ;; nil value disables a key binding but here that value is ;; interpreted as the binding for `embark-act'. So I just add ;; some obscure key that I do not have. I absolutely do not want ;; to cycle! (setq embark-cycle-key "<XF86Travel>") ;; I do not want `embark-org' and am not sure what is loading it. ;; So I just unsert all the keymaps... This is the nuclear option ;; but here we are. (with-eval-after-load 'embark-org (defvar prot/embark-org-keymaps '(embark-org-table-cell-map embark-org-table-map embark-org-link-copy-map embark-org-link-map embark-org-src-block-map embark-org-item-map embark-org-plain-list-map embark-org-export-in-place-map) "List of Embark keymaps for Org.") ;; Reset `prot/embark-org-keymaps'. (seq-do (lambda (keymap) (set keymap (make-sparse-keymap))) prot/embark-org-keymaps))) ;; I define my own keymaps because I only use a few functions in a ;; limited number of contexts. (use-package prot-embark :ensure nil :after embark :bind ( :map global-map ("C-," . prot-embark-act-no-quit) ("C-." . prot-embark-act-quit) :map embark-collect-mode-map ("C-," . prot-embark-act-no-quit) ("C-." . prot-embark-act-quit) :map minibuffer-local-filename-completion-map ("C-," . prot-embark-act-no-quit) ("C-." . prot-embark-act-quit)) :config (setq embark-keymap-alist '((buffer prot-embark-buffer-map) (command prot-embark-command-map) (expression prot-embark-expression-map) (file prot-embark-file-map) (function prot-embark-function-map) (identifier prot-embark-identifier-map) (package prot-embark-package-map) (region prot-embark-region-map) (symbol prot-embark-symbol-map) (url prot-embark-url-map) (variable prot-embark-variable-map) (t embark-general-map)))) ;; Needed for correct exporting while using Embark with Consult ;; commands. (use-package embark-consult :ensure t :after (embark consult)))
5.4.14. The prot-emacs-completion.el
section to configure completion annotations (marginalia
)
The marginalia
package, co-authored by Daniel Mendler and Omar
Antolín Camarena, provides helpful annotations to the side of
completion candidates. We see its effect, for example, when we call M-x
:
each command has a brief description next to it (taken from its doc
string) as well as a key binding, if it has one.
Annotations are provided on a per-category basis. Categories are
metadata associated with the completion table, which describe what the
candidates are about. I cover this concept also in my section on
completion styles where I configure pattern-matching algorithms
accordingly (The prot-emacs-completion.el
settings for completion styles).
The out-of-the-box settings of marginalia
are perfectly usable.
Though there are some categories that I either do not want to annotate
(like file
) or that I want to tweak their presentation of. You will
thus notice how my prot-marginalia.el
configures the user option
marginalia-annotator-registry
(The prot-marginalia.el
library).
;;; Detailed completion annotations (marginalia.el) (use-package marginalia :ensure t :hook (after-init . marginalia-mode) :config (setq marginalia-max-relative-age 0)) ; absolute time ;;;; Custom completion annotations (use-package prot-marginalia :ensure nil :after marginalia :config (setq marginalia-annotator-registry '((bookmark prot-marginalia-bookmark) (buffer prot-marginalia-buffer) (command marginalia-annotate-command) (function prot-marginalia-symbol) (symbol prot-marginalia-symbol) (variable prot-marginalia-symbol) (face marginalia-annotate-face) (imenu marginalia-annotate-imenu) (package prot-marginalia-package) (unicode-name marginalia-annotate-char))))
5.4.15. The prot-emacs-completion.el
setting to load a minibuffer UI submodule
In my init.el
I define a user option to select a user interface for
the minibuffer (The init.el
option to load a minibuffer user interface).
The choice is between my mct
package and vertico
by Daniel
Mendler. I think vertico
is the better choice overall and that is why I
set it as the default. Whereas mct
is for users who are more
familiar with the default minibuffer interface and want a bit more
interactivity or convenience on top.
Newer versions of Emacs keep receiving MCT-like tweaks to the default
UI, which means that mct
will eventually be superseded by built-in
options. Until then, I keep it around in a maintenance-only state for
those who need it.
Note that due to my particular needs to not display the minibuffer
eagerly (I do not wish to accidentaly share private details when doing
a presentation), I configure vertico
to be more “private” and thus
function as close to mct
as possible.
- The
prot-emacs-completion.el
submodule formct
(prot-emacs-mct.el
) - The
prot-emacs-completion.el
submodule forvertico
(prot-emacs-vertico.el
)
;;; The minibuffer user interface (mct, vertico, or none) (when prot-emacs-completion-ui (require (pcase prot-emacs-completion-ui ('mct 'prot-emacs-mct) ('vertico 'prot-emacs-vertico)))) (provide 'prot-emacs-completion)
5.4.15.1. The prot-emacs-completion.el
submodule for mct
(prot-emacs-mct.el
)
[ This is subject to a user option: The prot-emacs-completion.el
setting to load a minibuffer UI submodule. ]
The mct
package is my set of tweaks on top of the default minibuffer
and *Completions*
interface to make them work as part of a
contiguous space. It all starts by providing cycling motions from the
minibuffer to the *Completions*
and back, as well as a few extras to
perform the necessary actions. Candidates are updated live to match
the user input.
With mct
we can specify a passlist of commands or completion
categories that eagerly display the *Completions*
. Or we can have a
blocklist to never display that buffer, unless explicitly requested.
Overall, mct
is a capable tool with a minimal aesthetic.
Newer versions of Emacs keep gaining MCT-like capabilities, so I expect the out-of-the-box experience to eventually supersede my package. Until then, I keep it around for those who like it.
Note that I normally use vertico
in my setup, but configure it to
behave similar to what I have here for mct
, meaning that it does not
pop up the minibuffer eagerly (The prot-emacs-completion.el
submodule for vertico
(prot-emacs-vertico.el
)).
;;; Minibuffer and Completions in Tandem or Minibuffer Confines Transcended (mct) ;; Read the manual: <https://protesilaos.com/emacs/mct>. (use-package mct :ensure t :hook (after-init . mct-mode) :config (setq mct-hide-completion-mode-line t) (setq mct-completing-read-multiple-indicator t) ;; The blocklist and passlist accept either commands/functions or ;; completion categories. (setq mct-completion-blocklist '(notmuch-mua-new-mail notmuch-mua-prompt-for-sender)) (setq mct-completion-passlist '(;; Some commands prot-search-outline select-frame-by-name Info-goto-node Info-index Info-menu vc-retrieve-tag ;; Some completion categories consult-buffer consult-location embark-keybinding imenu file project-file buffer kill-ring consult-location)) (setq mct-remove-shadowed-file-names t) (setq mct-completion-window-size (cons #'mct-frame-height-third 1)) (setq mct-live-completion 'visible) (setq completions-sort #'mct-sort-multi-category) ;; Adaptation of `icomplete-fido-backward-updir'. (defun prot/mct-backward-updir () "Delete char before point or go up a directory." (interactive nil mct-mode) (cond ((and (eq (char-before) ?/) (eq (mct--completion-category) 'file)) (when (string-equal (minibuffer-contents) "~/") (delete-minibuffer-contents) (insert (expand-file-name "~/")) (goto-char (line-end-position))) (save-excursion (goto-char (1- (point))) (when (search-backward "/" (minibuffer-prompt-end) t) (delete-region (1+ (point)) (point-max))))) (t (call-interactively 'backward-delete-char)))) (define-key minibuffer-local-filename-completion-map (kbd "DEL") #'prot/mct-backward-updir)) (provide 'prot-emacs-mct)
5.4.15.2. The prot-emacs-completion.el
submodule for vertico
(prot-emacs-vertico.el
)
[ This is subject to a user option: The prot-emacs-completion.el
setting to load a minibuffer UI submodule. ]
The vertico
package by Daniel Mendler displays the minibuffer in a
vertical layout. Under the hood, it takes care to be responsive and to
handle even massive completion tables gracefully. Whereas, say, the
built-in completion user interface (and thus mct
) will suffer from a
noticeable performance penalty.
All we need to get a decent experience with vertico
is to enable the
vertico-mode
. For most users this is enough. In my case though, I
have to use the “multiform” mechanism of this package to make it not
show up eagerly. I do this frequently to control what I am displaying
while doing a presentation. As such, the overall experience I get with
vertico
is the same as with mct
, albeit with better performance
(The prot-emacs-completion.el
submodule for mct
(prot-emacs-mct.el
)).
Beside what I am using it for, the “multiform” mechanism allows us to
change the layout of vertico
on a per-command or per-category basis.
We can, for instance, have a horizontal presentation for some items. I
have tried this for a while, but ultimately decided to go with a more
predictable scheme.
The extras I provide for vertico
are in the prot-vertico.el
library.
;;; Vertical completion layout (vertico) (use-package vertico :ensure t :hook (after-init . vertico-mode) :config (setq vertico-scroll-margin 0) (setq vertico-count 5) (setq vertico-resize t) (setq vertico-cycle t) (with-eval-after-load 'rfn-eshadow ;; This works with `file-name-shadow-mode' enabled. When you are in ;; a sub-directory and use, say, `find-file' to go to your home '~/' ;; or root '/' directory, Vertico will clear the old path to keep ;; only your current input. (add-hook 'rfn-eshadow-update-overlay-hook #'vertico-directory-tidy))) ;;; Custom tweaks for vertico (prot-vertico.el) (use-package prot-vertico :ensure nil :demand t :after vertico :bind ( :map vertico-map ("<left>" . backward-char) ("<right>" . forward-char) ("TAB" . prot-vertico-private-complete) ("DEL" . vertico-directory-delete-char) ("M-DEL" . vertico-directory-delete-word) ("M-," . vertico-quick-insert) ("M-." . vertico-quick-exit) :map vertico-multiform-map ("C-n" . prot-vertico-private-next) ("<down>" . prot-vertico-private-next) ("C-p" . prot-vertico-private-previous) ("<up>" . prot-vertico-private-previous) ("C-l" . vertico-multiform-vertical)) :config (setq vertico-multiform-categories `(;; Maximal (embark-keybinding ,@prot-vertico-multiform-maximal) (multi-category ,@prot-vertico-multiform-maximal) (consult-location ,@prot-vertico-multiform-maximal) (imenu ,@prot-vertico-multiform-maximal) (unicode-name ,@prot-vertico-multiform-maximal) ;; Minimal (file ,@prot-vertico-multiform-minimal (vertico-preselect . prompt) (vertico-sort-function . prot-vertico-sort-directories-first)) (t ,@prot-vertico-multiform-minimal))) (vertico-multiform-mode 1)) (provide 'prot-emacs-vertico)
5.5. The prot-emacs-search.el
module
[ Watch: Emacs: basics of search and replace (2023-06-10). ]
Emacs provides lots of useful facilities to search the contents of
buffers or files. The most common scenario is to type C-s
(isearch-forward
) to perform a search forward from point or C-r
(isearch-backward
) to do so in reverse. These commands pack a ton of
functionality and they integrate nicely with related facilities, such
as those of (i) permanently highlighting the thing being searched,
(ii) putting all results in a buffer that is useful for navigation
purposes, among others, and (iii) replacing the given matching items
with another term.
Here I summarise the functionality, though do check the video I did on the basics of search and replace:
C-s
(isearch-forward
)- Search forward from point (incremental
search); retype
C-s
to move forth. C-r
(isearch-backward
)- Search backward from point
(incremental); retype
C-r
to move back. While using eitherC-s
andC-r
you can move in the opposite direction with either of those keys when performing a repeat. C-M-s
(isearch-forward-regexp
)- Same as
C-s
but matches a regular expression. TheC-s
andC-r
motions are the same after matches are found. C-M-r
(isearch-backward-regexp
)- The counterpart of the above
C-M-s
for starting in reverse. C-s C-w
(isearch-yank-word-or-char
)- Search forward for
word-at-point. Again,
C-s
andC-r
move forth and back, respectively. C-r C-w
(isearch-yank-word-or-char
)- Same as above, but backward.
M-s o
(occur
)- Search for the given regular expression
throughout the buffer and collect the matches in an
*occur*
buffer. Also check what I am doing with this in my custom extensions: Theprot-emacs-search.el
extras provided by theprot-search.el
library. C-u 5 M-s o
(occur
)- Like the above, but give it N lines of context when N is the prefix numeric argument (5 in this example).
C-s SEARCH
followed byM-s o
(isearch-forward
–>occur
)- Like
C-s
but then put the matches in an occur buffer. - (no term)
C-s SEARCH
followed byC-u 5 M-s o
(isearch-forward
–>occur
) :: Same as above, but now with N lines of context (5 in this example).M-%
(query-replace
)- Prompt for target to replace and then prompt for its replacement (see explanation)
C-M-%
(query-replace-regexp
)- Same as above, but for REGEXP
C-s SEARCH
followed byM-%
(isearch-forward
–>query-replace
)- Search
with
C-s
and then perform a query-replace for the following matches. - (no term)
C-M-s SEARCH M-%
(isearch-forward-regexp
–>query-replace-regexp
) :: As above, but regexp-aware.C-s SEARCH C-M-%
(isearch-forward
–>query-replace-regexp
)- Same as above.
M-s h r
(highlight-regexp
)- Prompt for a face (like
hi-yellow
) to highlight the given regular expression. M-s h u
(unhighlight-regexp
)- Prompt for an already highlighted regular expression to unhighlight (do it after the above).
For starters, just learn:
C-s
C-r
M-s o
M-%
Now on to the configurations.
5.5.1. The prot-emacs-search.el
on isearch lax space
As noted in the introduction to this module, the built-in Isearch
mechanism is at the centre of lots of useful patterns of interaction
(The prot-emacs-search.el
module).
The first thing I want to do for Isearch, is make it more convenient
for me to match words that occur in sequence but are not necessarily
following each other. By default, we can do that with something like
C-M-s
(isearch-forward-regexp
) followed by one.*two
. Though it
is inconvenient to be a regexp-aware search mode when all we want is
to just type one two
and have the space be interpreted as
“intermediate characters” rather than a literal space. The following
do exactly this for regular C-s
(isearch-forward
) and C-r
(isearch-backward
).
;;; Isearch, occur, grep, and extras (prot-search.el) (use-package isearch :ensure nil :demand t :config (setq search-whitespace-regexp ".*?" ; one `setq' here to make it obvious they are a bundle isearch-lax-whitespace t isearch-regexp-lax-whitespace nil))
5.5.2. The prot-emacs-search.el
settings for isearch highlighting
Here I am just tweaking the delay that affects when deferred
highlights are applied. The current match is highlighted immediately.
The rest are done after lazy-highlight-initial-delay
unless they are
longer in character count than lazy-highlight-no-delay-length
.
(use-package isearch :ensure nil :demand t :config (setq search-highlight t) (setq isearch-lazy-highlight t) (setq lazy-highlight-initial-delay 0.5) (setq lazy-highlight-no-delay-length 4))
5.5.3. The prot-emacs-search.el
on isearch match counter
I think the following options should be enabled by default. They
produce a counter next to the isearch prompt that shows the position
of the current match relative to the total count (like 5/20
). As we
move to the next/previous match, the counter is updated accordingly.
We have the option to place this information after the search terms,
though I prefer to have them as a prefix so as not to interfere with
what I am typing.
(use-package isearch :ensure nil :demand t :config (setq isearch-lazy-count t) (setq lazy-count-prefix-format "(%s/%s) ") (setq lazy-count-suffix-format nil))
5.5.4. The prot-emacs-search.el
tweaks to the isearch motion behaviour
With the default settings, when we are repeating an isearch in the
opposite direction, Emacs does not move directly to the next/previous
match. Instead, it places the cursor at the opposite end of the
current match. So, if we start with C-s
and search for word
we now
see word|
where the bar represents the cursor. With C-r
we now
have |word
on the same match we were on. I do not like this
behaviour so I configure isearch-repeat-on-direction-change
accordingly. Furthermore, I can always control where the cursor is
left after exiting the search either by performing the given motion
(e.g. M-f
(forward-word
)) or by using my custom command to exit on
the opposite end with C-RET
while in an isearch (prot-search-isearch-other-end
).
[ See: The prot-emacs-search.el
extras provided by the prot-search.el
library. ]
If you are using keyboard macros that rely on isearch, DO NOT set
isearch-wrap-pause
to the no-ding
value. That disables the error
isearch produces when it reaches the end of the matches. This error
exits the keyboard macro, which is exactly what you want if you are
calling it with a 0
numeric argument (to run from point until the
end of the buffer).
(use-package isearch :ensure nil :demand t :config (setq isearch-wrap-pause t) ; `no-ding' makes keyboard macros never quit (setq isearch-repeat-on-direction-change t))
5.5.5. The prot-emacs-search.el
tweaks for the occur buffer
Here I am making some minor tweaks to *occur*
buffer (remember to
read the introduction to this section (The prot-emacs-search.el
module)).
I always want (i) the cursor to be at the top of the buffer, (ii) the
current line to be highlighted, as it is easier for selection
purposes, and (iii) for long lines to be truncated, meaning to stretch
beyond the visible portion of the window without wrapping below, and
for this to be done silently without messaging me about it.
The latter depends on my prot-common.el
library, which is loaded
early at startup (The prot-emacs-essentials.el
block that loads my custom libraries).
(use-package isearch :ensure nil :demand t :config (setq list-matching-lines-jump-to-current-line nil) ; do not jump to current line in `*occur*' buffers (add-hook 'occur-mode-hook #'prot-common-truncate-lines-silently) ; from `prot-common.el' (add-hook 'occur-mode-hook #'hl-line-mode))
5.5.6. The prot-emacs-search.el
modified isearch and occur key bindings
These are some minor tweaks to the key maps for isearch and occur. I
don’t feel strongly about the rest, but the change to C-g
is
important for me as I want to exit the search altogether, not resume
the search of the previous succesful match.
(use-package isearch :ensure nil :demand t :bind ( :map minibuffer-local-isearch-map ("M-/" . isearch-complete-edit) :map occur-mode-map ("t" . toggle-truncate-lines) :map isearch-mode-map ("C-g" . isearch-cancel) ; instead of `isearch-abort' ("M-/" . isearch-complete)))
5.5.7. The prot-emacs-search.el
extras provided by the prot-search.el
library
My prot-search.el
provides lots of useful extras that I use on a
regular basis (The prot-search.el
library).
For isearch, I have:
- Move to next/previous match in isearch with the down/up arrow keys
(
C-s
orC-r
still work though). - Place the cursor on the opposite end of an isearch when exiting. Do
it with
C-RET
while in isearch (Theprot-emacs-search.el
tweaks to the isearch motion behaviour). - Delete the non-matching portion of a query in isearch with a single backspace instead of doing it character-by-character.
- Type
M-s M-<
(prot-search-isearch-beginning-of-buffer
) orM-s M->
(prot-search-isearch-end-of-buffer
) to search for the symbol at point starting from the beginning/end of the buffer.
For variants of occur
or grep
, which also benefit from the rules I have on
where windows/buffers are displayed (The prot-emacs-window.el
rules for displaying buffers (display-buffer-alist
)):
- Type
M-s s
(prot-search-outline
) to use minibuffer completion to match an entry across the buffer’s outline. This is probably my most used command. What I like about it is that it benefits from the extensive customisations I make to the completion mechanism and the minibuffer user interface (Theprot-emacs-completion.el
module). Concretely, I can benefit from the visualisation produced by thevertico
ormct
packages and match the heading with an out-of-order pattern usingorderless
. Simple and super effective! Note that theconsult
package provides theconsult-outline
command, which was the basis for my variant. Unlike mine, it shows a preview of the current match. I find previews disorienting when I type quickly and do not want to experience any motion sickness while using Emacs (this is not the fault ofconsult
, just me needing to have a more static interface). Though there are times I am slowing down the search and need the preview, at which point I call the Consult command onM-s M-s
(Theprot-emacs-completion.el
settings forconsult
). - Type
M-s M-o
(prot-search-occur-outline
) to produce an outline of the given buffer. What constitutes an outline is defined in the user optionprot-search-outline-regexp-alist
. I only configure it for the major modes I care about, though the mechanism should work for every buffer that has an outline. - Type
M-s M-t
(prot-search-grep-todo-keywords
) to produce an*occur*
buffer that matches keywords likeTODO
. The full regular expression is specified in the user optionprot-search-todo-keywords
. - Type
C-u M-s M-t
orM-s M-T
(prot-search-git-grep-todo-keywords
) to do the same as above but with thegit-grep
program instead of Emacs’occur
. This will match the keywords throughout the current Git repository. Grep buffers are editable, like those ofoccur
(Theprot-emacs-search.el
setup for editable grep buffers (wgrep
)). - Type
M-s g
(prot-search-grep
) to perform a “local grep” across the current directory. DoC-u M-s g
to perform a “recursive grep” from the current directory and into all subdirectories. This is basically a streamlined version ofM-x lgrep
andM-x rgrep
and is one of my favourite commands.
Note that the consult
package provides lots of useful commands that
perform a search while also displaying a preview of what you are
matching (The prot-emacs-completion.el
settings for consult
).
Depending on your workflow, this is better, though I seldom need the
preview as I know what to expect or otherwise peruse contents either
by visiting individual files or by using a grep buffer as an index.
(use-package prot-search :ensure nil :bind ( :map global-map ("M-s M-%" . prot-search-replace-markup) ; see `prot-search-markup-replacements' ("M-s M-<" . prot-search-isearch-beginning-of-buffer) ("M-s M->" . prot-search-isearch-end-of-buffer) ("M-s g" . prot-search-grep) ("M-s u" . prot-search-occur-urls) ("M-s t" . prot-search-occur-todo-keywords) ("M-s M-t" . prot-search-grep-todo-keywords) ; With C-u it runs `prot-search-git-grep-todo-keywords' ("M-s M-T" . prot-search-git-grep-todo-keywords) ("M-s s" . prot-search-outline) ("M-s M-o" . prot-search-occur-outline) ("M-s M-u" . prot-search-occur-browse-url) :map isearch-mode-map ("<up>" . prot-search-isearch-repeat-backward) ("<down>" . prot-search-isearch-repeat-forward) ("<backspace>" . prot-search-isearch-abort-dwim) ("<C-return>" . prot-search-isearch-other-end)) :config (setq prot-search-outline-regexp-alist '((emacs-lisp-mode . "^\\((\\|;;;+ \\)") (org-mode . "^\\(\\*+ +\\|#\\+[Tt][Ii][Tt][Ll][Ee]:\\)") (outline-mode . "^\\*+ +") (emacs-news-view-mode . "^\\*+ +") (conf-toml-mode . "^\\[") (markdown-mode . "^#+ +"))) (setq prot-search-todo-keywords (concat "TODO\\|FIXME\\|NOTE\\|REVIEW\\|XXX\\|KLUDGE" "\\|HACK\\|WARN\\|WARNING\\|DEPRECATED\\|BUG")) (with-eval-after-load 'pulsar (add-hook 'prot-search-outline-hook #'pulsar-recenter-center) (add-hook 'prot-search-outline-hook #'pulsar-reveal-entry)))
5.5.8. The prot-emacs-search.el
tweaks to xref
, re-builder
and grep
The xref.el
provides the infrastructure to jump to and from a
definition. For example, with point over a function call,
xref-find-definitions
will jump to the file and location where the
function is defined or provide an option to pick one among multiple
definitions, where applicable. The grep.el
is a wrapper for the Unix
program of the same name. Not much to add there. While re-builder.el
defines a command that lets us write a regexp that matches against the
current buffer, allowing us to test it live.
Note the use of the let
to decide whether I use the grep
or rg
(ripgrep
) program: this covers Xref as well.
;;; grep and xref (use-package re-builder :ensure nil :commands (re-builder regexp-builder) :config (setq reb-re-syntax 'read)) (use-package xref :ensure nil :commands (xref-find-definitions xref-go-back) :config ;; All those have been changed for Emacs 28 (setq xref-show-definitions-function #'xref-show-definitions-completing-read) ; for M-. (setq xref-show-xrefs-function #'xref-show-definitions-buffer) ; for grep and the like (setq xref-file-name-display 'project-relative)) (use-package grep :ensure nil :commands (grep lgrep rgrep) :config (setq grep-save-buffers nil) (setq grep-use-headings t) ; Emacs 30 (let ((executable (or (executable-find "rg") "grep")) (rgp (string-match-p "rg" grep-program))) (setq grep-program executable) (setq grep-template (if rgp "/usr/bin/rg -nH --null -e <R> <F>" "/usr/bin/grep <X> <C> -nH --null -e <R> <F>")) (setq xref-search-program (if rgp 'ripgrep 'grep))))
5.5.9. The prot-emacs-search.el
setup for editable grep buffers (grep-edit-mode
or wgrep
)
Starting with Emacs 31, buffers using grep-mode
can now be edited
directly. The idea is to collect the results of a search in one place
and quickly apply a change across all or some of them. We have the
same concept with occur (M-x occur
) as well as with Dired buffers
(The prot-emacs-dired.el
section about wdired
(writable Dired)).
I use this in tandem with my prot-search-grep
command.
For older versions of Emacs, we have the wgrep
package by Masahiro
Hayashi. I configure it to have key bindings like those of the occur
edit mode, which grep-edit-mode
also uses.
;;; wgrep (writable grep) ;; See the `grep-edit-mode' for the new built-in feature. (unless (>= emacs-major-version 31) (use-package wgrep :ensure t :after grep :bind ( :map grep-mode-map ("e" . wgrep-change-to-wgrep-mode) ("C-x C-q" . wgrep-change-to-wgrep-mode) ("C-c C-c" . wgrep-finish-edit)) :config (setq wgrep-auto-save-buffer t) (setq wgrep-change-readonly-file t)))
5.5.10. The prot-emacs-search.el
call to provide
Finally, we provide
the module. This is the mirror function of
require
(The init.el final part to load the individual modules).
(provide 'prot-emacs-search)
5.6. The prot-emacs-dired.el
module
[ Watch: https://protesilaos.com/codelog/2023-06-26-emacs-file-dired-basics/ (2023-06-26) ]
Dired is probably my favourite Emacs tool. It exemplifies how I see
Emacs as a whole: a layer of interactivity on top of Unix. The dired
interface wraps—and puts to synergy—standard commands like ls
,
cp
, mv
, rm
, mkdir
, chmod
, and related. All while granting
access to many other conveniences, such as (i) marking files to
operate on (individually, with a regexp, etc.), (ii) bulk renaming
files by making the buffer writable and editing it like a regular
file, (iii) showing only files you want, (iv) listing the contents of
any subdirectory, such as to benefit from the bulk-renaming
capability, (v) running a keyboard macro that edits file contents
while using Dired to navigate the file listing, (vi) open files in an
external application, and more.
Dired lets us work with our files in a way that still feels close to the command-line, yet has more powerful interactive features than even fully fledged, graphical file managers.
5.6.1. The prot-emacs-dired.el
settings for common operations
I add two settings which make all copy, rename/move, and delete operations more intuitive. I always want to perform those actions in a recursive manner, as this is the intent I have when I am targeting directories.
The delete-by-moving-to-trash
is a deviation from the behaviour of
the rm
program, as it sends the file into the virtual trash folder.
Depending on the system, files in the trash are either removed
automatically after a few days, or we still have to permanently delete
them manually. I prefer this extra layer of safety. Plus, we have the
trashed
package to navigate the trash folder in a Dired-like way
(The prot-emacs-dired.el
section about trashed.el
).
;;; Dired file manager and prot-dired.el extras (use-package dired :ensure nil :commands (dired) :config (setq dired-recursive-copies 'always) (setq dired-recursive-deletes 'always) (setq delete-by-moving-to-trash t))
5.6.2. The prot-emacs-dired.el
switches for ls
(how files are listed)
As I already explained, Dired is a layer of interactivity on top of
standard Unix tools (The prot-emacs-dired.el
module). We can see
this in how Dired produces the file listing and how we can affect it.
The ls
program accepts an -l
flag for a “long”, detailed list of
files. This is what Dired uses. But we can pass more flags by setting
the value of dired-listing-switches
. Do M-x man
and then search
for the ls
manpage to learn about what I have here. In short:
-A
- Show hidden files (“dotfiles”), such as
.bashrc
, but omit the implied.
and..
targets. The latter two refer to the present and parent directory, respectively. -G
- Do not show the group name in the long listing. Only show the owner of the file.
-F
- Differentiate regular from special files by appending a
character to them. The
*
is for executables, the/
is for directories, the|
is for a named pipe, the=
is for a socket, the@
and the>
are for stuff I have never seen. -h
- Make file sizes easier to read, such as
555k
instead of568024
(the size ofprot-emacs.org
as of this writing). -l
- Produce a long, detailed listing. Dired requires this.
-v
- Sort files by version numbers, such that
file1
,file2
, andfile10
appear in this order instead of 1, 10, 2. The latter is called “lexicograhic” and I have not found a single case where it is useful to me. --group-directories-first
- Does what it says to place all directories before files in the listing. I prefer this over a strict sorting that does not differentiate between files and directories.
--time-style=long-iso
- Uses the international standard for time
representation in the file listing. So we have something like
2023-12-30 06:38
to show the last modified time.
(use-package dired :ensure nil :commands (dired) :config (setq dired-listing-switches "-AGFhlv --group-directories-first --time-style=long-iso"))
5.6.3. The prot-emacs-dired.el
setting for dual-pane Dired
I often have two Dired buffers open side-by-side and want to move
files between them. By setting dired-dwim-target
to a t
value,
we get the other buffer as the default target of the current rename or
copy operation. This is exactly what I want.
If there are more than two windows showing Dired buffers, the default target is the previously visited window.
Note that this only affects how quickly we can access the default
value, as we can always type M-p
(previous-history-element
) and
M-n
(next-history-element
) to cycle through the minibuffer
history (The prot-emacs-completion.el
settings for saving the history (savehist-mode
)).
(use-package dired :ensure nil :commands (dired) :config (setq dired-dwim-target t))
5.6.4. The prot-emacs-dired.el
settings to open files externally
From inside a Dired buffer, we can type !
(M-x dired-do-shell-command
)
or &
(M-x dired-do-async-shell-command
) to run an arbitrary
command with the given file (or marked files) as an argument. These
commands will produce a minibuffer prompt, which expects us to type in
the name of the command. Emacs already tries to guess some relevant
defaults, though we can make it do what we want by configuring the
dired-guess-shell-alist-user
user option.
This variable takes an alist value: a list of lists. Each element
(each list) has the first item in the list as a regular expression to
match file names. We normally want to have file type extensions here,
though we can also target the full name of a file. The remaining
entries in the list are strings that specify the name of the external
program to use. We can have as many as we want and cycle between them
using the familiar M-p
and M-n
keys inside the minibuffer (which
call the commands previous-history-element
and next-history-element
,
respectively).
On Linux, the generic “open with default app” call is xdg-open
, so
we always want that as a fallback.
Note that Emacs 30 (current development target as of this writing on
2023-12-30 16:12 +0200), we have the command dired-do-open
, which is
the equivalent of typing &
and then specifying the xdg-open
command.
(use-package dired :ensure nil :commands (dired) :config (setq dired-guess-shell-alist-user ; those are the suggestions for ! and & in Dired '(("\\.\\(png\\|jpe?g\\|tiff\\)" "feh" "xdg-open") ("\\.\\(mp[34]\\|m4a\\|ogg\\|flac\\|webm\\|mkv\\)" "mpv" "xdg-open") (".*" "xdg-open"))))
5.6.5. The prot-emacs-dired.el
miscellaneous tweaks
These are some minor tweaks that I do not really care about. The only
one which is really nice in my opinion is the hook that involves the
dired-hide-details-mode
. This is the command that hides the noisy
output of the ls
-l
flag, leaving only the file names in the list
(The prot-emacs-dired.el
switches for ls
(how files are listed)).
We can toggle this effect at any time with the (
key, by default.
I disable the repetition of the j
key as I do use repeat-mode
(The prot-emacs-essentials.el
settings for repeat-mode
).
(use-package dired :ensure nil :commands (dired) :config (setq dired-auto-revert-buffer #'dired-directory-changed-p) ; also see `dired-do-revert-buffer' (setq dired-make-directory-clickable t) ; Emacs 29.1 (setq dired-free-space nil) ; Emacs 29.1 (setq dired-mouse-drag-files t) ; Emacs 29.1 (add-hook 'dired-mode-hook #'dired-hide-details-mode) (add-hook 'dired-mode-hook #'hl-line-mode) ;; In Emacs 29 there is a binding for `repeat-mode' which lets you ;; repeat C-x C-j just by following it up with j. For me, this is a ;; problem as j calls `dired-goto-file', which I often use. (define-key dired-jump-map (kbd "j") nil))
5.6.6. The prot-emacs-dired.el
section about various conveniences
The dired-aux.el
and dired-x.el
are two built-in libraries that
provide useful extras for Dired. The highlights from what I have here
are:
- the user option
dired-create-destination-dirs
anddired-create-destination-dirs-on-trailing-dirsep
, which offer to create the specified directory path if it is missing. - the user options
dired-clean-up-buffers-too
anddired-clean-confirm-killing-deleted-buffers
which cover the deletion of buffers related to files that we delete from Dired. - the key binding for
dired-do-open
, which opens the file or directory externally (Theprot-emacs-dired.el
settings to open files externally).
(use-package dired-aux :ensure nil :after dired :bind ( :map dired-mode-map ("C-+" . dired-create-empty-file) ("M-s f" . nil) ("C-<return>" . dired-do-open) ; Emacs 30 ("C-x v v" . dired-vc-next-action)) ; Emacs 28 :config (setq dired-isearch-filenames 'dwim) (setq dired-create-destination-dirs 'ask) ; Emacs 27 (setq dired-vc-rename-file t) ; Emacs 27 (setq dired-do-revert-buffer (lambda (dir) (not (file-remote-p dir)))) ; Emacs 28 (setq dired-create-destination-dirs-on-trailing-dirsep t)) ; Emacs 29 (use-package dired-x :ensure nil :after dired :bind ( :map dired-mode-map ("I" . dired-info)) :config (setq dired-clean-up-buffers-too t) (setq dired-clean-confirm-killing-deleted-buffers t) (setq dired-x-hands-off-my-keys t) ; easier to show the keys I use (setq dired-bind-man nil) (setq dired-bind-info nil))
5.6.7. The prot-emacs-dired.el
section about my extras (prot-dired.el
)
Dired is excellent out-of-the-box. What I provide are a few minor
commands that make it more convenient for me to perform common
actions (The prot-dired.el
library). Chief among them is the
prot-dired-limit-regexp
(bound to C-c C-l
), which is an easier way
to do this in standard Dired:
- Type
% m
(dired-mark-files-regexp
) to mark files you want to keep seeing. Provide a regular expression or simply a common word. - Toggle the mark so that you now cover everything you do not want to see.
- Invoke
dired-do-kill-lines
(bound tok
by default) to remove the marked files from the view until the buffer is generated again (withg
by default (revert-buffer
)).
All this is fine, but with prot-dired-limit-regexp
I simply provide
the regexp I want to see.
Another common use-case for me is to create a flat listing of all
files that match a regular expression, found recursively from the
current directory. I do this with prot-dired-search-flat-list
.
The other commands have situational uses. For example, the
prot-dired-grep-marked-files
is something I have only used a few
times where prot-search-grep
would produce too many results in a
given directory (The prot-emacs-search.el
extras provided by the prot-search.el
library).
(use-package prot-dired :ensure nil :hook (dired-mode . prot-dired-setup-imenu) :bind ( :map dired-mode-map ("i" . prot-dired-insert-subdir) ; override `dired-maybe-insert-subdir' ("/" . prot-dired-limit-regexp) ("C-c C-l" . prot-dired-limit-regexp) ("M-n" . prot-dired-subdirectory-next) ("C-c C-s" . prot-dired-search-flat-list) ("C-c C-n" . prot-dired-subdirectory-next) ("C-c C-p" . prot-dired-subdirectory-previous) ("M-s G" . prot-dired-grep-marked-files) ; M-s g is `prot-search-grep' ("M-p" . prot-dired-subdirectory-previous)))
5.6.8. The prot-emacs-dired.el
section about dired-subtree
The dired-subtree
package by Matúš Goljer provides the convenience
of quickly revealing the contents of the directory at point. We do not
have to insert its contents below the current listing, as we would
normally do in Dired, nor do we have to open it in another buffer just
to check if we need to go further.
I do not use this feature frequently, though I appreciate it when I do need it.
(use-package dired-subtree :ensure t :after dired :bind ( :map dired-mode-map ("<tab>" . dired-subtree-toggle) ("TAB" . dired-subtree-toggle) ("<backtab>" . dired-subtree-remove) ("S-TAB" . dired-subtree-remove)) :config (setq dired-subtree-use-backgrounds nil))
5.6.9. The prot-emacs-dired.el
section about wdired
(writable Dired)
As noted in the introduction, Dired can be made writable
(The prot-emacs-dired.el
module). This way, we can quickly rename
multiple files using Emacs’ panoply of editing capabilities.
Both of the variables I configure here have situational usage. I cannot remember the last time I benefited from them.
Note that we have a variant of wdired
for grep
buffers
(The prot-emacs-search.el
setup for editable grep buffers (wgrep
)).
(use-package wdired :ensure nil :commands (wdired-change-to-wdired-mode) :config (setq wdired-allow-to-change-permissions t) (setq wdired-create-parent-directories t))
5.6.10. The prot-emacs-dired.el
section about image-dired
The built-in image-dired
feature is one of those tools that I like
when I use, but never remember exactly how to use because of how
infrequent my need for it is. The gist is that we put thumbnails in a
buffer and can browse through them in a file listing. It is okay,
though if I really have to rely on image previewing, I might just open
the graphical file manager. It is not a sin to use something outside
of Emacs and, anyway, I don’t think Emacs’ editing capabilities are of
any relevance when we are just previewing a picture.
(use-package image-dired :ensure nil :commands (image-dired) :bind ( :map image-dired-thumbnail-mode-map ("<return>" . image-dired-thumbnail-display-external)) :config (setq image-dired-thumbnail-storage 'standard) (setq image-dired-external-viewer "xdg-open") (setq image-dired-thumb-size 80) (setq image-dired-thumb-margin 2) (setq image-dired-thumb-relief 0) (setq image-dired-thumbs-per-row 4))
5.6.11. The prot-emacs-dired.el
section about dired-preview
The dired-preview
is a package of mine that, unfortunately, I have
not had the time to develop beyond the original release: it coincided
with my relocation to the hut and I still am busy doing all sorts of
work here. Once I get the opportunity, I will resume development of
this package.
At any rate, dired-preview
previews the file at point in an Emacs
window. The goal is to make this work for large files, so that we
never experience any lag. I will expand this section once I feel we
are where we need to be.
- Package name (GNU ELPA):
dired-preview
- Official manual: https://protesilaos.com/emacs/dired-preview
- Git repositories:
- Backronym: Directories Invariably Render Everything Decently; PDFs Require Extra Viewing Instructions for Emacs to Work.
;;; Automatically preview Dired file at point (dired-preview.el) ;; One of my packages: <https://protesilaos.com/emacs> (use-package dired-preview :ensure t ;; :hook (dired-mode . (lambda () ;; (when (string-match-p "Pictures" default-directory) ;; (dired-preview-mode 1)))) :defer 1 :hook (after-init . dired-preview-global-mode) :config (setq dired-preview-max-size (* (expt 2 20) 10)) (setq dired-preview-delay 0.5) (setq dired-preview-ignored-extensions-regexp (concat "\\." "\\(gz\\|" "zst\\|" "tar\\|" "xz\\|" "rar\\|" "zip\\|" "iso\\|" "epub" "\\)")) ;; (setq dired-preview-display-action-alist ;; '((display-buffer-in-side-window) ;; (side . bottom) ;; (window-height . 0.2) ;; (preserve-size . (t . t)) ;; (window-parameters . ((mode-line-format . none) ;; (header-line-format . none))))) )
5.6.12. The prot-emacs-dired.el
section about multimedia previews (ready-player
)
This is a neat package by Alvaro Ramirez which produces a previewable
representation of multimedia file when we try to visit them in an
Emacs buffer. Without ready-player-major-mode
, we get a bunch of
gibberish from the binary data.
While this is a standalone package, it synergises with my
dired-preview
without the need to add any glue code between the two
packages (The prot-emacs-dired.el
section about dired-preview
).
When I move the cursor over a multimedia file, the Dired preview
buffer tries to visit the file at point, which is in turn rendered
using ready-player
.
(use-package ready-player :ensure t :mode ("\\.\\(mp3\\|m4a\\|mp4\\|mkv\\|webm\\)\\'" . ready-player-major-mode) :config (setq ready-player-autoplay nil) (setq ready-player-repeat nil))
5.6.13. The prot-emacs-dired.el
section about trashed.el
The trashed
package by Shingo Tanaka provides a Dired-like interface
to the system’s virtual trash directory. The few times I need to
restore a file, I do M-x dired
, then type r
to mark the file to be
restored (M-x trashed-flag-restore
), and then type x
(M-x trashed-do-execute
)
to apply the effect.
;;; dired-like mode for the trash (trashed.el) (use-package trashed :ensure t :commands (trashed) :config (setq trashed-action-confirmer 'y-or-n-p) (setq trashed-use-header-line t) (setq trashed-sort-key '("Date deleted" . t)) (setq trashed-date-format "%Y-%m-%d %H:%M:%S"))
5.6.14. The prot-emacs-dired.el
section about mandoura
(mpv
media player)
The mandoura
package is an experiment of mine to use Dired as a
starting point to play back some media files (or directory containing
such files) using the mpv
program. I use it daily and it works fine
for just playback, but I do need/want to develop it further as it
really is nothing but a prototype right now. You will thus notice that
the package declaration is actually installing the file from source:
this is a feature built into Emacs 30, though it also is available as
a package for Emacs 29.
- Package name (GNU ELPA):
mandoura
(⛔ not available yet) - Git repositories:
- Backronym: MPV Access Needs Dired to Output User’s Requested Audio.
;;; Play back media with Dired (mandoura.el) ;; This is yet another package of mine: <https://protesilaos.com/emacs> (use-package mandoura ;; The :vc keyword is part of Emacs 30. Read the manual for what keywords it reads: (info "(emacs) Fetching Package Sources") :vc ( :url "https://github.com/protesilaos/mandoura") :commands (mandoura-play-playlist) :after dired :bind ( :map global-map ("M-<AudioPlay>" . mandoura-return-track-title-and-time) ("M-<XF86AudioPlay>" . mandoura-return-track-title-and-time) :map dired-mode-map ("M-<return>" . mandoura-play-playlist) ("M-RET" . mandoura-play-playlist)) :config (setq mandoura-saved-playlist-directory "~/Music/playlists/"))
5.6.15. The prot-emacs-dired.el
call to provide
Finally, we provide
the module. This is the mirror function of
require
(The init.el final part to load the individual modules).
(provide 'prot-emacs-dired)
5.7. The prot-emacs-window.el
module
This module is all about buffers and windows. How they are managed and displayed.
5.7.1. The prot-emacs-window.el
section about running commands in popup frames
Sometimes we want to call a command from outside Emacs. I have
arranged for this to happen so that a new frame pops up, I do the
command, and then the frame is deleted. This requires that we use the
Emacs daemon process or server-mode
(The prot-emacs-essentials.el
arrangement to run Emacs as a server).
We can thus call emacsclient
from outside Emacs and instruct it to
run any arbitrary Emacs Lisp code.
My prot-window.el
provides the prot-window-define-with-popup-frame
macro which streamlines the creation of commands that have the desired
behaviour (The prot-window.el
library). All we need is to call the
macro and tell it which command should be empowered to run in such a
popup frame. I did a video demonstrating this functionality: Emacs: commands in popup frames with emacsclient
(2024-09-19).
All we need here is the hooks that close the frame after the commands
run succesfully. The commands I configure are for org-capture
and
tmr
:
- The
prot-emacs-org.el
Org capture templates (org-capture
) - The
prot-emacs-essentials.el
section abouttmr
(set timers)
(with-eval-after-load 'org-capture (add-hook 'org-capture-after-finalize-hook #'prot-window-delete-popup-frame)) (with-eval-after-load 'tmr (add-hook 'tmr-timer-created-functions #'prot-window-delete-popup-frame))
At my system level I also have keybindings bound to call the following
(you can try those on the command line—remember they depend on the
daemon or server-mode
):
# Run `org-capture` in a popup frame that is deleted after you are done. emacsclient -e '(prot-window-popup-org-capture)'
# Same idea for the `tmr` command. emacsclient -e '(prot-window-popup-tmr)'
5.7.2. The prot-emacs-window.el
section about uniquifying buffer names
When a buffer name is reserved, Emacs tries to produce the new buffer
by finding a suitable variant of the original name. The doc string of
the variable uniquify-buffer-name-style
does a good job at
explaining the various patterns:
For example, the files ‘/foo/bar/mumble/name’ and ‘/baz/quux/mumble/name’ would have the following buffer names in the various styles: forward bar/mumble/name quux/mumble/name reverse name\mumble\bar name\mumble\quux post-forward name|bar/mumble name|quux/mumble post-forward-angle-brackets name<bar/mumble> name<quux/mumble> nil name name<2>
I use the forward
style, which is the closest to the actual file
name.
;;; General window and buffer configurations (use-package uniquify :ensure nil :config ;;;; `uniquify' (unique names for buffers) (setq uniquify-buffer-name-style 'forward) (setq uniquify-strip-common-suffix t) (setq uniquify-after-kill-buffer-p t))
5.7.3. The prot-emacs-window.el
section about line highlighting (hl-line-mode
)
The built-in hl-line-mode
highlights the current line by adding a
background colour to it. I normally do not use this functionality. I
do it only when I need to draw attention to something I am demonstrating.
The nil
value for hl-line-sticky-flag
makes the line highlight not
show up in unfocused windows. I prefer to keep highlights at a
minimum, because I then find it harder to focus on where I am. The
hl-line-overlay-priority
is a more obscure aspect of how Emacs
decides which background should take precedence. You most probably do
not need this, though I had a case for it a while ago: the specifics
do not matter.
;;;; Line highlight (use-package hl-line :ensure nil :commands (hl-line-mode) :config (setq hl-line-sticky-flag nil) (setq hl-line-overlay-priority -50)) ; emacs28
5.7.4. The prot-emacs-window.el
section about negative space highlighting (whitespace-mode
)
Much like hl-line-mode
, I normally do not use the whitespace-mode
(The prot-emacs-window.el
section about line highlighting (hl-line-mode
)).
I do it only when I have to demonstrate something, or to quickly check
that my spacing is correct in some context where that matters.
The changes I have made to whitespace-style
are experimental and I
do not recommend you copy them. I basically tried to make it less
busy, by highlighting fewer elements. For example, I do not highlight
newline characters (represented as a $
) because these are easy to
infer in most cases.
;;;; Negative space highlight (use-package whitespace :ensure nil :bind (("<f6>" . whitespace-mode) ("C-c z" . delete-trailing-whitespace)) :config ;; NOTE 2023-08-14: This is experimental. I am not sure I like it. (setq whitespace-style '(face tabs spaces tab-mark space-mark trailing missing-newline-at-eof space-after-tab::tab space-after-tab::space space-before-tab::tab space-before-tab::space)))
5.7.5. The prot-emacs-window.el
section about line numbers (display-line-numbers-mode
)
As with the two previous sections, I do not like to see line numbers
by default and seldom use display-line-numbers-mode
. They do not
help me navigate a buffer, nor are they relevant in most cases. I
enable the mode only when I need to compare buffers or to get a sense
of how far apart two relevant sections are in a file.
;;; Line numbers on the side of the window (use-package display-line-numbers :ensure nil :bind ("<f7>" . display-line-numbers-mode) :config (setq-default display-line-numbers-type t) ;; Those two variables were introduced in Emacs 27.1 (setq display-line-numbers-major-tick 0) (setq display-line-numbers-minor-tick 0) ;; Use absolute numbers in narrowed buffers (setq-default display-line-numbers-widen t))
5.7.6. The prot-emacs-window.el
rules for displaying buffers (display-buffer-alist
)
[ Watch: control where buffers are displayed (the display-buffer-alist
) (2024-02-08). ]
The display-buffer-alist
is a powerful user option and somewhat hard
to get started with. The reason for its difficulty comes from the
knowledge required to understand the underlying display-buffer
mechanism.
Here is the gist of what we do with it:
- The alist is a list of lists.
Each element of the alist (i.e. one of the lists) is of the following form:
(BUFFER-MATCHER FUNCTIONS-TO-DISPLAY-BUFFER OTHER-PARAMETERS)
- The
BUFFER-MATCHER
is either a regular expression to match the buffer by its name or a method to get the buffer whose major mode is the one specified. In the latter case, you will see the use of cons cells (like(one . two)
) involving thederived-mode
symbol (remember that I build Emacs from source, soderived-mode
may not exist in your version of Emacs). - The
FUNCTIONS-TO-DISPLAY-BUFFER
is a list ofdisplay-buffer
functions that are tried in the order they appear in until one works. The list can be of one element, as you will notice with some of my entries. The
OTHER-PARAMETERS
are enumerated in the Emacs Lisp Reference Manual. Evaluate:(info "(elisp) Buffer Display Action Alists")
In my prot-window.el
library, I define functions that determine how
a buffer should be displayed, given size considerations (The prot-window.el
library).
You will find the functions prot-window-shell-or-term-p
to determine
what a shell or terminal is, prot-window-display-buffer-below-or-pop
to display the buffer below the current one or to its side depending
on how much width is available, and prot-window-select-fit-size
to
perform the two-fold task of selecting a window and making it fit up
to a certain height.
;;;; `window', `display-buffer-alist', and related (use-package prot-window :ensure nil :demand t :config ;; NOTE 2023-03-17: Remember that I am using development versions of ;; Emacs. Some of my `display-buffer-alist' contents are for Emacs ;; 29+. (setq display-buffer-alist `(;; no window ("\\`\\*Async Shell Command\\*\\'" (display-buffer-no-window)) ("\\`\\*\\(Warnings\\|Compile-Log\\|Org Links\\)\\*\\'" (display-buffer-no-window) (allow-no-window . t)) ;; bottom side window ("\\*Org \\(Select\\|Note\\)\\*" ; the `org-capture' key selection and `org-add-log-note' (display-buffer-in-side-window) (dedicated . t) (side . bottom) (slot . 0) (window-parameters . ((mode-line-format . none)))) ;; bottom buffer (NOT side window) ((or . ((derived-mode . flymake-diagnostics-buffer-mode) (derived-mode . flymake-project-diagnostics-mode) (derived-mode . messages-buffer-mode) (derived-mode . backtrace-mode))) (display-buffer-reuse-mode-window display-buffer-at-bottom) (window-height . 0.3) (dedicated . t) (preserve-size . (t . t))) ("\\*Embark Actions\\*" (display-buffer-reuse-mode-window display-buffer-below-selected) (window-height . fit-window-to-buffer) (window-parameters . ((no-other-window . t) (mode-line-format . none)))) ("\\*\\(Output\\|Register Preview\\).*" (display-buffer-reuse-mode-window display-buffer-at-bottom)) ;; below current window ("\\(\\*Capture\\*\\|CAPTURE-.*\\)" (display-buffer-reuse-mode-window display-buffer-below-selected)) ("\\*\\vc-\\(incoming\\|outgoing\\|git : \\).*" (display-buffer-reuse-mode-window display-buffer-below-selected) (window-height . 0.1) (dedicated . t) (preserve-size . (t . t))) ((derived-mode . reb-mode) ; M-x re-builder (display-buffer-reuse-mode-window display-buffer-below-selected) (window-height . 4) ; note this is literal lines, not relative (dedicated . t) (preserve-size . (t . t))) ((or . ((derived-mode . occur-mode) (derived-mode . grep-mode) (derived-mode . Buffer-menu-mode) (derived-mode . log-view-mode) (derived-mode . help-mode) ; See the hooks for `visual-line-mode' "\\*\\(|Buffer List\\|Occur\\|vc-change-log\\|eldoc.*\\).*" prot-window-shell-or-term-p ;; ,world-clock-buffer-name )) (prot-window-display-buffer-below-or-pop) (body-function . prot-window-select-fit-size)) ("\\*\\(Calendar\\|Bookmark Annotation\\|ert\\).*" (display-buffer-reuse-mode-window display-buffer-below-selected) (dedicated . t) (window-height . fit-window-to-buffer)) ;; NOTE 2022-09-10: The following is for `ispell-word', though ;; it only works because I override `ispell-display-buffer' ;; with `prot-spell-ispell-display-buffer' and change the ;; value of `ispell-choices-buffer'. ("\\*ispell-top-choices\\*.*" (display-buffer-reuse-mode-window display-buffer-below-selected) (window-height . fit-window-to-buffer)) ;; same window ;; NOTE 2023-02-17: `man' does not fully obey the ;; `display-buffer-alist'. It works for new frames and for ;; `display-buffer-below-selected', but otherwise is ;; unpredictable. See `Man-notify-method'. ((or . ((derived-mode . Man-mode) (derived-mode . woman-mode) "\\*\\(Man\\|woman\\).*")) (display-buffer-same-window)))))
The following settings are relevant for the display-buffer-alist
we
saw right above. Notice, in particular, the split-height-threshold
and split-width-threshold
which determine when to split the frame by
height or width. These are relevant for prot-window-display-buffer-below-or-pop
and the other more basic functions I have defined for this purpose.
(use-package prot-window :ensure nil :demand t :config (setq window-combination-resize t) (setq even-window-sizes 'height-only) (setq window-sides-vertical nil) (setq switch-to-buffer-in-dedicated-window 'pop) (setq split-height-threshold 80) (setq split-width-threshold 125) (setq window-min-height 3) (setq window-min-width 30))
5.7.7. The prot-emacs-window.el
setting to enable visual-line-mode
in some contexts
I normally do not use visual-line-mode
. What it does is to break
long lines to span multiple lines without actually affecting the
underlying text. In other words, we still have one long line only its
visualisation is as a paragraph.
For the cases where I am fine with visual-line-mode
, I enable the
mode by adding it to these mode hooks.
(use-package prot-window :ensure nil :demand t :hook ((epa-info-mode help-mode custom-mode) . visual-line-mode))
5.7.8. The prot-emacs-window.el
settings to truncate some buffers silently
This here is the opposite of what we saw above (The prot-emacs-window.el
setting to enable visual-line-mode
in some contexts).
Unlike visual-line-mode
where long lines are made to look like
paragraphs, “truncation” means to let the line cover its natural
length and simply cut it off screen.
I have a custom library that defines a function which performs line
truncation without displaying a message about the fact (The prot-common.el
library).
Why do we need this? Check the output of M-x calendar
in a tiny
window and you will see the reason. In short, it is better to have
lines not show their full contents than to have something that looks
completely broken.
The whole point of using hooks is to make these decisions on a case-by-case basis.
(use-package prot-window :ensure nil :demand t :hook ((world-clock-mode calendar-mode) . prot-common-truncate-lines-silently))
5.7.9. The prot-emacs-window.el
section key bindings
Here I simply define some key bindings. The commands I use the most in
this list are delete-other-windows-vertically
, previous-buffer
,
and next-buffer
. I rarely resize windows, though I am happy to use
the mouse for such a case.
(use-package prot-window :ensure nil :demand t :bind ( :map global-map ;; NOTE 2022-09-17: Also see `prot-simple-swap-window-buffers'. ("C-x <down>" . next-buffer) ("C-x <up>" . previous-buffer) ("C-x C-n" . next-buffer) ; override `set-goal-column' ("C-x C-p" . previous-buffer) ; override `mark-page' ("C-x !" . delete-other-windows-vertically) ("C-x _" . balance-windows) ; underscore ("C-x -" . fit-window-to-buffer) ; hyphen ("C-x +" . balance-windows-area) ("C-x }" . enlarge-window) ("C-x {" . shrink-window) ("C-x >" . enlarge-window-horizontally) ; override `scroll-right' ("C-x <" . shrink-window-horizontally) ; override `scroll-left' :map resize-window-repeat-map (">" . enlarge-window-horizontally) ("<" . shrink-window-horizontally)))
5.7.10. The prot-emacs-window.el
section about beframe
[ Also see: The prot-emacs-git.el
section about project.el
. ]
My beframe
package enables a frame-oriented Emacs workflow where
each frame has access to the list of buffers visited therein. In the
interest of brevity, we call buffers that belong to frames “beframed”.
Check the video demo I did and note that I consider this one of the
best changes I ever did to boost my productivity:
https://protesilaos.com/codelog/2023-02-28-emacs-beframe-demo/.
- Package name (GNU ELPA):
beframe
- Official manual: https://protesilaos.com/emacs/beframe
- Change log: https://protesilaos.com/emacs/beframe-changelog
- Git repositories:
- Backronym: Buffers Encapsulated in Frames Realise Advanced Management of Emacs.
;;; Frame-isolated buffers ;; Another package of mine. Read the manual: ;; <https://protesilaos.com/emacs/beframe>. (use-package beframe :ensure t :hook (after-init . beframe-mode) :config (setq beframe-functions-in-frames '(project-prompt-project-dir)) ;; I use this instead of :bind because I am binding a keymap and the ;; way `use-package' does it is by wrapping a lambda around it that ;; then breaks `describe-key' for those keys. (prot-emacs-keybind global-map ;; Override the `set-fill-column' that I have no use for. "C-x f" #'other-frame-prefix ;; Bind Beframe commands to a prefix key. Notice the -map as I am ;; binding keymap here, not a command. "C-c b" #'beframe-prefix-map ;; Replace the generic `buffer-menu'. With a prefix argument, this ;; commands prompts for a frame. Call the `buffer-menu' via M-x if ;; you absolutely need the global list of buffers. "C-x C-b" #'beframe-buffer-menu ;; Not specific to Beframe, but since it renames frames (by means ;; of `beframe-mode') it is appropriate to have this here: "C-x B" #'select-frame-by-name))
5.7.11. The prot-emacs-window.el
configuration of undelete-frame-mode
and winner-mode
Since I am using my beframe
package to isolate buffers per frame
(The prot-emacs-window.el
section about beframe
), I appreciate the
feature of Emacs 29 to undo the deletion of frames. Note the key
binding I use for this purpose. It overrides one of the alternatives
for the standard undo
command, though I personally only ever use
C-/
: everything else is free to use as I see fit.
;;; Frame history (undelete-frame-mode) (use-package frame :ensure nil :bind ("C-x u" . undelete-frame) ; I use only C-/ for `undo' :hook (after-init . undelete-frame-mode))
The winner-mode
is basically the same idea as undelete-frame-mode
but for window layouts. Or maybe I should phrase this the other way
round, given that winner
is the older package. But the point is that
we can quickly go back to an earlier arrangement of windows in a
frame.
;;; Window history (winner-mode) (use-package winner :ensure nil :hook (after-init . winner-mode) :bind (("C-x <right>" . winner-redo) ("C-x <left>" . winner-undo)))
5.7.12. The prot-emacs-window.el
keys for window motions (windmove
)
I usually cycle between windows with C-x o
(M-x other-window
).
This is because I use two or three windows at most. It is easier to
use the same key, especially with the addition of repeat-mode
(The prot-emacs-essentials.el
settings for repeat-mode
).
Nevertheless, the built-in windmove
package provides a set of useful
commands to move focus directionally between windows but also to shift
the placement of a window in the given direction.
;;; Directional window motions (windmove) (use-package windmove :ensure nil :bind ;; Those override some commands that are already available with ;; C-M-u, C-M-f, C-M-b. (("C-M-<up>" . windmove-up) ("C-M-<right>" . windmove-right) ("C-M-<down>" . windmove-down) ("C-M-<left>" . windmove-left) ("C-M-S-<up>" . windmove-swap-states-up) ("C-M-S-<right>" . windmove-swap-states-right) ; conflicts with `org-increase-number-at-point' ("C-M-S-<down>" . windmove-swap-states-down) ("C-M-S-<left>" . windmove-swap-states-left)) :config (setq windmove-create-window nil)) ; Emacs 27.1
5.7.13. The prot-emacs-window.el
use of contextual header line (breadcrumb
)
The breadcrumb
package by João Távora lets us display contextual
information about the current heading or code definition in the header
line. The header line is displayed above the contents of each buffer
in the given window. When we are editing an Org file, for example, we
see the path to the file, followed by a reference to the tree that
leads to the current heading. Same idea for programming modes. Neat!
;;; Header line context of symbol/heading (breadcrumb.el) (use-package breadcrumb :ensure t :functions (prot/breadcrumb-local-mode) :hook ((text-mode prog-mode) . prot/breadcrumb-local-mode) :config (setq breadcrumb-project-max-length 0.5) (setq breadcrumb-project-crumb-separator "/") (setq breadcrumb-imenu-max-length 1.0) (setq breadcrumb-imenu-crumb-separator " > ") (defun prot/breadcrumb-local-mode () "Enable `breadcrumb-local-mode' if the buffer is visiting a file." (when buffer-file-name (breadcrumb-local-mode 1))))
5.7.14. The prot-emacs-window.el
provide
form
As always, we provide
the module. This is the mirror function of
require
(The init.el final part to load the individual modules).
(provide 'prot-emacs-window)
5.8. The prot-emacs-git.el
module
[ Watch: Contribute to GNU Emacs core (2023-08-03). ]
Emacs can handle several version control programs, though I only ever
use git
. Version control is essential to any public-facing
programming project: the history of changes is stored persistently, so
the developer can retrace when a given snippet of code was introduced
or modified. Plus, it is integral to collaboration, which is what free
software (such as Emacs and all its packages) is all about.
This section covers my settings for version control per se, but more widely for tools related to checking different versions of files and working with so-called “projects”.
5.8.1. The prot-emacs-git.el
section about ediff
[ Watch: Emacs: ediff basics (2023-12-30) ]
The built-in ediff
feature provides several commands that let us
compare files or buffers side-by-side. The defaults of ediff
are bad,
in my opinion: it puts buffers one on top of the other and places the
“control panel” in a separate Emacs frame. The first time I tried to
use it, I thought I broke my setup because it is unlike anything we
normally interact with. As such, the settings I have for
ediff-split-window-function
and ediff-window-setup-function
are
what I would expect Emacs maintainers to adopt as the new default. I
strongly encourage everyone to start with them.
In my workflow, the points of entry to the ediff
feature are the
commands ediff-files
, ediff-buffers
. Sometimes I use the 3-way
variants with ediff-files3
and ediff-buffers3
, though this is rare.
Do watch the video I link to in the beginning of this section, as it
covers the main functionality of this neat tool. I also show how it
integrates with magit
(The prot-emacs-git.el
section about magit
(great Git client)).
;;;; `ediff' (use-package ediff :ensure nil :commands (ediff-buffers ediff-files ediff-buffers3 ediff-files3) :init (setq ediff-split-window-function 'split-window-horizontally) (setq ediff-window-setup-function 'ediff-setup-windows-plain) :config (setq ediff-keep-variants nil) (setq ediff-make-buffers-readonly-at-startup nil) (setq ediff-merge-revisions-with-ancestor t) (setq ediff-show-clashes-only t))
5.8.2. The prot-emacs-git.el
section about project.el
In Emacs parlance, a “project” is a collection of files and/or
directories that share the same root. The root of a project is
identified by a special file or directory, with .git/
being one of
the defaults as it is a version control system supported by the
built-in vc.el
(The prot-emacs-git.el
section about vc.el
and related).
We can specify more project roots as a list of strings in the user
option project-vc-extra-root-markers
. I work exclusively with Git
repositories, so I just add there a .project
file in case I ever
need to register a project without it being controlled by git
. In
that case, the .project
file is just an empty file in a directory
that I want to treat as the root of this project.
The common way to switch to a project is to type C-x p p
, which
calls the command project-switch-project
. It lists all registered
projects and also includes a ... (choose a dir)
option. By choosing
a new directory, we register it in our project list if it has a
recognisable root. Once we select a project, we are presented with a
list of common actions to start working on the project. These are
defined in the user option project-switch-commands
and are activated
by the final key that accesses them from the C-x p
prefix. As such,
do M-x describe-keymap
and check the project-prefix-map
. For
example, I bind project-dired
to C-x p RET
, so RET
accesses this
command after C-x p p
as well.
If any of the project.el
commands is called from outside a project,
it first prompts for a project and then carries out its action. For
example, project-find-file
will ask for a project to use, then
switch to it, and then prompt for a file inside of the specified
project.
While inside a project, we have many commands that operate on the
project level. For example, C-x p f
(project-find-file
) searches
for a file across the project, while C-x p b
(project-switch-to-buffer
)
switches to a buffer that is specific to the project. Again, check the
project-prefix-map
for available commands.
If not inside a project, the project-related commands will first
prompt to select a project (same as typing C-x p p
) and then carry
out their action.
I combine projects with my beframe
package, so that when I switch to
a project I get a new frame that limits the buffers I visit there
limited to that frame (The prot-emacs-window.el
section about beframe
).
Note that the prot-project.el
library defines functionality that is
useful, but which I personally do not need. I am thus not including it
here, but do take a look.
;;;; `project' (use-package project :ensure nil :bind (("C-x p ." . project-dired) ("C-x p C-g" . keyboard-quit) ("C-x p <return>" . project-dired) ("C-x p <delete>" . project-forget-project)) :config (setopt project-switch-commands '((project-find-file "Find file") (project-find-regexp "Find regexp") (project-find-dir "Find directory") (project-dired "Root dired") (project-vc-dir "VC-Dir") (project-shell "Shell") (keyboard-quit "Quit"))) (setq project-vc-extra-root-markers '(".project")) ; Emacs 29 (setq project-key-prompt-style t) ; Emacs 30 (advice-add #'project-switch-project :after #'prot-common-clear-minibuffer-message)) (use-package prot-project :ensure nil ;; Also check the command `prot-project-in-tab'. I do not use it ;; because I prefer to manage my buffers in frames, with my ;; `beframe' package. :bind ( :map project-prefix-map ("p" . prot-project-switch)))
5.8.3. The prot-emacs-git.el
section about diff-mode
This built-in mode is an easy and effective way to interact with
diffs. A “diff” is a Unix tradition of showing line-wise differences
in a file. If you, say, edit a line to replace this
with that
, the
diff output will show the original line prefixed with a minus sign and
the new line prefixed with a plus sign.
Individual words can also be highlighted. This word-wise operation is
known as “refining” the diff. In my setup, this is handled by the
agitate
package, which provides convenience functions for various
version control operations. The command is agitate-diff-refine-cycle
,
bound to C-c C-b
inside of diff buffers.
With diff-mode
, we can also apply the “diff hunk” at point, if we
have the corresponding files. The diff hunk is the section of the diff
that pertains to a given region in the file and is delimited by a
heading that enumerates the affected range, like @@ -6125,7 +6125,9 @@
.
Type C-c C-a
(diff-apply-hunk
).
The diff-mode
buffers specify the outline-regexp
, meaning that
they can be used with the built-in outline-minor-mode
to, for
example, fold the invidual diff hunks and move between them (The prot-emacs-langs.el
settings for outline-minor-mode
).
Personally, I combine this feature with my prot-search-outline
command to quickly jump to an outline heading using minibuffer
completion (The prot-emacs-search.el
extras provided by the prot-search.el
library).
Outside of Emacs, I have settings for git
which produce more
informative diff hunk headings in Elisp and Org buffers. I wrote about
it here: https://protesilaos.com/codelog/2021-01-26-git-diff-hunk-elisp-org/.
The configurations are part of my dotfiles (linked to at the opening
section of this file).
;;;; `diff-mode' (use-package diff-mode :ensure nil :defer t :config (setq diff-default-read-only t) (setq diff-advance-after-apply-hunk t) (setq diff-update-on-the-fly t) ;; The following are from Emacs 27.1 (setq diff-refine nil) ; I do it on demand, with my `agitate' package (more below) (setq diff-font-lock-prettify t) ; I think nil is better for patches, but let me try this for a while (setq diff-font-lock-syntax 'hunk-also))
5.8.4. The prot-emacs-git.el
section about vc.el
and related
The concept of “version control” pertains to a system of versioning
files, to track and visualise changes from record to record
(The prot-emacs-git.el
section about diff-mode
).
These version-controlled files may be part of a project
(The prot-emacs-git.el
section about project.el
).
There are many programs that fall in the category of Version Control
Software (VCS). I only use git
, simply because it is ubiquitous
though there are others which have technical merits as well.
VCSs have some common features, such as how they record a unit of
history, and how they handle the synchronisation of their state across
computers. Because of these commonalities, Emacs is able to provide a
layer of abstraction, known as “Version Control”, else vc.el
and its
accoutrements.
[ Technically, the vc.el
file is not the only one defining relevant
functionality. There are VCS-specific variants, such as vc-git.el
,
as well as complementary features like vc-annotate.el
. All these
are hereinafter referred to as vc
. ]
With vc
, we can carry out all the common actions related to version
control, such as to commit (to make a record of) changes and pull/push
them from/to the remote (i.e. the server with which we sync our
project). Whatever VCS we use, the workflow is the same:
- Make changes to a file.
- Type
C-x v v
(vc-next-action
).- If the file is already under version control,
vc
will produce a “log edit” buffer to let you commit the changes. - If the file is not under version control,
vc
will use a minibuffer prompt to ask which VCS to use. These are also known as backends and are stored in the user optionvc-handled-backends
. - If the file is not under version control but is in a directory which itself is version controlled, then the file will be added to the list of tracked files.
- If the file is already under version control,
- Type
C-x v v
again andvc
will proceed to the next action, which is to commit the changes to history. This is done in the newlog-edit
buffer. - By convention, the message of each commit is separated into a
summary and the body of the message. An empty line divides them. The
summary is the first line of the message and should, as a matter of
best practices, be brief yet sufficiently descriptive. The rest is
free form text. In the
log-edit
buffer, the empty separator line between the summary and the body is shown as a border, so there is no need to add another line there. - Once the message is ready, type
C-c C-c
(log-edit-done
) to confirm it orC-c C-k
(log-edit-kill-buffer
) to cancel the operation. - From the
log-edit
buffer, it is possible to see the underlying changes in a diff buffer. Do it withC-c C-d
(log-edit-show-diff
). - The record of commits to the history of the entire project is
accessed with the command
vc-print-root-log
, while that of individual files is handled by the commandvc-print-log
. - To pull from a remote, do
vc-update
. To push, invokevc-push
. - A Dired-like buffer is also available to perform these actions
across many edited files. Check the commands,
vc-dir
,vc-dir-root
, or evenproject-vc-dir
.
Remember to use C-h m
(describe-mode
) to get help for the buffer
you are in and to learn the relevant key bindings.
There is more functionality, though this should already give you an
overview of what is on offer. The gist is that vc
provides a fast
and minimalist way to accomplish the basic tasks related to version
control. For more demanding operations, there is either the
command-line or the wonderful magit
Emacs package
(The prot-emacs-git.el
section about magit
(great Git client)).
Note that in the following code block I redefine lots of key bindings.
They make more sense to me. Furthermore, my agitate
package defines
many extras on top of vc
that I use daily in my workflow
(The prot-emacs-git.el
section about agitate
).
;;; Version control framework (vc.el, vc-git.el, and more) (use-package vc :ensure nil :bind (;; NOTE: I override lots of the defaults :map global-map ("C-x v B" . vc-annotate) ; Blame mnemonic ("C-x v e" . vc-ediff) ("C-x v k" . vc-delete-file) ; 'k' for kill==>delete is more common ("C-x v G" . vc-log-search) ; git log --grep ("C-x v t" . vc-create-tag) ("C-x v c" . vc-clone) ; Emacs 31 ("C-x v d" . vc-diff) ("C-x v ." . vc-dir-root) ; `vc-dir-root' is from Emacs 28 ("C-x v <return>" . vc-dir-root) :map vc-dir-mode-map ("t" . vc-create-tag) ("O" . vc-log-outgoing) ("o" . vc-dir-find-file-other-window) ("d" . vc-diff) ; parallel to D: `vc-root-diff' ("k" . vc-dir-delete-file) ("G" . vc-revert) :map vc-git-stash-shared-map ("a" . vc-git-stash-apply-at-point) ("c" . vc-git-stash) ; "create" named stash ("k" . vc-git-stash-delete-at-point) ; symmetry with `vc-dir-delete-file' ("p" . vc-git-stash-pop-at-point) ("s" . vc-git-stash-snapshot) :map vc-annotate-mode-map ("M-q" . vc-annotate-toggle-annotation-visibility) ("C-c C-c" . vc-annotate-goto-line) ("<return>" . vc-annotate-find-revision-at-line) :map log-edit-mode-map ("M-s" . nil) ; I use M-s for my search commands ("M-r" . nil) ; I use `consult-history' :map log-view-mode-map ("<tab>" . log-view-toggle-entry-display) ("<return>" . log-view-find-revision) ("s" . vc-log-search) ("o" . vc-log-outgoing) ("f" . vc-log-incoming) ("F" . vc-update) ("P" . vc-push)) :init (setq vc-follow-symlinks t) :config ;; Those offer various types of functionality, such as blaming, ;; viewing logs, showing a dedicated buffer with changes to affected ;; files. (require 'vc-annotate) (require 'vc-dir) (require 'vc-git) (require 'add-log) (require 'log-view) ;; I only use Git. If I ever need another, I will include it here. ;; This may have an effect on performance, as Emacs will not try to ;; check for a bunch of backends. (setq vc-handled-backends '(Git)) ;; This one is for editing commit messages. (require 'log-edit) (setq log-edit-confirm 'changed) (setq log-edit-keep-buffer nil) (setq log-edit-require-final-newline t) (setq log-edit-setup-add-author nil) ;; I can see the files from the Diff with C-c C-d (remove-hook 'log-edit-hook #'log-edit-show-files) (setq vc-find-revision-no-save t) (setq vc-annotate-display-mode 'scale) ; scale to oldest ;; I use a different account for git commits (setq add-log-mailing-address "info@protesilaos.com") (setq add-log-keep-changes-together t) (setq vc-git-diff-switches '("--patch-with-stat" "--histogram")) (setq vc-git-log-switches '("--stat")) (setq vc-git-print-log-follow t) (setq vc-git-revision-complete-only-branches nil) ; Emacs 28 (setq vc-git-root-log-format `("%d %h %ai %an: %s" ;; The first shy group matches the characters drawn by --graph. ;; We use numbered groups because `log-view-message-re' wants the ;; revision number to be group 1. ,(concat "^\\(?:[*/\\|]+\\)\\(?:[*/\\| ]+\\)?" "\\(?2: ([^)]+) \\)?\\(?1:[0-9a-z]+\\) " "\\(?4:[0-9]\\{4\\}-[0-9-]\\{4\\}[0-9\s+:-]\\{16\\}\\) " "\\(?3:.*?\\):") ((1 'log-view-message) (2 'change-log-list nil lax) (3 'change-log-name) (4 'change-log-date)))) ;; These two are from Emacs 29 (setq vc-git-log-edit-summary-target-len 50) (setq vc-git-log-edit-summary-max-len 70))
5.8.5. The prot-emacs-git.el
section about agitate
This is an experimental package of mine whose role is to extend the
functionality provided by the vc
suite of tools (The prot-emacs-git.el
section about vc.el
and related),
as well as that of diff-mode
(The prot-emacs-git.el
section about diff-mode
).
What agitate
does is define several commands that are in the spirit
of those built-in tools.
Because it is (i) an experimental package and (ii) vc
is probably a
niche feature, I will not document much here.
Agitate is a collection of commands or potentially useful functions that expand on the available version control features of Emacs. Those are meant to complement a workflow that relies on the built-in Version Control framework and its accoutrements (`diff-mode.el`, `log-view.el`, `log-edit.el`, `vc-git.el`, and potentially others).
- Package name (GNU ELPA):
agitate
- Official manual: https://protesilaos.com/emacs/agitate
- Git repositories:
- Backronym: Another Git Interface Trying to Agitate Tranquil Emacsers
;;; Agitate ;; A package of mine to complement VC and friends. Read the manual ;; here: <https://protesilaos.com/emacs/agitate>. (use-package agitate :ensure t :hook ((diff-mode . agitate-diff-enable-outline-minor-mode) (after-init . agitate-log-edit-informative-mode)) :bind ( :map global-map ("C-x v =" . agitate-diff-buffer-or-file) ; replace `vc-diff' ("C-x v g" . agitate-vc-git-grep) ; replace `vc-annotate' ("C-x v f" . agitate-vc-git-find-revision) ("C-x v s" . agitate-vc-git-show) ("C-x v w" . agitate-vc-git-kill-commit-message) ("C-x v p p" . agitate-vc-git-format-patch-single) ("C-x v p n" . agitate-vc-git-format-patch-n-from-head) :map diff-mode-map ("C-c C-b" . agitate-diff-refine-cycle) ; replace `diff-refine-hunk' ("C-c C-n" . agitate-diff-narrow-dwim) ("L" . vc-print-root-log) ;; Emacs 29 can use C-x v v in diff buffers, which is great, but now I ;; need quick access to it... ("v" . vc-next-action) :map log-view-mode-map ("w" . agitate-log-view-kill-revision) ("W" . agitate-log-view-kill-revision-expanded) :map vc-git-log-view-mode-map ("c" . agitate-vc-git-format-patch-single) :map log-edit-mode-map ("C-c C-i C-n" . agitate-log-edit-insert-file-name) ;; See user options `agitate-log-edit-emoji-collection' and ;; `agitate-log-edit-conventional-commits-collection'. ("C-c C-i C-e" . agitate-log-edit-emoji-commit) ("C-c C-i C-c" . agitate-log-edit-conventional-commit)) :config (advice-add #'vc-git-push :override #'agitate-vc-git-push-prompt-for-remote) (setq agitate-log-edit-informative-show-root-log nil agitate-log-edit-informative-show-files nil))
5.8.6. The prot-emacs-git.el
section about magit
(great Git client)
The magit
package, maintained by Jonas Bernoulli, is the best
front-end to git
I have ever used. Not only is it excellent at
getting the job done, it also helps you learn more about what git
has to offer.
At the core of its interface is transient
. This is a library that
was originally developed as Magit-specific code that was then
abstracted away and ultimately incorporated into Emacs version 29.
With transient
, we get a window pop up with keys and commands
corresponding to them. The window is interactive, as the user can set
a value or toggle an option and have it take effect when the relevant
command is eventually invoked. For git
, in particular, this
interface is a genious way to surface the plethora of options.
To start, call the command magit-status
. It brings up a buffer that
shows information about the state of the repository. Sections include
an overview of the current HEAD
, untracked files, unstaged changes,
staged changes, and recent commits. Each section’s visibility state
can be cycled by pressing TAB
(variations of this are available—remember
to do C-h m
(describe-mode
) in an unfamiliar major mode to get
information about its key bindings).
From the status buffer, we can perform all the usual version control
operations. By typing ?
(magit-dispatch
), we bring up the main
transient
menu, with keys that then bring up their own submenus,
such as for viewing commit logs, setting the remotes, switching
branches, etc.
Before I used magit
, I only knew the basics of adding files for a
commit, writing a commit message inline with the -m
flag on the
command line, and pushing to the remote. Magit shows the staging
area in the status buffer and makes “staging” a key part of the
process of committing changes to history. To stage something, is to
make it a candidate for the next commit action: only the staged parts
are committed.
Magit has a refined understanding of context. We can target an individual line, a single diff hunk, a single file, or a range of files for staging or unstaging (among others). If the region is active, then only the selection is affected. If the cursor is on or somewhere inside a diff hunk, then that is targeted. If the cursor is over a file, then the file is the target. Same idea for the section heading, which then extends to everything under it.
This contextuality extends to every git
command that accepts a
commit hash as an argument. For example, if we are in a Magit commit
log view and want to do a hard reset on the commit at point, Magit
knows what commit hash to use (and presents it as an option when we
choose where to reset to). Same principle for rebasing, cherry
picking, and more.
Magit is good for newer users but also for those who have experience
with git
and the command-line in general. With it, I can easily
maintain a project that needs to track separate remotes and push/pull
between them in a fairly complicated manner. Partly supported by
transient
and partly by the Emacs completion user interface, we have
all we need to select targets with ease (The prot-emacs-completion.el
module).
The only downside of this wonderful package is that it is slow on
Windows (based on what others have told me and showed me)… In those
cases, a combination of vc
and the command-line will suffice
(The prot-emacs-git.el
section about vc.el
and related).
;;; Interactive and powerful git front-end (Magit) (use-package transient :defer t :config (setq transient-show-popup 0.5)) (use-package magit :ensure t :bind ("C-c g" . magit-status) :init (setq magit-define-global-key-bindings nil) (setq magit-section-visibility-indicator '("⮧")) :config (setq git-commit-summary-max-length 50) ;; NOTE 2023-01-24: I used to also include `overlong-summary-line' ;; in this list, but I realised I do not need it. My summaries are ;; always in check. When I exceed the limit, it is for a good ;; reason. (setq git-commit-style-convention-checks '(non-empty-second-line)) (setq magit-diff-refine-hunk t)) (use-package magit-repos :ensure nil ; part of `magit' :commands (magit-list-repositories) :init (setq magit-repository-directories '(("~/Git/Projects" . 1))))
5.8.7. The prot-emacs-git.el
call to provide
Finally, we provide
the module. This is the mirror function of
require
(The init.el final part to load the individual modules).
(provide 'prot-emacs-git)
5.9. The prot-emacs-org.el
module
Watch:
- Advanced literate configuration with Org (2023-12-18)
- Basics of Org mode (2023-05-23)
- Demo of my custom Org block agenda (2021-12-09)
- Primer on “org-capture” (2020-02-04)
At its core, Org is a plain text markup language. By “markup language”, we refer to the use of common characters to apply styling, such as how a word wrapped in asterisks acquires strong emphasis. Check the video I link to above on the basics of Org mode.
Though what makes Org powerful is not the markup per se, but the fact that it has a rich corpus of Emacs Lisp code that does a lot with this otherwise plain text notation. Some of the headline features:
- Cycle the visibility of any heading and its subheadings. This lets you quickly fold a section you do not need to see (or reveal the one you care about).
- Mix prose with code in a single document to either make the whole thing an actual program or to evaluate/demonstrate some snippets.
- Convert (“export”) an Org file to a variety of formats, including HTML and PDF.
- Use LaTeX inside of Org files to produce a scientific paper without all the markup of LaTeX.
- Manage TODO lists and implement a concomitant methodology of labelling task states.
- Quickly shift a “thing” (heading, list item, paragraph, …) further up or down in the file.
- Use tables with formulas as a lightweight alternative to spreadsheet software.
- Capture data or fleeting thoughts efficiently using templates.
- Maintain an agenda for all your date-bound activities.
- Clock in and out of tasks, to eventually track how you are spending your time.
- Link to files regardless of file type. This includes special links
such as to an Info manual or an email, if you also have that running
locally and integrated with Emacs (The
prot-emacs-email.el
module).
In other words, Org is highly capable and widely considered one of the killer apps of Emacs.
This section covers the relevant configurations. You will notice that it is not limited to Org, as some other built-in features are also relevant here.
5.9.1. The prot-emacs-org.el
section on the calendar
The calendar
is technically independent of Org, though it tightly
integrates with it. We witness this when we are setting timestamps,
such as while setting a SCHEDULED
or DEADLINE
entry for a given
heading. All I do here is set some stylistic preferences.
Note that Emacs also has a diary
command. I used it for a while, but
Org is far more capable, so I switched to it completely.
;;; Calendar (use-package calendar :ensure nil :commands (calendar) :config (setq calendar-mark-diary-entries-flag nil) (setq calendar-mark-holidays-flag t) (setq calendar-mode-line-format nil) (setq calendar-time-display-form '( 24-hours ":" minutes (when time-zone (format "(%s)" time-zone)))) (setq calendar-week-start-day 1) ; Monday (setq calendar-date-style 'iso) (setq calendar-time-zone-style 'numeric) ; Emacs 28.1 (require 'solar) (setq calendar-latitude 35.17 ; Not my actual coordinates calendar-longitude 33.36) (require 'cal-dst) (setq calendar-standard-time-zone-name "+0200") (setq calendar-daylight-time-zone-name "+0300"))
5.9.2. The prot-emacs-org.el
section about appointment reminders (appt.el
)
The built in appt.el
defines functionality for handling
notifications about appointments. It is originally designed to work
with the generic diary feature (the M-x diary
one, I mean), which I
do not use anymore, but also integrates nicely with the Org agenda
(The prot-emacs-org.el
Org agenda settings). I deepen this
integration further, such that after adding a task or changing its
state, the appointments mechanism re-reads my data to register new
notifications. This is done via a series of hooks and with the use of
the advice feature of Emacs Lisp.
Here I am setting some simple settings to keep appointment notifations minimal. I do not need them to inform me about the contents of my next entry on the agenda: just show text on the mode line telling me how many minutes are left until the event.
In Org files, every heading can have an APPT_WARNTIME
property: it takes
a numeric value representing minutes for a forewarning from appt.el
.
I use this in tandem with org-capture
for tasks that need to be
done at a specific time, such as coaching sessions (The prot-emacs-org.el
Org capture templates (org-capture
)).
;;; Appt (appointment reminders which also integrate with Org agenda) (use-package appt :ensure nil :commands (appt-activate) :config (setq appt-display-diary nil appt-display-format nil appt-display-mode-line t appt-display-interval 3 appt-audible nil ; TODO 2023-01-25: t does nothing because I disable `ring-bell-function'? appt-warning-time-regexp "appt \\([0-9]+\\)" ; This is for the diary appt-message-warning-time 6) (with-eval-after-load 'org-agenda (appt-activate 1) ;; NOTE 2021-12-07: In my `prot-org.el' (see further below), I add ;; `org-agenda-to-appt' to various relevant hooks. ;; ;; Create reminders for tasks with a due date when this file is read. (org-agenda-to-appt)))
5.9.3. The prot-emacs-org.el
section with basic Org settings
Org, also known as “Org mode”, is one of the potentially most useful feature sets available to every Emacs user. At its core, Org is a lightweight markup language: you can have headings and paragraphs, mark a portion of text with emphasis, produce bullet lists, include code blocks, and the like. Though what really sets Org apart from other markup languages is the rich corpus of Emacs Lisp written around it to do all sorts of tasks with this otherwise plain text format.
With Org you can write technical documents (e.g. the manuals of all my
Emacs packages), maintain a simple or highly sophisticated system for
task management, organise your life using the agenda, write tables
that can evaluate formulas to have spreadsheet functionality, have
embedded LaTeX, evaluate code blocks in a wide range of programming
languages and reuse their results for literate programming, include
the contents of other files into a singular file, use one file to
generate other files/directories with all their contents, and export
the Org document to a variety of formats like .pdf
and .odt
.
Furthermore, Org can be used as a lightweight, plain text database, as
each heading can have its own metadata. This has practical
applications in most of the aforementioned.
In short, if something can be done with plain text, Org probably does it already or has all the elements for piecing it together. This document, among many of my published works, is testament to Org’s sheer power, which I explained at greater length in a video demonstration: Advanced literate configuration with Org (2023-12-18).
This being Emacs, everything is customisable and Org is a good example of this. There are a lot of user options for us to tweak things to our liking. I do as much, though know that Org is perfectly usable without any configuration. The following sections contain further commentary on how I use Org.
;;; Org-mode (personal information manager) (use-package org :ensure nil :init (setq org-directory (expand-file-name "~/Documents/org/")) (setq org-imenu-depth 7) (add-to-list 'safe-local-variable-values '(org-hide-leading-stars . t)) (add-to-list 'safe-local-variable-values '(org-hide-macro-markers . t)) :bind ( :map global-map ("C-c l" . org-store-link) ("C-c o" . org-open-at-point-global) :map org-mode-map ;; I don't like that Org binds one zillion keys, so if I want one ;; for something more important, I disable it from here. ("C-'" . nil) ("C-," . nil) ("M-;" . nil) ("<C-return>" . nil) ("<C-S-return>" . nil) ("C-M-S-<right>" . nil) ("C-M-S-<left>" . nil) ("C-c ;" . nil) ("C-c M-l" . org-insert-last-stored-link) ("C-c C-M-l" . org-toggle-link-display) ("M-." . org-edit-special) ; alias for C-c ' (mnenomic is global M-. that goes to source) :map org-src-mode-map ("M-," . org-edit-src-exit) ; see M-. above :map narrow-map ("b" . org-narrow-to-block) ("e" . org-narrow-to-element) ("s" . org-narrow-to-subtree) :map ctl-x-x-map ("i" . prot-org-id-headlines) ("h" . prot-org-ox-html)) :config ;; My custom extras, which I use for the agenda and a few other Org features. (require 'prot-org) ;;;; general settings (setq org-ellipsis "⮧") (setq org-adapt-indentation nil) ; No, non, nein, όχι! (setq org-special-ctrl-a/e nil) (setq org-special-ctrl-k nil) (setq org-M-RET-may-split-line '((default . nil))) (setq org-hide-emphasis-markers nil) (setq org-hide-macro-markers nil) (setq org-hide-leading-stars nil) (setq org-cycle-separator-lines 0) (setq org-structure-template-alist '(("s" . "src") ("e" . "src emacs-lisp") ("E" . "src emacs-lisp :results value code :lexical t") ("t" . "src emacs-lisp :tangle FILENAME") ("T" . "src emacs-lisp :tangle FILENAME :mkdirp yes") ("x" . "example") ("X" . "export") ("q" . "quote"))) (setq org-fold-catch-invisible-edits 'show) (setq org-return-follows-link nil) (setq org-loop-over-headlines-in-active-region 'start-level) (setq org-modules '(ol-info ol-eww)) (setq org-use-sub-superscripts '{}) (setq org-insert-heading-respect-content t) (setq org-read-date-prefer-future 'time) (setq org-highlight-latex-and-related nil) ; other options affect elisp regexp in src blocks (setq org-fontify-quote-and-verse-blocks t) (setq org-fontify-whole-block-delimiter-line t) (setq org-track-ordered-property-with-tag t) (setq org-highest-priority ?A) (setq org-lowest-priority ?C) (setq org-default-priority ?A) (setq org-priority-faces nil) ;; See my `pulsar' package, defined elsewhere in this setup. (with-eval-after-load 'pulsar (dolist (hook '(org-agenda-after-show-hook org-follow-link-hook)) (add-hook hook #'pulsar-recenter-center) (add-hook hook #'pulsar-reveal-entry))))
5.9.4. The prot-emacs-org.el
Org to-do and refile settings
One of the many use-cases for Org is to maintain a plain text to-do
list. A heading that starts with a to-do keyword, such as TODO
is
treated as a task and its state is considered not completed.
We can switch between the task states with shift and the left or right
arrow keys. Or we can select a keyword directly with C-c C-t
, which
calls org-todo
by default. I personally prefer the latter approach,
as it is more precise.
Whenever a task state changes, we can log that event in a special
LOGBOOK
drawer. This is automatically placed right below the
heading, before any paragraph text. Logging data is an opt-in feature,
which I consider helpful (The prot-emacs-org.el
Org time/state logging).
Tasks can be associated with timestamps, typically a scheduled
date+time or a deadline+time. This can be helpful when we are
reviewing the source Org file, though it really shines in tandem with
the agenda. Any heading that has a timestamp and which belongs to a
file in the org-agenda-files
will show up on the agenda in the given
date (The prot-emacs-org.el
Org agenda settings).
By default, the org-todo-keywords
are TODO
and DONE
. We can
write more keywords if we wish to implement a descriptive workflow.
For example, we can have a WAIT
keyword for something that is to be
done but is not actionable yet. While the number of keywords is not
limited, the binary model is the same: we have words that represent
the incomplete state and those that count as the completion of the
task. For instance, both CANCEL
and DONE
mean that the task is not
actionable anymore and we move on to other things. As such, the extra
keywords are a way for the user to make tasks more descriptive and
easy to find. In the value of the org-todo-keywords
, we use the bar
character to separate the incomplete state to the left from the
completed one to the right.
One of the agenda’s headiline features is the ability to produce a view that lists headings with the given keyword. So having the right terms can make search and retrieval of data more easy. On the flip-side, too many keywords add cognitive load and require more explicit search terms to yield the desired results. I used to work with a more descriptive set of keywords, but ultimately decided to keep things simple.
The refile mechanism is how we can reparent a heading, by moving it
from one place to another. We do this with the command org-refile
,
bound to C-c C-w
by default. A common workflow where refiling is
essential is to have an “inbox” file or heading, where unprocessed
information is stored at, and periodically process its contents to
move the data where it belongs. Though it can also work fine without
any such inbox, in those cases where a heading should be stored
someplace else. The org-refile-targets
specifies the files that are
available when we try to refile the current heading. With how I set it
up, all the agenda files plus the current file’s headings up to level
2 are included as possible targets.
In terms of workflow, I have not done a refile in a very long time,
because my entries always stay in the same place as I had envisaged at
the capture phase (The prot-emacs-org.el
Org capture templates (org-capture
)).
;;;; refile, todo (use-package org :ensure nil :config (setq org-refile-targets '((org-agenda-files . (:maxlevel . 2)) (nil . (:maxlevel . 2)))) (setq org-refile-use-outline-path t) (setq org-refile-allow-creating-parent-nodes 'confirm) (setq org-refile-use-cache t) (setq org-reverse-note-order nil) ;; ;; NOTE 2023-04-07: Leaving this here for demo purposes. ;; (setq org-todo-keywords ;; '((sequence "TODO(t)" "MAYBE(m)" "WAIT(w@/!)" "|" "CANCEL(c@)" "DONE(d!)") ;; (sequence "COACH(k)" "|" "COACHED(K!)"))) (setq org-todo-keywords '((sequence "TODO(t)" "|" "CANCEL(c@)" "DONE(d!)") (sequence "COACH(k)" "|" "COACHED(K!)"))) (defface prot/org-bold-done '((t :inherit (bold org-done))) "Face for bold DONE-type Org keywords.") (setq org-todo-keyword-faces '(("CANCEL" . prot/org-bold-done))) (setq org-use-fast-todo-selection 'expert) (setq org-fontify-done-headline nil) (setq org-fontify-todo-headline nil) (setq org-fontify-whole-heading-line nil) (setq org-enforce-todo-dependencies t) (setq org-enforce-todo-checkbox-dependencies t))
5.9.5. The prot-emacs-org.el
Org heading tags
Each Org heading can have one or more tags associated with it, while
all headings inherit any potential #+FILETAGS
. We can add tags to a
heading when the cursor is over it by typing the ever flexible C-c C-c
.
Though the more specific org-set-tags-command
also gets the job
done, plus it does not require that the cursor is positioned on the
heading text.
Tagging is useful for searching and retrieving the data we store. The Org agenda, in particular, provides commands to filter tasks by tag:
The user option org-tag-alist
lets us specify tags we always want to
use, though we can write tags per file as well by using the #+TAGS
keyword. I do the latter as a global list of tags is not useful in my
case. For example, when I wan checking my coach.org
file for the
coaching sessions I provide, I do not need to see any of the tags that
make sense in my general tasks.org
.
Note that in the settings below I disable the auto-alignment that Org does where it shifts tags to the right of the heading. I do not like it.
;;;; tags (use-package org :ensure nil :config (setq org-tag-alist nil) (setq org-auto-align-tags nil) (setq org-tags-column 0))
5.9.6. The prot-emacs-org.el
Org time/state logging
Org can keep a record of state changes, such as when we set an entry
marked with the TODO
keyword as DONE
or when we reschedule an
appointment (The prot-emacs-org.el
Org agenda settings). This data
is stored in a LOGBOOK
drawer right below the heading. I choose to
keep track of this information, as it is sometimes useful to capture
mistakes or figure out intent in the absence of further clarification
(though I do tend to write why something happened).
;;;; log (use-package org :ensure nil :config (setq org-log-done 'time) (setq org-log-into-drawer t) (setq org-log-note-clock-out nil) (setq org-log-redeadline 'time) (setq org-log-reschedule 'time))
5.9.7. The prot-emacs-org.el
Org link settings
One of the nice things about Org is its flexible linking mechanism. It can produce links to a variety of file types or buffers and even navigate to a section therein.
At its simplest form, we have the file
link type, which points to a
file system path, with an optional extension for a match inside the
file, as documented in the manual. Evaluate this inside of Emacs:
(info "(org) Search Options")
Links to buffers are also common and valuable. For example, we can
have a link to a page produced by the man
command, which gives us
quick access to the documentation of some program. When Org follows
that link, it opens the buffer in the appropriate major mode. For me,
the most common scenario is a link to an email, which I typically
associate with a task that shows up in my agenda:
- The
prot-emacs-org.el
Org capture templates (org-capture
) - The
prot-emacs-notmuch.el
glue code fororg-capture
(ol-notmuch.el
)
Org supports lots of link types out-of-the-box, though more can be
added by packages. My Denote does this: it defines a denote
link
type which behaves the same way as the file
type except that it uses
the identifier of the file instead of its full path (so eve if the
file is renamed, the link will work for as long as the identifier
remains the same).
Links can be generated automatically as part of an org-capture
template. The command org-store-link
produces one manually, storing
it to a special data structure from which it can be retrieved later
for insertion with the command org-insert-link
. The latter command
can also create new links, simply by receiving data that is different
from what was already stored.
I bind org-store-link
in main section of the Org configuration:
The prot-emacs-org.el
section with basic Org settings.
;;;; links (use-package org :ensure nil :config (require 'prot-org) ; for the above commands (setq org-link-context-for-files t) (setq org-link-keep-stored-after-insertion nil) (setq org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id))
5.9.8. The prot-emacs-org.el
Org code block settings
This document benefits from Org’s ability to combine prose with code,
by placing the latter inside of a block that is delimited by
#+BEGIN_SRC
and #+END_SRC
lines.
Code blocks can use the syntax highlighting (“fontification” in Emacs
parlance) of a given major mode. They can also have optional
parameters passed to their header, which expand the capabilities of
the block. For instance, the following code block with my actual
configuration uses the fontification of the emacs-lisp-mode
and has
a :tangle
parameter with a value of a file system path. When I
invoke the command org-babel-tangle
, the contents of this block will
be added to that file, creating the file if necessary.
More generally, Org is capable of evaluating code blocks and passing their return value to other code blocks. It is thus possible to write a fully fledged program as an Org document. This paradigm is known as “literate programming”. In the case of an Emacs configuration, such as mine, it is called a “literate configuration” or variants thereof. I did a video about my setup: Advanced literate configuration with Org (2023-12-18).
Org can evaluate code blocks in many languages. This is known as “Org
Babel” and the files which implement support for a given language are
typically named ob-LANG.el
where LANG
is the name of the language.
We can load the requisite code for the languages we care about with
something like the following:
(require 'ob-python) ;; OR (use-package ob-python) ;; OR for more control (use-package ob-python :after org :config ;; Settings here )
I seldom need to work with Org Babel, so I do not load any language automatically. Note that Emacs Lisp is loaded by default.
To evaluate a code block, we type Org’s omnipotent C-c C-c
. The
results will be produced below the code block. There is an optional
parameter that controls how—or even if—the results are displayed.
There are many other types of block apart from SRC
. Those do
different things, such as:
#+BEGIN_QUOTE
- Treat the contents as a block quote or equivalent.
#+BEGIN_VERSE
- Do not reflow any like breaks (for poetry and such).
#+BEGIN_EXPORT
- Evaluate the code for the given export target
(like
html
orlatex
), optionally replacing it with its results or keeping both of them (Theprot-emacs-org.el
Org export settings).
This is a wonderful world of possibilities!
;;;; code blocks (use-package org :ensure nil :config (setq org-confirm-babel-evaluate nil) (setq org-src-window-setup 'current-window) (setq org-edit-src-persistent-message nil) (setq org-src-fontify-natively t) (setq org-src-preserve-indentation t) (setq org-src-tab-acts-natively t) (setq org-edit-src-content-indentation 0))
5.9.9. The prot-emacs-org.el
Org export settings
Org is a capable authoring tool in no small part because it can be converted to other file formats. A typical example is to write a technical document in Org and then export it to a PDF. Another use-case is what I commonly do with the Emacs packages I maintain, which I export to an Info manual (texinfo format) and an HTML web page.
The default set of export targets is specified in the value of the
user option org-export-backends
. It is one of those rare cases where
it has to be evaluated before the package is loaded. Other than that,
we can load an export backend by finding the correspond ox-FORMAT.el
file and either require
it or load it with use-package
, like what
I showed for Org Babel (The prot-emacs-org.el
Org code block settings).
;;;; export (use-package org :ensure nil :init ;; NOTE 2023-05-20: Must be evaluated before Org is loaded, ;; otherwise we have to use the Custom UI. No thanks! (setq org-export-backends '(html texinfo md)) :config (setq org-export-with-toc t) (setq org-export-headline-levels 8) (setq org-export-dispatch-use-expert-ui nil) (setq org-html-htmlize-output-type nil) (setq org-html-head-include-default-style nil) (setq org-html-head-include-scripts nil))
5.9.10. The prot-emacs-org.el
Org capture templates (org-capture
)
The org-capture
command allows us to quickly store data in some
structured way. This is done with the help of a templating system
where we can, for example, record the date the entry was recorded,
prompt for user input, automatically use the email’s subject as the
title of the task, and the like. The documentation string of
org-capture-templates
covers the technicalities.
I use two Org files for my tasks. The one is tasks.org
, which
contains the bulk of my entries. The other is coach.org
, which is
specific to my coaching work: https://protesilaos.com/coach.
The tasks.org
consists of several top-level headings. Each contains
subheadings I need to review. You will notice how most of my
entries in org-capture-templates
involve this file. With Org, it is
perfectly fine to work in a single file because we can fold headings
or narrow to them with org-narrow-to-subtree
. Furthermore, we can
navigate directly to a heading using minibuffer completion, such as
with the general purpose command prot-search-outline
(The prot-emacs-search.el
extras provided by the prot-search.el
library).
Despite the fact that Org copes well with large files, I still choose
to keep my coaching work in a separate file as a contingency plan.
Because coach.org
includes information about appointments, I need to
be able to read it with ease from anywhere. This includes different
types of hardware, but also any kind of generic text editor or
terminal pager. I do not want to depend on features like folding,
narrowing, and the like, in times when something has gone awry.
Granted, this has never happened, though the idea makes sense.
Besides, two files are not hard to manage in this case. The
coach.org
has a simple structure: each appointment is stored as a
top-level heading.
As for my workflow, here is an overview:
- When I want to capture data that I am not yet sure about, I add it
to the
tasks.org
“Unprocessed” heading. I periodically review those to decide if I want to do something with them or not. If I do not want them, I delete them. Otherwise, I file them under another heading in the same file using theorg-refile
command (Theprot-emacs-org.el
Org to-do and refile settings). Not everything goes into the “Unprocessed” headings, as I often known in advance what an item is about. This is just a fallback for those cases when I need more information to decide on the appropriate action. - Tasks that have an inherent time component are given a
SCHEDULED
orDEADLINE
timestamp (set those on demand with the commandsorg-schedule
andorg-deadline
, respectively). These are the only tasks I want to see on my daily agenda (Theprot-emacs-org.el
Org agenda settings). The difference betweenSCHEDULED
andDEADLINE
is that the former has no strict start or end time and so is flexible, while the latter is more rigid. For example, “visit the vet today” does not have a strict time associated with it because the doctor often deals with emergency situations and thus their agenda is fluid. While a coaching session of mine like “work on Emacs with PERSON” has to start at the agreed upon time. - I do not arbitrarily assign timestamps to tasks. If something does
not have a scheduled date or a deadline, then it does not belong on
the agenda. Otherwise, those arbitrarily defined “events” accumulate
in the agenda and crowd out the actual time-sensitive tasks. As a
result, the cognitive load is heavier and things will not be done.
So when I want to do something at some point, but have no specific
plan for it, I add is to the
tasks.org
“Wishlist”. When I have free time, I review my wishlist and pick something to work on from there depending on my available time and mood. This keeps my workflow both focused and stress-free. - Finally, my
coach.org
only has time-sensitive appointments with aDEADLINE
associated with them. I organise the rest of my activities in the given day based on those.
;;;; capture (use-package org-capture :ensure nil :bind ("C-c c" . org-capture) :config (require 'prot-org) (setq org-capture-templates `(("u" "Unprocessed" entry (file+headline "tasks.org" "Unprocessed") ,(concat "* %^{Title}\n" ":PROPERTIES:\n" ":CAPTURED: %U\n" ":CUSTOM_ID: h:%(format-time-string \"%Y%m%dT%H%M%S\")\n" ":END:\n\n" "%a\n%i%?") :empty-lines-after 1) ("w" "Wishlist" entry (file+olp "tasks.org" "All tasks" "Wishlist") ,(concat "* %^{Title} %^g\n" ":PROPERTIES:\n" ":CAPTURED: %U\n" ":CUSTOM_ID: h:%(format-time-string \"%Y%m%dT%H%M%S\")\n" ":END:\n\n" "%a\n%?") :empty-lines-after 1) ("t" "Task to do" entry (file+headline "tasks.org" "All tasks") ,(concat "* TODO %^{Title} %^g\n" ":PROPERTIES:\n" ":CAPTURED: %U\n" ":CUSTOM_ID: h:%(format-time-string \"%Y%m%dT%H%M%S\")\n" ":END:\n\n" "%a\n%?") :empty-lines-after 1) ("s" "Select file and heading to add to" entry (function prot-org-select-heading-in-file) ,(concat "* TODO %^{Title}%?\n" ":PROPERTIES:\n" ":CAPTURED: %U\n" ":CUSTOM_ID: h:%(format-time-string \"%Y%m%dT%H%M%S\")\n" ":END:\n\n") :empty-lines-after 1) ;; NOTE 2024-11-24: I am not using this, but am keeping it ;; here because the approach is good. ;; ("c" "Clock in and do immediately" entry ;; (file+headline "tasks.org" "Clocked tasks") ;; ,(concat "* TODO %^{Title}\n" ;; ":PROPERTIES:\n" ;; ":EFFORT: %^{Effort estimate in minutes|5|10|15|30|45|60|90|120}\n" ;; ":END:\n\n" ;; "%a\n") ;; :prepend t ;; :clock-in t ;; :clock-keep t ;; :immediate-finish t ;; :empty-lines-after 1) ("p" "Private lesson or service" entry (file "coach.org") #'prot-org-capture-coach :prepend t :empty-lines 1) ("P" "Private service clocked" entry (file+headline "coach.org" "Clocked services") #'prot-org-capture-coach-clock :prepend t :clock-in t :clock-keep t :immediate-finish t :empty-lines 1))) ;; NOTE 2024-11-10: I realised that I was not using this enough, so ;; I decided to simplify my setup. Keeping it here, in case I need ;; it again. ;; (setq org-capture-templates-contexts ;; '(("e" ((in-mode . "notmuch-search-mode") ;; (in-mode . "notmuch-show-mode") ;; (in-mode . "notmuch-tree-mode"))))) )
5.9.11. The prot-emacs-org.el
Org agenda settings
[ Watch: Demo of my custom Org block agenda (2021-12-09). It has changed a bit since then, but the idea is the same. ]
With the Org agenda, we can visualise the tasks we have collected in
our Org files or, more specifically, in the list of files specified in
the user option org-agenda-files
. In my workflow, only the files in
the org-directory
can feed data into the agenda. Though Org provides
commands to add/remove the current file on demand: org-remove-file
,
and org-agenda-file-to-front
. If I ever need to write a task that is
specific to a certain file or buffer, then I use Org’s linking
mechanism to point to the relevant context, but otherwise store my
task in the usual place (The prot-emacs-org.el
Org capture templates (org-capture
)).
By default, Org provides many so-called “views” for the agenda. One of
the them is the daily/weekly agenda. Others show only the headings
with TODO
keywords, or some other kind of search criteria. I
personally never use those views. I have my own custom agenda view,
which consolidates in a single buffer the following blocks on data, in
this order (The prot-org.el
library).:
- Important tasks without a date
- When I add a top priority to something, but there is no inherent deadline to it.
- Pending scheduled tasks
- Tasks with a
SCHEDULED
date may sometimes not be done when they ought to. So they need to be closer to the top for me to do them as soon as I can. - Today’s agenda
- What I am actually working on. Because I only assign a timestamp to tasks that are indeed time-sensitive, this always reflects the commitments I have for the day.
- Next three days
- Like the above, but for the near future.
- Upcoming deadlines (+14d)
- These are the deadlines I need to be aware of for the 14 days after the next three days I am only informed about.
The Org agenda has lots of other extras, such as to filter the view. Though I never use them. My custom agenda does exactly what I need from it and thus keeps me focused.
;;;; agenda (use-package org-agenda :ensure nil :bind ;; I bind `org-agenda' to C-c A, so this one puts me straight into my ;; custom block agenda. ( :map global-map ("C-c A" . org-agenda) ("C-c a" . (lambda () "Call Org agenda with `prot-org-custom-daily-agenda' configuration." (interactive) (org-agenda nil "A")))) :config ;;;;; Custom agenda blocks (setq org-agenda-format-date #'prot-org-agenda-format-date-aligned) ;; Check the variable `prot-org-custom-daily-agenda' in prot-org.el (setq org-agenda-custom-commands `(("A" "Daily agenda and top priority tasks" ,prot-org-custom-daily-agenda ((org-agenda-fontify-priorities nil) (org-agenda-prefix-format " %t %s") (org-agenda-dim-blocked-tasks nil))) ("P" "Plain text daily agenda and top priorities" ,prot-org-custom-daily-agenda ((org-agenda-with-colors nil) (org-agenda-prefix-format "%t %s") (org-agenda-current-time-string ,(car (last org-agenda-time-grid))) (org-agenda-fontify-priorities nil) (org-agenda-remove-tags t)) ("agenda.txt")))) ;;;;; Basic agenda setup (setq org-default-notes-file (make-temp-file "emacs-org-notes-")) ; send it to oblivion (setq org-agenda-files `(,org-directory)) (setq org-agenda-span 'week) (setq org-agenda-start-on-weekday 1) ; Monday (setq org-agenda-confirm-kill t) (setq org-agenda-show-all-dates t) (setq org-agenda-show-outline-path nil) (setq org-agenda-window-setup 'current-window) (setq org-agenda-skip-comment-trees t) (setq org-agenda-menu-show-matcher t) (setq org-agenda-menu-two-columns nil) (setq org-agenda-sticky nil) (setq org-agenda-custom-commands-contexts nil) (setq org-agenda-max-entries nil) (setq org-agenda-max-todos nil) (setq org-agenda-max-tags nil) (setq org-agenda-max-effort nil) ;;;;; General agenda view options ;; NOTE 2021-12-07: Check further below my `org-agenda-custom-commands' (setq org-agenda-prefix-format '((agenda . " %i %-12:c%?-12t% s") (todo . " %i %-12:c") (tags . " %i %-12:c") (search . " %i %-12:c"))) (setq org-agenda-sorting-strategy '(((agenda habit-down time-up priority-down category-keep) (todo priority-down category-keep) (tags priority-down category-keep) (search category-keep)))) (setq org-agenda-breadcrumbs-separator "->") (setq org-agenda-todo-keyword-format "%-1s") (setq org-agenda-fontify-priorities 'cookies) (setq org-agenda-category-icon-alist nil) (setq org-agenda-remove-times-when-in-prefix nil) (setq org-agenda-remove-timeranges-from-blocks nil) (setq org-agenda-compact-blocks nil) (setq org-agenda-block-separator ?—) ;;;;; Agenda marks (setq org-agenda-bulk-mark-char "#") (setq org-agenda-persistent-marks nil) ;;;;; Agenda diary entries (setq org-agenda-insert-diary-strategy 'date-tree) (setq org-agenda-insert-diary-extract-time nil) (setq org-agenda-include-diary nil) ;; I do not want the diary, but there is no way to disable it ;; altogether. This creates a diary file in the /tmp directory. (setq diary-file (make-temp-file "emacs-diary-")) (setq org-agenda-diary-file 'diary-file) ; TODO 2023-05-20: review Org diary substitute ;;;;; Agenda follow mode (setq org-agenda-start-with-follow-mode nil) (setq org-agenda-follow-indirect t) ;;;;; Agenda multi-item tasks (setq org-agenda-dim-blocked-tasks t) (setq org-agenda-todo-list-sublevels t) ;;;;; Agenda filters and restricted views (setq org-agenda-persistent-filter nil) (setq org-agenda-restriction-lock-highlight-subtree t) ;;;;; Agenda items with deadline and scheduled timestamps (setq org-agenda-include-deadlines t) (setq org-deadline-warning-days 0) (setq org-agenda-skip-scheduled-if-done nil) (setq org-agenda-skip-scheduled-if-deadline-is-shown t) (setq org-agenda-skip-timestamp-if-deadline-is-shown t) (setq org-agenda-skip-deadline-if-done nil) (setq org-agenda-skip-deadline-prewarning-if-scheduled 1) (setq org-agenda-skip-scheduled-delay-if-deadline nil) (setq org-agenda-skip-additional-timestamps-same-entry nil) (setq org-agenda-skip-timestamp-if-done nil) (setq org-agenda-search-headline-for-time nil) (setq org-scheduled-past-days 365) (setq org-deadline-past-days 365) (setq org-agenda-move-date-from-past-immediately-to-today t) (setq org-agenda-show-future-repeats t) (setq org-agenda-prefer-last-repeat nil) (setq org-agenda-timerange-leaders '("" "(%d/%d): ")) (setq org-agenda-scheduled-leaders '("Scheduled: " "Sched.%2dx: ")) (setq org-agenda-inactive-leader "[") (setq org-agenda-deadline-leaders '("Deadline: " "In %3d d.: " "%2d d. ago: ")) ;; Time grid (setq org-agenda-time-leading-zero t) (setq org-agenda-timegrid-use-ampm nil) (setq org-agenda-use-time-grid t) (setq org-agenda-show-current-time-in-grid t) (setq org-agenda-current-time-string (concat "Now " (make-string 70 ?.))) (setq org-agenda-time-grid '((daily today require-timed) ( 0500 0600 0700 0800 0900 1000 1100 1200 1300 1400 1500 1600 1700 1800 1900 2000 2100 2200) "" "")) (setq org-agenda-default-appointment-duration nil) ;;;;; Agenda global to-do list (setq org-agenda-todo-ignore-with-date t) (setq org-agenda-todo-ignore-timestamp t) (setq org-agenda-todo-ignore-scheduled t) (setq org-agenda-todo-ignore-deadlines t) (setq org-agenda-todo-ignore-time-comparison-use-seconds t) (setq org-agenda-tags-todo-honor-ignore-options nil) ;;;;; Agenda tagged items (setq org-agenda-show-inherited-tags t) (setq org-agenda-use-tag-inheritance '(todo search agenda)) (setq org-agenda-hide-tags-regexp nil) (setq org-agenda-remove-tags nil) (setq org-agenda-tags-column -100) ;;;;; Agenda entry ;; NOTE: I do not use this right now. Leaving everything to its ;; default value. (setq org-agenda-start-with-entry-text-mode nil) (setq org-agenda-entry-text-maxlines 5) (setq org-agenda-entry-text-exclude-regexps nil) (setq org-agenda-entry-text-leaders " > ") ;;;;; Agenda logging and clocking ;; NOTE: I do not use these yet, though I plan to. Leaving everything ;; to its default value for the time being. (setq org-agenda-log-mode-items '(closed clock)) (setq org-agenda-clock-consistency-checks '((:max-duration "10:00" :min-duration 0 :max-gap "0:05" :gap-ok-around ("4:00") :default-face ; This should definitely be reviewed ((:background "DarkRed") (:foreground "white")) :overlap-face nil :gap-face nil :no-end-time-face nil :long-face nil :short-face nil))) (setq org-agenda-log-mode-add-notes t) (setq org-agenda-start-with-log-mode nil) (setq org-agenda-start-with-clockreport-mode nil) (setq org-agenda-clockreport-parameter-plist '(:link t :maxlevel 2)) (setq org-agenda-search-view-always-boolean nil) (setq org-agenda-search-view-force-full-words nil) (setq org-agenda-search-view-max-outline-level 0) (setq org-agenda-search-headline-for-time t) (setq org-agenda-use-time-grid t) (setq org-agenda-cmp-user-defined nil) (setq org-agenda-sort-notime-is-late t) ; Org 9.4 (setq org-agenda-sort-noeffort-is-high t) ; Org 9.4 ;;;;; Agenda column view ;; NOTE I do not use these, but may need them in the future. (setq org-agenda-view-columns-initially nil) (setq org-agenda-columns-show-summaries t) (setq org-agenda-columns-compute-summary-properties t) (setq org-agenda-columns-add-appointments-to-effort-sum nil) (setq org-agenda-auto-exclude-function nil) (setq org-agenda-bulk-custom-functions nil) ;; ;;;;; Agenda habits ;; (require 'org-habit) ;; (setq org-habit-graph-column 50) ;; (setq org-habit-preceding-days 9) ;; ;; Always show the habit graph, even if there are no habits for ;; ;; today. ;; (setq org-habit-show-all-today t) )
5.9.12. The prot-emacs-org.el
call to provide
Finally, we provide
the module. This is the mirror function of
require
(The init.el final part to load the individual modules).
(provide 'prot-emacs-org)
5.10. The prot-emacs-langs.el
module
This module encompasses a wide range of packages and built-in
configurations that relate to “language” in the wider sense. Settings
here include basic patterns of interaction, such as what the TAB
key
does, to spell checking, code linting, and writing.
5.10.1. The prot-emacs-langs.el
settings for TAB
When I first switched to Emacs, I found the behaviour of the TAB
key
confusing. I was used to it inserting tabs or, at least, spaces.
Whereas in Emacs, TAB
tries to be smarter, depending on the major
mode, as it tries to indent the current line to where it should be
given the context. This works best when we mark a region of text and
hit TAB
there.
If we need to forcefully indent, we can use C-x C-i
(indent-rigidly
).
This command allows us to shift the region left or right using the
arrow keys. A common use-case for me is to paste some text I want to
indent, and then do C-u C-x C-i
, which indents by four spaces the
implicit region. I have explained this in my video about mark and register basics:
https://protesilaos.com/codelog/2023-06-28-emacs-mark-register-basics/.
To the settings I have here, the tab-always-indent
makes the TAB
key assume the dual role of indenting text as well as triggering
completion at point (The prot-emacs-completion.el
for in-buffer completion popup (corfu
)).
When it can perform indentation, it does that, otherwise it starts a
completion loop. The tab-first-completion
determines when not to
complete. Read its doc string for the technicalities.
Finally, the tab-width
and indent-tabs-mode
are about the use of
tabs. I never want them, as I only use spaces. Notice the setq-default
here: we need this form when the buffer’s value automatically becomes
buffer-local. This is very important, otherwise we will be setting the
value only in the Emacs init file, which contradicts our intent. There
are a few cases where setq-default
is needed, so keep this in mind.
;;;; Tabs, indentation, and the TAB key (use-package emacs :ensure nil :demand t :config (setq tab-always-indent 'complete) (setq tab-first-completion 'word-or-paren-or-punct) ; Emacs 27 (setq-default tab-width 4 indent-tabs-mode nil))
5.10.2. The prot-emacs-langs.el
settings for “electric” behaviour
Emacs describes as “electric” any behaviour that tries to be smart
about how to handle a given action. The electric-pair-mode
, for
example, automatically inserts a closing parenthesis when the user
inputs an opening parenthesis. Same idea with quotes, performed by the
electric-quote-mode
. While the electric-indent-mode
tries to be
smart about how to indent a line, which is fine for programming
purposes, it makes a mess of things in Org and related because you
have to delete back to the beginning of a line if you want to “escape”
from the indentation of a list or something.
If I need to manually wrap the symbol at point or a region in a pair
of characters, I use my prot-pair-insert
command (The prot-emacs-essentials.el
section about prot-pair.el
(insert character pairs)).
;;;; Disable "electric" behaviour (use-package electric :ensure nil :hook (prog-mode . electric-indent-local-mode) :config ;; I don't like auto indents in Org and related. They are okay for ;; programming. (electric-pair-mode -1) (electric-quote-mode -1) (electric-indent-mode -1))
5.10.3. The prot-emacs-langs.el
settings show-paren-mode
The built-in show-paren-mode
highlights the parenthesis on the
opposite end of the current symbolic expression. It also highlights
matching terms of control flow in programming languages that are not
using parentheses like Lisp: for instance, in a bash
shell script it
highlights the if
and fi
keywords. This mode also works for prose
and I use it globally. Simple and effective!
;;;; Parentheses (show-paren-mode) (use-package paren :ensure nil :hook (prog-mode . show-paren-local-mode) :config (setq show-paren-style 'parenthesis) (setq show-paren-when-point-in-periphery nil) (setq show-paren-when-point-inside-paren nil) (setq show-paren-context-when-offscreen 'overlay)) ; Emacs 29
5.10.4. The prot-emacs-langs.el
settings for plain text (no double spaces, auto-fill-mode
)
These are some basic settings for plain text files but also for any
major mode that inherits from text-mode
(like Org and Markdown). For
a long time, I was using double spaces after a sentence, as this is
the Emacs default. I don’t have a strong preference for or against it,
though I reverted to the single space convention as it is the norm
nowadays.
The technical benefit of double spaces is that it makes sentence navigation less ambiguous as you do not get false positives like “Dr.”. Though I realised I seldom use such language so why type more spaces for a theoretical advantage?
I still need to use double spaces for Elisp programming, otherwise the byte compiler produces warnings. It is annoyingly pedantic, but here we are…
The other setting that matters here is the use of “auto fill”, else
the auto-fill-mode
. This is about the automatic break of long lines
so that they wrap at the fill-column
length: it happens as you type.
This way, a paragraph is not a single long line, but several shorter
lines with newline characters between them. I find this much more
pleasant to work with than to have to rely on visual-line-mode
to
visually wrap long lines. I want my text to be readable even if I do
not use Emacs (e.g. if I use cat
or less
on the command-line).
Auto-filled text does not affect published prose, as the relevant
programs strip away the newlines inside a paragraph.
To manually fill a region of text, mark it and type M-q
. Or do M-q
to operate on the current paragraph without marking it. Depending on
the major mode you are in, this key binding calls a different command.
The generic one is fill-paragraph
. I use M-Q
to “unfill” text,
which corresponds to the prot-simple-unfill-region-or-paragraph
command (The prot-emacs-essentials.el
section about prot-simple.el
(custom basic commands)).
Finally, notice how I am adding an association to the auto-mode-alist
.
The file names specified in that regular expression will be using
text-mode
when I visit them.
;;;; Plain text (text-mode) (use-package text-mode :ensure nil :mode "\\`\\(README\\|CHANGELOG\\|COPYING\\|LICENSE\\)\\'" :hook ((text-mode . turn-on-auto-fill) (prog-mode . (lambda () (setq-local sentence-end-double-space t)))) :config (setq sentence-end-double-space nil) (setq sentence-end-without-period nil) (setq colon-double-space nil) (setq use-hard-newlines nil) (setq adaptive-fill-mode t))
5.10.5. The prot-emacs-langs.el
settings for common file types
As I explained above about auto-mode-alist
(The prot-emacs-langs.el
settings for plain text),
this is how we tell Emacs what major mode to use for files that match
the given regular expression. The PKGBUILD
is for Arch Linux package
recipes, by the way.
;;;; Arch Linux and AUR package scripts (sh-mode) (use-package sh-script :ensure nil :mode ("PKGBUILD" . sh-mode)) ;;;; SystemD and other configuration files (conf-mode) (use-package conf-mode :ensure nil :mode ("\\`dircolors\\'" "\\.\\(service\\|timer\\)\\'"))
5.10.6. The prot-emacs-langs.el
settings for eldoc
The built-in eldoc
feature is especially useful in programming
modes. While we are in a function call, it produces an indicator in
the echo area (where the minibuffer appears upon invocation) that
shows the name of the function, the arguments it takes, if any, and
highlights the current argument we are positioned at. This way, we do
not have to go back to review the signature of the function just to
remember its arity. Same principle for variables, where eldoc-mode
puts the first line of their documentation string in the echo area.
Of course, this works out-of-the-box for Emacs Lisp code. Other modes need to arrange how to use this feature.
;;;; Eldoc (Emacs live documentation feedback) (use-package eldoc :ensure nil :hook (prog-mode . eldoc-mode) :config (setq eldoc-message-function #'message)) ; don't use mode line for M-x eval-expression, etc.
5.10.7. The prot-emacs-langs.el
settings for eglot
(LSP client)
The built-in eglot
feature, developed and maintained by João Távora,
is Emacs’ own client for the Language Server Protocol (LSP). The LSP
technology is all about enhancing the ability of a text editor to work
with a given programming language. This works by installing a
so-called “language server” on your computer, which the “LSP client”
(i.e. eglot
) will plug into. A typical language server provides the
following capabilities:
- Code completion
- This can be visualised for in-buffer
automatic expansion of function calls, variables, and the like
(The
prot-emacs-completion.el
for in-buffer completion popup (corfu
)). - Code linting
- To display suggestions, warnings, or errors. These
are highlighted in the buffer, usually with an underline, and can
also be displayed in a standalone buffer with the commands
flymake-show-buffer-diagnostics
,flymake-show-project-diagnostics
(Theprot-emacs-langs.el
settings for code linting (flymake
)). - Code navigation and cross-referencing
While over a symbol, use a command to jump directly to its definition. The default key bindings for going forth and then back are
M-.
(xref-find-definitions
) andM-,
(xref-go-back
).[ Features such as the definition of the outline should, in principle, be implemented by the major mode though I see no reason why a language server cannot also be involved in this task. You can use the built-in
outline-minor-mode
to provide Org-like folding capabilties for outline headings (Theprot-emacs-langs.el
settings foroutline-minor-mode
). I usually navigate the outline using minibuffer completion, with the help of myprot-search-outline
command (Theprot-emacs-search.el
extras provided by theprot-search.el
library). ]
Assuming the language server is installed, to start using the LSP
client in a given file, do M-x eglot
. To make this happen
automatically for every newly visited file, add a hook like this:
(add-hook 'SOME-MAJOR-mode #'eglot-ensure)
I only code in Emacs Lisp, so I am actually not using eglot
anywhere. Though I have tried it in Bash and JavaScript files and it
worked fine.
;;;; Eglot (built-in client for the language server protocol) (use-package eglot :ensure nil :functions (eglot-ensure) :commands (eglot) :config (setq eglot-sync-connect nil) (setq eglot-autoshutdown t))
5.10.8. The prot-emacs-langs.el
settings for very long lines
Emacs is notoriously bad at performing well when parsing long lines.
I believe this has to do with how syntax highlighting and code
navigation are implemented. The global-so-long-mode
tries to
mitigate this problem by disabling such extras when we visit a file
with really long lines, such as minified JavaScript. I have not used
it enough to know how reliable this is, though it is nice to have just
in case.
;;;; Handle performance for very long lines (so-long.el) (use-package so-long :ensure nil :hook (after-init . global-so-long-mode))
5.10.9. The prot-emacs-langs.el
settings for markdown-mode
The markdown-mode
lets us edit Markdown files. We get syntax
highlighting and several extras, such as the folding of headings and
navigation between them. The mode actually provides lots of added
functionality for GitHub-flavoured Markdown and to preview a Markdown
file’s HTML representation on a web page. Though I only use it for
basic text editing.
;;; Markdown (markdown-mode) (use-package markdown-mode :ensure t :defer t :config (setq markdown-fontify-code-blocks-natively t))
5.10.10. The prot-emacs-langs.el
settings for csv-mode
The package csv-mode
provides support for .csv
files. I do need
this on occasion, even though my use-case is pretty basic. For me, the
killer feature is the ability to create a virtual tabulated listing
with the command csv-align-mode
: it hides the field delimiter (comma
or space) and shows a tab stop in its stead.
;;; csv-mode (use-package csv-mode :ensure t :commands (csv-align-mode))
5.10.11. The prot-emacs-langs.el
settings for sxhkdrc-mode
This is a major mode for editing sxhkdrc
files. SXHKD is the Simple
X Hot Key Daemon which is commonly used in minimalist desktop sessions
on Xorg, such as with the Binary Space Partitioning Window Manager
(BSPWM). The sxhkdrc
file configures key chords, binding them to
commands. For the technicalities, read the man page sxhkd(1)
.
- Package name (GNU ELPA):
sxhkdrc-mode
- Git repositories:
- Backronym: Such Xenotropic Hot Keys Demonstrate Robustness and Configurability … mode.
;;; SXHKDRC mode (one of my many packages) (use-package sxhkdrc-mode :ensure t ;; By default, it only applies to the sxhkdrc file, but I have other ;; relevant entries as well. I separate my keys into different ;; modules and load only what I need. :mode "sxhkdrc_.*")
5.10.12. The prot-emacs-langs.el
settings for spell checking
For spell checking on-demand, I rely on the built-in flyspell
feature that I complement with some small extras (The prot-spell.el
library).
In terms of workflow, I do not like to see any spell checking while I
type. I prefer to write out the entire draft and then do a spell check
at the end. Whatever typos do not bother me (and I have commented many
times before about my “alla prima” approach to creativity). It is
harder to set up multilingual spell checking with flyspell
and I do
not even bother trying as I seldom have that need.
;;; Flyspell and prot-spell.el (spell check) (use-package flyspell :ensure nil :bind ( :map flyspell-mode-map ("C-;" . nil) :map flyspell-mouse-map ("<mouse-3>" . flyspell-correct-word) :map ctl-x-x-map ("s" . flyspell-mode)) ; C-x x s :config (setq flyspell-issue-message-flag nil) (setq flyspell-issue-welcome-flag nil) (setq ispell-program-name "aspell") (setq ispell-dictionary "en_GB")) (use-package prot-spell :ensure nil :bind (("M-$" . prot-spell-spell-dwim) ("C-M-$" . prot-spell-change-dictionary) ("M-i" . prot-spell-spell-dwim) ; override `tab-to-tab-stop' ("C-M-i" . prot-spell-change-dictionary)) ; override `complete-symbol' :config (setq prot-spell-dictionaries '(("EN English" . "en") ("EL Ελληνικά" . "el") ("FR Français" . "fr") ("ES Espanõl" . "es"))) ;; Also check prot-spell.el for what I am doing with ;; `prot-spell-ispell-display-buffer'. Then refer to the ;; `display-buffer-alist' for the relevant entry. (setq ispell-choices-buffer "*ispell-top-choices*"))
5.10.13. The prot-emacs-langs.el
settings for code linting (flymake
)
The built-in flymake
feature defines an interface for viewing the
output of linter programs. A “linter” parses a file and reports
possible notes/warnings/errors in it. With flymake
we get these
diagnostics in the form of a standalone buffer as well as inline
highlights (typically underlines combined with fringe indicators) for
the portion of text in question. The linter report is displayed with
the command flymake-show-buffer-diagnostics
, or flymake-show-project-diagnostics
.
Highlights are shown in the context of the file.
The built-in eglot
feature uses flymake
internally to handle the
LSP linter output (The prot-emacs-langs.el
settings for eglot
).
As for what I have in this configuration block, the essentials for me
are the user options flymake-start-on-save-buffer
and flymake-start-on-flymake-mode
as they make the linter update its report when the buffer is saved and
when flymake-mode
is started, respectively. Otherwise, we have to
run it manually, which is cumbersome.
The package-lint-flymake
package by Steve Purcell adds the glue code
to make flymake
report issues with Emacs Lisp files for the purposes
of packaging. I use it whenever I work on my numerous Emacs packages.
;;; Flymake (use-package flymake :ensure nil :preface (defvar prot/flymake-mode-projects-path (file-name-as-directory (expand-file-name "Projects" "~/Git/")) "Path to my Git projects.") (defun prot/flymake-mode-lexical-binding () (when lexical-binding (flymake-mode 1))) (defun prot/flymake-mode-in-my-projects () (when-let* ((file (buffer-file-name)) ((string-prefix-p prot/flymake-mode-projects-path (expand-file-name file))) ((not (file-directory-p file))) ((file-regular-p file))) (add-hook 'find-file-hook #'prot/flymake-mode-lexical-binding nil t))) (add-hook 'emacs-lisp-mode-hook #'prot/flymake-mode-in-my-projects) :bind ( :map ctl-x-x-map ("m" . flymake-mode) ; C-x x m :map flymake-mode-map ("C-c ! s" . flymake-start) ("C-c ! d" . flymake-show-buffer-diagnostics) ; Emacs28 ("C-c ! D" . flymake-show-project-diagnostics) ; Emacs28 ("C-c ! n" . flymake-goto-next-error) ("C-c ! p" . flymake-goto-prev-error)) :config (setq flymake-fringe-indicator-position 'left-fringe) (setq flymake-suppress-zero-counters t) (setq flymake-no-changes-timeout nil) (setq flymake-start-on-flymake-mode t) (setq flymake-start-on-save-buffer t) (setq flymake-proc-compilation-prevents-syntax-check t) (setq flymake-wrap-around nil) (setq flymake-mode-line-format '("" flymake-mode-line-exception flymake-mode-line-counters)) ;; NOTE 2023-07-03: `prot-modeline.el' actually defines the counters ;; itself and ignores this. (setq flymake-mode-line-counter-format '("" flymake-mode-line-error-counter flymake-mode-line-warning-counter flymake-mode-line-note-counter "")) (setq flymake-show-diagnostics-at-end-of-line nil)) ; Emacs 30 ;;; Elisp packaging requirements (use-package package-lint-flymake :ensure t :after flymake :config (add-hook 'flymake-diagnostic-functions #'package-lint-flymake))
5.10.14. The prot-emacs-langs.el
settings for outline-minor-mode
The built-in outline-minor-mode
defines folding and navigation
commands for the file’s outline. The “outline” is the document’s
discernible structure, defined by the local value of the variable
outline-regexp
. Simply put, it is about the headings in the
document.
Any file can have its own outline. For example, in Emacs Lisp any
comment with three or more delimiters that starts at the beginning of
the line counts as a heading. Three delimiters make it a level 1
heading; four delimiters for level 2, and so on. You will notice that
I already use outlines in all my files. Sometimes I enable the
outline-minor-mode
, though I do not really need the folding
capabilities. Plus, I can navigate the file using minibuffer
completion among outline entries without enabling
outline-minor-mode
, courtesy of the command prot-search-outline
(The prot-emacs-search.el
extras provided by the prot-search.el
library).
A common question/remark about my literate configuration with Org is
why not use outline-minor-mode
or the external outshine
package to
get “the same features” without Org. The feature sets are not really
comparable. With Org we can comment at length without putting all that
in the actual code, whereas with outline-minor-mode
you would be
reading all this commentary in the source code: now you only read it
in this Org document and its website counterpart. Furthermore, we have
links between headings, a convenient export mechanism to a website,
and the ability to easily redirect where the code blocks are tangled
to. On the latter point, if, for instance, I ever choose to
consolidate all my Emacs setup in a monolithic init.el
, I just run a
quick replace for all the :tangle
values in this document. Finally,
we have to consider the use-case: if you have a private config, then
outline-minor-mode
may be enough. In my case, I maintain a massive
document which, I think, can be helpful for others in the community as
well.
[ Also read: Why use Org when you can have an outline in Elisp? ]
;;; General configurations for prose/writing ;;;; `outline' (`outline-mode' and `outline-minor-mode') (use-package outline :ensure nil :bind ("<f10>" . outline-minor-mode) :config (setq outline-minor-mode-highlight nil) ; emacs28 (setq outline-minor-mode-cycle t) ; emacs28 (setq outline-minor-mode-use-buttons nil) ; emacs29---bless you for the nil option! (setq outline-minor-mode-use-margins nil)) ; as above
5.10.15. The prot-emacs-langs.el
settings for dictionary
The built-in dictionary
feature lets us access a webpage (or local
server) to read dictionary entries. The command dictionary-search
is
also smart enough to use the word at point as the default value, so we
can type RET
at the minibuffer prompt to select it without typing it
out. A neat package overall!
;;;; `dictionary' (use-package dictionary :ensure nil :bind ("C-c d" . dictionary-search) :config (setq dictionary-server "dict.org" dictionary-default-popup-strategy "lev" ; read doc string dictionary-create-buttons nil dictionary-use-single-buffer t))
5.10.16. The prot-emacs-langs.el
settings for altcaps
(alternating letter casing)
What follows is another package of mine. I wrote it to practice some programming concepts, though I genuinely find it useful. How else are we supposed to mock people when they say wHy WoN’t YoU sPoOnFeEd Me AlL tHe AnSwErS?
Technically, the altcaps
package is a small, focused-in-scope tool
that helps users communicate mockery or sarcasm effectively. It does
this by alternating the letter casing of characters in the words it
affects.
- Package name (GNU ELPA):
altcaps
- Official manual: https://protesilaos.com/emacs/altcaps
- Change log: https://protesilaos.com/emacs/altcaps-changelog
- Git repositories:
- Backronyms: Alternating Letters Transform Casual Asides to Playful Statements. ALTCAPS Lets Trolls Convert Aphorisms to Proper Shitposts.
;;; aLtCaPs ;; Read the manual: <https://protesilaos.com/emacs/altcaps>. (use-package altcaps :ensure t :bind ("C-x C-a" . altcaps-dwim) :config ;; Force letter casing for certain characters (for legibility). (setq altcaps-force-character-casing '(;; Greek theta (?θ . downcase))))
5.10.17. The prot-emacs-langs.el
settings for denote
(notes and file-naming)
This is another one of my packages and is extended by my
consult-denote
package (The prot-emacs-langs.el
integration between Consult and Denote (consult-denote
)).
Denote is a simple note-taking tool for Emacs. It is based on the idea that notes should follow a predictable and descriptive file-naming scheme. The file name must offer a clear indication of what the note is about, without reference to any other metadata. Denote basically streamlines the creation of such files while providing facilities to link between them.
Denote’s file-naming scheme is not limited to “notes”. It can be used for all types of file, including those that are not editable in Emacs, such as videos. Naming files in a consistent way makes their filtering and retrieval considerably easier. Denote provides relevant facilities to rename files, regardless of file type.
- Package name (GNU ELPA):
denote
- Official manual: https://protesilaos.com/emacs/denote
- Change log: https://protesilaos.com/emacs/denote-changelog
- Git repositories:
- Video demo: https://protesilaos.com/codelog/2022-06-18-denote-demo/
- Backronyms: Denote Everything Neatly; Omit The Excesses. Don’t Ever Note Only The Epiphenomenal.
;;; Denote (simple note-taking and file-naming) ;; Read the manual: <https://protesilaos.com/emacs/denote>. This does ;; not include all the useful features of Denote. I have a separate ;; private setup for those, as I need to test everything is in order. (use-package denote :ensure t :hook ;; If you use Markdown or plain text files you want to fontify links ;; upon visiting the file (Org renders links as buttons right away). ((text-mode . denote-fontify-links-mode-maybe) ;; Highlight Denote file names in Dired buffers. Below is the ;; generic approach, which is great if you rename files Denote-style ;; in lots of places as I do. ;; ;; If you only want the `denote-dired-mode' in select directories, ;; then modify the variable `denote-dired-directories' and use the ;; following instead: ;; ;; (dired-mode . denote-dired-mode-in-directories) (dired-mode . denote-dired-mode)) :bind ;; Denote DOES NOT define any key bindings. This is for the user to ;; decide. Here I only have a subset of what Denote offers. ( :map global-map ("C-c n n" . denote) ("C-c n N" . denote-type) ("C-c n o" . denote-sort-dired) ; "order" mnemonic ;; Note that `denote-rename-file' can work from any context, not ;; just Dired buffers. That is why we bind it here to the ;; `global-map'. ;; ;; Also see `denote-rename-file-using-front-matter' further below. ("C-c n r" . denote-rename-file) ;; If you intend to use Denote with a variety of file types, it is ;; easier to bind the link-related commands to the `global-map', as ;; shown here. Otherwise follow the same pattern for ;; `org-mode-map', `markdown-mode-map', and/or `text-mode-map'. :map text-mode-map ("C-c n i" . denote-link) ; "insert" mnemonic ("C-c n I" . denote-add-links) ("C-c n b" . denote-backlinks) ;; Also see `denote-rename-file' further above. ("C-c n R" . denote-rename-file-using-front-matter) :map org-mode-map ("C-c n d l" . denote-org-extras-dblock-insert-links) ("C-c n d b" . denote-org-extras-dblock-insert-backlinks) ;; Key bindings specifically for Dired. :map dired-mode-map ("C-c C-d C-i" . denote-dired-link-marked-notes) ("C-c C-d C-r" . denote-dired-rename-marked-files) ("C-c C-d C-k" . denote-dired-rename-marked-files-with-keywords) ("C-c C-d C-f" . denote-dired-rename-marked-files-using-front-matter)) :config ;; Remember to check the doc strings of those variables. (setq denote-directory (expand-file-name "~/Documents/notes/")) (setq denote-file-type 'text) ; Org is the default file type ;; If you want to have a "controlled vocabulary" of keywords, ;; meaning that you only use a predefined set of them, then you want ;; `denote-infer-keywords' to be nil and `denote-known-keywords' to ;; have the keywords you need. (setq denote-known-keywords '("emacs" "philosophy" "politics" "economics")) (setq denote-infer-keywords t) (setq denote-sort-keywords t) (setq denote-excluded-directories-regexp nil) (setq denote-date-format nil) ; read its doc string (setq denote-rename-confirmations nil) ; CAREFUL with this if you are not familiar with Denote! (setq denote-backlinks-show-context nil) (setq denote-rename-buffer-format "[D] %t%b") (setq denote-buffer-has-backlinks-string " (<--->)") ;; Automatically rename Denote buffers when opening them so that ;; instead of their long file name they have a literal "[D]" ;; followed by the file's title. Read the doc string of ;; `denote-rename-buffer-format' for how to modify this. (denote-rename-buffer-mode 1))
5.10.17.1. The prot-emacs-langs.el
integration between Consult and Denote (consult-denote
)
This is another package of mine which extends my denote
package
(The prot-emacs-langs.el
settings for denote
(notes and file-naming)).
This is glue code to integrate denote
with Daniel Mendler’s
consult
(The prot-emacs-completion.el
settings for consult
). The
idea is to enhance minibuffer interactions, such as by providing a
preview of the file-to-linked/opened and by adding more sources to the
consult-buffer
command.
- Package name (GNU ELPA):
consult-denote
- Official manual: not available yet.
- Change log: not available yet.
- Git repositories:
- Backronym: Consult-Orchestrated Navigation and Selection of Unambiguous Targets…denote.
(when prot-emacs-completion-extras (use-package consult-denote :ensure t :bind (("C-c n f" . consult-denote-find) ("C-c n g" . consult-denote-grep)) :config (consult-denote-mode 1)))
5.10.18. The prot-emacs-langs.el
settings for logos
(writing extras and buffer navigation)
This package provides a simple approach to setting up a “focus mode”.
It uses the page-delimiter
(typically ^L
) or the outline together
with some commands to move between pages whether narrowing is in effect
or not. It also provides some optional aesthetic tweaks which come into
effect when the buffer-local logos-focus-mode
is enabled. The manual
shows how to extend the code to achieve the desired result.
I use logos
to do video presentations that involve “slides”. Each
heading/section becomes its own “slide” simply by narrowing to it.
- Package name (GNU ELPA):
logos
- Official manual: https://protesilaos.com/emacs/logos
- Change log: https://protesilaos.com/emacs/logos-changelog
- Git repositories:
- Video demo: https://protesilaos.com/codelog/2022-03-11-emacs-logos-demo/
- Backronyms:
^L
Only Generates Ostensible Slides; Logos Optionally Goes through Outline Sections
;;; Custom extensions for "focus mode" (logos.el) ;; Read the manual: <https://protesilaos.com/emacs/logos>. (use-package olivetti :ensure t :commands (olivetti-mode) :config (setq olivetti-body-width 0.7) (setq olivetti-minimum-body-width 80) (setq olivetti-recall-visual-line-mode-entry-state t)) (use-package logos :ensure t :bind (("C-x n n" . logos-narrow-dwim) ("C-x ]" . logos-forward-page-dwim) ("C-x [" . logos-backward-page-dwim) ;; I don't think I ever saw a package bind M-] or M-[... ("M-]" . logos-forward-page-dwim) ("M-[" . logos-backward-page-dwim) ("<f9>" . logos-focus-mode)) :config (setq logos-outlines-are-pages t) (setq logos-outline-regexp-alist `((emacs-lisp-mode . ,(format "\\(^;;;+ \\|%s\\)" logos-page-delimiter)) (org-mode . ,(format "\\(^\\*+ +\\|^-\\{5\\}$\\|%s\\)" logos-page-delimiter)) (markdown-mode . ,(format "\\(^\\#+ +\\|^[*-]\\{5\\}$\\|^\\* \\* \\*$\\|%s\\)" logos-page-delimiter)) (conf-toml-mode . "^\\["))) ;; These apply when `logos-focus-mode' is enabled. Their value is ;; buffer-local. (setq-default logos-hide-mode-line t) (setq-default logos-hide-header-line t) (setq-default logos-hide-buffer-boundaries t) (setq-default logos-hide-fringe t) (setq-default logos-variable-pitch t) ; see my `fontaine' configurations (setq-default logos-buffer-read-only nil) (setq-default logos-scroll-lock nil) (setq-default logos-olivetti t) (add-hook 'enable-theme-functions #'logos-update-fringe-in-buffers) ;;;; Extra tweaks ;; place point at the top when changing pages, but not in `prog-mode' (defun prot/logos--recenter-top () "Use `recenter' to reposition the view at the top." (unless (derived-mode-p 'prog-mode) (recenter 1))) ; Use 0 for the absolute top (add-hook 'logos-page-motion-hook #'prot/logos--recenter-top))
(provide 'prot-emacs-langs)
5.11. The prot-emacs-email.el
module
[ Also see: Overview of my email setup (mbsync
, msmtp
, mail indexer, and MUA). ]
Email inside of Emacs is one of the best changes I have ever made to
my workflow. I consider it far better than the mutt
setup I once
had. The benefits are down to the interconnectedness of the Emacs
environment (watch: Why Emacs itself is my “favourite Emacs package”
(2020-10-21)). All text editing capabilities are available. Same for
buffer navigation. Same for themes and fonts. Then we have integration
with org-capture
to quickly produce a task that shows up on the Org
agenda and has a link back to the original email (The prot-emacs-org.el
Org capture templates).
And there is also the seamless connection between Emacs and GPG, so any
encrypted file/email is decrypted behind the scenes with us
experiencing it as every other regular file. Fantastic stuff!
5.11.1. The prot-emacs-email.el
basic settings (including authinfo
)
Emacs reads login credentials from the list of files specified in the
user option auth-sources
. I set it to a single GPG-encrypted entry.
The contents of that file look like this:
machine pub port 993 login SOME-USER-NAME-HERE-1 password SOME-PASSWORD-HERE machine inf port 993 login SOME-USER-NAME-HERE-2 password SOME-PASSWORD-HERE machine smtp.some-server.com port 465 login SOME-USER-NAME-HERE-1 password SOME-PASSWORD-HERE machine mail.other-server.com port 465 login SOME-USER-NAME-HERE-2 password SOME-PASSWORD-HERE
Each line can be read as a map of key-value pairs. Think of it like this:
machine pub port 993 login SOME-USER-NAME-HERE-1 password SOME-PASSWORD-HERE-1
Depending on the settings and the applicable program, Emacs reads this
file to establish a connection. This is not limited to emails, mind
you, as we can have credentials such as for running sudo
via TRAMP
or logging in to IRC with the rcirc
command (The prot-emacs-web.el
module).
For my email purposes, Emacs does not read from the auth-sources
,
though I do keep this in place for other programs to read from. These
programs include mbsync
and msmtp
(How I use email in Emacs).
Other than that, all I do in this snippet is set the default name and email, which are consulted by miscellaneous pieces Emacs functionality.
;;;; File with authentication credentials (`auth-source') (use-package auth-source :ensure nil :defer t :config (setq auth-sources '("~/.authinfo.gpg") user-full-name "Protesilaos Stavrou" user-mail-address "public@protesilaos.com"))
5.11.2. The prot-emacs-email.el
message composition and encryption settings (message.el
)
Across all Emacs email clients that I have used (gnus
, mu4e
, and
notmuch
) message composition is handled by the same built-in library:
message.el
. It produces a buffer with email headers at the top,
followed by a separator line, and then the body of the message where
we write what we want. The body can have a signature automatically
appended to it.
Message buffers can integrate with the system’s GPG agent to encrypt
the current message. This is done by inserting a special MIME tag at
the top of the message body. I do it by typing C-c C-m C-e
, which
invokes the command mml-secure-message-sign-encrypt
. If the GPG
agent is running and the password is already saved in the keyring, the
message is sent encrypted without any further prompts (Emacs generally
treats encrypted files transparently if everything is set up at the
environment level).
When replying to emails I receive, I normally comment inline by citing the original message. Concretely, this looks like this:
> Some text I am commenting on. My comment on it.
The settings I have for citing messages ensure that the top of the
message includes the From
and Date
headers, so the original email
I am replying to looks like this in my message composition buffer:
> From: Some Name <name@domain.tld> > Date: Tue, 9 Jan 2024 06:58:38 +0200 > > Some text I am commenting on. My comment on it.
Check the documentation of the format-time-string
function to
understand how the date is defined in the user option message-citation-line-format
.
When citing a portion of text, I do not need to include the entirety
of it, but also the parts that are pertinent to the matter at hand. I
thus frequently elide text by marking it and then typing C-c C-e
(message-elide-region
). The message-elide-ellipsis
I specify here
turns the region into a cited snippet that references the line count, like:
> From: Some Name <name@domain.tld> > Date: Tue, 9 Jan 2024 06:58:38 +0200 > > Some text I am commenting on. My comment on it. > Something that shows lots of details I do not need to keep in place: > > [... 20 lines elided] Another comment of mine here.
To attach a file, type C-c C-a
(mml-attach-file
). This uses
minibuffer completion to select a single file. An alternative is to
use dired
to produce a file listing, mark the relevant files, and
attach them from there (The prot-emacs-email.el
integration with Dired for email attachments (gnus-dired-mode
))
Once the message is done, type C-c C-c
to send it. The exact command
depends on the mail user agent. In my case, it is notmuch-mua-send-and-exit
.
By default, Emacs prompts for confirmation before sending out the
message. I disable that by modifying message-confirm-send
.
Similarly, I prefer to delete the buffer of a sent message, so I
change the value of message-kill-buffer-on-exit
.
Between frictionless encryption and the ease of editing text in a
message composition buffer, the email setup I have in Emacs is the
most potent I have ever used (Overview of my email setup (mbsync
, msmtp
, mail indexer, and MUA)).
;;;; Encryption settings (`mm-encode' and `mml-sec') (use-package mm-encode :ensure nil :defer t :config (setq mm-encrypt-option nil ; use 'guided for both if you need more control mm-sign-option nil)) (use-package mml-sec :ensure nil :defer t :config (setq mml-secure-openpgp-encrypt-to-self t mml-secure-openpgp-sign-with-sender t mml-secure-smime-encrypt-to-self t mml-secure-smime-sign-with-sender t)) ;;;; Message composition (`message') (use-package message :ensure nil :defer t :hook (message-setup . message-sort-headers) :config (setq mail-user-agent 'message-user-agent message-mail-user-agent t) ; use `mail-user-agent' (setq mail-header-separator "--text follows this line--") (setq message-elide-ellipsis "\n> [... %l lines elided]\n") (setq compose-mail-user-agent-warnings nil) (setq message-signature "Protesilaos Stavrou\nhttps://protesilaos.com\n" mail-signature message-signature) (setq message-citation-line-function #'message-insert-formatted-citation-line) (setq message-citation-line-format (concat "> From: %f\n" "> Date: %a, %e %b %Y %T %z\n" ">") message-ignored-cited-headers "") ; default is "." for all headers (setq message-confirm-send nil) (setq message-kill-buffer-on-exit t) ;; (add-to-list 'mm-body-charset-encoding-alist '(utf-8 . base64)) (setq message-wide-reply-confirm-recipients nil))
5.11.3. The prot-emacs-email.el
integration with Dired for email attachments (gnus-dired-mode
)
The whole point of using Emacs is to draw linkages between different
specialised tools. One such case is to use the built-in file manager
to select some files and attach them to the currently open message
composition buffer (The prot-emacs-dired.el
module). Do it by typing
C-c C-m C-a
(gnus-dired-attach
). This also works without an open
message composition buffer. In that case, it produces such a buffer,
with the attachments in place. Though I usually have the message buffer
in place before going to dired
to find some attachments
(The prot-emacs-email.el
message composition and encryption settings (message.el
)).
Note that the minor mode which sets up the relevant settings is called
gnus-dired-mode
, although it does not require gnus
and its
numerous accoutrements.
;;;; Add attachments from Dired (`gnus-dired' does not require `gnus') (use-package gnus-dired :ensure nil :after message :hook (dired-mode . turn-on-gnus-dired-mode))
5.11.4. The prot-emacs-email.el
settings for sendmail
As I explain in the Overview of my email setup (mbsync
, msmtp
, mail indexer, and MUA),
I use the external msmtp
program to handle email sending, else to
set up the Mail Transfer Agent (MTA). With msmtp
and a compatibility
package installed (msmtp-mta
on Arch Linux), the standard sendmail
executable becomes a symlink to the msmtp
program.
I use this method because it is more portable and flexible. With the
standard built-in Emacs design to use the smtpmail-send-it
function
as the value of send-mail-function
, I also need to declare in Elisp
what the SMTP server is. I would rather encapsulate the login
credentials in a separate configuration file, independent of Emacs,
while also ensuring that different accounts can use their own SMTP
server.
;;;; `sendmail' (mail transfer agent) (use-package sendmail :ensure nil :after message :config (setq send-mail-function 'sendmail-send-it ;; ;; NOTE 2023-08-08: We do not need this if we have the Arch ;; ;; Linux `msmtp-mta' package installed: it replaces the ;; ;; generic sendmail executable with msmtp. ;; ;; sendmail-program (executable-find "msmtp") message-sendmail-envelope-from 'header))
5.11.5. The prot-emacs-email.el
loading of the email client and call to provide
All the above are the client-agnostic settings. Now I have to put in
place the specifics of my mail user agent. I could have a user option
here to select one among notmuch
, mu4e
, or gnus
, but it is not
practical to have two indexers check the same files, as I will need to
keep everything in sync for no apparent benefit. One MUA is more than
enough (The prot-emacs-email.el
submodule for notmuch
(prot-emacs-notmuch.el
)).
Finally, we provide
the module. This is the mirror function of
require
(The init.el final part to load the individual modules).
(when (executable-find "notmuch") (require 'prot-emacs-notmuch)) (provide 'prot-emacs-email)
5.11.6. The prot-emacs-email.el
submodule for notmuch
(prot-emacs-notmuch.el
)
When we refer to notmuch
we cover three distinct concepts:
- The command-line mail indexer
- It produces a database out of our local mail directory. We can then query the database to get to the email we are looking for.
- The Mail User Agent (MUA), else email client
- It provides an Emacs interface to the aforementioned indexer. This MUA also draws linkages between other programs, to write and send messages. The user does not need to know that under the hood it is not one “thing” that gets the job done, though this is how it appears prima facie.
- The system package
- At least on Arch Linux and Debian, the
notmuch
package bundles together the above two. In fact, the indexer and the Emacs MUA are maintained in tandem by the same developers.
I install the system package to get the command-line utility. Since it
ships with the Emacs files, I do not install the relevant Emacs
package, but arrange for the notmuch
system path to be added to the
Emacs load-path
. The load-path
is where Emacs checks to find Elisp
programs (when we require
some feature, it works because of its
directory being part of the load-path
).
;;; Notmuch (mail indexer and mail user agent (MUA)) ;; I install notmuch from the distro's repos because the CLI program is ;; not dependent on Emacs. Though the package also includes notmuch.el ;; which is what we use here (they are maintained by the same people). (use-package notmuch :load-path "/usr/share/emacs/site-lisp/" :defer t :commands (notmuch notmuch-mua-new-mail))
5.11.6.1. The prot-emacs-notmuch.el
section about the account settings
To actually use the notmuch
mail user agent (The prot-emacs-email.el
submodule for notmuch
(prot-emacs-notmuch.el
)),
we need to specify (i) who we are and (ii) where should sent mail be
directed to. This is what the user options notmuch-identities
and
notmuch-fcc-dirs
are about.
Notice that I use the prot-common-auth-get-field
function, which
reads data from my encrypted ~/.authinfo.gpg
file (The prot-emacs-email.el
basic settings (including authinfo
)).
In prot-common.el
, I define a few helper functions that are general
in nature and may be used by other Elisp snippets of mine (The prot-common.el
library).
Doing things this way ensures that I do not share the details about
the accounts I set.
At any rate, the format of notmuch-identities
is like this:
(setq notmuch-identities '("Protesilaos Stavrou <someone@somewhere.com>" "Prot <just-my-nickname@somewhere.com>"))
While the corresponding notmuch-fcc-dirs
map those accounts to the
relative path of their local sent mail folder (it is relative to the
root of the directory that notmuch
indexes, so something like
~/.mail/pub/Sent
needs to be written as pub/Sent
). The data
structure looks like this:
(setq notmuch-fcc-dirs '(("someone@somewhere.com" . "someone/Sent") ("nickname@somewhere.com" . "nickname/Sent")))
Now the actual code I use:
;;; Account settings (use-package notmuch :defer t :config (let ((prv (prot-common-auth-get-field "prv-gandi" :user)) (pub (prot-common-auth-get-field "pub-gandi" :user)) (inf (prot-common-auth-get-field "inf-gandi" :user)) (box (prot-common-auth-get-field "prot-gandi" :user))) (setq notmuch-identities (mapcar (lambda (str) (format "%s <%s>" user-full-name str)) (list prv pub inf box)) notmuch-fcc-dirs `((,prv . "gandi/Sent") (,inf . "gandi/Sent") (,pub . "gandi/Sent") (,box . "gandi/Sent")))))
5.11.6.2. The prot-emacs-notmuch.el
section about the general user interface
When we use the notmuch
Emacs command to get into the mail user
agent (The prot-emacs-email.el
submodule for notmuch
(prot-emacs-notmuch.el
)),
we are presented in the “hello” buffer. It contains a set of
pseudo-graphical widgets to check the available tags, view recent
searches, perform a new search, and the like. I find it too busy and
not useful, as all that functionality is already available directly
with key bindings. Why nagivate all the way to the search box when s
(notmuch-search
) initiates a new search? As always, use M-x
describe-mode
(or type C-h m
) to learn about the keys and commands
of the current major mode (as well as to check all the minor modes
that are effective therein).
My “hello” buffer is a single vertical listing of my saved searches.
Those include the name of the search, such as inbox
, followed by a
total message count. Everything else is removed. As such, the values
of notmuch-hello-recent-searches-max
and notmuch-show-all-tags-list
are not relevant.
;;;; General UI (use-package notmuch :defer t :config (setq notmuch-show-logo nil notmuch-column-control 1.0 notmuch-hello-auto-refresh t notmuch-hello-recent-searches-max 20 notmuch-hello-thousands-separator "" notmuch-hello-sections '(notmuch-hello-insert-saved-searches) notmuch-show-all-tags-list t))
5.11.6.3. The prot-emacs-notmuch.el
section about the presentation of search buffers
The search buffers are the interface I work with the most. They provide a listing of all message threads that match the given search terms.
Most of the configurations I have here are stylistic in nature. The
one that defines necessary functionality is the value of the user
option notmuch-saved-searches
. The :query
of those saved searches
is what we would normally pass at the command line on the notmuch
search
invocation, or inside of the mail user agent by typing s
(notmuch-search
).
The emoji spotted here and elsewhere are purely cosmetic: they do not form part of the search terms or tags.
;;;; Search (use-package notmuch :defer t :config (setq notmuch-search-oldest-first nil) (setq notmuch-search-result-format '(("date" . "%12s ") ("count" . "%-7s ") ("authors" . "%-20s ") ("subject" . "%-80s ") ("tags" . "(%s)"))) (setq notmuch-tree-result-format '(("date" . "%12s ") ("authors" . "%-20s ") ((("tree" . "%s") ("subject" . "%s")) . " %-80s ") ("tags" . "(%s)"))) (setq notmuch-search-line-faces '(("unread" . notmuch-search-unread-face) ;; ;; NOTE 2022-09-19: I disable this because I add a cosmeic ;; ;; emoji via `notmuch-tag-formats'. This way I do not get ;; ;; an intense style which is very distracting when I filter ;; ;; my mail to include this tag. ;; ;; ("flag" . notmuch-search-flagged-face) ;; ;; Using `italic' instead is just fine. Though I also tried ;; it without any face and I was okay with it. The upside of ;; having a face is that you can identify the message even ;; when the window is split and you don't see the tags. ("flag" . italic))) (setq notmuch-show-empty-saved-searches t) (setq notmuch-saved-searches `(( :name "📥 inbox" :query "tag:inbox" :sort-order newest-first :key ,(kbd "i")) ( :name "📚 all unread (inbox)" :query "tag:unread and tag:inbox" :sort-order newest-first :key ,(kbd "u")) ( :name "💬 unread other (inbox)" :query "tag:unread and tag:inbox and not tag:package and not tag:coach" :sort-order newest-first :key ,(kbd "o")) ( :name "🗂️ unread packages" :query "tag:unread and tag:package" :sort-order newest-first :key ,(kbd "p")) ;; My coaching job: <https://protesilaos.com/coach/>. ( :name "🏆 unread coaching" :query "tag:unread and tag:coach" :sort-order newest-first :key ,(kbd "c")))))
5.11.6.4. The prot-emacs-notmuch.el
section about tag settings
Part of the value of using a mail indexer is the ability to tag
messages. These help with data retrieval and filtering. For notmuch
,
tags are a purely indexing construct, meaning that they are not
written to the underlying file. An exception to this are the standard
IMAP tags for read/unread, seen, attachments, and deleted (I think
that’s all, but please double check).
The +
or -
prefix indicates whether a tag is added or removed from
the list. The same characters work as key bindings in all notmuch
buffers to bring up a minibuffer interface for adding/removing tags.
This interface accepts multiple entries, so even if we start with a
-
we can still continue with an addition.
Otherwise, tagging operations follow a predefined scheme, specified in
the user option notmuch-tagging-keys
. I do not specify its value
here because I give some of my custom functions, hence its
incorporation in the subsequent section about setting up
prot-notmuch.el
(The prot-emacs-notmuch.el
custom extensions (per prot-notmuch.el
)).
;;;; Tags (use-package notmuch :defer t :config (setq notmuch-archive-tags nil ; I do not archive email notmuch-message-replied-tags '("+replied") notmuch-message-forwarded-tags '("+forwarded") notmuch-show-mark-read-tags '("-unread") notmuch-draft-tags '("+draft") notmuch-draft-folder "drafts" notmuch-draft-save-plaintext 'ask) ;; Also see `notmuch-tagging-keys' in the `prot-notmuch' section ;; further below. ;; ;; All emoji are cosmetic. The tags are just the text. (setq notmuch-tag-formats '(("unread" (propertize tag 'face 'notmuch-tag-unread)) ("flag" (propertize tag 'face 'notmuch-tag-flagged) (concat tag "🚩"))) notmuch-tag-deleted-formats '(("unread" (notmuch-apply-face bare-tag 'notmuch-tag-deleted) (concat "👁️🗨️" tag)) (".*" (notmuch-apply-face tag 'notmuch-tag-deleted) (concat "🚫" tag))) notmuch-tag-added-formats '(("del" (notmuch-apply-face tag 'notmuch-tag-added) (concat "💥" tag)) (".*" (notmuch-apply-face tag 'notmuch-tag-added) (concat "🏷️" tag)))))
5.11.6.5. The prot-emacs-notmuch.el
section about email composition settings
Most of the settings here are stylistic. I would not mind having them
differently. They are complementary to those germane to the built-in
message.el
(The prot-emacs-email.el
message composition and encryption settings (message.el
)).
The notmuch-mua-attachment-regexp
is a neat little helper to prevent
me from sending out a message without its stipulated attachment. It
can give false positives, as I may write something that is about
“emotional attachment”, but on the balance I do like being asked for
confirmation where needed.
;;;; Email composition (use-package notmuch :defer t :config (setq notmuch-mua-compose-in 'current-window) (setq notmuch-mua-hidden-headers nil) (setq notmuch-address-command 'internal) ; NOTE 2024-01-09: I am not using this and must review it. (setq notmuch-always-prompt-for-sender t) (setq notmuch-mua-cite-function 'message-cite-original-without-signature) (setq notmuch-mua-reply-insert-header-p-function 'notmuch-show-reply-insert-header-p-never) (setq notmuch-mua-user-agent-function nil) (setq notmuch-maildir-use-notmuch-insert t) (setq notmuch-crypto-process-mime t) (setq notmuch-crypto-get-keys-asynchronously t) (setq notmuch-mua-attachment-regexp ; see `notmuch-mua-send-hook' (concat "\\b\\(attache\?ment\\|attached\\|attach\\|" "pi[èe]ce\s+jointe?\\|" "συνημμ[εέ]νο\\|επισυν[αά]πτω\\)\\b")))
5.11.6.6. The prot-emacs-notmuch.el
section about reading messages
Some simple tweaks here to get the presentation I like while reading messages. Everything here is valueble to me, though note the “wash” parts. These pertain to a behaviour where long quotes are hidden behind a button. The idea is to hide most of the text and reveal it on demand. I never want that: if there is a long section of text there, I need to see it.
Note that the presentation of HTML messages is affected by the state of
the built-in Simple HTML Renderer (The prot-emacs-web.el
settings about the Simple HTML Renderer (shr
)).
Concretely, there is the shr-use-colors
option, which I disable
because I do not want hardcoded values to mess up my theme. As such,
newletters, receipts, etc., which are usually rendered on a white
background will be dark while using a dark theme. This is considerably
nicer.
;;;; Reading messages (use-package notmuch :defer t :config (setq notmuch-show-relative-dates t) (setq notmuch-show-all-multipart/alternative-parts nil) (setq notmuch-show-indent-messages-width 0) (setq notmuch-show-indent-multipart nil) (setq notmuch-show-part-button-default-action 'notmuch-show-view-part) (setq notmuch-show-text/html-blocked-images ".") ; block everything (setq notmuch-wash-wrap-lines-length 120) (setq notmuch-unthreaded-show-out nil) (setq notmuch-message-headers '("To" "Cc" "Subject" "Date")) (setq notmuch-message-headers-visible t) (let ((count most-positive-fixnum)) ; I don't like the buttonisation of long quotes (setq notmuch-wash-citation-lines-prefix count notmuch-wash-citation-lines-suffix count)))
5.11.6.7. The prot-emacs-notmuch.el
section about hooks and key bindings
Here I set up the following:
- Remind me when I am mentioning an attachment but do not include one.
This is done by reading the contents of the message in search for
the
notmuch-mua-attachment-regexp
(Theprot-emacs-notmuch.el
section about email composition settings). - Do not use a
header-line
when showing a message. It adds visual clutter. - Do not activate the
notmuch-hl-line-mode
because I want the generichl-line-mode
to take effect instead. This is because I use mylin
package to remap buffer-locally the line highlight to be a bit more noticeable in major modes where line selection is the main action (Theprot-emacs-theme.el
section forlin
). - Define key bindings that make sense to me. The most important change
here is the flipped meaning of the
r
andR
keys, as I want to reply to all recipients by default. I define some more key bindings in the section about my custom extensions for Notmuch (Theprot-emacs-notmuch.el
custom extensions (perprot-notmuch.el
)).
;;;; Hooks and key bindings (use-package notmuch :hook (notmuch-mua-send . notmuch-mua-attachment-check) ; also see `notmuch-mua-attachment-regexp' (notmuch-show . (lambda () (setq-local header-line-format nil))) :config (remove-hook 'notmuch-show-hook #'notmuch-show-turn-on-visual-line-mode) (remove-hook 'notmuch-search-hook #'notmuch-hl-line-mode) ; Check my `lin' package :bind ( :map global-map ("C-c m" . notmuch) ("C-x m" . notmuch-mua-new-mail) ; override `compose-mail' :map notmuch-search-mode-map ; I normally don't use the tree view, otherwise check `notmuch-tree-mode-map' ("a" . nil) ; the default is too easy to hit accidentally and I do not archive stuff ("A" . nil) ("/" . notmuch-search-filter) ; alias for l ("r" . notmuch-search-reply-to-thread) ; easier to reply to all by default ("R" . notmuch-search-reply-to-thread-sender) :map notmuch-show-mode-map ("a" . nil) ; the default is too easy to hit accidentally and I do not archive stuff ("A" . nil) ("r" . notmuch-show-reply) ; easier to reply to all by default ("R" . notmuch-show-reply-sender) :map notmuch-hello-mode-map ("C-<tab>" . nil)))
5.11.6.8. The prot-emacs-notmuch.el
custom extensions (per prot-notmuch.el
)
My prot-notmuch.el
extension defines a few useful extras for my
email setup (The prot-notmuch.el
library). These are commands to
quickly perform a tagging operation, such as to mark a messge for
deletion and remove it from the inbox. The key bindings here extend
those that are for the base Notmuch package (The prot-emacs-notmuch.el
section about hooks and key bindings).
;;; My own tweaks for notmuch (prot-notmuch.el) (use-package prot-notmuch :ensure nil :after notmuch :bind ( :map notmuch-search-mode-map ("D" . prot-notmuch-search-delete-thread) ("S" . prot-notmuch-search-spam-thread) ("g" . prot-notmuch-refresh-buffer) :map notmuch-show-mode-map ("D" . prot-notmuch-show-delete-message) ("S" . prot-notmuch-show-spam-message) :map notmuch-show-stash-map ("S" . prot-notmuch-stash-sourcehut-link)) :config ;; Those are for the actions that are available after pressing 'k' ;; (`notmuch-tag-jump'). For direct actions, refer to the key ;; bindings below. (setq notmuch-tagging-keys `((,(kbd "d") prot-notmuch-mark-delete-tags "💥 Mark for deletion") (,(kbd "f") prot-notmuch-mark-flag-tags "🚩 Flag as important") (,(kbd "s") prot-notmuch-mark-spam-tags "🔥 Mark as spam") (,(kbd "r") ("-unread") "👁️🗨️ Mark as read") (,(kbd "u") ("+unread") "🗨️ Mark as unread"))) ;; These emoji are purely cosmetic. The tag remains the same: I ;; would not like to input emoji for searching. (add-to-list 'notmuch-tag-formats '("encrypted" (concat tag "🔒"))) (add-to-list 'notmuch-tag-formats '("attachment" (concat tag "📎"))) (add-to-list 'notmuch-tag-formats '("coach" (concat tag "🏆"))) (add-to-list 'notmuch-tag-formats '("package" (concat tag "🗂️"))))
5.11.6.9. The prot-emacs-notmuch.el
glue code for org-capture
(ol-notmuch.el
)
This package provides the glue code between Notmuch and Org capture:
- The
prot-emacs-email.el
submodule fornotmuch
(prot-emacs-notmuch.el
). - The
prot-emacs-org.el
Org capture templates (org-capture
).
In simple terms, I can create links that point to emails. When I follow the link, it opens in a fully fledged Notmuch buffer. This is how I build up my agenda of appointments. It highlights the power of Emacs’ interconnectedness, as I go from my email to the agenda, to editing, file management, and related.
;;; Glue code for notmuch and org-link (ol-notmuch.el) (use-package ol-notmuch :ensure t :after notmuch)
5.11.6.10. The prot-emacs-notmuch.el
section about the notmuch-indicator
This is a simple package of mine that renders an indicator with an
email count of the notmuch
index on the Emacs mode line. The
underlying mechanism is that of notmuch-count(1)
, which is used to
find the number of items that match the given search terms. In
practice, the user can define one or more searches and display their
counters. These form a listing which realistically is like: @50
😱1000 💕0
for unread messages, bills, and love letters,
respectively.
- Package name (GNU ELPA):
notmuch-indicator
- Official manual: https://protesilaos.com/emacs/notmuch-indicator
- Change log: https://protesilaos.com/emacs/notmuch-indicator-changelog
- Git repositories:
- Backronym: notmuch-… Interested in Notmuch Data Indicators that Count Any Terms Ordinarily Requested.
;;; notmuch-indicator (another package of mine) (use-package notmuch-indicator :ensure t :after notmuch :config (setq notmuch-indicator-args '(( :terms "tag:unread and tag:inbox" :label "[A] " :label-face prot-modeline-indicator-green :counter-face prot-modeline-indicator-green) ( :terms "tag:unread and tag:inbox and not tag:package and not tag:coach" :label "[U] " :label-face prot-modeline-indicator-cyan :counter-face prot-modeline-indicator-cyan) ( :terms "tag:unread and tag:package and tag:inbox" :label "[P] " :label-face prot-modeline-indicator-magenta :counter-face prot-modeline-indicator-magenta) ( :terms "tag:unread and tag:coach and tag:inbox" :label "[C] " :label-face prot-modeline-indicator-red :counter-face prot-modeline-indicator-red)) notmuch-indicator-refresh-count (* 60 3) notmuch-indicator-hide-empty-counters t notmuch-indicator-force-refresh-commands '(notmuch-refresh-this-buffer)) ;; I control its placement myself. See prot-emacs-modeline.el where ;; I set the `mode-line-format'. (setq notmuch-indicator-add-to-mode-line-misc-info nil) (notmuch-indicator-mode 1))
5.11.6.11. The prot-emacs-notmuch.el
call to provide
Finally, we provide
the submodule. This is the mirror function of
require
(The prot-emacs-email.el
loading of the email client and call to provide
).
(provide 'prot-emacs-notmuch)
5.11.7. The deprecated prot-emacs-mail.el
submodule for mu4e
(prot-emacs-mu4e.el
)
I did use Mu4e in the past as my email client. Its setup is similar to
the one I have now with Notmuch (Overview of my email setup (mbsync
, msmtp
, mail indexer, and MUA)).
The mu4e
package is the Emacs interface, or “Mail User Agent”, for
the user’s email: this is where one reads messages and responds to
them, moves files to different folders, changes email tags, and so on.
All email is indexed by the the external mu
executable: it is an
indexer of one’s local copy of their email. The indexer maintains a
database that the user can query. This search facility can be accessed
both from the command line and insider Emacs via mu4e
.
I prefer Notmuch over Mu4e due to stylistic choices and the overall workflow. I keep this setup around for two reasons: (i) to be able to test it for my themes and (ii) help others who are using it.
;; This is deprecated code as I stopped using mu4e a long time ago. I ;; keep it here for when I need to test it for my themes (`ef-themes', ;; `modus-themes', `standard-themes'). (use-package mu4e :ensure nil ;; This is an exception because I install it from the system ;; distribution's package archives (depends on non-Emacs code) :load-path "/usr/share/emacs/site-lisp/elpa/mu4e-1.8.14" :config (setq mu4e-maildir "~/.20240226-mail-mu4e") (setq mu4e-attachment-dir (expand-file-name "~/Downloads/mail-attachments/")) (setq mu4e-confirm-quit nil) (setq mu4e-mu-allow-temp-file t) ; mu 1.12.0 (setq mu4e-use-fancy-chars t ; Cool idea, but they create misalignments mu4e-headers-draft-mark '("D" . "⚒️") mu4e-headers-flagged-mark '("F" . "🚩") mu4e-headers-new-mark '("N" . "🔥") mu4e-headers-passed-mark '("P" . "📨") mu4e-headers-replied-mark '("R" . "✏️") mu4e-headers-seen-mark '("S" . "👁️🗨️") mu4e-headers-trashed-mark '("T" . "🚫") mu4e-headers-attach-mark '("a" . "📎") mu4e-headers-encrypted-mark '("x" . "🔒") mu4e-headers-signed-mark '("s" . "🔑") mu4e-headers-unread-mark '("u" . "💬") mu4e-headers-list-mark '("l" . "📬") mu4e-headers-personal-mark '("p" . "🦚") mu4e-headers-calendar-mark '("c" . "📅")) (setq mu4e-marks '((refile :char ("r" . "▶") :prompt "refile" :dyn-target (lambda (target msg) (mu4e-get-refile-folder msg)) :action (lambda (docid msg target) (mu4e--server-move docid (mu4e--mark-check-target target) "-N"))) (delete :char ("D" . "🚫") :prompt "Delete" :show-target (lambda (target) "delete") :action (lambda (docid msg target) (mu4e--server-remove docid))) (flag :char ("+" . "🚩") :prompt "+flag" :show-target (lambda (target) "flag") :action (lambda (docid msg target) (mu4e--server-move docid nil "+F-u-N"))) (move :char ("m" . "▷") :prompt "move" :ask-target mu4e--mark-get-move-target :action (lambda (docid msg target) (mu4e--server-move docid (mu4e--mark-check-target target) "-N"))) (read :char ("!" . "👁️🗨️") :prompt "!read" :show-target (lambda (target) "read") :action (lambda (docid msg target) (mu4e--server-move docid nil "+S-u-N"))) (trash :char ("d" . "🚫") :prompt "dtrash" :dyn-target (lambda (target msg) (mu4e-get-trash-folder msg)) :action (lambda (docid msg target) (mu4e--server-move docid (mu4e--mark-check-target target) "+T-N"))) (unflag :char ("-" . "➖") :prompt "-unflag" :show-target (lambda (target) "unflag") :action (lambda (docid msg target) (mu4e--server-move docid nil "-F-N"))) (untrash :char ("=" . "▲") :prompt "=untrash" :show-target (lambda (target) "untrash") :action (lambda (docid msg target) (mu4e--server-move docid nil "-T"))) (unread :char ("?" . "💬") :prompt "?unread" :show-target (lambda (target) "unread") :action (lambda (docid msg target) (mu4e--server-move docid nil "-S+u-N"))) (unmark :char " " :prompt "unmark" :action (mu4e-error "No action for unmarking")) (action :char ( "a" . "◯") :prompt "action" :ask-target (lambda () (mu4e-read-option "Action: " mu4e-headers-actions)) :action (lambda (docid msg actionfunc) (save-excursion (when (mu4e~headers-goto-docid docid) (mu4e-headers-action actionfunc))))) (something :char ("*" . "✱") :prompt "*something" :action (mu4e-error "No action for deferred mark")))) (setq mu4e-modeline-support t mu4e-modeline-unread-items '("U:" . "[U]") mu4e-modeline-all-read '("R:" . "[R]") mu4e-modeline-all-clear '("C:" . "[C]") mu4e-modeline-max-width 42) (setq mu4e-notification-support t ;; TODO 2024-02-26: Write custom mu4e notification function. mu4e-notification-filter #'mu4e--default-notification-filter) (setq mu4e-headers-advance-after-mark nil) (setq mu4e-headers-auto-update t) (setq mu4e-headers-date-format "%F %a, %T") (setq mu4e-headers-time-format "%R") (setq mu4e-headers-long-date-format "%F, %R") (setq mu4e-headers-leave-behavior 'apply) (setq mu4e-headers-fields '((:date . 26) (:flags . 8) (:from . 20) (:subject))) (setq mu4e-get-mail-command "true" ; I auto-fetch with a systemd timer mu4e-update-interval nil) (setq mu4e-hide-index-messages t) (setq mu4e-read-option-use-builtin nil mu4e-completing-read-function 'completing-read) (setq mu4e-search-results-limit -1 mu4e-search-sort-field :date mu4e-search-sort-direction 'descending) (setq mu4e-org-support t) (setq mu4e-sent-messages-behavior 'sent) (setq mu4e-view-show-addresses t) (setq mu4e-split-view 'horizontal) (setq mu4e-index-lazy-check t) (setq mu4e-change-filenames-when-moving t) ; better for `mbsync'? (setq mu4e-display-update-status-in-modeline nil) (setq mu4e-view-show-images nil) (setq mu4e-headers-include-related nil) (setq mu4e-view-auto-mark-as-read t) (setq mu4e-compose-complete-addresses nil mu4e-compose-complete-only-personal t) (setq mu4e-compose-signature "Protesilaos Stavrou\nprotesilaos.com\n") (setq mu4e-compose-signature-auto-include t) (setq mu4e-context-policy 'pick-first mu4e-compose-context-policy nil) (setq mu4e-contexts `(,@(mapcar (lambda (context) (let ((address (prot-common-auth-get-field context :user))) (make-mu4e-context :name context :match-func `(lambda (msg) (when msg (mu4e-message-contact-field-matches msg :to ,address))) :vars `((user-mail-address . ,address) (mu4e-trash-folder . ,(format "/%s/Trash" context)) (mu4e-sent-folder . ,(format "/%s/Sent" context)))))) '("pub" "inf" "prv")))) ;; 2024-02-26 10:34 +0200 WORK-IN-PROGRESS (setq mu4e-bookmarks `((:name "All unread messages" :query "g:unread AND NOT g:trashed" :key ?a) (:name "All messages" :query "*" :key ?A) ( :name "Personal unread" :query "contact:/@protesilaos/ or contact:protesilaos AND g:unread AND NOT contact:/@.*gnu/" :key ?u :favorite t) ( :name "Personal inbox" :query "contact:/@protesilaos/ or contact:protesilaos AND NOT contact:/@.*gnu/" :key ?U :favorite t) ( :name "Mailing list unread" :query "contact:/@.*gnu/ AND g:unread" :key ?m) ( :name "Mailing list inbox" :query "contact:/@.*gnu/" :key ?M))) (defun prot/mu4e (&rest args) (cl-letf (((symbol-function 'display-buffer-full-frame) #'display-buffer-same-window)) (apply args))) (advice-add #'mu4e-display-buffer :around #'prot/mu4e) (defun prot/mu4e-no-header-line () (setq-local header-line-format nil)) (add-hook 'mu4e-headers-mode-hook #'prot/mu4e-no-header-line) (prot-emacs-keybind global-map "C-c m" #'mu4e "C-x m" #'mu4e-compose-new) ; override `compose-mail' (prot-emacs-keybind mu4e-headers-mode-map "!" #'mu4e-headers-mark-for-flag "?" #'mu4e-headers-mark-for-unflag "r" #'mu4e-headers-mark-for-read "d" #'mu4e-headers-mark-for-delete ; I do not use the trash "u" #'mu4e-headers-mark-for-unread "m" #'mu4e-headers-mark-for-unmark "M" #'mu4e-mark-unmark-all) (prot-emacs-keybind mu4e-headers-mode-map "!" #'mu4e-headers-mark-for-flag "?" #'mu4e-headers-mark-for-unflag "r" #'mu4e-headers-mark-for-read "u" #'mu4e-headers-mark-for-unread "m" #'mu4e-headers-mark-for-unmark "M" #'mu4e-mark-unmark-all) (prot-emacs-keybind mu4e-view-mode-map ;; NOTE 2024-02-26: with mu 1.12.0 there is a "wide" reply that I ;; would bind to r and the regular reply to R. "r" #'mu4e-compose-reply "w" #'mu4e-copy-thing-at-point "s" #'mu4e-view-save-attachments "S" #'mu4e-view-raw-message ; "source" mnemonic "u" #'mu4e-view-mark-for-unread "U" #'mu4e-view-mark-for-read "d" #'mu4e-view-mark-for-delete ; overwrite mu4e-view-mark-for-trash "!" #'mu4e-view-mark-for-flag "?" #'mu4e-view-mark-for-unflag)) (provide 'prot-emacs-mu4e)
5.11.8. The deprecated prot-emacs-mail.el
submodule for Gnus (prot-emacs-gnus.el
)
I have long now stopped using Gnus (Overview of my email setup (mbsync
, msmtp
, mail indexer, and MUA)).
I keep this here in case I need to test the setup.
;; This is deprecated code as I stopped using Gnus a long time ago. I ;; keep it here for when I need to test it for my themes (`ef-themes', ;; `modus-themes', `standard-themes'). (use-package gnus :ensure nil :config (require 'gnus-sum) (require 'gnus-dired) (require 'gnus-topic) ;;; accounts (setq gnus-select-method '(nnnil "")) (setq gnus-secondary-select-methods '((nntp "news.gwene.org") ;; ;; NOTE 2021-05-13: Switched to notmuch. ;; (nnmaildir "prv" (directory "~/.mail/prv") ;; (gnus-search-engine gnus-search-notmuch ; this feature is from Emacs 28 ;; (remove-prefix "~/.mail/prv"))) ;; (nnmaildir "inf" (directory "~/.mail/inf") ;; (gnus-search-engine gnus-search-notmuch ;; (remove-prefix "~/.mail/inf"))) ;; (nnmaildir "pub" (directory "~/.mail/pub") ;; (gnus-search-engine gnus-search-notmuch ;; (remove-prefix "~/.mail/pub"))) )) (setq gnus-search-use-parsed-queries nil) ; Emacs 28 ;; ;; NOTE 2021-05-13: Switched to notmuch. ;; (setq gnus-parameters ;; '((".*" ; fallback option ;; (posting-style ;; (gcc "nnmaildir+inf:Sent") ;; (From ;; (format "%s <%s>" user-full-name ;; (prot-mail-auth-get-field "inf" :user))))) ;; ("prv" ;; (posting-style ;; (gcc "nnmaildir+prv:Sent") ;; (From ;; (format "%s <%s>" user-full-name ;; (prot-mail-auth-get-field "prv" :user))))) ;; ("pub" ;; (posting-style ; Uses default name+mail ;; (gcc "nnmaildir+pub:Sent"))))) (setq gnus-gcc-mark-as-read t) (setq gnus-agent t) (setq gnus-novice-user nil) ; careful with this ;; checking sources (setq gnus-check-new-newsgroups 'ask-server) (setq gnus-read-active-file 'some) ;; dribble (setq gnus-use-dribble-file t) (setq gnus-always-read-dribble-file t) ;;; agent (setq gnus-agent-article-alist-save-format 1) ; uncompressed (setq gnus-agent-cache t) (setq gnus-agent-confirmation-function 'y-or-n-p) (setq gnus-agent-consider-all-articles nil) (setq gnus-agent-directory "~/News/agent/") (setq gnus-agent-enable-expiration 'ENABLE) (setq gnus-agent-expire-all nil) (setq gnus-agent-expire-days 30) (setq gnus-agent-mark-unread-after-downloaded t) (setq gnus-agent-queue-mail t) ; queue if unplugged (setq gnus-agent-synchronize-flags nil) ;;; article (setq gnus-article-browse-delete-temp 'ask) (setq gnus-article-over-scroll nil) (setq gnus-article-show-cursor t) (setq gnus-article-sort-functions '((not gnus-article-sort-by-number) (not gnus-article-sort-by-date))) (setq gnus-article-truncate-lines nil) (setq gnus-html-frame-width 80) (setq gnus-html-image-automatic-caching t) (setq gnus-inhibit-images t) (setq gnus-max-image-proportion 0.7) (setq gnus-treat-display-smileys nil) (setq gnus-article-mode-line-format "%G %S %m") (setq gnus-visible-headers '("^From:" "^To:" "^Cc:" "^Subject:" "^Newsgroups:" "^Date:" "Followup-To:" "Reply-To:" "^Organization:" "^X-Newsreader:" "^X-Mailer:")) (setq gnus-sorted-header-list gnus-visible-headers) (setq gnus-article-x-face-too-ugly ".*") ; all images in headers are outright annoying---disabled! ;;; async (setq gnus-asynchronous t) (setq gnus-use-article-prefetch 15) ;;; group (setq gnus-level-subscribed 6) (setq gnus-level-unsubscribed 7) (setq gnus-level-zombie 8) (setq gnus-activate-level 1) (setq gnus-list-groups-with-ticked-articles nil) (setq gnus-group-sort-function '((gnus-group-sort-by-unread) (gnus-group-sort-by-alphabet) (gnus-group-sort-by-rank))) (setq gnus-group-line-format "%M%p%P%5y:%B%(%g%)\n") (setq gnus-group-mode-line-format "%%b") (setq gnus-topic-display-empty-topics nil) ;;; summary (setq gnus-auto-select-first nil) (setq gnus-summary-ignore-duplicates t) (setq gnus-suppress-duplicates t) (setq gnus-save-duplicate-list t) (setq gnus-summary-goto-unread nil) (setq gnus-summary-make-false-root 'adopt) (setq gnus-summary-thread-gathering-function 'gnus-gather-threads-by-subject) (setq gnus-summary-gather-subject-limit 'fuzzy) (setq gnus-thread-sort-functions '((not gnus-thread-sort-by-date) (not gnus-thread-sort-by-number))) (setq gnus-subthread-sort-functions 'gnus-thread-sort-by-date) (setq gnus-thread-hide-subtree nil) (setq gnus-thread-ignore-subject nil) (setq gnus-user-date-format-alist '(((gnus-seconds-today) . "Today at %R") ((+ (* 60 60 24) (gnus-seconds-today)) . "Yesterday, %R") (t . "%Y-%m-%d %R"))) ;; When the %f specifier in `gnus-summary-line-format' matches my ;; name, this will use the contents of the "To:" field, prefixed by ;; the string I specify. Useful when checking your "Sent" summary or ;; a mailing list you participate in. (setq gnus-ignored-from-addresses "Protesilaos Stavrou") (setq gnus-summary-to-prefix "To: ") (setq gnus-summary-line-format "%U%R %-18,18&user-date; %4L:%-25,25f %B%s\n") (setq gnus-summary-mode-line-format "[%U] %p") (setq gnus-sum-thread-tree-false-root "") (setq gnus-sum-thread-tree-indent " ") (setq gnus-sum-thread-tree-single-indent "") (setq gnus-sum-thread-tree-leaf-with-other "+-> ") (setq gnus-sum-thread-tree-root "") (setq gnus-sum-thread-tree-single-leaf "\\-> ") (setq gnus-sum-thread-tree-vertical "|") (add-hook 'dired-mode-hook #'gnus-dired-mode) ; dired integration (add-hook 'gnus-group-mode-hook #'gnus-topic-mode) (add-hook 'gnus-select-group-hook #'gnus-group-set-timestamp) (dolist (mode '(gnus-group-mode-hook gnus-summary-mode-hook gnus-browse-mode-hook)) (add-hook mode #'hl-line-mode)) ;; ;; NOTE 2021-05-13: Switched to notmuch. ;; (define-key global-map (kbd "C-c m") #'gnus) (let ((map gnus-article-mode-map)) (define-key map (kbd "i") #'gnus-article-show-images) (define-key map (kbd "s") #'gnus-mime-save-part) (define-key map (kbd "o") #'gnus-mime-copy-part)) (let ((map gnus-group-mode-map)) ; I always use `gnus-topic-mode' (define-key map (kbd "n") #'gnus-group-next-group) (define-key map (kbd "p") #'gnus-group-prev-group) (define-key map (kbd "M-n") #'gnus-topic-goto-next-topic) (define-key map (kbd "M-p") #'gnus-topic-goto-previous-topic)) (let ((map gnus-summary-mode-map)) (define-key map (kbd "<delete>") #'gnus-summary-delete-article) (define-key map (kbd "n") #'gnus-summary-next-article) (define-key map (kbd "p") #'gnus-summary-prev-article) (define-key map (kbd "N") #'gnus-summary-next-unread-article) (define-key map (kbd "P") #'gnus-summary-prev-unread-article) (define-key map (kbd "M-n") #'gnus-summary-next-thread) (define-key map (kbd "M-p") #'gnus-summary-prev-thread) (define-key map (kbd "C-M-n") #'gnus-summary-next-group) (define-key map (kbd "C-M-p") #'gnus-summary-prev-group) (define-key map (kbd "C-M-^") #'gnus-summary-refer-thread))) (use-package nnmail :ensure nil :config (setq nnmail-expiry-wait 30)) ; careful with this (provide 'prot-emacs-gnus)
5.11.9. Overview of my email setup (mbsync
, msmtp
, mail indexer, and MUA)
I have a regular IMAP email setup. This is the protocol that allows the server to retain messages, which is of practical benefit for accessing mail on multiple machines.
A copy of my server’s contents needs to be stored locally. This is valuable for backup purposes. It also provides the option to move to another email service provider by just syncing the local data to the new location while deleting the old one. More importantly though, local files can be indexed, granting us access to a powerful search mechanism (more below).
The program that stores email locally is called mbsync
. For whatever
reason, the system package is isync
. This is true for Arch Linux and
Debian. (I also heard it is the case with homebrew
, but please check.)
With mbsync
we synchronise the state from the server (known as the
“far” side) to our computer (the “near” side). The directionality can
be one-way, far to near or near to far. Or it can be two-way, as is
the case on my end. Concretely, what I have on my computer is mirrored
on the server.
The format mbsync
uses is called maildir
. This is in contrast to
the Unix mbox
. I do not know about the pros and cons of each format,
other than the fact that maildir
has been working flawlessly for me
for years—and email is my primary medium of communication online
(Contents of my .mbsyncrc
).
The local files are indexed with a “mail indexer” system program.
There are a few of those. The ones I have used are mu
and notmuch
.
Both are built on top of the xapian
search engine software and both
offer the same features. The noticeable differences exist at the level
of the email client, else the Mail User Agent (MUA), which I cover
further below.
Mail indexers create a database out of a local email directory for the
purposes of retrieving data from it. In practical terms, we can run a
search like notmuch search from:protesilaos
(with notmuch
) or mu
find from:protesilaos
(with mu
) to produce a list of all messages
whose From
mail header matches the given name. The search terms are
quite sophisticated, accepting tags, date ranges, and even queries for
message contents.
Indexation is an archiving process. We do not need a Mail User Agent (MUA) to handle all the interactivity involved. There are cases where we can have some old messages in such a read-only state. Though we most probably need to deal with email in present time, hence the need for a MUA.
The MUA is responsible for all the interactive aspects of dealing with email: show a list of messages, display an individual email, handle message composition, and others such as tagging, searching, downloading… In short, the mail user agent is what we normally understand as the “email app”.
The mu
and notmuch
indexers both come with their own Emacs MUA as
part of their system package. For the former, the Emacs package is known
as mu4e
(The deprecated prot-emacs-mail.el
submodule for mu4e
(prot-emacs-mu4e.el
)), while the
latter’s is notmuch
or notmuch.el
to differentiate it from the
system program (The prot-emacs-email.el
submodule for notmuch
(prot-emacs-notmuch.el
)).
I have used both MUAs extensively and settled with notmuch.el
simply
because it has a threaded view: mu4e
uses a tree view, which is fine
if you are not dealing with lots of emails, but is harder to spot what
you need when conversations start getting longer and you have plenty
of them. I never use the tree view provided by notmuch.el
, unless I
am testing something.
Behind the scenes, the MUA needs some other program to actually send
messages. For this purpose, I set up the msmtp
system package, which
defines a handler for the standard sendmail
system utility. Not to
bother you with the technicalities: msmtp
is basically the same as
mbsync
but for sending instead of fetching.
This all sounds complex to anyone who expects email to be encapsulated
in a single application. What we have here is the Unix paradigm in
action. Need another program for mail indexing? Good, just swap out
mbsync
for offlineimap
and continue using the rest of the tool
kit. This is the sort of idea.
Overall, I am happy with my setup. Email is integral to my online
communications: I need it to be reliable and efficient. Having the MUA
in Emacs is extra nice because it integrates an essential part of my
daily computing with the rest of what I do (The prot-emacs-email.el
module).
5.11.9.1. Contents of my .mbsyncrc
This file is stored in the home directory at ~/.mbsyncrc
(it can be
placed elsewhere, but I do not mind it there). Notice that the user
name and password are not written in the file directly but are instead
retrieved by a system call that reads the encrypted ~/.authinfo.gpg
file (The prot-emacs-email.el
basic settings (including authinfo
)).
What you see there is three separate email accounts.
# Gandi #################################################################################################### IMAPAccount gandi Host mail.gandi.net UserCmd "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gandi/ { print $(NF-2); exit; }'" PassCmd "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gandi/ { print $NF; exit; }'" SSLType IMAPS IMAPStore gandi-remote Account gandi MaildirStore gandi-local Subfolders Verbatim # The trailing "/" is important Path ~/.mail/gandi/ Inbox ~/.mail/gandi/Inbox Channel gandi Far :gandi-remote: Near :gandi-local: # Include everything Patterns * Sync All Create Both Remove Both Expunge Both SyncState * # # Gmail # #################################################################################################### # IMAPAccount gmail # Host imap.gmail.com # UserCmd "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gmail/ { print $(NF-2); exit; }'" # PassCmd "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gmail/ { print $NF; exit; }'" # SSLType IMAPS # # IMAPStore gmail-remote # Account gmail # # MaildirStore gmail-local # Subfolders Verbatim # # The trailing "/" is important # Path ~/.mail/gmail/ # Inbox ~/.mail/gmail/Inbox # # Channel gmail # Far :gmail-remote: # Near :gmail-local: # # Include everything # Patterns * # Sync All # Create Near # Remove Near # Expunge Near # SyncState * # # Mailbox # #################################################################################################### # IMAPAccount mailbox # Host imap.mailbox.org # UserCmd "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/mailbox/ { print $(NF-2); exit; }'" # PassCmd "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/mailbox/ { print $NF; exit; }'" # SSLType IMAPS # # IMAPStore mailbox-remote # Account mailbox # # MaildirStore mailbox-local # Subfolders Verbatim # # The trailing "/" is important # Path ~/.mail/mailbox/ # Inbox ~/.mail/mailbox/Inbox # # Channel mailbox # Far :mailbox-remote: # Near :mailbox-local: # # Include everything # Patterns * # Sync All # Create Near # Remove Near # Expunge Near # SyncState *
5.11.9.2. Contents of my msmtp
configuration file
This file is stored at ~/.config/msmtp/config
(well, unless you
change the XDG directory but then you know what you are doing). Just
as with mbsync
, I retrieve the user name and password via commands
that read from the ~/.authinfo
file (Contents of my .mbsyncrc
).
# See my mbsync config, which is reflected here. # https://github.com/protesilaos/dotfiles defaults auth on protocol smtp tls on tls_starttls on # Gandi #################################################################################################### account gandi host mail.gandi.net port 587 eval echo from $(gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gandi/ { print $(NF-2); exit; }') eval echo user $(gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gandi/ { print $(NF-2); exit; }') passwordeval "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gandi/ { print $NF; exit; }'" # # Gmail # #################################################################################################### # account gmail # host smtp.gmail.com # port 587 # eval echo from $(gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gmail/ { print $(NF-2); exit; }') # eval echo user $(gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gmail/ { print $(NF-2); exit; }') # passwordeval "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gmail/ { print $NF; exit; }'" # # Mailbox # #################################################################################################### # account mailbox # host smtp.mailbox.org # port 465 # eval echo from $(gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/mailbox/ { print $(NF-2); exit; }') # eval echo user $(gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/mailbox/ { print $(NF-2); exit; }') # passwordeval "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/mailbox/ { print $NF; exit; }'" # Set a default account (I copied from man msmtp) account default : gandi
5.12. TODO The prot-emacs-web.el
module
5.12.1. TODO The prot-emacs-web.el
settings about following links (browse-url
)
;;;; `browse-url' (use-package browse-url :ensure nil :defer 1 :config (setq browse-url-browser-function 'eww-browse-url) (setq browse-url-secondary-browser-function 'browse-url-default-browser))
5.12.2. TODO The prot-emacs-web.el
settings about buttonising links (goto-addr
)
;;;; `goto-addr' (use-package goto-addr :ensure nil :defer t :config (setq goto-address-url-face 'link) (setq goto-address-url-mouse-face 'highlight) (setq goto-address-mail-face nil) (setq goto-address-mail-mouse-face 'highlight))
5.12.3. TODO The prot-emacs-web.el
settings about the Simple HTML Renderer (shr
)
;;;; `shr' (Simple HTML Renderer) (use-package shr :ensure nil :defer t :config (setq shr-use-colors nil) ; t is bad for accessibility (setq shr-use-fonts nil) ; t is not for me (setq shr-max-image-proportion 0.6) (setq shr-image-animate nil) ; No GIFs, thank you! (setq shr-width fill-column) ; check `prot-eww-readable' (setq shr-max-width fill-column) (setq shr-discard-aria-hidden t) (setq shr-fill-text nil) ; Emacs 31 (setq shr-cookie-policy nil))
5.12.4. TODO The prot-emacs-web.el
settings about browser cookies
;;;; `url-cookie' (use-package url-cookie :ensure nil :defer t :config (setq url-cookie-untrusted-urls '(".*")))
5.12.5. TODO The prot-emacs-web.el
settings about the web browser (eww
)
;;;; `eww' (Emacs Web Wowser) (use-package eww :ensure nil :commands (eww) :bind ( :map eww-link-keymap ("v" . nil) ; stop overriding `eww-view-source' :map eww-mode-map ("L" . eww-list-bookmarks) :map dired-mode-map ("E" . eww-open-file) ; to render local HTML files :map eww-buffers-mode-map ("d" . eww-bookmark-kill) ; it actually deletes :map eww-bookmark-mode-map ("d" . eww-bookmark-kill)) ; same :config (setq eww-restore-desktop t) (setq eww-desktop-remove-duplicates t) (setq eww-header-line-format nil) (setq eww-search-prefix "https://duckduckgo.com/html/?q=") (setq eww-download-directory (expand-file-name "~/Documents/eww-downloads")) (setq eww-suggest-uris '(eww-links-at-point thing-at-point-url-at-point)) (setq eww-bookmarks-directory (locate-user-emacs-file "eww-bookmarks/")) (setq eww-history-limit 150) (setq eww-use-external-browser-for-content-type "\\`\\(video/\\|audio\\)") ; On GNU/Linux check your mimeapps.list (setq eww-browse-url-new-window-is-tab nil) (setq eww-form-checkbox-selected-symbol "[X]") (setq eww-form-checkbox-symbol "[ ]") ;; NOTE `eww-retrieve-command' is for Emacs28. I tried the following ;; two values. The first would not render properly some plain text ;; pages, such as by messing up the spacing between paragraphs. The ;; second is more reliable but feels slower. So I just use the ;; default (nil), though I find wget to be a bit faster. In that case ;; one could live with the occasional errors by using `eww-download' ;; on the offending page, but I prefer consistency. ;; ;; '("wget" "--quiet" "--output-document=-") ;; '("chromium" "--headless" "--dump-dom") (setq eww-retrieve-command nil))
5.12.6. TODO The prot-emacs-web.el
extras for eww
(prot-eww.el
)
;;;; `prot-eww' extras (use-package prot-eww :ensure nil :after eww :config (setq prot-eww-save-history-file (locate-user-emacs-file "prot-eww-visited-history")) (setq prot-eww-save-visited-history t) (setq prot-eww-bookmark-link nil) (add-hook 'prot-eww-history-mode-hook #'hl-line-mode) (define-prefix-command 'prot-eww-map) (define-key global-map (kbd "C-c w") 'prot-eww-map) (prot-emacs-keybind prot-eww-map "b" #'prot-eww-visit-bookmark "e" #'prot-eww-browse-dwim "s" #'prot-eww-search-engine) (prot-emacs-keybind eww-mode-map "B" #'prot-eww-bookmark-page "D" #'prot-eww-download-html "F" #'prot-eww-find-feed "H" #'prot-eww-list-history "b" #'prot-eww-visit-bookmark "e" #'prot-eww-browse-dwim "o" #'prot-eww-open-in-other-window "E" #'prot-eww-visit-url-on-page "J" #'prot-eww-jump-to-url-on-page "R" #'prot-eww-readable "Q" #'prot-eww-quit))
5.12.7. TODO The prot-emacs-web.el
RSS/Atom reader (elfeed
)
;;; Elfeed feed/RSS reader (use-package elfeed :ensure t :hook (elfeed-show-mode . visual-line-mode) :bind ("C-c e" . elfeed) :config (setq elfeed-use-curl nil) (setq elfeed-curl-max-connections 10) (setq elfeed-db-directory (concat user-emacs-directory "elfeed/")) (setq elfeed-enclosure-default-dir "~/Downloads/") (setq elfeed-search-filter "@2-weeks-ago +unread") (setq elfeed-sort-order 'descending) (setq elfeed-search-clipboard-type 'CLIPBOARD) (setq elfeed-search-title-max-width 100) (setq elfeed-search-title-min-width 30) (setq elfeed-search-trailing-width 25) (setq elfeed-show-truncate-long-urls t) (setq elfeed-show-unique-buffers t) (setq elfeed-search-date-format '("%F %R" 16 :left)) (prot-emacs-keybind elfeed-search-mode-map "w" #'elfeed-search-yank "g" #'elfeed-update "G" #'elfeed-search-update--force) (define-key elfeed-show-mode-map (kbd "w") #'elfeed-show-yank)) (use-package prot-elfeed :ensure nil :after elfeed :bind ( :map elfeed-search-mode-map ("s" . prot-elfeed-search-tag-filter) ("+" . prot-elfeed-toggle-tag) :map elfeed-show-mode-map ("+" . prot-elfeed-toggle-tag)) :hook (elfeed-search-mode . prot-elfeed-load-feeds) :config (setq prot-elfeed-tag-faces t) (prot-elfeed-fontify-tags))
5.12.8. TODO The prot-emacs-web.el
settings for the IRC client
;;; Rcirc (IRC client) (use-package rcirc :ensure nil :bind ("C-c i" . irc) :config (setq rcirc-server-alist `(("irc.libera.chat" :channels ("#emacs" "#rcirc") :port 6697 :encryption tls :password ,(prot-common-auth-get-field "libera" :secret)))) (setq rcirc-prompt "%t> ") ; Read the docs or use (customize-set-variable 'rcirc-prompt "%t> ") (setq rcirc-default-nick "protesilaos" rcirc-default-user-name rcirc-default-nick rcirc-default-full-name "Protesilaos Stavrou") ;; ;; NOTE 2021-11-28: demo from the days of EmacsConf 2021. I don't ;; ;; actually need this. ;; (setq rcirc-bright-nicks '("bandali" "sachac" "zaeph")) ;; NOTE 2021-11-28: Is there a canonical way to disable this? (setq rcirc-timeout-seconds most-positive-fixnum) (rcirc-track-minor-mode 1))
(provide 'prot-emacs-web)
5.13. The prot-emacs-which-key.el
module
There is a user option in my setup to load this module
(The init.el
option to enable which-key
).
When the which-key-mode
is enabled, any incomplete key sequence will
produce a popup at the lower part of the Emacs frame showing keys that
complete the current sequence together with the name of the command
they are invoking.
I personally do not use this, except when I need to do a demonstration.
Note that which-key
is built into Emacs 30. If you are on a previous
version, use :ensure t
to install it from a package archive.
(use-package which-key :ensure nil ; built into Emacs 30 :hook (after-init . which-key-mode) :config (setq which-key-separator " ") (setq which-key-prefix-prefix "... ") (setq which-key-max-display-columns 3) (setq which-key-idle-delay 1.5) (setq which-key-idle-secondary-delay 0.25) (setq which-key-add-column-padding 1) (setq which-key-max-description-length 40)) (provide 'prot-emacs-which-key)
5.14. The prot-emacs-icons.el
module (nerd-icons
for various packages)
I define a user option to conditionally load icons in various parts of
the Emacs interface (The init.el
option to load icons (nerd-icons.el
)).
These are purely cosmetic. I normally work without them, though I do
enable them for video demonstrations because they communicate more
information, which some people may find helpful.
Remember that these packages do not automatically install any font
files. The user must handle this step by invoking the command
nerd-icons-install-fonts
.
;;; Icons (use-package nerd-icons :ensure t) (use-package nerd-icons-completion :ensure t :if (display-graphic-p) :after marginalia ;; FIXME 2024-09-01: For some reason this stopped working because it ;; macroexpands to `marginalia-mode' instead of ;; `marginalia-mode-hook'. What is more puzzling is that this does ;; not happen in the next :hook... ;; :hook (marginalia-mode . nerd-icons-completion-marginalia-setup)) :config (add-hook 'marginalia-mode-hook #'nerd-icons-completion-marginalia-setup)) (use-package nerd-icons-corfu :ensure t :if (display-graphic-p) :after corfu :config (add-to-list 'corfu-margin-formatters #'nerd-icons-corfu-formatter)) (use-package nerd-icons-dired :ensure t :if (display-graphic-p) :hook (dired-mode . nerd-icons-dired-mode)) (provide 'prot-emacs-icons)
6. The custom libraries of my configuration
Each of the following subsections is dedicated to an individual custom library. These are “packages” of mine that are only relevant to my Emacs configuration, even though they are designed in accordance with best practices for packaging Emacs Lisp code. Many of my public-facing packages for Emacs started out as custom libraries like these (The init.el arrangements for my own modules and custom libraries).
Please bear in mind that the code I write here is not necessarily as high quality as what I put in my public packages, meaning that I do not test it as much and do not try to make it perfect.
6.1. The prot-abbrev.el
library
;;; prot-abbrev.el --- Functions for use with abbrev-mode -*- lexical-binding: t -*- ;; Copyright (C) 2024-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or (at ;; your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; Functions for use with `abbrev-mode'. ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (defgroup prot-abbrev () "Functions for use with `abbrev-mode'." :group 'editing) (defcustom prot-abbrev-time-specifier "%R" "Time specifier for `format-time-string'." :type 'string :group 'prot-abbrev) (defcustom prot-abbrev-date-specifier "%F" "Date specifier for `format-time-string'." :type 'string :group 'prot-abbrev) (defun prot-abbrev-current-time () "Insert the current time per `prot-abbrev-time-specifier'." (insert (format-time-string prot-abbrev-time-specifier))) (defun prot-abbrev-current-date () "Insert the current date per `prot-abbrev-date-specifier'." (insert (format-time-string prot-abbrev-date-specifier))) (defun prot-abbrev-jitsi-link () "Insert a Jitsi link." (insert (concat "https://meet.jit.si/" (format-time-string "%Y%m%dT%H%M%S")))) (defvar prot-abbrev-update-html-history nil "Minibuffer history for `prot-abbrev-update-html-prompt'.") (defun prot-abbrev-update-html-prompt () "Minibuffer prompt for `prot-abbrev-update-html'. Use completion among previous entries, retrieving their data from `prot-abbrev-update-html-history'." (completing-read "Insert update for manual: " prot-abbrev-update-html-history nil nil nil 'prot-abbrev-update-html-history)) (defun prot-abbrev-update-html () "Insert message to update NAME.html page, by prompting for NAME." (insert (format "Update %s.html" (prot-abbrev-update-html-prompt)))) (provide 'prot-abbrev) ;;; prot-abbrev.el ends here
6.2. The prot-comment.el
library
;;; prot-comment.el --- Extensions newcomment.el for my dotemacs -*- lexical-binding: t -*- ;; Copyright (C) 2021-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; This covers my newcomment.el extras, for use in my Emacs setup: ;; https://protesilaos.com/emacs/dotemacs. ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (require 'newcomment) (require 'prot-common) (defgroup prot-comment () "Extensions for newcomment.el." :group 'comment) (defcustom prot-comment-keywords '("TODO" "NOTE" "XXX" "REVIEW" "FIXME") "List of strings with keywords used by `prot-comment-timestamp-keyword'." :type '(repeat string) :group 'prot-comment) (defcustom prot-comment-timestamp-format-concise "%F" "Specifier for date in `prot-comment-timestamp-keyword'. Refer to the doc string of `format-time-string' for the available options." :type 'string :group 'prot-comment) (defcustom prot-comment-timestamp-format-verbose "%F %T %z" "Like `prot-comment-timestamp-format-concise', but longer." :type 'string :group 'prot-comment) ;;;###autoload (defun prot-comment (n) "Comment N lines, defaulting to the current one. When the region is active, comment its lines instead." (interactive "p") (if (use-region-p) (comment-or-uncomment-region (region-beginning) (region-end)) (comment-line n))) (make-obsolete 'prot-comment-comment-dwim 'prot-comment "2023-09-28") (defvar prot-comment--keyword-hist '() "Minibuffer history of `prot-comment--keyword-prompt'.") (defun prot-comment--keyword-prompt (keywords) "Prompt for candidate among KEYWORDS (per `prot-comment-timestamp-keyword')." (let ((def (car prot-comment--keyword-hist))) (completing-read (format "Select keyword [%s]: " def) keywords nil nil nil 'prot-comment--keyword-hist def))) (defun prot-comment--format-date (verbose) "Format date using `format-time-string'. VERBOSE has the same meaning as `prot-comment-timestamp-keyword'." (format-time-string (if verbose prot-comment-timestamp-format-verbose prot-comment-timestamp-format-concise))) (defun prot-comment--timestamp (keyword &optional verbose) "Format string using current time and KEYWORD. VERBOSE has the same meaning as `prot-comment-timestamp-keyword'." (format "%s %s: " keyword (prot-comment--format-date verbose))) (defun prot-comment--format-comment (string) "Format comment STRING per `prot-comment-timestamp-keyword'. STRING is a combination of a keyword and a time stamp." (concat comment-start (make-string comment-add (string-to-char comment-start)) comment-padding string comment-end)) (defun prot-comment--maybe-newline () "Call `newline' if current line is not empty. Check `prot-comment-timestamp-keyword' for the rationale." (unless (prot-common-line-regexp-p 'empty 1) (save-excursion (newline)))) ;;;###autoload (defun prot-comment-timestamp-keyword (keyword &optional verbose) "Add timestamped comment with KEYWORD. When called interactively, the list of possible keywords is that of `prot-comment-keywords', though it is possible to input arbitrary text. If point is at the beginning of the line or if line is empty (no characters at all or just indentation), the comment is started there in accordance with `comment-style'. Any existing text after the point will be pushed to a new line and will not be turned into a comment. If point is anywhere else on the line and the line is not empty, the comment is appended to the line with `comment-indent'. The comment is always formatted as DELIMITER KEYWORD DATE:, with the date format being controlled by the variable `prot-comment-timestamp-format-concise'. DELIMITER is the value of `comment-start', as defined by the current major mode. With optional VERBOSE argument (such as a prefix argument), use an alternative date format, as specified by `prot-comment-timestamp-format-verbose'." (interactive (list (prot-comment--keyword-prompt prot-comment-keywords) current-prefix-arg)) (let ((string (prot-comment--timestamp keyword verbose)) (beg (point))) (cond ((prot-common-line-regexp-p 'empty) (insert (prot-comment--format-comment string))) ((eq beg (line-beginning-position)) (insert (prot-comment--format-comment string)) (indent-region beg (point)) (prot-comment--maybe-newline)) (t (comment-indent t) (insert (concat " " string)))))) (provide 'prot-comment) ;;; prot-comment.el ends here
6.3. The prot-common.el
library
;;; prot-common.el --- Common functions for my dotemacs -*- lexical-binding: t -*- ;; Copyright (C) 2020-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or (at ;; your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; Common functions for my Emacs: <https://protesilaos.com/emacs/dotemacs/>. ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (eval-when-compile (require 'subr-x) (require 'cl-lib)) (defgroup prot-common () "Auxiliary functions for my dotemacs." :group 'editing) ;;;###autoload (defun prot-common-number-even-p (n) "Test if N is an even number." (if (numberp n) (= (% n 2) 0) (error "%s is not a number" n))) ;;;###autoload (defun prot-common-number-integer-p (n) "Test if N is an integer." (if (integerp n) n (error "%s is not an integer" n))) ;;;###autoload (defun prot-common-number-integer-positive-p (n) "Test if N is a positive integer." (if (prot-common-number-integer-p n) (> n 0) (error "%s is not a positive integer" n))) ;; Thanks to Gabriel for providing a cleaner version of ;; `prot-common-number-negative': <https://github.com/gabriel376>. ;;;###autoload (defun prot-common-number-negative (n) "Make N negative." (if (and (numberp n) (> n 0)) (* -1 n) (error "%s is not a valid positive number" n))) ;;;###autoload (defun prot-common-reverse-percentage (number percent change-p) "Determine the original value of NUMBER given PERCENT. CHANGE-P should specify the increase or decrease. For simplicity, nil means decrease while non-nil stands for an increase. NUMBER must satisfy `numberp', while PERCENT must be `natnump'." (unless (numberp number) (user-error "NUMBER must satisfy numberp")) (unless (natnump percent) (user-error "PERCENT must satisfy natnump")) (let* ((pc (/ (float percent) 100)) (pc-change (if change-p (+ 1 pc) pc)) (n (if change-p pc-change (float (- 1 pc-change))))) ;; FIXME 2021-12-21: If float, round to 4 decimal points. (/ number n))) ;;;###autoload (defun prot-common-percentage-change (n-original n-final) "Find percentage change between N-ORIGINAL and N-FINAL numbers. When the percentage is not an integer, it is rounded to 4 floating points: 16.666666666666664 => 16.667." (unless (numberp n-original) (user-error "N-ORIGINAL must satisfy numberp")) (unless (numberp n-final) (user-error "N-FINAL must satisfy numberp")) (let* ((difference (float (abs (- n-original n-final)))) (n (* (/ difference n-original) 100)) (round (floor n))) ;; FIXME 2021-12-21: Any way to avoid the `string-to-number'? (if (> n round) (string-to-number (format "%0.4f" n)) round))) ;; REVIEW 2023-04-07 07:43 +0300: I just wrote the conversions from ;; seconds. Hopefully they are correct, but I need to double check. (defun prot-common-seconds-to-minutes (seconds) "Convert a number representing SECONDS to MM:SS notation." (let ((minutes (/ seconds 60)) (seconds (% seconds 60))) (format "%.2d:%.2d" minutes seconds))) (defun prot-common-seconds-to-hours (seconds) "Convert a number representing SECONDS to HH:MM:SS notation." (let* ((hours (/ seconds 3600)) (minutes (/ (% seconds 3600) 60)) (seconds (% seconds 60))) (format "%.2d:%.2d:%.2d" hours minutes seconds))) ;;;###autoload (defun prot-common-seconds-to-minutes-or-hours (seconds) "Convert SECONDS to either minutes or hours, depending on the value." (if (> seconds 3599) (prot-common-seconds-to-hours seconds) (prot-common-seconds-to-minutes seconds))) ;;;###autoload (defun prot-common-rotate-list-of-symbol (symbol) "Rotate list value of SYMBOL by moving its car to the end. Return the first element before performing the rotation. This means that if `sample-list' has an initial value of `(one two three)', this function will first return `one' and update the value of `sample-list' to `(two three one)'. Subsequent calls will continue rotating accordingly." (unless (symbolp symbol) (user-error "%s is not a symbol" symbol)) (when-let* ((value (symbol-value symbol)) (list (and (listp value) value)) (first (car list))) (set symbol (append (cdr list) (list first))) first)) ;;;###autoload (defun prot-common-empty-buffer-p () "Test whether the buffer is empty." (or (= (point-min) (point-max)) (save-excursion (goto-char (point-min)) (while (and (looking-at "^\\([a-zA-Z]+: ?\\)?$") (zerop (forward-line 1)))) (eobp)))) ;;;###autoload (defun prot-common-minor-modes-active () "Return list of active minor modes for the current buffer." (let ((active-modes)) (mapc (lambda (m) (when (and (boundp m) (symbol-value m)) (push m active-modes))) minor-mode-list) active-modes)) ;;;###autoload (defun prot-common-truncate-lines-silently () "Toggle line truncation without printing messages." (let ((inhibit-message t)) (toggle-truncate-lines t))) ;; NOTE 2023-08-12: I tried the `clear-message-function', but it did ;; not work. What I need is very simple and this gets the job done. ;;;###autoload (defun prot-common-clear-minibuffer-message (&rest _) "Print an empty message to clear the echo area. Use this as advice :after a noisy function." (message "")) ;;;###autoload (defun prot-common-disable-hl-line () "Disable Hl-Line-Mode (for hooks)." (hl-line-mode -1)) ;;;###autoload (defun prot-common-window-bounds () "Return start and end points in the window as a cons cell." (cons (window-start) (window-end))) ;;;###autoload (defun prot-common-page-p () "Return non-nil if there is a `page-delimiter' in the buffer." (or (save-excursion (re-search-forward page-delimiter nil t)) (save-excursion (re-search-backward page-delimiter nil t)))) ;;;###autoload (defun prot-common-window-small-p () "Return non-nil if window is small. Check if the `window-width' or `window-height' is less than `split-width-threshold' and `split-height-threshold', respectively." (or (and (numberp split-width-threshold) (< (window-total-width) split-width-threshold)) (and (numberp split-height-threshold) (> (window-total-height) split-height-threshold)))) (defun prot-common-window-narrow-p () "Return non-nil if window is narrow. Check if the `window-width' is less than `split-width-threshold'." (and (numberp split-width-threshold) (< (window-total-width) split-width-threshold))) ;;;###autoload (defun prot-common-three-or-more-windows-p (&optional frame) "Return non-nil if three or more windows occupy FRAME. If FRAME is non-nil, inspect the current frame." (>= (length (window-list frame :no-minibuffer)) 3)) ;;;###autoload (defun prot-common-read-data (file) "Read Elisp data from FILE." (with-temp-buffer (insert-file-contents file) (read (current-buffer)))) ;;;###autoload (defun prot-common-completion-category () "Return completion category." (when-let* ((window (active-minibuffer-window))) (with-current-buffer (window-buffer window) (completion-metadata-get (completion-metadata (buffer-substring-no-properties (minibuffer-prompt-end) (max (minibuffer-prompt-end) (point))) minibuffer-completion-table minibuffer-completion-predicate) 'category)))) ;; Thanks to Omar Antolín Camarena for providing this snippet! ;;;###autoload (defun prot-common-completion-table (category candidates) "Pass appropriate metadata CATEGORY to completion CANDIDATES. This is intended for bespoke functions that need to pass completion metadata that can then be parsed by other tools (e.g. `embark')." (lambda (string pred action) (if (eq action 'metadata) `(metadata (category . ,category)) (complete-with-action action candidates string pred)))) ;;;###autoload (defun prot-common-completion-table-no-sort (category candidates) "Pass appropriate metadata CATEGORY to completion CANDIDATES. Like `prot-common-completion-table' but also disable sorting." (lambda (string pred action) (if (eq action 'metadata) `(metadata (category . ,category) (display-sort-function . ,#'identity)) (complete-with-action action candidates string pred)))) ;; Thanks to Igor Lima for the `prot-common-crm-exclude-selected-p': ;; <https://github.com/0x462e41>. ;; This is used as a filter predicate in the relevant prompts. (defvar crm-separator) ;;;###autoload (defun prot-common-crm-exclude-selected-p (input) "Filter out INPUT from `completing-read-multiple'. Hide non-destructively the selected entries from the completion table, thus avoiding the risk of inputting the same match twice. To be used as the PREDICATE of `completing-read-multiple'." (if-let* ((pos (string-match-p crm-separator input)) (rev-input (reverse input)) (element (reverse (substring rev-input 0 (string-match-p crm-separator rev-input)))) (flag t)) (progn (while pos (if (string= (substring input 0 pos) element) (setq pos nil) (setq input (substring input (1+ pos)) pos (string-match-p crm-separator input) flag (when pos t)))) (not flag)) t)) ;; The `prot-common-line-regexp-p' and `prot-common--line-regexp-alist' ;; are contributed by Gabriel: <https://github.com/gabriel376>. They ;; provide a more elegant approach to using a macro, as shown further ;; below. (defvar prot-common--line-regexp-alist '((empty . "[\s\t]*$") (indent . "^[\s\t]+") (non-empty . "^.+$") (list . "^\\([\s\t#*+]+\\|[0-9]+[^\s]?[).]+\\)") (heading . "^[=-]+")) "Alist of regexp types used by `prot-common-line-regexp-p'.") (defun prot-common-line-regexp-p (type &optional n) "Test for TYPE on line. TYPE is the car of a cons cell in `prot-common--line-regexp-alist'. It matches a regular expression. With optional N, search in the Nth line from point." (save-excursion (goto-char (line-beginning-position)) (and (not (bobp)) (or (beginning-of-line n) t) (save-match-data (looking-at (alist-get type prot-common--line-regexp-alist)))))) ;; The `prot-common-shell-command-with-exit-code-and-output' function is ;; courtesy of Harold Carr, who also sent a patch that improved ;; `prot-eww-download-html' (from the `prot-eww.el' library). ;; ;; More about Harold: <http://haroldcarr.com/about/>. (defun prot-common-shell-command-with-exit-code-and-output (command &rest args) "Run COMMAND with ARGS. Return the exit code and output in a list." (with-temp-buffer (list (apply 'call-process command nil (current-buffer) nil args) (buffer-string)))) (defvar prot-common-url-regexp (concat "~?\\<\\([-a-zA-Z0-9+&@#/%?=~_|!:,.;]*\\)" "[.@]" "\\([-a-zA-Z0-9+&@#/%?=~_|!:,.;]+\\)\\>/?") "Regular expression to match (most?) URLs or email addresses.") (autoload 'auth-source-search "auth-source") ;;;###autoload (defun prot-common-auth-get-field (host prop) "Find PROP in `auth-sources' for HOST entry." (when-let* ((source (auth-source-search :host host))) (if (eq prop :secret) (funcall (plist-get (car source) prop)) (plist-get (flatten-list source) prop)))) ;;;###autoload (defun prot-common-parse-file-as-list (file) "Return the contents of FILE as a list of strings. Strings are split at newline characters and are then trimmed for negative space. Use this function to provide a list of candidates for completion (per `completing-read')." (split-string (with-temp-buffer (insert-file-contents file) (buffer-substring-no-properties (point-min) (point-max))) "\n" :omit-nulls "[\s\f\t\n\r\v]+")) (defun prot-common-ignore (&rest _) "Use this as override advice to make a function do nothing." nil) ;; NOTE 2023-06-02: The `prot-common-wcag-formula' and ;; `prot-common-contrast' are taken verbatim from my `modus-themes' ;; and renamed to have the prefix `prot-common-' instead of ;; `modus-themes-'. This is all my code, of course, but I do it this ;; way to ensure that this file is self-contained in case someone ;; copies it. ;; This is the WCAG formula: <https://www.w3.org/TR/WCAG20-TECHS/G18.html>. (defun prot-common-wcag-formula (hex) "Get WCAG value of color value HEX. The value is defined in hexadecimal RGB notation, such #123456." (cl-loop for k in '(0.2126 0.7152 0.0722) for x in (color-name-to-rgb hex) sum (* k (if (<= x 0.03928) (/ x 12.92) (expt (/ (+ x 0.055) 1.055) 2.4))))) ;;;###autoload (defun prot-common-contrast (c1 c2) "Measure WCAG contrast ratio between C1 and C2. C1 and C2 are color values written in hexadecimal RGB." (let ((ct (/ (+ (prot-common-wcag-formula c1) 0.05) (+ (prot-common-wcag-formula c2) 0.05)))) (max ct (/ ct)))) ;;;; EXPERIMENTAL macros (not meant to be used anywhere) ;; TODO 2023-09-30: Try the same with `cl-defmacro' and &key (defmacro prot-common-if (condition &rest consequences) "Separate the CONSEQUENCES of CONDITION semantically. Like `if', `when', `unless' but done by using `:then' and `:else' keywords. The forms under each keyword of `:then' and `:else' belong to the given subset of CONSEQUENCES. - The absence of `:else' means: (if CONDITION (progn CONSEQUENCES)). - The absence of `:then' means: (if CONDITION nil CONSEQUENCES). - Otherwise: (if CONDITION (progn then-CONSEQUENCES) else-CONSEQUENCES)." (declare (indent 1)) (let (then-consequences else-consequences last-kw) (dolist (elt consequences) (let ((is-keyword (keywordp elt))) (cond ((and (not is-keyword) (eq last-kw :then)) (push elt then-consequences)) ((and (not is-keyword) (eq last-kw :else)) (push elt else-consequences)) ((and is-keyword (eq elt :then)) (setq last-kw :then)) ((and is-keyword (eq elt :else)) (setq last-kw :else))))) `(if ,condition ,(if then-consequences `(progn ,@(nreverse then-consequences)) nil) ,@(nreverse else-consequences)))) (provide 'prot-common) ;;; prot-common.el ends here
6.4. The prot-dired.el
library
;;; prot-dired.el --- Extensions to dired.el for my dotemacs -*- lexical-binding: t -*- ;; Copyright (C) 2020-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; This covers my dired.el extensions, for use in my Emacs setup: ;; https://protesilaos.com/emacs/dotemacs. ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (require 'prot-common) (require 'dired) (require 'dired-aux) (defgroup prot-dired () "Extensions for Dired." :group 'dired) ;;;; Flat Dired listing (defvar prot-dired-regexp-history nil "Minibuffer history of `prot-dired-regexp-prompt'.") (defun prot-dired-regexp-prompt () (let ((default (car prot-dired-regexp-history))) (read-regexp (format-prompt "Files matching REGEXP" default) default 'prot-dired-regexp-history))) (defun prot-dired--get-files (regexp) "Return files matching REGEXP, recursively from `default-directory'." (directory-files-recursively default-directory regexp nil)) ;;;###autoload (defun prot-dired-search-flat-list (regexp) "Return a Dired buffer for files matching REGEXP. Perform the search recursively from the current directory." (interactive (list (prot-dired-regexp-prompt))) (if-let* ((files (prot-dired--get-files regexp)) (relative-paths (mapcar #'file-relative-name files))) (dired (cons (format "prot-flat-dired for `%s'" regexp) relative-paths)) (error "No files matching `%s'" regexp))) ;;;; General commands ;; NOTE 2023-06-27: This user option is quick-and-dirty. I prefer not ;; to have an option at all and simply do the right thing based on ;; `dired-guess-shell-alist-user'. (defcustom prot-dired-always-external-regexp "\\(mkv\\|mp4\\|mp4\\|ogg\\|m4a\\|webm\\)" "Regular expression of file extensions to open externally. The test is performed by `prot-dired-open-dwim', which then defers to the `dired-guess-shell-alist-user'." :group 'prot-dired :type 'string) ;; NOTE 2023-06-27: This is a proof-of-concept. See the previous ;; note. (defun prot-dired-open-dwim (files) "Open FILES using the appropriate program." (interactive (list (dired-get-marked-files))) (if-let* ((extension (file-name-extension (car files))) ((string-match-p extension prot-dired-always-external-regexp)) (guess (dired-guess-default files)) (program (if (listp guess) (car guess) guess))) (dired-do-async-shell-command program nil files) (find-file (car files)))) (defvar prot-dired--limit-hist '() "Minibuffer history for `prot-dired-limit-regexp'.") ;;;###autoload (defun prot-dired-limit-regexp (regexp omit) "Limit Dired to keep files matching REGEXP. With optional OMIT argument as a prefix (\\[universal-argument]), exclude files matching REGEXP. Restore the buffer with \\<dired-mode-map>`\\[revert-buffer]'." (interactive (list (read-regexp (concat "Files " (when current-prefix-arg (propertize "NOT " 'face 'warning)) "matching PATTERN: ") nil 'prot-dired--limit-hist) current-prefix-arg)) (dired-mark-files-regexp regexp) (unless omit (dired-toggle-marks)) (dired-do-kill-lines) (add-to-history 'prot-dired--limit-hist regexp)) (defvar prot-dired--find-grep-hist '() "Minibuffer history for `prot-dired-grep-marked-files'.") ;; Also see `prot-search-grep' from prot-search.el. ;;;###autoload (defun prot-dired-grep-marked-files (regexp &optional arg) "Run `find' with `grep' for REGEXP on marked files. When no files are marked or when just a single one is marked, search the entire directory instead. With optional prefix ARG target a single marked file. We assume that there is no point in marking a single file and running find+grep on its contents. Visit it and call `occur' or run grep directly on it without the whole find part." (interactive (list (read-string "grep for PATTERN (marked files OR current directory): " nil 'prot-dired--find-grep-hist) current-prefix-arg) dired-mode) (when-let* ((marks (dired-get-marked-files 'no-dir)) (files (mapconcat #'identity marks " ")) (args (if (or arg (length> marks 1)) ;; Thanks to Sean Whitton for pointing out an ;; earlier superfluity of mine: we do not need ;; to call grep through find when we already ;; know the files we want to search in. Check ;; Sean's dotfiles: ;; <https://git.spwhitton.name/dotfiles>. ;; ;; Any other errors or omissions are my own. (format "grep -nH --color=auto %s %s" (shell-quote-argument regexp) files) (concat "find . -not " (shell-quote-argument "(") " -wholename " (shell-quote-argument "*/.git*") " -prune " (shell-quote-argument ")") " -type f" " -exec grep -nHE --color=auto " regexp " " (shell-quote-argument "{}") " " (shell-quote-argument ";") " ")))) (compilation-start args 'grep-mode (lambda (mode) (format "*prot-dired-find-%s for '%s'" mode regexp)) t))) ;;;; Subdir extras and Imenu setup (defvar prot-dired--directory-header-regexp "^ +\\(.+\\):\n" "Pattern to match Dired directory headings.") ;;;###autoload (defun prot-dired-subdirectory-next (&optional arg) "Move to next or optional ARGth Dired subdirectory heading. For more on such headings, read `dired-maybe-insert-subdir'." (interactive "p") (let ((pos (point)) (subdir prot-dired--directory-header-regexp)) (goto-char (line-end-position)) (if (re-search-forward subdir nil t (or arg nil)) (progn (goto-char (match-beginning 1)) (goto-char (line-beginning-position))) (goto-char pos)))) ;;;###autoload (defun prot-dired-subdirectory-previous (&optional arg) "Move to previous or optional ARGth Dired subdirectory heading. For more on such headings, read `dired-maybe-insert-subdir'." (interactive "p") (let ((pos (point)) (subdir prot-dired--directory-header-regexp)) (goto-char (line-beginning-position)) (if (re-search-backward subdir nil t (or arg nil)) (goto-char (line-beginning-position)) (goto-char pos)))) (autoload 'dired-current-directory "dired") (autoload 'dired-kill-subdir "dired-aux") ;;;###autoload (defun prot-dired-remove-inserted-subdirs () "Remove all inserted Dired subdirectories." (interactive) (goto-char (point-max)) (while (and (prot-dired-subdirectory-previous) (not (equal (dired-current-directory) (expand-file-name default-directory)))) (dired-kill-subdir))) (autoload 'cl-remove-if-not "cl-seq") (defun prot-dired--dir-list (list) "Filter out non-directory file paths in LIST." (cl-remove-if-not (lambda (dir) (file-directory-p dir)) list)) (defun prot-dired--insert-dir (dir &optional flags) "Insert DIR using optional FLAGS." (dired-maybe-insert-subdir (expand-file-name dir) (or flags nil))) (autoload 'dired-get-filename "dired") (autoload 'dired-get-marked-files "dired") (autoload 'dired-maybe-insert-subdir "dired-aux") (defvar dired-subdir-switches) (defvar dired-actual-switches) ;;;###autoload (defun prot-dired-insert-subdir (&optional arg) "Generic command to insert subdirectories in Dired buffers. When items are marked, insert those which are subsirectories of the current directory. Ignore regular files. If no marks are active and point is on a subdirectory line, insert it directly. If no marks are active and point is not on a subdirectory line, prompt for a subdirectory using completion. With optional ARG as a single prefix (`\\[universal-argument]') argument, prompt for command line flags to pass to the underlying ls program. With optional ARG as a double prefix argument, remove all inserted subdirectories." (interactive "p") (let* ((name (dired-get-marked-files)) (flags (when (eq arg 4) (read-string "Flags for `ls' listing: " (or dired-subdir-switches dired-actual-switches))))) (cond ; NOTE 2021-07-20: `length>', `length=' are from Emacs28 ((eq arg 16) (prot-dired-remove-inserted-subdirs)) ((and (length> name 1) (prot-dired--dir-list name)) (mapc (lambda (file) (when (file-directory-p file) (prot-dired--insert-dir file flags))) name)) ((and (length= name 1) (file-directory-p (car name))) (prot-dired--insert-dir (car name) flags)) (t (let ((selection (read-directory-name "Insert directory: "))) (prot-dired--insert-dir selection flags)))))) (defun prot-dired--imenu-prev-index-position () "Find the previous file in the buffer." (let ((subdir prot-dired--directory-header-regexp)) (re-search-backward subdir nil t))) (defun prot-dired--imenu-extract-index-name () "Return the name of the file at point." (file-relative-name (buffer-substring-no-properties (+ (line-beginning-position) 2) (1- (line-end-position))))) ;;;###autoload (defun prot-dired-setup-imenu () "Configure imenu for the current Dired buffer. Add this to `dired-mode-hook'." (set (make-local-variable 'imenu-prev-index-position-function) 'prot-dired--imenu-prev-index-position) (set (make-local-variable 'imenu-extract-index-name-function) 'prot-dired--imenu-extract-index-name)) (provide 'prot-dired) ;;; prot-dired.el ends here
6.5. The prot-elfeed.el
library
;;; prot-elfeed.el --- Elfeed extensions for my dotemacs -*- lexical-binding: t -*- ;; Copyright (C) 2021-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; NOTE 2022-06-08: This is old code. There are things I would like to ;; improve. ;; ;; Extensions for Elfeed, intended for use in my Emacs setup: ;; https://protesilaos.com/emacs/dotemacs. ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (eval-when-compile (require 'subr-x)) (require 'elfeed nil t) (require 'url-util) (require 'prot-common) (defgroup prot-elfeed () "Personal extensions for Elfeed." :group 'elfeed) (defcustom prot-elfeed-feeds-file (thread-last user-emacs-directory (expand-file-name "feeds.el.gpg")) "Path to file with `elfeed-feeds'." :type 'string :group 'prot-elfeed) (defcustom prot-elfeed-archives-directory "~/Documents/feeds/" "Path to directory for storing Elfeed entries." :type 'string :group 'prot-elfeed) (defcustom prot-elfeed-tag-faces nil "Add faces for certain tags. The tags are: critical, important, personal." :type 'boolean :group 'prot-elfeed) (defcustom prot-elfeed-search-tags '(critical important personal) "List of user-defined tags. Used by `prot-elfeed-toggle-tag'." :type 'list :group 'prot-elfeed) (defface prot-elfeed-entry-critical '((t :inherit font-lock-warning-face)) "Face for Elfeed entries tagged with `critical'.") (defface prot-elfeed-entry-important '((t :inherit font-lock-constant-face)) "Face for Elfeed entries tagged with `important'.") (defface prot-elfeed-entry-personal '((t :inherit font-lock-variable-name-face)) "Face for Elfeed entries tagged with `personal'.") ;;;; Utilities ;;;###autoload (defun prot-elfeed-load-feeds () "Load file containing the `elfeed-feeds' list. Add this to `elfeed-search-mode-hook'." (let ((feeds prot-elfeed-feeds-file)) (if (file-exists-p feeds) (load-file feeds) (user-error "Missing feeds' file")))) (defvar elfeed-search-face-alist) ;;;###autoload (defun prot-elfeed-fontify-tags () "Expand Elfeed faces if `prot-elfeed-tag-faces' is non-nil." (if prot-elfeed-tag-faces (setq elfeed-search-face-alist '((critical prot-elfeed-entry-critical) (important prot-elfeed-entry-important) (personal prot-elfeed-entry-personal) (unread elfeed-search-unread-title-face))) (setq elfeed-search-face-alist '((unread elfeed-search-unread-title-face))))) (defvar prot-elfeed--tag-hist '() "History of inputs for `prot-elfeed-toggle-tag'.") (defun prot-elfeed--character-prompt (tags) "Helper of `prot-elfeed-toggle-tag' to read TAGS." (let ((def (car prot-elfeed--tag-hist))) (completing-read (format "Toggle tag [%s]: " def) tags nil t nil 'prot-elfeed--tag-hist def))) (defvar elfeed-show-entry) (declare-function elfeed-tagged-p "elfeed") (declare-function elfeed-search-toggle-all "elfeed") (declare-function elfeed-show-tag "elfeed") (declare-function elfeed-show-untag "elfeed") ;;;###autoload (defun prot-elfeed-toggle-tag (tag) "Toggle TAG for the current item. When the region is active in the `elfeed-search-mode' buffer, all entries encompassed by it are affected. Otherwise the item at point is the target. For `elfeed-show-mode', the current entry is always the target. The list of tags is provided by `prot-elfeed-search-tags'." (interactive (list (intern (prot-elfeed--character-prompt prot-elfeed-search-tags)))) (if (derived-mode-p 'elfeed-show-mode) (if (elfeed-tagged-p tag elfeed-show-entry) (elfeed-show-untag tag) (elfeed-show-tag tag)) (elfeed-search-toggle-all tag))) (defvar elfeed-show-truncate-long-urls) (declare-function elfeed-entry-title "elfeed") (declare-function elfeed-show-refresh "elfeed") ;;;; General commands (defvar elfeed-search-filter-active) (defvar elfeed-search-filter) (declare-function elfeed-db-get-all-tags "elfeed") (declare-function elfeed-search-update "elfeed") (declare-function elfeed-search-clear-filter "elfeed") (defun prot-elfeed--format-tags (tags sign) "Prefix SIGN to each tag in TAGS." (mapcar (lambda (tag) (format "%s%s" sign tag)) tags)) ;;;###autoload (defun prot-elfeed-search-tag-filter () "Filter Elfeed search buffer by tags using completion. Completion accepts multiple inputs, delimited by `crm-separator'. Arbitrary input is also possible, but you may have to exit the minibuffer with something like `exit-minibuffer'." (interactive) (unwind-protect (elfeed-search-clear-filter) (let* ((elfeed-search-filter-active :live) (db-tags (elfeed-db-get-all-tags)) (plus-tags (prot-elfeed--format-tags db-tags "+")) (minus-tags (prot-elfeed--format-tags db-tags "-")) (all-tags (delete-dups (append plus-tags minus-tags))) (tags (completing-read-multiple "Apply one or more tags: " all-tags #'prot-common-crm-exclude-selected-p t)) (input (string-join `(,elfeed-search-filter ,@tags) " "))) (setq elfeed-search-filter input)) (elfeed-search-update :force))) (provide 'prot-elfeed) ;;; prot-elfeed.el ends here
6.6. The prot-embark.el
library
;;; prot-embark.el --- Custom Embark keymaps -*- lexical-binding: t -*- ;; Copyright (C) 2023-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1") (embark "0.23")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or (at ;; your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (require 'embark) (defvar-keymap prot-embark-general-map :parent embark-general-map "i" #'embark-insert "w" #'embark-copy-as-kill "E" #'embark-export "S" #'embark-collect "A" #'embark-act-all "DEL" #'delete-region) (defvar-keymap prot-embark-url-map :parent embark-general-map "b" #'browse-url "d" #'embark-download-url "e" #'eww) (defvar-keymap prot-embark-buffer-map :parent embark-general-map "k" #'prot-simple-kill-buffer "o" #'switch-to-buffer-other-window "e" #'ediff-buffers) (add-to-list 'embark-post-action-hooks (list 'prot-simple-kill-buffer 'embark--restart)) (defvar-keymap prot-embark-file-map :parent embark-general-map "f" #'find-file "j" #'embark-dired-jump "c" #'copy-file "e" #'ediff-files) (defvar-keymap prot-embark-identifier-map :parent embark-general-map "h" #'display-local-help "." #'xref-find-definitions "o" #'occur) (defvar-keymap prot-embark-command-map :parent embark-general-map "h" #'describe-command "." #'embark-find-definition) (defvar-keymap prot-embark-expression-map :parent embark-general-map "e" #'pp-eval-expression "m" #'pp-macroexpand-expression) (defvar-keymap prot-embark-function-map :parent embark-general-map "h" #'describe-function "." #'embark-find-definition) (defvar-keymap prot-embark-package-map :parent embark-general-map "h" #'describe-package "i" #'package-install "d" #'package-delete "r" #'package-reinstall "u" #'embark-browse-package-url "w" #'embark-save-package-url) (defvar-keymap prot-embark-symbol-map :parent embark-general-map "h" #'describe-symbol "." #'embark-find-definition) (defvar-keymap prot-embark-variable-map :parent embark-general-map "h" #'describe-variable "." #'embark-find-definition) (defvar-keymap prot-embark-region-map :parent embark-general-map "a" #'align-regexp "D" #'delete-duplicate-lines "f" #'flush-lines "i" #'epa-import-keys-region "d" #'epa-decrypt-armor-in-region "r" #'repunctuate-sentences "s" #'sort-lines "u" #'untabify) ;; The minimal indicator shows cycling options, but I have no use ;; for those. I want it to be silent. (defun prot-embark-no-minimal-indicator ()) (advice-add #'embark-minimal-indicator :override #'prot-embark-no-minimal-indicator) (defun prot-embark-act-no-quit () "Call `embark-act' but do not quit after the action." (interactive) (let ((embark-quit-after-action nil)) (call-interactively #'embark-act))) (defun prot-embark-act-quit () "Call `embark-act' and quit after the action." (interactive) (let ((embark-quit-after-action t)) (call-interactively #'embark-act)) (when (and (> (minibuffer-depth) 0) (derived-mode-p 'completion-list-mode)) (abort-recursive-edit))) (provide 'prot-embark) ;;; prot-embark.el ends here
6.7. The prot-eww.el
library
;;; prot-eww.el --- Extensions for EWW -*- lexical-binding: t -*- ;; Copyright (C) 2021-2024 Protesilaos Stavrou, Abhiseck Paira ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; Abhiseck Paira <abhiseckpaira@disroot.org> ;; Maintainer: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or (at ;; your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; Extensions for the eww, intended for my Emacs setup: ;; <https://protesilaos.com/emacs/dotemacs/>. ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;; XXX NOTE XXX 2023-05-19: Much of this code is severely out of date. ;; I plan to review it. DO NOT USE!!! ;;; Code: (require 'shr) (require 'eww) (require 'url-parse) (require 'prot-common) (defgroup prot-eww () "Tweaks for EWW." :group 'eww) ;;;; Basic setup ;; TODO 2021-10-15: Deprecate this in favour of what we added to Emacs29. ;; <https://protesilaos.com/codelog/2021-10-15-emacs-29-eww-rename-buffers/>. (defun prot-eww--rename-buffer () "Rename EWW buffer using page title or URL. To be used by `eww-after-render-hook'." (let ((name (if (equal "" (plist-get eww-data :title)) (plist-get eww-data :url) (plist-get eww-data :title)))) (rename-buffer (format "*%s # eww*" name) t))) (add-hook 'eww-after-render-hook #'prot-eww--rename-buffer) (advice-add 'eww-back-url :after #'prot-eww--rename-buffer) (advice-add 'eww-forward-url :after #'prot-eww--rename-buffer) ;;;; History extras (defvar prot-eww-visited-history '() "History of visited URLs.") (defcustom prot-eww-save-history-file (locate-user-emacs-file "prot-eww-visited-history") "File to save the value of `prot-eww-visited-history'." :type 'file :group 'prot-eww) (defcustom prot-eww-save-visited-history nil "Whether to save `prot-eww-visited-history'. If non-nil, save the value of `prot-eww-visited-history' in `prot-eww-save-history-file'." :type 'boolean :group 'prot-eww) (defcustom prot-eww-list-history-buffer "*prot-eww-history*" "Name of buffer for `prot-eww-list-history'." :type 'string :group 'prot-eww) ;; These history related functions are adapted from eww. (defun prot-eww--save-visited-history () "Save the value of `prot-eww-visited-history' in a file. The file is determined by the variable `prot-eww-save-history-file'." (when prot-eww-save-visited-history (with-temp-file prot-eww-save-history-file (insert (concat ";; Auto-generated file;" " don't edit -*- mode: lisp-data -*-\n")) (pp prot-eww-visited-history (current-buffer))))) (defun prot-eww--read-visited-history (&optional error-out) "Read history from `prot-eww-save-history-file'. If ERROR-OUT, signal `user-error' if there is no history." (when prot-eww-save-visited-history (let ((file prot-eww-save-history-file)) (setq prot-eww-visited-history (unless (zerop (or (file-attribute-size (file-attributes file)) 0)) (with-temp-buffer (insert-file-contents file) (read (current-buffer))))) (when (and error-out (not prot-eww-visited-history)) (user-error "No history is defined"))))) (unless prot-eww-visited-history (prot-eww--read-visited-history t)) (defun prot-eww--history-prepare () "Prepare dedicated buffer for browsing history." (set-buffer (get-buffer-create prot-eww-list-history-buffer)) (prot-eww-history-mode) (let ((inhibit-read-only t) start) (erase-buffer) (setq-local header-line-format "EWW Browsing History (prot-eww)") (dolist (history prot-eww-visited-history) (setq start (point)) (insert (format "%s" history) "\n") (put-text-property start (1+ start) 'prot-eww-history history)) (goto-char (point-min)))) ;;;###autoload (defun prot-eww-list-history () "Display `prot-eww-visited-history' in a dedicated buffer. This is a replacement for `eww-list-histories' (or equivalent), as it can combine URLs in the Gopher or Gemini protocols." (interactive) (when prot-eww-visited-history (prot-eww--save-visited-history)) (prot-eww--read-visited-history t) (pop-to-buffer prot-eww-list-history-buffer) (prot-eww--history-prepare)) (defvar prot-eww-history-kill-ring nil "Store the killed history element.") (defun prot-eww-history-kill () "Kill the current history." (interactive) (let* ((start (line-beginning-position)) (history (get-text-property start 'prot-eww-history)) (inhibit-read-only t)) (unless history (user-error "No history on the current line")) (forward-line 1) (push (buffer-substring start (point)) prot-eww-history-kill-ring) (delete-region start (point)) (setq prot-eww-visited-history (delq history prot-eww-visited-history)) (prot-eww--save-visited-history))) (defun prot-eww-history-yank () "Yank a previously killed history to the current line." (interactive) (unless prot-eww-history-kill-ring (user-error "No previously killed history")) (beginning-of-line) (let ((inhibit-read-only t) (start (point)) history) (insert (pop prot-eww-history-kill-ring)) (setq history (get-text-property start 'prot-eww-history)) (if (= start (point-min)) (push history prot-eww-visited-history) (let ((line (count-lines start (point)))) (setcdr (nthcdr (1- line) prot-eww-visited-history) (cons history (nthcdr line prot-eww-visited-history))))) (prot-eww--save-visited-history))) (defun prot-eww-history-browse () "Browse the history under point." (interactive) (let ((history (get-text-property (line-beginning-position) 'prot-eww-history))) (unless history (user-error "No history on the current line")) (quit-window) (prot-eww history))) (defvar prot-eww-history-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "C-k") 'prot-eww-history-kill) (define-key map (kbd "C-y") 'prot-eww-history-yank) (define-key map (kbd "<RET>") 'prot-eww-history-browse) (easy-menu-define nil map "Menu for `prot-eww-history-mode-map'." '("prot-eww history" ["Exit" quit-window t] ["Browse" prot-eww-history-browse :active (get-text-property (line-beginning-position) 'prot-eww-history)] ["Kill" prot-eww-history-kill :active (get-text-property (line-beginning-position) 'prot-eww-history)] ["Yank" prot-eww-history-yank :active prot-eww-history-kill-ring])) map)) (define-derived-mode prot-eww-history-mode special-mode "prot-eww-history" "Mode for listing history. \\{prot-eww-history-mode-map}" (buffer-disable-undo) (setq truncate-lines t)) (defun prot-eww--record-history () "Store URL in `prot-eww-visited-history'. To be used by `eww-after-render-hook'." (let ((url (plist-get eww-data :url))) (add-to-history 'prot-eww-visited-history url))) (add-hook 'eww-after-render-hook #'prot-eww--record-history) (advice-add 'eww-back-url :after #'prot-eww--record-history) (advice-add 'eww-forward-url :after #'prot-eww--record-history) ;; Is there a better function to add this advice? ;;;; Commands ;; handler that browse-url calls. (defun prot-eww--get-current-url () "Return the current-page's URL." (when (eq major-mode 'eww-mode) (plist-get eww-data :url))) ;;;###autoload (defun prot-eww (url &optional arg) "Pass URL to appropriate client. With optional ARG, use a new buffer." (interactive (list (browse-url-interactive-arg "URL: ") current-prefix-arg)) (eww url arg)) ;;;###autoload (defun prot-eww-browse-dwim (url &optional arg) "Visit a URL, maybe from `eww-prompt-history', with completion. With optional prefix ARG (\\[universal-argument]) open URL in a new eww buffer. If URL does not look like a valid link, run a web query using `eww-search-prefix'. When called from an eww buffer, provide the current link as \\<minibuffer-local-map>\\[next-history-element]." (interactive (let ((all-history (delete-dups (append prot-eww-visited-history eww-prompt-history))) (current-url (prot-eww--get-current-url))) (list (completing-read "Run EWW on: " all-history nil nil current-url 'eww-prompt-history current-url) (prefix-numeric-value current-prefix-arg)))) (prot-eww url arg)) ;; NOTE 2021-09-08: This uses the EWW-specific bookmarks, NOT those of ;; bookmark.el. Further below I provide integration with the latter, ;; meaning that we must either make this obsolete or make it work with ;; the new system. ;;;###autoload (defun prot-eww-visit-bookmark (&optional arg) "Visit bookmarked URL. With optional prefix ARG (\\[universal-argument]) open URL in a new EWW buffer." (interactive "P") (eww-read-bookmarks) (let ((list (gensym))) (dolist (bookmark eww-bookmarks) (push (plist-get bookmark :url) list)) (if eww-bookmarks (eww (completing-read "Visit EWW bookmark: " list) (when arg 4)) (user-error "No bookmarks")))) (defun prot-eww--capture-url-on-page (&optional position) "Capture all the links on the current web page. Return a list of strings. Strings are in the form LABEL @ URL. When optional argument POSITION is non-nil, include position info in the strings too, so strings take the form LABEL @ URL ~ POSITION." (let (links match) (save-excursion (goto-char (point-max)) ;; NOTE 2021-07-25: The first clause in the `or' is meant to ;; address a bug where if a URL is in `point-min' it does not get ;; captured. (while (setq match (text-property-search-backward 'shr-url)) (let* ((raw-url (prop-match-value match)) (start-point-prop (prop-match-beginning match)) (end-point-prop (prop-match-end match)) (url (when (stringp raw-url) (propertize raw-url 'face 'link))) (label (replace-regexp-in-string "\n" " " ; NOTE 2021-07-25: newlines break completion (buffer-substring-no-properties start-point-prop end-point-prop))) (point start-point-prop) (line (line-number-at-pos point t)) (column (save-excursion (goto-char point) (current-column))) (coordinates (propertize (format "%d,%d (%d)" line column point) 'face 'shadow))) (when url (if position (push (format "%-15s ~ %s @ %s" coordinates label url) links) (push (format "%s @ %s" label url) links)))))) links)) (defmacro prot-eww-act-visible-window (&rest body) "Run BODY within narrowed-region. If region is active run BODY within active region instead. Return the value of the last form of BODY." `(save-restriction (if (use-region-p) (narrow-to-region (region-beginning) (region-end)) (narrow-to-region (window-start) (window-end))) ,@body)) ;;;###autoload (defun prot-eww-visit-url-on-page (&optional arg) "Visit URL from list of links on the page using completion. With optional prefix ARG (\\[universal-argument]) open URL in a new EWW buffer." (interactive "P") (when (derived-mode-p 'eww-mode) (let* ((links (prot-eww--capture-url-on-page)) (selection (completing-read "Browse URL from page: " links nil t)) (url (replace-regexp-in-string ".*@ " "" selection))) (eww url (when arg 4))))) ;;;###autoload (defun prot-eww-jump-to-url-on-page (&optional arg) "Jump to URL position on the page using completion. When called without ARG (\\[universal-argument]) get URLs only from the visible portion of the buffer. But when ARG is provided consider whole buffer." (interactive "P") (when (derived-mode-p 'eww-mode) (let* ((links (if arg (prot-eww--capture-url-on-page t) (prot-eww-act-visible-window (prot-eww--capture-url-on-page t)))) (prompt-scope (if arg (propertize "URL on the page" 'face 'warning) "visible URL")) (prompt (format "Jump to %s: " prompt-scope)) (selection (completing-read prompt links nil t)) (position (replace-regexp-in-string "^.*(\\([0-9]+\\))[\s\t]+~" "\\1" selection)) (point (string-to-number position))) (goto-char point)))) (defvar prot-eww--occur-feed-regexp (concat "\\(rss\\|atom\\)\\+xml.\\(.\\|\n\\)" ".*href=[\"']\\(.*?\\)[\"']") "Regular expression to match web feeds in HTML source.") ;;;###autoload (defun prot-eww-find-feed () "Produce bespoke buffer with RSS/Atom links from XML source." (interactive) (let* ((url (or (plist-get eww-data :start) (plist-get eww-data :contents) (plist-get eww-data :home) (plist-get eww-data :url))) (title (or (plist-get eww-data :title) url)) (source (plist-get eww-data :source)) (buf-name (format "*feeds: %s # eww*" title))) (with-temp-buffer (insert source) (occur-1 prot-eww--occur-feed-regexp "\\3" (list (current-buffer)) buf-name)) ;; Handle relative URLs, so that we get an absolute URL out of them. ;; Findings like "rss.xml" are not particularly helpful. ;; ;; NOTE 2021-03-31: the base-url heuristic may not always be ;; correct, though it has worked in all cases I have tested it on. (when (get-buffer buf-name) (with-current-buffer (get-buffer buf-name) (let ((inhibit-read-only t) (base-url (replace-regexp-in-string "\\(.*/\\)[^/]+\\'" "\\1" url))) (goto-char (point-min)) (unless (re-search-forward prot-common-url-regexp nil t) (re-search-forward ".*") (replace-match (concat base-url "\\&")))))))) ;;TODO: Add this variable as user-option, that is, define it with ;;`defcustom' so that users can use the customization interface to ;;modify it. (defvar prot-eww-search-engines '((debbugs . (debbugs "https://debbugs.gnu.org/cgi/bugreport.cgi?bug=" hist-var prot-eww--debbugs-hist)) (wikipedia . (wikipedia "https://en.m.wikipedia.org/w/index.php?search=" hist-var prot-eww--wikipedia-hist)) (archwiki . (archwiki "https://wiki.archlinux.org/index.php?search=" hist-var prot-eww--archwiki-hist)) (aur . (aur "https://aur.archlinux.org/packages/?K=" hist-var prot-eww--aur-hist))) "Alist of Plist of web search engines related data. From now on refer to this type of data as APLIST. Each element of APLIST is (KEY . VALUE) pair. KEY is a symbol specifying search engine name. The VALUE is property list. The plist has two key-value pairs. K1 is the same symbol has KEY and V1 is search string of the search engine. K2 is the symbol 'hist-var', V2 is also a symbol that has a format 'prot-eww--K1-hist'. NOTE: If you modify this variable after `prot-eww' is loaded you need to run the following code after modification: (prot-eww--define-hist-var prot-eww-search-engines)") ;; Below 's-string' is short for 'search-string'. For wikipedia which ;; is this string: "https://en.m.wikipedia.org/w/index.php?search=". I ;; use this name because I don't know it's proper name. ;; Define constructor and selectors functions to access ;; `prot-eww-search-engines'. ;; the constructor (defun prot-eww--cons-search-engines (name s-string) "Include a new Alist element. The alist element is added to variable `prot-eww-search-engines'. NAME should be symbol representing the search engine. S-STRING should be string, which is specific to named search engine." (let ((my-plist `(,name ,s-string)) (hist-var-name (format "prot-eww--%s-hist" (symbol-name name)))) (plist-put my-plist 'hist-var (intern hist-var-name)) (let ((my-alist (cons name my-plist))) (add-to-list 'prot-eww-search-engines my-alist)))) ;; Selectors definitions start (defun prot-eww--select-hist-name (aplist engine-name) "Get hist-var-name from APLIST of ENGINE-NAME." (let ((hist-var-name (plist-get (alist-get engine-name aplist) 'hist-var))) hist-var-name)) (defun prot-eww--select-engine-names (aplist) "Return a list of search-engine names from APLIST. Each value of the list is a string." (mapcar (lambda (x) (format "%s" (car x))) aplist)) (defun prot-eww--select-s-string (aplist engine-name) "Return the search-string for specified ENGINE-NAME from APLIST." (plist-get (alist-get engine-name aplist) engine-name)) ;; Selector definitions end here. (defun prot-eww--define-hist-var (aplist) "Initialize APLIST hist-variables to empty list; return nil." (let ((engine-names (prot-eww--select-engine-names aplist))) (dolist (engine engine-names) (let ((hist-var-name (prot-eww--select-hist-name aplist (intern engine)))) (set hist-var-name '()))))) (prot-eww--define-hist-var prot-eww-search-engines) ;;;###autoload (defun prot-eww-search-engine (engine s-term &optional arg) "Search S-TERM using ENGINE. ENGINE is an assossiation defined in `prot-eww-search-engines'. With optional prefix ARG (\\[universal-argument]) open the search result in a new buffer." (interactive (let* ((engine-list (prot-eww--select-engine-names prot-eww-search-engines)) (engine-name (completing-read "Search with: " engine-list nil t nil 'prot-eww--engine-hist)) (history-list (prot-eww--select-hist-name prot-eww-search-engines (intern engine-name))) (search-term (read-string "Search for: " nil history-list))) (list engine-name search-term (prefix-numeric-value current-prefix-arg)))) (let* ((s-string (prot-eww--select-s-string prot-eww-search-engines (intern engine))) (eww-pass (format "%s%s" s-string s-term)) (history-list (prot-eww--select-hist-name prot-eww-search-engines (intern engine)))) (add-to-history history-list s-term) (eww eww-pass arg))) ;;;###autoload (defun prot-eww-open-in-other-window () "Use `eww-open-in-new-buffer' in another window." (interactive) (other-window-prefix) ; For emacs28 -- it's a hack, but why not? (eww-open-in-new-buffer)) ;;;###autoload (defun prot-eww-readable () "Use more opinionated `eww-readable'. Set width is set to `current-fill-column'. Adjust size of images." (interactive) (let ((shr-width (current-fill-column)) (shr-max-image-proportion 0.35)) (eww-readable))) ;; NOTE 2021-09-08: This uses the EWW-specific bookmarks, NOT those of ;; bookmark.el. Further below I provide integration with the latter, ;; meaning that we must either make this obsolete or make it work with ;; the new system. ;;;###autoload (defun prot-eww-bookmark-page (title) "Add eww bookmark named with TITLE." (interactive (list (read-string "Set bookmark title: " (plist-get eww-data :title)))) (plist-put eww-data :title title) (eww-add-bookmark)) (defvar prot-eww--punctuation-regexp "[][{}!@#$%^&*()_=+'\"?,.\|;:~`‘’“”]*" "Regular expression of punctionation that should be removed.") (defun prot-eww--slug-no-punct (str) "Convert STR to a file name slug." (replace-regexp-in-string prot-eww--punctuation-regexp "" str)) (defun prot-eww--slug-hyphenate (str) "Replace spaces with hyphens in STR. Also replace multiple hyphens with a single one and remove any trailing hyphen." (replace-regexp-in-string "-$" "" (replace-regexp-in-string "-\\{2,\\}" "-" (replace-regexp-in-string "--+\\|\s+" "-" str)))) (defun prot-eww--sluggify (str) "Make STR an appropriate file name slug." (downcase (prot-eww--slug-hyphenate (prot-eww--slug-no-punct str)))) ;;;###autoload (defun prot-eww-download-html (name) "Download web page and call the file with NAME." (interactive (list (prot-eww--sluggify (read-string "Set downloaded file name: " (plist-get eww-data :title))))) (let* ((path (thread-last eww-download-directory (expand-file-name (concat (format-time-string "%Y%m%d_%H%M%S") "--" name ".html")))) (out (prot-common-shell-command-with-exit-code-and-output "wget" "-q" (format "%s" (plist-get eww-data :url)) "-O" (format "%s" (shell-quote-argument path))))) (if (= (car out) 0) (message "Downloaded page at %s" path) (message "Error downloading page: %s" (cdr out))))) (defun prot-eww--kill-buffers-when (predicate) "Kill buffers when PREDICATE is non-nil. Loop through the buffer list, calling PREDICATE with each buffer. When calling PREDICATE with a buffer returns non-nil, kill that buffer. PREDICATE must be function that takes buffer-object as the one and only argument. It should return nil or non-nil." (let ((list-buffers (buffer-list))) (dolist (buffer list-buffers) (when (funcall predicate buffer) (kill-buffer buffer))))) (defun prot-eww--kill-eww-buffers-p (buffer) "Predicate function. Return nil or non-nil. Take BUFFER, make it current, check if it has 'eww-mode' as the `major-mode' or if its major mode is derived from `special-mode' and has \"eww\" in the buffer-name. Then return non-nil." (let ((case-fold-search t)) ; ignore case (with-current-buffer buffer (or (eq major-mode 'eww-mode) (and (derived-mode-p 'special-mode) (string-match "\\*.*eww.*\\*" (buffer-name))))))) (defun prot-eww-kill-eww-buffers () "Kill all EWW buffers. Also kill special buffers made by EWW for example buffers like \"*eww-bookmarks*\", \"*eww-history*\" etc." (prot-eww--kill-buffers-when 'prot-eww--kill-eww-buffers-p)) (defcustom prot-eww-delete-cookies t "If non-nil delete cookies when `prot-eww-quit' is called." :type 'boolean :group 'prot-eww) (defun prot-eww-delete-cookies () "Delete cookies from the cookie file." (when prot-eww-delete-cookies (url-cookie-delete-cookies))) ;; TODO: Make it defcustom (defvar prot-eww-quit-hook nil "Run this hook when `prot-eww-quit' is called.") ;; Populate the hook with these functions. (dolist (func '(prot-eww-delete-cookies prot-eww-kill-eww-buffers prot-eww--save-visited-history)) (add-hook 'prot-eww-quit-hook func)) ;;;###autoload (defun prot-eww-quit () "Quit eww, kill all its buffers, delete all cookies. As a final step, save `prot-eww-visited-history' to a file (see `prot-eww-save-history-file')." (interactive) (if prot-eww-save-visited-history (when (y-or-n-p "Are you sure you want to quit eww? ") (run-hooks 'prot-eww-quit-hook)) ;; ;; Now users have full control what `prot-eww-quit' does, by ;; modifying `prot-eww-quit-hook'. (when (yes-or-no-p "Are you sure you want to quit eww?") (run-hooks 'prot-eww-quit-hook)))) (provide 'prot-eww) ;;; prot-eww.el ends here
6.8. The prot-marginalia.el
library
;;; prot-marginalia.el --- Code for my custom mode line -*- lexical-binding: t -*- ;; Copyright (C) 2023-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or (at ;; your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (require 'bookmark) (require 'package) (defun prot-marginalia-truncate (string) "Truncate STRING to `fill-column', if necessary." (if (> (length string) fill-column) (concat (substring string 0 fill-column) "...") string)) (defun prot-marginalia-display (string) "Propertize the display of STRING for completion annotation purposes." (when (stringp string) (format "%s%s" (propertize " " 'display `(space :align-to 40)) (propertize (prot-marginalia-truncate string) 'face 'completions-annotations)))) (defun prot-marginalia-bookmark (bookmark) "Annotate BOOKMARK with its file path." (when-let* ((bm (assoc bookmark (bound-and-true-p bookmark-alist))) (path (bookmark-get-filename bookmark))) (prot-marginalia-display path))) (defun prot-marginalia-buffer (buffer) "Annotate BUFFER with the return value of function `buffer-file-name'." (if-let* ((name (buffer-file-name (get-buffer buffer)))) (prot-marginalia-display (abbreviate-file-name name)) (prot-marginalia-display (format "%s" (buffer-local-value 'major-mode (get-buffer buffer)))))) (defun prot-marginalia-package (package) "Annotate PACKAGE with its summary." (when-let* ((pkg-alist (bound-and-true-p package-alist)) (pkg (intern-soft package)) (desc (or (when (package-desc-p pkg) pkg) (car (alist-get pkg pkg-alist)) (if-let* ((built-in (assq pkg package--builtins))) (package--from-builtin built-in) (car (alist-get pkg package-archive-contents)))))) (prot-marginalia-display (package-desc-summary desc)))) (defun prot-marginalia--get-symbol-doc (symbol) "Return documentation string according to SYMBOL type." (cond ((or (functionp symbol) (macrop symbol)) (documentation symbol)) (t (get symbol 'variable-documentation)))) (defun prot-marginalia--first-line-documentation (symbol) "Return first line of SYMBOL documentation string." (when-let* ((doc-string (prot-marginalia--get-symbol-doc symbol)) ((stringp doc-string)) ((not (string-empty-p doc-string)))) (car (split-string doc-string "[?!.\n]")))) (defun prot-marginalia-symbol (symbol) "Annotate SYMBOL with its documentation string." (when-let* ((sym (intern-soft symbol)) (doc-string (prot-marginalia--first-line-documentation sym))) (prot-marginalia-display doc-string))) (provide 'prot-marginalia) ;;; prot-marginalia.el ends here
6.9. The prot-modeline.el
library
[ Watch: write a custom mode line (2023-07-29) and customise mode line colours (2024-01-13). ]
;;; prot-modeline.el --- Code for my custom mode line -*- lexical-binding: t -*- ;; Copyright (C) 2023-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or (at ;; your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (require 'prot-common) (defgroup prot-modeline nil "Custom modeline that is stylistically close to the default." :group 'mode-line) (defgroup prot-modeline-faces nil "Faces for my custom modeline." :group 'prot-modeline) (defcustom prot-modeline-string-truncate-length 9 "String length after which truncation should be done in small windows." :type 'natnum) ;;;; Faces (defface prot-modeline-indicator-button nil "Generic face used for indicators that have a background. Modify this face to, for example, add a :box attribute to all relevant indicators (combines nicely with my `spacious-padding' package).") (defface prot-modeline-indicator-red '((default :inherit bold) (((class color) (min-colors 88) (background light)) :foreground "#880000") (((class color) (min-colors 88) (background dark)) :foreground "#ff9f9f") (t :foreground "red")) "Face for modeline indicators (e.g. see my `notmuch-indicator')." :group 'prot-modeline-faces) (defface prot-modeline-indicator-red-bg '((default :inherit (bold prot-modeline-indicator-button)) (((class color) (min-colors 88) (background light)) :background "#aa1111" :foreground "white") (((class color) (min-colors 88) (background dark)) :background "#ff9090" :foreground "black") (t :background "red" :foreground "black")) "Face for modeline indicators with a background." :group 'prot-modeline-faces) (defface prot-modeline-indicator-green '((default :inherit bold) (((class color) (min-colors 88) (background light)) :foreground "#005f00") (((class color) (min-colors 88) (background dark)) :foreground "#73fa7f") (t :foreground "green")) "Face for modeline indicators (e.g. see my `notmuch-indicator')." :group 'prot-modeline-faces) (defface prot-modeline-indicator-green-bg '((default :inherit (bold prot-modeline-indicator-button)) (((class color) (min-colors 88) (background light)) :background "#207b20" :foreground "white") (((class color) (min-colors 88) (background dark)) :background "#77d077" :foreground "black") (t :background "green" :foreground "black")) "Face for modeline indicators with a background." :group 'prot-modeline-faces) (defface prot-modeline-indicator-yellow '((default :inherit bold) (((class color) (min-colors 88) (background light)) :foreground "#6f4000") (((class color) (min-colors 88) (background dark)) :foreground "#f0c526") (t :foreground "yellow")) "Face for modeline indicators (e.g. see my `notmuch-indicator')." :group 'prot-modeline-faces) (defface prot-modeline-indicator-yellow-bg '((default :inherit (bold prot-modeline-indicator-button)) (((class color) (min-colors 88) (background light)) :background "#805000" :foreground "white") (((class color) (min-colors 88) (background dark)) :background "#ffc800" :foreground "black") (t :background "yellow" :foreground "black")) "Face for modeline indicators with a background." :group 'prot-modeline-faces) (defface prot-modeline-indicator-blue '((default :inherit bold) (((class color) (min-colors 88) (background light)) :foreground "#00228a") (((class color) (min-colors 88) (background dark)) :foreground "#88bfff") (t :foreground "blue")) "Face for modeline indicators (e.g. see my `notmuch-indicator')." :group 'prot-modeline-faces) (defface prot-modeline-indicator-blue-bg '((default :inherit (bold prot-modeline-indicator-button)) (((class color) (min-colors 88) (background light)) :background "#0000aa" :foreground "white") (((class color) (min-colors 88) (background dark)) :background "#77aaff" :foreground "black") (t :background "blue" :foreground "black")) "Face for modeline indicators with a background." :group 'prot-modeline-faces) (defface prot-modeline-indicator-magenta '((default :inherit bold) (((class color) (min-colors 88) (background light)) :foreground "#6a1aaf") (((class color) (min-colors 88) (background dark)) :foreground "#e0a0ff") (t :foreground "magenta")) "Face for modeline indicators (e.g. see my `notmuch-indicator')." :group 'prot-modeline-faces) (defface prot-modeline-indicator-magenta-bg '((default :inherit (bold prot-modeline-indicator-button)) (((class color) (min-colors 88) (background light)) :background "#6f0f9f" :foreground "white") (((class color) (min-colors 88) (background dark)) :background "#e3a2ff" :foreground "black") (t :background "magenta" :foreground "black")) "Face for modeline indicators with a background." :group 'prot-modeline-faces) (defface prot-modeline-indicator-cyan '((default :inherit bold) (((class color) (min-colors 88) (background light)) :foreground "#004060") (((class color) (min-colors 88) (background dark)) :foreground "#30b7cc") (t :foreground "cyan")) "Face for modeline indicators (e.g. see my `notmuch-indicator')." :group 'prot-modeline-faces) (defface prot-modeline-indicator-cyan-bg '((default :inherit (bold prot-modeline-indicator-button)) (((class color) (min-colors 88) (background light)) :background "#006080" :foreground "white") (((class color) (min-colors 88) (background dark)) :background "#40c0e0" :foreground "black") (t :background "cyan" :foreground "black")) "Face for modeline indicators with a background." :group 'prot-modeline-faces) (defface prot-modeline-indicator-gray '((t :inherit shadow)) "Face for modeline indicators (e.g. see my `notmuch-indicator')." :group 'prot-modeline-faces) (defface prot-modeline-indicator-gray-bg '((default :inherit (bold prot-modeline-indicator-button)) (((class color) (min-colors 88) (background light)) :background "#808080" :foreground "white") (((class color) (min-colors 88) (background dark)) :background "#a0a0a0" :foreground "black") (t :inverse-video t)) "Face for modeline indicatovrs with a background." :group 'prot-modeline-faces) ;;;; Common helper functions (defun prot-modeline--string-truncate-p (str) "Return non-nil if STR should be truncated." (if (string-empty-p str) str (and (prot-common-window-narrow-p) (> (length str) prot-modeline-string-truncate-length) (not (one-window-p :no-minibuffer))))) (defun prot-modeline--truncate-p () "Return non-nil if truncation should happen. This is a more general and less stringent variant of `prot-modeline--string-truncate-p'." (and (prot-common-window-narrow-p) (not (one-window-p :no-minibuffer)))) (defun prot-modeline-string-cut-end (str) "Return truncated STR, if appropriate, else return STR. Cut off the end of STR by counting from its start up to `prot-modeline-string-truncate-length'." (if (prot-modeline--string-truncate-p str) (concat (substring str 0 prot-modeline-string-truncate-length) "...") str)) (defun prot-modeline-string-cut-beginning (str) "Return truncated STR, if appropriate, else return STR. Cut off the beginning of STR by counting from its end up to `prot-modeline-string-truncate-length'." (if (prot-modeline--string-truncate-p str) (concat "..." (substring str (- prot-modeline-string-truncate-length))) str)) (defun prot-modeline-string-cut-middle (str) "Return truncated STR, if appropriate, else return STR. Cut off the middle of STR by counting half of `prot-modeline-string-truncate-length' both from its beginning and end." (let ((half (floor prot-modeline-string-truncate-length 2))) (if (prot-modeline--string-truncate-p str) (concat (substring str 0 half) "..." (substring str (- half))) str))) (defun prot-modeline--first-char (str) "Return first character from STR." (substring str 0 1)) (defun prot-modeline-string-abbreviate (str) "Abbreviate STR individual hyphen or underscore separated words. Also see `prot-modeline-string-abbreviate-but-last'." (if (prot-modeline--string-truncate-p str) (mapconcat #'prot-modeline--first-char (split-string str "[_-]") "-") str)) (defun prot-modeline-string-abbreviate-but-last (str nthlast) "Abbreviate STR, keeping NTHLAST words intact. Also see `prot-modeline-string-abbreviate'." (if (prot-modeline--string-truncate-p str) (let* ((all-strings (split-string str "[_-]")) (nbutlast-strings (nbutlast (copy-sequence all-strings) nthlast)) (last-strings (nreverse (ntake nthlast (nreverse (copy-sequence all-strings))))) (first-component (mapconcat #'prot-modeline--first-char nbutlast-strings "-")) (last-component (mapconcat #'identity last-strings "-"))) (if (string-empty-p first-component) last-component (concat first-component "-" last-component))) str)) ;;;; Keyboard macro indicator (defvar-local prot-modeline-kbd-macro '(:eval (when (and (mode-line-window-selected-p) defining-kbd-macro) (propertize " KMacro " 'face 'prot-modeline-indicator-blue-bg))) "Mode line construct displaying `mode-line-defining-kbd-macro'. Specific to the current window's mode line.") ;;;; Narrow indicator (defvar-local prot-modeline-narrow '(:eval (when (and (mode-line-window-selected-p) (buffer-narrowed-p) (not (derived-mode-p 'Info-mode 'help-mode 'special-mode 'message-mode))) (propertize " Narrow " 'face 'prot-modeline-indicator-cyan-bg))) "Mode line construct to report the narrowed state of the current buffer.") ;;;; Input method (defvar-local prot-modeline-input-method '(:eval (when current-input-method-title (propertize (format " %s " current-input-method-title) 'face 'prot-modeline-indicator-green-bg 'mouse-face 'mode-line-highlight))) "Mode line construct to report the multilingual environment.") ;;;; Buffer status ;; TODO 2023-07-05: What else is there beside remote files? If ;; nothing, this must be renamed accordingly. (defvar-local prot-modeline-buffer-status '(:eval (when (file-remote-p default-directory) (propertize " @ " 'face 'prot-modeline-indicator-red-bg 'mouse-face 'mode-line-highlight))) "Mode line construct for showing remote file name.") ;;;; Dedicated window (defvar-local prot-modeline-window-dedicated-status '(:eval (when (window-dedicated-p) (propertize " = " 'face 'prot-modeline-indicator-gray-bg 'mouse-face 'mode-line-highlight))) "Mode line construct for dedicated window indicator.") ;;;; Buffer name and modified status (defun prot-modeline-buffer-identification-face () "Return appropriate face or face list for `prot-modeline-buffer-identification'." (let ((file (buffer-file-name))) (cond ((and (mode-line-window-selected-p) file (buffer-modified-p)) '(italic mode-line-buffer-id)) ((and file (buffer-modified-p)) 'italic) ((mode-line-window-selected-p) 'mode-line-buffer-id)))) (defun prot-modeline--buffer-name () "Return `buffer-name', truncating it if necessary. See `prot-modeline-string-cut-middle'." (when-let* ((name (buffer-name))) (prot-modeline-string-cut-middle name))) (defun prot-modeline-buffer-name () "Return buffer name, with read-only indicator if relevant." (let ((name (prot-modeline--buffer-name))) (if buffer-read-only (format "%s %s" (char-to-string #xE0A2) name) name))) (defun prot-modeline-buffer-name-help-echo () "Return `help-echo' value for `prot-modeline-buffer-identification'." (concat (propertize (buffer-name) 'face 'mode-line-buffer-id) "\n" (propertize (or (buffer-file-name) (format "No underlying file.\nDirectory is: %s" default-directory)) 'face 'font-lock-doc-face))) (defvar-local prot-modeline-buffer-identification '(:eval (propertize (prot-modeline-buffer-name) 'face (prot-modeline-buffer-identification-face) 'mouse-face 'mode-line-highlight 'help-echo (prot-modeline-buffer-name-help-echo))) "Mode line construct for identifying the buffer being displayed. Propertize the current buffer with the `mode-line-buffer-id' face. Let other buffers have no face.") ;;;; Major mode (defun prot-modeline-major-mode-indicator () "Return appropriate propertized mode line indicator for the major mode." (let ((indicator (cond ((derived-mode-p 'text-mode) "§") ((derived-mode-p 'prog-mode) "λ") ((derived-mode-p 'comint-mode) ">_") (t "◦")))) (propertize indicator 'face 'shadow))) (defun prot-modeline-major-mode-name () "Return capitalized `major-mode' without the -mode suffix." (capitalize (string-replace "-mode" "" (symbol-name major-mode)))) (defun prot-modeline-major-mode-help-echo () "Return `help-echo' value for `prot-modeline-major-mode'." (if-let* ((parent (get major-mode 'derived-mode-parent))) (format "Symbol: `%s'. Derived from: `%s'" major-mode parent) (format "Symbol: `%s'." major-mode))) (defvar-local prot-modeline-major-mode (list (propertize "%[" 'face 'prot-modeline-indicator-red) '(:eval (concat (prot-modeline-major-mode-indicator) " " (propertize (prot-modeline-string-abbreviate-but-last (prot-modeline-major-mode-name) 2) 'mouse-face 'mode-line-highlight 'help-echo (prot-modeline-major-mode-help-echo)))) (propertize "%]" 'face 'prot-modeline-indicator-red)) "Mode line construct for displaying major modes.") (defvar-local prot-modeline-process (list '("" mode-line-process)) "Mode line construct for the running process indicator.") ;;;; Git branch and diffstat (declare-function vc-git--symbolic-ref "vc-git" (file)) (defun prot-modeline--vc-branch-name (file backend) "Return capitalized VC branch name for FILE with BACKEND." (when-let* ((rev (vc-working-revision file backend)) (branch (or (vc-git--symbolic-ref file) (substring rev 0 7)))) (capitalize branch))) ;; NOTE 2023-07-27: This is a good idea, but it hardcodes Git, whereas ;; I want a generic VC method. Granted, I only use Git but I still ;; want it to work as a VC extension. ;; (defun prot-modeline-diffstat (file) ;; "Return shortened Git diff numstat for FILE." ;; (when-let* ((output (shell-command-to-string (format "git diff --numstat %s" file))) ;; (stats (split-string output "[\s\t]" :omit-nulls "[\s\f\t\n\r\v]+")) ;; (added (nth 0 stats)) ;; (deleted (nth 1 stats))) ;; (cond ;; ((and (equal added "0") (equal deleted "0")) ;; "") ;; ((and (not (equal added "0")) (equal deleted "0")) ;; (propertize (format "+%s" added) 'face 'shadow)) ;; ((and (equal added "0") (not (equal deleted "0"))) ;; (propertize (format "-%s" deleted) 'face 'shadow)) ;; (t ;; (propertize (format "+%s -%s" added deleted) 'face 'shadow))))) (declare-function vc-git-working-revision "vc-git" (file)) (defvar prot-modeline-vc-map (let ((map (make-sparse-keymap))) (define-key map [mode-line down-mouse-1] 'vc-diff) (define-key map [mode-line down-mouse-3] 'vc-root-diff) map) "Keymap to display on VC indicator.") (defun prot-modeline--vc-help-echo (file) "Return `help-echo' message for FILE tracked by VC." (format "Revision: %s\nmouse-1: `vc-diff'\nmouse-3: `vc-root-diff'" (vc-working-revision file))) (defun prot-modeline--vc-text (file branch &optional face) "Prepare text for Git controlled FILE, given BRANCH. With optional FACE, use it to propertize the BRANCH." (concat (propertize (char-to-string #xE0A0) 'face 'shadow) " " (propertize branch 'face face 'mouse-face 'mode-line-highlight 'help-echo (prot-modeline--vc-help-echo file) 'local-map prot-modeline-vc-map) ;; " " ;; (prot-modeline-diffstat file) )) (defun prot-modeline--vc-details (file branch &optional face) "Return Git BRANCH details for FILE, truncating it if necessary. The string is truncated if the width of the window is smaller than `split-width-threshold'." (prot-modeline-string-cut-end (prot-modeline--vc-text file branch face))) (defvar prot-modeline--vc-faces '((added . vc-locally-added-state) (edited . vc-edited-state) (removed . vc-removed-state) (missing . vc-missing-state) (conflict . vc-conflict-state) (locked . vc-locked-state) (up-to-date . vc-up-to-date-state)) "VC state faces.") (defun prot-modeline--vc-get-face (key) "Get face from KEY in `prot-modeline--vc-faces'." (alist-get key prot-modeline--vc-faces 'up-to-date)) (defun prot-modeline--vc-face (file backend) "Return VC state face for FILE with BACKEND." (prot-modeline--vc-get-face (vc-state file backend))) (defvar-local prot-modeline-vc-branch '(:eval (when-let* (((mode-line-window-selected-p)) (file (buffer-file-name)) (backend (vc-backend file)) ;; ((vc-git-registered file)) (branch (prot-modeline--vc-branch-name file backend)) (face (prot-modeline--vc-face file backend))) (prot-modeline--vc-details file branch face))) "Mode line construct to return propertized VC branch.") ;;;; Flymake errors, warnings, notes (declare-function flymake--severity "flymake" (type)) (declare-function flymake-diagnostic-type "flymake" (diag)) ;; Based on `flymake--mode-line-counter'. (defun prot-modeline-flymake-counter (type) "Compute number of diagnostics in buffer with TYPE's severity. TYPE is usually keyword `:error', `:warning' or `:note'." (let ((count 0)) (dolist (d (flymake-diagnostics)) (when (= (flymake--severity type) (flymake--severity (flymake-diagnostic-type d))) (cl-incf count))) (when (cl-plusp count) (number-to-string count)))) (defvar prot-modeline-flymake-map (let ((map (make-sparse-keymap))) (define-key map [mode-line down-mouse-1] 'flymake-show-buffer-diagnostics) (define-key map [mode-line down-mouse-3] 'flymake-show-project-diagnostics) map) "Keymap to display on Flymake indicator.") (defmacro prot-modeline-flymake-type (type indicator &optional face) "Return function that handles Flymake TYPE with stylistic INDICATOR and FACE." `(defun ,(intern (format "prot-modeline-flymake-%s" type)) () (when-let* ((count (prot-modeline-flymake-counter ,(intern (format ":%s" type))))) (concat (propertize ,indicator 'face 'shadow) (propertize count 'face ',(or face type) 'mouse-face 'mode-line-highlight ;; FIXME 2023-07-03: Clicking on the text with ;; this buffer and a single warning present, the ;; diagnostics take up the entire frame. Why? 'local-map prot-modeline-flymake-map 'help-echo "mouse-1: buffer diagnostics\nmouse-3: project diagnostics"))))) (prot-modeline-flymake-type error "☣") (prot-modeline-flymake-type warning "!") (prot-modeline-flymake-type note "·" success) (defvar-local prot-modeline-flymake `(:eval (when (and (bound-and-true-p flymake-mode) (mode-line-window-selected-p)) (list ;; See the calls to the macro `prot-modeline-flymake-type' '(:eval (prot-modeline-flymake-error)) '(:eval (prot-modeline-flymake-warning)) '(:eval (prot-modeline-flymake-note))))) "Mode line construct displaying `flymake-mode-line-format'. Specific to the current window's mode line.") ;;;; Eglot (with-eval-after-load 'eglot (setq mode-line-misc-info (delete '(eglot--managed-mode (" [" eglot--mode-line-format "] ")) mode-line-misc-info))) (defvar-local prot-modeline-eglot `(:eval (when (and (featurep 'eglot) (mode-line-window-selected-p)) '(eglot--managed-mode eglot--mode-line-format))) "Mode line construct displaying Eglot information. Specific to the current window's mode line.") ;;;; Miscellaneous (defvar-local prot-modeline-notmuch-indicator '(notmuch-indicator-mode (" " (:eval (when (mode-line-window-selected-p) notmuch-indicator--counters)))) "The equivalent of `notmuch-indicator-mode-line-construct'. Display the indicator only on the focused window's mode line.") (defvar-local prot-modeline-misc-info '(:eval (when (mode-line-window-selected-p) mode-line-misc-info)) "Mode line construct displaying `mode-line-misc-info'. Specific to the current window's mode line.") ;;;; Risky local variables ;; NOTE 2023-04-28: The `risky-local-variable' is critical, as those ;; variables will not work without it. (dolist (construct '(prot-modeline-kbd-macro prot-modeline-narrow prot-modeline-input-method prot-modeline-buffer-status prot-modeline-window-dedicated-status prot-modeline-buffer-identification prot-modeline-major-mode prot-modeline-process prot-modeline-vc-branch prot-modeline-flymake prot-modeline-eglot ;; prot-modeline-align-right prot-modeline-notmuch-indicator prot-modeline-misc-info)) (put construct 'risky-local-variable t)) (provide 'prot-modeline) ;;; prot-modeline.el ends here
6.10. The prot-notmuch.el
library
;;; prot-notmuch.el --- Tweaks for my notmuch.el configurations -*- lexical-binding: t -*- ;; Copyright (C) 2021-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; This covers my tweaks for notmuch.el that are meant for use in my ;; Emacs setup: https://protesilaos.com/emacs/dotemacs. ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (require 'prot-common) (eval-when-compile (require 'cl-lib)) (defgroup prot-notmuch () "Extensions for notmuch.el." :group 'notmuch) (defcustom prot-notmuch-delete-tag "del" "Single tag that applies to mail marked for deletion. This is used by `prot-notmuch-delete-mail'." :type 'string :group 'prot-notmuch) (defcustom prot-notmuch-mark-delete-tags `(,(format "+%s" prot-notmuch-delete-tag) "-inbox" "-unread") "List of tags to mark for deletion. To actually delete email, refer to `prot-notmuch-delete-mail'." :type '(repeat string) :group 'prot-notmuch) (defcustom prot-notmuch-mark-flag-tags '("+flag" "-unread") "List of tags to mark as important (flagged). This gets the `notmuch-tag-flagged' face, if that is specified in `notmuch-tag-formats'." :type '(repeat string) :group 'prot-notmuch) (defcustom prot-notmuch-mark-spam-tags '("+spam" "-inbox" "-unread") "List of tags to mark as spam." :type '(repeat string) :group 'prot-notmuch) ;;;; Commands (autoload 'notmuch-interactive-region "notmuch") (autoload 'notmuch-tag-change-list "notmuch") (autoload 'notmuch-search-next-thread "notmuch") (autoload 'notmuch-search-tag "notmuch") (defmacro prot-notmuch-search-tag-thread (name tags) "Produce NAME function parsing TAGS." (declare (indent defun)) `(defun ,name (&optional untag beg end) ,(format "Mark with `%s' the currently selected thread. Operate on each message in the currently selected thread. With optional BEG and END as points delimiting a region that encompasses multiple threads, operate on all those messages instead. With optional prefix argument (\\[universal-argument]) as UNTAG, reverse the application of the tags. This function advances to the next thread when finished." tags) (interactive (cons current-prefix-arg (notmuch-interactive-region))) (when ,tags (notmuch-search-tag (notmuch-tag-change-list ,tags untag) beg end)) (when (eq beg end) (notmuch-search-next-thread)))) (prot-notmuch-search-tag-thread prot-notmuch-search-delete-thread prot-notmuch-mark-delete-tags) (prot-notmuch-search-tag-thread prot-notmuch-search-flag-thread prot-notmuch-mark-flag-tags) (prot-notmuch-search-tag-thread prot-notmuch-search-spam-thread prot-notmuch-mark-spam-tags) (defmacro prot-notmuch-show-tag-message (name tags) "Produce NAME function parsing TAGS." (declare (indent defun)) `(defun ,name (&optional untag) ,(format "Apply `%s' to message. With optional prefix argument (\\[universal-argument]) as UNTAG, reverse the application of the tags." tags) (interactive "P") (when ,tags (apply 'notmuch-show-tag-message (notmuch-tag-change-list ,tags untag))))) (prot-notmuch-show-tag-message prot-notmuch-show-delete-message prot-notmuch-mark-delete-tags) (prot-notmuch-show-tag-message prot-notmuch-show-flag-message prot-notmuch-mark-flag-tags) (prot-notmuch-show-tag-message prot-notmuch-show-spam-message prot-notmuch-mark-spam-tags) (autoload 'notmuch-refresh-this-buffer "notmuch") (autoload 'notmuch-refresh-all-buffers "notmuch") (defun prot-notmuch-refresh-buffer (&optional arg) "Run `notmuch-refresh-this-buffer'. With optional prefix ARG (\\[universal-argument]) call `notmuch-refresh-all-buffers'." (interactive "P") (if arg (notmuch-refresh-all-buffers) (notmuch-refresh-this-buffer))) ;;;###autoload (defun prot-notmuch-delete-mail () "Permanently delete mail marked as `prot-notmuch-delete-mail'. Prompt for confirmation before carrying out the operation. Do not attempt to refresh the index. This will be done upon the next invocation of 'notmuch new'." (interactive) (let* ((del-tag prot-notmuch-delete-tag) (count (string-to-number (with-temp-buffer (shell-command (format "notmuch count tag:%s" prot-notmuch-delete-tag) t) (buffer-substring-no-properties (point-min) (1- (point-max)))))) (mail (if (> count 1) "mails" "mail"))) (unless (> count 0) (user-error "No mail marked as `%s'" del-tag)) (when (yes-or-no-p (format "Delete %d %s marked as `%s'?" count mail del-tag)) (shell-command (format "notmuch search --output=files --format=text0 tag:%s | xargs -r0 rm" del-tag) t)))) ;;;; SourceHut-related setup (defconst prot-notmuch-patch-control-codes '("PROPOSED" "NEEDS_REVISION" "SUPERSEDED" "APPROVED" "REJECTED" "APPLIED") "Control codes for SourceHut patches. See `prot-notmuch-patch-add-email-control-code' for how to apply them.") (defun prot-notmuch--rx-in-sourcehut-mail (rx-group string) "Return RX-GROUP of SourceHut mail in STRING." (when (string-match-p "lists\\.sr\\.ht" string) (string-clean-whitespace (replace-regexp-in-string ".*?[<]?\\(\\([-a-zA-Z0-9=._+~/]+\\)@\\(lists\\.sr\\.ht\\)\\)[>]?.*?" (format "\\%s" rx-group) string)))) (declare-function notmuch-show-get-header "notmuch-show" (header &optional props)) (declare-function message-fetch-field "message" (header &optional first)) (defun prot-notmuch--get-to-or-cc-header () "Get appropriate To or Cc header." (cond ((derived-mode-p 'notmuch-message-mode) (concat (message-fetch-field "To") " " (message-fetch-field "Cc"))) ((derived-mode-p 'notmuch-show-mode) (concat (notmuch-show-get-header :To) " " (notmuch-show-get-header :Cc))))) ;; NOTE 2022-04-19: This assumes that we only have one list... I think ;; that is okay, but it might cause problems. (defun prot-notmuch--extract-sourcehut-mail (rx-group) "Extract RX-GROUP from SourceHut mailing list address. 1 is the full email address, 2 is the local part, while 3 is the domain." (prot-notmuch--rx-in-sourcehut-mail rx-group (prot-notmuch--get-to-or-cc-header))) (declare-function message-add-header "message" (&rest headers)) ;; Read: <https://man.sr.ht/lists.sr.ht/#email-controls>. ;;;###autoload (defun prot-notmuch-patch-add-email-control-code (control-code) "Add custom header for SourceHut email controls. The CONTROL-CODE is among `prot-notmuch-patch-control-codes'." (interactive (list (completing-read "Select control code: " prot-notmuch-patch-control-codes nil t))) (if (member control-code prot-notmuch-patch-control-codes) (unless (message-fetch-field "X-Sourcehut-Patchset-Update") (message-add-header (format "X-Sourcehut-Patchset-Update: %s" control-code))) (user-error "%s is not specified in `prot-notmuch-patch-control-codes'" control-code))) ;;;###autoload (defun prot-notmuch-ask-sourcehut-control-code () "Use `prot-notmuch-patch-add-email-control-code' programmatically. Add this to `notmuch-mua-send-hook'." (when-let* ((header (message-fetch-field "Subject")) (subject (when (>= (length header) 6) (substring header 0 6))) ((string= "[PATCH" subject)) ; Is [ always there? ((prot-notmuch--extract-sourcehut-mail 1)) ((not (message-fetch-field "X-Sourcehut-Patchset-Update"))) ((y-or-n-p "Add control code for SourceHut PATCH?"))) (call-interactively #'prot-notmuch-patch-add-email-control-code))) ;; NOTE 2022-04-19: Ideally we should be able to use the ;; `notmuch-show-stash-mlarchive-link-alist' for ;; `prot-notmuch-stash-sourcehut-link', but it assumes that the base URL ;; is fixed for all message IDs, whereas those on SourceHut are not. (declare-function notmuch-show-get-message-id "notmuch-show" (&optional bare)) (declare-function notmuch-show-message-top "notmuch-show") (declare-function notmuch-common-do-stash "notmuch-lib" (text)) ;;;###autoload (defun prot-notmuch-stash-sourcehut-link (&optional current) "Stash web link to current SourceHut thread. With optional CURRENT argument, produce a link to the current message, else use the topmost message (start of the thread). Note that the topmost message is assumed to hold the id of the base URL, though this is not necessarily true." (interactive "P") (let* ((ml (prot-notmuch--extract-sourcehut-mail 2)) (base-id (save-excursion (goto-char (point-min)) (notmuch-show-message-top) (notmuch-show-get-message-id t))) (current-id (notmuch-show-get-message-id t))) (notmuch-common-do-stash (if current (format "https://lists.sr.ht/%s/<%s>#<%s>" ml base-id current-id) (format "https://lists.sr.ht/%s/<%s>" ml base-id))))) ;;;###autoload (defun prot-notmuch-check-valid-sourcehut-email () "Check if SourceHut address is correct. Add this to `notmuch-mua-send-hook'." (when-let* ((ml (prot-notmuch--extract-sourcehut-mail 1)) ((not (string-match-p "^\\(~\\|\\.\\)" ml))) ((not (y-or-n-p "SourceHut address looks wrong. Send anyway?")))) (user-error "Incorrect SourceHut address"))) (provide 'prot-notmuch) ;;; prot-notmuch.el ends here
6.11. The prot-orderless.el
library
;;; prot-orderless.el --- Extensions for Orderless -*- lexical-binding: t -*- ;; Copyright (C) 2020-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or (at ;; your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; Extensions for the Orderless completion style for use in my Emacs ;; setup: <https://protesilaos.com/emacs/dotemacs/>. ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: ;;;; Style dispatchers (defun prot-orderless-literal (word _index _total) "Read WORD= as a literal string." (when (string-suffix-p "=" word) ;; The `orderless-literal' is how this should be treated by ;; orderless. The `substring' form omits the `=' from the ;; pattern. `(orderless-literal . ,(substring word 0 -1)))) (defun prot-orderless-file-ext (word _index _total) "Expand WORD. to a file suffix when completing file names." (when (and minibuffer-completing-file-name (string-suffix-p "." word)) `(orderless-regexp . ,(format "\\.%s\\'" (substring word 0 -1))))) (defun prot-orderless-beg-or-end (word _index _total) "Expand WORD~ to \\(^WORD\\|WORD$\\)." (when-let* (((string-suffix-p "~" word)) (word (substring word 0 -1))) `(orderless-regexp . ,(format "\\(^%s\\|%s$\\)" word word)))) (provide 'prot-orderless) ;;; prot-orderless.el ends here
6.12. The prot-org.el
library
;;; prot-org.el --- Tweaks for my org-mode configurations -*- lexical-binding: t -*- ;; Copyright (C) 2021-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; This covers my tweaks for Org that are meant for use in my ;; Emacs setup: https://protesilaos.com/emacs/dotemacs. ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (require 'prot-common) (require 'org) (defgroup prot-org () "Extensions for org.el." :group 'org) ;;;; org-capture (defvar prot-org--capture-coach-person-history nil) (declare-function message-fetch-field "message" (header &optional first)) (declare-function notmuch-show-get-header "notmuch-show") (defun prot-org--capture-coach-person-message-from () "Return default value for `prot-org--capture-coach-person-prompt'." (when-let* ((from (cond ((derived-mode-p 'message-mode) (message-fetch-field "To")) ((derived-mode-p 'notmuch-show-mode) (notmuch-show-get-header :From))))) (string-clean-whitespace (car (split-string from "<"))))) (defun prot-org--capture-coach-person-message-from-and-subject () "Return default value for `prot-org--capture-coach-person-prompt'." (cond ((derived-mode-p 'message-mode) (message-fetch-field "Subject")) ((derived-mode-p 'notmuch-show-mode) (notmuch-show-get-header :Subject)))) (defun prot-org--capture-coach-person-prompt () "Prompt for person for use in `prot-org-capture-coach'." (completing-read "Person to coach: " prot-org--capture-coach-person-history nil nil nil 'prot-org--capture-coach-person-history (prot-org--capture-coach-person-message-from))) (defvar prot-org--capture-coach-description-history nil) (defun prot-org--capture-coach-description-prompt () "Prompt for description in `prot-org-capture-coach'." (read-string "Description: " nil 'prot-org--capture-coach-description-history (prot-org--capture-coach-person-message-from-and-subject))) (defun prot-org--capture-coach-date-prompt-range () "Prompt for Org date and return it as a +1h range. For use in `prot-org-capture-coach'." (let ((date (org-read-date :with-time))) ;; We cannot use this here, unfortunately, as the Org agenda ;; interprets it both as a deadline and an event with the date ;; range. ;; ;; (format "DEADLINE: <%s>--<%s>\n" date (format "<%s>--<%s>\n" date (org-read-date :with-time nil "++1h" nil (org-encode-time (org-parse-time-string date)))))) (defun prot-org-capture-coach () "Contents of an Org capture template for my coaching lessons." (let ((identifier (format-time-string "%Y%m%dT%H%M%S"))) (format "* COACH %s %s :lesson: DEADLINE: %%^T :PROPERTIES: :CAPTURED: %%U :CUSTOM_ID: h:%s :APPT_WARNTIME: 20 :END: %%a%%?" (prot-org--capture-coach-person-prompt) (prot-org--capture-coach-description-prompt) identifier identifier))) (defun prot-org-capture-coach-clock () "Contents of an Org capture for my clocked coaching services." (format "* COACH %s %s :service: :PROPERTIES: :CAPTURED: %%U :CUSTOM_ID: h:%s :END: %%a%%?" (prot-org--capture-coach-person-prompt) (prot-org--capture-coach-description-prompt) (format-time-string "%Y%m%dT%H%M%S"))) (declare-function cl-letf "cl-lib") ;; Adapted from source: <https://stackoverflow.com/a/54251825>. ;; ;; Thanks to Juanjo Presa (@uningan on GitHub) for discovering that the ;; original version was causing an error in `org-roam'. I then figure ;; we were missing the `&rest': ;; <https://github.com/org-roam/org-roam/issues/2142#issuecomment-1100718373>. (defun prot-org--capture-no-delete-windows (&rest args) "Apply ARGS while ignoring `delete-other-windows'." (cl-letf (((symbol-function 'delete-other-windows) 'ignore)) (apply args))) ;; Same source as above (advice-add 'org-capture-place-template :around 'prot-org--capture-no-delete-windows) (advice-add 'org-add-log-note :around 'prot-org--capture-no-delete-windows) ;;;;; Custom function to select a project to add to (defun prot-org--get-outline (&optional file) "Return `outline-regexp' headings and line numbers of current file or FILE." (with-current-buffer (find-file-noselect file) (let ((outline-regexp (format "^\\(?:%s\\)" (or (bound-and-true-p outline-regexp) "[*\^L]+"))) candidates) (save-excursion (goto-char (point-min)) (while (if (bound-and-true-p outline-search-function) (funcall outline-search-function) (re-search-forward outline-regexp nil t)) (push ;; NOTE 2024-11-24: The -5 (minimum width) is a sufficiently high number to keep the ;; alignment consistent in most cases. Larger files will simply shift the heading text ;; in minibuffer, but this is not an issue anymore. (format "%-5s\t%s" (line-number-at-pos (point)) (buffer-substring-no-properties (line-beginning-position) (line-end-position))) candidates) (goto-char (1+ (line-end-position))))) (if candidates (nreverse candidates) (user-error "No outline"))))) (defvar prot-org-outline-history nil "Minibuffer history for `prot-org-outline-prompt'.") (defun prot-org-outline-prompt (&optional file) "Prompt for outline among headings retrieved by `prot-org--get-outline'. With optional FILE use the outline of it, otherwise use that of the current file." (let ((current-file (or file buffer-file-name)) (default (car prot-org-outline-history))) (completing-read (format-prompt (format "Select heading inside `%s': " (propertize (file-name-nondirectory current-file) 'face 'error)) default) (prot-common-completion-table-no-sort 'imenu (prot-org--get-outline current-file)) nil :require-match nil 'prot-org-outline-history default))) (defvar prot-org-file-history nil "Minibuffer history of `prot-org-file-prompt'.") (defun prot-org--not-useful-p (file) "Return non-nil if FILE is not a useful Org file for `org-capture'." (or (string-match-p "\\.org_archive\\'" file) (backup-file-name-p file) (not (string-match-p "\\.org\\'" file)))) (defun prot-org-file-prompt () "Select a file in the `org-directory'." (if-let* ((dir org-directory) (files (directory-files-recursively org-directory ".*" nil)) (files (seq-remove #'prot-org--not-useful-p files))) (let ((default (car prot-org-file-history))) (completing-read (format-prompt "Select file" default) (prot-common-completion-table 'file files) nil :require-match nil 'prot-org-file-history default)) (user-error "There are no files in the `org-directory'"))) ;;;###autoload (defun prot-org-select-heading-in-file () "Like `prot-org-select-project' but specifically for `org-capture'." (declare (interactive-only t)) (interactive) (pcase-let* ((file (prot-org-file-prompt)) (line-with-heading (prot-org-outline-prompt file)) (`(,line ,text) (split-string line-with-heading "\t")) (line (string-to-number line))) ;; NOTE 2024-11-24: `with-current-buffer' does not work with `org-capture'. (find-file file) (goto-char (point-min)) (forward-line (1- line)))) (defalias 'prot-org-goto-heading-in-file 'prot-org-select-heading-in-file "Alias for `prot-org-select-heading-in-file'.") ;;;; Org clock report (defvar prot-org-clock--template-with-effort "#+BEGIN: clocktable :formula % :properties (\"Effort\") :timestamp t :sort (1 . ?a) :link t :scope nil :hidefiles t :maxlevel 8 :stepskip0 t #+END:" "Clock table with effort estimate column to use for custom clock reports.") (defvar prot-org-clock--template-no-effort "#+BEGIN: clocktable :formula % :timestamp t :sort (1 . ?a) :link nil :scope nil :hidefiles t :maxlevel 8 :stepskip0 t #+END:" "Clock table to use for custom clock reports.") (defvar prot-org-clock--ranges '( today yesterday thisweek lastweek thismonth lastmonth thisyear lastyear untilnow) "Time ranges of my interest for clock reports.") (defvar prot-org-clock--report-range-history nil "Minibuffer history for `prot-org-clock--report-range-prompt'.") (defun prot-org-clock--report-range-prompt () "Prompt for a clock table range among `prot-org-clock--ranges'." (let ((default (car prot-org-clock--report-range-history))) (completing-read (format-prompt "Select a time range for the clock" default) prot-org-clock--ranges nil :require-match nil 'prot-org-clock--report-range-history default))) (defun prot-org-clock--get-report (scope) "Produce clock report with current file SCOPE and return its buffer. SCOPE is a symbol of either `file' or `subtree'. If the former, then use the entire file's contents. Else use those of the current subtree." (let ((buffer (get-buffer-create "*prot-org-custom-clock-report*"))) (save-restriction (unwind-protect (progn (pcase scope ('file nil) ('subtree (org-narrow-to-subtree)) (_ (error "The scope `%s' is unknown" scope))) (let ((contents (buffer-substring (point-min) (point-max)))) (with-current-buffer buffer (erase-buffer) (org-mode) (save-excursion (insert (format "%s\n\n" prot-org-clock--template-with-effort)) (insert contents)) (save-excursion (let ((range (prot-org-clock--report-range-prompt))) (goto-char (line-end-position)) (insert (concat " :block " range)))) (org-dblock-update)))) (widen))) buffer)) ;;;###autoload (defun prot-org-clock-report-current-subtree-or-file (&optional whole-buffer) "Produce a clock report in a new buffer for the subtree at point. With optional WHOLE-BUFFER as a non-nil value, operate on the entire file. When called interactively WHOLE-BUFFER is a prefix argument." (interactive "P") (when-let* ((buffer (prot-org-clock--get-report (if whole-buffer 'file 'subtree)))) (pop-to-buffer buffer))) ;;;###autoload (defun prot-org-clock-select-heading-and-clock-report () "Select a heading in a file and do a clock report for it in a new buffer." (interactive) (call-interactively 'prot-org-select-heading-in-file) (call-interactively 'prot-org-clock-report-current-subtree-or-file)) ;;;;; Coaching-related Org custom clocking ;; TODO 2024-12-15: This sort of thing must exist in Org, but I did ;; not find it. (defun prot-org--timestamp-to-time (string) "Return time object of STRING timestamp." (org-timestamp-to-time (org-timestamp-from-string string))) (defun prot-org-coach--get-entries (todo-keyword string since) "Get Org entries matching TODO-KEYWORD followed by STRING in the heading. Limit entries to those whole deadline/scheduled is equal or greater to SINCE date. Each entry is a plist of :heading, :contents, :started, :closed." (or (delq nil (org-map-entries (lambda () (when-let* ((case-fold-search t) (started (prot-org--timestamp-to-time (or (org-entry-get nil "DEADLINE") (org-entry-get nil "SCHEDULED")))) (closed (prot-org--timestamp-to-time (org-entry-get nil "CLOSED"))) ((re-search-forward (format "\\<%s\\>.*\\<%s\\>" todo-keyword string) (line-end-position) t 1)) ((org-time-less-p since started))) (list :heading (org-get-heading :no-tags :no-todo :no-priority :no-comment) :contents (org-get-entry) :started started :closed closed))))) (user-error "No entries with heading matching `\\<%s\\>.*\\<%s\\>'" todo-keyword string))) (defvar prot-org-coach--name-history nil "Minibuffer history of `prot-org-coach--name-prompt'.") (defun prot-org-coach--name-prompt () "Prompt for name of person." (let ((default (car prot-org-coach--name-history))) (read-string (format-prompt "Name of person" default) nil 'prot-org-coach--name-history default))) ;;;###autoload (defun prot-org-coach-report (name since) "Produce clock report for coaching with person of NAME. SINCE is the date (of time 00:00) to count from until now." (interactive (list (prot-org-coach--name-prompt) (format "[%s]" (org-read-date)))) (if-let* ((since-object (prot-org--timestamp-to-time since)) (entries (prot-org-coach--get-entries "done" name since-object)) (buffer (get-buffer-create "*prot-org-coach-entries*"))) (with-current-buffer (pop-to-buffer buffer) (erase-buffer) (org-mode) (dolist (entry entries) (insert (format "* %s\n%s\n\n" (plist-get entry :heading) (plist-get entry :contents))) (org-clock-in nil (plist-get entry :started)) (org-clock-out nil t (plist-get entry :closed))) (goto-char (point-min)) (save-excursion (insert (format "%s\n\n" prot-org-clock--template-no-effort))) (save-excursion (goto-char (line-end-position)) (insert (format " :tstart %S" since))) (org-dblock-update)) (user-error "No entries for name `%s'" name))) ;;;; org-agenda (declare-function calendar-day-name "calendar") (declare-function calendar-day-of-week "calendar") (declare-function calendar-month-name "calendar") (declare-function org-days-to-iso-week "org") (declare-function calendar-absolute-from-gregorian "calendar") (defvar org-agenda-format-date) ;;;###autoload (defun prot-org-agenda-format-date-aligned (date) "Format a DATE string for display in the daily/weekly agenda. This function makes sure that dates are aligned for easy reading. Slightly tweaked version of `org-agenda-format-date-aligned' that produces dates with a fixed length." (require 'cal-iso) (let* ((dayname (calendar-day-name date t)) (day (cadr date)) (day-of-week (calendar-day-of-week date)) (month (car date)) (monthname (calendar-month-name month t)) (year (nth 2 date)) (iso-week (org-days-to-iso-week (calendar-absolute-from-gregorian date))) ;; (weekyear (cond ((and (= month 1) (>= iso-week 52)) ;; (1- year)) ;; ((and (= month 12) (<= iso-week 1)) ;; (1+ year)) ;; (t year))) (weekstring (if (= day-of-week 1) (format " (W%02d)" iso-week) ""))) (format "%s %2d %s %4d%s" dayname day monthname year weekstring))) (defvar org-priority-highest) (defun prot-org-agenda-include-priority-no-timestamp () "Return nil if heading has a priority but no timestamp. Otherwise, return the buffer position from where the search should continue, per `org-agenda-skip-function'." (let ((point (point))) (if (and (eq (nth 3 (org-heading-components)) ?A) (not (org-get-deadline-time point)) (not (org-get-scheduled-time point))) nil (line-beginning-position 2)))) (defvar prot-org-custom-daily-agenda ;; NOTE 2021-12-08: Specifying a match like the following does not ;; work. ;; ;; tags-todo "+PRIORITY=\"A\"" ;; ;; So we match everything and then skip entries with ;; `org-agenda-skip-function'. `((tags-todo "*" ((org-agenda-overriding-header "Important tasks without a date\n") ;; NOTE 2024-10-31: Those used to work, but now the ;; query for the timestamp is ignored. I thus wrote ;; `prot-org-agenda-include-priority-no-timestamp'. ;; ;; (org-agenda-skip-function '(org-agenda-skip-subtree-if nil '(timestamp))) ;; (org-agenda-skip-function ;; `(org-agenda-skip-entry-if ;; 'notregexp ,(format "\\[#%s\\]" (char-to-string org-priority-highest)))) (org-agenda-skip-function #'prot-org-agenda-include-priority-no-timestamp) (org-agenda-block-separator nil))) (agenda "" ((org-agenda-overriding-header "\nPending scheduled tasks") (org-agenda-time-grid nil) (org-agenda-start-on-weekday nil) (org-agenda-span 1) (org-agenda-show-all-dates nil) (org-scheduled-past-days 365) ;; Excludes today's scheduled items (org-scheduled-delay-days 1) (org-agenda-block-separator nil) (org-agenda-entry-types '(:scheduled)) (org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done)) (org-agenda-skip-function '(org-agenda-skip-entry-if 'regexp "ROUTINE")) (org-agenda-day-face-function (lambda (date) 'org-agenda-date)) (org-agenda-format-date ""))) (agenda "" ((org-agenda-overriding-header "\nToday's agenda\n") (org-agenda-span 1) (org-deadline-warning-days 0) (org-agenda-block-separator nil) (org-scheduled-past-days 0) (org-agenda-skip-function '(org-agenda-skip-entry-if 'regexp "ROUTINE")) ;; We don't need the `org-agenda-date-today' ;; highlight because that only has a practical ;; utility in multi-day views. (org-agenda-day-face-function (lambda (date) 'org-agenda-date)) (org-agenda-format-date "%A %-e %B %Y"))) ;; (agenda "" ((org-agenda-overriding-header "\nRoutine") ;; (org-agenda-time-grid nil) ;; (org-agenda-start-on-weekday nil) ;; (org-agenda-span 1) ;; (org-agenda-show-all-dates nil) ;; (org-scheduled-past-days 365) ;; ;; Excludes today's scheduled items ;; (org-scheduled-delay-days 1) ;; (org-agenda-block-separator nil) ;; (org-agenda-entry-types '(:scheduled)) ;; (org-agenda-skip-function '(org-agenda-skip-entry-if 'notregexp "ROUTINE")) ;; (org-agenda-day-face-function (lambda (date) 'org-agenda-date)) ;; (org-agenda-format-date ""))) (agenda "" ((org-agenda-overriding-header "\nNext three days\n") (org-agenda-start-on-weekday nil) (org-agenda-start-day nil) (org-agenda-start-day "+1d") (org-agenda-span 3) (org-deadline-warning-days 0) (org-agenda-block-separator nil) (org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done)))) (agenda "" ((org-agenda-overriding-header "\nUpcoming deadlines (+14d)\n") (org-agenda-time-grid nil) (org-agenda-start-on-weekday nil) ;; We don't want to replicate the previous section's ;; three days, so we start counting from the day after. (org-agenda-start-day "+4d") (org-agenda-span 14) (org-agenda-show-all-dates nil) (org-deadline-warning-days 0) (org-agenda-block-separator nil) (org-agenda-entry-types '(:deadline)) (org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))))) "Custom agenda for use in `org-agenda-custom-commands'.") ;;;;; agenda appointments (defvar prot-org-agenda-after-edit-hook nil "Hook that runs after select Org commands. To be used with `advice-add'.") (defun prot-org--agenda-after-edit (&rest _) "Run `prot-org-agenda-after-edit-hook'." (run-hooks 'prot-org-agenda-after-edit-hook)) (defvar prot-org-after-deadline-or-schedule-hook nil "Hook that runs after `org--deadline-or-schedule'. To be used with `advice-add'.") (defvar prot-org--appt-agenda-commands '( org-agenda-archive org-agenda-deadline org-agenda-schedule org-agenda-todo org-archive-subtree) "List of commands that run `prot-org-agenda-after-edit-hook'.") (dolist (fn prot-org--appt-agenda-commands) (advice-add fn :after #'prot-org--agenda-after-edit)) (defun prot-org--after-deadline-or-schedule (&rest _) "Run `prot-org-after-deadline-or-schedule-hook'." (run-hooks 'prot-org-after-deadline-or-schedule-hook)) (defun prot-org-org-agenda-to-appt () "Make `org-agenda-to-appt' always refresh appointment list." (org-agenda-to-appt :refresh)) (dolist (hook '(org-capture-after-finalize-hook org-after-todo-state-change-hook org-agenda-after-show-hook prot-org-agenda-after-edit-hook)) (add-hook hook #'prot-org-org-agenda-to-appt)) (declare-function org--deadline-or-schedule "org" (arg type time)) (advice-add #'org--deadline-or-schedule :after #'prot-org--after-deadline-or-schedule) (add-hook 'prot-org-after-deadline-or-schedule-hook #'prot-org-org-agenda-to-appt) ;;;; org-export (declare-function org-html-export-as-html "org") (declare-function org-texinfo-export-to-info "org") ;;;###autoload (defun prot-org-ox-html () "Streamline HTML export." (interactive) (org-html-export-as-html nil nil nil t nil)) ;;;###autoload (defun prot-org-ox-texinfo () "Streamline Info export." (interactive) (org-texinfo-export-to-info)) ;;;; org-id (declare-function org-id-add-location "org") (declare-function org-with-point-at "org") (declare-function org-entry-get "org") (declare-function org-id-new "org") (declare-function org-entry-put "org") ;; Original idea: ;; <https://writequit.org/articles/emacs-org-mode-generate-ids.html>. (defun prot-org--id-get () "Get the CUSTOM_ID of the current entry. If the entry already has a CUSTOM_ID, return it as-is, else create a new one." (let* ((pos (point)) (id (org-entry-get pos "CUSTOM_ID"))) (if (and id (stringp id) (string-match-p "\\S-" id)) id (setq id (org-id-new "h")) (org-entry-put pos "CUSTOM_ID" id) id))) (declare-function org-map-entries "org") ;;;###autoload (defun prot-org-id-headlines () "Add missing CUSTOM_ID to all headlines in current file." (interactive) (org-map-entries (lambda () (prot-org--id-get)))) ;;;###autoload (defun prot-org-id-headline () "Add missing CUSTOM_ID to headline at point." (interactive) (prot-org--id-get)) ;;;###autoload (defun prot-org-get-dotemacs-link () "Get URL to current heading in my dotemacs file." (declare (interactive-only t)) (interactive) (if (and (derived-mode-p 'org-mode) (string-match-p "prot-emacs\\.org\\'" buffer-file-name)) (if-let* ((id (org-entry-get (point) "CUSTOM_ID")) (url (concat "https://protesilaos.com/emacs/dotemacs#" id))) (progn (kill-new url) (message "Copied %s" (propertize url 'face 'success))) (error "No CUSTOM_ID for the current entry")) (user-error "You are not in the right file"))) (provide 'prot-org) ;;; prot-org.el ends here
6.13. The prot-pair.el
library
;;; prot-pair.el --- Insert character pair around symbol or region -*- lexical-binding: t -*- ;; Copyright (C) 2023-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or (at ;; your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; Insert character pair around symbol or region using minibuffer ;; completion. ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (defgroup prot-pair nil "Insert character pair around symbol or region." :group 'editing) (defcustom prot-pair-pairs '((?' :description "Single quotes" :pair (?' . ?')) (?\" :description "Double quotes" :pair (?\" . ?\")) (?‘ :description "Single curly quotes" :pair (?‘ . ?’)) (?“ :description "Double curly quotes" :pair (?“ . ?”)) (?\> :description "Natural language quotes" :pair prot-pair-insert-natural-language-quotes) (?\( :description "Parentheses" :pair (?\( . ?\))) (?{ :description "Curly brackets" :pair (?{ . ?})) (?\[ :description "Square brackets" :pair (?\[ . ?\])) (?\< :description "Angled brackets" :pair (?\< . ?\>)) (?@ :description "At signs" :pair (?@ . ?@)) (?= :description "Equals signs" :pair (?= . ?=)) (?+ :description "Plus signs" :pair (?+ . ?+)) (?` :description "Backticks" :pair prot-pair-insert-backticks) (?~ :description "Tildes" :pair (?~ . ?~)) (?* :description "Asterisks" :pair (?* . ?*)) (?/ :description "Forward slashes" :pair (?/ . ?/)) (?_ :description "Underscores" :pair (?_ . ?_))) "Alist of pairs for use with `prot-pair-insert'. Each element in the list is a list whose `car' is a character and the `cdr' is a plist with a `:description' and `:pair' keys. The `:description' is a string used to describe the character/pair in interactive use, while `:pair' is a cons cell referencing the opening and closing characters. The value of `:pair' can also be the unquoted symbol of a function. The function is called with no arguments and must return a cons cell of two characters. Examples of such functions are `prot-pair-insert-natural-language-quotes' and `prot-pair-insert-backticks'" :type '(alist :key-type character :value-type (plist :options (((const :tag "Pair description" :description) string) ((const :tag "Characters" :pair) (choice (cons character character) function))))) :group 'prot-pair) (defun prot-pair-insert-backticks () "Return pair of backticks for `prot-pair-pairs'. When the major mode is derived from `lisp-mode', return a pair of backtick and single quote, else two backticks." (if (derived-mode-p 'lisp-mode 'lisp-data-mode) (cons ?` ?') (cons ?` ?`))) (defun prot-pair-insert-natural-language-quotes () "Return pair of quotes for `prot-pair-pairs', per natural language." ;; There are more here: <https://en.wikipedia.org/wiki/Quotation_mark>. ;; I cover the languages I might type in. (cond ((and current-input-method (string-match-p "\\(greek\\|french\\|spanish\\)" current-input-method)) (cons ?« ?»)) (t (cons ?\" ?\")))) (defvar prot-pair--insert-history nil "Minibuffer history of `prot-pair--insert-prompt'.") (defun prot-pair--annotate (character) "Annotate CHARACTER with its description in `prot-pair-pairs'." (when-let* ((char (if (characterp character) character (string-to-char character))) (plist (alist-get char prot-pair-pairs)) (description (plist-get plist :description))) (format " %s" description))) (defun prot-pair--get-pair (character) "Get the pair of corresponding to CHARACTER." (when-let* ((char (if (characterp character) character (string-to-char character))) (plist (alist-get char prot-pair-pairs)) (pair (plist-get plist :pair))) pair)) (defun prot-pair--insert-prompt () "Prompt for pair among `prot-pair-pairs'." (let ((default (car prot-pair--insert-history)) (candidates (mapcar (lambda (char) (char-to-string (car char))) prot-pair-pairs)) (completion-extra-properties `(:annotation-function ,#'prot-pair--annotate))) (completing-read (format-prompt "Select pair" default) candidates nil :require-match nil 'prot-pair--insert-history default))) (defun prot-pair--insert-bounds () "Return boundaries of symbol at point or active region." (if (region-active-p) (cons (region-beginning) (region-end)) (bounds-of-thing-at-point 'symbol))) ;;;###autoload (defun prot-pair-insert (pair n) "Insert N number of PAIR around object at point. PAIR is one among `prot-pair-pairs'. The object at point is either a symbol or the boundaries of the active region. N is a numeric prefix argument, defaulting to 1 if none is provided in interactive use." (interactive (list (prot-pair--get-pair (prot-pair--insert-prompt)) (prefix-numeric-value current-prefix-arg))) (let* ((bounds (prot-pair--insert-bounds)) (beg (car bounds)) (end (1+ (cdr bounds))) ; 1+ because we want the character after it (characters (if (functionp pair) (funcall pair) pair))) (dotimes (_ n) (save-excursion (goto-char beg) (insert (car characters)) (goto-char end) (setq end (1+ end)) (insert (cdr characters)))) (goto-char (+ end (1- n))))) ;;;###autoload (defun prot-pair-delete () "Delete pair following or preceding point. For Emacs version 28 or higher, the feedback's delay is controlled by `delete-pair-blink-delay'." (interactive) (if (eq (point) (cdr (bounds-of-thing-at-point 'sexp))) (delete-pair -1) (delete-pair 1))) (provide 'prot-pair) ;;; prot-pair.el ends here
6.14. The prot-prefix.el
library
[ Watch: define prefix/leader key (nested key maps) (2024-01-29). ]
;;; prot-prefix.el --- Prefix keymap for my dotemacs -*- lexical-binding: t -*- ;; Copyright (C) 2023-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or (at ;; your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; Prefix keymap for my custom keymaps. ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (declare-function prot-simple-kill-buffer-current "prot-simple" (&optional arg)) (declare-function prot-simple-rename-file-and-buffer "prot-simple" (name)) (declare-function prot-simple-buffers-major-mode "prot-simple") (declare-function prot-simple-buffers-vc-root "prot-simple") (declare-function beframe-buffer-menu "beframe" (&optional frame &key sort)) (defvar-keymap prot-prefix-buffer-map :doc "Prefix keymap for buffers." :name "Buffer" :prefix 'prot-prefix-buffer "m" #'beframe-buffer-menu "b" #'switch-to-buffer "B" #'prot-simple-buffers-major-mode "c" #'clone-indirect-buffer-other-window "f" #'fit-window-to-buffer "k" #'prot-simple-kill-buffer-current "g" #'revert-buffer-quick "r" #'prot-simple-rename-file-and-buffer "n" #'next-buffer "p" #'previous-buffer "v" #'prot-simple-buffers-vc-root) (defvar-keymap prot-prefix-file-map :doc "Prefix keymaps for files." :name "File" :prefix 'prot-prefix-file "f" #'find-file "F" #'find-file-other-window "b" #'bookmark-jump "d" #'dired "l" #'find-library "m" #'man) (defvar-keymap prot-prefix-insert-map :doc "Prefix keymap for character insertion." :name "Insert" :prefix 'prot-prefix-insert "i" #'insert-char "e" #'emoji-search "q" #'quoted-insert "s" #'emoji-search "l" #'emoji-list) (declare-function logos-focus-mode "logos") (declare-function keycast-mode-line-mode "keycast") (declare-function rainbow-mode "rainbow") (declare-function spacious-padding-mode "spacious-padding") (defvar-keymap prot-prefix-mode-map :doc "Prefix keymap for minor mode toggles." :name "Toggle" :prefix 'prot-prefix-mode "f" #'flymake-mode "h" #'hl-line-mode "k" #'keycast-mode-line-mode "l" #'logos-focus-mode "m" #'menu-bar-mode "n" #'display-line-numbers-mode "t" #'toggle-truncate-lines "s" #'spacious-padding-mode "r" #'rainbow-mode "v" #'variable-pitch-mode) (defvar-keymap prot-prefix-window-map :doc "Prefix keymap for windows." :name "Window" :prefix 'prot-prefix-window "u" #'winner-undo "r" #'winner-redo "b" #'balance-windows-area "d" #'toggle-window-dedicated "0" #'delete-window "1" #'delete-other-windows "!" #'delete-other-windows-vertically "2" #'split-window-below "@" #'split-root-window-below "3" #'split-window-right "#" #'split-root-window-right "o" #'other-window "^" #'tear-off-window "h" #'windmove-left "j" #'windmove-down "k" #'windmove-up "l" #'windmove-right "H" #'windmove-swap-states-left "J" #'windmove-swap-states-down "K" #'windmove-swap-states-up "L" #'windmove-swap-states-right) (declare-function consult-find "consult" (&optional dir initial)) (declare-function consult-ripgrep "consult" (&optional dir initial)) (declare-function prot-search-grep "prot-search" (regexp &optional recursive)) (declare-function prot-search-grep-todo-keywords "prot-search" (&optional arg)) (declare-function prot-search-occur-browse-url "prot-search") (declare-function prot-search-occur-outline "prot-search" (&optional arg)) (declare-function prot-simple-flush-and-diff "prot-simple" (regexp beg end)) (defvar-keymap prot-prefix-search-map :doc "Prefix keymap for search (and replace) commands." :name "Search" :prefix 'prot-prefix-search "f" #'consult-find "d" #'prot-simple-flush-and-diff "g" #'prot-search-grep "o" #'prot-search-occur-outline "r" #'consult-ripgrep "t" #'prot-search-grep-todo-keywords "u" #'prot-search-occur-browse-url) (declare-function prot-simple-transpose-chars "prot-simple") (declare-function prot-simple-transpose-lines "prot-simple" (arg)) (declare-function prot-simple-transpose-paragraphs "prot-simple" (arg)) (declare-function prot-simple-transpose-sentences "prot-simple" (arg)) (declare-function prot-simple-transpose-words "prot-simple" (arg)) (declare-function prot-simple-transpose-sexps "prot-simple" (arg)) (defvar-keymap prot-prefix-transpose-map :doc "Prefix keymap for object transposition." :name "Transpose" :prefix 'prot-prefix-transpose "c" #'prot-simple-transpose-chars "l" #'prot-simple-transpose-lines "p" #'prot-simple-transpose-paragraphs "s" #'prot-simple-transpose-sentences "w" #'prot-simple-transpose-words "x" #'prot-simple-transpose-sexps) (defvar-keymap prot-prefix-expression-map :doc "Prefix keymap for s-expression motions." :name "S-EXP" :prefix 'prot-prefix-expression "a" #'beginning-of-defun "e" #'end-of-defun "f" #'forward-sexp "b" #'backward-sexp "n" #'forward-list "p" #'backward-list "d" #'up-list ; confusing name for what looks "out and down" to me "t" #'transpose-sexps "u" #'backward-up-list ; the actual "up" "k" #'kill-sexp "DEL" #'backward-kill-sexp) (declare-function winner-undo "winner") (declare-function winner-redo "winner") (declare-function magit-status "magit" (&optional directory cache)) (declare-function prot-simple-other-windor-or-frame "prot-simple") ;; NOTE 2024-02-17: Some cons cells here have a symbol as a `cdr' and ;; some do not. The former are those which define a prefix command ;; (per `define-prefix-command'). This is a symbol that references ;; the keymaps, thus making our binding an indirection: if we update ;; the key map, we automatically get the new key bindings. Whereas ;; when we bind a key to the value of a variable, we have to update ;; the key map and then the binding for changes to propagate. (defvar-keymap prot-prefix-map :doc "Prefix keymap with multiple subkeymaps." :name "Prot Prefix" :prefix 'prot-prefix "0" #'delete-window "1" #'delete-other-windows "!" #'delete-other-windows-vertically "^" #'tear-off-window "2" #'split-window-below "@" #'split-root-window-below "3" #'split-window-right "#" #'split-root-window-right "o" #'other-window "O" #'prot-simple-other-windor-or-frame "Q" #'save-buffers-kill-emacs "b" (cons "Buffer" 'prot-prefix-buffer) "c" #'world-clock "f" (cons "File" 'prot-prefix-file) "g" #'magit-status "h" (cons "Help" help-map) "i" (cons "Insert" 'prot-prefix-insert) "j" #'dired-jump "m" (cons "Minor modes" 'prot-prefix-mode) "n" (cons "Narrow" narrow-map) "p" (cons "Project" project-prefix-map) "r" (cons "Rect/Registers" ctl-x-r-map) "s" (cons "Search" 'prot-prefix-search) "t" (cons "Transpose" 'prot-prefix-transpose) "u" #'universal-argument "v" (cons "Version Control" 'vc-prefix-map) "w" (cons "Window" 'prot-prefix-window) "x" (cons "S-EXP" 'prot-prefix-expression)) ;; ;; NOTE 2024-02-17: This is not needed anymore, because I bind a cons ;; ;; cell to the key. The `car' of it is the description, which ;; ;; `which-key-mode' understands. ;; ;; (with-eval-after-load 'which-key ;; (which-key-add-keymap-based-replacements prot-prefix-map ;; "b" `("Buffer" . ,prot-prefix-buffer-map) ;; "f" `("File" . ,prot-prefix-file-map) ;; "h" `("Help" . ,help-map) ;; "i" `("Insert" . ,prot-prefix-insert-map) ;; "m" `("Mode" . ,prot-prefix-mode-map) ;; "n" `("Narrow" . ,narrow-map) ;; "p" `("Project" . ,project-prefix-map) ;; "r" `("C-x r" . ,ctl-x-r-map) ;; "s" `("Search" . ,prot-prefix-search-map) ;; "t" `("Transpose" . ,prot-prefix-transpose-map) ;; "v" `("C-x v" . ,vc-prefix-map) ;; "w" `("Window" . ,prot-prefix-window-map) ;; "x" `("S-EXP" . ,prot-prefix-expression-map))) ;; What follows is an older experiment with transient. I like its ;; visuals, though find it hard to extend. Keymaps are easier for me, ;; as I can add commands to one of the subkeymaps and they are readily ;; available without evaluating anything else. Probably transient can ;; do this, though it is not obvious to me as to how. ;; (require 'transient) ;; ;; (transient-define-prefix prot-prefix-file nil ;; "Transient with file commands." ;; [["File or directory" ;; ("f" "find-file" find-file) ;; ("F" "find-file-other-window" find-file-other-window)] ;; ["Directory only" ;; ("d" "dired" dired) ;; ("D" "dired-other-window" dired-other-window)] ;; ["Documentation" ;; ("l" "find-library" find-library) ;; ("m" "man" man)]]) ;; ;; (transient-define-prefix prot-prefix-buffer nil ;; "Transient with buffer commands." ;; [["Switch" ;; ("b" "switch buffer" switch-to-buffer) ;; ("B" "switch buf other window" switch-to-buffer-other-window) ;; ("n" "next-buffer" next-buffer) ;; ("p" "previous-buffer" previous-buffer) ;; ("m" "buffer-menu" buffer-menu) ;; ("q" "bury-buffer" bury-buffer)] ;; ["Persist" ;; ("c" "clone buffer" clone-indirect-buffer) ;; ("C" "clone buf other window" clone-indirect-buffer-other-window) ;; ("r" "rename-buffer" rename-buffer) ;; ("R" "rename-uniquely" rename-uniquely) ;; ("s" "save-buffer" save-buffer) ;; ("w" "write-file" write-file)] ;; ["Destroy" ;; ("k" "kill-current-buffer" kill-current-buffer) ;; ("K" "kill-buffer-and-window" kill-buffer-and-window) ;; ("r" "revert-buffer" revert-buffer)]]) ;; ;; (transient-define-prefix prot-prefix-search nil ;; "Transient with search commands." ;; [["Search" ;; ("s" "isearch-forward" isearch-forward) ;; ("S" "isearch-forward-regexp" isearch-forward-regexp) ;; ("r" "isearch-backward" isearch-backward) ;; ("R" "isearch-backward-regexp" isearch-backward-regexp) ;; ("o" "occur" occur)] ;; ["Edit" ;; ("f" "flush-lines" flush-lines) ;; ("k" "keep-lines" keep-lines) ;; ("q" "query-replace" query-replace) ;; ("Q" "query-replace-regexp" query-replace-regexp)]]) ;; ;; (transient-define-prefix prot-prefix-window nil ;; "Transient with window commands." ;; [["Manage" ;; ("b" "balance-windows" balance-windows) ;; ("f" "fit-window-to-buffer" fit-window-to-buffer) ;; ("t" "tear-off-window" tear-off-window)] ;; ["Popup" ;; ("c" "calc" calc) ;; ("f" "list-faces-display" list-faces-display) ;; ("r" "re-builder" re-builder) ;; ("w" "world-clock" world-clock)]]) ;; ;; ;; This is independent of the transient, though still useful. ;; (defvar-keymap prot-prefix-repeat-map ;; :doc "Global prefix map for repeatable keybindings (per `repeat-mode')." ;; :name "Repeat" ;; :repeat t ;; "n" #'next-buffer ;; "p" #'previous-buffer ;; "<down>" #'enlarge-window ;; "<right>" #'enlarge-window-horizontally ;; "<up>" #'shrink-window ;; "<left>" #'shrink-window-horizontally) ;; ;; (transient-define-prefix prot-prefix-toggle nil ;; "Transient with minor mode toggles." ;; [["Interface" ;; ("c" "context-menu-mode" context-menu-mode) ;; ("m" "menu-bar-mode" menu-bar-mode) ;; ("s" "scroll-bar-mode" scroll-bar-mode) ;; ("C-t" "tool-bar-mode" tool-bar-mode)] ;; ["Tools" ;; ("d" "toggle-debug-on-error" toggle-debug-on-error) ;; ("f" "follow-mode" follow-mode) ;; ("l" "visual-line-mode" visual-line-mode) ;; ("v" "variable-pitch-mode" variable-pitch-mode) ;; ("t" "toggle-truncate-lines" toggle-truncate-lines) ;; ("C-s" "window-toggle-side-windows" window-toggle-side-windows)]]) ;; ;; (transient-define-prefix prot-prefix nil ;; "Transient with common commands. ;; Commands that bring up transients have ... in their description." ;; [["Common" ;; ("b" "Buffer..." prot-prefix-buffer) ;; ("f" "File..." prot-prefix-file) ;; ("s" "Search..." prot-prefix-search) ;; ("w" "Window..." prot-prefix-window) ;; ("t" "Toggle..." prot-prefix-toggle)] ;; ["Resize" ;; (" <up>" "Shrink vertically" shrink-window) ;; (" <down>" "Enlarge vertically" enlarge-window) ;; (" <left>" "Shrink horizontally" shrink-window-horizontally) ;; ("<right>" "Enlarge horizontally" enlarge-window-horizontally)] ;; ["Misc" ;; ("e" "Emoji transient..." emoji-insert) ;; ("E" "Emoji search" emoji-search) ;; ("C-e" "Emoji buffer" emoji-list) ;; ("RET" "Insert unicode" insert-char) ;; ("\\" "toggle-input-method" toggle-input-method)]]) (provide 'prot-prefix) ;;; prot-prefix.el ends here
6.15. The prot-project.el
library
;;; prot-project.el --- Extensions for project.el -*- lexical-binding: t -*- ;; Copyright (C) 2024-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or (at ;; your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; Extensions for project.el. ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (require 'project) (require 'tab-bar) ;;;; Switch to a project root Dired outright (defun prot-project--switch (directory &optional command) "Do the work of `project-switch-project' in the given DIRECTORY. With optional COMMAND, run it in DIRECTORY." (let ((command (or (when (functionp command) command) (if (symbolp project-switch-commands) project-switch-commands (project--switch-project-command)))) (buffer (current-buffer))) (unwind-protect (progn (setq-local project-current-directory-override directory) (call-interactively command)) (with-current-buffer buffer (kill-local-variable 'project-current-directory-override))))) (defun prot-project--frame-names () "Return a list of frame names." (mapcar #'car (make-frame-names-alist))) ;;;###autoload (defun prot-project-switch (directory) "Switch to project DIRECTORY. If DIRECTORY exists in a frame, select it. Otherwise switch to the project in DIRECTORY using `project-dired'." (interactive (list (funcall project-prompter))) (project--remember-dir directory) (let ((name (file-name-nondirectory (directory-file-name directory)))) (if (member name (prot-project--frame-names)) (select-frame-by-name name) (prot-project--switch directory 'project-dired)))) ;;;; Produce a VC root log for the project (defun prot-project-rename-vc-root-log (&rest _) "Rename the buffer of `vc-print-root-log' to mention the project." (when-let* ((root (vc-root-dir)) ((consp project--list)) ((member root (mapcar #'car project--list)))) (rename-buffer (format "*vc-root-log: %s*" root)))) (advice-add #'vc-print-root-log :after #'prot-project-rename-vc-root-log) ;;;; One tab per project ;; NOTE 2024-01-15 07:07:52 +0200: I define the "in tab" functions as ;; a coding exercise. I don't have a use for it, as I prefer to use ;; the approach of my `beframe' package instead. (defun prot-project-in-tab--get-tab-names (&optional frame) "Return list of tab names associated with FRAME. If FRAME is nil, use the current frame." (mapcar (lambda (tab) (alist-get 'name tab)) (frame-parameter frame 'tabs))) (defun prot-project-in-tab--create-tab (directory name) "Create new tab visiting DIRECTORY and named NAME." (tab-new) (find-file directory) (unwind-protect (prot-project--switch directory) (tab-rename name) ;; NOTE 2024-01-15 06:52 +0200: I am adding this because ;; `tab-rename' is not persistent for some reason. Probably a bug... (let* ((tabs (funcall tab-bar-tabs-function)) (tab-to-rename (nth (tab-bar--current-tab-index) tabs))) (setf (alist-get 'explicit-name tab-to-rename) name)))) ;;;###autoload (defun prot-project-in-tab (directory) "Switch to project DIRECTORY in a tab. If a tab is named after the non-directory component of DIRECTORY, switch to it. Otherwise, create a new tab and name it after the non-directory component of DIRECTORY. Use this as an alternative to `project-switch-project'." (interactive (list (funcall project-prompter))) (project--remember-dir directory) (let ((name (file-name-nondirectory (directory-file-name directory)))) (if (member name (prot-project-in-tab--get-tab-names)) (tab-switch name) (prot-project-in-tab--create-tab directory name)))) ;;;; Set up a project root ;; I don't actually have a use-case for `prot-project-find-root', ;; but I wrote it once so I keep it here in case I ever need it. ;; Use it like this: (prot-project-find-root c-mode "Makefile") (defmacro prot-project-find-root (mode file) "Define project root check for MODE given FILE. MODE must be the symbol of the major mode, without a quote. FILE is a string." (let ((project-find-fn (intern (format "project-find-%s-root" mode))) (major-mode-fn (intern (format "prot-%s-project-find-function" mode))) (file-symbol (intern file))) `(progn (defun ,project-find-fn (dir) (when-let* ((root (locate-dominating-file dir ,file))) (cons ',file-symbol root))) (cl-defmethod project-root ((project (head ,file-symbol))) (cdr project)) (defun ,(intern (format "prot-%s-project-find-function" mode)) () (add-hook 'project-find-functions #',project-find-fn :depth :local)) (add-hook ',(intern (format "%s-hook" mode)) #',major-mode-fn)))) (provide 'prot-project) ;;; prot-project.el ends here
6.16. The prot-scratch.el
library
;;; prot-scratch.el --- Scratch buffers for editable major mode of choice -*- lexical-binding: t -*- ;; Copyright (C) 2023-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; Set up a scratch buffer for an editable major mode of choice. The ;; idea is based on the `scratch.el' package by Ian Eure: ;; <https://github.com/ieure/scratch-el>. ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (require 'prot-common) (defgroup prot-scratch () "Scratch buffers for editable major mode of choice." :group 'editing) (defcustom prot-scratch-default-mode 'text-mode "Default major mode for `prot-scratch-scratch-buffer'." :type 'symbol :group 'prot-scratch) (defun prot-scratch--scratch-list-modes () "List known major modes." (let (symbols) (mapatoms (lambda (symbol) (when (and (functionp symbol) (or (provided-mode-derived-p symbol 'text-mode) (provided-mode-derived-p symbol 'prog-mode))) (push symbol symbols)))) symbols)) (defun prot-scratch--insert-comment () "Insert comment for major mode, if appropriate. Insert a comment if `comment-start' is non-nil and the buffer is empty." (when (and (prot-common-empty-buffer-p) comment-start) (insert (format "Scratch buffer for: %s\n\n" major-mode)) (goto-char (point-min)) (comment-region (line-beginning-position) (line-end-position)))) (defun prot-scratch--prepare-buffer (region &optional mode) "Add contents to scratch buffer and name it accordingly. REGION is added to the contents to the new buffer. Use the current buffer's major mode by default. With optional MODE use that major mode instead." (let ((major (or mode major-mode))) (with-current-buffer (pop-to-buffer (format "*%s scratch*" major)) (funcall major) (prot-scratch--insert-comment) (goto-char (point-max)) (unless (string-empty-p region) (when (prot-common-line-regexp-p 'non-empty) (insert "\n\n")) (insert region))))) (defvar prot-scratch--major-mode-history nil "Minibuffer history of `prot-scratch--major-mode-prompt'.") (defun prot-scratch--major-mode-prompt () "Prompt for major mode and return the choice as a symbol." (intern (completing-read "Select major mode: " (prot-scratch--scratch-list-modes) nil :require-match nil 'prot-scratch--major-mode-history))) (defun prot-scratch--capture-region () "Capture active region, else return empty string." (if (region-active-p) (buffer-substring-no-properties (region-beginning) (region-end)) "")) ;;;###autoload (defun prot-scratch-buffer (&optional arg) "Produce a scratch buffer matching the current major mode. With optional ARG as a prefix argument (\\[universal-argument]), use `prot-scratch-default-mode'. With ARG as a double prefix argument, prompt for a major mode with completion. Candidates are derivatives of `text-mode' or `prog-mode'. If region is active, copy its contents to the new scratch buffer. Buffers are named as *MAJOR-MODE scratch*. If one already exists for the given MAJOR-MODE, any text is appended to it." (interactive "P") (let ((region (prot-scratch--capture-region))) (pcase (prefix-numeric-value arg) (16 (prot-scratch--prepare-buffer region (prot-scratch--major-mode-prompt))) (4 (prot-scratch--prepare-buffer region prot-scratch-default-mode)) (_ (prot-scratch--prepare-buffer region))))) (provide 'prot-scratch) ;;; prot-scratch.el ends here
6.17. The prot-search.el
library
;;; prot-search.el --- Extensions to isearch, replace, grep for my dotemacs -*- lexical-binding: t -*- ;; Copyright (C) 2020-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; This covers my isearch.el, replace.el, and grep.el extensions, for ;; use in my Emacs setup: <https://protesilaos.com/emacs/dotemacs>. ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (require 'isearch) (require 'replace) (require 'grep) (require 'prot-common) (defgroup prot-search () "Setup for Isearch, Occur, and related." :group 'search) ;; NOTE 2021-09-16: Based on my git config for headings in diffs. Read: ;; <https://protesilaos.com/codelog/2021-01-26-git-diff-hunk-elisp-org/>. (defcustom prot-search-outline-regexp-alist '((emacs-lisp-mode . "^\\((\\|;;;+ \\)") (org-mode . "^\\(\\*+ +\\|#\\+[Tt][Ii][Tt][Ll][Ee]:\\)")) "Alist of regular expressions per major mode. For best results the key must be a symbol that corresponds to a major mode. To be used by `prot-search-occur-outline'." :type 'alist :group 'prot-search) (defcustom prot-search-todo-keywords (concat "TODO\\|FIXME\\|NOTE\\|REVIEW\\|XXX\\|KLUDGE" "\\|HACK\\|WARN\\|WARNING\\|DEPRECATED\\|BUG") "Regexp with search to-do keywords." :type 'string :group 'prot-search) ;;;; Isearch ;;;###autoload (defun prot-search-isearch-other-end () "End current search in the opposite side of the match. Particularly useful when the match does not fall within the confines of word boundaries (e.g. multiple words)." (interactive) (isearch-done) (when isearch-other-end (goto-char isearch-other-end))) ;;;###autoload (defun prot-search-isearch-abort-dwim () "Delete failed `isearch' input, single char, or cancel search. This is a modified variant of `isearch-abort' that allows us to perform the following, based on the specifics of the case: (i) delete the entirety of a non-matching part, when present; (ii) delete a single character, when possible; (iii) exit current search if no character is present and go back to point where the search started." (interactive) (if (eq (length isearch-string) 0) (isearch-cancel) (isearch-del-char) (while (or (not isearch-success) isearch-error) (isearch-pop-state))) (isearch-update)) ;;;###autoload (defun prot-search-isearch-repeat-forward (&optional arg) "Move forward, keeping point at the beginning of the match. Optionally move to ARGth match in the given direction." (interactive "p") (when (and isearch-forward isearch-other-end) (goto-char isearch-other-end)) (isearch-repeat-forward (or arg 1))) ;;;###autoload (defun prot-search-isearch-repeat-backward (&optional arg) "Move backward, keeping point at the beginning of the match. Optionally move to ARGth match in the given direction." (interactive "p") (when (and (not isearch-forward) isearch-other-end) (goto-char isearch-other-end)) (isearch-repeat-backward (or arg 1))) (defmacro prot-search-isearch-occurrence (name edge &optional doc) "Construct function for moving to `isearch' occurrence. NAME is the name of the function. EDGE is either the beginning or the end of the buffer. Optional DOC is the resulting function's docstring." `(defun ,name (&optional arg) ,doc (interactive "p") (let ((x (or arg 1)) (command (intern (format "isearch-%s-of-buffer" ,edge)))) (isearch-forward-symbol-at-point) (funcall command x)))) (prot-search-isearch-occurrence prot-search-isearch-beginning-of-buffer "beginning" "Run `isearch-beginning-of-buffer' for the symbol at point. With numeric ARG, move to ARGth occurrence counting from the beginning of the buffer.") (prot-search-isearch-occurrence prot-search-isearch-end-of-buffer "end" "Run `isearch-end-of-buffer' for the symbol at point. With numeric ARG, move to ARGth occurrence counting from the end of the buffer.") ;;;; Replace/Occur (defvar prot-search-markup-replacements '((elisp-to-org-code "`\\(.*?\\)'" "~\\1~") (elisp-to-org-verbatim "`\\(.*?\\)'" "=\\1=") (org-to-elisp-quote "[=~]\\(.*?\\)[=~]" "`\\1'") (org-to-markdown-code "[=~]\\(.*?\\)[=~]" "`\\1`")) "Common markup replacement patterns.") (defvar prot-search--replace-markup-history '() "Minibuffer history of `prot-search-replace-markup'.") (defun prot-search--replace-markup-prompt () "Prompt for `prot-search-replace-markup'." (let* ((def (nth 0 prot-search--replace-markup-history)) (prompt (if def (format "Replace markup TYPE [%s]: " def) "Replace markup TYPE: "))) (intern (completing-read prompt ;; TODO 2022-05-01: maybe older Emacs versions need to explicitly ;; map through the car of each list? prot-search-markup-replacements nil t nil 'prot-search--replace-markup-history def)))) (defun prot-search-replace-markup (type) "Perform TYPE of markup replacement. TYPE is the car of a list in `prot-search-markup-replacements'. When used interactively, prompt for completion among the available types. When the region is active, only perform replacements within its boundaries, else start from point to the end of the buffer." (interactive (list (prot-search--replace-markup-prompt))) (if-let* ((types prot-search-markup-replacements) ((memq type (mapcar #'car types))) (association (alist-get type types)) (search (nth 0 association)) (replace (nth 1 association))) (if (use-region-p) (replace-regexp-in-region search replace (region-beginning) (region-end)) (while (re-search-forward search nil t) (replace-match replace))) (user-error "`%s' is not part of `prot-search-markup-replacements'" type))) ;; NOTE 2023-01-14: See my `substitute' package instead of the ;; following: <https://github.com/protesilaos/substitute>. ;; (defun prot-search-isearch-replace-symbol () ;; "Run `query-replace-regexp' for the symbol at point." ;; (interactive) ;; (isearch-forward-symbol-at-point) ;; (isearch-query-replace-regexp)) (autoload 'goto-address-mode "goto-addr") ;;;###autoload (defun prot-search-occur-urls () "Produce buttonised list of all URLs in the current buffer." (interactive) (let ((buf-name (format "*links in <%s>*" (buffer-name)))) (add-hook 'occur-hook #'goto-address-mode) (occur-1 prot-common-url-regexp "\\&" (list (current-buffer)) buf-name) (remove-hook 'occur-hook #'goto-address-mode))) ;;;###autoload (defun prot-search-occur-browse-url () "Point browser at a URL in the buffer using completion. Which web browser to use depends on the value of the variable `browse-url-browser-function'. Also see `prot-search-occur-urls'." (interactive) (let ((matches nil)) (save-excursion (goto-char (point-min)) (while (search-forward-regexp prot-common-url-regexp nil t) (push (match-string-no-properties 0) matches))) (funcall browse-url-browser-function (completing-read "Browse URL: " matches nil t)))) (defvar prot-search--occur-outline-hist '() "Minibuffer history of `prot-search-occur-outline'.") (defun prot-search--occur-outline-prompt () "Helper prompt for `prot-search-occur-outline'." (let* ((alist prot-search-outline-regexp-alist) (key (car (assoc major-mode alist))) (default (or key (nth 1 prot-search--occur-outline-hist)))) (completing-read (format "Outline style [%s]: " default) (mapcar #'car alist) nil nil nil 'prot-search--occur-outline-hist default))) (defvar-local prot-search--remap-cookie nil "Current local value of `prot-search--remap-match-face'.") (defface prot-search-match '((t :inherit default)) "Face intended to override `match' buffer-locally.") (defun prot-search--remap-match-face (buf) "Remap `match' to `prot-search-match' in BUF." (with-current-buffer buf (setq prot-search--remap-cookie (face-remap-add-relative 'match 'prot-search-match)))) ;;;###autoload (defun prot-search-occur-outline (&optional arg) "Produce buffer outline from `prot-search-outline-regexp-alist'. With optional prefix ARG (\\[universal-argument]), prompt for a preset among the entries in `prot-search-outline-regexp-alist'. ARG may also be a string (or regular expression) when called from Lisp." (interactive "P") (let* ((regexp (when (and arg (not (stringp arg))) (prot-search--occur-outline-prompt))) (rx (cond ((stringp arg) arg) ((and arg (string= major-mode regexp)) (alist-get regexp prot-search-outline-regexp-alist)) ((assoc major-mode prot-search-outline-regexp-alist) (alist-get major-mode prot-search-outline-regexp-alist)) (t (user-error "Unknown outline style")))) (buf-name (format "*outline of <%s>*" (buffer-name)))) (occur-1 rx nil (list (current-buffer)) buf-name) ;; Because we are producing an outline, we do not need to know what ;; the exact matches are. (prot-search--remap-match-face buf-name) (add-to-history 'prot-search--occur-outline-hist regexp))) ;;;###autoload (defun prot-search-occur-todo-keywords (&optional context) "Produce Occur buffer with `prot-search-todo-keywords'. With optional numeric prefix argument for CONTEXT, show as many lines before and after each match. When called from Lisp CONTEXT must satisfy `natnump'. A faulty value is read as 0. Also see `prot-search-grep-todo-keywords'." (interactive "P") (let* ((case-fold-search nil) (num (cond (current-prefix-arg (prefix-numeric-value current-prefix-arg)) (t (if (natnump context) context 0)))) (buf-name (format "*keywords in <%s>*" (buffer-name)))) (occur-1 prot-search-todo-keywords num (list (current-buffer)) buf-name))) ;;;; Outline (defun prot-search--get-outline () "Return alist of outline outline-regexp and positions." (let* ((outline-regexp (format "^\\(?:%s\\)" (or (bound-and-true-p outline-regexp) "[*\^L]+"))) (heading-alist (bound-and-true-p outline-heading-alist)) (level-fun (or (bound-and-true-p outline-level) (lambda () ;; as in the default from outline.el (or (cdr (assoc (match-string 0) heading-alist)) (- (match-end 0) (match-beginning 0)))))) candidates) (save-excursion (goto-char (point-min)) (while (if (bound-and-true-p outline-search-function) (funcall outline-search-function) (re-search-forward outline-regexp nil t)) (push (format "%-5s %s" (line-number-at-pos (point)) (buffer-substring-no-properties (line-beginning-position) (line-end-position))) candidates) (goto-char (1+ (line-end-position))))) (if candidates (nreverse candidates) (user-error "No outline")))) (defun prot-search--outline-prompt () "Prompt for outline among headings retrieved by `prot-search--get-outline'." (completing-read "Go to outline: " (prot-common-completion-table-no-sort 'imenu (prot-search--get-outline)) nil :require-match)) (defvar prot-search-outline-hook nil "Normal hook to run at the end of `prot-search-outline'.") ;;;###autoload (defun prot-search-outline () "Go to the line of the given outline using completion." (interactive) (when-let* ((selection (prot-search--outline-prompt)) (line (string-to-number (car (split-string selection "\t"))))) (goto-line line) (run-hooks 'prot-search-outline-hook))) ;;;; Grep (defvar prot-search--grep-hist nil "Input history of grep searches.") (defun prot-search-grep-prompt (&optional recursive) "Prompt for grep pattern. With optional RECURSIVE, indicate that the search will be called recursively." (read-regexp (concat (if recursive (propertize "Recursive" 'face 'warning) "Local") " grep for PATTERN: ") nil 'prot-search--grep-hist)) ;;;###autoload (defun prot-search-grep (regexp &optional recursive) "Run grep for REGEXP. Search in the current directory using `lgrep'. With optional prefix argument (\\[universal-argument]) for RECURSIVE, run a search starting from the current directory with `rgrep'." (interactive (list (prot-search-grep-prompt current-prefix-arg) current-prefix-arg)) (unless grep-command (grep-compute-defaults)) (if recursive (rgrep regexp "*" default-directory) (lgrep regexp "*" default-directory))) ;;;###autoload (defun prot-search-grep-todo-keywords (&optional arg) "Use `prot-search-grep' to find `prot-search-todo-keywords'. With optional prefix ARG use git-grep instead for the entire repository (runs `prot-search-git-grep-todo-keywords'). If Git is not available on the system, run `prot-search-grep' recursively, starting from the current directory. Also see `prot-search-occur-todo-keywords'." (interactive "P") (cond (arg (if (executable-find "git") (prot-search-git-grep-todo-keywords) (prot-search-grep prot-search-todo-keywords t))) (t (prot-search-grep prot-search-todo-keywords)))) ;; NOTE 2022-01-30: We could use `project-find-regexp' but I prefer ;; grep's editable buffers. Besides, where is the fun in that when we ;; can use `compilation-start' instead? ;;;###autoload (defun prot-search-git-grep-todo-keywords () "Use the git-grep mechanism for `prot-search-todo-keywords'." (interactive) (let ((regexp prot-search-todo-keywords) (default-directory (or (vc-root-dir) (locate-dominating-file "." ".git") default-directory))) (compilation-start (format "git --no-pager grep -n --color=auto -r -I -E -e %s" regexp) 'grep-mode (lambda (mode) (format "*prot-search-git-%s for '%s'" mode regexp)) t))) (defun prot-search--add-revert-function (buffer mode fn regexp) "Append `revert-buffer-function' for FN with REGEXP to MODE BUFFER variables. See `prot-search-find-grep-buffer' (or related) for the kind of BUFFER this works with." (with-current-buffer buffer (setq-local revert-buffer-function (lambda (_ignore-auto _noconfirm) (funcall fn regexp)) ;; FIXME 2023-04-04: The `compile-command' does not ;; feel right here. We do it because in grep-mode the ;; g key runs `recompile' which falls back to the ;; `compile-command'. We want it to do the same thing ;; as `revert-buffer'. compile-command `(funcall ',fn ,regexp)) (let ((inhibit-read-only t)) (goto-char (point-min)) (when (re-search-forward (format "-*- mode: %s;" mode) (line-end-position) :no-error 1) (insert (format " revert-buffer-function: %S; compile-command %S;" `(lambda (_ignore-auto _noconfirm) (,fn ,regexp)) `(funcall ,fn ,regexp))))))) (defun prot-search--start-compilation (args mode buffer command query) "Run compilation with ARGS for MODE in BUFFER given COMMAND running QUERY." (compilation-start args (intern (format "%s-mode" mode)) (lambda (_mode) buffer) :highlight-regexp) (prot-search--add-revert-function buffer mode command query)) (defvar prot-search--find-grep-hist '() "Minibuffer history for `prot-search-find-grep-buffer' and related.") (defmacro prot-search-make-search (command docstring prompt function mode) "Produce COMMAND with DOCSTRING given PROMPT, FUNCTION, and MODE." `(defun ,command (query) ,(format "%s. Place the output in a buffer that runs `%s'. Store the invocation of this command with REGEXP in a buffer-local variable. When the buffer is written to a file, per `write-file', the `revert-buffer' command (typically bound to `g') can be used to re-run the search. The buffer contains information about the search results, including the exact command line flags that were used, the time the results were produced, and the number of matches. All matching entries are buttonized and function as links to the context they reference." docstring mode) (interactive (list (read-regexp ,prompt nil 'prot-search--find-grep-hist))) (let ((args (,function query)) (buffer-name (format "*prot-search-find for '%s'*" query))) (prot-search--start-compilation args ,mode buffer-name ',command query)))) (defun prot-search--find-grep-args (regexp) "Return find args to produce grep results for REGEXP." (concat "find " default-directory " -not " (shell-quote-argument "(") " -path " (shell-quote-argument "*/.git*") " -prune " (shell-quote-argument ")") " -type f" " -exec grep -nHE --color=auto " regexp " " (shell-quote-argument "{}") " " (shell-quote-argument ";") " ")) ;;;###autoload (autoload 'prot-search-find-grep-buffer "prot-search") (prot-search-make-search prot-search-find-grep-buffer "Combine find with grep to produce a buffer for REGEXP matches" "Find files matching REGEXP and show a grep buffer: " prot-search--find-grep-args "grep") (defun prot-search--find-grep-files-args (regexp) "Return find args to produce file listing with contents matching REGEXP." (concat "find " default-directory " -not " (shell-quote-argument "(") " -path " (shell-quote-argument "*/.git*") " -prune " (shell-quote-argument ")") " -type f" " -exec grep -qo --color=auto " regexp " " (shell-quote-argument "{}") " " (shell-quote-argument ";") " " "-ls")) ;;;###autoload (autoload 'prot-search-find-grep-files-buffer "prot-search") (prot-search-make-search prot-search-find-grep-files-buffer "Combine find with grep to produce a buffer for files matching REGEXP" "Find files with contents matching REGEXP and show a file listing: " prot-search--find-grep-files-args "dired") (defun prot-search--find-file-names-args (regexp) "Return find args to produce file listing with file names matching REGEXP." (concat "find " default-directory " -not " (shell-quote-argument "(") " -path " (shell-quote-argument "*/.git*") " -prune " (shell-quote-argument ")") " -type f" " -iname '*" regexp "*'" " -exec ls -AFhldvN --group-directories-first --time-style=long-iso --color=auto --hyperlink=never " (shell-quote-argument "{}") " " (shell-quote-argument ";"))) ;;;###autoload (autoload 'prot-search-find-files-buffer "prot-search") (prot-search-make-search prot-search-find-files-buffer "Use find to produce a buffer for file names matching REGEXP" "Find files with name matching REGEXP and show a file listing: " prot-search--find-file-names-args "dired") ;; (defun prot-search-find-grep-file (regexp) ;; "Use find to produce list of files that include REGEXP." ;; (interactive ;; (list ;; (read-regexp "Find and grep for REGEXP: " nil 'prot-search--find-grep-hist))) ;; (let ((files (process-lines "find" ;; "-type" "f" ;; "-exec" "grep" "-nHE" "--color=auto" (format "'%s" regexp) " " ;; "-ls" " " ;; "{};") ;; )) ;; (find-file (completing-read "Find file: "files)))) (provide 'prot-search) ;;; prot-search.el ends here
6.18. The prot-shell.el
library
;;; prot-shell.el --- M-x shell extensions for my dotemacs -*- lexical-binding: t -*- ;; Copyright (C) 2023-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; This covers my shell.el extensions, for use in my Emacs setup: ;; <https://protesilaos.com/emacs/dotemacs>. ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (require 'shell) ;;;; Helper functions (defun prot-shell--beginning-of-prompt-p () "Return non-nil if point is at the beginning of a shell prompt." (if comint-use-prompt-regexp (looking-back comint-prompt-regexp (line-beginning-position)) (eq (point) (comint-line-beginning-position)))) (defun prot-shell--insert-and-send (&rest args) "Insert and execute ARGS in the last shell prompt. ARGS is a list of strings." (if (prot-shell--beginning-of-prompt-p) (progn (insert (mapconcat #'identity args " ")) (comint-send-input)) (user-error "Not at the beginning of prompt; won't insert: %s" args))) (defun prot-shell--last-input () "Return last input as a string." (buffer-substring-no-properties comint-last-input-start comint-last-input-end)) ;;;; Input from shell command history using completion (defun prot-shell--build-input-history () "Return `comint-input-ring' as a list." (when (and (ring-p comint-input-ring) (not (ring-empty-p comint-input-ring))) (let (history) ;; We have to build up a list ourselves from the ring vector. (dotimes (index (ring-length comint-input-ring)) (push (ring-ref comint-input-ring index) history)) (delete-dups history)))) (defvar prot-shell--input-history-completion-history nil "Minibuffer history of `prot-shell--input-history-prompt'. Not to be confused with the shell input history, which is stored in the `comint-input-ring' (see `prot-shell--build-input-history').") (defun prot-shell--input-history-prompt () "Prompt for completion against `prot-shell--build-input-history'." (let* ((history (prot-shell--build-input-history)) (default (car history))) (completing-read (format-prompt "Insert input from history" default) history nil :require-match nil 'prot-shell--input-history-completion-history default))) ;;;###autoload (defun prot-shell-input-from-history () "Insert command from shell input history. Only account for the history Emacs knows about, ignoring `comint-input-ring-file-name' (e.g. ~/.bash_history)." (declare (interactive-only t)) (interactive) (prot-shell--insert-and-send (prot-shell--input-history-prompt))) ;;;; Directory navigation ;;;;; Directory tracking (defvar prot-shell-cd-directories nil "List of accumulated `shell-last-dir'.") (with-eval-after-load 'savehist (add-to-list 'savehist-additional-variables 'prot-shell-cd-directories)) (defun prot-shell-track-cd (&rest _) "Track shell input of cd commands. Push `shell-last-dir' to `prot-shell-cd-directories'." (when-let* ((input (prot-shell--last-input)) ((string-match-p "cd " input))) (push shell-last-dir prot-shell-cd-directories))) (defun prot-shell-update-name-on-cd (&rest _) "Update the shell buffer name after a cd for use in `prot-shell'." (when-let* ((input (prot-shell--last-input)) ((string-match-p "cd " input))) (rename-buffer (format "*prot-shell in %s*" default-directory) :make-unique))) (defvar prot-shell--cd-history nil "Minibuffer history for `prot-shell-cd'.") (defun prot-shell--cd-prompt () "Prompt for a directory among `prot-shell-cd-directories'." (if-let* ((history prot-shell-cd-directories) (dirs (cons default-directory history)) (def (if (listp dirs) (car dirs) shell-last-dir))) (completing-read (format-prompt "Select directory" def) dirs nil :require-match nil 'prot-shell--cd-history def) (user-error "No directories have been tracked"))) ;;;###autoload (defun prot-shell-cd () "Switch to `prot-shell-cd-directories' using minibuffer completion." (declare (interactive-only t)) (interactive) (prot-shell--insert-and-send "cd" (prot-shell--cd-prompt))) ;;;;; VC root directory (defun prot-shell--get-vc-root-dir () "Return `vc-root-dir' or root of present Git repository." (or (vc-root-dir) (locate-dominating-file "." ".git"))) ;;;###autoload (defun prot-shell-cd-vc-root-dir () "Change into the `vc-root-dir'." (interactive) (if-let* ((root (prot-shell--get-vc-root-dir))) (prot-shell--insert-and-send "cd" root) (user-error "Cannot find the VC root of `%s'" default-directory))) ;;;; Bookmark support ;; NOTE 2023-08-18: I sent this to the Emacs maintainers as a patch ;; (bug#65039). I received approval to proceed with the change, but I ;; did not do it because a user reported an issue with SSH (TRAMP). I ;; do not have access to SSH and am not familiar with such workflows. ;; If/when that changes, I will try again. In the meantime, this is ;; good code and it works for me. ;; Adapted from esh-mode.el (declare-function bookmark-prop-get "bookmark" (bookmark prop)) (defun prot-shell-bookmark-name () "Return name of bookmark based on currect directory." (format "prot-shell-%s" (file-name-nondirectory (directory-file-name (file-name-directory default-directory))))) (defvar sh-shell-file) (defun prot-shell-bookmark-make-record () "Create a bookmark for the current Shell buffer." `(,(prot-shell-bookmark-name) (location . ,default-directory) (shell-file-name . ,sh-shell-file) (handler . prot-shell-bookmark-jump))) ;;;###autoload (defun prot-shell-bookmark-jump (bookmark) "Default BOOKMARK handler for Shell buffers." (let ((default-directory (bookmark-prop-get bookmark 'location)) (explicit-shell-file-name (bookmark-prop-get bookmark 'shell-file-name))) (shell (get-buffer-create (car bookmark))))) (put 'prot-shell-bookmark-jump 'bookmark-handler-type "Shell") ;; ;;;; Convert YouTube links to Invidious ;; ;; (defvar prot-shell-invidious-domains ;; '("invidious.io.lol" ;; "invidious.lunar.icu" ;; "iv.nboeck.de" ;; "vid.priv.au" ;; "invidious.tiekoetter.com" ;; "inv.in.projectsegfau.lt" ;; "onion.tube" ;; "yt.artemislena.eu" ;; "invidious.no-logs.com" ;; "yewtu.be" ;; "invidious.projectsegfau.lt" ;; "yt.oelrichsgarcia.de" ;; "invidious.0011.lt" ;; "inv.zzls.xyz" ;; "inv.bp.projectsegfau.lt" ;; "invidious.flokinet.to" ;; "iv.ggtyler.dev" ;; "invidious.slipfox.xyz" ;; "vid.puffyan.us" ;; "inv.pistasjis.net" ;; "inv.citw.lgbt" ;; "invidious.protokolla.fi" ;; "inv.makerlab.tech" ;; "inv.tux.pizza" ;; "invidious.privacydev.net") ;; "List of Invidious domains.") ;; ;; (defvar prot-shell-youtube-domains ;; '("www.youtube.com" ;; "youtu.be") ;; "List of YouTube domains.") ;; ;; (defvar prot-shell-yt-invidious-domains ;; (append prot-shell-youtube-domains ;; prot-shell-invidious-domains) ;; "List of YouTube and Invidious domains.") ;; ;; (defun prot-shell--get-random-invidious-instance () ;; "Return `random' index from `prot-shell-invidious-domains'." ;; (nth ;; (random (length prot-shell-invidious-domains)) ;; prot-shell-invidious-domains)) ;; ;; ;;;###autoload ;; (defun prot-shell-invidious () ;; "Convert `prot-shell-yt-invidious-domains' into a random Invidious instance." ;; (interactive) ;; (save-excursion ;; (goto-char (line-beginning-position)) ;; (while (re-search-forward (regexp-opt prot-shell-yt-invidious-domains) (line-end-position) :no-error) ;; (replace-match (prot-shell--get-random-invidious-instance))))) ;;;; Built-in Emacs commands ;; ;; `comint-input-filter-functions' ;; (defun prot-shell--intercept-input (input) ;; (when (string-match-p "man " input) ;; (comint-interrupt-subjob) ;; ;; TODO 2023-08-18: The idea is to interrupt the input, and split ;; ;; it such that, say, "man echo" becomes (man "echo") ;; ;; ;; ;; (let ((proc (get-buffer-process (current-buffer))) ;; ;; (inhibit-read-only t) ;; ;; replacement) ;; ;; (save-excursion ;; ;; (let ((pmark (progn (goto-char (process-mark proc)) ;; ;; (forward-line 0) ;; ;; (point-marker)))) ;; ;; (delete-region comint-last-input-end pmark) ;; ;; (goto-char (process-mark proc)) ;; ;; (setq replacement (concat "*** Called command externally ***\n" ;; ;; (buffer-substring pmark (point)))) ;; ;; (delete-region pmark (point)))) ;; ;; (comint-output-filter proc replacement)) ;; )) ;; ;; (add-hook 'comint-input-filter-functions #'prot-shell--intercept-input) ;;;; General commands (defun prot-shell--history-or-motion (history-fn motion-fn arg) "Call HISTORY-FN or MOTION-FN with ARG depending on where point is. If `prot-shell--beginning-of-prompt-p' returns non-nil call HISTORY-FN, else MOTION-FN." (let ((fn (if (or (prot-shell--beginning-of-prompt-p) (eq last-command 'comint-next-input) (eq last-command 'comint-previous-input)) history-fn motion-fn))) (funcall-interactively fn arg) (setq this-command fn))) ;;;###autoload (defun prot-shell-up-dwim (arg) "Return previous ARGth history input or go ARGth lines up. If point is at the beginning of a shell prompt, return previous input, otherwise perform buffer motion." (interactive "^p") (prot-shell--history-or-motion 'comint-previous-input 'previous-line arg)) ;;;###autoload (defun prot-shell-down-dwim (arg) "Return next ARGth history input or or go ARGth lines down. If point is at the beginning of a shell prompt, return previous input, otherwise perform buffer motion." (interactive "^p") (prot-shell--history-or-motion 'comint-next-input 'next-line arg)) ;;;###autoload (defun prot-shell () "Like `shell' but always start a new shell. Name the shell buffer after the `default-directory'. If the name of that buffer already exists, then reuse it." (interactive) (with-current-buffer (shell (format "*prot-shell in %s*" default-directory)) (add-hook 'comint-output-filter-functions #'prot-shell-update-name-on-cd nil :local))) ;;;; Minor mode setup (defvar-keymap prot-shell-mode-map :doc "Key map for `prot-shell-mode'." "<up>" #'prot-shell-up-dwim "<down>" #'prot-shell-down-dwim "C-c C-d" #'prot-shell-cd ;; "C-c C-i" #'prot-shell-invidious "C-c C-j" #'prot-shell-input-from-history "C-c C-." #'prot-shell-cd-vc-root-dir "C-c C-r" #'prot-shell-cd-vc-root-dir) (define-minor-mode prot-shell-mode "Provide extra functionality for the Emacs `shell'. Add a bookmark handler for shell buffer and activate the `prot-shell-mode-map': \\{prot-shell-mode-map}" :init-value nil :global nil (if prot-shell-mode (progn (add-hook 'comint-output-filter-functions #'prot-shell-track-cd nil :local) (setq-local bookmark-make-record-function #'prot-shell-bookmark-make-record)) (remove-hook 'comint-output-filter-functions #'prot-shell-track-cd :local) (setq-local bookmark-make-record-function nil))) (provide 'prot-shell) ;;; prot-shell.el ends here
6.19. The prot-simple.el
library
;;; prot-simple.el --- Common commands for my dotemacs -*- lexical-binding: t -*- ;; Copyright (C) 2020-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or (at ;; your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; Common commands for my Emacs: <https://protesilaos.com/emacs/dotemacs/>. ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (eval-when-compile (require 'cl-lib)) (require 'prot-common) (defgroup prot-simple () "Generic utilities for my dotemacs." :group 'editing) (defcustom prot-simple-date-specifier "%F" "Date specifier for `format-time-string'. Used by `prot-simple-inset-date'." :type 'string :group 'prot-simple) (defcustom prot-simple-time-specifier "%R %z" "Time specifier for `format-time-string'. Used by `prot-simple-inset-date'." :type 'string :group 'prot-simple) ;;; Commands ;;;; General commands (defun prot-simple--mark (bounds) "Mark between BOUNDS as a cons cell of beginning and end positions." (push-mark (car bounds)) (goto-char (cdr bounds)) (activate-mark)) ;;;###autoload (defun prot-simple-mark-sexp () "Mark symbolic expression at or near point. Repeat to extend the region forward to the next symbolic expression." (interactive) (if (and (region-active-p) (eq last-command this-command)) (ignore-errors (forward-sexp 1)) (when-let* ((thing (cond ((thing-at-point 'url) 'url) ((thing-at-point 'sexp) 'sexp) ((thing-at-point 'string) 'string) ((thing-at-point 'word) 'word)))) (prot-simple--mark (bounds-of-thing-at-point thing))))) ;;;###autoload (defun prot-simple-keyboard-quit-dwim () "Do-What-I-Mean behaviour for a general `keyboard-quit'. The generic `keyboard-quit' does not do the expected thing when the minibuffer is open. Whereas we want it to close the minibuffer, even without explicitly focusing it. The DWIM behaviour of this command is as follows: - When the region is active, disable it. - When a minibuffer is open, but not focused, close the minibuffer. - When the Completions buffer is selected, close it. - In every other case use the regular `keyboard-quit'." (interactive) (cond ((region-active-p) (keyboard-quit)) ((derived-mode-p 'completion-list-mode) (delete-completion-window)) ((> (minibuffer-depth) 0) (abort-recursive-edit)) (t (keyboard-quit)))) ;; DEPRECATED 2023-12-26: I have not used `prot-simple-describe-symbol' ;; since a very long time. The idea is fine, but having a key binding ;; to provide a shortcut for C-h o RET is wasteful. ;; (autoload 'symbol-at-point "thingatpt") ;; ;; ;;;###autoload ;; (defun prot-simple-describe-symbol () ;; "Run `describe-symbol' for the `symbol-at-point'." ;; (interactive) ;; (describe-symbol (symbol-at-point))) ;; DEPRECATED 2023-12-26: The `prot-simple-goto-definition' is a good ;; idea but it needs more work. Ultimately though, it is easier to ;; just produce a Help buffer and just go to the source from there by ;; typing 's'. ;; (declare-function help--symbol-completion-table "help-fns" (string pred action)) ;; ;; ;;;###autoload ;; (defun prot-simple-goto-definition (symbol) ;; "Prompt for SYMBOL and go to its source. ;; When called from Lisp, SYMBOL is a string." ;; (interactive ;; (list ;; (completing-read "Go to source of SYMBOL: " ;; #'help--symbol-completion-table ;; nil :require-match))) ;; (xref-find-definitions symbol)) ;; DEPRECATED 2023-12-26: I have no need for these commands. I was ;; just experimenting with a simple implementation. It is not robust. ;; I can fix it, but I will still not use it, so I am deprecating it ;; instead. ;; (autoload 'number-at-point "thingatpt") ;; ;; (defun prot-simple--number-operate (number amount operation) ;; "Perform OPERATION on NUMBER given AMOUNT and return the result. ;; OPERATION is the keyword `:increment' or `:decrement' to perform ;; `1+' or `1-', respectively." ;; (when (and (numberp number) (numberp amount)) ;; (let ((fn (pcase operation ;; (:increment #'+) ;; (:decrement #'-) ;; (_ (user-error "Unknown operation `%s' for number `%s'" operation number))))) ;; (funcall fn number amount)))) ;; ;; (defun prot-simple--number-replace (number amount operation) ;; "Perform OPERATION on NUMBER at point by AMOUNT." ;; (when-let* ((bounds (bounds-of-thing-at-point 'number)) ;; (replacement (prot-simple--number-operate number amount operation))) ;; (delete-region (car bounds) (cdr bounds)) ;; (save-excursion ;; (insert (number-to-string replacement))))) ;; ;; ;;;###autoload ;; (defun prot-simple-number-increment (number amount) ;; "Increment NUMBER by AMOUNT. ;; When called interactively, NUMBER is the one at point, while ;; AMOUNT is either 1 or that of a number prefix argument." ;; (interactive ;; (list ;; (number-at-point) ;; (prefix-numeric-value current-prefix-arg))) ;; (prot-simple--number-replace number amount :increment)) ;; ;; ;;;###autoload ;; (defun prot-simple-number-decrement (number amount) ;; "Decrement NUMBER by AMOUNT. ;; When called interactively, NUMBER is the one at point, while ;; AMOUNT is either 1 or that of a number prefix argument." ;; (interactive ;; (list ;; (number-at-point) ;; (prefix-numeric-value current-prefix-arg))) ;; (prot-simple--number-replace number amount :decrement)) ;;;; Commands for lines ;;;###autoload (defun prot-simple-new-line-below (n) "Create N empty lines below the current one. When called interactively without a prefix numeric argument, N is 1." (interactive "p") (goto-char (line-end-position)) (dotimes (_ n) (insert "\n"))) ;;;###autoload (defun prot-simple-new-line-above (n) "Create N empty lines above the current one. When called interactively without a prefix numeric argument, N is 1." (interactive "p") (let ((point-min (point-min))) (if (or (bobp) (eq (point) point-min) (eq (line-number-at-pos point-min) 1)) (progn (goto-char (line-beginning-position)) (dotimes (_ n) (insert "\n")) (forward-line (- n))) (forward-line (- n)) (prot-simple-new-line-below n)))) ;;;###autoload (defun prot-simple-copy-line () "Copy the current line to the `kill-ring'." (interactive) (copy-region-as-kill (line-beginning-position) (line-end-position))) (make-obsolete 'prot-simple-copy-line-or-region 'prot-simple-copy-line "2023-09-26") ;;;###autoload (defun prot-simple-kill-ring-save (beg end) "Copy the current region or line. When the region is active, use `kill-ring-save' between the BEG and END positions. Otherwise, copy the current line." (interactive "r") (if (region-active-p) (kill-ring-save beg end) (prot-simple-copy-line))) (defun prot-simple--duplicate-buffer-substring (boundaries) "Duplicate buffer substring between BOUNDARIES. BOUNDARIES is a cons cell representing buffer positions." (unless (consp boundaries) (error "`%s' is not a cons cell" boundaries)) (let ((beg (car boundaries)) (end (cdr boundaries))) (goto-char end) (newline) (insert (buffer-substring-no-properties beg end)))) ;;;###autoload (defun prot-simple-duplicate-line-or-region () "Duplicate the current line or active region." (interactive) (unless mark-ring ; needed when entering a new buffer (push-mark (point) t nil)) (prot-simple--duplicate-buffer-substring (if (region-active-p) (cons (region-beginning) (region-end)) (cons (line-beginning-position) (line-end-position))))) ;;;###autoload (defun prot-simple-yank-replace-line-or-region () "Replace line or region with latest kill. This command can then be followed by the standard `yank-pop' (default is bound to \\[yank-pop])." (interactive) (if (use-region-p) (delete-region (region-beginning) (region-end)) (delete-region (line-beginning-position) (line-end-position))) (yank)) ;;;###autoload (defun prot-simple-multi-line-below () "Move half a screen below." (interactive) (forward-line (floor (window-height) 2)) (setq this-command 'scroll-up-command)) ;;;###autoload (defun prot-simple-multi-line-above () "Move half a screen above." (interactive) (forward-line (- (floor (window-height) 2))) (setq this-command 'scroll-down-command)) ;;;###autoload (defun prot-simple-kill-line-backward () "Kill from point to the beginning of the line." (interactive) (kill-line 0)) ;;;###autoload (define-minor-mode prot-simple-auto-fill-visual-line-mode "Enable `visual-line-mode' and disable `auto-fill-mode' in the current buffer." :global nil (if prot-simple-auto-fill-visual-line-mode (progn (auto-fill-mode -1) (visual-line-mode 1)) (auto-fill-mode 1) (visual-line-mode -1))) ;;;; Commands for text insertion or manipulation ;;;###autoload (defun prot-simple-insert-date (&optional arg) "Insert the current date as `prot-simple-date-specifier'. With optional prefix ARG (\\[universal-argument]) also append the current time understood as `prot-simple-time-specifier'. When region is active, delete the highlighted text and replace it with the specified date." (interactive "P") (let* ((date prot-simple-date-specifier) (time prot-simple-time-specifier) (format (if arg (format "%s %s" date time) date))) (when (use-region-p) (delete-region (region-beginning) (region-end))) (insert (format-time-string format)))) (defun prot-simple--pos-url-on-line (char) "Return position of `prot-common-url-regexp' at CHAR." (when (integer-or-marker-p char) (save-excursion (goto-char char) (re-search-forward prot-common-url-regexp (line-end-position) :noerror)))) ;;;###autoload (defun prot-simple-escape-url-line (char) "Escape all URLs or email addresses on the current line. When called from Lisp CHAR is a buffer position to operate from until the end of the line. In interactive use, CHAR corresponds to `line-beginning-position'." (interactive (list (if current-prefix-arg (re-search-forward prot-common-url-regexp (line-end-position) :no-error (prefix-numeric-value current-prefix-arg)) (line-beginning-position)))) (when-let* ((regexp-end (prot-simple--pos-url-on-line char))) (goto-char regexp-end) (unless (looking-at ">") (insert ">") (when (search-backward "\s" (line-beginning-position) :noerror) (forward-char 1)) (insert "<")) (prot-simple-escape-url-line (1+ regexp-end))) (goto-char (line-end-position))) ;; Thanks to Bruno Boal for the original `prot-simple-escape-url-region'. ;; Check Bruno's Emacs config: <https://github.com/BBoal/emacs-config>. ;;;###autoload (defun prot-simple-escape-url-region (&optional beg end) "Apply `prot-simple-escape-url-line' on region lines between BEG and END." (interactive (if (region-active-p) (list (region-beginning) (region-end)) (error "There is no region!"))) (let ((beg (min beg end)) (end (max beg end))) (save-excursion (goto-char beg) (setq beg (line-beginning-position)) (while (<= beg end) (prot-simple-escape-url-line beg) (beginning-of-line 2) (setq beg (point)))))) ;;;###autoload (defun prot-simple-escape-url-dwim () "Escape URL on the current line or lines implied by the active region. Call the commands `prot-simple-escape-url-line' and `prot-simple-escape-url-region' ." (interactive) (if (region-active-p) (prot-simple-escape-url-region (region-beginning) (region-end)) (prot-simple-escape-url-line (line-beginning-position)))) ;;;###autoload (defun prot-simple-zap-to-char-backward (char &optional arg) "Backward `zap-to-char' for CHAR. Optional ARG is a numeric prefix to match ARGth occurance of CHAR." (interactive (list (read-char-from-minibuffer "Zap to char: " nil 'read-char-history) (prefix-numeric-value current-prefix-arg))) (zap-to-char (- arg) char t)) (defvar prot-simple-flush-and-diff-history nil "Minibuffer history for `prot-simple-flush-and-diff'.") ;;;###autoload (defun prot-simple-flush-and-diff (regexp beg end) "Call `flush-lines' for REGEXP and produce diff if file is modified. When region is active, operate between the region boundaries demarcated by BEG and END." (interactive (let ((regionp (region-active-p))) (list (read-regexp "Flush lines using REGEXP: " nil 'prot-simple-flush-and-diff-history) (and regionp (region-beginning)) (and regionp (region-end))))) (flush-lines regexp (or beg (point-min)) (or end (point-max)) :no-message) (when (and (buffer-modified-p) buffer-file-name) (diff-buffer-with-file (current-buffer)))) ;; FIXME 2023-09-28: The line prefix is problematic. I plan to rewrite it. ;; (defcustom prot-simple-line-prefix-strings '(">" "+" "-") ;; "List of strings used as line prefixes. ;; The command which serves as the point of entry is ;; `prot-simple-insert-line-prefix'." ;; :type '(repeat string) ;; :group 'prot-simple) ;; ;; (defun prot-simple--line-prefix-regexp (&optional string) ;; "Format regular expression for `prot-simple--line-prefix-p'. ;; With optional STRING use it directly. Else format the regexp by ;; concatenating `prot-simple-line-prefix-strings'." ;; (if string ;; (format "^%s " string) ;; (format "^[%s] " (apply #'concat prot-simple-line-prefix-strings)))) ;; ;; (defun prot-simple--line-prefix-p (&optional string) ;; "Return non-nil if line beginning has an appropriate string prefix. ;; With optional STRING test that it is at the beginning of the line." ;; (save-excursion ;; (goto-char (line-beginning-position)) ;; (looking-at (prot-simple--line-prefix-regexp string)))) ;; ;; (defun prot-simple--line-prefix-insert (string) ;; "Insert STRING at the beginning of the line, followed by a space." ;; (save-excursion ;; (goto-char (line-beginning-position)) ;; (insert string) ;; (insert " "))) ;; ;; (defun prot-simple--line-prefix-infer-string () ;; "Return line prefix string if it matches `prot-simple--line-prefix-p'." ;; (when (prot-simple--line-prefix-p) ;; (string-trim ;; (buffer-substring-no-properties (match-beginning 0) (match-end 0))))) ;; ;; (defun prot-simple--line-prefix-toggle (string) ;; "Insert or remove STRING at the beginning of the line." ;; (if (prot-simple--line-prefix-p string) ;; (delete-region (match-beginning 0) (match-end 0)) ;; (prot-simple--line-prefix-insert string))) ;; ;; (defvar prot-simple--line-prefix-history nil ;; "Minibuffer history of `prot-simple--line-prefix-prompt'.") ;; ;; (defun prot-simple--line-prefix-prompt () ;; "Prompt for string to use as line prefix. ;; Provide `prot-simple-line-prefix-strings' as completion ;; candidates, though accept arbitrary input." ;; (let ((default (car prot-simple--line-prefix-history))) ;; (completing-read ;; (format-prompt "Select line prefix" default) ;; prot-simple-line-prefix-strings ;; nil nil nil ;; 'prot-simple--line-prefix-history default))) ;; ;; (defun prot-simple-line-prefix-infer-or-prompt () ;; "Infer string for line prefix or prompt for one." ;; (or (prot-simple--line-prefix-infer-string) ;; (prot-simple--line-prefix-prompt))) ;; ;; ;;;###autoload ;; (defun prot-simple-insert-line-prefix-dwim (string) ;; "Toggle presence of STRING at the beginning of the line. ;; ;; When called interactively try to infer STRING based on the line ;; prefix. If one is found among `prot-simple-line-prefix-strings', ;; perform a removal outright. ;; ;; If no string can be inferred, prompt for STRING among ;; `prot-simple-line-prefix-strings'. Accept arbitrary strings at ;; the prompt. ;; ;; When the region is active, toggle the presence of STRING for each ;; line in the region." ;; (interactive (list (prot-simple-line-prefix-infer-or-prompt))) ;; (if-let* ((region-p (region-active-p)) ;; (beg (region-beginning)) ;; (end (line-number-at-pos (region-end)))) ;; (progn ;; (goto-char beg) ;; (push-mark (point)) ;; (while (<= (line-number-at-pos (point)) end) ;; (prot-simple--line-prefix-toggle string) ;; (forward-line 1))) ;; (prot-simple--line-prefix-toggle string))) ;;;; Commands for object transposition ;; The "move" functions all the way to `prot-simple-move-below-dwim' ;; are courtesy of Bruno Boal: <https://git.sr.ht/~bboal>. With minor ;; tweaks by me. (defun prot-simple--move-line (count dir) "Move line or region COUNTth times in DIR direction." (let* ((start (pos-bol)) (end (pos-eol)) diff-eol-point diff-eol-mark) (when-let* (((use-region-p)) (pos (point)) (mrk (mark)) (line-diff-mark-point (1+ (- (line-number-at-pos mrk) (line-number-at-pos pos))))) (if (> pos mrk) (setq start (pos-bol line-diff-mark-point)) ; pos-bol of where the mark is (setq end (pos-eol line-diff-mark-point))) ; pos-eol of the line where the mark is (setq diff-eol-mark (1+ (- end mrk)))) ; 1+ to get the \n ;; this is valid for region or a single line (setq diff-eol-point (1+ (- end (point)))) (let* ((max (point-max)) (end (1+ end)) (end (if (> end max) max end)) (deactivate-mark) (lines (delete-and-extract-region start end))) (forward-line (* count dir)) ;; Handle the special case when there isn't a newline as the eob. (when (and (eq (point) max) (/= (current-column) 0)) (insert "\n")) (insert lines) ;; if user provided a region (when diff-eol-mark (set-mark (- (point) diff-eol-mark))) ;; either way go to same point location reference initial motion (goto-char (- (point) diff-eol-point))))) (defun prot-simple--move-line-user-error (boundary) "Return `user-error' with message accounting for BOUNDARY. BOUNDARY is a buffer position, expected to be `point-min' or `point-max'." (when-let* ((bound (line-number-at-pos boundary)) (scope (cond ((and (use-region-p) (or (= (line-number-at-pos (point)) bound) (= (line-number-at-pos (mark)) bound))) "region is ") ((= (line-number-at-pos (point)) bound) "") (t nil)))) (user-error (format "Warning: %salready in the last line!" scope)))) (defun prot-simple-move-above-dwim (arg) "Move line or region ARGth times up. If ARG is nil, do it one time." (interactive "p") (unless (prot-simple--move-line-user-error (point-min)) (prot-simple--move-line arg -1))) (defun prot-simple-move-below-dwim (arg) "Move line or region ARGth times down. If ARG is nil, do it one time." (interactive "p") (unless (prot-simple--move-line-user-error (point-max)) (prot-simple--move-line arg 1))) (defmacro prot-simple-define-transpose (scope) "Define transposition command for SCOPE. SCOPE is the text object to operate on. The command's name is prot-simple-transpose-SCOPE." `(defun ,(intern (format "prot-simple-transpose-%s" scope)) (arg) ,(format "Transpose %s. Transposition over an active region will swap the object at the region beginning with the one at the region end." scope) (interactive "p") (let ((fn (intern (format "%s-%s" "transpose" ,scope)))) (if (use-region-p) (funcall fn 0) (funcall fn arg))))) ;;;###autoload (autoload 'prot-simple-transpose-lines "prot-simple") ;;;###autoload (autoload 'prot-simple-transpose-paragraphs "prot-simple") ;;;###autoload (autoload 'prot-simple-transpose-sentences "prot-simple") ;;;###autoload (autoload 'prot-simple-transpose-sexps "prot-simple") ;;;###autoload (autoload 'prot-simple-transpose-words "prot-simple") (prot-simple-define-transpose "lines") (prot-simple-define-transpose "paragraphs") (prot-simple-define-transpose "sentences") (prot-simple-define-transpose "sexps") (prot-simple-define-transpose "words") ;;;###autoload (defun prot-simple-transpose-chars () "Always transposes the two characters before point. There is no dragging the character forward. This is the behaviour of `transpose-chars' when point is at the end of the line." (interactive) (transpose-chars -1) (forward-char)) ;;;; Commands for paragraphs ;;;###autoload (defun prot-simple-unfill-region-or-paragraph (&optional beg end) "Unfill paragraph or, when active, the region. Join all lines in region delimited by BEG and END, if active, while respecting any empty lines (so multiple paragraphs are not joined, just unfilled). If no region is active, operate on the paragraph. The idea is to produce the opposite effect of both `fill-paragraph' and `fill-region'." (interactive "r") (let ((fill-column most-positive-fixnum)) (if (use-region-p) (fill-region beg end) (fill-paragraph)))) ;;;; Commands for windows and pages ;;;###autoload (defun prot-simple-other-window () "Wrapper for `other-window' and `next-multiframe-window'. If there is only one window and multiple frames, call `next-multiframe-window'. Otherwise, call `other-window'." (interactive) (if (and (one-window-p) (length> (frame-list) 1)) (progn (call-interactively #'next-multiframe-window) (setq this-command #'next-multiframe-window)) (call-interactively #'other-window) (setq this-command #'other-window))) ;;;###autoload (defun prot-simple-narrow-visible-window () "Narrow buffer to wisible window area. Also check `prot-simple-narrow-dwim'." (interactive) (let* ((bounds (prot-common-window-bounds)) (window-area (- (cdr bounds) (car bounds))) (buffer-area (- (point-max) (point-min)))) (if (/= buffer-area window-area) (narrow-to-region (car bounds) (cdr bounds)) (user-error "Buffer fits in the window; won't narrow")))) ;;;###autoload (defun prot-simple-narrow-dwim () "Do-what-I-mean narrowing. If region is active, narrow the buffer to the region's boundaries. If pages are defined by virtue of `prot-common-page-p', narrow to the current page boundaries. If no region is active and no pages exist, narrow to the visible portion of the window. If narrowing is in effect, widen the view." (interactive) (unless mark-ring ; needed when entering a new buffer (push-mark (point) t nil)) (cond ((and (use-region-p) (null (buffer-narrowed-p))) (narrow-to-region (region-beginning) (region-end))) ((prot-common-page-p) (narrow-to-page)) ((null (buffer-narrowed-p)) (prot-simple-narrow-visible-window)) ((widen)))) (defun prot-simple--narrow-to-page (count &optional back) "Narrow to COUNTth page with optional BACK motion." (if back (narrow-to-page (or (- count) -1)) (narrow-to-page (or (abs count) 1))) ;; Avoids the problem of skipping pages while cycling back and forth. (goto-char (point-min))) ;;;###autoload (defun prot-simple-forward-page-dwim (&optional count) "Move to next or COUNTth page forward. If buffer is narrowed to the page, keep the effect while performing the motion. Always move point to the beginning of the narrowed page." (interactive "p") (if (buffer-narrowed-p) (prot-simple--narrow-to-page count) (forward-page count) (setq this-command 'forward-page))) ;;;###autoload (defun prot-simple-backward-page-dwim (&optional count) "Move to previous or COUNTth page backward. If buffer is narrowed to the page, keep the effect while performing the motion. Always move point to the beginning of the narrowed page." (interactive "p") (if (buffer-narrowed-p) (prot-simple--narrow-to-page count t) (backward-page count) (setq this-command 'backward-page))) ;;;###autoload (defun prot-simple-delete-page-delimiters (&optional beg end) "Delete lines with just page delimiters in the current buffer. When region is active, only operate on the region between BEG and END, representing the point and mark." (interactive "r") (let (b e) (if (use-region-p) (setq b beg e end) (setq b (point-min) e (point-max))) (widen) (flush-lines (format "%s$" page-delimiter) b e) (setq this-command 'flush-lines))) ;; NOTE 2023-06-18: The idea of narrowing to a defun in an indirect ;; buffer is still experimental. (defun prot-simple-narrow--guess-defun-symbol () "Try to return symbol of current defun as a string." (save-excursion (beginning-of-defun) (search-forward " ") (thing-at-point 'symbol :no-properties))) ;;;###autoload (defun prot-simple-narrow-to-cloned-buffer () "Narrow to defun in cloned buffer. Name the buffer after the defun's symbol." (interactive) (clone-indirect-buffer-other-window (format "%s -- %s" (buffer-name) (prot-simple-narrow--guess-defun-symbol)) :display) (narrow-to-defun)) ;;;; Commands for buffers (defun prot-simple--display-unsaved-buffers (buffers buffer-menu-name) "Produce buffer menu listing BUFFERS called BUFFER-MENU-NAME." (let ((old-buf (current-buffer)) (buf (get-buffer-create buffer-menu-name))) (with-current-buffer buf (Buffer-menu-mode) (setq-local Buffer-menu-files-only nil Buffer-menu-buffer-list buffers Buffer-menu-filter-predicate nil) (list-buffers--refresh buffers old-buf) (tabulated-list-print)) (display-buffer buf))) (defun prot-simple--get-unsaved-buffers () "Get list of unsaved buffers." (seq-filter (lambda (buffer) (and (buffer-file-name buffer) (buffer-modified-p buffer))) (buffer-list))) ;;;###autoload (defun prot-simple-display-unsaved-buffers () "Produce buffer menu listing unsaved file-visiting buffers." (interactive) (if-let* ((unsaved-buffers (prot-simple--get-unsaved-buffers))) (prot-simple--display-unsaved-buffers unsaved-buffers "*Unsaved buffers*") (message "No unsaved buffers"))) (defun prot-simple-display-unsaved-buffers-on-exit (&rest _) "Produce buffer menu listing unsaved file-visiting buffers. Add this as :before advice to `save-buffers-kill-emacs'." (when-let* ((unsaved-buffers (prot-simple--get-unsaved-buffers))) (prot-simple--display-unsaved-buffers unsaved-buffers "*Unsaved buffers*"))) ;;;###autoload (defun prot-simple-copy-current-buffer-name () "Add the current buffer's name to the `kill-ring'." (declare (interactive-only t)) (interactive) (kill-new (buffer-name (current-buffer)))) ;;;###autoload (defun prot-simple-copy-current-buffer-file () "Add the current buffer's file path to the `kill-ring'." (declare (interactive-only t)) (interactive) (if buffer-file-name (kill-new buffer-file-name) (user-error "%s is not associated with a file" (buffer-name (current-buffer))))) ;;;###autoload (defun prot-simple-kill-buffer (buffer) "Kill current BUFFER without confirmation. When called interactively, prompt for BUFFER." (interactive (list (read-buffer "Select buffer: "))) (let ((kill-buffer-query-functions nil)) (kill-buffer (or buffer (current-buffer))))) ;;;###autoload (defun prot-simple-kill-buffer-current (&optional arg) "Kill current buffer. With optional prefix ARG (\\[universal-argument]) delete the buffer's window as well. Kill the window regardless of ARG if it satisfies `prot-common-window-small-p' and it has no previous buffers in its history." (interactive "P") (let ((kill-buffer-query-functions nil)) (if (or (and (prot-common-window-small-p) (null (window-prev-buffers))) (and arg (not (one-window-p)))) (kill-buffer-and-window) (kill-buffer)))) ;;;###autoload (defun prot-simple-rename-file-and-buffer (name) "Apply NAME to current file and rename its buffer. Do not try to make a new directory or anything fancy." (interactive (list (read-string "Rename current file: " (buffer-file-name)))) (let ((file (buffer-file-name))) (if (vc-registered file) (vc-rename-file file name) (rename-file file name)) (set-visited-file-name name t t))) (defun prot-simple--buffer-major-mode-prompt () "Prompt of `prot-simple-buffers-major-mode'. Limit list of buffers to those matching the current `major-mode' or its derivatives." (let ((read-buffer-function nil) (current-major-mode major-mode)) (read-buffer (format "Buffer for %s: " major-mode) nil :require-match (lambda (pair) ; pair is (name-string . buffer-object) (with-current-buffer (cdr pair) (derived-mode-p current-major-mode)))))) ;;;###autoload (defun prot-simple-buffers-major-mode () "Select BUFFER matching the current one's major mode." (interactive) (switch-to-buffer (prot-simple--buffer-major-mode-prompt))) (defun prot-simple--buffer-vc-root-prompt () "Prompt of `prot-simple-buffers-vc-root'." (let ((root (or (vc-root-dir) (locate-dominating-file "." ".git"))) (read-buffer-function nil)) (read-buffer (format "Buffers in %s: " root) nil t (lambda (pair) ; pair is (name-string . buffer-object) (with-current-buffer (cdr pair) (string-match-p root default-directory)))))) ;;;###autoload (defun prot-simple-buffers-vc-root () "Select buffer matching the current one's VC root." (interactive) (switch-to-buffer (prot-simple--buffer-vc-root-prompt))) ;;;###autoload (defun prot-simple-swap-window-buffers (counter) "Swap states of live buffers. With two windows, transpose their buffers. With more windows, perform a clockwise rotation. Do not alter the window layout. Just move the buffers around. With COUNTER as a prefix argument, do the rotation counter-clockwise." (interactive "P") (when-let* ((winlist (if counter (reverse (window-list)) (window-list))) (wincount (count-windows)) ((> wincount 1))) (dotimes (i (- wincount 1)) (window-swap-states (elt winlist i) (elt winlist (+ i 1)))))) ;;;; Commands for files (cl-defmethod register--type ((_regval vector)) 'vector) (cl-defmethod register-val-describe ((val vector) _verbose) (if-let* ((pos (aref val 2)) (file (aref val 1))) (princ (format "%s at position %s" file pos)) (princ "Garbage data"))) ;;;###autoload (defun prot-simple-file-to-register (register) "Store current location of file's point in REGISTER." (interactive (list (register-read-with-preview "File with point to register: "))) (set-register register (vector 'file-with-point (buffer-file-name) (point)))) (defvar prot-simple-file-to-register-jump-hook nil "Normal hook called after jumping to a file register. See `prot-simple-file-to-register'.") ;;;###autoload (cl-defmethod register-val-jump-to ((val vector) delete) "Handle how to jump to a location register. This is like the default, but does not ask to visit a file: it does it outright." (cond ((eq (aref val 0) 'file-with-point) (find-file (aref val 1)) (goto-char (aref val 2)) (run-hooks 'prot-simple-file-to-register-jump-hook)) (t (cl-call-next-method val delete)))) ;;;; Commands of a general nature (autoload 'color-rgb-to-hex "color") (autoload 'color-name-to-rgb "color") (defun prot-simple-accessible-colors (variant) "Return list of accessible `defined-colors'. VARIANT is either `dark' or `light'." (let ((variant-color (if (eq variant 'black) "#000000" "#ffffff"))) (seq-filter (lambda (c) (let* ((rgb (color-name-to-rgb c)) (r (nth 0 rgb)) (g (nth 1 rgb)) (b (nth 2 rgb)) (hex (color-rgb-to-hex r g b 2))) (when (>= (prot-common-contrast variant-color hex) 4.5) c))) (defined-colors)))) (defun prot-simple--list-accessible-colors-prompt () "Use `read-multiple-choice' to return white or black background." (intern (cadr (read-multiple-choice "Variant" '((?b "black" "Black background") (?w "white" "White background")) "Choose between white or black background.")))) ;;;###autoload (defun prot-simple-list-accessible-colors (variant) "Return buffer with list of accessible `defined-colors'. VARIANT is either `dark' or `light'." (interactive (list (prot-simple--list-accessible-colors-prompt))) (list-colors-display (prot-simple-accessible-colors variant))) (provide 'prot-simple) ;;; prot-simple.el ends here
6.20. The prot-spell.el
library
;;; prot-spell.el --- Spelling-related extensions for my dotemacs -*- lexical-binding: t -*- ;; Copyright (C) 2021-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; This covers my spelling-related extensions, for use in my Emacs ;; setup: https://protesilaos.com/emacs/dotemacs. ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (require 'ispell) (defgroup prot-spell () "Extensions for ispell and flyspell." :group 'ispell) (defcustom prot-spell-dictionaries '(("EN English" . "en") ("EL Ελληνικά" . "el") ("FR Français" . "fr") ("ES Espanõl" . "es")) "Alist of strings with descriptions and dictionary keys. Used by `prot-spell-change-dictionary'." :type 'alist :group 'prot-spell) (defvar prot-spell--dictionary-hist '() "Input history for `prot-spell-change-dictionary'.") (defun prot-spell--dictionary-prompt () "Helper prompt to select from `prot-spell-dictionaries'." (let ((def (car prot-spell--dictionary-hist))) (completing-read (format "Select dictionary [%s]: " def) (mapcar #'car prot-spell-dictionaries) nil t nil 'prot-spell--dictionary-hist def))) ;;;###autoload (defun prot-spell-change-dictionary (dictionary) "Select a DICTIONARY from `prot-spell-dictionaries'." (interactive (list (prot-spell--dictionary-prompt))) (let* ((key (cdr (assoc dictionary prot-spell-dictionaries))) (desc (car (assoc dictionary prot-spell-dictionaries)))) (ispell-change-dictionary key) (message "Switched dictionary to %s" (propertize desc 'face 'bold)))) ;;;###autoload (defun prot-spell-spell-dwim (beg end) "Spell check between BEG END, current word, or select a dictionary. Use `flyspell-region' on the active region and deactivate the mark. With point over a word and no active region invoke `ispell-word'. Else call `prot-spell-change-dictionary'." (interactive "r") (cond ((use-region-p) (flyspell-region beg end) (deactivate-mark)) ((thing-at-point 'word) (call-interactively 'ispell-word)) (t (call-interactively 'prot-spell-change-dictionary)))) (defun prot-spell-ispell-display-buffer (buffer) "Function to override `ispell-display-buffer' for BUFFER. Use this as `advice-add' to override the aforementioned Ispell function. Then you can control the buffer's specifics via `display-buffer-alist' (how it ought to be!)." (pop-to-buffer buffer) (set-window-point (get-buffer-window buffer) (point-min))) (advice-add #'ispell-display-buffer :override #'prot-spell-ispell-display-buffer) (provide 'prot-spell) ;;; prot-spell.el ends here
6.21. The prot-vertico.el
library
;;; prot-vertico.el --- Custom Vertico extras -*- lexical-binding: t -*- ;; Copyright (C) 2023-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or (at ;; your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (require 'vertico) (defvar prot-vertico-multiform-minimal '(unobtrusive (vertico-flat-format . ( :multiple "" :single "" :prompt "" :separator "" :ellipsis "" :no-match ""))) "List of configurations for minimal Vertico multiform. The minimal view is intended to be more private or less revealing. This is important when, for example, a prompt shows names of people. Of course, such a view also provides a minimal style for general usage. Toggle the vertical view with the `vertico-multiform-vertical' command or use the commands `prot-vertico-private-next' and `prot-vertico-private-previous', which toggle the vertical view automatically.") (defvar prot-vertico-multiform-maximal '((vertico-count . 10) (vertico-resize . t)) "List of configurations for maximal Vertico multiform.") (defun prot-vertico--match-directory (str) "Match directory delimiter in STR." (string-suffix-p "/" str)) ;; From the Vertico documentation. (defun prot-vertico-sort-directories-first (files) "Sort directories before FILES." (setq files (vertico-sort-alpha files)) (nconc (seq-filter #'prot-vertico--match-directory files) (seq-remove #'prot-vertico--match-directory files))) (defun prot-vertico-private-next () "Like `vertico-next' but toggle vertical view if needed. This is done to accommodate `prot-vertico-multiform-minimal'." (interactive) (if vertico-unobtrusive-mode (let ((vertico--index 0)) (vertico-multiform-vertical) (vertico-next 1)) (vertico-next 1))) (defun prot-vertico-private-previous () "Like `vertico-previous' but toggle vertical view if needed. This is done to accommodate `prot-vertico-multiform-minimal'." (interactive) (if vertico-unobtrusive-mode (progn (vertico-multiform-vertical) (vertico-previous 1)) (vertico-previous 1))) (defun prot-vertico-private-complete () "Expand contents and show remaining candidates, if needed. This is done to accommodate `prot-vertico-multiform-minimal'." (interactive) (if (and vertico-unobtrusive-mode (> vertico--total 1)) (progn (minibuffer-complete) (vertico-multiform-vertical)) (vertico-insert))) (provide 'prot-vertico) ;;; prot-vertico.el ends here
6.22. The prot-window.el
library
;;; prot-window.el --- Display-buffer and window-related extensions for my dotemacs -*- lexical-binding: t -*- ;; Copyright (C) 2023-2024 Protesilaos Stavrou ;; Author: Protesilaos Stavrou <info@protesilaos.com> ;; URL: https://protesilaos.com/emacs/dotemacs ;; Version: 0.1.0 ;; Package-Requires: ((emacs "30.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; This covers my window and display-buffer extensions, for use in my ;; Emacs setup: https://protesilaos.com/emacs/dotemacs. ;; ;; Remember that every piece of Elisp that I write is for my own ;; educational and recreational purposes. I am not a programmer and I ;; do not recommend that you copy any of this if you are not certain of ;; what it does. ;;; Code: (require 'prot-common) (defvar prot-window-window-sizes '( :max-height (lambda () (floor (frame-height) 3)) :min-height 10 :max-width (lambda () (floor (frame-width) 4)) :min-width 20) "Property list of maximum and minimum window sizes. The property keys are `:max-height', `:min-height', `:max-width', and `:min-width'. They all accept a value of either a number (integer or floating point) or a function.") (defun prot-window--get-window-size (key) "Extract the value of KEY from `prot-window-window-sizes'." (when-let* ((value (plist-get prot-window-window-sizes key))) (cond ((functionp value) (funcall value)) ((numberp value) value) (t (error "The value of `%s' is neither a number nor a function" key))))) (defun prot-window-select-fit-size (window) "Select WINDOW and resize it. The resize pertains to the maximum and minimum values for height and width, per `prot-window-window-sizes'. Use this as the `body-function' in a `display-buffer-alist' entry." (select-window window) (fit-window-to-buffer window (prot-window--get-window-size :max-height) (prot-window--get-window-size :min-height) (prot-window--get-window-size :max-width) (prot-window--get-window-size :min-width)) ;; If we did not use `display-buffer-below-selected', then we must ;; be in a lateral window, which has more space. Then we do not ;; want to dedicate the window to this buffer, because we will be ;; running out of space. (when (or (window-in-direction 'above) (window-in-direction 'below)) (set-window-dedicated-p window t))) (defun prot-window--get-display-buffer-below-or-pop () "Return list of functions for `prot-window-display-buffer-below-or-pop'." (list #'display-buffer-reuse-mode-window (if (or (prot-common-window-small-p) (prot-common-three-or-more-windows-p)) #'display-buffer-below-selected #'display-buffer-pop-up-window))) (defun prot-window-display-buffer-below-or-pop (&rest args) "Display buffer below current window or pop a new window. The criterion for choosing to display the buffer below the current one is a non-nil return value for `prot-common-window-small-p'. Apply ARGS expected by the underlying `display-buffer' functions. This as the action function in a `display-buffer-alist' entry." (let ((functions (prot-window--get-display-buffer-below-or-pop))) (catch 'success (dolist (fn functions) (when (apply fn args) (throw 'success fn)))))) (defun prot-window-shell-or-term-p (buffer &rest _) "Check if BUFFER is a shell or terminal. This is a predicate function for `buffer-match-p', intended for use in `display-buffer-alist'." (when (string-match-p "\\*.*\\(e?shell\\|v?term\\).*" (buffer-name (get-buffer buffer))) (with-current-buffer buffer ;; REVIEW 2022-07-14: Is this robust? (and (not (derived-mode-p 'message-mode 'text-mode)) (derived-mode-p 'eshell-mode 'shell-mode 'comint-mode 'fundamental-mode))))) (defun prot-window-remove-dedicated (&rest _) "Remove dedicated window parameter. Use this as :after advice to `delete-other-windows' and `delete-window'." (when (one-window-p :no-mini) (set-window-dedicated-p nil nil))) (mapc (lambda (fn) (advice-add fn :after #'prot-window-remove-dedicated)) '(delete-other-windows delete-window)) (defmacro prot-window-define-full-frame (name &rest args) "Define command to call ARGS in new frame with `display-buffer-full-frame' bound. Name the function prot-window- followed by NAME. If ARGS is nil, call NAME as a function." (declare (indent 1)) `(defun ,(intern (format "prot-window-%s" name)) () ,(format "Call `prot-window-%s' in accordance with `prot-window-define-full-frame'." name) (interactive) (let ((display-buffer-alist '((".*" (display-buffer-full-frame))))) (with-selected-frame (make-frame) ,(if args `(progn ,@args) `(funcall ',name)) (modify-frame-parameters nil '((buffer-list . nil))))))) (defun prot-window--get-shell-buffers () "Return list of `shell' buffers." (seq-filter (lambda (buffer) (with-current-buffer buffer (derived-mode-p 'shell-mode))) (buffer-list))) (defun prot-window--get-new-shell-buffer () "Return buffer name for `shell' buffers." (if-let* ((buffers (prot-window--get-shell-buffers)) (buffers-length (length buffers)) ((>= buffers-length 1))) (format "*shell*<%s>" (1+ buffers-length)) "*shell*")) ;;;###autoload (autoload 'prot-window-shell "prot-window") (prot-window-define-full-frame shell (let ((name (prot-window--get-new-shell-buffer))) (shell name) (set-frame-name name) (when-let* ((buffer (get-buffer name))) (with-current-buffer buffer (add-hook 'delete-frame-functions (lambda (_) ;; FIXME 2023-09-09: Works for multiple frames (per ;; `make-frame-command'), but not if the buffer is in two ;; windows in the same frame. (unless (> (safe-length (get-buffer-window-list buffer nil t)) 1) (let ((kill-buffer-query-functions nil)) (kill-buffer buffer)))) nil :local))))) ;;;###autoload (autoload 'prot-window-coach "prot-window") (prot-window-define-full-frame coach (let ((buffer (get-buffer-create "*scratch for coach*"))) (with-current-buffer buffer (funcall initial-major-mode)) (display-buffer buffer) (set-frame-name "Coach"))) ;; REVIEW 2023-06-25: Does this merit a user option? I don't think I ;; will ever set it to the left. It feels awkward there. (defun prot-window-scroll-bar-placement () "Control the placement of scroll bars." (when scroll-bar-mode (setq default-frame-scroll-bars 'right) (set-scroll-bar-mode 'right))) (add-hook 'scroll-bar-mode-hook #'prot-window-scroll-bar-placement) (defun prot-window-no-minibuffer-scroll-bar (frame) "Remove the minibuffer scroll bars from FRAME." (set-window-scroll-bars (minibuffer-window frame) nil nil nil nil :persistent)) (add-hook 'after-make-frame-functions 'prot-window-no-minibuffer-scroll-bar) ;;;; Run commands in a popup frame (via emacsclient) (defun prot-window-delete-popup-frame (&rest _) "Kill selected selected frame if it has parameter `prot-window-popup-frame'. Use this function via a hook." (when (frame-parameter nil 'prot-window-popup-frame) (delete-frame))) (defmacro prot-window-define-with-popup-frame (command) "Define function which calls COMMAND in a new frame. Make the new frame have the `prot-window-popup-frame' parameter." `(defun ,(intern (format "prot-window-popup-%s" command)) () ,(format "Run `%s' in a popup frame with `prot-window-popup-frame' parameter. Also see `prot-window-delete-popup-frame'." command) (interactive) (let ((frame (make-frame '((prot-window-popup-frame . t))))) (select-frame frame) (switch-to-buffer " prot-window-hidden-buffer-for-popup-frame") (condition-case nil (call-interactively ',command) ((quit error user-error) (delete-frame frame)))))) (declare-function org-capture "org-capture" (&optional goto keys)) (defvar org-capture-after-finalize-hook) ;;;###autoload (autoload 'prot-window-popup-org-capture "prot-window") (prot-window-define-with-popup-frame org-capture) (declare-function tmr "tmr" (time &optional description acknowledgep)) (defvar tmr-timer-created-functions) ;;;###autoload (autoload 'prot-window-popup-tmr "prot-window") (prot-window-define-with-popup-frame tmr) (provide 'prot-window) ;;; prot-window.el ends here
7. Frequently Asked Questions (FAQ)
7.1. Why many modules instead of one init.el?
[ This question is in relation to the the anatomy of my Emacs configuration. ]
I prefer to keep things separate by splitting them into modules and custom libraries. Smaller files are easier to work with, including from the command line or with a generic text editor. They are also easier to share with others, which I do frequently.
A large file is fully dependent on the capabilities of Emacs. This is fine if you know your way around. But it does not work for people of different skill levels. Remember that I am sharing my configuration and keeping everything well documented to help others learn. The target audience is not Emacs veterans. I cannot expect an inexperienced user to already be familiar with how to navigate the outline, narrow to a defun, jump back to a mark, and so on, just to navigate one massive file.
Furthermore, I am not convinced by the argument that loading one large
init.el
is more efficient than loading many smaller modules. The
reason is that the init file will anyway have multiple require
calls
or use autoloaded functions which themselves load all those other
files. If, say, you are anyway loading a ~100 files through your
init.el
, do a few extra files really affect performance at startup?
And if yes, is this enough to forgo the aforementioned usability and
accessibility considerations?
Thoe granted, my current setup does not make me dependent on Org
because I can at any moment stop editing this prot-emacs.org
file
and continue my work in the many small files I already have.
7.2. Why use Org when you can have an outline in Elisp?
[ Also read: Why many modules instead of one init.el? ]
This question is about the use of the built-in outline-minor-mode
or
the outshine
package (The prot-emacs-langs.el
settings for outline-minor-mode
).
They provide folding capabilities like those of Org mode and can, in
principle, be enabled in any buffer (provided it has a recognisable
outline).
I am, in fact, using outlines in all my Elisp files. Not just here,
but also in all my public-facing Emacs packages. You can spot the
entries to the outline as comments that have three or more delimiters.
I can thus visit the .el
file I am interested in and enable
outline-minor-mode
to get the folding capabilities and extras.
Furthermore, I can use the consult-outline
or prot-search-outline
command to navigate to a heading with minibuffer completion.
What the prot-emacs.org
provides is an optional single point of
entry to my Emacs configuration. I use this to produce all the
individual files (Anatomy of my Emacs configuration). It allows me to
document my comprehensive corpus of work for the benefit of the Emacs
community at-large:
- I can establish links between sections and use richer typography,
something that
outline-minor-mode
does not provide. - The extensive commentary included herein is not added to the source code, thus making it easier for me or others to focus on the programming parts when we want to.
- This document is exported to my website as a standalone web page. People can find what they need there and share it with friends, regardless of their skill level.
- Anyone may read the source of this file to learn more about the
technicalities of how this is done. And they can still use only the
.el
files, if they do not want to deal with Org. Everybody is covered.
The prot-emacs.org
does not introduce a dependency on Org mode.
This file is not loaded at startup. I can remove prot-emacs.org
at
any moment and my setup will continue to work. This arrangement is so
effective that I might actually convert the entirety of my dotfiles to
it (long-term though, as it is a lot of work to document everything).
7.3. Why do you use multiple setq
instead of one?
This question is about the following pattern:
;; I usually have this: (setq var-1 val-1) (setq var-2 val-2) (setq var-3 val-3) ;; But why not this for everything? (setq var-1 val-1 var-2 val-2 var-3 val-3)
I actually use both approaches. To me, the latter is better when I want to say “these go together”, while the former makes it easier for me to copy-paste what I need when communicating with someone.
Furthermore, the single setq
call is harder to read when (i) there
are lots of variables involved and/or (ii) the values are longer lists
which themselves take some effort to figure out.
7.4. Why don’t you remap keys?
This is about the following pattern:
(define-key some-map [remap old-command] #'new-command)
This is a good way to replace in situ a command you don’t use for the
one you want. The problem is that it is not didactic for new users.
The person reading my code will not know which key binding I am
referencing. Sure, they can type C-h w
(where-is
command) to
search for the command in the current major mode, but this too
requires some experience. Whereas an explicit key binding is self
explanatory.
7.5. Why not use Org block arguments in the properties drawer?
The code blocks I define in this document have directives to tangle
their contents to files. I do this for each code block, though it is
possible to set the same settings inside the parent heading’s
PROPERTIES
drawer and achieve the same result.
The problem I have with that approach is that it is less discoverable in a massive file like this one. People are not expected to read this document from start to end. Instead, they will jump directly to the section that is of interest to them (perhaps by performing a search). They will thus skip past whatever parent heading declares the arguments. Whereas the code block encapsulates its own data and thus is self-documenting.
7.6. What hardware and software do you use?
I have a Lenovo ThinkPad T470. It is a second-hand laptop that I got from Ebay courtesy of a generous donation by Anush V. I run Debian on this laptop. I do not mind having older versions of system packages. The only program I need to run an up-to-date version of is Emacs—and I compile that from source.
During the summer of 2023, I used the laptop to experiment with the GNU Guix system but I eventually abandoned that project. Guix solves problems that I do not have, while it does things very differently to how other Unix-like systems are doing them. I cannot justify spending a considerable amount of time for something I do not really need, especially with the busy schedule I have.
I also use a desktop computer that I built in 2021 with donations I got from the Emacs community. At the time, I had a Lenovo ThinkPad X220 laptop as my sole computer: it broke and I posted an announcement to the effect that I would not be available for a while. Then donations started coming in and I was able to be back online after a month or so. It was a special moment.
The desktop computer is more powerful: it cost me a bit less than 600 EUR to build it. Back then I did not know enough about hardware and was able to assemble it with the help of a local who knows more about computer hardware than I do (I have learnt since then and I can do it myself now). I installed Arch when I first got this computer and it has been stable ever since (Arch is reliable if you know what you are doing).
In early 2024 I migrated my desktop to Debian. I did it because the Internet connection at my hut is metered (I moved to the hut at the end of summer 2023) and I did not want to spend a lot of data on system maintenance. Arch never caused me any issues. It is a top tier distro, as is Debian. Those are the two I trust the most.
On the laptop, I use the GNOME desktop environment. It is good for keyboard usage, plus it has excellent support for the trackpad. On the desktop, I normally use a tiling window manager. Wayland still does not work 100% for me, so I prefer to stay on Xorg until further notice.
For many years, the keyboard I had on my desktop was a generic, toy-grade, Qwerty model (I literally bought it from a toy store, together with the mouse for ~10 EUR). In autumn 2023, Arialdo Martini bought me the Keychron K5 Pro, which is a programmable keyboard with a traditional, full-key form factor. While in early spring 2024 “Andreas” got me the Iris keyboards from Keebio, which is a more ergonomic split design with a columnar stagger for how the keys are arranged. I discuss this at greater length here: https://protesilaos.com/news/2024-04-11-my-rsi-keyboard-ergonomics/. I also write about mechanical keyboards and ergonomics on this section of my website: https://protesilaos.com/keeb/.
Finally, I have a mobile phone that Dmitry Matveyev bought for me. It is a Samsung A53 model. I run the factory operating system on it. I have removed all apps that can be removed and use it only for its camera and phone capabilities. I know I could run Emacs on android (Emacs 30 has an Android build), but that is the sort of rabbit hole I need to avoid.
7.7. What is your desktop setup?
You can check my dotfiles to find all about the tiling window managers and other programs I use:
- Git repositories:
I have been using tiling window managers for most of my time on Linux (I switched to Linux in the summer of 2016 and I think I started with i3wm in 2017).