At the moment, I am not committing my capture templates to git history, as I want a way to moove this code into the personal settings part of this configuration
7270 lines
281 KiB
Org Mode
7270 lines
281 KiB
Org Mode
#+title: GNU Emacs configuration
|
||
#+author: Vedang Manerikar
|
||
#+email: vedang@unravel.tech
|
||
#+language: en
|
||
#+options: ':t toc:nil num:t author:t email:t
|
||
|
||
This configuration is inspired from the work of [[https://github.com/protesilaos/dotfiles/blob/master/emacs/.emacs.d/prot-emacs.org][my hero Prot]]. I've copied straight from his config file, because I think the explanations he has created are worthwhile and should be read by everyone. Prot's files are all prefixed with =prot-emacs-*=. I have changed this to =unravel-*=. The reason for this is that I have picked up only what I need and changed it where needed. As such, any issues you face with this configuration are likely introduced by me.
|
||
|
||
I use emacs on Mac OSX as my primary development environment. I install the latest available pre-compiled Emacs (with native compilation), as provided by Jimeh here: [[https://github.com/jimeh/emacs-builds?tab=readme-ov-file][jimeh/emacs-builds]].
|
||
|
||
Any quote without attribution is directly taken from [[https://github.com/protesilaos/dotfiles/blob/master/emacs/.emacs.d/prot-emacs.org][Prot's org file]]. You should read through it if you want detailed explanations of things you find here.
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle no :results none
|
||
(org-babel-tangle)
|
||
#+end_src
|
||
|
||
#+toc: headlines 2
|
||
|
||
Here is what the generated directory structure should look like:
|
||
|
||
#+begin_src sh :dir ~/src/prototypes/emacs-up :results raw
|
||
fd -e el -e org -E elpa | tree --fromfile
|
||
#+end_src
|
||
|
||
#+RESULTS:
|
||
.
|
||
├── early-init.el
|
||
├── init.el
|
||
├── unravel-emacs.org
|
||
└── unravel-modules
|
||
├── unravel-completion.el
|
||
├── unravel-essentials.el
|
||
├── unravel-langs.el
|
||
└── unravel-theme.el
|
||
|
||
2 directories, 7 files
|
||
|
||
To make a change to this Emacs configuration, edit this file and then type =C-c C-v C-t= (=M-x org-babel-tangle=) to republish all the relevant files.
|
||
|
||
* The =early-init.el= file
|
||
|
||
#+begin_quote
|
||
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
|
||
#+end_quote
|
||
|
||
** The =early-init.el= basic frame settings
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:a1288a07-93f6-4e14-894e-707d5ad8b6dc
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "early-init.el"
|
||
;;;; No GUI
|
||
;; 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)
|
||
#+end_src
|
||
|
||
** The =early-init.el= tweaks to startup time and garbage collection
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:50d28f3c-3ada-4db5-b830-bbbbee7fec4e
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "early-init.el"
|
||
;; A big contributor to startup times is garbage collection.
|
||
|
||
;; We up the gc threshold to temporarily prevent it from running, then
|
||
;; reset it later after startup is complete. Not resetting it will
|
||
;; cause stuttering/freezes.
|
||
|
||
(setq gc-cons-threshold most-positive-fixnum)
|
||
(setq 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))
|
||
(setq gc-cons-percentage 0.1)
|
||
(setq file-name-handler-alist prot-emacs--file-name-handler-alist)
|
||
(setq vc-handled-backends prot-emacs--vc-handled-backends)))
|
||
#+end_src
|
||
|
||
** If you have both .el and .elc files, load the newer one
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:F8987E20-3E36-4E27-9EAE-D0680303A95B
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "early-init.el"
|
||
;; When both .el and .elc / .eln files are available,
|
||
;; load the latest one.
|
||
|
||
(setq load-prefer-newer t)
|
||
#+end_src
|
||
|
||
** The =early-init.el= initialises the package cache
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:7a037504-8a2f-4df0-8482-ce6476354440
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "early-init.el"
|
||
;; Ensure that `describe-package' does not require a
|
||
;; `package-refresh-contents'.
|
||
(setq package-enable-at-startup t)
|
||
#+end_src
|
||
|
||
** The =early-init.el= gives a name to the default frame
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:ad227f7e-b0a7-43f8-91d6-b50db82da9ad
|
||
:END:
|
||
|
||
Naming frames allows you to select them using completion (=M-x select-frame-by-name=).
|
||
|
||
#+begin_src emacs-lisp :tangle "early-init.el"
|
||
;; Name the default frame
|
||
;; You can select a frame with M-x select-frame-by-name
|
||
(add-hook 'after-init-hook (lambda () (set-frame-name "unravel/emacs")))
|
||
#+end_src
|
||
|
||
* The =init.el= file
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:dae63bd9-93a8-41c4-af1b-d0f39ba50974
|
||
:END:
|
||
|
||
This is the main initialisation file of Emacs. Everything loads from here, even if it has been split into multiple files for convenience.
|
||
|
||
** The =init.el= tweaks to make native compilation silent
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:3563ceb5-b70c-4191-9c81-f2f5a202c4da
|
||
:END:
|
||
|
||
These warnings are unnecessarily scary.
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "init.el"
|
||
;; 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
|
||
#+end_src
|
||
|
||
** The =init.el= setting to send ~custom-file~ to oblivion
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:f2ffe0e9-a58d-4bba-9831-cc35940ea83f
|
||
:END:
|
||
|
||
There is no need to use the =M-x customize= infrastructure. It's easier to just rely on the init file instead.
|
||
|
||
#+begin_quote
|
||
I would prefer to just have an option to avoid the Custom
|
||
infrastructure altogether, but this is not possible. So here we are...
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "init.el"
|
||
;; Disable custom.el by making it disposable.
|
||
(setq custom-file (make-temp-file "emacs-custom-"))
|
||
#+end_src
|
||
|
||
** The =init.el= settings to enable commands disabled by default
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:4ed6593f-6f55-4258-a1c2-ddb50e9e2465
|
||
:END:
|
||
|
||
These commands are actually useful, especially in org-mode.
|
||
|
||
#+begin_src emacs-lisp :tangle "init.el"
|
||
;; Enable these commands which have been disabled by default
|
||
(mapc
|
||
(lambda (command)
|
||
(put command 'disabled nil))
|
||
'(list-timers narrow-to-region narrow-to-page upcase-region downcase-region))
|
||
#+end_src
|
||
|
||
** The =init.el= settings to disable unnecessary commands enabled by default
|
||
|
||
These commands are "unsafe", in that we should be using the alternatives (like ~vterm~ and ~org~)
|
||
|
||
#+begin_src emacs-lisp :tangle "init.el"
|
||
;; Disable these commands which have been enabled by default
|
||
(mapc
|
||
(lambda (command)
|
||
(put command 'disabled t))
|
||
'(eshell project-eshell overwrite-mode iconify-frame diary))
|
||
#+end_src
|
||
|
||
** Add the modules folder to the load-path
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:e289a614-4f17-4d6c-a028-42fe45aebe66
|
||
:END:
|
||
|
||
This is where all the custom configuration sits for all the packages we use. We write configuration on a per-file basis instead of in a giant file, because these smaller files are more readable, approachable and shareable.
|
||
|
||
#+begin_src emacs-lisp :tangle "init.el"
|
||
(mapc
|
||
(lambda (string)
|
||
(add-to-list 'load-path (locate-user-emacs-file string)))
|
||
'("unravel-modules" "custom-lisp"))
|
||
#+end_src
|
||
|
||
** The =init.el= settings for packages (=package.el=)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:424340cc-f3d7-4083-93c9-d852d40dfd40
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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 ([[#h:7a037504-8a2f-4df0-8482-ce6476354440][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/>.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "init.el"
|
||
;;;; 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)))
|
||
|
||
;; Let `package-install' suggest upgrades for built-in packages too.
|
||
(setq package-install-upgrade-built-in t)
|
||
#+end_src
|
||
|
||
** The =init.el= macro to do nothing with Elisp code (~prot-emacs-comment~)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:3b14faa6-83fd-4d5f-b3bc-85f72fd572d4
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_example emacs-lisp
|
||
(defmacro prot-emacs-comment (&rest body)
|
||
"Do nothing with BODY and return nil, with no side effects."
|
||
(declare (indent defun))
|
||
nil)
|
||
#+end_example
|
||
|
||
#+begin_quote
|
||
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:
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "init.el"
|
||
(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))))
|
||
#+end_src
|
||
|
||
** The =init.el= macro to define abbreviations (~prot-emacs-abbrev~)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:e7a12825-7848-42bd-b99b-b87903012814
|
||
:END:
|
||
|
||
[ Watch Prot's video: [[https://protesilaos.com/codelog/2024-02-03-emacs-abbrev-mode/][abbreviations with abbrev-mode (quick text expansion)]] (2024-02-03). ]
|
||
|
||
#+begin_src emacs-lisp :tangle "init.el"
|
||
(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)))
|
||
#+end_src
|
||
|
||
** The =init.el= final part to load the individual modules
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:e6c4acf5-5b51-4b38-a86a-bf3f698ac872
|
||
:END:
|
||
|
||
Now we are ready to load our per-module configuration files:
|
||
|
||
#+begin_src emacs-lisp :tangle "init.el"
|
||
(require 'unravel-theme)
|
||
(require 'unravel-essentials)
|
||
(require 'unravel-completion)
|
||
(require 'unravel-search)
|
||
(require 'unravel-dired)
|
||
(require 'unravel-window)
|
||
(require 'unravel-git)
|
||
(require 'unravel-org)
|
||
(require 'unravel-shell)
|
||
(require 'unravel-langs)
|
||
#+end_src
|
||
|
||
* The =unravel-theme.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:8cf67c82-1ebb-4be8-b0e7-161bbf5419ce
|
||
:END:
|
||
|
||
This module defines everything related to the aesthetics of Emacs.
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-theme.el" :mkdirp yes
|
||
;;; Everything related to the look of Emacs
|
||
|
||
#+end_src
|
||
|
||
** The =unravel-theme.el= section for cool, modern themes (~ef-themes~)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:2b2a27a1-6d2e-4b59-bf60-94682e173f2f
|
||
:END:
|
||
|
||
I use themes from the ~ef-themes~ package exclusively.
|
||
|
||
Prot is the lead developer and maintainer of this package.
|
||
|
||
+ Package name (GNU ELPA): ~ef-themes~
|
||
+ Official manual: <https://protesilaos.com/emacs/ef-themes>
|
||
+ Change log: <https://protesilaos.com/emacs/ef-themes-changelog>
|
||
+ Git repositories:
|
||
- GitHub: <https://github.com/protesilaos/ef-themes>
|
||
- GitLab: <https://gitlab.com/protesilaos/ef-themes>
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-theme.el"
|
||
;;; 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-to-toggle '(ef-elea-light ef-elea-dark))
|
||
(setq ef-themes-variable-pitch-ui t)
|
||
(setq ef-themes-mixed-fonts t)
|
||
(setq ef-themes-rotate ef-themes-items)
|
||
(setq 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))))
|
||
(setq ef-themes-disable-other-themes t)
|
||
(mapc #'disable-theme custom-enabled-themes))
|
||
#+end_src
|
||
|
||
** The =unravel-theme.el= section for ~lin~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:bf5b4d08-8f33-4a8c-8ecd-fca19bf2497a
|
||
:END:
|
||
|
||
~lin~ is an improvement on ~hl-line-mode~.
|
||
|
||
Prot is the lead developer and maintainer of this package.
|
||
|
||
+ 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>
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-theme.el"
|
||
;;;; 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
|
||
(setopt lin-face 'lin-cyan))
|
||
#+end_src
|
||
|
||
** The =unravel-theme.el= section for ~spacious-padding~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:6c118185-fcb1-4c9a-93af-71814cb84279
|
||
:END:
|
||
|
||
~spacious-padding~ gives us a comfortable reading experience.
|
||
|
||
Prot is the lead developer and maintainer of this package.
|
||
|
||
#+begin_quote
|
||
Inspiration for this package comes from [[https://github.com/rougier][Nicolas Rougier's impressive
|
||
designs]] and [[https://github.com/minad/org-modern][Daniel Mendler's ~org-modern~ package]].
|
||
#+end_quote
|
||
|
||
+ Package name (GNU ELPA): ~spacious-padding~
|
||
+ Official manual: <https://protesilaos.com/emacs/spacious-padding>
|
||
+ Git repositories:
|
||
- GitHub: <https://github.com/protesilaos/spacious-padding>
|
||
- GitLab: <https://gitlab.com/protesilaos/spacious-padding>
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-theme.el"
|
||
;;;; Increase padding of windows/frames
|
||
;; <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)
|
||
: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))
|
||
|
||
;; Read the doc string of `spacious-padding-subtle-mode-line' as
|
||
;; it is very flexible.
|
||
(setq spacious-padding-subtle-mode-line t))
|
||
#+end_src
|
||
|
||
** The =unravel-theme.el= section for ~rainbow-mode~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:9438236e-a8a4-45e0-8c61-8268c634d50b
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-theme.el"
|
||
;;;; 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))
|
||
#+end_src
|
||
|
||
** The =unravel-theme.el= section for ~cursory~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:34ce98fe-0b57-44d9-b5f3-0224632114a5
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
Prot is the lead developer and maintainer.
|
||
|
||
+ Package name (GNU ELPA): ~cursory~
|
||
+ Official manual: <https://protesilaos.com/emacs/cursory>
|
||
+ Change log: <https://protesilaos.com/emacs/cursory-changelog>
|
||
+ Git repositories:
|
||
- GitHub: <https://github.com/protesilaos/cursory>
|
||
- GitLab: <https://gitlab.com/protesilaos/cursory>
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-theme.el"
|
||
;;; 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))
|
||
#+end_src
|
||
|
||
** The =unravel-theme.el= section for ~theme-buffet~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:2af10314-c8c2-4946-bf9c-a5b0f5fe881b
|
||
:END:
|
||
|
||
The ~theme-buffet~ package automatically changes the theme based on time of day.
|
||
|
||
Bruno Boal is the lead developer and Prot is 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>
|
||
+ Mailing list: <https://lists.sr.ht/~bboal/general-issues>
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-theme.el"
|
||
;;;; Theme buffet
|
||
;; <https://git.sr.ht/~bboal/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-time-offset 0)
|
||
(setq theme-buffet-end-user
|
||
'(: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)))
|
||
|
||
(when (or modus-themes-p ef-themes-p)
|
||
(theme-buffet-timer-hours 2)
|
||
(theme-buffet-a-la-carte))))
|
||
#+end_src
|
||
|
||
** The =unravel-theme.el= section about ~fontaine~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:cb41fef0-41a5-4a85-9552-496d96290258
|
||
:END:
|
||
|
||
[ Watch Prot's video: [[https://protesilaos.com/codelog/2024-01-16-customize-emacs-fonts/][Customise Emacs fonts]] (2024-01-16) ]
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
Prot is the lead developer and maintainer.
|
||
|
||
+ Package name (GNU ELPA): ~fontaine~
|
||
+ Official manual: <https://protesilaos.com/emacs/fontaine>
|
||
+ Change log: <https://protesilaos.com/emacs/fontaine-changelog>
|
||
+ Git repositories:
|
||
- GitHub: <https://github.com/protesilaos/fontaine>
|
||
- GitLab: <https://gitlab.com/protesilaos/fontaine>
|
||
|
||
Another section defines some complementary functionality
|
||
([[#h:60d6aae2-6e4b-402c-b6a8-411fc49a6857][The =unravel-theme.el= section about ~variable-pitch-mode~ and font resizing]]).
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-theme.el"
|
||
;;;; 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))))
|
||
(enable-theme-functions . fontaine-apply-current-preset)
|
||
(ef-themes-post-load . fontaine-apply-current-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-family "Iosevka"
|
||
:default-height 130)
|
||
(regular
|
||
:default-height 150)
|
||
(medium
|
||
:default-weight semilight
|
||
:default-height 170
|
||
:bold-weight extrabold)
|
||
(large
|
||
:inherit medium
|
||
:default-height 190)
|
||
(presentation
|
||
:inherit medium
|
||
:default-height 250)
|
||
(jumbo
|
||
:inherit medium
|
||
:default-height 330)
|
||
(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"
|
||
:default-weight regular
|
||
:default-slant normal
|
||
:default-width normal
|
||
:default-height 150
|
||
|
||
:fixed-pitch-family nil ; falls back to :default-family
|
||
:fixed-pitch-weight nil ; falls back to :default-weight
|
||
: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"
|
||
: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))))
|
||
#+end_src
|
||
|
||
** The =unravel-theme.el= section about ~show-font~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:60a005be-77bd-49f1-a865-78d7cf75bd2a
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
Prot is the developer and maintainer of this package.
|
||
|
||
+ 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>
|
||
|
||
To actually set fonts, use the ~fontaine~ package ([[#h:cb41fef0-41a5-4a85-9552-496d96290258][The =unravel-theme.el= section about ~fontaine~]]).
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-theme.el"
|
||
;;;; Show Font (preview fonts)
|
||
;; Read the manual: <https://protesilaos.com/emacs/show-font>
|
||
(use-package show-font
|
||
:ensure t
|
||
: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
|
||
"))
|
||
#+end_src
|
||
|
||
** The =unravel-theme.el= section about ~variable-pitch-mode~ and font resizing
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:60d6aae2-6e4b-402c-b6a8-411fc49a6857
|
||
:END:
|
||
|
||
[ Watch Prot's video: [[https://protesilaos.com/codelog/2024-01-16-customize-emacs-fonts/][Customise Emacs fonts]] (2024-01-16) ]
|
||
|
||
#+begin_quote
|
||
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 ([[#h:cb41fef0-41a5-4a85-9552-496d96290258][The =unravel-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~.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-theme.el"
|
||
;;;;; `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)))
|
||
#+end_src
|
||
|
||
*** Information about the Iosevka Comfy fonts
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:3b989679-7e3f-4f03-b4bb-611006ea01ce
|
||
:END:
|
||
|
||
/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:
|
||
- GitHub: <https://github.com/protesilaos/iosevka-comfy>
|
||
- GitLab: <https://gitlab.com/protesilaos/iosevka-comfy>
|
||
+ Sample pictures: <https://protesilaos.com/emacs/iosevka-comfy-pictures>
|
||
+ Backronym: Iosevka ... Could Only Modify a Font, Yes
|
||
|
||
|
||
** Finally, we provide the =unravel-theme.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:bac0ce0a-db68-42e7-ba2c-f350f91f80ef
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-theme.el"
|
||
(provide 'unravel-theme)
|
||
#+end_src
|
||
|
||
* The =unravel-essentials.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:0ef52ed9-7b86-4329-ae4e-eff9ab8d07f2
|
||
:END:
|
||
|
||
** The =unravel-essentials.el= block with basic configurations
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:713ede33-3802-40c6-a8e3-7e1fc0d0a924
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-essentials.el" :mkdirp yes
|
||
;;; Essential configurations
|
||
(use-package emacs
|
||
:ensure nil
|
||
:demand t
|
||
:config
|
||
;;;; General settings and common custom functions
|
||
(setq help-window-select t)
|
||
(setq next-error-recenter '(4)) ; center of the window
|
||
(setq find-library-include-other-files nil) ; Emacs 29
|
||
(setq tramp-connection-timeout (* 60 10)) ; seconds
|
||
(setq save-interprogram-paste-before-kill t)
|
||
(setq mode-require-final-newline t)
|
||
(setq-default truncate-partial-width-windows nil)
|
||
(setq eval-expression-print-length nil)
|
||
(setq kill-do-not-save-duplicates t)
|
||
(setq scroll-error-top-bottom t)
|
||
(setq echo-keystrokes-help t) ; Emacs 30
|
||
(setq epa-keys-select-method 'minibuffer)) ; Emacs 30
|
||
#+end_src
|
||
|
||
** The =unravel-essentials.el= section for fixing PATH on OSX (~exec-path-from-shell~)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:D4517B67-0D90-417E-97D7-60A08EABB3DA
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-essentials.el"
|
||
(use-package exec-path-from-shell
|
||
:ensure t
|
||
:demand t
|
||
:config
|
||
(exec-path-from-shell-initialize))
|
||
#+end_src
|
||
|
||
** The =unravel-essentials.el= configuration to track recently visited files (~recentf~)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:f9aa7523-d88a-4080-add6-073f36cb8b9a
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-essentials.el"
|
||
(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))
|
||
#+end_src
|
||
|
||
** The =unravel-essentials.el= settings for bookmarks
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:581aa0ff-b136-4099-a321-3b86edbfbccb
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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 [[#h:5685df62-4484-42ad-a062-d55ab19022e3][the =unravel-essentials.el= settings for registers]].
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-essentials.el"
|
||
;;;; 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))
|
||
#+end_src
|
||
|
||
** The =unravel-essentials.el= settings for registers
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:5685df62-4484-42ad-a062-d55ab19022e3
|
||
:END:
|
||
|
||
[ Watch Prot's video: [[https://protesilaos.com/codelog/2023-06-28-emacs-mark-register-basics/][Mark and register basics]] (2023-06-28). ]
|
||
|
||
#+begin_quote
|
||
Much like bookmarks, registers store data that we can reinstate
|
||
quickly ([[#h:581aa0ff-b136-4099-a321-3b86edbfbccb][The =unravel-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~ ([[#h:25765797-27a5-431e-8aa4-cc890a6a913a][The =unravel-completion.el= settings for saving the history (~savehist-mode~)]]).
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-essentials.el"
|
||
;;;; 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)))
|
||
#+end_src
|
||
|
||
** The =unravel-essentials.el= settings for files
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-essentials.el"
|
||
(use-package files
|
||
:ensure nil
|
||
:config
|
||
(setq confirm-kill-emacs #'y-or-n-p)
|
||
(setq require-final-newline t))
|
||
#+end_src
|
||
|
||
** The =unravel-essentials.el= section for ~delete-selection-mode~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:d551b90d-d730-4eb5-976a-24b010fd4db3
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-essentials.el"
|
||
;;;; Delete selection
|
||
(use-package delsel
|
||
:ensure nil
|
||
:hook (after-init . delete-selection-mode))
|
||
#+end_src
|
||
|
||
** The =unravel-essentials.el= settings for tooltips
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:26afeb95-7920-45ed-8ff6-3648256c280b
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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).
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-essentials.el"
|
||
;;;; 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))))
|
||
#+end_src
|
||
|
||
** The =unravel-essentials.el= arrangement to run Emacs as a server
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:7709b7e9-844f-49f3-badf-784aacec4bca
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-essentials.el"
|
||
;;;; 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)))
|
||
#+end_src
|
||
|
||
** The =unravel-essentials.el= section about ~easy-kill~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:891BA3F6-6229-45B5-B5E8-80FA4837662B
|
||
:END:
|
||
|
||
~easy-kill~ is a drop-in replacement for ~kill-ring-save~, letting me easily and quickly copy / kill anything I want.
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-essentials.el"
|
||
(use-package easy-kill
|
||
:ensure t
|
||
:bind
|
||
("M-w" . easy-kill)) ; re-map kill-ring-save
|
||
#+end_src
|
||
|
||
** The =unravel-essentials.el= section about ~expreg~ (tree-sitter mark syntactically)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:ceb193bf-0de3-4c43-8ab7-6daa50817754
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
I expect ~expreg~ to eventually completely replace ~easy-kill~ ()
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-essentials.el"
|
||
;;; Mark syntactic constructs efficiently if tree-sitter is available (expreg)
|
||
(when (treesit-available-p)
|
||
(use-package expreg
|
||
:ensure t
|
||
:functions (prot/expreg-expand prot/expreg-expand-dwim)
|
||
: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)))))))
|
||
#+end_src
|
||
|
||
** The =unravel-essentials.el= section for Battery display
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:080aa291-95b4-4d54-8783-d156b13190e9
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-essentials.el"
|
||
;;;; Show battery status on the mode line (battery.el)
|
||
(use-package battery
|
||
:ensure nil
|
||
:hook (after-init . display-battery-mode)
|
||
:config
|
||
(setq battery-mode-line-format
|
||
(cond
|
||
((eq battery-status-function #'battery-linux-proc-acpi)
|
||
"⏻ %b%p%%,%d°C ")
|
||
(battery-status-function
|
||
"⏻ %b%p%% "))))
|
||
#+end_src
|
||
|
||
** The =unravel-essentials.el= section for OSX changes
|
||
|
||
These are modifications to basic configuration I use on my Mac OSX machine.
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-essentials.el"
|
||
;;;; Configuration on Mac OS X machine
|
||
(when (eq system-type 'darwin)
|
||
(use-package ns-win
|
||
:ensure nil
|
||
:config
|
||
(defun copy-from-osx ()
|
||
"Make cut and paste work with the OS X clipboard"
|
||
(shell-command-to-string "pbpaste"))
|
||
|
||
(defun paste-to-osx (text &optional push)
|
||
"Make cut and paste work with the OS X clipboard"
|
||
(let ((process-connection-type nil))
|
||
(let ((proc (start-process "pbcopy" "*Messages*" "pbcopy")))
|
||
(process-send-string proc text)
|
||
(process-send-eof proc))))
|
||
|
||
(setq mac-command-modifier 'meta)
|
||
(setq mac-option-modifier 'alt)
|
||
(setq interprogram-cut-function #'paste-to-osx)
|
||
(setq interprogram-paste-function #'copy-from-osx)
|
||
;; Work around a bug on OS X where system-name is a fully qualified
|
||
;; domain name
|
||
(setq system-name (car (split-string system-name "\\.")))
|
||
;;; Binaries
|
||
(setq vc-git-program (or (executable-find "git") "/usr/local/bin/git"))
|
||
(setq epg-gpg-program (or (executable-find "gpg") "/usr/local/bin/gpg"))
|
||
;;; Source dirs
|
||
;; Note: These are hard-coded to my machine.
|
||
(setq source-directory (expand-file-name "~/src/emacs/src/"))
|
||
(setq find-function-C-source-directory (expand-file-name "~/src/emacs/src/"))))
|
||
#+end_src
|
||
|
||
** The =unravel-essentials.el= section for ~simple.el~ changes
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:6B18F988-DBAD-458C-97BE-129D1FF988F4
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-essentials.el"
|
||
(defun vedang/backward-kill-word-or-kill-region (&optional arg)
|
||
"Rebind `C-w' to work differently based on whether a region is active.
|
||
|
||
If the region is selected, retain the original behaviour, otherwise call
|
||
`backward-kill-word' instead. ARG is passed to `backward-kill-word'."
|
||
(interactive "p")
|
||
(if (region-active-p)
|
||
(kill-region (region-beginning) (region-end))
|
||
(backward-kill-word arg)))
|
||
|
||
(use-package simple
|
||
:ensure nil
|
||
:after vertico ;; so that we can bind to vertico-map
|
||
:bind
|
||
;; Rebind `C-w' to work differently based on whether a region is
|
||
;; active.
|
||
( :map global-map
|
||
("C-w" . vedang/backward-kill-word-or-kill-region)
|
||
:map vertico-map
|
||
("C-l" . vedang/backward-kill-word-or-kill-region))
|
||
:hook
|
||
((before-save . delete-trailing-whitespace)
|
||
(text-mode . turn-on-visual-line-mode))
|
||
:config
|
||
(setq column-number-mode t))
|
||
#+end_src
|
||
|
||
** Finally, we provide the =unravel-essentials.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:c8b2f021-fe5a-4f6b-944c-20340f764fb2
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-essentials.el"
|
||
(provide 'unravel-essentials)
|
||
#+end_src
|
||
|
||
* The =unravel-completion.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:15edf2c3-4419-4101-928a-6e224958a741
|
||
:END:
|
||
|
||
** The =unravel-completion.el= settings for completion styles
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:14b09958-279e-4069-81e3-5a16c9b69892
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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 expands =prefix= and then
|
||
add back to it the =suffix=.
|
||
|
||
- 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 completes =suffix=.
|
||
|
||
- 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 match
|
||
=professional= as well as =reproduce=.
|
||
|
||
- 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 match
|
||
~list-faces-display~ as well as ~pulsar-highlight-dwim~.
|
||
|
||
- initials :: Completion of acronyms and initialisms. Typing =lfd=
|
||
will thus match ~list-faces-display~. This completion style can also
|
||
be used for file system navigation, though I prefer to only have
|
||
~partial-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
|
||
([[#h:7cc77fd0-8f98-4fc0-80be-48a758fcb6e2][The =unravel-completion.el= for the ~orderless~ 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 aforementioned ~flex~ and
|
||
~initials~.
|
||
|
||
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~.
|
||
#+end_quote
|
||
|
||
(There are more details in Prot's file, for the interested reader)
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-completion.el" :mkdirp yes
|
||
;;; 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))))))
|
||
#+end_src
|
||
|
||
** The =unravel-completion.el= for the ~orderless~ completion style
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:7cc77fd0-8f98-4fc0-80be-48a758fcb6e2
|
||
:END:
|
||
|
||
#+begin_quote
|
||
The ~orderless~ package by Omar Antolín Camarena provides one of the
|
||
completion styles that I use ([[#h:14b09958-279e-4069-81e3-5a16c9b69892][The =unravel-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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-completion.el"
|
||
;;; Orderless completion style
|
||
(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)))
|
||
#+end_src
|
||
|
||
** The =unravel-completion.el= settings to ignore letter casing
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:7fe1787d-dba3-46fe-82a9-5dc5f8ea6217
|
||
:END:
|
||
|
||
#+begin_quote
|
||
I never really need to match letters case-sensitively in the
|
||
minibuffer. Let's have everything ignore casing by default.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-completion.el"
|
||
(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)
|
||
#+end_src
|
||
|
||
** The =unravel-completion.el= settings for recursive minibuffers
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:4299825a-db51-49fe-b415-fb1749eed289
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-completion.el"
|
||
(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))
|
||
#+end_src
|
||
|
||
** The =unravel-completion.el= settings for default values
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:aebbdd4c-6e5b-4773-9f0a-c69f0d3c7158
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-completion.el"
|
||
(use-package minibuf-eldef
|
||
:ensure nil
|
||
:hook (after-init . minibuffer-electric-default-mode)
|
||
:config
|
||
(setq minibuffer-default-prompt-format " [%s]")) ; Emacs 29
|
||
#+end_src
|
||
|
||
** The =unravel-completion.el= settings for common interactions
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:b640f032-ad11-413e-ad8f-63408671d500
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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 the =TODO= keyword of a task with =C-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 for ~org-use-fast-todo-selection~.
|
||
|
||
- The ~read-answer-short~ is complementary to ~use-short-answers~.
|
||
This is about providing the shorter version to some confirmation
|
||
prompt, such as =y= instead of =yes=.
|
||
|
||
- 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 to ~completing-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 (with =C-c C-q= else =M-x org-set-tags-command=)
|
||
where more than one candidate can be provided using completion,
|
||
provided each candidate is separated by the ~crm-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 the ~crm-separator~, and repeat until
|
||
you are done selecting at which point you type =RET=.
|
||
|
||
- Finally the ~file-name-shadow-mode~ is a neat little feature to
|
||
remove the "shadowed" part of a file prompt while using something
|
||
like =C-x C-f= (=M-x find-file=). File name shadowing happens when
|
||
we invoke ~find-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. With ~file-name-shadow-mode~ the "shadowed" part is removed
|
||
altogether. This is especially nice when combined with the
|
||
completion style called ~partial-completion~
|
||
([[#h:14b09958-279e-4069-81e3-5a16c9b69892][The =unravel-completion.el= settings for completion styles]]).
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-completion.el"
|
||
(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))
|
||
|
||
;; 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))
|
||
#+end_src
|
||
|
||
** The =unravel-completion.el= generic minibuffer UI settings
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:de61a607-0bdf-462b-94cd-c0898319590e
|
||
:END:
|
||
|
||
These are some settings for the default completion user interface.
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-completion.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))
|
||
#+end_src
|
||
|
||
** The =unravel-completion.el= settings for saving the history (~savehist-mode~)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:25765797-27a5-431e-8aa4-cc890a6a913a
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_quote
|
||
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:
|
||
|
||
- [[#h:804b858f-7913-47ef-aaf4-8eef5b59ecb4][The =unravel-completion.el= for in-buffer completion popup and preview (~corfu~)]]
|
||
- [[#h:5685df62-4484-42ad-a062-d55ab19022e3][The =unravel-essentials.el= settings for registers]]
|
||
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-completion.el"
|
||
;;;; `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))
|
||
#+end_src
|
||
|
||
** The =unravel-completion.el= settings for dynamic text expansion (~dabbrev~)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:567bb00f-1d82-4746-93e5-e0f60721728a
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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...
|
||
|
||
The term "dabbrev" stands for "dynamic abbreviation". Emacs also has
|
||
static, user-defined abbreviations ([[#h:fd84b79a-351e-40f0-b383-bf520d77834b][Settings for static text expansion
|
||
(~abbrev~)]]).
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-completion.el"
|
||
(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)))
|
||
#+end_src
|
||
|
||
** The =unravel-completion.el= settings for dynamic text expansion (~hippie~)
|
||
|
||
Hippie is a built-in expansion mechanism that competes with dabbrev. I prefer hippie because of the simpler configuration and detailed expansion options it provides.
|
||
|
||
Hippie uses Dabbrev as one of the expansion sources, so all the dabbrev settings above are still important.
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-completion.el"
|
||
(use-package hippie-ext
|
||
:ensure nil
|
||
:bind
|
||
;; Replace the default dabbrev
|
||
("M-/" . hippie-expand))
|
||
#+end_src
|
||
|
||
** The =unravel-completion.el= for in-buffer completion popup (~corfu~)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:804b858f-7913-47ef-aaf4-8eef5b59ecb4
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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 [[#h:567bb00f-1d82-4746-93e5-e0f60721728a][the =unravel-completion.el= settings for dynamic text expansion (~dabbrev~)]].
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-completion.el"
|
||
;;; Corfu (in-buffer completion popup)
|
||
(use-package corfu
|
||
:ensure t
|
||
: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)))
|
||
#+end_src
|
||
|
||
** The =unravel-completion.el= settings for ~consult~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:22e97b4c-d88d-4deb-9ab3-f80631f9ff1d
|
||
:END:
|
||
|
||
#+begin_quote
|
||
~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 generic =C-x b=
|
||
(=M-x switch-to-buffer=). It is a one-stop-shop for buffers,
|
||
recently visited files (if ~recentf-mode~ is used---I don't),
|
||
bookmarks ([[#h:581aa0ff-b136-4099-a321-3b86edbfbccb][The =unravel-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
|
||
minibuffer =b 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 command ~consult-narrow-help~).
|
||
Every multi-source command from ~consult~ 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's ~grep~ 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 send =prot-= to the ~grep~ program and then use =completion=
|
||
inside of the minibuffer to perform the subsequent pattern-matching
|
||
(e.g. with help from ~orderless~ ([[#h:7cc77fd0-8f98-4fc0-80be-48a758fcb6e2][The =unravel-completion.el= for the ~orderless~ 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, the ~consult-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: [[#h:e0f9c30e-3a98-4479-b709-7008277749e4][The =unravel-search.el= module]].
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-completion.el"
|
||
;;; Enhanced minibuffer commands (consult.el)
|
||
(use-package consult
|
||
:ensure t
|
||
:hook (completion-list-mode . consult-preview-at-point-mode)
|
||
:bind
|
||
(:map global-map
|
||
;; Prot's bindings
|
||
("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) ; Start opening anything from here
|
||
("M-s M-f" . consult-fd)
|
||
("M-s M-g" . consult-ripgrep)
|
||
("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)
|
||
;; Overriding defaults: C-x bindings in `ctl-x-map'
|
||
("C-x M-:" . consult-complex-command) ;; orig. repeat-complex-command
|
||
("C-x 4 b" . consult-buffer-other-window) ;; orig. switch-to-buffer-other-window
|
||
("C-x 5 b" . consult-buffer-other-frame) ;; orig. switch-to-buffer-other-frame
|
||
("C-x t b" . consult-buffer-other-tab) ;; orig. switch-to-buffer-other-tab
|
||
("C-x r b" . consult-bookmark) ;; orig. bookmark-jump
|
||
("C-x p b" . consult-project-buffer) ;; orig. project-switch-to-buffer
|
||
;; Custom M-# bindings for fast register access
|
||
("M-#" . consult-register-load)
|
||
("M-'" . consult-register-store) ;; orig. abbrev-prefix-mark (unrelated)
|
||
("C-M-#" . consult-register)
|
||
;; Other custom bindings
|
||
("M-y" . consult-yank-pop) ;; orig. yank-pop
|
||
;; M-g bindings in `goto-map'
|
||
("M-g e" . consult-compile-error)
|
||
("M-g f" . consult-flymake) ;; Alternative: consult-flycheck
|
||
("M-g g" . consult-goto-line) ;; orig. goto-line
|
||
("M-g M-g" . consult-goto-line) ;; orig. goto-line
|
||
("M-g o" . consult-outline) ;; Alternative: consult-org-heading
|
||
;; My bindings from my Helm workflow
|
||
("C-x c i" . consult-imenu)
|
||
("C-c s" . consult-ripgrep)
|
||
: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))
|
||
;; the `imenu' extension is in its own file
|
||
(require 'consult-imenu))
|
||
#+end_src
|
||
|
||
** The =unravel-completion.el= section about ~embark~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:61863da4-8739-42ae-a30f-6e9d686e1995
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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 ([[#h:f8b08a77-f3a8-42fa-b1a9-f940348889c3][The =unravel-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.
|
||
|
||
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~,
|
||
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 ([[#h:fb034be5-c316-4c4f-a46f-cebcab332a47][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
|
||
[[https://karthinks.com/software/fifteen-ways-to-use-embark/][Fifteen ways to use Embark]]. If you plan on becoming an ~embark~ power
|
||
user, this will help you.
|
||
#+end_quote
|
||
|
||
Here are the main keybindings you should be aware of:
|
||
|
||
- ~C-.~ to enter embark with ~prot-embark-act-quit~
|
||
- ~E~ to ~embark-export~ (eg: when you want to see grep results in the ~grep-mode~ window
|
||
- ~S~ to ~embark-collect~ (eg: when you just want to save the consult results for inspection)
|
||
- ~A~ to ~embark-act~ (eg: to choose a new action in the middle of an existing action)
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-completion.el"
|
||
;;; Extended minibuffer actions and more (embark.el and prot-embark.el)
|
||
(use-package embark
|
||
:ensure t
|
||
:defer 1
|
||
:config
|
||
(setq embark-confirm-act-all nil)
|
||
(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))
|
||
#+end_src
|
||
|
||
** The =unravel-completion.el= section to configure completion annotations (~marginalia~)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:bd3f7a1d-a53d-4d3e-860e-25c5b35d8e7e
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-completion.el"
|
||
;;; Detailed completion annotations (marginalia.el)
|
||
(use-package marginalia
|
||
:ensure t
|
||
:hook (after-init . marginalia-mode)
|
||
:config
|
||
(setq marginalia-max-relative-age 0)) ; absolute time
|
||
#+end_src
|
||
|
||
** The =unravel-completion.el= section for ~vertico~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:cff33514-d3ac-4c16-a889-ea39d7346dc5
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
I use ~vertico-repeat~ to mimic the functionality that ~helm-resume~ would provide. The configuration for that is also part of this section.
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-completion.el"
|
||
;;; 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)))
|
||
|
||
(use-package vertico-repeat
|
||
:after vertico
|
||
:bind ( :map global-map
|
||
("M-R" . vertico-repeat)
|
||
:map vertico-map
|
||
("M-N" . vertico-repeat-next)
|
||
("M-P" . vertico-repeat-previous))
|
||
:hook (minibuffer-setup . vertico-repeat-save))
|
||
|
||
(use-package vertico-suspend
|
||
:after vertico
|
||
;; Note: `enable-recursive-minibuffers' must be t
|
||
:bind ( :map global-map
|
||
("M-S" . vertico-suspend)
|
||
("C-x c b" . vertico-suspend)))
|
||
#+end_src
|
||
|
||
** Finally, we provide the ~unravel-completion.el~ module
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-completion.el"
|
||
(provide 'unravel-completion)
|
||
#+end_src
|
||
* The =unravel-search.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:e0f9c30e-3a98-4479-b709-7008277749e4
|
||
:END:
|
||
|
||
[ Watch Prot's talk: [[https://protesilaos.com/codelog/2023-06-10-emacs-search-replace-basics/][Emacs: basics of search and replace]] (2023-06-10). ]
|
||
|
||
#+begin_quote
|
||
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 either =C-s=
|
||
and =C-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. The =C-s= and =C-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= and =C-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: [[#h:b902e6a3-cdd2-420f-bc99-3d973c37cd20][The =unravel-search.el= extras provided by the =prot-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 by =M-s o= (~isearch-forward~ --> ~occur~) ::
|
||
Like =C-s= but then put the matches in an *occur* buffer.
|
||
|
||
- =C-s SEARCH= followed by =C-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 by =M-%= (~isearch-forward~ --> ~query-replace~) :: Search
|
||
with =C-s= and then perform a query-replace for the following
|
||
matches.
|
||
|
||
- =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.
|
||
#+end_quote
|
||
|
||
** The =unravel-search.el= section on imenu
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:7151F001-75DB-4808-95CB-3BC6BEC6A8CA
|
||
:END:
|
||
|
||
~imenu~ is amazing and I use it as my primary tool for navigating any file. Here, we make a small tweak to ensure that ~consult-imenu~ returns good ~imenu~ results to us.
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-search.el" :mkdirp yes
|
||
(use-package imenu
|
||
:ensure nil
|
||
:config
|
||
;; Don't limit item length, this makes imenu less useful in
|
||
;; consult-imenu
|
||
(setq imenu-max-item-length 'unlimited))
|
||
#+end_src
|
||
|
||
** The =unravel-search.el= section on isearch lax space
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:95947b37-2071-4ee7-a201-8e19bf3322e9
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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~).
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-search.el"
|
||
;;; Isearch, occur, grep
|
||
(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))
|
||
#+end_src
|
||
|
||
** The =unravel-search.el= settings for isearch highlighting
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:ed1307e7-f8a0-4b0a-8d91-2de9c1e2479c
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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~.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-search.el"
|
||
(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))
|
||
#+end_src
|
||
|
||
** The =unravel-search.el= section on isearch match counter
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:acfdc17f-7ffb-48d3-90ff-49bd00463934
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-search.el"
|
||
(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))
|
||
#+end_src
|
||
|
||
** The =unravel-search.el= tweaks for the occur buffer
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:85aca4da-b89b-4fbe-89e9-3ec536ad7b0d
|
||
:END:
|
||
|
||
#+begin_quote
|
||
Here I am making some minor tweaks to =*occur*= buffer (remember to
|
||
read the introduction to this section ([[#h:e0f9c30e-3a98-4479-b709-7008277749e4][The =unravel-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 ...
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-search.el"
|
||
(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 #'hl-line-mode))
|
||
#+end_src
|
||
|
||
** The =unravel-search.el= modified isearch and occur key bindings
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:5ce6216d-f318-4191-9d4f-9681c92f7582
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-search.el"
|
||
(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)))
|
||
#+end_src
|
||
|
||
** The =unravel-search.el= tweaks to ~xref~, ~re-builder~ and ~grep~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:ceb286c5-a5f7-4cc8-b883-89d20a75ea02
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-search.el"
|
||
;;; 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)
|
||
(setq xref-search-program (if (or (executable-find "rg")
|
||
(executable-find "ripgrep"))
|
||
'ripgrep
|
||
'grep)))
|
||
|
||
(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))))
|
||
#+end_src
|
||
|
||
** The =unravel-search.el= setup for editable grep buffers (~grep-edit-mode~ or ~wgrep~)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:9a3581df-ab18-4266-815e-2edd7f7e4852
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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
|
||
([[#h:1b53bc10-8b1b-4f68-bbec-165909761e43][The =unravel-dired.el= section about ~wdired~ (writable Dired)]]).
|
||
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-search.el"
|
||
;;; 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)))
|
||
#+end_src
|
||
|
||
** Finally, we provide the =unravel-search.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:c8b2f021-fe5a-4f6b-944c-20340f764fb2
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-search.el"
|
||
(provide 'unravel-search)
|
||
#+end_src
|
||
|
||
* The =unravel-dired.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:f8b08a77-f3a8-42fa-b1a9-f940348889c3
|
||
:END:
|
||
|
||
[ Watch Prot's talk: <https://protesilaos.com/codelog/2023-06-26-emacs-file-dired-basics/> (2023-06-26) ]
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
** The =unravel-dired.el= settings for common operations
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:39fb0eab-54bb-4e5b-8e38-9443dbe5c5ee
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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
|
||
([[#h:2e005bd1-d098-426d-91f9-2a31a6e55caa][The =unravel-dired.el= section about =trashed.el=]]).
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-dired.el" :mkdirp yes
|
||
;;; 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))
|
||
#+end_src
|
||
|
||
** The =unravel-dired.el= switches for ~ls~ (how files are listed)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:679e4460-b306-450f-aa20-497243057e02
|
||
:END:
|
||
|
||
#+begin_quote
|
||
As I already explained, Dired is a layer of interactivity on top of
|
||
standard Unix tools ([[#h:f8b08a77-f3a8-42fa-b1a9-f940348889c3][The =unravel-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 of
|
||
=568024= (the size of =unravel.org= as of this writing).
|
||
|
||
- =-l= :: Produce a long, detailed listing. Dired requires this.
|
||
|
||
- =-v= :: Sort files by version numbers, such that =file1=, =file2=,
|
||
and =file10= 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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-dired.el"
|
||
(use-package dired
|
||
:ensure nil
|
||
:commands (dired)
|
||
:config
|
||
(setq insert-directory-program
|
||
(or (executable-find "gls") "/opt/homebrew/bin/gls"))
|
||
(setq dired-listing-switches
|
||
"-AGFhlv --group-directories-first --time-style=long-iso"))
|
||
#+end_src
|
||
|
||
** The =unravel-dired.el= setting for dual-pane Dired
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:8225364c-3856-48bc-bf64-60d40ddd3320
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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 ([[#h:25765797-27a5-431e-8aa4-cc890a6a913a][The =unravel-completion.el= settings for saving the history (~savehist-mode~)]]).
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-dired.el"
|
||
(use-package dired
|
||
:ensure nil
|
||
:commands (dired)
|
||
:config
|
||
(setq dired-dwim-target t))
|
||
#+end_src
|
||
|
||
** The =unravel-dired.el= miscellaneous tweaks
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:6327e6ba-a468-416f-ad26-b530c32fe235
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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
|
||
([[#h:679e4460-b306-450f-aa20-497243057e02][The =unravel-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~
|
||
([[#h:fbe6f9da-25ee-46a3-bb03-8fa7c1d48dab][The =unravel-essentials.el= settings for ~repeat-mode~]]).
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-dired.el"
|
||
(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))
|
||
#+end_src
|
||
|
||
** The =unravel-dired.el= section about various conveniences
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:6758bf16-e47e-452e-b39d-9d67c2b9aa4b
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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~ and
|
||
~dired-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~ and
|
||
~dired-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 ([[#h:d7d5e619-3d50-4d9e-a951-7262462a60c9][The =unravel-dired.el= settings to open files externally]]).
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-dired.el"
|
||
(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))
|
||
#+end_src
|
||
|
||
** The =unravel-dired.el= section about ~dired-subtree~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:3a4a29bc-3491-4d01-9d64-1cef63b3116a
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-dired.el"
|
||
(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))
|
||
#+end_src
|
||
|
||
** The =unravel-dired.el= section about ~wdired~ (writable Dired)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:1b53bc10-8b1b-4f68-bbec-165909761e43
|
||
:END:
|
||
|
||
#+begin_quote
|
||
As noted in the introduction, Dired can be made writable
|
||
([[#h:f8b08a77-f3a8-42fa-b1a9-f940348889c3][The =unravel-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
|
||
([[#h:9a3581df-ab18-4266-815e-2edd7f7e4852][The =unravel-search.el= setup for editable grep buffers (~wgrep~)]]).
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-dired.el"
|
||
(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))
|
||
#+end_src
|
||
|
||
** The =unravel-dired.el= section about ~trashed~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:2e005bd1-d098-426d-91f9-2a31a6e55caa
|
||
:END:
|
||
|
||
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 trashed=, 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.
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-dired.el"
|
||
;;; 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"))
|
||
#+end_src
|
||
|
||
** Finally, we provide the =unravel-dired.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:c8b2f021-fe5a-4f6b-944c-20340f764fb2
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-dired.el"
|
||
(provide 'unravel-dired)
|
||
#+end_src
|
||
|
||
* The =unravel-window.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:b5fa481d-8549-4424-869e-91091cdf730b
|
||
:END:
|
||
|
||
#+begin_quote
|
||
This module is all about buffers and windows. How they are managed and
|
||
displayed.
|
||
#+end_quote
|
||
|
||
** The =unravel-window.el= section about uniquifying buffer names
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:cfbea29c-3290-4fd1-a02a-d7e887c15674
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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:
|
||
|
||
#+begin_example
|
||
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>
|
||
#+end_example
|
||
|
||
I use the =forward= style, which is the closest to the actual file
|
||
name.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el"
|
||
;;; 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))
|
||
#+end_src
|
||
|
||
** The =unravel-window.el= rules for displaying buffers (~display-buffer-alist~)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:50f8b1e4-b14e-453f-a37e-1c0e495ab80f
|
||
:END:
|
||
|
||
[ Watch Prot's talk: [[https://protesilaos.com/codelog/2024-02-08-emacs-window-rules-display-buffer-alist/][control where buffers are displayed (the ~display-buffer-alist~)]] (2024-02-08). ]
|
||
|
||
#+begin_quote
|
||
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:
|
||
|
||
#+begin_example
|
||
(BUFFER-MATCHER
|
||
FUNCTIONS-TO-DISPLAY-BUFFER
|
||
OTHER-PARAMETERS)
|
||
#+end_example
|
||
|
||
- 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 the ~derived-mode~ symbol
|
||
(remember that I build Emacs from source, so ~derived-mode~ may not
|
||
exist in your version of Emacs).
|
||
|
||
- The =FUNCTIONS-TO-DISPLAY-BUFFER= is a list of ~display-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:
|
||
|
||
#+begin_src emacs-lisp
|
||
(info "(elisp) Buffer Display Action Alists")
|
||
#+end_src
|
||
|
||
In my =prot-window.el= library, I define functions that determine how
|
||
a buffer should be displayed, given size considerations ([[#h:35b8a0a5-c447-4301-a404-bc274596238d][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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el"
|
||
;;;; `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))
|
||
;; 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)))))
|
||
#+end_src
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el"
|
||
(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))
|
||
#+end_src
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el"
|
||
(use-package window
|
||
:ensure nil
|
||
:config
|
||
(setq display-buffer-reuse-frames t)
|
||
(setq recenter-positions '(top middle bottom)))
|
||
#+end_src
|
||
|
||
** The =unravel-window.el= section about ~beframe~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:77e4f174-0c86-460d-8a54-47545f922ae9
|
||
:END:
|
||
|
||
[ Also see: [[#h:7dcbcadf-8af6-487d-b864-e4ce56d69530][The =unravel-git.el= section about =project.el=]]. ]
|
||
|
||
#+begin_quote
|
||
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/>.
|
||
#+end_quote
|
||
|
||
+ Package name (GNU ELPA): ~beframe~
|
||
+ Official manual: <https://protesilaos.com/emacs/beframe>
|
||
+ Change log: <https://protesilaos.com/emacs/beframe-changelog>
|
||
+ Git repositories:
|
||
- GitHub: <https://github.com/protesilaos/beframe>
|
||
- GitLab: <https://gitlab.com/protesilaos/beframe>
|
||
+ Backronym: Buffers Encapsulated in Frames Realise Advanced
|
||
Management of Emacs.
|
||
|
||
Some notes on how I use beframe:
|
||
|
||
- I add ~beframe~ as a source for ~consult-buffers~.
|
||
+ This code comes from the ~beframe~ manual and lets me look at frame-specific buffers first when using ~consult-buffer~ (=M-s M-b=) ([[#h:22e97b4c-d88d-4deb-9ab3-f80631f9ff1d][The =unravel-completion.el= settings for ~consult~]])
|
||
- I replace the default =C-x b= (~switch-to-buffer~) with the beframe'd version of the same command.
|
||
+ This lets me focus on the buffers in the current frame, which is what I generally want. When I need to access files or buffers which are not open yet, I use either:
|
||
- The consult version of the command (=M-s M-b=)
|
||
- ~project.el~ to find a file in the current project (=C-x p f=)
|
||
- When I am done with the frame, I delete all the buffers in the frame using ~beframe-kill-buffers-matching-regexp~ (=C-c b k=) with regex ~*~. This automatically kills the frame.
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el"
|
||
;;; 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)
|
||
:bind
|
||
("C-x f" . other-frame-prefix)
|
||
("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)
|
||
("C-x b" . beframe-switch-buffer)
|
||
("C-x B" . select-frame-by-name)
|
||
:config
|
||
(setq beframe-functions-in-frames '(project-prompt-project-dir))
|
||
|
||
(defvar consult-buffer-sources)
|
||
(declare-function consult--buffer-state "consult")
|
||
|
||
(with-eval-after-load 'consult
|
||
(defface beframe-buffer
|
||
'((t :inherit font-lock-string-face))
|
||
"Face for `consult' framed buffers.")
|
||
|
||
(defun my-beframe-buffer-names-sorted (&optional frame)
|
||
"Return the list of buffers from `beframe-buffer-names' sorted by visibility.
|
||
With optional argument FRAME, return the list of buffers of FRAME."
|
||
(beframe-buffer-names frame :sort #'beframe-buffer-sort-visibility))
|
||
|
||
(defvar beframe-consult-source
|
||
`( :name "Frame-specific buffers (current frame)"
|
||
:narrow ?F
|
||
:category buffer
|
||
:face beframe-buffer
|
||
:history beframe-history
|
||
:items ,#'my-beframe-buffer-names-sorted
|
||
:action ,#'switch-to-buffer
|
||
:state ,#'consult--buffer-state))
|
||
|
||
(add-to-list 'consult-buffer-sources 'beframe-consult-source)))
|
||
#+end_src
|
||
|
||
** The =unravel-window.el= configuration of ~undelete-frame-mode~ and ~winner-mode~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:2df15080-77f9-45f8-a3b5-1adddc70a512
|
||
:END:
|
||
|
||
#+begin_quote
|
||
Since I am using my ~beframe~ package to isolate buffers per frame
|
||
([[#h:77e4f174-0c86-460d-8a54-47545f922ae9][The =unravel-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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el"
|
||
;;; 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))
|
||
#+end_src
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el"
|
||
;;; 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)))
|
||
#+end_src
|
||
|
||
** COMMENT The =unravel-window.el= use of contextual header line (~breadcrumb~)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:29ced61b-4b5a-4f63-af00-fe311468d1cd
|
||
:END:
|
||
|
||
~breadcrumb~ is an interesting package when demo'ing something, but not useful to me in day-to-day use. Commenting it out until I need it.
|
||
|
||
#+begin_quote
|
||
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!
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el"
|
||
;;; 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))))
|
||
#+end_src
|
||
|
||
** The =unravel-window.el= section for Zone (~zone~)
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el"
|
||
(use-package zone
|
||
:ensure nil
|
||
:config
|
||
(zone-when-idle 300))
|
||
#+end_src
|
||
|
||
** The =unravel-window.el= section for displaying time
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:8E669AD7-D3BC-4168-AFAD-C6D4A25AF860
|
||
:END:
|
||
|
||
I like being able to see the time in the modeline.
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el"
|
||
(use-package time
|
||
:ensure nil
|
||
:config
|
||
(setq display-time-day-and-date t)
|
||
(display-time))
|
||
#+end_src
|
||
|
||
** Finally, we provide the =unravel-window.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:2124c200-734d-49c4-aeb1-513caaf957ae
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-window.el"
|
||
(provide 'unravel-window)
|
||
#+end_src
|
||
|
||
* The =unravel-git.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:65e3eff5-0bff-4e1f-b6c5-0d3aa1a0d232
|
||
:END:
|
||
|
||
[ Watch Prot's talk: [[https://protesilaos.com/codelog/2023-08-03-contribute-core-emacs/][Contribute to GNU Emacs core]] (2023-08-03). ]
|
||
|
||
#+begin_quote
|
||
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".
|
||
#+end_quote
|
||
|
||
** The =unravel-git.el= section about ediff
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:89edea05-4d94-4ea1-b2a8-5ad01422618c
|
||
:END:
|
||
|
||
[ Watch Prot's talk: [[https://protesilaos.com/codelog/2023-11-17-emacs-ediff-basics/][Emacs: ediff basics]] (2023-12-30) ]
|
||
|
||
#+begin_quote
|
||
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~ ([[#h:b08af527-9ebf-4425-ac3a-24b4f371a4fd][The =unravel-git.el= section about ~magit~ (great Git client)]]).
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-git.el" :mkdirp yes
|
||
;;;; `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))
|
||
#+end_src
|
||
|
||
** The =unravel-git.el= section about =project.el=
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:7dcbcadf-8af6-487d-b864-e4ce56d69530
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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= ([[#h:50add1d8-f0f4-49be-9e57-ab280a4aa300][The =unravel-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 ([[#h:77e4f174-0c86-460d-8a54-47545f922ae9][The =unravel-window.el= section about ~beframe~]]).
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-git.el"
|
||
;;;; `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
|
||
#+end_src
|
||
|
||
** The =unravel-git.el= section about ~diff-mode~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:8b426a69-e3cd-42ac-8788-f41f6629f879
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-git.el"
|
||
;;;; `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))
|
||
#+end_src
|
||
|
||
** The =unravel-git.el= section about ~magit~ (great Git client)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:b08af527-9ebf-4425-ac3a-24b4f371a4fd
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-git.el"
|
||
;;; 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-x 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
|
||
'(("~/src/prototypes" . 1))))
|
||
#+end_src
|
||
|
||
** The =unravel-git.el= call to ~provide~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:4e7035c5-9350-4c51-be85-85f2539ed295
|
||
:END:
|
||
|
||
Finally, we ~provide~ the module.
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-git.el"
|
||
(provide 'unravel-git)
|
||
#+end_src
|
||
|
||
* The =unravel-org.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:d799c3c0-bd6a-40bb-bd1a-ba4ea5367840
|
||
:END:
|
||
|
||
Watch these talks that I've given about Org:
|
||
|
||
Watch these talks by Prot:
|
||
|
||
- [[https://protesilaos.com/codelog/2023-12-18-emacs-org-advanced-literate-conf/][Advanced literate configuration with Org]] (2023-12-18)
|
||
- [[https://protesilaos.com/codelog/2023-05-23-emacs-org-basics/][Basics of Org mode]] (2023-05-23)
|
||
- [[https://protesilaos.com/codelog/2021-12-09-emacs-org-block-agenda/][Demo of my custom Org block agenda]] (2021-12-09)
|
||
- [[https://protesilaos.com/codelog/2020-02-04-emacs-org-capture-intro/][Primer on "org-capture"]] (2020-02-04)
|
||
|
||
|
||
#+begin_quote
|
||
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 ([[#h:755e195b-9471-48c7-963b-33055969b4e2][The =unravel-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.
|
||
#+end_quote
|
||
|
||
** The =unravel-org.el= section on the ~calendar~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:94d48381-1711-4d6b-8449-918bc1e3836c
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-org.el" :mkdirp yes
|
||
;;; 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"))
|
||
#+end_src
|
||
|
||
** The =unravel-org.el= section about appointment reminders (=appt.el=)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:bd4b0dcb-a925-4bd7-90db-6379a7ca6f5e
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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
|
||
([[#h:7fe87b83-2815-4617-a5f9-d3417dd9d248][The =unravel-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 ([[#h:f8f06938-0dfe-45c3-b4cf-996d36cba82d][The =unravel-org.el= Org capture templates (~org-capture~)]]).
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-org.el"
|
||
;;; 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)
|
||
|
||
;; Create reminders for tasks with a due date when this file is read.
|
||
(org-agenda-to-appt)))
|
||
#+end_src
|
||
|
||
** The =unravel-org.el= section on paragraphs
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:71AF485F-9C6B-4936-B50F-3A126663FFB0
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-org.el"
|
||
(use-package paragraphs
|
||
:ensure nil
|
||
:config
|
||
(setq sentence-end-double-space nil))
|
||
#+end_src
|
||
|
||
** The =unravel-org.el= section with basic Org settings
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:e03df1e0-b43e-49b5-978e-6a511165617c
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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: [[https://protesilaos.com/codelog/2023-12-18-emacs-org-advanced-literate-conf/][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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-org.el"
|
||
;;; Org-mode (personal information manager)
|
||
(use-package org
|
||
:ensure nil
|
||
:init
|
||
(setq org-directory (expand-file-name "~/Tresors/Documents/diary"))
|
||
(setq org-default-notes-file (expand-file-name "brain/daily.org" org-directory))
|
||
(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-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
|
||
)
|
||
:config
|
||
;;;; 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-use-speed-commands t)
|
||
(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))
|
||
#+end_src
|
||
|
||
** The =unravel-org.el= section for archival settings
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:119A0C7D-03E1-41D0-AC98-F14FD92245F1
|
||
:CREATED: [2024-11-28 Thu 11:46]
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-org.el"
|
||
;;;; archival, org-archive
|
||
(use-package org
|
||
:ensure nil
|
||
:config
|
||
;; Setup directory and file paths for org
|
||
(defvar org-archive-directory (concat org-directory "/archive")
|
||
"Directory under which all archived content is stored.")
|
||
(setq org-archive-location (concat org-archive-directory "/%s_archive::")))
|
||
#+end_src
|
||
|
||
** The =unravel-org.el= section for narrowing and folding
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:D11C6331-86BB-466D-AF65-A2A50F49A808
|
||
:CREATED: [2024-11-28 Thu 15:55]
|
||
:END:
|
||
Narrowing and folding are really powerful ways of working with org-mode, since we tend to create so many nodes and subtrees when we write documents in Org
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-org.el"
|
||
;;; Narrowing and Folding
|
||
(use-package org
|
||
:ensure nil
|
||
:bind
|
||
( :map narrow-map
|
||
("b" . org-narrow-to-block)
|
||
("e" . org-narrow-to-element)
|
||
("s" . org-narrow-to-subtree)))
|
||
|
||
(use-package org-fold
|
||
:ensure nil
|
||
:config
|
||
(setq org-fold-catch-invisible-edits 'show-and-error))
|
||
#+end_src
|
||
** The =unravel-org.el= Org to-do and refile settings
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:024dd541-0061-4a10-b10b-b17dcd4794b9
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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 ([[#h:0884658e-9eb5-47e3-9338-66e09004a1a0][The =unravel-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 ([[#h:7fe87b83-2815-4617-a5f9-d3417dd9d248][The =unravel-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 ([[#h:f8f06938-0dfe-45c3-b4cf-996d36cba82d][The =unravel-org.el= Org capture templates (~org-capture~)]]).
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-org.el"
|
||
;;;; 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)
|
||
(setq org-todo-keywords
|
||
'((sequence "TODO(t)" "|" "CANCEL(c@)" "DONE(d!)")))
|
||
|
||
(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))
|
||
#+end_src
|
||
|
||
** The =unravel-org.el= Org heading tags
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:81de4e32-a1af-4e1f-9e10-90eb0c90afa2
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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:
|
||
|
||
- [[#h:024dd541-0061-4a10-b10b-b17dcd4794b9][The =unravel-org.el= Org to-do and refile settings]]
|
||
- [[#h:7fe87b83-2815-4617-a5f9-d3417dd9d248][The =unravel-org.el= Org agenda settings]]
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-org.el"
|
||
;;;; tags
|
||
(use-package org
|
||
:ensure nil
|
||
:config
|
||
(setq org-tag-alist nil)
|
||
(setq org-auto-align-tags nil)
|
||
(setq org-tags-column 0))
|
||
#+end_src
|
||
|
||
** The =unravel-org.el= Org time/state logging
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:0884658e-9eb5-47e3-9338-66e09004a1a0
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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 ([[#h:7fe87b83-2815-4617-a5f9-d3417dd9d248][The =unravel-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).
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-org.el"
|
||
;;;; 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))
|
||
#+end_src
|
||
|
||
** The =unravel-org.el= Org link settings
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:da8ce883-7f21-4a6e-a41f-d668ad762b41
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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:
|
||
|
||
#+begin_example emacs-lisp
|
||
(info "(org) Search Options")
|
||
#+end_example
|
||
#+end_quote
|
||
|
||
I bind ~org-store-link~ in main section of the Org configuration:
|
||
[[#h:e03df1e0-b43e-49b5-978e-6a511165617c][The =unravel-org.el= section with basic Org settings]].
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-org.el"
|
||
;;;; links
|
||
(use-package org
|
||
:ensure nil
|
||
:config
|
||
(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))
|
||
#+end_src
|
||
|
||
** The =unravel-org.el= Org code block settings
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:1f5a0d46-5202-48dd-8048-b48ce17f3df8
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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: [[https://protesilaos.com/codelog/2023-12-18-emacs-org-advanced-literate-conf/][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:
|
||
|
||
#+begin_example emacs-lisp
|
||
(require 'ob-python)
|
||
|
||
;; OR
|
||
|
||
(use-package ob-python)
|
||
|
||
;; OR for more control
|
||
|
||
(use-package ob-python
|
||
:after org
|
||
:config
|
||
;; Settings here
|
||
)
|
||
#+end_example
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-org.el"
|
||
;;;; 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 nil)
|
||
(setq org-src-tab-acts-natively t)
|
||
(setq org-edit-src-content-indentation 2))
|
||
#+end_src
|
||
|
||
** The =unravel-org.el= Org export settings
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:bd11d4d8-6e9f-4536-87a4-4018783bf8f5
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-org.el"
|
||
;;;; 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))
|
||
#+end_src
|
||
|
||
** The =unravel-org.el= Org capture templates (~org-capture~)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:f8f06938-0dfe-45c3-b4cf-996d36cba82d
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-org.el"
|
||
;;;; org-capture
|
||
(use-package org-capture
|
||
:ensure nil
|
||
:bind ("C-c c" . org-capture))
|
||
#+end_src
|
||
|
||
** The =unravel-org.el= section on YASnippet
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:75C672E5-4636-4CC0-A5ED-500EAFAB01DE
|
||
:END:
|
||
|
||
~yasnippet~ is the primary tool I use when I need templates of any kind. Since most of my templating is in ~org-mode~, it makes sense to keep YAS in this section.
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-org.el"
|
||
(use-package yasnippet
|
||
:ensure t
|
||
:config
|
||
(yas-global-mode 1)
|
||
(add-to-list 'hippie-expand-try-functions-list
|
||
'yas-hippie-try-expand))
|
||
|
||
(use-package yasnippet-snippets
|
||
:ensure t
|
||
:after yasnippet)
|
||
#+end_src
|
||
|
||
** The =unravel-org.el= Org agenda settings
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:7fe87b83-2815-4617-a5f9-d3417dd9d248
|
||
:END:
|
||
|
||
[ Watch Prot's talk: [[https://protesilaos.com/codelog/2021-12-09-emacs-org-block-agenda/][Demo of my custom Org block agenda]] (2021-12-09). It has
|
||
changed a bit since then, but the idea is the same. ]
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-org.el"
|
||
;;;; agenda
|
||
(use-package org-agenda
|
||
:ensure nil
|
||
:bind
|
||
("C-c a" . org-agenda)
|
||
:config
|
||
|
||
;;;;; Basic agenda setup
|
||
(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)
|
||
)
|
||
#+end_src
|
||
|
||
** Finally, we provide the =unravel-org.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:62eb7ca3-2f79-45a6-a018-38238b486e98
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-org.el"
|
||
(provide 'unravel-org)
|
||
#+end_src
|
||
|
||
* The =unravel-shell.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:1E30455D-BB40-44E7-9FB1-92529FE03BDE
|
||
:END:
|
||
|
||
I use ~vterm~ for my shell inside Emacs, and at the moment, this section is about ~vterm~ configuration only.
|
||
|
||
** The =unravel-shell.el= section for ~vterm~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:2945DD5C-6AF1-4323-A287-A4F5C109471C
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-shell.el" :mkdirp yes
|
||
;;;; Vterm
|
||
(use-package vterm
|
||
:ensure t
|
||
:bind
|
||
("C-x m" . vterm)
|
||
:config
|
||
(setq vterm-shell (or (executable-find "fish") "/opt/homebrew/bin/fish")))
|
||
#+end_src
|
||
|
||
** Finally, we provide the =unravel-shell.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:83F2ADFB-D72B-4F8C-8B01-BA1A9CA4939C
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-shell.el"
|
||
(provide 'unravel-shell)
|
||
#+end_src
|
||
|
||
* The =unravel-langs.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:f44afb76-a1d7-4591-934d-b698cc79a792
|
||
:END:
|
||
|
||
** The =unravel-langs.el= settings for TAB
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:559713c8-0e1e-44aa-bca8-0caae01cc8bb
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-langs.el" :mkdirp yes
|
||
;;;; 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))
|
||
#+end_src
|
||
|
||
** The =unravel-langs.el= settings ~show-paren-mode~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:7cd21ea6-c5d8-4258-999d-ad94cac2d8bf
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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!
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-langs.el"
|
||
;;;; Parentheses (show-paren-mode)
|
||
(use-package paren
|
||
:ensure nil
|
||
:hook (prog-mode . show-paren-local-mode)
|
||
:config
|
||
(setq show-paren-style 'mixed)
|
||
(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
|
||
#+end_src
|
||
|
||
** The =unravel-langs.el= settings for ~eldoc~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:a5773a39-a78f-43fa-8feb-669492c1d5a9
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-langs.el"
|
||
;;;; 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.
|
||
#+end_src
|
||
|
||
** The =unravel-langs.el= settings for ~eglot~ (LSP client)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:92258aa8-0d8c-4c12-91b4-5f44420435ce
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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
|
||
([[#h:804b858f-7913-47ef-aaf4-8eef5b59ecb4][The =unravel-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~
|
||
([[#h:df6d1b52-0306-4ace-9099-17dded11fbed][The =unravel-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~)
|
||
and =M-,= (~xref-go-back~).
|
||
|
||
...
|
||
|
||
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)
|
||
#+end_quote
|
||
|
||
I use ~eglot~ as the main LSP entry point, and as such, I have key-bindings for the common functionality implemented by ~eglot~.
|
||
|
||
Note that demanding ~eglot~ is not a mistake. I want it loaded so that I can advice functions for different programming langauges. For example, Python uses ~emacs-pet~ for tracking the environment of each project, and this needs to load after ~eglot~ so that it can advice the necessary functions.
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-langs.el"
|
||
;;;; Eglot (built-in client for the language server protocol)
|
||
(use-package eglot
|
||
:ensure nil
|
||
:demand t ; Not a mistake, we need to load Eglot elisp code before
|
||
; we open any Python file.
|
||
:functions (eglot-ensure)
|
||
:commands (eglot)
|
||
:bind
|
||
( :map eglot-mode-map
|
||
("C-c e r" . eglot-rename)
|
||
("C-c e o" . eglot-code-action-organize-imports)
|
||
("C-c e d" . eldoc)
|
||
("C-c e c" . eglot-code-actions)
|
||
("C-c e f" . eglot-format)
|
||
;; Since eglot plugs into flymake anyway
|
||
("C-c e l" . flymake-show-buffer-diagnostics))
|
||
:config
|
||
(setq eglot-sync-connect nil)
|
||
(setq eglot-autoshutdown t)
|
||
(setq eglot-extend-to-xref t))
|
||
#+end_src
|
||
|
||
** The =unravel-langs.el= settings for ~markdown-mode~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:c9063898-07ae-4635-8853-bb5f4bbab421
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-langs.el"
|
||
;;; Markdown (markdown-mode)
|
||
(use-package markdown-mode
|
||
:ensure t
|
||
:defer t
|
||
:config
|
||
(setq markdown-fontify-code-blocks-natively t))
|
||
#+end_src
|
||
|
||
** The =unravel-langs.el= settings for ~csv-mode~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:bae58479-86c1-410f-867e-c548def65b1c
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-langs.el"
|
||
;;; csv-mode
|
||
(use-package csv-mode
|
||
:ensure t
|
||
:commands (csv-align-mode))
|
||
#+end_src
|
||
|
||
** The =unravel-langs.el= settings for spell checking (~flyspell~)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:115806c4-88b0-43c1-8db2-d9d8d20a5c17
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-langs.el"
|
||
;;; Flyspell
|
||
(use-package flyspell
|
||
:ensure nil
|
||
:bind
|
||
( :map flyspell-mode-map
|
||
("C-;" . nil)
|
||
:map flyspell-mouse-map
|
||
("<mouse-3>" . flyspell-correct-word))
|
||
:config
|
||
(setq flyspell-issue-message-flag nil)
|
||
(setq flyspell-issue-welcome-flag nil)
|
||
(setq ispell-program-name "aspell")
|
||
(setq ispell-dictionary "en_GB"))
|
||
#+end_src
|
||
|
||
** The =unravel-langs.el= settings for code linting (~flymake~)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:df6d1b52-0306-4ace-9099-17dded11fbed
|
||
:END:
|
||
|
||
#+begin_quote
|
||
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 ([[#h:92258aa8-0d8c-4c12-91b4-5f44420435ce][The =unravel-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.
|
||
#+end_quote
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-langs.el"
|
||
;;; Flymake
|
||
(use-package flymake
|
||
:ensure nil
|
||
:bind
|
||
( :map flymake-mode-map
|
||
("C-c ! s" . flymake-start)
|
||
("C-c ! l" . flymake-show-buffer-diagnostics) ; Emacs28
|
||
("C-c ! L" . 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))
|
||
(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))
|
||
#+end_src
|
||
|
||
** The =unravel-langs.el= settings for ~outline-minor-mode~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:ffff5f7b-a62b-4d4a-ae29-af75402e5c35
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-langs.el"
|
||
;;; 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
|
||
#+end_src
|
||
|
||
** The =unravel-langs.el= settings for ~dictionary~
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:f91563d8-f176-4555-b45b-ece56de03279
|
||
:END:
|
||
|
||
Use the entry point ~M-x dictionary-search~
|
||
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-langs.el"
|
||
;;;; `dictionary'
|
||
(use-package dictionary
|
||
:ensure nil
|
||
:config
|
||
(setq dictionary-server "dict.org"
|
||
dictionary-default-popup-strategy "lev" ; read doc string
|
||
dictionary-create-buttons nil
|
||
dictionary-use-single-buffer t))
|
||
#+end_src
|
||
|
||
** The =unravel-langs.el= settings for ~denote~ (notes and file-naming)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:e86a66dc-7ef9-4f09-ad7e-946de2034e8d
|
||
:END:
|
||
|
||
#+begin_quote
|
||
This is another one of my packages and is extended by my
|
||
~consult-denote~ package ([[#h:ee82e629-fb05-4c75-9175-48a760a25691][The =unravel-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.
|
||
#+end_quote
|
||
|
||
Prot is the developer and maintainer of this package.
|
||
|
||
+ Package name (GNU ELPA): ~denote~
|
||
+ Official manual: <https://protesilaos.com/emacs/denote>
|
||
+ Change log: <https://protesilaos.com/emacs/denote-changelog>
|
||
+ Git repositories:
|
||
- GitHub: <https://github.com/protesilaos/denote>
|
||
- GitLab: <https://gitlab.com/protesilaos/denote>
|
||
+ 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.
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-langs.el"
|
||
;;; 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 d n" . denote-create-note)
|
||
("C-c d N" . denote-silo-extras-select-silo-then-command)
|
||
("C-c d o" . denote-open-or-create)
|
||
("C-c d O" . denote-silo-extras-open-or-create)
|
||
("C-c d l" . denote-link-or-create)
|
||
("C-c d L" . denote-link-after-creating-with-command)
|
||
;; 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 d 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'.
|
||
("C-c d j" . denote-journal-extras-new-entry)
|
||
("C-c d s" . denote-sort-dired)
|
||
:map text-mode-map
|
||
("C-c d B" . denote-backlinks)
|
||
("C-c d b" . denote-find-backlink)
|
||
;; Also see `denote-rename-file' further above.
|
||
("C-c d R" . denote-rename-file-using-front-matter)
|
||
("C-c d k" . denote-rename-file-keywords)
|
||
:map org-mode-map
|
||
("C-c d h" . denote-org-extras-link-to-heading)
|
||
("C-c d d l" . denote-org-extras-dblock-insert-links)
|
||
("C-c d d b" . denote-org-extras-dblock-insert-backlinks)
|
||
("C-c d d m" . denote-org-extras-dblock-insert-missing-links)
|
||
;; 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-A" . denote-dired-rename-marked-files-add-keywords)
|
||
("C-c C-d C-K" . denote-dired-rename-marked-files-remove-keywords)
|
||
("C-c C-d C-f" . denote-dired-rename-marked-files-using-front-matter))
|
||
:config
|
||
(require 'denote-silo-extras)
|
||
(require 'denote-journal-extras)
|
||
(require 'denote-org-extras)
|
||
|
||
;; Remember to check the doc strings of those variables.
|
||
(setq denote-directory (expand-file-name "~/Tresors/Documents/diary/notes"))
|
||
(setq denote-infer-keywords t)
|
||
(setq denote-sort-keywords t)
|
||
(setq denote-excluded-directories-regexp "data") ; external data related to headings is stored in these directories (web archives)
|
||
(setq denote-date-format nil) ; read its doc string
|
||
(setq denote-date-prompt-use-org-read-date t)
|
||
(setq denote-prompts '(title keywords subdirectory signature))
|
||
|
||
(setq denote-rename-confirmations nil) ; CAREFUL with this if you are not familiar with Denote!
|
||
(setq denote-save-buffers t)
|
||
(setq denote-rename-buffer-format "[D] %s %t%b")
|
||
;; 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)
|
||
|
||
(setq denote-buffer-has-backlinks-string " (<--->)")
|
||
(setq denote-backlinks-show-context t)
|
||
(setq denote-org-store-link-to-heading t)
|
||
|
||
;; Journal settings
|
||
(setq denote-journal-extras-keyword "")
|
||
|
||
;; I use Yasnippet to expand these into a better template.
|
||
(add-to-list 'denote-templates '(reference-note . "reference"))
|
||
(add-to-list 'denote-templates '(morning . "morningpage"))
|
||
(add-to-list 'denote-templates '(emotion . "emotion"))
|
||
(add-to-list 'denote-templates '(insight . "insight"))
|
||
(add-to-list 'denote-templates '(weekly_intentions . "weekint"))
|
||
(add-to-list 'denote-templates '(weekly_report . "weekrpt"))
|
||
(add-to-list 'denote-templates '(sketch . "sketch"))
|
||
|
||
;; Front-matter for Org files
|
||
(setq denote-org-front-matter
|
||
":PROPERTIES:
|
||
:ID: %4$s
|
||
:CREATED: %2$s
|
||
:END:
|
||
,#+title: %1$s
|
||
,#+filetags: %3$s
|
||
,#+date: %2$s
|
||
,#+identifier: %4$s
|
||
\n"))
|
||
|
||
(defun vedang/denote-publishing-extras-new-blog-entry (&optional date)
|
||
"Create a new blog entry.
|
||
|
||
With optional DATE as a prefix argument, prompt for a date. If
|
||
`denote-date-prompt-use-org-read-date' is non-nil, use the Org
|
||
date selection module.
|
||
|
||
When called from Lisp DATE is a string and has the same format as
|
||
that covered in the documentation of the `denote' function. It
|
||
is internally processed by `denote-parse-date'."
|
||
(interactive (list (when current-prefix-arg (denote-date-prompt))))
|
||
(let ((internal-date (denote-parse-date date))
|
||
(denote-directory (file-name-as-directory (expand-file-name "published" denote-directory))))
|
||
(denote
|
||
(denote-title-prompt)
|
||
'("draft")
|
||
nil nil date
|
||
;; See YASnippet
|
||
"fullblog")))
|
||
|
||
(defun vedang/denote-publishing-extras-new-microblog-entry (&optional date)
|
||
"Create a new microblog entry.
|
||
Set the title of the new entry according to the value of the user option
|
||
`denote-journal-extras-title-format'.
|
||
|
||
With optional DATE as a prefix argument, prompt for a date. If
|
||
`denote-date-prompt-use-org-read-date' is non-nil, use the Org
|
||
date selection module.
|
||
|
||
When called from Lisp DATE is a string and has the same format as
|
||
that covered in the documentation of the `denote' function. It
|
||
is internally processed by `denote-parse-date'."
|
||
(interactive (list (when current-prefix-arg (denote-date-prompt))))
|
||
(let ((internal-date (denote-parse-date date))
|
||
(denote-directory (file-name-as-directory (expand-file-name "published" denote-directory))))
|
||
(denote
|
||
(denote-journal-extras-daily--title-format internal-date)
|
||
'("draft" "microblog")
|
||
nil nil date
|
||
;; See YASnippet
|
||
"microblog")))
|
||
|
||
(defun vedang/denote-link-ol-get-id ()
|
||
"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.
|
||
|
||
If we are creating a new ID, add a CREATED property with the current
|
||
timestamp as well.
|
||
|
||
This function is based on `denote-link-ol-get-id', with minor
|
||
modifications."
|
||
(interactive)
|
||
(let* ((pos (point))
|
||
(id (org-entry-get pos "CUSTOM_ID"))
|
||
(created (org-entry-get pos "CREATED")))
|
||
(if (and (stringp id) (string-match-p "\\S-" id))
|
||
id
|
||
(setq id (org-id-new "h"))
|
||
(org-entry-put pos "CUSTOM_ID" id))
|
||
(when (not created)
|
||
(setq created (format-time-string (org-time-stamp-format t t) (current-time)))
|
||
(org-entry-put pos "CREATED" created))
|
||
id))
|
||
|
||
(defun vedang/denote--split-luhman-sig (signature)
|
||
"Split numbers and letters in Luhmann-style SIGNATURE string."
|
||
(replace-regexp-in-string
|
||
"\\([a-zA-Z]+?\\)\\([0-9]\\)" "\\1=\\2"
|
||
(replace-regexp-in-string
|
||
"\\([0-9]+?\\)\\([a-zA-Z]\\)" "\\1=\\2"
|
||
signature)))
|
||
|
||
(defun vedang/denote--pad-sig (signature)
|
||
"Create a new signature with padded spaces for all components"
|
||
(combine-and-quote-strings
|
||
(mapcar
|
||
(lambda (x)
|
||
(string-pad x 5 32 t))
|
||
(split-string (vedang/denote--split-luhman-sig signature) "=" t))
|
||
"="))
|
||
|
||
(defun vedang/denote-sort-for-signatures (sig1 sig2)
|
||
"Return non-nil if SIG1 is smaller that SIG2.
|
||
|
||
Perform the comparison with `string<'."
|
||
(string< (vedang/denote--pad-sig sig1) (vedang/denote--pad-sig sig2)))
|
||
|
||
(use-package denote
|
||
:ensure t
|
||
:bind
|
||
( :map global-map
|
||
;; Bindings to personal functions (defined above)
|
||
("C-c d p m" . vedang/denote-publishing-extras-new-microblog-entry)
|
||
("C-c d p b" . vedang/denote-publishing-extras-new-blog-entry))
|
||
:config
|
||
(setq denote-sort-signature-comparison-function #'vedang/denote-sort-for-signatures))
|
||
#+end_src
|
||
|
||
*** The =unravel-langs.el= integration between Consult and Denote (~consult-denote~)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:ee82e629-fb05-4c75-9175-48a760a25691
|
||
:END:
|
||
|
||
#+begin_quote
|
||
This is another package of mine which extends my ~denote~ package
|
||
([[#h:e86a66dc-7ef9-4f09-ad7e-946de2034e8d][The =unravel-langs.el= settings for ~denote~ (notes and file-naming)]]).
|
||
|
||
This is glue code to integrate ~denote~ with Daniel Mendler's
|
||
~consult~ ([[#h:22e97b4c-d88d-4deb-9ab3-f80631f9ff1d][The =unravel-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.
|
||
#+end_quote
|
||
|
||
Prot is the developer of this package.
|
||
|
||
+ Package name (GNU ELPA): ~consult-denote~
|
||
+ Official manual: not available yet.
|
||
+ Git repositories:
|
||
+ GitHub: <https://github.com/protesilaos/consult-denote>
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-langs.el"
|
||
(use-package consult-denote
|
||
:ensure t
|
||
:bind
|
||
(("C-c d f" . consult-denote-find)
|
||
("C-c d g" . consult-denote-grep))
|
||
:config
|
||
(consult-denote-mode 1))
|
||
#+end_src
|
||
|
||
** The =unravel-langs.el= settings for ~paredit~ (paren matching)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:885F9DED-E9C9-4B5B-9FE0-1A33CBD23126
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-langs.el"
|
||
(use-package paredit
|
||
:ensure t
|
||
:bind ( :map paredit-mode-map
|
||
("C-o" . paredit-open-round)
|
||
("M-D" . paredit-splice-sexp)
|
||
("C-A-d" . paredit-forward-down)
|
||
("C-A-u" . paredit-backward-up)
|
||
;; Unbind things that I don't need
|
||
("M-s" . nil) ; used for search related keybindings
|
||
("M-?" . nil)) ; `xref-find-references` uses it.
|
||
:hook ((lisp-data-mode lisp-mode clojure-mode clojure-ts-mode cider-repl-mode inferior-emacs-lisp-mode) . paredit-mode))
|
||
#+end_src
|
||
|
||
** The =unravel-langs.el= settings for ~apheleia~ (code formatting)
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:07B0E6F4-050E-4A7D-B489-E919E4887FF5
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-langs.el"
|
||
(use-package apheleia
|
||
:ensure t
|
||
:demand t
|
||
:config
|
||
(apheleia-global-mode +1)
|
||
(with-eval-after-load 'apheleia-formatters
|
||
(push '(zprint . ("zprint")) apheleia-formatters)))
|
||
#+end_src
|
||
|
||
** The =unravel-langs.el= section for Python
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:EA5EA223-F97D-4EE9-8663-99822A037618
|
||
:CREATED: [2024-11-21 Thu 22:51]
|
||
:END:
|
||
|
||
The built-in Python mode for Emacs goes a long way. I use the following stack when programming Python:
|
||
|
||
- =poetry= for package and venv management
|
||
- =pylsp= as the language server
|
||
- =ruff= as the linting and formatting tool
|
||
|
||
Run the following commands in every virtualenv environment to setup the necessary developer tooling:
|
||
|
||
- =poetry add ruff python-lsp-server python-lsp-ruff --group dev=
|
||
+ Ruff is an extremely fast Python linter, and code formatter, written in Rust. Ruff is also a langauge server, but it only provides functionality related to formating and linting. As it adds more over time (like go-to definition), I may make it my primary language server
|
||
+ Python LSP Server (provides the binary ~pylsp~) is the defacto language server for Python.
|
||
+ =python-lsp-ruff= provides tight integration between ~pylsp~ and ~ruff~, enabling the language server to use ruff for it's linting and formatting capabilities.
|
||
- =poetry add pytest --group test=
|
||
|
||
Poetry takes care of setting up the venv properly, so if you replace the default commands with poetry versions, you are good to go. In practice, this means:
|
||
|
||
- Use ~C-u C-c C-p~ command (=run-python=, with an argument) to start the Inferior Python Shell, instead of ~C-c C-p~.
|
||
- This will prompt you for a command, with the default value being =python3 -i=. Change it to =poetry run python3 -i=.
|
||
- Modify the ~C-c C-v~ command (=python-check=) to =poetry run ruff check <filename>=
|
||
|
||
I run ~eglot~ on demand, that is, I do not start a language server automatically when I open a Python file. This ensures that ~emacs-pet~ can setup the venv for the project and use executables only from the virtualenv. (Note: We also setup the ~emacs-pet~ hook to be the first thing that runs when python mode is activated, so automatically starting ~eglot~ by uncommenting the ~eglot-ensure~ hook below should also work. But running manually is just how I prefer it.)
|
||
|
||
If you want to start the language server automatically you need to:
|
||
|
||
- Install ~python-language-server~ and ~ruff~ globally, so that it's always available to Emacs.
|
||
- =brew install pipx=
|
||
- =pipx install ruff python-language-server=
|
||
- Uncomment the ~:hook~ in the Python ~use-package~ form below
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-langs.el"
|
||
;;;; Configuration for Python Programming
|
||
|
||
(use-package python
|
||
:ensure nil
|
||
:ensure-system-package (dasel sqlite3)
|
||
;;; Uncomment this if you want Eglot to start automatically. I don't
|
||
;;; recommend it because it does not give you time to activate the
|
||
;;; appropriate VirtualEnv and get the best of the situation.
|
||
;; :hook ((python-base-mode . eglot-ensure))
|
||
:config
|
||
(setq python-shell-dedicated 'project)
|
||
;; Apheleia is an Emacs package for formatting code as you save
|
||
;; it. Here we are asking Apheleia to use Ruff for formatting our
|
||
;; Python code.
|
||
(with-eval-after-load 'apheleia
|
||
(setf (alist-get 'python-mode apheleia-mode-alist)
|
||
'(ruff-isort ruff))
|
||
(setf (alist-get 'python-ts-mode apheleia-mode-alist)
|
||
'(ruff-isort ruff)))
|
||
(with-eval-after-load 'eglot
|
||
(require 'vedang-pet)
|
||
;; The -10 here is a way to define the priority of the function in
|
||
;; the list of hook functions. We want `pet-mode' to run before
|
||
;; any other configured hook function.
|
||
(add-hook 'python-base-mode-hook #'pet-mode -10)))
|
||
#+end_src
|
||
|
||
*** Tooling I have tried and rejected or failed to setup correctly
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:AA769AC9-18D9-4971-81C9-8DF7AD920013
|
||
:END:
|
||
|
||
- Auto-virtualenv: For virtualenv setup and detection
|
||
- Pyvenv's WORKON_HOME: WORKON_HOME is useful if you keep all your venvs in the same directory. This is not generally what most people do.
|
||
- Config: ~(setenv "WORKON_HOME" "~/.cache/venvs/")~
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package pyvenv
|
||
:ensure t
|
||
:after python
|
||
:commands (pyvenv-create pyvenv-workon pyvenv-activate pyvenv-deactivate)
|
||
:hook
|
||
((python-base-mode . pyvenv-mode)))
|
||
|
||
(use-package pet ;; Python Environment Tracker
|
||
:ensure t
|
||
:after (python eglot)
|
||
:ensure-system-package (dasel sqlite3)
|
||
:config
|
||
;; We use `add-hook' instead of :hook to be able to specify the -10
|
||
;; (call as early as possible)
|
||
(add-hook 'python-base-mode-hook #'pet-mode -10))
|
||
|
||
(use-package auto-virtualenv
|
||
:ensure t
|
||
:after python
|
||
:config
|
||
(setq auto-virtualenv-verbose t)
|
||
(auto-virtualenv-setup))
|
||
#+end_src
|
||
|
||
** The =unravel-langs.el= section for Ziglang
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:4C1D1E7E-7FA1-4D76-A6CF-6D89A10376B6
|
||
:CREATED: [2024-11-27 Wed 22:51]
|
||
:END:
|
||
|
||
I use the Emacs major mode ~zig-mode~, along with the Ziglang langauge server ~zls~ (via ~eglot~) for all my Zig programming requirements
|
||
|
||
To install ~zig~ and ~zls~ on MacOS:
|
||
|
||
=brew install zig zls=
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-langs.el"
|
||
;;;; Configuration for Zig Programming
|
||
|
||
(use-package zig-mode
|
||
:ensure t
|
||
;;; Uncomment this if you want Eglot to start automatically. I don't
|
||
;;; recommend it, but that's just me.
|
||
;; :hook ((zig-mode . eglot-ensure))
|
||
)
|
||
#+end_src
|
||
|
||
** Finally, we provide the =unravel-langs.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:924224F1-61A0-4CCD-A3C3-4F230D3CF0A0
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-langs.el"
|
||
(provide 'unravel-langs)
|
||
#+end_src
|
||
|
||
* The =unravel-personal.el= module
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:0EAB9FBA-98AD-4E0E-BC01-71D1EC920B8D
|
||
:END:
|
||
I want this section to contain all the personal configuration that anyone using this repo should modify. But I don't know how to do this properly at the moment, so just noting things here until then.
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-essentials.el"
|
||
(use-package startup
|
||
:ensure nil
|
||
:config
|
||
(setq user-mail-address "vedang@unravel.tech"))
|
||
|
||
#+end_src
|
||
|
||
** My personal settings for ~org-capture~
|
||
|
||
#+begin_src emacs-lisp :tangle "unravel-modules/unravel-org.el"
|
||
;;;; org-capture
|
||
(use-package org-capture
|
||
:ensure nil
|
||
:config
|
||
;;; Default definitions for variables used in capture templates
|
||
(when (not (boundp 'org-blogpost-file))
|
||
(defvar org-blogpost-file org-default-notes-file
|
||
"File in which blogposts and microblogposts are stored."))
|
||
(when (not (boundp 'org-company-file))
|
||
(defvar org-company-file org-default-notes-file
|
||
"File in which company documentation is stored."))
|
||
|
||
;;; *CRITICAL NOTE* Read before modifying the push stack below:
|
||
;; Pushing to capture templates is a stack. What goes in first shows
|
||
;; up at the bottom of the capture templates list.
|
||
|
||
;;; Templates for thinking tools
|
||
(push '("T" "Templates for Helping Me Think") org-capture-templates)
|
||
;; Capture a decision that you've taken, for review and reflection later.
|
||
(push `("Td" "Decision Journal" entry
|
||
(file+headline org-default-notes-file "Helping Me Think")
|
||
(file ,(expand-file-name "capture-templates/thinking.decision.org"))
|
||
:prepend nil
|
||
:clock-in t
|
||
:clock-resume t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
;; Create a Current Reality Tree for a problem
|
||
(push `("Tc" "Current Reality Tree" entry
|
||
(file+headline org-default-notes-file "Helping Me Think")
|
||
(file ,(expand-file-name "capture-templates/thinking.crt.org"))
|
||
:prepend nil
|
||
:clock-in t
|
||
:clock-resume t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
;; Create an Evaporating Cloud for a problem
|
||
(push `("Te" "Evaporating Cloud" entry
|
||
(file+headline org-default-notes-file "Helping Me Think")
|
||
(file ,(expand-file-name "capture-templates/thinking.ec.org"))
|
||
:prepend nil
|
||
:clock-in t
|
||
:clock-resume t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
;; Create a Future Reality Tree for a problem
|
||
(push `("Tf" "Future Reality Tree" entry
|
||
(file+headline org-default-notes-file "Helping Me Think")
|
||
(file ,(expand-file-name "capture-templates/thinking.frt.org"))
|
||
:prepend nil
|
||
:clock-in t
|
||
:clock-resume t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
;; Create a Prerequisite Tree for a problem
|
||
(push `("Tp" "Prerequisite Tree" entry
|
||
(file+headline org-default-notes-file "Helping Me Think")
|
||
(file ,(expand-file-name "capture-templates/thinking.prt.org"))
|
||
:prepend nil
|
||
:clock-in t
|
||
:clock-resume t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
;; Create a Transition Tree for a problem
|
||
(push `("Tt" "Transition Tree" entry
|
||
(file+headline org-default-notes-file "Helping Me Think")
|
||
(file ,(expand-file-name "capture-templates/thinking.trt.org"))
|
||
:prepend nil
|
||
:clock-in t
|
||
:clock-resume t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
;; Capture a new Business idea for sketching out / thinking through
|
||
(push `("Tb" "Business Canvas" entry
|
||
(file+headline org-default-notes-file "Helping Me Think")
|
||
(file ,(expand-file-name "capture-templates/business.canvas.org"))
|
||
:prepend nil
|
||
:clock-in t
|
||
:clock-resume t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
;; Capture a customer persona, note that this is always captured in
|
||
;; the current clocking task, and is something I should do under the
|
||
;; business canvas.
|
||
(push `("TP" "Customer Persona (under Business Canvas)" entry
|
||
(clock)
|
||
(file ,(expand-file-name "capture-templates/business.customer.persona.org"))
|
||
:prepend nil
|
||
:clock-in t
|
||
:clock-resume t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
;; Capture a customer journey through your product, note that this is
|
||
;; always captured in the current clocking task
|
||
(push `("Tj" "Customer Journey (under Business Canvas)" entry
|
||
(clock)
|
||
(file ,(expand-file-name "capture-templates/business.customer.journey.org"))
|
||
:prepend nil
|
||
:clock-in t
|
||
:clock-resume t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
;;; Templates for capturing data about myself on a day-to-day basis
|
||
(push '("d" "Templates for Capturing Data (personal)") org-capture-templates)
|
||
|
||
;; Capture weight / food. This seems hard to get into a laptop habit.
|
||
;; This is the kind of quantitative life that a mobile solution would
|
||
;; have helped with.
|
||
|
||
(push `("dw" "Weight Tracking" entry
|
||
(file+olp+datetree org-default-notes-file)
|
||
(file ,(expand-file-name "capture-templates/bodylog.weight.org"))
|
||
:clock-in t
|
||
:clock-resume t
|
||
:immediate-finish t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
(push `("df" "Food Tracking" entry
|
||
(file+olp+datetree org-default-notes-file)
|
||
(file ,(expand-file-name "capture-templates/bodylog.food.org"))
|
||
:clock-in t
|
||
:clock-resume t
|
||
:immediate-finish t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
(push `("dd" "Downtime Tracking" entry
|
||
(file+olp+datetree org-default-notes-file)
|
||
(file ,(expand-file-name "capture-templates/bodylog.dt.org"))
|
||
:clock-in t
|
||
:clock-resume t
|
||
:immediate-finish t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
;;; Templates for capturing build in public ideas
|
||
(push '("b" "Templates for Capturing Build in Public") org-capture-templates)
|
||
|
||
;; Capture Micro-blogging
|
||
(push `("bm" "New Microblogging entry" entry
|
||
(file+olp+datetree org-blogpost-file "Microblogging")
|
||
(file ,(expand-file-name "capture-templates/microblog.org"))
|
||
:prepend nil
|
||
:clock-in t
|
||
:clock-resume t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
;; New blogpost idea
|
||
(push `("bb" "New Blogpost entry" entry
|
||
(file+headline org-blogpost-file "Meta: Blogposts to write")
|
||
(file ,(expand-file-name "capture-templates/todo.org"))
|
||
:prepend nil
|
||
:clock-in t
|
||
:clock-resume t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
;;; Templates for when I want to capture specific feedback about something
|
||
(push '("f" "Templates for Feedback, Reflection, Journaling") org-capture-templates)
|
||
|
||
;; Capture feedback for people I am working with
|
||
(push `("fp" "Feedback for People I'm working with" item
|
||
(file+headline org-company-file "Feedback")
|
||
(file ,(expand-file-name "capture-templates/feedback.others.org"))
|
||
:clock-in t
|
||
:clock-resume t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
;; The monthly newsletter to send to investors, friends and mentors
|
||
(push `("fn" "Company Newsletters" entry
|
||
(file+headline org-company-file "Company Newsletters")
|
||
(file ,(expand-file-name "capture-templates/business.updates.org"))
|
||
:prepend nil
|
||
:clock-in t
|
||
:clock-resume t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
;; Capture suggestions / ideas from other people, which can be
|
||
;; expanded into actual projects later.
|
||
(push `("fs" "Ideas and Suggestions" entry
|
||
(file+headline org-company-file "Ideas and Suggestions")
|
||
(file ,(expand-file-name "capture-templates/suggestion.org"))
|
||
:prepend t
|
||
:clock-in t
|
||
:clock-resume t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
;;; Templates for planning on a day-to-day basis
|
||
(push '("p" "Templates for Planning") org-capture-templates)
|
||
|
||
;; Deliberately plan out and make a routine out of start of day and
|
||
;; end of day activities.
|
||
|
||
(push `("ps" "The Start of Day Planning Routine" entry
|
||
(file+olp+datetree org-default-notes-file)
|
||
(file ,(expand-file-name "capture-templates/workday.start.org"))
|
||
:prepend nil
|
||
:clock-in t
|
||
:clock-resume t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
(push `("pe" "The End of Day Reflection Routine" entry
|
||
(file+olp+datetree org-default-notes-file)
|
||
(file ,(expand-file-name "capture-templates/workday.end.org"))
|
||
:prepend nil
|
||
:clock-in t
|
||
:clock-resume t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
(push `("pn" "The Next Day Intentions Routine" entry
|
||
(file+olp+datetree org-default-notes-file)
|
||
(file ,(expand-file-name "capture-templates/workday.next.org"))
|
||
:prepend nil
|
||
:clock-in t
|
||
:clock-resume t
|
||
:empty-lines 1)
|
||
org-capture-templates)
|
||
|
||
;;; Templates for capturing meetings, events, something happening at this time
|
||
(push '("m" "Templates for Capturing Meetings or Events") org-capture-templates)
|
||
|
||
;; Capture an upcoming meeting or one that has already happened
|
||
(push `("mp" "Meeting some other day" entry
|
||
(file+olp+datetree org-default-notes-file)
|
||
(file ,(expand-file-name "capture-templates/meeting.org"))
|
||
:prepend t
|
||
:clock-in t
|
||
:clock-resume t
|
||
:time-prompt t)
|
||
org-capture-templates)
|
||
|
||
;; Capture notes for an ongoing meeting or a meeting that's already
|
||
;; happened.
|
||
(push `("mn" "Meeting today" entry
|
||
(file+olp+datetree org-default-notes-file)
|
||
(file ,(expand-file-name "capture-templates/meeting.org"))
|
||
:prepend t
|
||
:clock-in t
|
||
:clock-resume t)
|
||
org-capture-templates)
|
||
|
||
;;; Templates for Capturing Tasks
|
||
(push '("t" "Templates for Capturing Tasks") org-capture-templates)
|
||
|
||
;; Set up a new habit for tracking. This should be refiled to the
|
||
;; correct location later.
|
||
(push `("th" "Habit" entry
|
||
(file+headline org-default-notes-file "My Habit Tracker")
|
||
(file ,(expand-file-name "capture-templates/habit.org")))
|
||
org-capture-templates)
|
||
|
||
;; One-click Capture for replying to emails from notmuch. Creates a
|
||
;; task to remind you that you need to reply to this email.
|
||
(push `("tr" "Respond to email" entry
|
||
(file+olp+datetree org-default-notes-file)
|
||
(file ,(expand-file-name "capture-templates/reply.org"))
|
||
:clock-in t
|
||
:clock-resume t
|
||
:immediate-finish t)
|
||
org-capture-templates)
|
||
|
||
;; One-click capture of links from the clipboard. Used in conjunction
|
||
;; with `org-protocol', or as a stand-alone to capture links.
|
||
(push `("tw" "Website Link Immediate Capture" entry
|
||
(file+olp org-default-notes-file "Links Captured from the Browser")
|
||
(file ,(expand-file-name "capture-templates/website.org"))
|
||
:immediate-finish t)
|
||
org-capture-templates)
|
||
|
||
;; A more nuanced capture for browser links, which I use for cleaning
|
||
;; out my browser 2/3 times a week.
|
||
(push `("tl" "Website Link Pinboard Capture" entry
|
||
(file+olp org-default-notes-file "Links Captured from the Browser")
|
||
(file ,(expand-file-name "capture-templates/pinboard.capture.org"))
|
||
:clock-in t
|
||
:clock-resume t
|
||
:immediate-finish t)
|
||
org-capture-templates)
|
||
|
||
;; Capture a task where someone expects me to communicate when it's done
|
||
(push `("tj" "Jira or External-facing Task" entry
|
||
(file+olp+datetree org-default-notes-file)
|
||
(file ,(expand-file-name "capture-templates/jira.org"))
|
||
:clock-in t
|
||
:clock-resume t)
|
||
org-capture-templates)
|
||
|
||
;; One-click Capture for Tasks. Captures the task immediately and gets
|
||
;; out of your way.
|
||
(push `("ti" "Simple Task Immediate Finish" entry
|
||
(file+olp+datetree org-default-notes-file)
|
||
(file ,(expand-file-name "capture-templates/todo.org"))
|
||
:clock-in t
|
||
:clock-resume t
|
||
:immediate-finish t)
|
||
org-capture-templates))
|
||
#+end_src
|
||
|
||
|
||
* Custom libraries
|
||
** The =prot-common.el= library
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:3fccfadf-22e9-457f-b9fd-ed1b48600d23
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "custom-lisp/prot-common.el" :mkdirp yes
|
||
;;; 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
|
||
#+end_src
|
||
|
||
** The =prot-embark.el= library
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:fb034be5-c316-4c4f-a46f-cebcab332a47
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "custom-lisp/prot-embark.el" :mkdirp yes
|
||
;;; 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
|
||
#+end_src
|
||
|
||
** The =prot-window.el= library
|
||
:PROPERTIES:
|
||
:CUSTOM_ID: h:35b8a0a5-c447-4301-a404-bc274596238d
|
||
:END:
|
||
|
||
#+begin_src emacs-lisp :tangle "custom-lisp/prot-window.el" :mkdirp yes
|
||
;;; 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
|
||
#+end_src
|
||
|
||
|
||
** The =vedang-pet.el= library
|
||
|
||
#+begin_src emacs-lisp :tangle "custom-lisp/vedang-pet.el" :mkdirp yes
|
||
;;; pet.el --- Executable and virtualenv tracker for python-mode -*- lexical-binding: t -*-
|
||
|
||
;; Author: Jimmy Yuen Ho Wong <wyuenho@gmail.com>
|
||
;; Maintainer: Jimmy Yuen Ho Wong <wyuenho@gmail.com>
|
||
;; Version: 3.1.0
|
||
;; Package-Requires: ((emacs "26.1") (f "0.6.0") (map "3.3.1") (seq "2.24"))
|
||
;; Homepage: https://github.com/wyuenho/emacs-pet/
|
||
;; Keywords: tools
|
||
|
||
|
||
;; 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:
|
||
|
||
;; __P__ython __E__xecutable __T__racker. Tracks downs the correct Python
|
||
;; executables from the various virtualenv management tools and assign them to
|
||
;; buffer local variables. The package to end all Emacs virtualenv packages.
|
||
|
||
;;; Code:
|
||
|
||
|
||
(require 'cl-lib)
|
||
(require 'f)
|
||
(require 'filenotify)
|
||
(require 'let-alist)
|
||
(require 'map)
|
||
(require 'pcase)
|
||
(require 'project)
|
||
(require 'python)
|
||
(require 'seq)
|
||
(require 'subr-x)
|
||
(require 'tramp)
|
||
|
||
(when (< emacs-major-version 27)
|
||
(require 'json))
|
||
|
||
(defgroup pet nil
|
||
"Customization group for `pet'."
|
||
:group 'python
|
||
:prefix "pet-")
|
||
|
||
(defcustom pet-debug nil
|
||
"Whether to turn on debug messages."
|
||
:group 'pet
|
||
:type 'boolean)
|
||
|
||
(defcustom pet-toml-to-json-program "dasel"
|
||
"Name of the program to convert TOML to JSON.
|
||
|
||
The program must accept input from STDIN and output a JSON to
|
||
STDOUT.
|
||
|
||
You can customize the arguments that will be passed to the
|
||
program by adjusting `pet-toml-to-json-program-arguments'"
|
||
:group 'pet
|
||
:type '(choice (const "dasel")
|
||
(const "tomljson")
|
||
(string :tag "Other")))
|
||
|
||
(defcustom pet-toml-to-json-program-arguments '("-f" "-" "-r" "toml" "-w" "json")
|
||
"Arguments for `pet-toml-to-json-program'."
|
||
:group 'pet
|
||
:type '(repeat string))
|
||
|
||
(defcustom pet-yaml-to-json-program "dasel"
|
||
"Name of the program to convert YAML to JSON.
|
||
|
||
The program must accept input from STDIN and output a JSON to
|
||
STDOUT.
|
||
|
||
You can customize the arguments that will be passed to the
|
||
program by adjusting `pet-yaml-to-json-program-arguments'"
|
||
:group 'pet
|
||
:type '(choice (const "dasel")
|
||
(const "yq")
|
||
(string :tag "Other")))
|
||
|
||
(defcustom pet-yaml-to-json-program-arguments '("-f" "-" "-r" "yaml" "-w" "json")
|
||
"Arguments for `pet-yaml-to-json-program'."
|
||
:group 'pet
|
||
:type '(repeat string))
|
||
|
||
(defcustom pet-find-file-functions '(pet-find-file-from-project-root
|
||
pet-locate-dominating-file
|
||
pet-find-file-from-project-root-recursively)
|
||
"Order in which `pet-find-file-from-project' should search for a config file.
|
||
|
||
Each function should take a file name as its sole argument and
|
||
return an absolute path to the file found in the current project
|
||
and nil otherwise."
|
||
:group 'pet
|
||
:type '(repeat (choice (const pet-find-file-from-project-root)
|
||
(const pet-locate-dominating-file)
|
||
(const pet-find-file-from-project-root-recursively)
|
||
function)))
|
||
|
||
(defcustom pet-venv-dir-names '(".venv" "venv" "env")
|
||
"Directory names to search for when looking for a virtualenv at the project root."
|
||
:group 'pet
|
||
:type '(repeat string))
|
||
|
||
|
||
|
||
(defun pet--executable-find (command &optional remote)
|
||
"Like Emacs 27's `executable-find', ignore REMOTE on Emacs 26.
|
||
|
||
See `executable-find' for the meaning of COMMAND and REMOTE."
|
||
(if (>= emacs-major-version 27)
|
||
(executable-find command remote)
|
||
(executable-find command)))
|
||
|
||
(defun pet-system-bin-dir ()
|
||
"Determine the correct script directory based on `system-type'."
|
||
(if (eq (if (file-remote-p default-directory)
|
||
(tramp-get-connection-property
|
||
(tramp-dissect-file-name default-directory)
|
||
"uname"
|
||
'windows-nt)
|
||
system-type)
|
||
'windows-nt)
|
||
"Scripts" "bin"))
|
||
|
||
(defun pet-report-error (err)
|
||
"Report ERR to the minibuffer.
|
||
|
||
Only reports to the minibuffer if `pet-debug' is non-nil."
|
||
(when pet-debug
|
||
(minibuffer-message (error-message-string err)))
|
||
nil)
|
||
|
||
(defun pet-project-root ()
|
||
"Return the path of root of the project.
|
||
|
||
If `projectile' is available, the function
|
||
`projectile-project-root' is used to find the project root.
|
||
Otherwise, `project-root' is used."
|
||
(or (and (functionp 'projectile-project-root)
|
||
(projectile-project-root))
|
||
(when-let ((project (project-current)))
|
||
(or (and (functionp 'project-root)
|
||
(expand-file-name (project-root project)))
|
||
(and (functionp 'project-roots)
|
||
(when-let ((root (car (project-roots project))))
|
||
(expand-file-name root)))))))
|
||
|
||
(defun pet-find-file-from-project-root (file)
|
||
"Find FILE from the current project's root.
|
||
|
||
FILE is a file name or a wildcard.
|
||
|
||
Return absolute path to FILE if found in the project root, nil
|
||
otherwise."
|
||
(when-let ((root (pet-project-root)))
|
||
(car (file-expand-wildcards (concat (file-name-as-directory root) file) t))))
|
||
|
||
(defun pet-locate-dominating-file (file)
|
||
"Find FILE by walking up `default-directory' until the current project's root.
|
||
|
||
FILE is a file name or a wildcard.
|
||
|
||
Return absolute path to FILE if found, nil otherwise."
|
||
(when-let* ((root (pet-project-root))
|
||
(dir (locate-dominating-file
|
||
default-directory
|
||
(lambda (dir)
|
||
(car
|
||
(file-expand-wildcards
|
||
(concat (file-name-as-directory dir) file))))))
|
||
(dir (expand-file-name dir)))
|
||
(when (string-prefix-p root dir)
|
||
(car (file-expand-wildcards (concat (file-name-as-directory dir) file) t)))))
|
||
|
||
(defun pet-find-file-from-project-root-recursively (file)
|
||
"Find FILE by recursively searching down from the current project's root.
|
||
|
||
FILE is a file name or a wildcard.
|
||
|
||
Return absolute path to FILE if found, nil otherwise."
|
||
(condition-case err
|
||
(when-let ((root (pet-project-root))
|
||
(fileset
|
||
(cond ((functionp 'projectile-dir-files)
|
||
(mapcar (apply-partially #'concat root)
|
||
(projectile-dir-files (pet-project-root))))
|
||
((functionp 'project-files)
|
||
(project-files (project-current)))
|
||
(t (directory-files-recursively
|
||
(pet-project-root)
|
||
(wildcard-to-regexp file))))))
|
||
(seq-find (lambda (f)
|
||
(string-match-p
|
||
(wildcard-to-regexp file)
|
||
(file-name-nondirectory f)))
|
||
(sort fileset 'string<)))
|
||
(error (pet-report-error err))))
|
||
|
||
(defun pet-find-file-from-project (file)
|
||
"Find FILE from the current project.
|
||
|
||
Try each function in `pet-find-file-functions' in order and
|
||
return the absolute path found by the first function, nil
|
||
otherwise."
|
||
(seq-some (lambda (fn) (funcall fn file)) pet-find-file-functions))
|
||
|
||
(defun pet-parse-json (str)
|
||
"Parse JSON STR to an alist. Arrays are converted to lists."
|
||
(if (functionp 'json-parse-string)
|
||
(json-parse-string str :object-type 'alist :array-type 'list)
|
||
(let ((json-array-type 'list))
|
||
(json-read-from-string str))))
|
||
|
||
(defun pet-parse-config-file (file-path)
|
||
"Parse a configuration file at FILE-PATH into JSON alist."
|
||
(condition-case err
|
||
(let* ((ext (downcase (or (file-name-extension file-path) "")))
|
||
(auto-mode-alist-matcher (lambda (entry)
|
||
(pcase-let ((`(,pat . ,mode) entry))
|
||
(when (string-match-p pat file-path)
|
||
mode))))
|
||
(mode (seq-some auto-mode-alist-matcher auto-mode-alist))
|
||
(json-p (or (equal ext "json")
|
||
(eq 'json-mode mode)
|
||
(eq 'json-ts-mode mode)
|
||
(eq 'jsonian-mode mode)))
|
||
(toml-p (or (equal ext "toml")
|
||
(eq 'conf-toml-mode mode)
|
||
(eq 'toml-ts-mode mode)))
|
||
(yaml-p (or (string-match-p "ya?ml" ext)
|
||
(eq 'yaml-mode mode)
|
||
(eq 'yaml-ts-mode mode))))
|
||
|
||
(let ((output (get-buffer-create " *pet parser output*")))
|
||
(unwind-protect
|
||
(let ((exit-code
|
||
(when (or toml-p yaml-p)
|
||
(condition-case err
|
||
(apply #'process-file
|
||
(cond (toml-p pet-toml-to-json-program)
|
||
(yaml-p pet-yaml-to-json-program))
|
||
file-path
|
||
output
|
||
nil
|
||
(cond (toml-p pet-toml-to-json-program-arguments)
|
||
(yaml-p pet-yaml-to-json-program-arguments)))
|
||
(error (error-message-string err))))))
|
||
|
||
(cond ((and (integerp exit-code) (zerop exit-code))
|
||
(with-current-buffer output
|
||
(pet-parse-json (buffer-string))))
|
||
(json-p
|
||
(with-temp-buffer
|
||
(insert-file-contents file-path)
|
||
(pet-parse-json (buffer-string))))
|
||
(t
|
||
(error (if (stringp exit-code)
|
||
exit-code
|
||
(with-current-buffer output
|
||
(buffer-string)))))))
|
||
(kill-buffer output))))
|
||
|
||
(error (pet-report-error err))))
|
||
|
||
(defvar pet-watched-config-files nil)
|
||
|
||
(defun pet-make-config-file-change-callback (cache-var parser)
|
||
"Make callback for `file-notify-add-watch'.
|
||
|
||
Return a callback with CACHE-VAR and PARSER captured in
|
||
itsenvironment. CACHE-VAR is the symbol to the cache variable to
|
||
update. PARSER is the symbol to the parser to parse the file.
|
||
|
||
When invoked, the callback returned will parse the file with
|
||
PARSER and cache the result in CACHE-VAR if the file was changed.
|
||
If the file was deleted or renamed, remove the file's watcher,
|
||
and delete the file entry from CACHE-VAR and
|
||
`pet-watched-config-files'."
|
||
(lambda (event)
|
||
(pcase-let ((`(,_ ,action ,file . ,_) event))
|
||
(pcase action
|
||
((or 'deleted 'renamed)
|
||
(file-notify-rm-watch (assoc-default file pet-watched-config-files))
|
||
(setf (alist-get file (symbol-value cache-var) nil t 'equal) nil)
|
||
(setf (alist-get file pet-watched-config-files nil t 'equal) nil))
|
||
('changed
|
||
(setf (alist-get file (symbol-value cache-var) nil nil 'equal)
|
||
(funcall parser file)))))))
|
||
|
||
(defun pet-watch-config-file (config-file cache-var parser)
|
||
"Keep cache fresh by watching for change in the config file.
|
||
|
||
CONFIG-FILE is the path to the configuration file to watch for
|
||
changes. CACHE-VAR is the symbol to the variable where the
|
||
parsed configuration file content is stored. PARSER is the
|
||
symbol to a function that takes a file path and parses its
|
||
content into an alist."
|
||
(unless (assoc-default config-file pet-watched-config-files)
|
||
(push (cons config-file
|
||
(file-notify-add-watch
|
||
config-file
|
||
'(change)
|
||
(pet-make-config-file-change-callback cache-var parser)))
|
||
pet-watched-config-files)))
|
||
|
||
(cl-defmacro pet-def-config-accessor (name &key file-name parser)
|
||
"Create a function for reading the content of a config file.
|
||
|
||
NAME will be used to create a memorized funcion named `pet-NAME'
|
||
to return the content of the configuration file FILE-NAME.
|
||
FILE-NAME is the name or glob pattern of the configuration file
|
||
that will be searched in the project. The content of the file
|
||
will be parsed by PARSER and then cached in a variable called
|
||
`pet-NAME-cache'.
|
||
|
||
Changes to the file will automatically update the cached content
|
||
See `pet-watch-config-file' for details."
|
||
(let* ((accessor-name (concat "pet-" (symbol-name name)))
|
||
(path-accessor-name (concat accessor-name "-path"))
|
||
(cache-var (intern (concat accessor-name "-cache")))
|
||
(accessor-docstring
|
||
(format "Accessor for `%s' in the current Python project.
|
||
|
||
If the file is found in the current Python project, cache its
|
||
content in `%s' and return it.
|
||
|
||
If the file content change, it is parsed again and the cache is
|
||
refreshed automatically. If it is renamed or deleted, the cache
|
||
entry is deleted.
|
||
"
|
||
name (symbol-name cache-var)))
|
||
(path-accessor-docstring (format "Path of `%s' in the current Python project.
|
||
|
||
Return nil if the file is not found." file-name))
|
||
(cache-var-docstring
|
||
(format "Cache for `%s'.
|
||
|
||
This variable is an alist where the key is the absolute path to a
|
||
`%s' in some Python project and the value is the parsed content.
|
||
" name name)))
|
||
`(progn
|
||
(defvar ,cache-var nil ,cache-var-docstring)
|
||
|
||
(defun ,(intern path-accessor-name) ()
|
||
,path-accessor-docstring
|
||
(pet-find-file-from-project ,file-name))
|
||
|
||
(defun ,(intern accessor-name) ()
|
||
,accessor-docstring
|
||
(when-let ((config-file (,(intern path-accessor-name))))
|
||
(if-let ((cached-content (assoc-default config-file ,cache-var)))
|
||
cached-content
|
||
(pet-watch-config-file config-file ',cache-var #',parser)
|
||
(when-let ((content (funcall #',parser config-file)))
|
||
(push (cons config-file content) ,cache-var)
|
||
content)))))))
|
||
|
||
(pet-def-config-accessor pre-commit-config
|
||
:file-name ".pre-commit-config.yaml"
|
||
:parser pet-parse-config-file)
|
||
|
||
(pet-def-config-accessor pyproject
|
||
:file-name "pyproject.toml"
|
||
:parser pet-parse-config-file)
|
||
|
||
(pet-def-config-accessor python-version
|
||
:file-name ".python-version"
|
||
:parser f-read-text)
|
||
|
||
(pet-def-config-accessor pipfile
|
||
:file-name "Pipfile"
|
||
:parser pet-parse-config-file)
|
||
|
||
;; So `pet-parse-config-file' knows Pipfile can be parsed with `pet-toml-to-json-program'.
|
||
(add-to-list 'auto-mode-alist '("/Pipfile\\'" . conf-toml-mode))
|
||
|
||
(pet-def-config-accessor environment
|
||
:file-name "environment*.y*ml"
|
||
:parser pet-parse-config-file)
|
||
|
||
(defun pet-use-pre-commit-p ()
|
||
"Whether the current project is using `pre-commit'.
|
||
|
||
Returns the path to the `pre-commit' executable."
|
||
(and (pet-pre-commit-config)
|
||
(or (pet--executable-find "pre-commit" t)
|
||
(and (when-let* ((venv (pet-virtualenv-root))
|
||
(exec-path (list (concat (file-name-as-directory venv) (pet-system-bin-dir))))
|
||
(process-environment (copy-sequence process-environment)))
|
||
(setenv "PATH" (string-join exec-path path-separator))
|
||
(pet--executable-find "pre-commit" t))))))
|
||
|
||
(defun pet-use-conda-p ()
|
||
"Whether the current project is using `conda'.
|
||
|
||
Returns the path to the `conda' executable variant found."
|
||
(and (pet-environment)
|
||
(or (pet--executable-find "conda" t)
|
||
(pet--executable-find "mamba" t)
|
||
(pet--executable-find "micromamba" t))))
|
||
|
||
(defun pet-use-poetry-p ()
|
||
"Whether the current project is using `poetry'.
|
||
|
||
Returns the path to the `poetry' executable."
|
||
(and (string-match-p
|
||
"poetry"
|
||
(or (let-alist (pet-pyproject)
|
||
.build-system.build-backend)
|
||
""))
|
||
(pet--executable-find "poetry" t)))
|
||
|
||
(defun pet-use-pyenv-p ()
|
||
"Whether the current project is using `pyenv'.
|
||
|
||
Returns the path to the `pyenv' executable."
|
||
(and (pet-python-version)
|
||
(pet--executable-find "pyenv" t)))
|
||
|
||
(defun pet-use-pipenv-p ()
|
||
"Whether the current project is using `pipenv'.
|
||
|
||
Returns the path to the `pipenv' executable."
|
||
(and (pet-pipfile)
|
||
(pet--executable-find "pipenv" t)))
|
||
|
||
(defun pet-pre-commit-config-has-hook-p (id)
|
||
"Determine if the `pre-commit' configuration has a hook.
|
||
|
||
Return non-nil if the `pre-commit' configuration for the current
|
||
project has hook ID set up."
|
||
(member id (cl-loop for repo in (let-alist (pet-pre-commit-config) .repos)
|
||
append (cl-loop for hook in (let-alist repo .hooks)
|
||
collect (let-alist hook .id)))))
|
||
|
||
(defun pet-parse-pre-commit-db (db-file)
|
||
"Parse `pre-commit' database.
|
||
|
||
Read the pre-commit SQLite database located at DB-FILE into an alist."
|
||
(if (and (functionp 'sqlite-available-p)
|
||
(sqlite-available-p))
|
||
(let ((db (sqlite-open db-file)))
|
||
(unwind-protect
|
||
(let* ((result-set (sqlite-select db "select * from repos" nil 'set))
|
||
result
|
||
row)
|
||
(while (setq row (sqlite-next result-set))
|
||
(setq result (cons (seq-mapn (lambda (a b) (cons (intern a) b))
|
||
(sqlite-columns result-set)
|
||
row)
|
||
result)))
|
||
(sqlite-finalize result-set)
|
||
result)
|
||
(sqlite-close db)))
|
||
|
||
(condition-case err
|
||
(with-temp-buffer
|
||
(process-file "sqlite3" nil t nil "-json" db-file "select * from repos")
|
||
(pet-parse-json (buffer-string)))
|
||
(error (pet-report-error err)))))
|
||
|
||
(defvar pet-pre-commit-database-cache nil)
|
||
|
||
(defun pet-pre-commit-virtualenv-path (hook-id)
|
||
"Find the virtualenv location from the `pre-commit' database.
|
||
|
||
If the `pre-commit' hook HOOK-ID is found in the current Python
|
||
project's `.pre-commit-config.yaml' file, the hook ID and its
|
||
additional dependencies are used to construct a key for looking
|
||
up a virtualenv for the hook from the pre-commit database.
|
||
|
||
In order to find the hook virtualenv, `pre-commit' and the hooks
|
||
must both be installed into the current project first."
|
||
(when-let* ((db-file
|
||
(concat
|
||
(expand-file-name
|
||
(file-name-as-directory
|
||
(or (getenv "PRE_COMMIT_HOME")
|
||
(getenv "XDG_CACHE_HOME")
|
||
"~/.cache/")))
|
||
(unless (getenv "PRE_COMMIT_HOME") "pre-commit/")
|
||
"db.db"))
|
||
|
||
(db
|
||
(or (assoc-default db-file pet-pre-commit-database-cache)
|
||
(when (file-exists-p db-file)
|
||
(pet-watch-config-file db-file 'pet-pre-commit-database-cache 'pet-parse-pre-commit-db)
|
||
(when-let ((content (pet-parse-pre-commit-db db-file)))
|
||
(push (cons db-file content) pet-pre-commit-database-cache)
|
||
content))))
|
||
|
||
(repo-config
|
||
(seq-find
|
||
(lambda (repo)
|
||
(seq-find
|
||
(lambda (hook)
|
||
(equal (let-alist hook .id) hook-id))
|
||
(let-alist repo .hooks)))
|
||
(let-alist (pet-pre-commit-config) .repos)))
|
||
|
||
(repo-url
|
||
(let-alist repo-config .repo))
|
||
|
||
(repo-dir
|
||
(let* ((additional-deps
|
||
(let-alist repo-config
|
||
(let-alist (seq-find (lambda (hook) (let-alist hook (equal .id hook-id))) .hooks)
|
||
.additional_dependencies)))
|
||
(unsorted-repo-url (concat repo-url ":" (string-join additional-deps ",")))
|
||
(sorted-repo-url (concat repo-url ":" (string-join (sort (copy-sequence additional-deps) 'string<) ","))))
|
||
(let-alist (seq-find
|
||
(lambda (row)
|
||
(let-alist row
|
||
(and (if additional-deps
|
||
(or (equal .repo unsorted-repo-url)
|
||
(equal .repo sorted-repo-url))
|
||
(equal .repo repo-url))
|
||
(equal .ref (let-alist repo-config .rev)))))
|
||
db)
|
||
.path))))
|
||
|
||
(car
|
||
(last
|
||
(file-expand-wildcards
|
||
(concat (file-name-as-directory repo-dir) "py_env-*")
|
||
t)))))
|
||
|
||
|
||
|
||
;;;###autoload
|
||
(defun pet-executable-find (executable)
|
||
"Find the correct EXECUTABLE for the current Python project.
|
||
|
||
Search for EXECUTABLE first in the `pre-commit' virtualenv, then
|
||
whatever environment if found by `pet-virtualenv-root', then
|
||
`pyenv', then finally from the variable `exec-path'.
|
||
|
||
The executable will only be searched in an environment created by
|
||
a Python virtualenv management tool if the project is set up to
|
||
use it."
|
||
(cond ((and (pet-use-pre-commit-p)
|
||
(not (string-prefix-p "python" executable))
|
||
(pet-pre-commit-config-has-hook-p executable))
|
||
(condition-case err
|
||
(let* ((venv (or (pet-pre-commit-virtualenv-path executable)
|
||
(user-error "`pre-commit' is configured but the hook `%s' does not appear to be installed" executable)))
|
||
(bin-dir (concat (file-name-as-directory venv) (pet-system-bin-dir)))
|
||
(bin-path (concat bin-dir "/" executable)))
|
||
(if (file-exists-p bin-path)
|
||
bin-path
|
||
(user-error "`pre-commit' is configured but `%s' is not found in %s" executable bin-dir)))
|
||
(error (pet-report-error err))))
|
||
((when-let* ((venv (pet-virtualenv-root))
|
||
(path (list (concat (file-name-as-directory venv) (pet-system-bin-dir))))
|
||
(exec-path path)
|
||
(tramp-remote-path path)
|
||
(process-environment (copy-sequence process-environment)))
|
||
(setenv "PATH" (string-join exec-path path-separator))
|
||
(pet--executable-find executable t)))
|
||
((when (pet--executable-find "pyenv" t)
|
||
(condition-case err
|
||
(car (process-lines "pyenv" "which" executable))
|
||
(error (pet-report-error err)))))
|
||
(t (or (pet--executable-find executable t)
|
||
(pet--executable-find (concat executable "3") t)))))
|
||
|
||
(defvar pet-project-virtualenv-cache nil)
|
||
|
||
;;;###autoload
|
||
(defun pet-virtualenv-root ()
|
||
"Find the path to the virtualenv for the current Python project.
|
||
|
||
Selects a virtualenv in the follow order:
|
||
|
||
1. The value of the environment variable `VIRTUAL_ENV' if defined.
|
||
2. If the current project is using any `conda' variant, return the absolute path
|
||
to the virtualenv directory for the current project.
|
||
3. Ditta for `poetry'.
|
||
4. Ditto for `pipenv'.
|
||
5. A directory in `pet-venv-dir-names' in the project root if found.
|
||
6. If the current project is using `pyenv', return the path to the virtualenv
|
||
directory by looking up the prefix from `.python-version'."
|
||
(let ((root (pet-project-root)))
|
||
(or (assoc-default root pet-project-virtualenv-cache)
|
||
(when-let ((ev (getenv "VIRTUAL_ENV")))
|
||
(expand-file-name ev))
|
||
(let ((venv-path
|
||
(cond ((when-let* ((program (pet-use-conda-p))
|
||
(default-directory (file-name-directory (pet-environment-path))))
|
||
(condition-case err
|
||
(with-temp-buffer
|
||
(let ((exit-code (process-file program nil t nil "info" "--json"))
|
||
(output (string-trim (buffer-string))))
|
||
(if (zerop exit-code)
|
||
(let* ((json-output (pet-parse-json output))
|
||
(env-dirs (or (let-alist json-output .envs_dirs)
|
||
(let-alist json-output .envs\ directories)))
|
||
(env-name (alist-get 'name (pet-environment)))
|
||
(env (seq-find 'file-directory-p
|
||
(seq-map (lambda (dir)
|
||
(file-name-as-directory
|
||
(concat
|
||
(file-name-as-directory dir)
|
||
env-name)))
|
||
env-dirs))))
|
||
(or env
|
||
(user-error "Please create the environment with `$ %s create --file %s' first" program (pet-environment-path))))
|
||
(user-error (buffer-string)))))
|
||
(error (pet-report-error err)))))
|
||
((when-let ((program (pet-use-poetry-p))
|
||
(default-directory (file-name-directory (pet-pyproject-path))))
|
||
(condition-case err
|
||
(with-temp-buffer
|
||
(let ((exit-code (process-file program nil t nil "env" "info" "--no-ansi" "--path"))
|
||
(output (string-trim (buffer-string))))
|
||
(if (zerop exit-code)
|
||
output
|
||
(user-error (buffer-string)))))
|
||
(error (pet-report-error err)))))
|
||
((when-let ((program (pet-use-pipenv-p))
|
||
(default-directory (file-name-directory (pet-pipfile-path))))
|
||
(condition-case err
|
||
(with-temp-buffer
|
||
(let ((exit-code (process-file program nil '(t nil) nil "--quiet" "--venv"))
|
||
(output (string-trim (buffer-string))))
|
||
(if (zerop exit-code)
|
||
output
|
||
(user-error (buffer-string)))))
|
||
(error (pet-report-error err)))))
|
||
((when-let ((dir (cl-loop for name in pet-venv-dir-names
|
||
with dir = nil
|
||
if (setq dir (locate-dominating-file default-directory name))
|
||
return (file-name-as-directory (concat dir name)))))
|
||
(expand-file-name dir)))
|
||
((when-let ((program (pet-use-pyenv-p))
|
||
(default-directory (file-name-directory (pet-python-version-path))))
|
||
(condition-case err
|
||
(with-temp-buffer
|
||
(let ((exit-code (process-file program nil t nil "prefix"))
|
||
(output (string-trim (buffer-string))))
|
||
(if (zerop exit-code)
|
||
(file-truename output)
|
||
(user-error (buffer-string)))))
|
||
(error (pet-report-error err))))))))
|
||
;; root maybe nil when not in a project, this avoids caching a nil
|
||
(when root
|
||
(setf (alist-get root pet-project-virtualenv-cache nil nil 'equal) venv-path))
|
||
venv-path))))
|
||
|
||
|
||
|
||
(defvar flycheck-mode)
|
||
(defvar flycheck-python-mypy-config)
|
||
(defvar flycheck-pylintrc)
|
||
(defvar flycheck-python-flake8-executable)
|
||
(defvar flycheck-python-pylint-executable)
|
||
(defvar flycheck-python-mypy-executable)
|
||
(defvar flycheck-python-pyright-executable)
|
||
(defvar flycheck-python-pycompile-executable)
|
||
(defvar flycheck-python-ruff-executable)
|
||
|
||
(defun pet-flycheck-python-pylint-find-pylintrc ()
|
||
"Polyfill `flycheck-pylintrc'.
|
||
|
||
Find the correct `pylint' configuration file according to the
|
||
algorithm described at
|
||
`https://pylint.pycqa.org/en/latest/user_guide/usage/run.html'."
|
||
(let* ((pylintrc '("pylintrc" ".pylintrc" "pyproject.toml" "setup.cfg"))
|
||
(found (cond ((cl-loop for f in pylintrc
|
||
with path = nil
|
||
do (setq path (concat default-directory f))
|
||
if (file-exists-p path)
|
||
return (expand-file-name path)))
|
||
((and (buffer-file-name)
|
||
(file-exists-p (concat (file-name-directory (buffer-file-name)) "__init__.py")))
|
||
(when-let ((path (cl-loop for f in pylintrc
|
||
with dir = nil
|
||
do (setq dir (locate-dominating-file default-directory f))
|
||
if dir
|
||
return (concat dir f))))
|
||
(expand-file-name path))))))
|
||
(if found
|
||
found
|
||
(cond ((when-let* ((ev (getenv "PYLINTRC"))
|
||
(path (expand-file-name ev)))
|
||
(and (file-exists-p path) path)))
|
||
((let* ((ev (getenv "XDG_CONFIG_HOME"))
|
||
(config-dir
|
||
(or (and ev (file-name-as-directory ev))
|
||
"~/.config/"))
|
||
(xdg-file-path (expand-file-name (concat config-dir "pylintrc"))))
|
||
(and (file-exists-p xdg-file-path) xdg-file-path)))
|
||
((let ((home-dir-pylintrc (expand-file-name "~/.pylintrc")))
|
||
(and (file-exists-p home-dir-pylintrc) home-dir-pylintrc)))
|
||
(t "/etc/pylintrc")))))
|
||
|
||
(defun pet-flycheck-toggle-local-vars ()
|
||
"Toggle buffer local variables for `flycheck' Python checkers.
|
||
|
||
When `flycheck-mode' is non-nil, set up all supported Python
|
||
checker executable variables buffer-locally. Reset them to
|
||
default otherwise."
|
||
(if (bound-and-true-p flycheck-mode)
|
||
(progn
|
||
(when (derived-mode-p (if (functionp 'python-base-mode) 'python-base-mode 'python-mode))
|
||
(setq-local flycheck-python-mypy-config `("mypy.ini" ".mypy.ini" "pyproject.toml" "setup.cfg"
|
||
,(expand-file-name
|
||
(concat
|
||
(or (when-let ((xdg-config-home (getenv "XDG_CONFIG_HOME")))
|
||
(file-name-as-directory xdg-config-home))
|
||
"~/.config/")
|
||
"mypy/config"))
|
||
,(expand-file-name "~/.mypy.ini")))
|
||
(setq-local flycheck-pylintrc (pet-flycheck-python-pylint-find-pylintrc))
|
||
(setq-local flycheck-python-flake8-executable (pet-executable-find "flake8"))
|
||
(setq-local flycheck-python-pylint-executable (pet-executable-find "pylint"))
|
||
(setq-local flycheck-python-mypy-executable (pet-executable-find "mypy"))
|
||
(setq-local flycheck-python-mypy-python-executable (pet-executable-find "python"))
|
||
(setq-local flycheck-python-pyright-executable (pet-executable-find "pyright"))
|
||
(setq-local flycheck-python-pycompile-executable python-shell-interpreter)
|
||
(setq-local flycheck-python-ruff-executable (pet-executable-find "ruff"))))
|
||
(kill-local-variable 'flycheck-python-mypy-config)
|
||
(kill-local-variable 'flycheck-pylintrc)
|
||
(kill-local-variable 'flycheck-python-flake8-executable)
|
||
(kill-local-variable 'flycheck-python-pylint-executable)
|
||
(kill-local-variable 'flycheck-python-mypy-executable)
|
||
(kill-local-variable 'flycheck-python-mypy-python-executable)
|
||
(kill-local-variable 'flycheck-python-pyright-executable)
|
||
(kill-local-variable 'flycheck-python-pycompile-executable)
|
||
(kill-local-variable 'flycheck-python-ruff-executable)))
|
||
|
||
(defun pet-flycheck-python-find-project-root-advice (_)
|
||
"Delegate `flycheck-python-find-project-root' to `pet-virtualenv-root'."
|
||
(pet-virtualenv-root))
|
||
|
||
;;;###autoload
|
||
(defun pet-flycheck-setup ()
|
||
"Set up all `flycheck' Python checker configuration."
|
||
(advice-add 'flycheck-python-find-project-root :override #'pet-flycheck-python-find-project-root-advice)
|
||
(add-hook 'flycheck-mode-hook #'pet-flycheck-toggle-local-vars))
|
||
|
||
;;;###autoload
|
||
(defun pet-flycheck-teardown ()
|
||
"Reset all `flycheck' Python checker configuration to default."
|
||
(advice-remove 'flycheck-python-find-project-root #'pet-flycheck-python-find-project-root-advice)
|
||
(remove-hook 'flycheck-mode-hook #'pet-flycheck-toggle-local-vars)
|
||
(kill-local-variable 'flycheck-python-mypy-config)
|
||
(kill-local-variable 'flycheck-pylintrc)
|
||
(kill-local-variable 'flycheck-python-flake8-executable)
|
||
(kill-local-variable 'flycheck-python-pylint-executable)
|
||
(kill-local-variable 'flycheck-python-mypy-executable)
|
||
(kill-local-variable 'flycheck-python-mypy-python-executable)
|
||
(kill-local-variable 'flycheck-python-pyright-executable)
|
||
(kill-local-variable 'flycheck-python-pycompile-executable)
|
||
(kill-local-variable 'flycheck-python-ruff-executable))
|
||
|
||
|
||
|
||
(defvar eglot-workspace-configuration)
|
||
(declare-function jsonrpc--process "ext:jsonrpc")
|
||
(declare-function eglot--executable-find "ext:eglot")
|
||
(declare-function eglot--workspace-configuration-plist "ext:eglot")
|
||
(declare-function eglot--guess-contact "ext:eglot")
|
||
|
||
(defun pet-eglot--executable-find-advice (fn &rest args)
|
||
"Look up Python language servers using `pet-executable-find'.
|
||
|
||
FN is `eglot--executable-find', or `executable-find' (1.17+), depending
|
||
on the version of `eglot' being used. ARGS is the arguments to FN."
|
||
(pcase-let ((`(,command . ,_) args))
|
||
(if (member command '("pylsp" "pyls" "basedpyright-langserver" "pyright-langserver" "jedi-language-server" "ruff-lsp"))
|
||
(pet-executable-find command)
|
||
(apply fn args))))
|
||
|
||
(defun pet-lookup-eglot-server-initialization-options (command)
|
||
"Return LSP initializationOptions for Eglot.
|
||
|
||
COMMAND is the name of the Python language server command."
|
||
(cond ((string-match-p "pylsp" command)
|
||
`(:pylsp
|
||
(:plugins
|
||
(:jedi
|
||
(:environment
|
||
,(pet-virtualenv-root))
|
||
:ruff
|
||
(:executable
|
||
,(pet-executable-find "ruff"))
|
||
:pylsp_mypy
|
||
(:overrides
|
||
["--python-executable" ,(pet-executable-find "python") t])
|
||
:flake8
|
||
(:executable
|
||
,(pet-executable-find "flake8"))
|
||
:pylint
|
||
(:executable
|
||
,(pet-executable-find "pylint"))))))
|
||
((string-match-p "pyls" command)
|
||
`(:pyls
|
||
(:plugins
|
||
(:jedi
|
||
(:environment
|
||
,(pet-virtualenv-root))
|
||
:pylint
|
||
(:executable
|
||
,(pet-executable-find "pylint"))))))
|
||
((string-match-p "pyright-langserver" command)
|
||
`(:python
|
||
(:pythonPath
|
||
,(pet-executable-find "python")
|
||
:venvPath
|
||
,(pet-virtualenv-root))))
|
||
((string-match-p "jedi-language-server" command)
|
||
`(:jedi
|
||
(:executable
|
||
(:command
|
||
,(pet-executable-find "jedi-language-server"))
|
||
:workspace
|
||
(:environmentPath
|
||
,(pet-executable-find "python")))))
|
||
((string-match-p "ruff-lsp" command)
|
||
`(:settings
|
||
(:interpreter
|
||
,(pet-executable-find "python")
|
||
:path
|
||
,(pet-executable-find "ruff"))))
|
||
(t nil)))
|
||
|
||
(defalias 'pet--proper-list-p 'proper-list-p)
|
||
(eval-when-compile
|
||
(when (and (not (functionp 'proper-list-p))
|
||
(functionp 'format-proper-list-p))
|
||
(defun pet--proper-list-p (l)
|
||
(and (format-proper-list-p l)
|
||
(length l)))))
|
||
|
||
(defun pet--plistp (object)
|
||
"Non-nil if and only if OBJECT is a valid plist."
|
||
(let ((len (pet--proper-list-p object)))
|
||
(and len
|
||
(zerop (% len 2))
|
||
(seq-every-p
|
||
(lambda (kvp)
|
||
(keywordp (car kvp)))
|
||
(seq-split object 2)))))
|
||
|
||
(defun pet-merge-eglot-initialization-options (a b)
|
||
"Deep merge plists A and B."
|
||
(map-merge-with 'plist
|
||
(lambda (c d)
|
||
(cond ((and (pet--plistp c) (pet--plistp d))
|
||
(pet-merge-eglot-initialization-options c d))
|
||
((and (vectorp c) (vectorp d))
|
||
(vconcat (seq-union c d)))
|
||
(t d)))
|
||
(copy-tree a t)
|
||
(copy-tree b t)))
|
||
|
||
(defun pet-eglot--workspace-configuration-plist-advice (fn &rest args)
|
||
"Enrich `eglot-workspace-configuration' with paths found by `pet'.
|
||
|
||
FN is `eglot--workspace-configuration-plist', ARGS is the
|
||
arguments to `eglot--workspace-configuration-plist'."
|
||
(let* ((path (cadr args))
|
||
(canonical-path (if (and path (file-directory-p path))
|
||
(file-name-as-directory path)
|
||
path))
|
||
(server (car args))
|
||
(command (process-command (jsonrpc--process server)))
|
||
(program (and (listp command) (car command)))
|
||
(pet-config (pet-lookup-eglot-server-initialization-options program))
|
||
(user-config (apply fn server (and canonical-path (cons canonical-path (cddr args))))))
|
||
(pet-merge-eglot-initialization-options user-config pet-config)))
|
||
|
||
(defun pet-eglot--guess-contact-advice (fn &rest args)
|
||
"Enrich `eglot--guess-contact' with paths found by `pet'.
|
||
|
||
FN is `eglot--guess-contact', ARGS is the arguments to
|
||
`eglot--guess-contact'."
|
||
(let* ((result (apply fn args))
|
||
(contact (nth 3 result))
|
||
(probe (seq-position contact :initializationOptions))
|
||
(program-with-args (seq-subseq contact 0 (or probe (length contact))))
|
||
(program (car program-with-args))
|
||
(init-opts (plist-get (seq-subseq contact (or probe 0)) :initializationOptions)))
|
||
(if init-opts
|
||
(append (seq-subseq result 0 3)
|
||
(list
|
||
(append
|
||
program-with-args
|
||
(list
|
||
:initializationOptions
|
||
(pet-merge-eglot-initialization-options
|
||
init-opts
|
||
(pet-lookup-eglot-server-initialization-options
|
||
program)))))
|
||
(seq-subseq result 4))
|
||
result)))
|
||
|
||
(defun pet-eglot-setup ()
|
||
"Set up Eglot to use server executables and virtualenvs found by PET."
|
||
(if (fboundp 'eglot--executable-find)
|
||
;; Eglot version 1.16 or below
|
||
(advice-add 'eglot--executable-find :around #'pet-eglot--executable-find-advice)
|
||
;; Eglot version 1.17 and above
|
||
(advice-add 'executable-find :around #'pet-eglot--executable-find-advice))
|
||
(advice-add 'eglot--workspace-configuration-plist :around #'pet-eglot--workspace-configuration-plist-advice)
|
||
(advice-add 'eglot--guess-contact :around #'pet-eglot--guess-contact-advice))
|
||
|
||
(defun pet-eglot-teardown ()
|
||
"Tear down PET advices to Eglot."
|
||
(if (fboundp 'eglot--executable-find)
|
||
;; Eglot version 1.16 or below
|
||
(advice-remove 'eglot--executable-find #'pet-eglot--executable-find-advice)
|
||
;; Eglot version 1.17 and above
|
||
(advice-remove 'executable-find #'pet-eglot--executable-find-advice))
|
||
(advice-remove 'eglot--workspace-configuration-plist #'pet-eglot--workspace-configuration-plist-advice)
|
||
(advice-remove 'eglot--guess-contact #'pet-eglot--guess-contact-advice))
|
||
|
||
|
||
(defvar dape-command)
|
||
(defvar dape-cwd-fn)
|
||
|
||
(defun pet-dape-setup ()
|
||
"Set up the buffer local variables for `dape'."
|
||
(if-let* ((main (pet-find-file-from-project-root-recursively "__main__.py"))
|
||
(module (let* ((dir (file-name-directory main))
|
||
(dir-file-name (directory-file-name dir))
|
||
(module))
|
||
(while (file-exists-p (concat dir "__init__.py"))
|
||
(push (file-name-nondirectory dir-file-name) module)
|
||
(setq dir (file-name-directory dir-file-name))
|
||
(setq dir-file-name (directory-file-name dir)))
|
||
(string-join module "."))))
|
||
(setq-local dape-command `(debugpy-module command ,(pet-executable-find "python") :module ,module))
|
||
(setq-local dape-command `(debugpy command ,(pet-executable-find "python"))))
|
||
(setq-local dape-cwd-fn #'pet-project-root))
|
||
|
||
(defun pet-dape-teardown ()
|
||
"Tear down the buffer local variables for `dape'."
|
||
(kill-local-variable 'dape-command)
|
||
(kill-local-variable 'dape-cwd-fn))
|
||
|
||
|
||
|
||
(defvar lsp-jedi-executable-command)
|
||
(defvar lsp-pyls-plugins-jedi-environment)
|
||
(defvar lsp-pylsp-plugins-jedi-environment)
|
||
(defvar lsp-pyright-python-executable-cmd)
|
||
(defvar lsp-pyright-venv-path)
|
||
(defvar lsp-ruff-server-command)
|
||
(defvar lsp-ruff-python-path)
|
||
(defvar dap-python-executable)
|
||
(defvar dap-variables-project-root-function)
|
||
(defvar python-pytest-executable)
|
||
(defvar python-black-command)
|
||
(defvar python-isort-command)
|
||
(defvar ruff-format-command)
|
||
(defvar blacken-executable)
|
||
(defvar yapfify-executable)
|
||
(defvar py-autopep8-command)
|
||
|
||
(defun pet-buffer-local-vars-setup ()
|
||
"Set up the buffer local variables for Python tools.
|
||
|
||
Assign all supported Python tooling executable variables to
|
||
buffer local values."
|
||
(setq-local python-shell-interpreter (pet-executable-find "python"))
|
||
(setq-local python-shell-virtualenv-root (pet-virtualenv-root))
|
||
|
||
(pet-flycheck-setup)
|
||
|
||
(setq-local lsp-jedi-executable-command
|
||
(pet-executable-find "jedi-language-server"))
|
||
(setq-local lsp-pyls-plugins-jedi-environment python-shell-virtualenv-root)
|
||
(setq-local lsp-pylsp-plugins-jedi-environment python-shell-virtualenv-root)
|
||
(setq-local lsp-pyright-venv-path python-shell-virtualenv-root)
|
||
(setq-local lsp-pyright-python-executable-cmd python-shell-interpreter)
|
||
(setq-local lsp-ruff-server-command (list (pet-executable-find "ruff") "server"))
|
||
(setq-local lsp-ruff-python-path python-shell-interpreter)
|
||
(setq-local dap-python-executable python-shell-interpreter)
|
||
(setq-local dap-variables-project-root-function #'pet-project-root)
|
||
(setq-local python-pytest-executable (pet-executable-find "pytest"))
|
||
(setq-local python-black-command (pet-executable-find "black"))
|
||
(setq-local python-isort-command (pet-executable-find "isort"))
|
||
(setq-local ruff-format-command (pet-executable-find "ruff"))
|
||
(setq-local blacken-executable python-black-command)
|
||
(setq-local yapfify-executable (pet-executable-find "yapf"))
|
||
(setq-local py-autopep8-command (pet-executable-find "autopep8"))
|
||
|
||
(pet-eglot-setup)
|
||
(pet-dape-setup))
|
||
|
||
(defun pet-buffer-local-vars-teardown ()
|
||
"Reset all supported buffer local variable values to default."
|
||
|
||
(kill-local-variable 'python-shell-interpreter)
|
||
(kill-local-variable 'python-shell-virtualenv-root)
|
||
|
||
(pet-flycheck-teardown)
|
||
|
||
(kill-local-variable 'lsp-jedi-executable-command)
|
||
(kill-local-variable 'lsp-pyls-plugins-jedi-environment)
|
||
(kill-local-variable 'lsp-pylsp-plugins-jedi-environment)
|
||
(kill-local-variable 'lsp-pyright-venv-path)
|
||
(kill-local-variable 'lsp-pyright-python-executable-cmd)
|
||
(kill-local-variable 'lsp-ruff-python-path)
|
||
(kill-local-variable 'lsp-ruff-server-command)
|
||
(kill-local-variable 'dap-python-executable)
|
||
(kill-local-variable 'dap-variables-project-root-function)
|
||
(kill-local-variable 'python-pytest-executable)
|
||
(kill-local-variable 'python-black-command)
|
||
(kill-local-variable 'python-isort-command)
|
||
(kill-local-variable 'ruff-format-command)
|
||
(kill-local-variable 'blacken-executable)
|
||
(kill-local-variable 'yapfify-executable)
|
||
(kill-local-variable 'py-autopep8-command)
|
||
|
||
(pet-eglot-teardown)
|
||
(pet-dape-teardown))
|
||
|
||
(defun pet-verify-setup ()
|
||
"Verify the values of buffer local variables visually.
|
||
|
||
Print all of the buffer local variable values `pet-mode'
|
||
has assigned to."
|
||
(interactive)
|
||
|
||
(unless (derived-mode-p 'python-base-mode 'python-mode)
|
||
(user-error "You are not in python-mode!"))
|
||
|
||
(let ((kvp (mapcar (lambda (sym)
|
||
(cons sym
|
||
(if (boundp sym)
|
||
(let ((val (symbol-value sym)))
|
||
(if (consp val)
|
||
(apply #'string-join
|
||
(mapcar (apply-partially #'abbreviate-file-name)
|
||
(mapcar (apply-partially #'format "%s") val))
|
||
(list ", "))
|
||
(abbreviate-file-name (format "%s" val))))
|
||
'unbound)))
|
||
'(python-shell-interpreter
|
||
python-shell-virtualenv-root
|
||
flycheck-python-flake8-executable
|
||
flycheck-pylintrc
|
||
flycheck-python-pylint-executable
|
||
flycheck-python-mypy-executable
|
||
flycheck-python-mypy-config
|
||
flycheck-python-mypy-python-executable
|
||
flycheck-python-pyright-executable
|
||
flycheck-python-pycompile-executable
|
||
flycheck-python-ruff-executable
|
||
lsp-jedi-executable-command
|
||
lsp-pyls-plugins-jedi-environment
|
||
lsp-pylsp-plugins-jedi-environment
|
||
lsp-pyright-python-executable-cmd
|
||
lsp-pyright-venv-path
|
||
lsp-ruff-server-command
|
||
lsp-ruff-python-path
|
||
dap-python-executable
|
||
dap-variables-project-root-function
|
||
dape-command
|
||
dape-cwd-fn
|
||
python-pytest-executable
|
||
python-black-command
|
||
blacken-executable
|
||
python-isort-command
|
||
ruff-format-command
|
||
yapfify-executable
|
||
py-autopep8-command))))
|
||
|
||
(with-current-buffer-window "*pet info*" nil nil
|
||
(mapc (pcase-lambda (`(,key . ,value))
|
||
(insert (propertize (format "%-40s" (concat (symbol-name key) ":")) 'face 'font-lock-variable-name-face))
|
||
(insert (format "%s" value))
|
||
(insert "\n"))
|
||
kvp)
|
||
(insert (propertize (format "%-40s"
|
||
(concat (symbol-name (if (file-remote-p default-directory)
|
||
'tramp-remote-path
|
||
'exec-path))
|
||
":"))
|
||
'face 'font-lock-variable-name-face) "\n")
|
||
(mapc (lambda (dir)
|
||
(insert (abbreviate-file-name (format "%s" dir)) "\n"))
|
||
(if (file-remote-p default-directory)
|
||
tramp-remote-path
|
||
exec-path))
|
||
(special-mode))))
|
||
|
||
;;;###autoload
|
||
(define-minor-mode pet-mode
|
||
"Minor mode to set up buffer local variables for Python tools."
|
||
:lighter " Pet"
|
||
:group 'pet
|
||
(if pet-mode
|
||
(progn
|
||
(pet-buffer-local-vars-setup)
|
||
(add-hook 'kill-buffer-hook #'pet-cleanup-watchers-and-caches t))
|
||
(pet-buffer-local-vars-teardown)
|
||
(remove-hook 'kill-buffer-hook #'pet-cleanup-watchers-and-caches t)))
|
||
|
||
(defun pet-cleanup-watchers-and-caches ()
|
||
"Clean up configuration file caches and watchers.
|
||
|
||
Delete configuration file caches and watchers when all
|
||
`python-mode' buffers of a project have been closed."
|
||
(when (and (buffer-file-name)
|
||
(derived-mode-p 'python-base-mode 'python-mode))
|
||
(when-let ((root (pet-project-root)))
|
||
(when (null (cl-loop for buf in (buffer-list)
|
||
if (and (not (equal buf (current-buffer)))
|
||
(string-prefix-p root (buffer-file-name buf)))
|
||
return buf))
|
||
|
||
(setf (alist-get root pet-project-virtualenv-cache nil t 'equal) nil)
|
||
|
||
(pcase-dolist (`(,config-file . ,watcher) pet-watched-config-files)
|
||
(when (string-prefix-p root config-file)
|
||
(file-notify-rm-watch watcher)
|
||
(setf (alist-get config-file pet-watched-config-files nil t 'equal) nil)))
|
||
|
||
(dolist (cache '(pet-pre-commit-config-cache
|
||
pet-pyproject-cache
|
||
pet-python-version-cache
|
||
pet-pipfile-cache
|
||
pet-environment-cache))
|
||
(pcase-dolist (`(,key . ,_) (symbol-value cache))
|
||
(when (string-prefix-p root key)
|
||
(setf (alist-get key (symbol-value cache) nil t 'equal) nil))))))))
|
||
|
||
(provide 'vedang-pet)
|
||
|
||
;;; pet.el ends here
|
||
|
||
#+end_src
|