Emacs config
Emacs config
One day we won’t hate one another, no young boy will march to war and I will clean up my Emacs config. But that day isn’t today.
- Me, The Dark Element - “The Pallbearer Walks Alone”. T_T in commit 93a0573. Adapted from
Introduction
My configuration of GNU Emacs, an awesome text editor piece of software that can do almost anything.
At the moment of writing this, that “almost anything” includes:
- Programming environment. With LSP & Co, Emacs is as good as many IDEs and is certainly on par with editors like VS Code.
Emacs is also particularly great at writing Lisp code, e.g. Clojure, Common Lisp, and of course, Emacs Lisp. - Org Mode is useful for a lot of things. My use cases include:
- Literate configuration
- Interactive programming à la Jupyter Notebook
- Task / project management
- Formatting documents. I’ve written my Master’s Thesis in Org Mode.
- Notetaking, mostly with org-roam and org-journal
- File management. Dired is my primary file manager.
- Email, with notmuch.
- Multimedia management, with EMMS.
- RSS feed reader, with elfeed.
- Managing passwords, with pass.
- Messengers:
- IRC, with ERC.
- Telegram, with telega.el
- X Window management, with EXWM. I literally live in Emacs.
- …
As I mentioned above, this document is a piece of literate configuration, i.e. program code interwoven with (occasionally semi-broken) English-language commentary.
I find that approach helpful for maintaining the configuration, but the quality and quantity of comments may vary. I also usually incorporate my Emacs-related blog posts back into this config.
So, you might extract something of value from here if you’re an avid Emacs user, but probably not if you’re a newcomer to the Elisp wonderland. If the latter applies to you, I’d advise checking out David Wilson’s System Crafters YouTube channel.
Some remarks
I decided not to keep configs for features that I do not use anymore because this config is already huge. But here are the last commits that had these features presented.
Feature | Last commit |
---|---|
org-roam dailies | d2648918fcc338bd5c1cd6d5c0aa60a65077ccf7 |
org-roam projects | 025278a1e180e86f3aade20242e4ac1cdc1a2f13 |
treemacs | 3d87852745caacc0863c747f1fa9871d367240d2 |
tab-bar.el | 19ff54db9fe21fd5bdf404a8d2612176baa8a6f5 |
spaceline | 19ff54db9fe21fd5bdf404a8d2612176baa8a6f5 |
code compass | 8594d6f53e42c70bbf903e168607841854818a38 |
vue-mode | 8594d6f53e42c70bbf903e168607841854818a38 |
svelte-mode | 8594d6f53e42c70bbf903e168607841854818a38 |
pomidor | 8594d6f53e42c70bbf903e168607841854818a38 |
elfeed-score | 8e591e0d2afd909ae5be00caf17f9b17c6cd8b61 |
org-trello | 3f5967a5f63928ea9c8567d8d9f31e84cdbbc21f |
jabber | 9b0e73a4703ff35a2d30fd704200052888191217 |
wallabag | 9b0e73a4703ff35a2d30fd704200052888191217 |
conda | 609fc84e439b11ea5064f3a948079daebb654aca |
notmuch tags keybindings | eac134c5456051171c1c777254f503cc71ce12cd |
expand-region | ab0d01c525f2b44dd64ec09747daf0fced4bd9c7 |
org-latex-impatient | ab0d01c525f2b44dd64ec09747daf0fced4bd9c7 |
dired-single | ab0d01c525f2b44dd64ec09747daf0fced4bd9c7 |
progidy | ab0d01c525f2b44dd64ec09747daf0fced4bd9c7 |
tree-sitter | 1920a48aec49837d63fa88ca315928dc4e9d14c2 |
org-roam-protocol | 2f0c20eb01b8899d00d129cc7ca5c6b263c69c65 |
eshell-info-banner | 4ccc0bbc412b68e1401533264d801d86b1fc8cc7 |
aweshell | 4ccc0bbc412b68e1401533264d801d86b1fc8cc7 |
link tasks to meetings | 23496bfacc31ffedf2092da04e4e602b71373425 |
Initial setup
Setting up the environment, performance tuning and a few basic settings.
First things first, lexical binding.
;;; -*- lexical-binding: t -*-
Packages
straight.el
Straight.el is my Emacs package manager of choice. Its advantages & disadvantages over other options are listed pretty thoroughly in the README file in the repo.
The following is the bootstrap script of straight.el
.
References:
(defvar bootstrap-version)
(let ((bootstrap-file
(expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
(bootstrap-version 5))
(unless (file-exists-p bootstrap-file)
(with-current-buffer
(url-retrieve-synchronously
"https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
'silent 'inhibit-cookies)
(goto-char (point-max))
(eval-print-last-sexp)))
(load bootstrap-file nil 'nomessage))
use-package
A macro to simplify package specification & configuration. Integrates with straight.el.
Set use-package-verbose
to t
to print out loading times for individual packages.
References:
(straight-use-package 'use-package)
(eval-when-compile (require 'use-package))
Variables & environment
This section is about optioning the Emacs config.
The following is true is Emacs is run on a remote server where I don’t need stuff like my org workflow
(setq my/remote-server
(or (string= (getenv "IS_REMOTE") "true")
(string= (system-name) "dev-digital")
(string= (system-name) "viridian")))
And the following is true if Emacs is run from termux on Android.
(setq my/is-termux (string-match-p (rx (* nonl) "com.termux" (* nonl)) (getenv "HOME")))
Custom system name logic because on termux it’s always “localhost”.
(defun my/system-name ()
(or (getenv "ANDROID_NAME")
(system-name)))
Also, I sometimes need to know if a program is running inside Emacs (say, inside a terminal emulator). And sometimes I need to know if I’m running a nested Emacs session. To do that, I set the following environment variable:
(setq my/nested-emacs (and (getenv "IS_EMACS") t))
(setenv "IS_EMACS" "true")
Finally, I want to have a minimal Emacs config for debugging purposes. This has just straight.el, use-packages, and evil.
<<minimal>>
To launch Emacs with this config, run
emacs -q -l ~/.emacs.d/init-minimal.el
Performance
Measure startup speed
A small function to print out the loading time and number of GCs during the loading. Can be useful as a point of data for optimizing Emacs startup time.
(setq my/emacs-started nil)
(add-hook 'emacs-startup-hook
(lambda ()
(message "*** Emacs loaded in %s with %d garbage collections."
(format "%.2f seconds"
(float-time
(time-subtract after-init-time before-init-time)))
gcs-done)
(setq my/emacs-started t)))
Set the following to t
to print debug information during the startup. This will include package loading order and time.
(setq use-package-verbose nil)
(setq use-package-compute-statistics t)
Garbage collection
Just setting gc-cons-treshold
to a larger value.
(setq gc-cons-threshold 80000000)
(setq read-process-output-max (* 1024 1024))
Run garbage collection when Emacs is unfocused
Run GC when Emacs loses focus. Time will tell if that’s a good idea.
I still don’t know if there is any quantifiable advantage to this, but it doesn’t hurt.
(add-hook 'emacs-startup-hook
(lambda ()
(if (boundp 'after-focus-change-function)
(add-function :after after-focus-change-function
(lambda ()
(unless (frame-focus-state)
(garbage-collect))))
(add-hook 'after-focus-change-function 'garbage-collect))))
Measure RAM usage
I’ve noticed that Emacs occasionally eats a lot of RAM, especially when used with EXWM. This is my attempt to measure RAM usage.
I have some concerns that ps -o rss
may be unrepresentative because of shared memory, but I guess this shouldn’t be a problem here because there’s only one process of Emacs.
(defun my/get-ram-usage-async (callback)
(let* ((temp-buffer (generate-new-buffer "*ps*"))
(proc (start-process "ps" temp-buffer "ps"
"-p" (number-to-string (emacs-pid)) "-o" "rss")))
(set-process-sentinel
proc
(lambda (process _msg)
(when (eq (process-status process) 'exit)
(let* ((output (with-current-buffer temp-buffer
(buffer-string)))
(usage (string-to-number (nth 1 (split-string output "\n")))))
(ignore-errors
(funcall callback usage)))
(kill-buffer temp-buffer))))))
(defun my/ram-usage ()
(interactive)
(my/get-ram-usage-async
(lambda (data)
(message "%f Gb" (/ (float data) 1024 1024)))))
Micromamba
mamba is a faster alternative to Anaconda, a package and environment manager. micromamba
is a tiny version that provides a subset of mamba commands.
micromamba.el is my package to interact with the latter.
(use-package micromamba
:straight t
:if (executable-find "micromamba")
:config
(micromamba-activate "general"))
Config files
Custom file location
By default, custom
writes stuff to init.el
, which is somewhat annoying. The following makes it write to a separate file custom.el
(setq custom-file (concat user-emacs-directory "custom.el"))
(load custom-file 'noerror)
authinfo
Use only the gpg-encrypted version of the file.
(setq auth-source-debug nil)
(setq auth-sources '("~/.authinfo.gpg"))
Private config
I have some variables which I don’t commit to the repo, e.g. my current location. They are stored in private.el
(let ((private-file (expand-file-name "private.el" user-emacs-directory)))
(when (file-exists-p private-file)
(load-file private-file)))
No littering
By default Emacs and its packages create a lot files in .emacs.d
and in other places. no-littering is a collective effort to redirect all of that to two folders in user-emacs-directory
.
(use-package no-littering
:straight t)
Helper functions
Run command in background
I think I’ve copied it from somewhere.
(defun my/run-in-background (command)
(let ((command-parts (split-string command "[ ]+")))
(apply #'call-process `(,(car command-parts) nil 0 nil ,@(cdr command-parts)))))
Close buffer and its windows
(defun my/quit-window-and-buffer ()
(interactive)
(quit-window t))
Prevent Emacs from closing
This adds a confirmation to avoid accidental Emacs closing.
(setq confirm-kill-emacs 'y-or-n-p)
Scratch buffer
I have a problem with emacs-lisp-mode
as initial-major-mode
because in my config it loads lispy
, which loads org-mode
.
So until I’ve made a better loading screen, this will do.
(setq initial-major-mode 'fundamental-mode)
(setq initial-scratch-message "Hello there <3\n\n")
General settings
Keybindings
general.el
general.el provides a convenient interface to manage Emacs keybindings.
References:
(use-package general
:straight t
:config
(general-evil-setup))
which-key
A package that displays the available keybindings in a popup. The package is pretty useful, as Emacs seems to have more keybindings than I can remember at any given point.
References:
(use-package which-key
:config
(setq which-key-idle-delay 0.3)
(setq which-key-popup-type 'frame)
(which-key-mode)
(which-key-setup-side-window-bottom)
(set-face-attribute 'which-key-local-map-description-face nil
:weight 'bold)
:straight t)
dump keybindings
A function to dump keybindings starting with a prefix to a buffer in a tree-like form.
(defun my/dump-bindings-recursive (prefix &optional level buffer)
(dolist (key (which-key--get-bindings (kbd prefix)))
(with-current-buffer buffer
(when level
(insert (make-string level ? )))
(insert (apply #'format "%s%s%s\n" key)))
(when (string-match-p
(rx bos "+" (* nonl))
(substring-no-properties (elt key 2)))
(my/dump-bindings-recursive
(concat prefix " " (substring-no-properties (car key)))
(+ 2 (or level 0))
buffer))))
(defun my/dump-bindings (prefix)
"Dump keybindings starting with PREFIX in a tree-like form."
(interactive "sPrefix: ")
(let ((buffer (get-buffer-create "bindings")))
(with-current-buffer buffer
(erase-buffer))
(my/dump-bindings-recursive prefix 0 buffer)
(with-current-buffer buffer
(goto-char (point-min))
(setq-local buffer-read-only t))
(switch-to-buffer-other-window buffer)))
Evil
An entire ecosystem of packages that emulates the main features of Vim. Probably the best vim emulator out there.
The only problem is that the package name makes it hard to google anything by just typing “evil”.
References:
Evil-mode
Basic evil configuration.
(use-package evil
:straight t
:init
(setq evil-want-integration t)
(setq evil-want-C-u-scroll t)
(setq evil-want-keybinding nil)
(setq evil-search-module 'evil-search)
(setq evil-split-window-below t)
(setq evil-vsplit-window-right t)
(unless (display-graphic-p)
(setq evil-want-C-i-jump nil))
:config
(evil-mode 1)
;; (setq evil-respect-visual-line-mode t)
(when (fboundp #'undo-tree-undo)
(evil-set-undo-system 'undo-tree))
(when (fboundp #'general-define-key)
(general-define-key
:states '(motion)
"ze" nil)))
Addons
evil-surround emulates one of my favorite vim plugins, surround.vim. Adds a lot of parentheses management options.
(use-package evil-surround
:straight t
:after evil
:config
(global-evil-surround-mode 1))
evil-commentary emulates commentary.vim. It provides actions for quick insertion and deletion of comments.
(use-package evil-commentary
:straight t
:after evil
:config
(evil-commentary-mode))
evil-quickscope emulates quickscope.vim. It highlights certain target characters for f, F, t, T keys.
(use-package evil-quickscope
:straight t
:after evil
:config
:hook ((prog-mode . turn-on-evil-quickscope-mode)
(LaTeX-mode . turn-on-evil-quickscope-mode)
(org-mode . turn-on-evil-quickscope-mode)))
evil-numbers allows incrementing and decrementing numbers at point.
(use-package evil-numbers
:straight t
:commands (evil-numbers/inc-at-pt evil-numbers/dec-at-pt)
:init
(general-nmap
"g+" 'evil-numbers/inc-at-pt
"g-" 'evil-numbers/dec-at-pt))
evil-lion provides alignment operators, somewhat similar to vim-easyalign.
(use-package evil-lion
:straight t
:config
(setq evil-lion-left-align-key (kbd "g a"))
(setq evil-lion-right-align-key (kbd "g A"))
(evil-lion-mode))
evil-matchit makes “%” to match things like tags. It doesn’t work perfectly, so I occasionally turn it off.
(use-package evil-matchit
:straight t
:disabled
:config
(global-evil-matchit-mode 1))
My additions
Do ex search in other buffer. Like *
, but switch to other buffer and search there.
(defun my/evil-ex-search-word-forward-other-window (count &optional symbol)
(interactive (list (prefix-numeric-value current-prefix-arg)
evil-symbol-word-search))
(save-excursion
(evil-ex-start-word-search nil 'forward count symbol))
(other-window 1)
(evil-ex-search-next))
(general-define-key
:states '(normal)
"&" #'my/evil-ex-search-word-forward-other-window)
evil-collection
evil-collection is a package that provides evil bindings for a lot of different packages. One can see the complete list in the modes folder.
(use-package evil-collection
:straight t
:after evil
:config
(evil-collection-init
'(eww devdocs proced emms pass calendar dired debug guix calc
docker ibuffer geiser pdf info elfeed edebug bookmark company
vterm flycheck profiler cider explain-pause-mode notmuch custom
xref eshell helpful compile comint git-timemachine magit prodigy
slime forge deadgrep vc-annonate telega doc-view gnus outline)))
My keybindings
Various keybinding settings that I can’t put anywhere else.
Escape key
Use the escape key instead of No, not really after 2 years… But I’ll keep this fragment.C-g
whenever possible
I must have copied it from somewhere, but as I googled to find out the source, I discovered quite a number of variations of the following code over time. I wonder if Richard Dawkins was inspired by something like this a few decades ago.
(defun minibuffer-keyboard-quit ()
"Abort recursive edit.
In Delete Selection mode, if the mark is active, just deactivate it;
then it takes a second \\[keyboard-quit] to abort the minibuffer."
(interactive)
(if (and delete-selection-mode transient-mark-mode mark-active)
(setq deactivate-mark t)
(when (get-buffer "*Completions*") (delete-windows-on "*Completions*"))
(abort-recursive-edit)))
(defun my/escape-key ()
(interactive)
(evil-ex-nohighlight)
(keyboard-quit))
(general-define-key
:keymaps '(normal visual global)
[escape] #'my/escape-key)
(general-define-key
:keymaps '(minibuffer-local-map
minibuffer-local-ns-map
minibuffer-local-completion-map
minibuffer-local-must-match-map
minibuffer-local-isearch-map)
[escape] 'minibuffer-keyboard-quit)
Home & end
(general-def :states '(normal insert visual)
"<home>" 'beginning-of-line
"<end>" 'end-of-line)
My leader
Using the SPC
key as a leader key, like in Doom Emacs or Spacemacs.
(general-create-definer my-leader-def
:keymaps 'override
:prefix "SPC"
:states '(normal motion emacs))
(general-def :states '(normal motion emacs)
"SPC" nil
"M-SPC" (general-key "SPC"))
(general-def :states '(insert)
"M-SPC" (general-key "SPC" :state 'normal))
(my-leader-def "?" 'which-key-show-top-level)
(my-leader-def "E" 'eval-expression)
(general-def :states '(insert)
"<f1> e" #'eval-expression)
general.el
has a nice integration with which-key, so I use that to show more descriptive annotations for certain groups of keybindings (the default annotation is just prefix
).
(my-leader-def
"a" '(:which-key "apps"))
Universal argument
Change the universal argument to M-u
. I use C-u
to scroll up, as I’m used to from vim.
(general-def
:keymaps 'universal-argument-map
"M-u" 'universal-argument-more)
(general-def
:keymaps 'override
:states '(normal motion emacs insert visual)
"M-u" 'universal-argument)
Profiler
The built-in profiler is a magnificent tool to troubleshoot performance issues.
(my-leader-def
:infix "P"
"" '(:which-key "profiler")
"s" 'profiler-start
"e" 'profiler-stop
"p" 'profiler-report)
Buffer switching
Some keybindings I used in vim to switch buffers and can’t let go of. But I think I started to use these less since I made an attempt in i3 integration.
(general-define-key
:keymaps 'override
"C-<right>" 'evil-window-right
"C-<left>" 'evil-window-left
"C-<up>" 'evil-window-up
"C-<down>" 'evil-window-down
"C-h" 'evil-window-left
"C-l" 'evil-window-right
"C-k" 'evil-window-up
"C-j" 'evil-window-down
"C-x h" 'previous-buffer
"C-x l" 'next-buffer)
(general-define-key
:keymaps 'evil-window-map
"x" 'kill-buffer-and-window
"d" 'kill-current-buffer)
winner-mode
to keep the history of window states.
It doesn’t play too well with perspective.el, that is it has a single history list for all of the perspectives. But it is still quite usable.
(winner-mode 1)
(general-define-key
:keymaps 'evil-window-map
"u" 'winner-undo
"U" 'winner-redo)
Buffer management
The following is necessary since my scratch buffer isn’t lisp-interaction.
(defun my/lisp-interaction-buffer ()
(interactive)
(let ((buf (get-buffer-create "*lisp-interaction*")))
(with-current-buffer buf
(lisp-interaction-mode))
(switch-to-buffer buf)))
(my-leader-def
:infix "b"
"" '(:which-key "buffers")
"s" '(my/lisp-interaction-buffer
:which-key "*lisp-interaction*")
"m" '((lambda () (interactive) (persp-switch-to-buffer "*Messages*"))
:which-key "*Messages*")
"l" 'next-buffer
"h" 'previous-buffer
"k" 'kill-buffer
;; "b" 'persp-ivy-switch-buffer
"b" #'persp-switch-to-buffer*
"r" 'revert-buffer
"u" 'ibuffer)
xref
Some keybindings for xref and go to definition.
(general-nmap
"gD" 'xref-find-definitions-other-window
"gr" 'xref-find-references
"gd" 'evil-goto-definition)
(my-leader-def
"fx" 'xref-find-apropos)
(use-package xref
:straight (:type built-in))
Folding
There are multiple ways to fold text in Emacs.
The most versatile is the built-in hs-minor-mode
, which seems to work out of the box for Lisps, C-like languages, and Python. outline-minor-mode
works for org-mode, LaTeX and the like. There is a 3rd-party solution origami.el, which I found to be somewhat less stable.
Evil does a pretty good job of abstracting all these packages with a set of vim-like keybindings. I was using SPC
in vim, but as now this isn’t an option, I set TAB
to toggle folding.
(require 'hideshow)
(general-define-key
:keymaps '(hs-minor-mode-map outline-minor-mode-map outline-mode-map)
:states '(normal motion)
"ze" 'hs-hide-level
"TAB" 'evil-toggle-fold)
Zoom UI
(defun my/zoom-in ()
"Increase font size by 10 points"
(interactive)
(set-face-attribute 'default nil
:height
(+ (face-attribute 'default :height) 10)))
(defun my/zoom-out ()
"Decrease font size by 10 points"
(interactive)
(set-face-attribute 'default nil
:height
(- (face-attribute 'default :height) 10)))
;; change font size, interactively
(global-set-key (kbd "C-+") 'my/zoom-in)
(global-set-key (kbd "C-=") 'my/zoom-out)
Termux
For some reason my ONYX device has the tilde and escape wrong.
(when (and my/is-termux (not (equal (my/system-name) "snow")))
(define-key key-translation-map (kbd "`") (kbd "<escape>"))
(define-key key-translation-map (kbd "<escape>") (kbd "`")))
And the screen is less wide.
(when my/is-termux
(setq split-width-threshold 90))
i3 integration
UPD post for a somewhat better presentation.
. I have finally switched to EXWM as my window manager, but as long as I keep i3 as a backup solution, this section persists. Check out theOne advantage of EXWM for an Emacs user is that EXWM gives one set of keybindings to manage both Emacs windows and X windows. In every other WM, like my preferred i3wm, two orthogonal keymaps seem to be necessary. But, as both programs are quite customizable, I want to see whether I can replicate at least some part of the EXWM goodness in i3.
But why not just use EXWM? One key reason is that to my taste (and perhaps on my hardware) EXWM didn’t feel snappy enough. Also, I really like i3’s tree-based layout structure; I feel like it fits my workflow much better than anything else I tried, including the master/stack paradigm of XMonad, for instance.
One common point of criticism of i3 is that it is not extensible enough, especially compared to WMs that are configured in an actual programing language, like the mentioned XMonad, Qtile, Awesome, etc. But I think i3’s extensibility is underappreciated, although the contents of this section may lie closer to the limits of how far one can go there.
The basic idea is to launch a normal i3 command with i3-msg
in case the current window is not Emacs, otherwise pass that command to Emacs with emacsclient
. In Emacs, execute the command if possible, otherwise pass the command back to i3.
This may seem like a lot of overhead, but I didn’t feel it even in the worst case (i3 -> Emacs -> i3), so at least in that regard, the interaction feels seamless. The only concern is that this command flow is vulnerable to Emacs getting stuck, but it is still much less of a problem than with EXWM.
One interesting observation here is that Emacs windows and X windows are sort of one-level entities, so I can talk just about “windows”.
At any rate, we need a script to do the i3 -> Emacs part:
if [[ $(xdotool getactivewindow getwindowname) =~ ^emacs(:.*)?@.* ]]; then
command="(my/emacs-i3-integration \"$@\")"
emacsclient -e "$command"
else
i3-msg $@
fi
This script is being run from the i3 configuration.
For this to work, we need to make sure that Emacs starts a server, so here is an expression to do just that:
(unless (or my/remote-server my/nested-emacs)
(add-hook 'after-init-hook #'server-start))
And here is a simple macro to do the Emacs -> i3 part:
(defmacro i3-msg (&rest args)
`(start-process "emacs-i3-windmove" nil "i3-msg" ,@args))
Now we have to handle the required set of i3 commands. It is worth noting here that I’m not trying to implement a general mechanism to apply i3 commands to Emacs, rather I’m implementing a small subset that I use in my i3 configuration and that maps reasonably to the Emacs concepts.
Also, I use evil-mode and generally configure the software to have vim-style bindings where possible. So if you don’t use evil-mode you’d have to detangle the given functions from evil, but then, I guess, you do not use super+hjkl to manage windows either.
First, for the focus
command I want to move to an Emacs window in the given direction if there is one, otherwise move to an X window in the same direction. Fortunately, i3 and windmove have the same names for directions, so the function is rather straightforward.
One caveat here is that the minibuffer is always the bottom-most Emacs window, so it is necessary to check for that as well.
(defun my/emacs-i3-windmove (dir)
(let ((other-window (windmove-find-other-window dir)))
(if (or (null other-window) (window-minibuffer-p other-window))
(i3-msg "focus" (symbol-name dir))
(windmove-do-window-select dir))))
For the move
I want the following behavior:
- if there is space in the required direction, move the Emacs window there;
- if there is no space in the required direction, but space in two orthogonal directions, move the Emacs window so that there is no more space in the orthogonal directions;
- otherwise, move an X window (Emacs frame).
For the first part, window-swap-states
with windmove-find-other-window
do well enough.
evil-move-window
works well for the second part. By itself it doesn’t behave quite like i3, for instance, (evil-move-window 'right)
in a three-column split would move the window from the far left side to the far right side (bypassing center). Hence the combination as described here.
So here is a simple predicate which checks whether there is space in the given direction.
(defun my/emacs-i3-direction-exists-p (dir)
(cl-some (lambda (dir)
(let ((win (windmove-find-other-window dir)))
(and win (not (window-minibuffer-p win)))))
(pcase dir
('width '(left right))
('height '(up down)))))
And the implementation of the move command.
(defun my/emacs-i3-move-window (dir)
(let ((other-window (windmove-find-other-window dir))
(other-direction (my/emacs-i3-direction-exists-p
(pcase dir
('up 'width)
('down 'width)
('left 'height)
('right 'height)))))
(cond
((and other-window (not (window-minibuffer-p other-window)))
(window-swap-states (selected-window) other-window))
(other-direction
(evil-move-window dir))
(t (i3-msg "move" (symbol-name dir))))))
Next on the line are resize grow
and resize shrink
. evil-window-
functions do nicely for this task.
This function also checks whether there is space to resize in the given direction with the help of the predicate defined above. The command is forwarded back to i3 if there is not.
(defun my/emacs-i3-resize-window (dir kind value)
(if (or (one-window-p)
(not (my/emacs-i3-direction-exists-p dir)))
(i3-msg "resize" (symbol-name kind) (symbol-name dir)
(format "%s px or %s ppt" value value))
(setq value (/ value 2))
(pcase kind
('shrink
(pcase dir
('width
(evil-window-decrease-width value))
('height
(evil-window-decrease-height value))))
('grow
(pcase dir
('width
(evil-window-increase-width value))
('height
(evil-window-increase-height value)))))))
transpose-frame is a package to “transpose” the current frame layout, which behaves someone similar to the layout toggle split
command in i3, so I’ll use it as well.
(use-package transpose-frame
:straight t
:commands (transpose-frame))
Finally, the entrypoint for the Emacs integration. In addition to the commands defined above, it processes split
and kill
commands and passes every other command back to i3.
(defun my/emacs-i3-integration (command)
(pcase command
((rx bos "focus")
(my/emacs-i3-windmove
(intern (elt (split-string command) 1))))
((rx bos "move")
(my/emacs-i3-move-window
(intern (elt (split-string command) 1))))
((rx bos "resize")
(my/emacs-i3-resize-window
(intern (elt (split-string command) 2))
(intern (elt (split-string command) 1))
(string-to-number (elt (split-string command) 3))))
("layout toggle split" (transpose-frame))
("split h" (evil-window-split))
("split v" (evil-window-vsplit))
("kill" (evil-quit))
(- (i3-msg command))))
Editing text
Various packages, tricks, and settings that help with the central task of Emacs - editing text.
Indentation & whitespace
Aggressive Indent
A package to keep the code intended.
Doesn’t work too well with many ecosystems because the LSP-based indentation is rather slow but nice for Lisps.
References:
(use-package aggressive-indent
:commands (aggressive-indent-mode)
:straight t)
Delete trailing whitespace
Delete trailing whitespace on save, unless in particular modes where trailing whitespace is important, like Markdown.
(setq my/trailing-whitespace-modes '(markdown-mode))
(require 'cl-extra)
(add-hook 'before-save-hook
(lambda ()
(unless (cl-some #'derived-mode-p my/trailing-whitespace-modes)
(delete-trailing-whitespace))))
Tabs
Some default settings to manage tabs.
(setq tab-always-indent nil)
(setq-default default-tab-width 4)
(setq-default tab-width 4)
(setq-default evil-indent-convert-tabs nil)
(setq-default indent-tabs-mode nil)
(setq-default evil-shift-round nil)
Settings
Scrolling
(setq scroll-conservatively scroll-margin)
(setq scroll-step 1)
(setq scroll-preserve-screen-position t)
(setq scroll-error-top-bottom t)
(setq mouse-wheel-progressive-speed nil)
(setq mouse-wheel-inhibit-click-time nil)
Clipboard
(setq select-enable-clipboard t)
(setq mouse-yank-at-point t)
Backups
(setq backup-inhibited t)
(setq auto-save-default nil)
Undo Tree
Replaces Emacs built-in sequential undo system with a tree-based one. Probably one of the greatest options of Emacs as a text editor.
References:
(use-package undo-tree
:straight t
:config
(global-undo-tree-mode)
(evil-set-undo-system 'undo-tree)
(setq undo-tree-visualizer-diff t)
(setq undo-tree-visualizer-timestamps t)
(setq undo-tree-auto-save-history nil)
(my-leader-def "u" 'undo-tree-visualize)
(fset 'undo-auto-amalgamate 'ignore)
(setq undo-limit 6710886400)
(setq undo-strong-limit 100663296)
(setq undo-outer-limit 1006632960))
Snippets
A snippet system for Emacs and a collection of pre-built snippets.
yasnippet-snippets
has to be loaded before yasnippet
for user snippets to override the pre-built ones.
Edit yasnippet-snippets
, so I’d rather write stuff manually.
References:
(use-package yasnippet-snippets
:disabled
:straight t)
(use-package yasnippet
:straight t
:config
(setq yas-snippet-dirs
`(,(concat (expand-file-name user-emacs-directory) "snippets")
;; yasnippet-snippets-dir
))
(setq yas-triggers-in-field t)
(yas-global-mode 1)
(my-leader-def
:keymaps 'yas-minor-mode-map
:infix "es"
"" '(:wk "yasnippet")
"n" #'yas-new-snippet
"s" #'yas-insert-snippet
"v" #'yas-visit-snippet-file))
(general-imap "M-TAB" 'company-yasnippet)
Input Method
I have to switch layouts all the time, especially in LaTeX documents, because for some reason the Bolsheviks abandoned the idea of replacing Russian Cyrillic letters with Latin ones.
- Me, SystemCrafters/crafter-configs. , in a commit to
Fortunately, Emacs offers a way out of the above with input methods.
References:
- https://protesilaos.com/codelog/2023-12-12-emacs-multilingual-editing/ - A video by Prot from which I learned about this feature.
(setq default-input-method "russian-computer")
I also want to call xkb-switch
in EXWM buffers with the same keybindig.
Guix dependency |
---|
xkb-switch |
(defun my/toggle-input-method ()
(interactive)
(if (derived-mode-p 'exwm-mode)
(my/run-in-background "xkb-switch -n")
(if (or
(not (executable-find "xkb-switch"))
(equal (string-trim
(shell-command-to-string "xkb-switch -p"))
"us"))
(toggle-input-method)
(my/run-in-background "xkb-switch -s us"))))
M-x delete-horizontal-space
doesn’t feel that useful to me.
(general-define-key
:keymaps 'global
"M-\\" #'my/toggle-input-method)
Other small packages
Managing parentheses (smartparens)
A minor mode to deal with pairs. Its functionality overlaps with evil-surround, but smartparens provides the most comfortable way to do stuff like automatically insert pairs.
References:
(use-package smartparens
:straight t)
Visual fill column mode
(use-package visual-fill-column
:straight t
:commands (visual-fill-column-mode)
:config
;; How did it get here?
;; (add-hook 'visual-fill-column-mode-hook
;; (lambda () (setq visual-fill-column-center-text t)))
)
Accents
Input accented characters.
(defvar my/default-accents
'((a . ä)
(o . ö)
(u . ü)
(s . ß)
(A . Ä)
(O . Ö)
(U . Ü)
(S . ẞ)))
(defun my/accent (arg)
(interactive "P")
(require 'accent)
(message "%s" arg)
(let* ((after? (eq accent-position 'after))
(char (if after? (char-after) (char-before)))
(curr (intern (string char)))
(default-diac (cdr (assoc curr my/default-accents))))
(if (and default-diac (not arg))
(progn
(delete-char (if after? 1 -1))
(insert (format "%c" default-diac)))
(call-interactively #'accent-company))))
(use-package accent
:straight (:host github :repo "eliascotto/accent")
:init
(general-define-key
:states '(normal)
"gs" #'accent-company)
(general-define-key
:states '(normal insert)
"M-n" #'my/accent)
:commands (accent-menu)
:config
(general-define-key
:keymaps 'popup-menu-keymap
"C-j" #'popup-next
"C-k" #'popup-previous
"M-j" #'popup-next
"M-k" #'popup-previous)
(setq accent-custom '((a (ā))
(A (Ā)))))
Working with projects
Packages related to managing projects.
I used to have Treemacs here, but in the end decided that dired with dired-sidebar does a better job. Dired has its separate section in “Applications”.
Projectile
Projectile gives a bunch of useful functions for managing projects, like finding files within a project, fuzzy-find, replace, etc.
(use-package projectile
:straight t
:config
(projectile-mode +1)
(setq projectile-project-search-path '("~/Code" "~/Documents"))
(general-define-key
:keymaps 'projectile-command-map
"b" #'consult-project-buffer))
(my-leader-def
"p" '(:keymap projectile-command-map :which-key "projectile"))
(general-nmap "C-p" #'projectile-find-file)
Git & Magit
Magit is a git interface for Emacs.
A few CLI alternatives:
(use-package magit
:straight t
:commands (magit-status magit-file-dispatch)
:init
(my-leader-def
"m" 'magit
"M" 'magit-file-dispatch)
:config
(require 'forge)
(setq magit-blame-styles
'((headings
(heading-format . "%-20a %C %s\n"))
(highlight
(highlight-face . magit-blame-highlight))
(lines
(show-lines . t)
(show-message . t)))))
git-gutter is shows git changes for each line (added/changed/deleted lines).
(use-package git-gutter
:straight t
:config
(global-git-gutter-mode +1))
git-timemachine allows visiting previous versions of a file.
(use-package git-timemachine
:straight t
:commands (git-timemachine))
Guix dependency |
---|
difftastic-bin |
difftastic.el is a wrapper package for difftastic.
(use-package difftastic
:straight t
:commands (difftastic-magit-diff
difftastic-magit-show
difftastic-files
difftastic-buffers)
:init
(with-eval-after-load 'magit-diff
(transient-append-suffix 'magit-diff '(-1 -1)
[("D" "Difftastic diff (dwim)" difftastic-magit-diff)
("S" "Difftastic show" difftastic-magit-show)])
(general-define-key
:keymaps 'magit-blame-read-only-mode-map
:states 'normal
"D" #'difftastic-magit-show
"S" #'difftastic-magit-show))
:config
(setq difftastic-executable (executable-find "difft"))
(general-define-key
:keymaps 'difftastic-mode-map
:states '(normal)
"gr" #'difftastic-rerun
"q" #'kill-buffer-and-window))
My screen isn’t wide enough to run difftastic
in vertical split, so…
(defun my/difftastic-pop-at-bottom (buffer-or-name _requested-width)
(let ((window (split-window-below)))
(select-window window)
(evil-move-window 'below))
(set-window-buffer (selected-window) buffer-or-name))
(setq difftastic-display-buffer-function #'my/difftastic-pop-at-bottom)
And I suspect the built-in window width function doesn’t work as intended because of global-display-line-numbers-mode
.
(setq difftastic-requested-window-width-function
(lambda () (- (frame-width) 4)))
Forge and code-review
forge provides integration with forges, such as GitHub and GitLab.
(use-package forge
:after magit
:straight t
:config
(add-to-list 'forge-alist '("gitlab.etu.ru"
"gitlab.etu.ru/api/v4"
"gitlab.etu.ru"
forge-gitlab-repository)))
forge
depends on a package called ghub. I don’t like that it uses auth-source
to store the token so I’ll advise it to use password-store
.
(defun my/password-store-get-field (entry field)
(if-let (field (password-store-get-field entry field))
field
(my/password-store-get-field entry field)))
(defun my/ghub--token (host username package &optional nocreate forge)
(cond ((and (or (equal host "gitlab.etu.ru/api/v4")
(equal host "gitlab.etu.ru/api"))
(equal username "pvkorytov"))
(my/password-store-get-field
"Job/Digital/Infrastructure/gitlab.etu.ru"
(format "%s-token" package)))
(t (error "Don't know token: %s %s %s" host username package))))
(with-eval-after-load 'ghub
(advice-add #'ghub--token :override #'my/ghub--token))
code-review is a package that implements code review in Emacs. The main branch is broken, but this PR works.
(use-package code-review
:straight (:host github :repo "phelrine/code-review" :branch "fix/closql-update")
:after forge
:config
(setq code-review-auth-login-marker 'forge)
(setq code-review-gitlab-base-url "gitlab.etu.ru")
(setq code-review-gitlab-host "gitlab.etu.ru/api")
(setq code-review-gitlab-graphql-host "gitlab.etu.ru/api")
(general-define-key
:states '(normal visual)
:keymaps '(code-review-mode-map)
"RET" #'code-review-comment-add-or-edit
"gr" #'code-review-reload
"r" #'code-review-transient-api
"s" #'code-review-comment-code-suggestion
"d" #'code-review-submit-single-diff-comment-at-point
"TAB" #'magit-section-toggle)
(general-define-key
:states '(normal)
:keymaps '(forge-topic-mode-map)
"M-RET" #'code-review-forge-pr-at-point))
Fix issue 253:
(defun my/code-review-comment-quit ()
"Quit the comment window."
(interactive)
(magit-mode-quit-window t)
(with-current-buffer (get-buffer code-review-buffer-name)
(goto-char code-review-comment-cursor-pos)
(code-review-comment-reset-global-vars)))
(with-eval-after-load 'code-review
(advice-add #'code-review-comment-quit :override #'my/code-review-comment-quit))
Editorconfig
Editorconfig support for Emacs.
References:
(use-package editorconfig
:straight t
:config
(add-to-list 'editorconfig-indentation-alist
'(emmet-mode emmet-indentation)))
Editing files
A minor mode to remember recently edited files.
(recentf-mode 1)
Save the last place visited in the file.
(save-place-mode nil)
Deadgrep
deadgrep is a nice Emacs interface for ripgrep. Running ivy-occur
in counsel-rg
does something a bit similar, but the deadgrep is more full-featured.
Somehow I couldn’t hook toogle-truncate-lines
into the existing package hooks, so here goes advice.
(defun my/deadgrep-fix-buffer-advice (fun &rest args)
(let ((buf (apply fun args)))
(with-current-buffer buf
(toggle-truncate-lines 1))
buf))
(use-package deadgrep
:straight t
:commands (deadgrep)
:config
(advice-add #'deadgrep--buffer :around #'my/deadgrep-fix-buffer-advice))
Navigation
Things to navigate in Emacs.
Registers
References:
Somehow there’s no built-in function to clear a register.
(defun my/register-clear (register)
(interactive (list (register-read-with-preview "Clear register: ")))
(setq register-alist (delq (assoc register register-alist) register-alist)))
(setq register-preview-delay which-key-idle-delay)
(my-leader-def
:infix "g"
"" '(:wk "registers & marks")
"y" #'copy-to-register
"p" #'insert-register
"o" #'point-to-register
"c" #'my/register-clear
"r" #'jump-to-register
"R" #'consult-register
"w" #'window-configuration-to-register)
Marks
References:
- The Mark and the Region (GNU Emacs Manual)
- Fixing the mark commands in transient mark mode - Mastering Emacs
transient-mark-mode
makes using marks for navigation a bit more cumbersome, but I’m not sure of potential side effects of disabling it… As of now, I want only to push a mark without activating it, so here’s a function for that (taken from Mickey Peterson’s article):
(defun my/push-mark-no-activate ()
"Pushes `point' to `mark-ring' and does not activate the region
Equivalent to \\[set-mark-command] when \\[transient-mark-mode] is disabled"
(interactive)
(push-mark (point) t nil)
(message "Pushed mark to ring"))
Also a function to clear the current mark ring.
(defun my/mark-ring-clear ()
(interactive)
(setq mark-ring nil))
Keybindings:
(my-leader-def
:infix "g"
"G" #'consult-global-mark
"g" #'consult-mark
"C" #'my/mark-ring-clear
"m" #'my/push-mark-no-activate)
(general-define-key
:keymaps 'global
"C-SPC" #'my/push-mark-no-activate)
Avy
Avy is a package that helps navigate Emacs in a tree-like manner.
References:
(use-package avy
:straight t
:config
(setq avy-timeout-seconds 0.5)
(setq avy-ignored-modes
'(image-mode doc-view-mode pdf-view-mode exwm-mode))
(general-define-key
:states '(normal motion)
"-" nil
"--" #'avy-goto-char-2
"-=" #'avy-goto-symbol-1))
ace-link is a package to jump to links with avy.
(use-package ace-link
:straight t
:commands (ace-link-info ace-link-help ace-link-woman ace-link-eww))
Completion
vertico
vertico is a vertical completion framework. I switched to it from Ivy (and to Ivy from Helm).
(use-package vertico
:straight t
:config
(setq enable-recursive-minibuffers t)
(general-define-key
:keymaps '(vertico-map)
"M-j" #'vertico-next
"M-k" #'vertico-previous
"TAB" #'minibuffer-complete)
(vertico-mode))
Add prompt indicator to completing-read-multiple
:
(defun crm-indicator (args)
(cons (format "[CRM%s] %s"
(replace-regexp-in-string
"\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" ""
crm-separator)
(car args))
(cdr args)))
(with-eval-after-load 'crm
(advice-add #'completing-read-multiple :filter-args #'crm-indicator))
Persist history over Emacs restarts.
(use-package savehist
:init
(savehist-mode))
Vertico extensions
Vertico has a lot of extensions.
vertico-directory simplifies directory navigation.
(use-package vertico-directory
:after (vertico)
:config
(general-define-key
:keymaps '(vertico-map)
"RET" #'vertico-directory-enter
"DEL" #'vertico-directory-delete-char)
(add-hook 'rfn-eshadow-update-overlay-hook #'vertico-directory-tidy))
vertico-grid enabled grid display. It is useful when there are no annotations in the completion buffer.
(use-package vertico-grid
:after (vertico))
vertico-multiform enables per-mode configuration.
(defun my/sort-directories-first (files)
(setq files (vertico-sort-alpha files))
(nconc (seq-filter (lambda (x) (string-suffix-p "/" x)) files)
(seq-remove (lambda (x) (string-suffix-p "/" x)) files)))
(use-package vertico-multiform
:after vertico
:config
(vertico-multiform-mode)
(general-define-key
:keymap 'vertico-multiform-map
"M-b" #'vertico-multiform-buffer
"M-g" #'vertico-multiform-grid)
(setq vertico-multiform-categories
'((file (vertico-sort-function . my/sort-directories-first))
(password-store-pass grid)))
(setq vertico-multiform-commands
'((eshell-atuin-history (vertico-sort-function . nil))
(my/index-nav (vertico-sort-function . nil))
(org-ql-view (vertico-sort-function . nil))
(my/consult-line (vertico-sort-function . nil)))))
orderless
orderless is a flexible completion style framework. Completion style refers to the way entries are filtered in the completion buffer.
I used to use prescient.el with Ivy; unlike prescient, orderless doesn’t sort completion entries.
(use-package orderless
:straight t
:config
(setq completion-styles '(orderless basic))
(setq completion-category-defaults nil)
(setq completion-category-overrides
'((file (styles partial-completion))))
(setq orderless-matching-styles
'(orderless-literal orderless-initialism orderless-regexp)))
Disable orderless for company:
(defun company-completion-styles (capf-fn &rest args)
(let ((completion-styles '(basic partial-completion)))
(apply capf-fn args)))
(with-eval-after-load 'company
(advice-add 'company-capf :around #'company-completion-styles))
consult
consult provides various commands based on the completing-read
API.
(use-package consult
:straight t)
marginalia
marginalia provides annotations in the completion interface.
(use-package marginalia
:straight t
:config
(marginalia-mode)
(push '(projectile-find-file . file)
marginalia-command-categories))
embark
embark provides minibuffer actions.
(use-package embark
:straight t
:commands (embark-act embark-dwim embark-bindings)
:init
(general-define-key
"M-e" #'embark-act))
(use-package embark-consult
:straight t
:after (embark)
:config
(add-hook 'embark-collect-mode #'consult-preview-at-point-mode))
Use which-key
like indicator. Take from the Embark wiki.
(defun embark-which-key-indicator ()
"An embark indicator that displays keymaps using which-key.
The which-key help message will show the type and value of the
current target followed by an ellipsis if there are further
targets."
(lambda (&optional keymap targets prefix)
(if (null keymap)
(which-key--hide-popup-ignore-command)
(which-key--show-keymap
(if (eq (plist-get (car targets) :type) 'embark-become)
"Become"
(format "Act on %s '%s'%s"
(plist-get (car targets) :type)
(embark--truncate-target (plist-get (car targets) :target))
(if (cdr targets) "…" "")))
(if prefix
(pcase (lookup-key keymap prefix 'accept-default)
((and (pred keymapp) km) km)
(_ (key-binding prefix 'accept-default)))
keymap)
nil nil t (lambda (binding)
(not (string-suffix-p "-argument" (cdr binding))))))))
(defun embark-hide-which-key-indicator (fn &rest args)
"Hide the which-key indicator immediately when using the completing-read prompter."
(which-key--hide-popup-ignore-command)
(let ((embark-indicators
(remq #'embark-which-key-indicator embark-indicators)))
(apply fn args)))
(with-eval-after-load 'embark
(advice-add #'embark-completing-read-prompter
:around #'embark-hide-which-key-indicator)
(setq embark-indicators (delq #'embark-mixed-indicator embark-indicators))
(push #'embark-which-key-indicator embark-indicators))
keybindings
Setting up quick access to various completions.
(my-leader-def
:infix "f"
"" '(:which-key "various completions")'
"b" #'persp-switch-to-buffer*
"e" 'micromamba-activate
"f" 'project-find-file
"c" 'consult-yank-pop
"a" 'consult-ripgrep
"d" 'deadgrep)
(general-define-key
:states '(insert normal)
"C-y" 'consult-yank-pop)
(defun my/consult-line ()
(interactive)
(if current-prefix-arg
(call-interactively #'consult-line-multi)
(consult-line nil t)))
;; (my-leader-def "SPC SPC" 'ivy-resume)
(my-leader-def "s" 'my/consult-line)
company
A completion framework for Emacs.
References:
(use-package company
:straight t
:config
(global-company-mode)
(setq company-idle-delay 0.2)
(setq company-dabbrev-downcase nil)
(setq company-show-numbers t))
(general-imap "C-SPC" 'company-complete)
A company frontend with nice icons.
Disabled since the base company got icons support and since company-box has some issues with spaceline. Enabled back because I didn’t like spaceline.
(use-package company-box
:straight t
:if (display-graphic-p)
:after (company)
:hook (company-mode . company-box-mode))
Help
- CREDIT: Thanks @phundrak on the System Crafters Discord for suggesting
help-map
helpful package improves the *help*
buffer.
(use-package helpful
:straight t
:commands (helpful-callable
helpful-variable
helpful-key
helpful-macro
helpful-function
helpful-command))
As I use C-h
to switch buffers, I moved the help to SPC-h
with the code below.
(my-leader-def
"h" '(:keymap help-map :which-key "help"))
(my-leader-def
:infix "h"
"" '(:which-key "help")
"h" '(:keymap help-map :which-key "help-map")
"f" 'helpful-function
"k" 'helpful-key
"v" 'helpful-variable
"o" 'helpful-symbol
"i" 'info)
(general-define-key
:keymaps 'help-map
"f" 'helpful-function
"k" 'helpful-key
"v" 'helpful-variable
"o" 'helpful-symbol)
Time trackers
Time trackers I happen to use.
References:
WakaTime
Before I figure out how to package this for Guix:
- Clone the repo
- Run
go build
- Copy the binary to the
~/bin
folder
(use-package wakatime-mode
:straight (:host github :repo "SqrtMinusOne/wakatime-mode")
:if (not (or my/remote-server))
:config
(setq wakatime-ignore-exit-codes '(0 1 102 112))
(advice-add 'wakatime-init :after
(lambda ()
(setq wakatime-cli-path (or
(executable-find "wakatime-cli")
(expand-file-name "~/bin/wakatime-cli")))))
(when (file-exists-p "~/.wakatime.cfg")
(setq wakatime-api-key
(string-trim
(shell-command-to-string "awk '/api-key/{print $NF}' ~/.wakatime.cfg"))))
;; (setq wakatime-cli-path (executable-find "wakatime"))
(global-wakatime-mode))
ActivityWatch
(use-package request
:straight t
:defer t)
(use-package activity-watch-mode
:straight t
:if (not (or my/is-termux my/remote-server))
:config
(global-activity-watch-mode))
UI settings
General settings
Miscellaneous
Disable GUI elements
(unless my/is-termux
(tool-bar-mode -1)
(menu-bar-mode -1)
(scroll-bar-mode -1))
(when my/is-termux
(menu-bar-mode -1))
Transparency. Not setting it here, as I used to use picom with i3, and EXWM config has its own settings.
;; (set-frame-parameter (selected-frame) 'alpha '(90 . 90))
;; (add-to-list 'default-frame-alist '(alpha . (90 . 90)))
Prettify symbols. Also not setting it, ligatures seem to be enough for me.
;; (global-prettify-symbols-mode)
Do not show GUI dialogs
(setq use-dialog-box nil)
No start screen
(setq inhibit-startup-screen t)
Visual bell
(setq visible-bell 0)
y or n instead of yes or no
(defalias 'yes-or-no-p 'y-or-n-p)
Hide mouse cursor while typing
(setq make-pointer-invisible t)
Show pairs
(show-paren-mode 1)
Highlight the current line
(global-hl-line-mode 1)
Line numbers
Line numbers. There seems to be a catch with the relative number setting:
visual
doesn’t take folding into account but also doesn’t take wrapped lines into account (i.e. there are multiple numbers for a single wrapped line)relative
makes a single number for a wrapped line, but counts folded lines.
visual
option seems to be less of a problem in most cases.
(global-display-line-numbers-mode 1)
(line-number-mode nil)
(setq display-line-numbers-type 'visual)
(column-number-mode)
Word wrapping
Word wrapping. These settings aren’t too obvious compared to :set wrap
from vim:
word-wrap
means just “don’t split one word between two lines”. So, if there isn’t enough place to put a word at the end of the line, it will be put on a new one. RunM-x toggle-word-wrap
to toggle that.visual-line-mode
seems to be a superset ofword-wrap
. It also enables some editing commands to work on visual lines instead of logical ones, hence the naming.auto-fill-mode
does the same asword-wrap
, except it actually edits the buffer to make lines break in the appropriate places.truncate-lines
truncates long lines instead of continuing them. RunM-x toggle-truncate-lines
to toggle that. I find thattruncate-lines
behaves strangely whenvisual-line-mode
is on, so I use one or another.
(setq word-wrap 1)
(global-visual-line-mode 1)
Custom frame format
Title format, which used to look something like emacs:project@hostname
. Now it’s just emacs
.
(setq-default frame-title-format
'(""
"emacs"
;; (:eval
;; (let ((project-name (projectile-project-name)))
;; (if (not (string= "-" project-name))
;; (format ":%s@%s" project-name (system-name))
;; (format "@%s" (system-name)))))
))
Olivetti
Olivetti is a package that limits the current text body width. It’s pretty nice to use when writing texts.
(use-package olivetti
:straight t
:if (display-graphic-p)
:commands (olivetti-mode)
:config
(setq-default olivetti-body-width 86))
Keycast
Showing the last pressed key. Occasionally useful.
(use-package keycast
:straight t
:init
(define-minor-mode keycast-mode
"Keycast mode"
:global t
(if keycast-mode
(progn
(add-to-list 'global-mode-string '("" keycast-mode-line " "))
(add-hook 'pre-command-hook 'keycast--update t) )
(remove-hook 'pre-command-hook 'keycast--update)
(setq global-mode-string (delete '("" keycast-mode-line " ") global-mode-string))))
:commands (keycast--update))
Themes and colors
Theme packages
My colorschemes of choice.
(use-package doom-themes
:straight t
;; Not deferring becuase I want `doom-themes-visual-bell-config'
:config
(setq doom-themes-enable-bold t
doom-themes-enable-italic t)
;; (if my/remote-server
;; (load-theme 'doom-gruvbox t)
;; (load-theme 'doom-palenight t))
(doom-themes-visual-bell-config)
(setq doom-themes-treemacs-theme "doom-colors")
(doom-themes-treemacs-config))
(use-package modus-themes
:straight t)
Let’s see…
(use-package ef-themes
:straight t
:config
(setq ef-duo-light-palette-overrides
'((constant green))))
Custom theme
Here I define a few things on the top of Emacs theme, because:
- Occasionally I want to have more theme-derived faces
- I also want Emacs theme to be applied to the rest of the system (see the Desktop config on that)
Theme-derived faces have to placed in a custom theme, because if one calls custom-set-faces
and custom-set-variables
in code, whenever a variable is changed and saved in a customize buffer, data from all calls of these functions is saved as well.
Get color values
Here’s a great package with various color tools:
(use-package ct
:straight t)
As of now I want this to support doom-themes
and modus-themes
. So, let’s get which one is enabled:
(defun my/doom-p ()
(seq-find (lambda (x) (string-match-p (rx bos "doom") (symbol-name x)))
custom-enabled-themes))
(defun my/modus-p ()
(seq-find (lambda (x) (string-match-p (rx bos "modus") (symbol-name x)))
custom-enabled-themes))
(defun my/ef-p ()
(seq-find (lambda (x) (string-match-p (rx bos "ef") (symbol-name x)))
custom-enabled-themes))
I also want to know if the current theme is light or not:
(defun my/light-p ()
(ct-light-p (my/color-value 'bg)))
(defun my/dark-p ()
(not (my/light-p)))
Now, let’s get the current color from doom
. doom-themes
provide doom-color
, but I also want to:
- override some colors
- add
black
,white
,light-*
andborder
(defconst my/theme-override
'((doom-palenight
(red . "#f07178"))))
(defvar my/alpha-for-light 7)
(defun my/doom-color (color)
(when (doom-color 'bg)
(let ((override (alist-get (my/doom-p) my/theme-override))
(color-name (symbol-name color))
(is-light (ct-light-p (doom-color 'bg))))
(or
(alist-get color override)
(cond
((eq 'black color)
(if is-light (doom-color 'fg) (doom-color 'bg)))
((eq 'white color)
(if is-light (doom-color 'bg) (doom-color 'fg)))
((eq 'border color)
(if is-light (doom-color 'base0) (doom-color 'base8)))
((string-match-p (rx bos "light-") color-name)
(ct-edit-hsl-l-inc (my/doom-color (intern (substring color-name 6)))
my/alpha-for-light))
((string-match-p (rx bos "dark-") color-name)
(or (doom-color color)
(ct-edit-hsl-l-dec (my/doom-color (intern (substring color-name 5)))
my/alpha-for-light)))
(t (doom-color color)))))))
And the same for modus-themes
. my/modus-color
has to accept the same arguments as I use for my/doom-color
for backward compatibility, which requires a bit more tuning.
(defun my/modus-get-base (color)
(let ((base-value (string-to-number (substring (symbol-name color) 4 5)))
(base-start (cadr (assoc 'bg-main (modus-themes--current-theme-palette))))
(base-end (cadr (assoc 'fg-dim (modus-themes--current-theme-palette)))))
(nth base-value (ct-gradient 9 base-start base-end t))))
(defun my/prot-color (color palette)
(let ((is-light (ct-light-p (cadr (assoc 'bg-main palette)))))
(cond
((member color '(black white light-black light-white))
(let ((bg-main (cadr (assoc 'bg-main palette)))
(fg-main (cadr (assoc 'fg-main palette))))
(pcase color
('black (if is-light fg-main bg-main))
('white (if is-light bg-main fg-main))
('light-black (ct-edit-hsl-l-inc
(if is-light fg-main bg-main)
15))
('light-white (ct-edit-hsl-l-inc
(if is-light bg-main fg-main)
15)))))
((or (eq color 'bg))
(cadr (assoc 'bg-main palette)))
((or (eq color 'fg))
(cadr (assoc 'fg-main palette)))
((eq color 'bg-alt)
(cadr (assoc 'bg-dim palette)))
((eq color 'violet)
(cadr (assoc 'magenta-cooler palette)))
((string-match-p (rx bos "base" digit) (symbol-name color))
(my/modus-get-base color))
((string-match-p (rx bos "dark-") (symbol-name color))
(cadr (assoc (intern (format "%s-cooler" (substring (symbol-name color) 5)))
palette)))
((eq color 'grey)
(my/modus-get-base 'base5))
((string-match-p (rx bos "light-") (symbol-name color))
(or
(cadr (assoc (intern (format "%s-intense" (substring (symbol-name color) 6))) palette))
(cadr (assoc (intern (format "bg-%s-intense" (substring (symbol-name color) 6))) palette))))
(t (cadr (assoc color palette))))))
(defun my/modus-color (color)
(my/prot-color color (modus-themes--current-theme-palette)))
(defun my/ef-color (color)
(my/prot-color color (ef-themes--current-theme-palette)))
Test the three functions.
(defconst my/test-colors-list
'(black red green yellow blue magenta cyan white light-black
dark-red dark-green dark-yellow dark-blue dark-magenta dark-cyan
light-red light-green light-yellow light-blue light-magenta
light-cyan light-white bg bg-alt fg fg-alt violet grey base0 base1
base2 base3 base4 base5 base6 base7 base8 border))
(defun my/test-colors ()
(interactive)
(let ((buf (generate-new-buffer "*colors-test*")))
(with-current-buffer buf
(insert (format "%-20s %-10s %-10s %-10s" "Color" "Doom" "Modus" "Ef") "\n")
(cl-loop for color in my/test-colors-list
do (insert
(format "%-20s %-10s %-10s %-10s\n"
(prin1-to-string color)
(my/doom-color color)
(my/modus-color color)
(my/ef-color color))))
(special-mode)
(rainbow-mode))
(switch-to-buffer buf)))
Finally, one function to get the value of a color in the current theme.
(defun my/color-value (color)
(cond
((stringp color) (my/color-value (intern color)))
((eq color 'bg-other)
(or (my/color-value 'bg-dim)
(let ((color (my/color-value 'bg)))
(if (ct-light-p color)
(ct-edit-hsl-l-dec color 2)
(ct-edit-hsl-l-dec color 3)))))
((eq color 'modeline)
(or
(my/color-value 'bg-mode-line-active)
(my/color-value 'bg-mode-line)
(if (my/light-p)
(ct-edit-hsl-l-dec (my/color-value 'bg-alt) 10)
(ct-edit-hsl-l-inc (my/color-value 'bg-alt) 15))))
((my/doom-p) (my/doom-color color))
((my/modus-p) (my/modus-color color))
((my/ef-p) (my/ef-color color))))
And a few more functions
Custom theme
So, the custom theme:
(deftheme my-theme-1)
A macro to simplify defining custom colors.
(defvar my/my-theme-update-color-params nil)
(defmacro my/use-colors (&rest data)
`(progn
,@(cl-loop for i in data collect
`(setf (alist-get ',(car i) my/my-theme-update-color-params)
(list ,@(cl-loop for (key value) on (cdr i) by #'cddr
append `(,key ',value)))))
(when (and (or (my/doom-p) (my/modus-p)) my/emacs-started)
(my/update-my-theme))))
This macro puts lambdas to my/my-theme-update-colors-hook
that updates faces in my-theme-1
. Now I have to call this hook:
(defun my/update-my-theme (&rest _)
(interactive)
(cl-loop for (face . values) in my/my-theme-update-color-params
do (custom-theme-set-faces
'my-theme-1
`(,face ((t ,@(cl-loop for (key value) on values by #'cddr
collect key
collect (eval value)))))))
(enable-theme 'my-theme-1))
(unless my/is-termux
(advice-add 'load-theme :after #'my/update-my-theme)
(add-hook 'emacs-startup-hook #'my/update-my-theme))
Defining colors for tab-bar.el
:
(my/use-colors
(tab-bar-tab :background (my/color-value 'bg)
:foreground (my/color-value 'yellow)
:underline (my/color-value 'yellow))
(tab-bar :background 'unspecified :foreground 'unspecified)
(magit-section-secondary-heading :foreground (my/color-value 'blue)
:weight 'bold))
Switch theme
The built-in load-theme
does not deactivate the previous theme, so here’s a function that does that:
(defun my/switch-theme (theme)
(interactive
(list (intern (completing-read "Load custom theme: "
(mapcar #'symbol-name
(custom-available-themes))))))
(cl-loop for enabled-theme in custom-enabled-themes
if (not (or (eq enabled-theme 'my-theme-1)
(eq enabled-theme theme)))
do (disable-theme enabled-theme))
(load-theme theme t)
(when current-prefix-arg
(my/regenerate-desktop)))
(if my/is-termux
(progn
(my/switch-theme 'modus-operandi-tinted))
(my/switch-theme 'ef-duo-light))
Extending current theme
Colors that aren’t set in themes.
(with-eval-after-load 'transient
(my/use-colors
(transient-key-exit :foreground (my/color-value 'dark-red))
(transient-key-noop :foreground (my/color-value 'grey))
(transient-key-return :foreground (my/color-value 'yellow))
(transient-key-stay :foreground (my/color-value 'green))))
Dim inactive buffers
Dim inactive buffers.
(use-package auto-dim-other-buffers
:straight t
:if (display-graphic-p)
:config
(auto-dim-other-buffers-mode t)
(my/use-colors
(auto-dim-other-buffers-face
:background (my/color-value 'bg-other))))
ANSI colors
ansi-color.el
is a built-in Emacs package that translates ANSI color escape codes into faces.
It is used by many other packages but doesn’t seem to have an integration with doom-themes
, so here is one.
(with-eval-after-load 'ansi-color
(my/use-colors
(ansi-color-black
:foreground (my/color-value 'base2) :background (my/color-value 'base0))
(ansi-color-red
:foreground (my/color-value 'red) :background (my/color-value 'red))
(ansi-color-green
:foreground (my/color-value 'green) :background (my/color-value 'green))
(ansi-color-yellow
:foreground (my/color-value 'yellow) :background (my/color-value 'yellow))
(ansi-color-blue
:foreground (my/color-value 'dark-blue) :background (my/color-value 'dark-blue))
(ansi-color-magenta
:foreground (my/color-value 'violet) :background (my/color-value 'violet))
(ansi-color-cyan
:foreground (my/color-value 'dark-cyan) :background (my/color-value 'dark-cyan))
(ansi-color-white
:foreground (my/color-value 'base8) :background (my/color-value 'base8))
(ansi-color-bright-black
:foreground (my/color-value 'base5) :background (my/color-value 'base5))
(ansi-color-bright-red
:foreground (my/color-value 'orange) :background (my/color-value 'orange))
(ansi-color-bright-green
:foreground (my/color-value 'teal) :background (my/color-value 'teal))
(ansi-color-bright-yellow
:foreground (my/color-value 'yellow) :background (my/color-value 'yellow))
(ansi-color-bright-blue
:foreground (my/color-value 'blue) :background (my/color-value 'blue))
(ansi-color-bright-magenta
:foreground (my/color-value 'magenta) :background (my/color-value 'magenta))
(ansi-color-bright-cyan
:foreground (my/color-value 'cyan) :background (my/color-value 'cyan))
(ansi-color-bright-white
:foreground (my/color-value 'fg) :background (my/color-value 'fg))))
Fonts
Frame font
To install a font, download the font and unpack it into the .local/share/fonts
directory. Create one if it doesn’t exist.
As I use nerd fonts elsewhere, I use one in Emacs as well.
References:
(when (display-graphic-p)
(if (x-list-fonts "JetBrainsMono Nerd Font")
(let ((font "-JB -JetBrainsMono Nerd Font-medium-normal-normal-*-17-*-*-*-m-0-iso10646-1"))
(set-frame-font font nil t)
(add-to-list 'default-frame-alist `(font . ,font)))
(message "Install JetBrainsMono Nerd Font!")))
To make the icons work (e.g. in the Doom Modeline), run M-x all-the-icons-install-fonts
. The package definition is somewhere later in the config.
Other fonts
(when (display-graphic-p)
(set-face-attribute 'variable-pitch nil :family "Cantarell" :height 1.0)
(set-face-attribute
'italic nil
:family "JetBrainsMono Nerd Font"
:weight 'regular
:slant 'italic))
Ligatures
Ligature setup for the JetBrainsMono font.
(use-package ligature
:straight (:host github :repo "mickeynp/ligature.el")
:if (display-graphic-p)
:config
(ligature-set-ligatures
'(
typescript-mode
typescript-ts-mode
js2-mode
javascript-ts-mode
vue-mode
svelte-mode
scss-mode
php-mode
python-mode
python-ts-mode
js-mode
markdown-mode
clojure-mode
go-mode
sh-mode
haskell-mode
web-mode)
'("--" "---" "==" "===" "!=" "!==" "=!=" "=:=" "=/=" "<="
">=" "&&" "&&&" "&=" "++" "+++" "***" ";;" "!!" "??"
"?:" "?." "?=" "<:" ":<" ":>" ">:" "<>" "<<<" ">>>"
"<<" ">>" "||" "-|" "_|_" "|-" "||-" "|=" "||=" "##"
"###" "####" "#{" "#[" "]#" "#(" "#?" "#_" "#_(" "#:"
"#!" "#=" "^=" "<$>" "<$" "$>" "<+>" "<+" "+>" "<*>"
"<*" "*>" "</" "</>" "/>" "<!--" "<#--" "-->" "->" "->>"
"<<-" "<-" "<=<" "=<<" "<<=" "<==" "<=>" "<==>" "==>" "=>"
"=>>" ">=>" ">>=" ">>-" ">-" ">--" "-<" "-<<" ">->" "<-<"
"<-|" "<=|" "|=>" "|->" "<->" "<~~" "<~" "<~>" "~~" "~~>"
"~>" "~-" "-~" "~@" "[||]" "|]" "[|" "|}" "{|" "[<"
">]" "|>" "<|" "||>" "<||" "|||>" "<|||" "<|>" "..." ".."
".=" ".-" "..<" ".?" "::" ":::" ":=" "::=" ":?" ":?>"
"//" "///" "/*" "*/" "/=" "//=" "/==" "@_" "__"))
(global-ligature-mode t))
Icons
I switched to nerd-icons from all-the-icons.
Run M-x all-the-icons-install-fonts
at first setup.
(use-package nerd-icons
:straight t)
Text highlight
Highlight indent guides.
(use-package highlight-indent-guides
:straight t
:if (not (or my/remote-server))
:hook ((prog-mode . highlight-indent-guides-mode)
(LaTeX-mode . highlight-indent-guides-mode))
:config
(setq highlight-indent-guides-method 'bitmap)
(setq highlight-indent-guides-bitmap-function 'highlight-indent-guides--bitmap-line))
Rainbow parentheses.
(use-package rainbow-delimiters
:straight t
:hook ((prog-mode . rainbow-delimiters-mode)))
Highlight colors
(use-package rainbow-mode
:commands (rainbow-mode)
:straight t)
Highlight TODOs and stuff
(use-package hl-todo
:hook (prog-mode . hl-todo-mode)
:straight t)
Doom Modeline
A modeline from Doom Emacs. A big advantage of this package is that it just works out of the box and does not require much customization.
I tried a bunch of other options, including spaceline, but in the end, decided that Doom Modeline works best for me.
References:
(use-package doom-modeline
:straight t
;; :if (not (display-graphic-p))
:init
(setq doom-modeline-env-enable-python nil)
(setq doom-modeline-env-enable-go nil)
(setq doom-modeline-buffer-encoding 'nondefault)
(setq doom-modeline-hud t)
(setq doom-modeline-persp-icon nil)
(setq doom-modeline-persp-name nil)
(setq doom-modeline-display-misc-in-all-mode-lines nil)
(when my/is-termux
(setopt doom-modeline-icon nil))
:config
(setq doom-modeline-minor-modes nil)
(setq doom-modeline-irc nil)
(setq doom-modeline-buffer-state-icon nil)
(doom-modeline-mode 1))
Doom Modeline as Tab Bar
(defun my/tab-bar-mode-line--format ()
(unless (derived-mode-p 'company-box-mode)
(cl-letf (((symbol-function 'window-pixel-width)
'frame-pixel-width)
((symbol-function 'window-margins)
(lambda (&rest _)
(list nil))))
(let ((doom-modeline-window-width-limit nil)
(doom-modeline--limited-width-p nil))
(format-mode-line
'("%e"
(:eval
(doom-modeline-format--main))))))))
(defun my/hide-mode-line-if-only-window ()
(let* ((windows (window-list))
(hide-mode-line-p (length= windows 1)))
(dolist (win windows)
(with-current-buffer (window-buffer win)
(unless (eq hide-mode-line-p hide-mode-line-mode)
(hide-mode-line-mode
(if hide-mode-line-p +1 -1)))))))
(define-minor-mode my/tab-bar-mode-line-mode
"Use tab-bar as mode line mode."
:global t
(if my/tab-bar-mode-line-mode
(progn
(tab-bar-mode +1)
(setq tab-bar-format '(my/tab-bar-mode-line--format))
(set-face-attribute 'tab-bar nil :inherit 'mode-line)
(add-hook 'window-configuration-change-hook #'my/hide-mode-line-if-only-window)
(dolist (buf (buffer-list))
(with-current-buffer buf
(doom-modeline-set-modeline 'minimal)))
(doom-modeline-set-modeline 'minimal 'default)
(dolist (frame (frame-list))
(with-selected-frame frame
(my/hide-mode-line-if-only-window))
(when-let (cb-frame (company-box--get-frame frame))
(set-frame-parameter cb-frame 'tab-bar-lines 0)))
(setenv "POLYBAR_BOTTOM" "false")
(when (fboundp #'my/exwm-run-polybar)
(my/exwm-run-polybar)))
(tab-bar-mode -1)
(setq tab-bar-format
'(tab-bar-format-history tab-bar-format-tabs tab-bar-separator tab-bar-format-add-tab))
(set-face-attribute 'tab-bar nil :inherit 'default)
(remove-hook 'window-configuration-change-hook #'my/hide-mode-line-if-only-window)
(global-hide-mode-line-mode -1)
(dolist (buf (buffer-list))
(with-current-buffer buf
(doom-modeline-set-modeline 'main)))
(doom-modeline-set-modeline 'main 'default)
(setenv "POLYBAR_BOTTOM" "true")
(when (fboundp #'my/exwm-run-polybar)
(my/exwm-run-polybar))))
perspective.el
perspective.el is a package that groups buffers in “perspectives”.
tab-bar.el
can be configured to behave in a similar way, but I’m too invested in this package already.
One thing I don’t like is that the list perspectives is displayed in the modeline, but I’ll probably look how to move them to the bar at the top of the frame at some point.
(use-package perspective
:straight t
:init
;; (setq persp-show-modestring 'header)
(setq persp-sort 'created)
(setq persp-suppress-no-prefix-key-warning t)
:config
(persp-mode)
(my-leader-def "x" '(:keymap perspective-map :which-key "perspective"))
(general-define-key
:keymaps 'override
:states '(normal emacs)
"gt" 'persp-next
"gT" 'persp-prev
"gn" 'persp-switch
"gN" 'persp-kill)
(general-define-key
:keymaps 'perspective-map
"b" 'persp-switch-to-buffer
"x" 'persp-switch-to-buffer*
"u" 'persp-ibuffer))
Functions to manage buffers
Move the current buffer to a perspective and switch to it.
(defun my/persp-move-window-and-switch ()
(interactive)
(let* ((buffer (current-buffer)))
(call-interactively #'persp-switch)
(persp-set-buffer (buffer-name buffer))
(switch-to-buffer buffer)))
Copy the current buffer to a perspective and switch to it.
(defun my/persp-copy-window-and-switch ()
(interactive)
(let* ((buffer (current-buffer)))
(call-interactively #'persp-switch)
(persp-add-buffer (buffer-name buffer))
(switch-to-buffer buffer)))
Add keybindings to the default map.
(with-eval-after-load 'perspective
(general-define-key
:keymaps 'perspective-map
"m" #'my/persp-move-window-and-switch
"f" #'my/persp-copy-window-and-switch))
Automating perspectives
Out-of-the-box, perspective.el
doesn’t feature much (or any) capacity for automation. We’re supposed to manually assign buffers to perspectives, which kinda makes sense… But I still want automation.
First, let’s define a variable with “rules”:
(setq my/perspective-assign-alist '())
One rule looks as follows:
(major-mode workspace-index persp-name)
And a function to act on these rules.
(defvar my/perspective-assign-ignore nil
"If non-nil, ignore `my/perspective-assign'")
(defun my/perspective-assign ()
(when-let* ((_ (not my/perspective-assign-ignore))
(rule (alist-get major-mode my/perspective-assign-alist)))
(let ((workspace-index (car rule))
(persp-name (cadr rule))
(buffer (current-buffer)))
(if (fboundp #'perspective-exwm-assign-window)
(progn
(perspective-exwm-assign-window
:workspace-index workspace-index
:persp-name persp-name)
(when workspace-index
(exwm-workspace-switch workspace-index))
(when persp-name
(persp-switch persp-name)))
(with-perspective persp-name
(persp-set-buffer buffer))
(persp-switch-to-buffer buffer)))))
Also advise to ignore the assignment:
(defun my/perspective-assign-ignore-advice (fun &rest args)
(let ((my/perspective-assign-ignore t))
(apply fun args)))
If EXWM is available, then so is mine perspective-exwm
package, which features a convenient procedure called perspective-exwm-assign-window
. Otherwise, we just work with perspectives.
Now, we have to put this function somewhere, and after-change-major-mode-hook
seems like a perfect place for it.
(add-hook 'after-change-major-mode-hook #'my/perspective-assign)
And here is a simple macro to add rules to the list.
(defmacro my/persp-add-rule (&rest body)
(declare (indent 0))
(unless (= (% (length body) 3) 0)
(error "Malformed body in my/persp-add-rule"))
(let (result)
(while body
(let ((major-mode (pop body))
(workspace-index (pop body))
(persp-name (pop body)))
(push
`(add-to-list 'my/perspective-assign-alist
'(,major-mode . (,workspace-index ,persp-name)))
result)))
`(progn
,@result)))
Also, the logic above works only for cases when the buffer is created. Occasionally, packages run switch-to-buffer
, which screws both EXWM workspaces and perspectives; to work around that, I define a macro that runs a command in the context of a given perspective and workspace.
(defmacro my/command-in-persp (command-name persp-name workspace-index &rest args)
`'((lambda ()
(interactive)
(when (and ,workspace-index (fboundp #'exwm-workspace-switch-create))
(exwm-workspace-switch-create ,workspace-index))
(persp-switch ,persp-name)
(delete-other-windows)
,@args)
:wk ,command-name))
This is meant to be used in the definitions of general.el
.
Programming
General setup
Treemacs
Treemacs is a rather large & powerful package, but as of now I’ve replaced it with dired. I still have a small configuration because lsp-mode and dap-mode depend on it.
(use-package treemacs
:straight t
:defer t
:config
;; (setq treemacs-follow-mode nil)
;; (setq treemacs-follow-after-init nil)
(setq treemacs-space-between-root-nodes nil)
;; (treemacs-git-mode 'extended)
;; (add-to-list 'treemacs-pre-file-insert-predicates #'treemacs-is-file-git-ignored?)
(general-define-key
:keymaps 'treemacs-mode-map
[mouse-1] #'treemacs-single-click-expand-action
"M-l" #'treemacs-root-down
"M-h" #'treemacs-root-up
"q" #'treemacs-quit)
(general-define-key
:keymaps 'treemacs-mode-map
:states '(normal emacs)
"q" 'treemacs-quit))
(use-package treemacs-evil
:after (treemacs evil)
:straight t)
LSP
LSP-mode provides an IDE-like experience for Emacs - real-time diagnostics, code actions, intelligent autocompletion, etc.
References:
Setup
(use-package lsp-mode
:straight t
:if (not (or my/is-termux my/remote-server))
:hook (
(typescript-mode . lsp)
(js-mode . lsp)
(vue-mode . lsp)
(go-mode . lsp)
(svelte-mode . lsp)
;; (python-mode . lsp)
(json-mode . lsp)
(haskell-mode . lsp)
(haskell-literate-mode . lsp)
(java-mode . lsp)
;; (csharp-mode . lsp)
)
:commands lsp
:init
(setq lsp-keymap-prefix nil)
:config
(setq lsp-idle-delay 1)
(setq lsp-eslint-server-command '("node" "/home/pavel/.emacs.d/.cache/lsp/eslint/unzipped/extension/server/out/eslintServer.js" "--stdio"))
(setq lsp-eslint-run "onSave")
(setq lsp-signature-render-documentation nil)
;; (lsp-headerline-breadcrumb-mode nil)
(setq lsp-headerline-breadcrumb-enable nil)
(setq lsp-modeline-code-actions-enable nil)
(setq lsp-modeline-diagnostics-enable nil)
(add-to-list 'lsp-language-id-configuration '(svelte-mode . "svelte")))
(use-package lsp-ui
:straight t
:commands lsp-ui-mode
:config
(setq lsp-ui-doc-delay 2)
(setq lsp-ui-sideline-show-hover nil))
t
Integrations
The only integration left now is treemacs.
Origami should’ve leveraged LSP folding, but it was too unstable at the moment I tried it.
;; (use-package helm-lsp
;; :straight t
;; :commands helm-lsp-workspace-symbol)
;; (use-package origami
;; :straight t
;; :hook (prog-mode . origami-mode))
;; (use-package lsp-origami
;; :straight t
;; :config
;; (add-hook 'lsp-after-open-hook #'lsp-origami-try-enable))
(use-package lsp-treemacs
:after (lsp)
:straight t
:commands lsp-treemacs-errors-list)
Keybindings
(my-leader-def
:infix "l"
"" '(:which-key "lsp")
"d" 'lsp-ui-peek-find-definitions
"r" 'lsp-rename
"u" 'lsp-ui-peek-find-references
"s" 'lsp-ui-find-workspace-symbol
"l" 'lsp-execute-code-action
"e" 'list-flycheck-errors)
UI
I don’t like how some language servers print the full filename in the progress indicator.
(defun my/lsp--progress-status ()
"Returns the status of the progress for the current workspaces."
(-let ((progress-status
(s-join
"|"
(-keep
(lambda (workspace)
(let ((tokens (lsp--workspace-work-done-tokens workspace)))
(unless (ht-empty? tokens)
(mapconcat
(-lambda ((&WorkDoneProgressBegin :message? :title :percentage?))
(concat (if percentage?
(if (numberp percentage?)
(format "%.0f%%%% " percentage?)
(format "%s%%%% " percentage?))
"")
(let ((msg (url-unhex-string (or message\? title))))
(if (string-match-p "\\`file:///" msg)
(file-name-nondirectory msg)))))
(ht-values tokens)
"|"))))
(lsp-workspaces)))))
(unless (s-blank? progress-status)
(concat lsp-progress-prefix progress-status))))
(with-eval-after-load 'lsp-mode
(advice-add 'lsp--progress-status :override #'my/lsp--progress-status))
Flycheck
A syntax checking extension for Emacs. Integrates with LSP-mode, but can also use various standalone checkers.
References:
(use-package flycheck
:straight t
:config
(global-flycheck-mode)
(setq flycheck-check-syntax-automatically '(save idle-buffer-switch mode-enabled))
;; (add-hook 'evil-insert-state-exit-hook
;; (lambda ()
;; (if flycheck-checker
;; (flycheck-buffer))
;; ))
(advice-add 'flycheck-eslint-config-exists-p :override (lambda() t))
(add-to-list 'display-buffer-alist
`(,(rx bos "*Flycheck errors*" eos)
(display-buffer-reuse-window
display-buffer-in-side-window)
(side . bottom)
(reusable-frames . visible)
(window-height . 0.33))))
General additional config
Have to put this before tree-sitter because I need my/set-smartparens-indent
there.
Make smartparens behave the way I like for C-like languages.
(defun my/set-smartparens-indent (mode)
(sp-local-pair mode "{" nil :post-handlers '(("|| " "SPC") ("||\n[i]" "RET")))
(sp-local-pair mode "[" nil :post-handlers '(("|| " "SPC") ("||\n[i]" "RET")))
(sp-local-pair mode "(" nil :post-handlers '(("|| " "SPC") ("||\n[i]" "RET"))))
Override flycheck checker with eslint.
(defun my/set-flycheck-eslint()
"Override flycheck checker with eslint."
(setq-local lsp-diagnostic-package :none)
(setq-local flycheck-checker 'javascript-eslint))
Tree Sitter
Tree-Sitter integration with Emacs 29.
References:
(use-package treesit
:straight (:type built-in)
:if (featurep 'treesit)
:config
(setq treesit-language-source-alist
(mapcar
(lambda (item)
(let ((lang (nth 0 item))
(url (nth 1 item))
(rev (nth 2 item))
(source-dir (nth 3 item)))
`(,lang ,url ,rev ,source-dir
,(executable-find "gcc") ,(executable-find "c++"))))
'((bash "https://github.com/tree-sitter/tree-sitter-bash")
(cmake "https://github.com/uyha/tree-sitter-cmake")
(css "https://github.com/tree-sitter/tree-sitter-css")
(elisp "https://github.com/Wilfred/tree-sitter-elisp")
(go "https://github.com/tree-sitter/tree-sitter-go")
(html "https://github.com/tree-sitter/tree-sitter-html")
(javascript "https://github.com/tree-sitter/tree-sitter-javascript" "master" "src")
(json "https://github.com/tree-sitter/tree-sitter-json")
(make "https://github.com/alemuller/tree-sitter-make")
(markdown "https://github.com/ikatyang/tree-sitter-markdown")
(python "https://github.com/tree-sitter/tree-sitter-python")
(toml "https://github.com/tree-sitter/tree-sitter-toml")
(tsx "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src")
(typescript "https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src")
(yaml "https://github.com/ikatyang/tree-sitter-yaml"))))
(setq treesit-font-lock-level 4)
(setq major-mode-remap-alist
'((typescript-mode . typescript-ts-mode)
(js-mode . javascript-ts-mode)
(python-mode . python-ts-mode)
(json-mode . json-ts-mode)))
(cl-loop for (old-mode . new-mode) in major-mode-remap-alist
do (my/set-smartparens-indent new-mode)
do (set (intern (concat (symbol-name new-mode) "-hook"))
(list
(eval `(lambda ()
(run-hooks
',(intern (concat (symbol-name old-mode) "-hook")))))))))
DAP
An Emacs client for Debugger Adapter Protocol.
Okay, so, I tried to use it many times… Chrome DevTools and ipdb / pudb are just better for me. Maybe I’ll check out RealGUD instead… Will see.
References:
(use-package dap-mode
:straight t
:if (not (or my/remote-server my/is-termux))
:commands (dap-debug)
:init
(setq lsp-enable-dap-auto-configure nil)
:config
(setq dap-ui-variable-length 100)
(setq dap-auto-show-output nil)
(require 'dap-node)
(dap-node-setup)
(require 'dap-chrome)
(dap-chrome-setup)
(require 'dap-python)
(require 'dap-php)
(dap-mode 1)
(dap-ui-mode 1)
(dap-tooltip-mode 1)
(tooltip-mode 1))
Controls
I don’t like some keybindings in the built-in hydra, and there seems to be no easy way to modify the existing hydra, so I create my own. I tried to use transient, but the transient buffer seems to conflict with special buffers of DAP, and hydra does not.
Also, I want the hydra to toggle UI windows instead of just opening them, so here is a macro that defines such functions:
(with-eval-after-load 'dap-mode
(defmacro my/define-dap-ui-window-toggler (name)
`(defun ,(intern (concat "my/dap-ui-toggle-" name)) ()
,(concat "Toggle DAP " name "buffer")
(interactive)
(if-let (window (get-buffer-window ,(intern (concat "dap-ui--" name "-buffer"))))
(quit-window nil window)
(,(intern (concat "dap-ui-" name))))))
(my/define-dap-ui-window-toggler "locals")
(my/define-dap-ui-window-toggler "expressions")
(my/define-dap-ui-window-toggler "sessions")
(my/define-dap-ui-window-toggler "breakpoints")
(my/define-dap-ui-window-toggler "repl"))
And here is the hydra:
(defhydra my/dap-hydra (:color pink :hint nil :foreign-keys run)
"
^Stepping^ ^UI^ ^Switch^ ^Breakpoints^ ^Debug^ ^Expressions
^^^^^^^^------------------------------------------------------------------------------------------------------------------------------------------
_n_: Next _uc_: Controls _ss_: Session _bb_: Toggle _dd_: Debug _ee_: Eval
_i_: Step in _ue_: Expressions _st_: Thread _bd_: Delete _dr_: Debug recent _er_: Eval region
_o_: Step out _ul_: Locals _sf_: Stack frame _ba_: Add _dl_: Debug last _es_: Eval thing at point
_c_: Continue _ur_: REPL _su_: Up stack frame _bc_: Set condition _de_: Edit debug template _ea_: Add expression
_r_: Restart frame _uo_: Output _sd_: Down stack frame _bh_: Set hit count _Q_: Disconnect _ed_: Remove expression
_us_: Sessions _sF_: Stack frame filtered _bl_: Set log message _eu_: Refresh expressions
_ub_: Breakpoints "
("n" dap-next)
("i" dap-step-in)
("o" dap-step-out)
("c" dap-continue)
("r" dap-restart-frame)
("uc" dap-ui-controls-mode)
("ue" my/dap-ui-toggle-expressions)
("ul" my/dap-ui-toggle-locals)
("ur" my/dap-ui-toggle-repl)
("uo" dap-go-to-output-buffer)
("us" my/dap-ui-toggle-sessions)
("ub" my/dap-ui-toggle-breakpoints)
("ss" dap-switch-session)
("st" dap-switch-thread)
("sf" dap-switch-stack-frame)
("sF" my/dap-switch-stack-frame)
("su" dap-up-stack-frame)
("sd" dap-down-stack-frame)
("bb" dap-breakpoint-toggle)
("ba" dap-breakpoint-add)
("bd" dap-breakpoint-delete)
("bc" dap-breakpoint-condition)
("bh" dap-breakpoint-hit-condition)
("bl" dap-breakpoint-log-message)
("dd" dap-debug)
("dr" dap-debug-recent)
("dl" dap-debug-last)
("de" dap-debug-edit-template)
("ee" dap-eval)
("ea" dap-ui-expressions-add)
("er" dap-eval-region)
("es" dap-eval-thing-at-point)
("ed" dap-ui-expressions-remove)
("eu" dap-ui-expressions-refresh)
("q" nil "quit" :color blue)
("Q" dap-disconnect :color red))
(my-leader-def "d" #'my/dap-hydra/body)
UI Fixes
There are some problems with DAP UI in my setup.
First, DAP uses Treemacs buffers quite extensively, and they hide the doom modeline for some reason, so I can’t tell which buffer is active and can’t see borders between buffers.
Second, lines are truncated in some strange way, but calling toggle-truncate-lines
seems to fix that.
So I define a macro that creates a function that I can further use in advices.
(defvar my/dap-mode-buffer-fixed nil)
(with-eval-after-load 'dap-mode
(defmacro my/define-dap-tree-buffer-fixer (buffer-var buffer-name)
`(defun ,(intern (concat "my/fix-dap-ui-" buffer-name "-buffer")) (&rest _)
(with-current-buffer ,buffer-var
(unless my/dap-mode-buffer-fixed
(toggle-truncate-lines 1)
(doom-modeline-set-modeline 'info)
(setq-local my/dap-mode-buffer-fixed t)))))
(my/define-dap-tree-buffer-fixer dap-ui--locals-buffer "locals")
(my/define-dap-tree-buffer-fixer dap-ui--expressions-buffer "expressions")
(my/define-dap-tree-buffer-fixer dap-ui--sessions-buffer "sessions")
(my/define-dap-tree-buffer-fixer dap-ui--breakpoints-buffer "breakpoints")
(advice-add 'dap-ui-locals :after #'my/fix-dap-ui-locals-buffer)
(advice-add 'dap-ui-expressions :after #'my/fix-dap-ui-expressions-buffer)
(advice-add 'dap-ui-sessions :after #'my/fix-dap-ui-sessions-buffer)
(advice-add 'dap-ui-breakpoints :after #'my/fix-dap-ui-breakpoints-buffer))
Helper functions
Some helper functions that make debugging with DAP easier.
DAP seems to mess with window parameters from time to time. This function clears “bad” window parameters.
(defun my/clear-bad-window-parameters ()
"Clear window parameters that interrupt my workflow."
(interactive)
(let ((window (get-buffer-window (current-buffer))))
(set-window-parameter window 'no-delete-other-windows nil)))
A function to kill a value from a treemacs node.
(defun my/dap-yank-value-at-point (node)
(interactive (list (treemacs-node-at-point)))
(kill-new (message (plist-get (button-get node :item) :value))))
A function to open a value from a treemacs node in a new buffer.
(defun my/dap-display-value (node)
(interactive (list (treemacs-node-at-point)))
(let ((value (plist-get (button-get node :item) :value)))
(when value
(let ((buffer (generate-new-buffer "dap-value")))
(with-current-buffer buffer
(insert value))
(select-window (display-buffer buffer))))))
Switch to stack frame with filter
One significant improvement over Chrome Inspector for my particular stack is an ability to filter the stack frame list, for instance, to see only frames that relate to my current project.
So, here are functions that customize the filters:
(with-eval-after-load 'dap-mode
(setq my/dap-stack-frame-filters
`(("node_modules,node:internal" . ,(rx (or "node_modules" "node:internal")))
("node_modules" . ,(rx (or "node_modules")))
("node:internal" . ,(rx (or "node:internal")))))
(setq my/dap-stack-frame-current-filter (cdar my/dap-stack-frame-filters))
(defun my/dap-stack-frame-filter-set ()
(interactive)
(setq my/dap-stack-frame-current-filter
(cdr
(assoc
(completing-read "Filter: " my/dap-stack-frame-filters)
my/dap-stack-frame-filters))))
(defun my/dap-stack-frame-filter (frame)
(when-let (path (dap--get-path-for-frame frame))
(not (string-match my/dap-stack-frame-current-filter path)))))
And here is a version of dap-switch-stack-frame
that uses the said filter.
(defun my/dap-switch-stack-frame ()
"Switch stackframe by selecting another stackframe stackframes from current thread."
(interactive)
(when (not (dap--cur-session))
(error "There is no active session"))
(-if-let (thread-id (dap--debug-session-thread-id (dap--cur-session)))
(-if-let (stack-frames
(gethash
thread-id
(dap--debug-session-thread-stack-frames (dap--cur-session))))
(let* ((index 0)
(stack-framces-filtered
(-filter
#'my/dap-stack-frame-filter
stack-frames))
(new-stack-frame
(dap--completing-read
"Select active frame: "
stack-framces-filtered
(-lambda ((frame &as &hash "name"))
(if-let (frame-path (dap--get-path-for-frame frame))
(format "%s: %s (in %s)"
(cl-incf index) name frame-path)
(format "%s: %s" (cl-incf index) name)))
nil
t)))
(dap--go-to-stack-frame (dap--cur-session) new-stack-frame))
(->> (dap--cur-session)
dap--debug-session-name
(format "Current session %s is not stopped")
error))
(error "No thread is currently active %s" (dap--debug-session-name (dap--cur-session)))))
Smarter switch to stack frame
- CREDIT: Thanks @yyoncho on the Emacs LSP Discord for helping me with this!
By default, when a breakpoint is hit, dap always pop us the buffer in the active EXWM workspace and in the active perspective. I’d like it to switch to an existing buffer instead.
So first we need to locate EXWM workspace for the file with path
:
(defun my/exwm-perspective-find-buffer (path)
"Find a buffer with PATH in all EXWM perspectives.
Returns (<buffer> . <workspace-index>) or nil."
(let* ((buf (cl-loop for buf being buffers
if (and (buffer-file-name buf)
(f-equal-p (buffer-file-name buf) path))
return buf))
(target-workspace
(and buf
(cl-loop for frame in exwm-workspace--list
if (with-selected-frame frame
(cl-loop for persp-name being the hash-keys of (perspectives-hash)
if (member buf (persp-buffers
(gethash persp-name (perspectives-hash))))
return persp-name))
return (cl-position frame exwm-workspace--list)))))
(when target-workspace (cons buf target-workspace))))
And override dap--go-to-stack-frame
to take that into account. For some reason, evaluating this before dap-mode
doesn’t work.
(defun my/dap--go-to-stack-frame-override (debug-session stack-frame)
"Make STACK-FRAME the active STACK-FRAME of DEBUG-SESSION."
(with-lsp-workspace (dap--debug-session-workspace debug-session)
(when stack-frame
(-let* (((&hash "line" line "column" column "name" name) stack-frame)
(path (dap--get-path-for-frame stack-frame)))
(setf (dap--debug-session-active-frame debug-session) stack-frame)
;; If we have a source file with path attached, open it and
;; position the point in the line/column referenced in the
;; stack trace.
(if (and path (file-exists-p path))
(progn
(let ((exwm-target (my/exwm-perspective-find-buffer path)))
(if exwm-target
(progn
(unless (= (cdr exwm-target) exwm-workspace-current-index)
(exwm-workspace-switch (cdr exwm-target)))
(persp-switch-to-buffer (car exwm-target)))
(select-window (get-mru-window (selected-frame) nil))
(find-file path)))
(goto-char (point-min))
(forward-line (1- line))
(forward-char column))
(message "No source code for %s. Cursor at %s:%s." name line column))))
(run-hook-with-args 'dap-stack-frame-changed-hook debug-session)))
(with-eval-after-load 'exwm
(with-eval-after-load 'dap-mode
(advice-add #'dap--go-to-stack-frame :override #'my/dap--go-to-stack-frame-override)))
;; (advice-remove #'dap--go-to-stack-frame #'my/dap--go-to-stack-frame-override)
Debug templates
Some debug templates I frequently use.
(with-eval-after-load 'dap-mode
(dap-register-debug-template
"Node::Nest.js"
(list :type "node"
:request "attach"
:name "Node::Attach"
:port 9229
:outFiles ["${workspaceFolder}/dist/**/*.js"]
:sourceMaps t
:program "${workspaceFolder}/src/app.ts"))
(dap-register-debug-template
"Node::Babel"
(list :type "node"
:request "attach"
:name "Node::Attach"
:port 9229
:program "${workspaceFolder}/dist/bin/www.js")))
Reformatter
A general-purpose package to run formatters on files. While the most popular formatters are already packaged for Emacs, those that aren’t can be invoked with this package.
(use-package reformatter
:straight t)
copilot
GitHub Copilot is a project of GitHub and OpenAI that provides code completions. It’s somewhat controversial in the Emacs community but I opt in for now.
(defun my/copilot-tab ()
(interactive)
(or (copilot-accept-completion)
(when (my/should-run-emmet-p) (my/emmet-or-tab))
(when (and (eq evil-state 'normal)
(or hs-minor-mode outline-minor-mode))
(evil-toggle-fold)
t)
(indent-for-tab-command)))
(use-package copilot
:straight (:host github :repo "copilot/copilot.el")
:commands (copilot-mode)
:if (not (or my/remote-server my/is-termux))
:init
(add-hook 'emacs-startup-hook
(lambda ()
(add-hook 'prog-mode-hook #'copilot-mode)))
:config
(push '(copilot) warning-suppress-types)
(setq copilot-node-executable "/home/pavel/.guix-extra-profiles/dev/dev/bin/node")
(general-define-key
:keymaps 'company-active-map
"<backtab>" #'my/copilot-tab)
(general-define-key
:keymaps 'copilot-mode-map
"<tab>" #'my/copilot-tab
"M-j" #'copilot-accept-completion-by-line
"M-l" #'copilot-accept-completion-by-word))
Web development
Configs for various web development technologies I’m using.
Emmet
Emmet is a toolkit which greatly speeds up typing HTML & CSS.
Type | Note |
---|---|
TODO | make expand div[disabled] as <div disabled></div> |
My bit of config here:
- makes
TAB
the only key I have to use
(defun my/should-run-emmet-p ()
(and (bound-and-true-p emmet-mode)
(or (and (derived-mode-p 'web-mode)
(member (web-mode-language-at-pos) '("html" "css")))
(not (derived-mode-p 'web-mode)))))
(use-package emmet-mode
:straight t
:hook ((vue-html-mode . emmet-mode)
(svelte-mode . emmet-mode)
(web-mode . emmet-mode)
(html-mode . emmet-mode)
(css-mode . emmet-mode)
(scss-mode . emmet-mode))
:config
(defun my/emmet-or-tab (&optional arg)
(interactive)
(if (my/should-run-emmet-p)
(or (emmet-expand-line arg)
(emmet-go-to-edit-point 1)
(indent-for-tab-command arg))
(indent-for-tab-command arg)))
(general-imap :keymaps 'emmet-mode-keymap
"TAB" 'my/emmet-or-tab
"<backtab>" 'emmet-prev-edit-point))
Prettier
(use-package prettier
:commands (prettier-prettify)
:straight t
:init
(my-leader-def
:keymaps '(js-mode-map
web-mode-map
typescript-mode-map
typescript-ts-mode-map
vue-mode-map
svelte-mode-map)
"rr" #'prettier-prettify))
TypeScript
(use-package typescript-mode
:straight t
:mode "\\.ts\\'"
:init
(add-hook 'typescript-mode-hook #'smartparens-mode)
(add-hook 'typescript-mode-hook #'rainbow-delimiters-mode)
(add-hook 'typescript-mode-hook #'hs-minor-mode)
:config
(my/set-smartparens-indent 'typescript-mode))
JavaScript
(add-hook 'js-mode-hook #'smartparens-mode)
(add-hook 'js-mode-hook #'hs-minor-mode)
(my/set-smartparens-indent 'js-mode)
Jest
(use-package jest-test-mode
:straight t
:hook ((typescript-mode . jest-test-mode)
(js-mode . jest-test-mode))
:config
(my-leader-def
:keymaps 'jest-test-mode-map
:infix "t"
"t" #'jest-test-run-at-point
"d" #'jest-test-debug-run-at-point
"r" #'jest-test-run
"a" #'jest-test-run-all-tests)
(defmacro my/jest-test-with-debug-flags (form)
"Execute FORM with debugger flags set."
(declare (indent 0))
`(let ((jest-test-options (seq-concatenate 'list jest-test-options (list "--runInBand") ))
(jest-test-npx-options (seq-concatenate 'list jest-test-npx-options (list "--node-options" "--inspect-brk"))))
,form))
(defun my/jest-test-debug ()
"Run the test with an inline debugger attached."
(interactive)
(my/jest-test-with-debug-flags
(jest-test-run)))
(defun my/jest-test-debug-rerun-test ()
"Run the test with an inline debugger attached."
(interactive)
(my/jest-test-with-debug-flags
(jest-test-rerun-test)))
(defun my/jest-test-debug-run-at-point ()
"Run the test with an inline debugger attached."
(interactive)
(my/jest-test-with-debug-flags
(jest-test-run-at-point)))
(advice-add #'jest-test-debug :override #'my/jest-test-debug)
(advice-add #'jest-test-debug-rerun-test :override #'my/jest-test-debug-rerun-test)
(advice-add #'jest-test-debug-run-at-point
:override #'my/jest-test-debug-run-at-point))
(defun my/jest-test-run-at-point-copy ()
"Run the top level describe block of the current buffer's point."
(interactive)
(let ((filename (jest-test-find-file))
(example (jest-test-unit-at-point)))
(if (and filename example)
(jest-test-from-project-directory filename
(let ((jest-test-options (seq-concatenate 'list jest-test-options (list "-t" example))))
(kill-new (jest-test-command filename))))
(message jest-test-not-found-message))))
web-mode
web-mode.el is a major mode to edit various web templates.
Trying this one out instead of vue-mode and svelte-mode, because this one seems to have better support for tree-sitter and generally less problems.
Set web-mode-auto-pairs
not nil
because smartparens already fulfills that role.
(use-package web-mode
:straight t
:commands (web-mode)
:init
(add-to-list 'auto-mode-alist '("\\.svelte\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.vue\\'" . web-mode))
:config
(add-hook 'web-mode-hook 'smartparens-mode)
(add-hook 'web-mode-hook 'hs-minor-mode)
(my/set-smartparens-indent 'web-mode)
(setq web-mode-auto-pairs nil))
Hooking this up with lsp.
(setq my/web-mode-lsp-extensions
`(,(rx ".svelte" eos)
,(rx ".vue" eos)))
(defun my/web-mode-lsp ()
(when (seq-some
(lambda (regex) (string-match-p regex (buffer-name)))
my/web-mode-lsp-extensions)
(lsp-deferred)))
(add-hook 'web-mode-hook #'my/web-mode-lsp)
Vue settings
(defun my/web-mode-vue-setup (&rest _)
(when (string-match-p (rx ".vue" eos) (buffer-name))
(setq-local web-mode-script-padding 0)
(setq-local web-mode-style-padding 0)
(setq-local create-lockfiles nil)
(setq-local web-mode-enable-auto-pairing nil)))
(add-hook 'web-mode-hook 'my/web-mode-vue-setup)
(add-hook 'editorconfig-after-apply-functions 'my/web-mode-vue-setup)
SCSS
(add-hook 'scss-mode-hook #'smartparens-mode)
(add-hook 'scss-mode-hook #'hs-minor-mode)
(my/set-smartparens-indent 'scss-mode)
PHP
(use-package php-mode
:straight t
:mode "\\.php\\'"
:config
(add-hook 'php-mode-hook #'smartparens-mode)
(add-hook 'php-mode-hook #'lsp)
(my/set-smartparens-indent 'php-mode))
LaTeX
AUCTeX
The best LaTeX editing environment I’ve found so far.
References:
(use-package tex
:straight auctex
:defer t
:config
(setq-default TeX-auto-save t)
(setq-default TeX-parse-self t)
(TeX-PDF-mode)
;; Use XeLaTeX & stuff
(setq-default TeX-engine 'xetex)
(setq-default TeX-command-extra-options "-shell-escape")
(setq-default TeX-source-correlate-method 'synctex)
(TeX-source-correlate-mode)
(setq-default TeX-source-correlate-start-server t)
(setq-default LaTeX-math-menu-unicode t)
(setq-default font-latex-fontify-sectioning 1.3)
;; Scale preview for my DPI
(setq-default preview-scale-function 1.4)
(when (boundp 'tex--prettify-symbols-alist)
(assoc-delete-all "--" tex--prettify-symbols-alist)
(assoc-delete-all "---" tex--prettify-symbols-alist))
(add-hook 'LaTeX-mode-hook
(lambda ()
(TeX-fold-mode 1)
(outline-minor-mode)))
(add-to-list 'TeX-view-program-selection
'(output-pdf "Zathura"))
;; Do not run lsp within templated TeX files
(add-hook 'LaTeX-mode-hook
(lambda ()
(unless (string-match "\.hogan\.tex$" (buffer-name))
(lsp))
(setq-local lsp-diagnostic-package :none)
(setq-local flycheck-checker 'tex-chktex)))
(add-hook 'LaTeX-mode-hook #'rainbow-delimiters-mode)
(add-hook 'LaTeX-mode-hook #'smartparens-mode)
(add-hook 'LaTeX-mode-hook #'prettify-symbols-mode)
(my/set-smartparens-indent 'LaTeX-mode)
(require 'smartparens-latex)
(general-nmap
:keymaps '(LaTeX-mode-map latex-mode-map)
"RET" 'TeX-command-run-all
"C-c t" 'orgtbl-mode)
<<init-greek-latex-snippets>>
<<init-english-latex-snippets>>
<<init-math-latex-snippets>>
<<init-section-latex-snippets>>)
Import *.sty
A function to import .sty
files to the LaTeX document.
(defun my/list-sty ()
(reverse
(sort
(seq-filter
(lambda (file) (if (string-match ".*\.sty$" file) 1 nil))
(directory-files
(seq-some
(lambda (dir)
(if (and
(f-directory-p dir)
(seq-some
(lambda (file) (string-match ".*\.sty$" file))
(directory-files dir))
) dir nil))
(list "./styles" "../styles/" "." "..")) :full))
(lambda (f1 f2)
(let ((f1b (file-name-base f1))
(f1b (file-name-base f2)))
(cond
((string-match-p ".*BibTex" f1) t)
((and (string-match-p ".*Locale" f1) (not (string-match-p ".*BibTex" f2))) t)
((string-match-p ".*Preamble" f2) t)
(t (string-lessp f1 f2))))))))
(defun my/import-sty ()
(interactive)
(insert
(apply #'concat
(cl-mapcar
(lambda (file) (concat "\\usepackage{" (file-name-sans-extension (file-relative-name file default-directory)) "}\n"))
(my/list-sty)))))
(defun my/import-sty-org ()
(interactive)
(insert
(apply #'concat
(cl-mapcar
(lambda (file) (concat "#+LATEX_HEADER: \\usepackage{" (file-name-sans-extension (file-relative-name file default-directory)) "}\n"))
(my/list-sty)))))
Snippets
Note | Type |
---|---|
TODO | Move yasnippet snippets here? Maybe extract to a separate file? |
Greek letters
Autogenerate snippets for greek letters. I have a few blocks like this because it’s faster & more flexible than usual yasnippet snippets.
Noweb points to the AUCTeX config block.
(setq my/greek-alphabet
'(("a" . "\\alpha")
("b" . "\\beta" )
("g" . "\\gamma")
("d" . "\\delta")
("e" . "\\epsilon")
("z" . "\\zeta")
("h" . "\\eta")
("o" . "\\theta")
("i" . "\\iota")
("k" . "\\kappa")
("l" . "\\lambda")
("m" . "\\mu")
("n" . "\\nu")
("x" . "\\xi")
("p" . "\\pi")
("r" . "\\rho")
("s" . "\\sigma")
("t" . "\\tau")
("u" . "\\upsilon")
("f" . "\\phi")
("c" . "\\chi")
("v" . "\\psi")
("g" . "\\omega")))
(setq my/latex-greek-prefix "'")
;; The same for capitalized letters
(dolist (elem my/greek-alphabet)
(let ((key (car elem))
(value (cdr elem)))
(when (string-equal key (downcase key))
(add-to-list 'my/greek-alphabet
(cons
(capitalize (car elem))
(concat
(substring value 0 1)
(capitalize (substring value 1 2))
(substring value 2)))))))
(yas-define-snippets
'latex-mode
(mapcar
(lambda (elem)
(list (concat my/latex-greek-prefix (car elem)) (cdr elem) (concat "Greek letter " (car elem))))
my/greek-alphabet))
English letters
(setq my/english-alphabet
'("a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z"))
(dolist (elem my/english-alphabet)
(when (string-equal elem (downcase elem))
(add-to-list 'my/english-alphabet (upcase elem))))
(setq my/latex-mathbb-prefix "`")
(yas-define-snippets
'latex-mode
(mapcar
(lambda (elem)
(list (concat my/latex-mathbb-prefix elem) (concat "\\mathbb{" elem "}") (concat "Mathbb letter " elem)))
my/english-alphabet))
Math symbols
(setq my/latex-math-symbols
'(("x" . "\\times")
("." . "\\cdot")
("v" . "\\forall")
("s" . "\\sum_{$1}^{$2}$0")
("p" . "\\prod_{$1}^{$2}$0")
("d" . "\\partial")
("e" . "\\exists")
("i" . "\\int_{$1}^{$2}$0")
("c" . "\\cap")
("u" . "\\cup")
("0" . "\\emptyset")
("^" . "\\widehat{$1}$0")
("_" . "\\overline{$1}$0")
("~" . "\\sim")
("|" . "\\mid")
("_|" . "\\perp")))
(setq my/latex-math-prefix ";")
(yas-define-snippets
'latex-mode
(mapcar
(lambda (elem)
(let ((key (car elem))
(value (cdr elem)))
(list (concat my/latex-math-prefix key) value (concat "Math symbol " value))))
my/latex-math-symbols))
Section snippets
Section snippets. The code turned out to be more complicated than just writing the snippets by hand.
(setq my/latex-section-snippets
'(("ch" . "\\chapter{$1}")
("sec" . "\\section{$1}")
("ssec" . "\\subsection{$1}")
("sssec" . "\\subsubsection{$1}")
("par" . "\\paragraph{$1}}")))
(setq my/latex-section-snippets
(mapcar
(lambda (elem)
`(,(car elem)
,(cdr elem)
,(progn
(string-match "[a-z]+" (cdr elem))
(match-string 0 (cdr elem)))))
my/latex-section-snippets))
(dolist (elem my/latex-section-snippets)
(let* ((key (nth 0 elem))
(value (nth 1 elem))
(desc (nth 2 elem))
(star-index (string-match "\{\$1\}" value)))
(add-to-list 'my/latex-section-snippets
`(,(concat key "*")
,(concat
(substring value 0 star-index)
"*"
(substring value star-index))
,(concat desc " with *")))
(add-to-list 'my/latex-section-snippets
`(,(concat key "l")
,(concat value "%\n\\label{sec:$2}")
,(concat desc " with label")))))
(dolist (elem my/latex-section-snippets)
(setf (nth 1 elem) (concat (nth 1 elem) "\n$0")))
(yas-define-snippets
'latex-mode
my/latex-section-snippets)
Markup & natural languages
Markdown
(use-package markdown-mode
:straight t
:mode "\\.md\\'"
:config
(setq markdown-command
(concat
"pandoc"
" --from=markdown --to=html"
" --standalone --mathjax --highlight-style=pygments"
" --css=pandoc.css"
" --quiet"
))
(setq markdown-live-preview-delete-export 'delete-on-export)
(setq markdown-asymmetric-header t)
(setq markdown-open-command "/home/pavel/bin/scripts/chromium-sep")
(add-hook 'markdown-mode-hook #'smartparens-mode)
(general-define-key
:keymaps 'markdown-mode-map
"M-<left>" 'markdown-promote
"M-<right>" 'markdown-demote))
;; (use-package livedown
;; :straight (:host github :repo "shime/emacs-livedown")
;; :commands livedown-preview
;; :config
;; (setq livedown-browser "qutebrowser"))
Ascii Doc
(use-package adoc-mode
:mode (rx (| ".asciidoc") eos)
:straight t)
PlantUML
Guix dependency |
---|
plantuml |
(use-package plantuml-mode
:straight t
:mode "(\\.\\(plantuml?\\|uml\\|puml\\)\\'"
:config
(setq plantuml-executable-path "/home/pavel/.guix-extra-profiles/emacs/emacs/bin/plantuml")
(setq plantuml-default-exec-mode 'executable)
(setq plantuml-indent-level 2)
(setq my/plantuml-indent-regexp-return "^\s*return\s+.+$")
(;; (add-to-list
;; 'plantuml-indent-regexp-end
;; my/plantuml-indent-regexp-return)
)
(add-to-list 'auto-mode-alist '("\\.plantuml\\'" . plantuml-mode))
(add-to-list 'auto-mode-alist '("\\.uml\\'" . plantuml-mode))
(add-hook 'plantuml-mode-hook #'smartparens-mode)
(general-nmap
:keymaps 'plantuml-mode-map
"RET" 'plantuml-preview))
Subtitles
A major mode to work with subtitles.
(use-package subed
:straight (:host github :repo "rndusr/subed" :files ("subed/*.el")
:build (:not native-compile))
;; (cons (rx (| "srt" "vtt" "ass") eos) #'subed-mode)
:mode ("\\(?:ass\\|\\(?:sr\\|vt\\)t\\)\\'" . subed-mode)
:config
(general-define-key
:keymaps '(subed-mode-map subed-vtt-mode-map)
:states '(normal)
"gp" #'subed-mpv-toggle-pause))
LTeX
ltex-ls is a tool that wraps LanguageTool into a language server.
It takes maybe 10 seconds to run on my Master’s thesis file (M-x count words
: 13453 words and 117566 characters), but it’s totally worth it. And it’s much faster on smaller files. The good thing is that it supports markup syntaxes like Org and Markdown, whereas LanguageTool by itself produces a lot of false positives on these files.
It shouldn’t be too hard to package that for guix, but I’ve installed the nix version for now.
(use-package lsp-ltex
:straight t
:after (lsp)
:init
(setq lsp-ltex-version "15.2.0")
(setq lsp-ltex-check-frequency "save"))
A function to switch the current language.
(defun my/ltex-lang ()
(interactive)
(setq lsp-ltex-language (completing-read
"Language: "
'("en-GB" "ru-RU" "de-DE")))
(lsp-workspace-restart (lsp--read-workspace)))
Check whether it’s necessary to run LTeX:
(defun my/ltex-need-p ()
(let ((file-name (buffer-file-name)))
(cond
(my/is-termux nil)
((null file-name) nil)
((string-match-p (rx "/home/pavel/" (+ alnum) ".org" eos) file-name) nil)
((string-match-p (rx (literal org-directory) "/" (or "roam" "inbox-notes" "literature-notes" "journal")) file-name) t)
((string-match-p (rx (literal org-directory)) file-name) nil)
((string-match-p (rx (literal (expand-file-name user-emacs-directory))) file-name) nil)
(t t))))
To use it in text-mode-hook
(defun my/text-mode-lsp-maybe ()
(when (my/ltex-need-p)
(lsp)))
(add-hook 'text-mode-hook #'my/text-mode-lsp-maybe)
LanguageTool
LanguageTool is a great offline spell checker. For some reason, the download link is nowhere to be found on the home page, so it is listed in the references as well.
References:
(use-package langtool
:straight t
:commands (langtool-check)
:config
(setq langtool-language-tool-server-jar "/home/pavel/bin/LanguageTool-6.4/languagetool-server.jar")
(setq langtool-mother-tongue "ru")
(setq langtool-default-language "en-US"))
(my-leader-def
:infix "L"
"" '(:which-key "languagetool")
"c" 'langtool-check
"s" 'langtool-server-stop
"d" 'langtool-check-done
"n" 'langtool-goto-next-error
"p" 'langtool-goto-previous-error
"l" 'langtool-correct-buffer)
Reverso
reverso.el is a package of mine that provides Emacs interface for https://reverso.net.
(use-package reverso
:straight (:host github :repo "SqrtMinusOne/reverso.el")
:init
(my-leader-def "ar" #'reverso)
:commands (reverso)
:config
(setq reverso-languages '(russian english german))
(reverso-history-mode))
Lisp
Meta Lisp
Some packages for editing various Lisps.
(use-package lispy
:commands (lispy-mode)
:straight t)
(use-package lispyville
:hook (lispy-mode . lispyville-mode)
:straight t)
(sp-with-modes sp-lisp-modes
(sp-local-pair "'" nil :actions nil))
Emacs Lisp
Package Lint
A package that checks for the metadata in Emacs Lisp packages.
(use-package flycheck-package
:straight t
:defer t
:init
(defun my/flycheck-package-setup ()
(require 'flycheck-package)
(flycheck-package-setup)
(remove-hook 'emacs-lisp-mode-hook #'my/flycheck-package-setup))
(add-hook 'emacs-lisp-mode-hook #'my/flycheck-package-setup))
General settings
(add-hook 'emacs-lisp-mode-hook #'aggressive-indent-mode)
;; (add-hook 'emacs-lisp-mode-hook #'smartparens-strict-mode)
(add-hook 'emacs-lisp-mode-hook #'lispy-mode)
Helper functions
Remove all advice from function. Source: https://emacs.stackexchange.com/questions/24657/unadvise-a-function-remove-all-advice-from-it
(defun advice-unadvice (sym)
"Remove all advices from symbol SYM."
(interactive "aFunction symbol: ")
(advice-mapc (lambda (advice _props) (advice-remove sym advice)) sym))
IELM
(add-hook 'inferior-emacs-lisp-mode-hook #'smartparens-mode)
(my-leader-def "bi" #'ielm)
Common lisp
SLIME
(use-package slime
:straight t
:commands (slime)
:config
(setq inferior-lisp-program "sbcl")
(add-hook 'slime-repl-mode 'smartparens-mode))
General settings
(add-hook 'lisp-mode-hook #'aggressive-indent-mode)
;; (add-hook 'emacs-lisp-mode-hook #'smartparens-strict-mode)
(add-hook 'lisp-mode-hook #'lispy-mode)
Clojure
(use-package clojure-mode
:straight t
:mode "\\.clj[sc]?\\'"
:config
;; (add-hook 'clojure-mode-hook #'smartparens-strict-mode)
(add-hook 'clojure-mode-hook #'lispy-mode)
(add-hook 'clojure-mode-hook #'aggressive-indent-mode))
(use-package cider
:after clojure-mode
:straight t)
Hy
Python requirements:
hy
jedhy
(use-package hy-mode
:straight t
:mode "\\.hy\\'"
:config
(add-hook 'hy-mode-hook #'lispy-mode)
(add-hook 'hy-mode-hook #'aggressive-indent-mode))
Scheme
(use-package geiser
:straight t
:commands (geiser run-geiser)
:config
(setq geiser-default-implementation 'guile))
(use-package geiser-guile
:straight t
:after geiser)
(add-hook 'scheme-mode-hook #'aggressive-indent-mode)
(add-hook 'scheme-mode-hook #'lispy-mode)
CLIPS
An honorary Lisp.
(use-package clips-mode
:straight t
:mode "\\.cl\\'"
:disabled t
:config
(add-hook 'clips-mode 'lispy-mode))
Python
ein
ein is a package that allows for running Jupyter notebooks in Emacs.
(use-package ein
:commands (ein:run)
:straight t)
pyright
For some reason it doesn’t use pipenv python executable, so here is a small workaround.
(setq my/pipenv-python-alist '())
(defun my/get-pipenv-python ()
(let ((default-directory (projectile-project-root)))
(if (file-exists-p "Pipfile")
(let ((asc (assoc default-directory my/pipenv-python-alist)))
(if asc
(cdr asc)
(let ((python-executable
(string-trim (shell-command-to-string "PIPENV_IGNORE_VIRTUALENVS=1 pipenv run which python 2>/dev/null"))))
(if (string-match-p ".*not found.*" python-executable)
(message "Pipfile found, but not pipenv executable!")
(message (format "Found pipenv python: %s" python-executable))
(add-to-list 'my/pipenv-python-alist (cons default-directory python-executable))
python-executable))))
"python")))
(use-package lsp-pyright
:straight t
:defer t
:hook (python-mode . (lambda ()
(require 'lsp-pyright)
(setq-local lsp-pyright-python-executable-cmd (my/get-pipenv-python))
(lsp))))
(add-hook 'python-mode-hook #'smartparens-mode)
(add-hook 'python-mode-hook #'hs-minor-mode)
pipenv
Pipenv is a package manager for Python.
Automatically creates & manages virtualenvs and stores data in Pipfile
and Pipfile.lock
(like npm’s package.json
and package-lock.json
).
(use-package pipenv
:straight t
:hook (python-mode . pipenv-mode)
:init
(setq
pipenv-projectile-after-switch-function
#'pipenv-projectile-after-switch-extended))
OFF (OFF) yapf
yapf is a formatter for Python files.
Guix dependency |
---|
python-yapf |
References:
(use-package yapfify
:straight (:repo "JorisE/yapfify" :host github)
:disabled
:commands (yapfify-region
yapfify-buffer
yapfify-region-or-buffer
yapf-mode))
Global config:
[style]
based_on_style = facebook
column_limit = 80
black
black is a formatter for Python files.
Guix dependency |
---|
python-black |
(use-package python-black
:straight t
:commands (python-black-buffer)
:config
(setq python-black-command "black"))
isort
isort is a Python package to sort Python imports.
Guix dependency |
---|
python-isort |
References:
(use-package py-isort
:straight t
:commands (py-isort-buffer py-isort-region))
The following binding calls yapf & isort on the buffer
(my-leader-def
:keymaps '(python-mode-map python-ts-mode-map)
"rr" (lambda ()
(interactive)
(unless (and (fboundp #'org-src-edit-buffer-p) (org-src-edit-buffer-p))
(py-isort-buffer))
(python-black-buffer)))
OFF sphinx-doc
A package to generate sphinx-compatible docstrings.
(use-package sphinx-doc
:straight t
:hook (python-mode . sphinx-doc-mode)
:config
(my-leader-def
:keymaps 'sphinx-doc-mode-map
"rd" 'sphinx-doc))
numpydoc
numpydoc.el is a package to generate docstring in Python functions.
(use-package numpydoc
:straight t
:commands (numpydoc-generate)
:init
(my-leader-def
:keymaps 'python-ts-mode-map
"rd" #'numpydoc-generate)
:config
(setq numpydoc-insertion-style 'prompt)
(setq numpydoc-insert-return-without-typehint nil))
pytest
pytest is a unit testing framework for Python.
Once again a function to set pytest executable from pipenv.
References:
(defun my/set-pipenv-pytest ()
(setq-local
python-pytest-executable
(concat (my/get-pipenv-python) " -m pytest")))
(use-package python-pytest
:straight t
:commands (python-pytest-dispatch)
:init
(my-leader-def
:keymaps 'python-mode-map
:infix "t"
"t" 'python-pytest-dispatch)
:config
<<override-pytest-run>>
(add-hook 'python-mode-hook #'my/set-pipenv-pytest)
(when (derived-mode-p 'python-mode)
(my/set-pipenv-pytest)))
Fix comint buffer width
For some reason, the default comint output width is way too large.
To fix that, I’ve modified the following function in the python-pytest
package.
(cl-defun python-pytest--run-as-comint (&key command)
"Run a pytest comint session for COMMAND."
(let* ((buffer (python-pytest--get-buffer))
(process (get-buffer-process buffer)))
(with-current-buffer buffer
(when (comint-check-proc buffer)
(unless (or compilation-always-kill
(yes-or-no-p "Kill running pytest process?"))
(user-error "Aborting; pytest still running")))
(when process
(delete-process process))
(let ((inhibit-read-only t))
(erase-buffer))
(unless (eq major-mode 'python-pytest-mode)
(python-pytest-mode))
(compilation-forget-errors)
(display-buffer buffer)
(setq command (format "export COLUMNS=%s; %s"
(- (window-width (get-buffer-window buffer)) 5)
command))
(insert (format "cwd: %s\ncmd: %s\n\n" default-directory command))
(setq python-pytest--current-command command)
(when python-pytest-pdb-track
(add-hook
'comint-output-filter-functions
'python-pdbtrack-comint-output-filter-function
nil t))
(run-hooks 'python-pytest-setup-hook)
(make-comint-in-buffer "pytest" buffer "bash" nil "-c" command)
(run-hooks 'python-pytest-started-hook)
(setq process (get-buffer-process buffer))
(set-process-sentinel process #'python-pytest--process-sentinel))))
code-cells
Support for text with magic comments.
Guix dependency | Disabled |
---|---|
python-jupytext | t |
(use-package code-cells
:straight t
:commands (code-cells-mode code-cells-convert-ipynb))
tensorboard
A function to start up TensorBoard.
(setq my/tensorboard-buffer "TensorBoard-out")
(defun my/tensorboard ()
(interactive)
(start-process
"tensorboard"
my/tensorboard-buffer
"tensorboard"
"serve"
"--logdir"
(car (find-file-read-args "Directory: " t)))
(display-buffer my/tensorboard-buffer))
Data serialization
JSON
(use-package json-mode
:straight t
:mode "\\.json\\'"
:config
(add-hook 'json-mode-hook #'smartparens-mode)
(add-hook 'json-mode-hook #'hs-minor-mode)
(my/set-smartparens-indent 'json-mode))
CSV
(use-package csv-mode
:straight t
:disabled
:mode "\\.csv\\'")
YAML
(use-package yaml-mode
:straight t
:mode "\\.yml\\'"
:config
(add-hook 'yaml-mode-hook 'smartparens-mode)
(add-hook 'yaml-mode-hook 'highlight-indent-guides-mode)
(add-to-list 'auto-mode-alist '("\\.yml\\'" . yaml-mode)))
Configuration
.env
(use-package dotenv-mode
:straight t
:mode "\\.env\\..*\\'")
.gitignore
A package to quickly create .gitignore
files.
(use-package gitignore-templates
:straight t
:commands (gitignore-templates-insert
gitignore-templates-new-file))
Docker
(use-package dockerfile-mode
:mode "Dockerfile\\'"
:straight t
:config
(add-hook 'dockerfile-mode 'smartparens-mode))
Jenkins
(use-package jenkinsfile-mode
:straight t
:mode "Jenkinsfile\\'"
:config
(add-hook 'jenkinsfile-mode-hook #'smartparens-mode)
(my/set-smartparens-indent 'jenkinsfile-mode))
crontab
(use-package crontab-mode
:mode "/crontab\\(\\.X*[[:alnum:]]+\\)?\\'"
:straight t)
nginx
(use-package nginx-mode
:straight t
:config
(my/set-smartparens-indent 'nginx-mode))
HCL
(use-package hcl-mode
:mode "\\.hcl\\'"
:straight t)
Shell
sh
(add-hook 'sh-mode-hook #'smartparens-mode)
fish
(use-package fish-mode
:straight t
:mode "\\.fish\\'"
:config
(add-hook 'fish-mode-hook #'smartparens-mode))
Query languages
SQL
sql-formatter is a nice JavaScript package for pretty-printing SQL queries. It is not packaged for Emacs, so the easiest way to use it seems to be to define a custom formatter via reformatter.
Also, I’ve made a simple function to switch dialects because I often alternate between them.
So far I didn’t find a nice SQL client for Emacs, but I occasionally run SQL queries in Org Mode, so this quite package is handy.
(setq my/sqlformatter-dialect-choice
'("db2" "mariadb" "mysql" "n1ql" "plsql" "postgresql" "redshift" "spark" "sql" "tsql"))
(setq my/sqlformatter-dialect "postgresql")
(defun my/sqlformatter-set-dialect ()
"Set dialect for sql-formatter"
(interactive)
(setq my/sqlformatter-dialect
(completing-read "Dialect: " my/sqlformatter-dialect-choice)))
(reformatter-define sqlformat
:program (executable-find "sql-formatter")
:args `("-l" ,my/sqlformatter-dialect))
(my-leader-def
:keymaps '(sql-mode-map)
"rr" #'sqlformat-buffer)
SPARQL
(use-package sparql-mode
:mode "\\.sparql\\'"
:straight t)
GraphQL
(use-package graphql-mode
:mode (rx (| "gql" "grapql") eos)
:straight t)
Documents
DocView
Don’t know about this.
doc-view
doesn’t look great with the default doc-view-resolution
of 100. 300 is fine, but then it becomes slow.
(defun my/doc-view-setup ()
(display-line-numbers-mode -1)
(undo-tree-mode -1))
(use-package doc-view
:straight (:type built-in)
:config
(setq doc-view-resolution 300)
(add-hook 'doc-view-mode-hook #'my/doc-view-setup)
(general-define-key
:states '(normal)
:keymaps '(doc-view-mode-map)
"j" #'doc-view-next-line-or-next-page
"k" #'doc-view-previous-line-or-previous-page))
Gnuplot
Emacs integration for gnuplot.
(use-package gnuplot
:straight t
:commands (gnuplot-mode gnuplot-make-buffer)
:init
(add-to-list 'auto-mode-alist '("\\.gp\\'" . gnuplot-mode))
:config
(general-define-key
:keymaps 'gnuplot-mode-map
"C-c C-c" #'gnuplot-send-buffer-to-gnuplot)
(general-define-key
:states '(normal)
:keymaps 'gnuplot-mode-map
"RET" #'gnuplot-send-buffer-to-gnuplot)
(add-hook 'gnuplot-mode-hook #'smartparens-mode))
x509
(use-package x509-mode
:commands (x509-dwim)
:straight (:host github :repo "jobbflykt/x509-mode"
:build (:not native-compile)))
Java
(use-package lsp-java
:straight t
:after (lsp)
:config
(setq lsp-java-jdt-download-url "https://download.eclipse.org/jdtls/milestones/0.57.0/jdt-language-server-0.57.0-202006172108.tar.gz"))
(add-hook 'java-mode-hook #'smartparens-mode)
;; (add-hook 'java-mode-hook #'hs-minor-mode)
(my/set-smartparens-indent 'java-mode)
Go
(use-package go-mode
:straight t
:mode "\\.go\\'"
:config
(my/set-smartparens-indent 'go-mode)
(add-hook 'go-mode-hook #'smartparens-mode)
(add-hook 'go-mode-hook #'hs-minor-mode))
.NET
C#
Guix dependencies | Disabled |
---|---|
omnisharp | t |
dotnet | t |
Disabled that for now because it depends on the old tree sitter.
(use-package csharp-mode
:straight t
:mode "\\.cs\\'"
:disabled t
:config
(setq lsp-csharp-server-path (executable-find "omnisharp-wrapper"))
(add-hook 'csharp-mode-hook #'csharp-tree-sitter-mode)
(add-hook 'csharp-tree-sitter-mode-hook #'smartparens-mode)
(add-hook 'csharp-mode-hook #'hs-minor-mode)
(my/set-smartparens-indent 'csharp-tree-sitter-mode))
MSBuild
(use-package csproj-mode
:straight t
:mode "\\.csproj\\'"
:config
(add-hook 'csproj-mode #'smartparens-mode))
Haskell
(use-package haskell-mode
:straight t
:mode "\\.hs\\'")
(use-package lsp-haskell
:straight t
:after (lsp haskell-mode))
nix
(use-package nix-mode
:straight t
:mode "\\.nix\\'"
:config
(add-hook 'nix-mode-hook #'smartparens-mode)
(my/set-smartparens-indent 'nix-mode))
Lua
(use-package lua-mode
:straight t
:mode "\\.lua\\'"
:hook (lua-mode . smartparens-mode))
(my/set-smartparens-indent 'lua-mode)
Org Mode
Org mode is a tool that leverages plain-text files for tasks like making notes, literate programming, task management, etc.
References:
Installation & basic settings
Use the built-in org mode (:type built-in
).
(setq org-directory (expand-file-name "~/30-39 Life/32 org-mode"))
(use-package org
:straight (:type built-in)
:if (not my/remote-server)
:defer t
:init
(unless (file-exists-p org-directory)
(mkdir org-directory t))
:config
(setq org-startup-indented (not my/is-termux))
(setq org-return-follows-link t)
(setq org-src-tab-acts-natively nil)
(add-hook 'org-mode-hook 'smartparens-mode)
(add-hook 'org-agenda-mode-hook
(lambda ()
(visual-line-mode -1)
(toggle-truncate-lines 1)
(display-line-numbers-mode 0)))
(add-hook 'org-mode-hook
(lambda ()
(rainbow-delimiters-mode -1))))
Encryption
Setting up org-crypt
to encrypt parts of file.
(with-eval-after-load 'org
(require 'org-crypt)
(org-crypt-use-before-save-magic)
(setq org-tags-exclude-from-inheritance '("crypt"))
(setq org-crypt-key "C1EC867E478472439CC82410DE004F32AFA00205"))
(with-eval-after-load 'epg
(setq epg-gpg-program "gpg")
(setq epg-config--program-alist
`((OpenPGP
epg-gpg-program
;; ("gpg2" . ,epg-gpg2-minimum-version)
("gpg" . ((,epg-gpg-minimum-version . "2.0")
,epg-gpg2-minimum-version)))
(CMS
epg-gpgsm-program
("gpgsm" . "2.0.4")))))
This enables encryption for Org segments tagged :crypt:
.
Another way to encrypt Org files is to save them with the extension .org.gpg
. However, by default EPA always prompts for the key, which is not what I want when there is only one key to select. Hence the following advice:
(defun my/epa--select-keys-around (fun prompt keys)
(if (= (seq-length keys) 1)
keys
(funcall fun prompt keys)))
(with-eval-after-load 'epa
(advice-add #'epa--select-keys :around #'my/epa--select-keys-around))
(unless my/remote-server
(setq epa-file-encrypt-to '("DE004F32AFA00205")))
org-contrib
org-contrib
is a package with various additions to Org. I use the following:
ox-extra
- extensions for org export
This used to have org-contacts
and ol-notmuch
at some point, but they have since been migrated to separate repos.
(use-package org-contrib
:straight (org-contrib
:type git
:repo "https://git.sr.ht/~bzg/org-contrib"
:build t)
:after (org)
:if (not my/remote-server)
:config
(require 'ox-extra)
(ox-extras-activate '(latex-header-blocks ignore-headlines)))
ol-notmuch
ol-notmuch is a package that adds Org links to notmuch messages.
(unless (or my/remote-server my/is-termux)
(use-package ol-notmuch
:straight t
:after (org notmuch)))
org-tempo
org-tempo
is a convinient package that provides snippets for various org blocks.
(with-eval-after-load 'org
(require 'org-tempo)
(add-to-list 'org-structure-template-alist '("el" . "src emacs-lisp"))
(add-to-list 'org-structure-template-alist '("py" . "src python"))
(add-to-list 'org-structure-template-alist '("sq" . "src sql")))
evil-org
Better integration with evil-mode.
(use-package evil-org
:straight t
:hook (org-mode . evil-org-mode)
:config
(add-hook 'evil-org-mode-hook
(lambda ()
(evil-org-set-key-theme '(navigation insert textobjects additional calendar todo))))
(add-to-list 'evil-emacs-state-modes 'org-agenda-mode)
(require 'evil-org-agenda)
(evil-org-agenda-set-keys))
Support for relative URLs
Source: https://emacs.stackexchange.com/questions/9807/org-mode-dont-change-relative-urls
(defun my/export-rel-url (path desc format)
(cl-case format
(html (format "<a href=\"%s\">%s</a>" path (or desc path)))
(latex (format "\\href{%s}{%s}" path (or desc path)))
(otherwise path)))
(with-eval-after-load 'org
(org-link-set-parameters "rel" :follow #'browse-url :export #'my/export-rel-url))
Keybindings & stuff
I’ve moved this block above because the my-leader-def
expression in the next block seems to override the previous ones. So it has to be on the top.
General keybindings
(with-eval-after-load 'org
(general-define-key
:keymaps 'org-mode-map
"C-c d" #'org-decrypt-entry
"C-c e" #'org-encrypt-entry
"M-p" #'org-latex-preview
"M-o" #'org-redisplay-inline-images)
(general-define-key
:keymaps 'org-mode-map
:states '(normal emacs)
"L" #'org-shiftright
"H" #'org-shiftleft
"S-<next>" #'org-next-visible-heading
"S-<prior>" #'org-previous-visible-heading
"M-0" #'org-next-visible-heading
"M-9" #'org-previous-visible-heading
"C-0" #'org-forward-heading-same-level
"C-9" #'org-backward-heading-same-level
"M-]" #'org-babel-next-src-block
"M-[" #'org-babel-previous-src-block)
(general-define-key
:keymaps 'org-agenda-mode-map
"M-]" #'org-agenda-later
"M-[" #'org-agenda-earlier)
(general-nmap :keymaps 'org-mode-map "RET" 'org-ctrl-c-ctrl-c))
Copy a link
(defun my/org-link-copy (&optional arg)
"Extract URL from org-mode link and add it to kill ring."
(interactive "P")
(let* ((link (org-element-lineage (org-element-context) '(link) t))
(type (org-element-property :type link))
(url (org-element-property :path link))
(url (concat type ":" url)))
(kill-new url)
(message (concat "Copied URL: " url))))
(with-eval-after-load 'org
(general-nmap :keymaps 'org-mode-map
"C-x C-l" 'my/org-link-copy))
Navigating source blocks
An idea born from discussing Org Mode navigation with @Infu.
Modifying org-babel-next-src-block
and org-babel-previous-src-block
to ignore hidden source blocks.
(defun my/org-babel-next-visible-src-block (arg)
"Move to the next visible source block.
With ARG, repeats or can move backward if negative."
(interactive "p")
(let ((regexp org-babel-src-block-regexp))
(if (< arg 0)
(beginning-of-line)
(end-of-line))
(while (and (< arg 0) (re-search-backward regexp nil :move))
(unless (bobp)
(while (pcase (get-char-property-and-overlay (point) 'invisible)
(`(outline . ,o)
(goto-char (overlay-start o))
(re-search-backward regexp nil :move))
(_ nil))))
(cl-incf arg))
(while (and (> arg 0) (re-search-forward regexp nil t))
(while (pcase (get-char-property-and-overlay (point) 'invisible)
(`(outline . ,o)
(goto-char (overlay-end o))
(re-search-forward regexp nil :move))
(_ (end-of-line) nil)))
(re-search-backward regexp nil :move)
(cl-decf arg))
(if (> arg 0) (goto-char (point-max)) (beginning-of-line))))
(defun my/org-babel-previous-visible-src-block (arg)
"Move to the prevous visible source block.
With ARG, repeats or can move backward if negative."
(interactive "p")
(my/org-babel-next-visible-src-block (- arg)))
(with-eval-after-load 'org
(general-define-key
:keymaps 'org-mode-map
:states '(normal emacs)
"M-]" #'my/org-babel-next-visible-src-block
"M-[" #'my/org-babel-previous-visible-src-block))
Open a file from org-directory
A function to open a file from org-directory
, excluding a few directories like roam
and journal
.
(defun my/org-file-open ()
(interactive)
(let* ((files
(thread-last
'("projects" "misc" "learning")
(mapcar (lambda (f)
(directory-files (concat org-directory "/" f) t (rx ".org" eos))))
(apply #'append)
(mapcar (lambda (file)
(string-replace (concat org-directory "/") "" file)))
(append
'("inbox.org" "contacts.org")))))
(find-file
(concat org-directory "/"
(completing-read "Org file: " files)))))
UI
LaTeX fragments
A function to enable LaTeX native highlighting. Not setting this as default, because it loads LaTeX stuff.
(defun my/enable-org-latex ()
(interactive)
(customize-set-variable 'org-highlight-latex-and-related '(native))
(add-hook 'org-mode-hook (lambda () (yas-activate-extra-mode 'LaTeX-mode)))
(sp-local-pair 'org-mode "$" "$")
(sp--remove-local-pair "'"))
Call the function before opening an org file or reopen a buffer after calling the function.
Scale latex fragments preview.
(with-eval-after-load 'org
(setq my/org-latex-scale 1.75)
(setq org-format-latex-options (plist-put org-format-latex-options :scale my/org-latex-scale)))
Also, LaTeX fragments preview tends to break whenever the are custom #+LATEX_HEADER
entries. To circumvent this, I add a custom header and modify the org-preview-latex-process-alist
variable
(with-eval-after-load 'org
(setq my/latex-preview-header "\\documentclass{article}
\\usepackage[usenames]{color}
\\usepackage{graphicx}
\\usepackage{grffile}
\\usepackage{longtable}
\\usepackage{wrapfig}
\\usepackage{rotating}
\\usepackage[normalem]{ulem}
\\usepackage{amsmath}
\\usepackage{textcomp}
\\usepackage{amssymb}
\\usepackage{capt-of}
\\usepackage{hyperref}
\\pagestyle{empty}")
(setq org-preview-latex-process-alist
(mapcar
(lambda (item)
(cons
(car item)
(plist-put (cdr item) :latex-header my/latex-preview-header)))
org-preview-latex-process-alist)))
Better headers
org-superstar-mode is a package that makes Org heading lines look a bit prettier.
Disabled it for now because of overlapping functionality with org-bars.
(use-package org-superstar
:straight t
:disabled
:hook (org-mode . org-superstar-mode))
org-bars highlights Org indentation with bars.
(use-package org-bars
:straight (:repo "tonyaldon/org-bars" :host github)
:if (display-graphic-p)
:hook (org-mode . org-bars-mode))
Fallback to the standard org-indent-mode
on terminal.
(unless (display-graphic-p)
(add-hook 'org-mode-hook #'org-indent-mode))
Remove the ellipsis at the end of folded headlines, as it seems unnecessary with org-bars
.
(defun my/org-no-ellipsis-in-headlines ()
(remove-from-invisibility-spec '(outline . t))
(add-to-invisibility-spec 'outline))
(with-eval-after-load 'org-bars
(add-hook 'org-mode-hook #'my/org-no-ellipsis-in-headlines)
(when (eq major-mode 'org-mode)
(my/org-no-ellipsis-in-headlines)))
Override colors
(my/use-colors
(org-block :background (my/color-value 'bg-other))
(org-block-begin-line :background (my/color-value 'bg-other)
:foreground (my/color-value 'grey)))
Hide stuff in buffer
org-appear is a package that toggles visibility of hidden elements upon entering and leaving them.
(use-package org-appear
:after (org)
:straight t)
org-fragtog does the same for LaTeX fragment previews.
(use-package org-fragtog
:after (org)
:straight t)
Literate programing
Python & Jupyter
Use jupyter kernels for Org Mode.
References:
(use-package jupyter
:straight t
:after (org)
:if (not (or my/remote-server my/is-termux)))
Refresh kernelspecs.
Kernelspecs by default are hashed, so even switching Anaconda environments doesn’t change the kernel (i.e. kernel from the first environment is run after the switch to the second one).
(defun my/jupyter-refresh-kernelspecs ()
"Refresh Jupyter kernelspecs"
(interactive)
(jupyter-available-kernelspecs t))
Also, if some kernel wasn’t present at the moment of the load of emacs-jupyter
, it won’t be added to the org-src-lang-modes
list. E.g. I have Hy kernel installed in a separate Anaconda environment, so if Emacs hasn’t been launched in this environment, I wouldn’t be able to use hy
in org-src blocks.
Fortunately, emacs-jupyter
provides a function for that problem as well.
(defun my/jupyter-refesh-langs ()
"Refresh Jupyter languages"
(interactive)
(org-babel-jupyter-aliases-from-kernelspecs t))
A function to load jupyter
. The problem with doing that on startup is that it tried to locate the jupyter
executable, which I have only in an environment.
(defun my/org-load-jupyter ()
(interactive)
(org-babel-do-load-languages
'org-babel-load-languages
'((jupyter . t)))
(my/jupyter-refesh-langs))
Hy
(use-package ob-hy
:after (org)
:if (not my/remote-server)
:straight t)
View HTML in browser
Open HTML in the begin_export
block with xdg-open.
(setq my/org-view-html-tmp-dir "/tmp/org-html-preview/")
(use-package f
:straight t)
(defun my/org-view-html ()
(interactive)
(let ((elem (org-element-at-point))
(temp-file-path (concat my/org-view-html-tmp-dir (number-to-string (random (expt 2 32))) ".html")))
(cond
((not (eq 'export-block (car elem)))
(message "Not in an export block!"))
((not (string-equal (plist-get (car (cdr elem)) :type) "HTML"))
(message "Export block is not HTML!"))
(t (progn
(f-mkdir my/org-view-html-tmp-dir)
(f-write (plist-get (car (cdr elem)) :value) 'utf-8 temp-file-path)
(start-process "org-html-preview" nil "xdg-open" temp-file-path))))))
PlantUML
(with-eval-after-load 'org
(setq org-plantuml-executable-path "/home/pavel/.guix-extra-profiles/emacs/emacs/bin/plantuml")
(setq org-plantuml-exec-mode 'plantuml)
(add-to-list 'org-src-lang-modes '("plantuml" . plantuml)))
Restclient
restclient.el is an Emacs package to send HTTP requests. ob-restclient provides interaction with Org Babel.
References:
(use-package restclient
:if (not my/remote-server)
:straight t
:mode ("\\.http\\'" . restclient-mode)
:config
(general-define-key
:keymaps 'restclient-mode-map
:states '(normal visual)
"RET" #'restclient-http-send-current
"M-RET" #'restclient-http-send-current-stay-in-window
"y" nil
"M-y" #'restclient-copy-curl-command)
(general-define-key
:keymaps 'restclient-response-mode-map
:states '(normal visual)
"q" #'quit-window))
(use-package ob-restclient
:after (org restclient)
:if (not my/remote-server)
:straight t)
Org Babel Setup
Enable languages
(with-eval-after-load 'org
(org-babel-do-load-languages
'org-babel-load-languages
`((emacs-lisp . t)
(python . t)
(sql . t)
;; (typescript .t)
(hy . t)
(shell . t)
(plantuml . t)
(octave . t)
(sparql . t)
(gnuplot . t)))
(add-hook 'org-babel-after-execute-hook 'org-redisplay-inline-images))
Use Jupyter block instead of built-in Python.
(with-eval-after-load 'ob-jupyter
(org-babel-jupyter-override-src-block "python")
(org-babel-jupyter-override-src-block "hy"))
Turn of some minor modes in source blocks.
(add-hook 'org-src-mode-hook
(lambda ()
;; (hs-minor-mode -1)
;; (electric-indent-local-mode -1)
;; (rainbow-delimiters-mode -1)
(highlight-indent-guides-mode -1)))
Async code blocks evaluations. Jupyter blocks have a built-in async, so they are set as ignored.
(use-package ob-async
:straight t
:after (org)
:config
(setq ob-async-no-async-languages-alist '("python" "hy" "jupyter-python" "jupyter-octave" "restclient")))
Managing Jupyter kernels
Functions for managing local Jupyter kernels.
my/insert-jupyter-kernel
inserts a path to an active Jupyter kernel to the buffer. Useful to quickly write a header like:
#+PROPERTY: header-args:python :session <path-to-kernel>
my/jupyter-connect-repl
opens a emacs-jupyter
REPL, connected to an active kernel. my/jupyter-qtconsole
runs a standalone Jupyter QtConsole.
Requirements: ss
(setq my/jupyter-runtime-folder (expand-file-name "~/.local/share/jupyter/runtime"))
(defun my/get-open-ports ()
(mapcar
#'string-to-number
(split-string (shell-command-to-string "ss -tulpnH | awk '{print $5}' | sed -e 's/.*://'") "\n")))
(defun my/list-jupyter-kernel-files ()
(mapcar
(lambda (file) (cons (car file) (cdr (assq 'shell_port (json-read-file (car file))))))
(sort
(directory-files-and-attributes my/jupyter-runtime-folder t ".*kernel.*json$")
(lambda (x y) (not (time-less-p (nth 6 x) (nth 6 y)))))))
(defun my/select-jupyter-kernel ()
(let ((ports (my/get-open-ports))
(files (my/list-jupyter-kernel-files)))
(completing-read
"Jupyter kernels: "
(seq-filter
(lambda (file)
(member (cdr file) ports))
files))))
(defun my/insert-jupyter-kernel ()
"Insert a path to an active Jupyter kernel into the buffer"
(interactive)
(insert (my/select-jupyter-kernel)))
(defun my/jupyter-connect-repl ()
"Open an emacs-jupyter REPL, connected to a Jupyter kernel"
(interactive)
(jupyter-connect-repl (my/select-jupyter-kernel) nil nil nil t))
(defun my/jupyter-qtconsole ()
"Open Jupyter QtConsole, connected to a Jupyter kernel"
(interactive)
(start-process "jupyter-qtconsole" nil "setsid" "jupyter" "qtconsole" "--existing"
(file-name-nondirectory (my/select-jupyter-kernel))))
I’ve also noticed that there are JSON files left in the runtime folder whenever the kernel isn’t stopped correctly. So here is a cleanup function.
(defun my/jupyter-cleanup-kernels ()
(interactive)
(let* ((ports (my/get-open-ports))
(files (my/list-jupyter-kernel-files))
(to-delete (seq-filter
(lambda (file)
(not (member (cdr file) ports)))
files)))
(when (and (length> to-delete 0)
(y-or-n-p (format "Delete %d files?" (length to-delete))))
(dolist (file to-delete)
(delete-file (car file))))))
Output post-processing
Do not wrap the output in emacs-jupyter
Emacs-jupyter has its own insertion mechanisms, which always prepends output statements with :
. That is not desirable in cases where a kernel supports only plain output, e.g. calysto_hy kernel.
So there we have a minor mode that overrides this behavior.
(defun my/jupyter-org-scalar (value)
(cond
((stringp value) value)
(t (jupyter-org-scalar value))))
(define-minor-mode my/emacs-jupyter-raw-output
"Make emacs-jupyter do raw output")
(defun my/jupyter-org-scalar-around (fun value)
(if my/emacs-jupyter-raw-output
(my/jupyter-org-scalar value)
(funcall fun value)))
(with-eval-after-load 'jupyter
(advice-add 'jupyter-org-scalar :around #'my/jupyter-org-scalar-around))
Wrap source code output
A function to remove the :RESULTS: drawer from results. Once again, it’s necessary because emacs-jupyter doesn’t seem to respect :results raw
.
(defun my/org-strip-results (data)
(replace-regexp-in-string ":\\(RESULTS\\|END\\):\n" "" data))
And an all-in-one function to:
- prepend
#+NAME:
and#+CAPTION:
to the source block output. Useful if the output is an image. - strip the :RESULTS: drawer from the output, if necessary
- wrap results in the
src
block
As for now, it looks sufficient to format source code outputs to get a tolerable LaTeX.
(defun my/org-caption-wrap (data &optional name caption attrs strip-drawer src-wrap)
(let* ((data-s (if (and strip-drawer (not (string-empty-p strip-drawer)))
(my/org-strip-results data)
data))
(drawer-start (if (string-match-p "^:RESULTS:.*" data-s) 10 0)))
(concat
(substring data-s 0 drawer-start)
(and name (not (string-empty-p name)) (concat "#+NAME:" name "\n"))
(and caption (not (string-empty-p caption)) (concat "#+CAPTION:" caption "\n"))
(and attrs (not (string-empty-p attrs)) (concat "#+ATTR_LATEX:" attrs "\n"))
(if (and src-wrap (not (string-empty-p src-wrap)))
(concat "#+begin_src " src-wrap "\n"
(substring data-s drawer-start)
(when (not (string-match-p ".*\n" data-s)) "\n")
"#+end_src")
(substring data-s drawer-start)))))
To use, add the following snippet to the org file:
#+NAME: out_wrap
#+begin_src emacs-lisp :var data="" caption="" name="" attrs="" strip-drawer="" src-wrap="" :tangle no :exports none
(my/org-caption-wrap data name caption attrs strip-drawer src-wrap)
#+end_src
Example usage:
:post out_wrap(name="fig:chart", caption="График", data=*this*)
Apply ANSI color codes
SOURCE: Apply ANSI color escape sequences for Org Babel results
A minor mode to apply ANSI color codes after execution.
(defun my/babel-ansi ()
(when-let ((beg (org-babel-where-is-src-block-result nil nil)))
(save-excursion
(goto-char beg)
(when (looking-at org-babel-result-regexp)
(let ((end (org-babel-result-end))
(ansi-color-context-region nil))
(ansi-color-apply-on-region beg end))))))
(define-minor-mode org-babel-ansi-colors-mode
"Apply ANSI color codes to Org Babel results."
:global t
:after-hook
(if org-babel-ansi-colors-mode
(add-hook 'org-babel-after-execute-hook #'my/babel-ansi)
(remove-hook 'org-babel-after-execute-hook #'my/babel-ansi)))
Executing stuff
A few convinient functions and keybindings to execute things in an org buffer.
First, execute things above and below the point:
(defun my/org-babel-execute-buffer-below (&optional arg)
(interactive "P")
(org-babel-eval-wipe-error-buffer)
(let ((point (point)))
(org-save-outline-visibility t
(org-babel-map-executables nil
(when (>= (point) point)
(if (memq (org-element-type (org-element-context))
'(babel-call inline-babel-call))
(org-babel-lob-execute-maybe)
(org-babel-execute-src-block arg)))))))
(defun my/org-babel-execute-buffer-above (&optional arg)
(interactive "P")
(org-babel-eval-wipe-error-buffer)
(let ((point (point)))
(org-save-outline-visibility t
(org-babel-map-executables nil
(when (<= (point) point)
(if (memq (org-element-type (org-element-context))
'(babel-call inline-babel-call))
(org-babel-lob-execute-maybe)
(org-babel-execute-src-block arg)))))))
Some keybindings:
(with-eval-after-load 'org
(general-define-key
:keymaps 'org-babel-map
"B" #'my/org-babel-execute-buffer-below
"A" #'my/org-babel-execute-buffer-above)
(my-leader-def
:keymaps 'org-mode-map
"SPC b" '(:wk "org-babel")
"SPC b" org-babel-map))
Managing a literate programming project
A few tricks to do literate programming. I actually have only one (sqrt-data), and I’m not convinced in the benefits of the approach…
Anyway, Org files are better off in a separated directory (e.g. org
). So I’ve come up with the following solution to avoid manually prefixing the :tangle
arguments.
Set up the following argument with the path to the project root:
#+PROPERTY: PRJ-DIR ..
A function to do the prefixing:
(defun my/org-prj-dir (path)
(expand-file-name path (org-entry-get nil "PRJ-DIR" t)))
Example usage is as follows:
:tangle (my/org-prj-dir "sqrt_data/api/__init__.py")
Tools
Various small packages.
Presentations
Doing presentations with org-present.
(use-package hide-mode-line
:straight t
:commands (hide-mode-line-mode))
(defun my/present-next-with-latex ()
(interactive)
(org-present-next)
(org-latex-preview '(16)))
(defun my/present-prev-with-latex ()
(interactive)
(org-present-prev)
(org-latex-preview '(16)))
(use-package org-present
:straight (:host github :repo "rlister/org-present")
:if (not my/remote-server)
:commands (org-present)
:config
(general-define-key
:keymaps 'org-present-mode-keymap
"<next>" 'my/present-next-with-latex
"<prior>" 'my/present-prev-with-latex)
(setq org-present-mode-hook
(list (lambda ()
(blink-cursor-mode 0)
(org-present-big)
(org-bars-mode -1)
;; (org-display-inline-images)
(org-present-hide-cursor)
(org-present-read-only)
(display-line-numbers-mode 0)
(hide-mode-line-mode +1)
(setq-local org-format-latex-options
(plist-put org-format-latex-options
:scale (* org-present-text-scale my/org-latex-scale 0.5)))
;; (org-latex-preview '(16))
;; TODO ^somehow this stucks at running LaTeX^
(setq-local olivetti-body-width 60)
(olivetti-mode 1))))
(setq org-present-mode-quit-hook
(list (lambda ()
(blink-cursor-mode 1)
(org-present-small)
(org-bars-mode 1)
;; (org-remove-inline-images)
(org-present-show-cursor)
(org-present-read-write)
(display-line-numbers-mode 1)
(hide-mode-line-mode 0)
(setq-local org-format-latex-options (plist-put org-format-latex-options :scale my/org-latex-scale))
(org-latex-preview '(64))
(olivetti-mode -1)
(setq-local olivetti-body-width (default-value 'olivetti-body-width))))))
TOC
Make a TOC inside the org file.
References:
(use-package org-make-toc
:after (org)
:if (not my/remote-server)
:commands
(org-make-toc
org-make-toc-insert
org-make-toc-set
org-make-toc-at-point)
:straight t)
Screenshots
A nice package to make screenshots and insert them to the Org document.
(use-package org-attach-screenshot
:commands (org-attach-screenshot)
:straight t)
Transclusion
A package that implements transclusions in Org Mode, i.e. rendering part of one file inside another file.
(use-package org-transclusion
:after org
:straight (:host github :repo "nobiot/org-transclusion")
:config
(add-to-list 'org-transclusion-extensions 'org-transclusion-indent-mode)
(require 'org-transclusion-indent-mode)
(general-define-key
:keymaps '(org-transclusion-map)
:states '(normal)
"RET" #'org-transclusion-open-source
"gr" #'org-transclusion-refresh)
(general-define-key
:keymaps '(org-mode-map)
:states 'normal
"C-c t a" #'org-transclusion-add
"C-c t A" #'org-transclusion-add-all
"C-c t t" #'org-transclusion-mode))
Drawing
This package is unbelievably good. I would have never thought it’s even possible to have this in Emacs.
(use-package edraw-org
:straight (:host github :repo "misohena/el-easydraw")
:if (and (not my/is-termux) (not my/remote-server))
:after (org)
:config
(edraw-org-setup-default))
Managing tables
I use Org to manage some small tables which I want to process further. So here is a function that saves each table to a CSV file.
(defun my/export-org-tables-to-csv ()
(interactive)
(org-table-map-tables
(lambda ()
(when-let
(name
(plist-get (cadr (org-element-at-point)) :name))
(org-table-export
(concat
(file-name-directory
(buffer-file-name))
name ".csv")
"orgtbl-to-csv")))))
Partial scrolling
(use-package phscroll
:straight (:host github :repo "misohena/phscroll")
:commands (org-phscroll-mode)
:config
(with-eval-after-load 'org
(require 'org-phscroll)
(org-phscroll-deactivate)))
Productivity & Knowledge management
My ongoing effort to get a productivity setup manage something in my life in Org.
Initial inspirations (
):- Nicolas P. Rougier. Get Things Done with Emacs
- Jetro Kuan. Org-mode Workflow
- Alexey Shmalko: How I note
- Rohit Goswami: An Orgmode Note Workflow
Current status of what I ended up using (
):- org-journal for keeping a journal
- org-roam for a knowledge base.
- org-agenda with org-clock for tasks
Org Agenda & Project Management
This section had seen a lot of experimentation over the last… well, years.
Agenda & refile files
All my project files live in the /projects
directory, so here’s a function to set up org-agenda-files
and org-refile-targets
accordingly.
Also, my project structure is somewhat chaotic, so I have an .el
file in the org directory that defines some of the refile targets.
(defun my/update-org-agenda ()
(interactive)
(let ((project-files
(when (file-directory-p (concat org-directory "/projects"))
(thread-last "/projects"
(concat org-directory)
(directory-files)
(mapcar (lambda (f)
(concat
org-directory "/projects/" f)))
(seq-filter (lambda (f)
(not (file-directory-p f))))))))
(setq org-agenda-files
(seq-filter #'file-exists-p
(append
project-files
(mapcar (lambda (f)
(concat org-directory "/" f))
'("inbox.org"
"misc/habit.org"
"contacts.org")))))
(setq org-refile-targets
`(,@(mapcar
(lambda (f) `(,f . (:tag . "refile")))
project-files)
,@(mapcar
(lambda (f) `(,f . (:regexp . "Tasks")))
project-files)))
(when (file-exists-p (concat org-directory "/scripts/refile.el"))
(load-file (concat org-directory "/scripts/refile.el"))
(run-hooks 'my/org-refile-hooks))))
(setq org-roam-directory (concat org-directory "/roam"))
(with-eval-after-load 'org
(require 'seq)
(my/update-org-agenda))
Refile settings
(setq org-refile-use-outline-path 'file)
(setq org-outline-path-complete-in-steps nil)
My day ends late sometimes. Thanks John Wigley.
(setq org-extend-today-until 4)
Capture templates
Settings for Org capture mode. The goal here is to have a non-disruptive process to capture various ideas.
(defun my/generate-inbox-note-name ()
(format
"%s/inbox-notes/%s%s.org"
org-directory
(format-time-string "%Y%m%d%H%M%S")
(let ((note-name (read-string "Note name: ")))
(if (not (string-empty-p note-name))
(string-replace " " "-" (concat "-" (downcase note-name)))
""))))
(setq org-capture-templates
`(("i" "Inbox" entry (file "inbox.org")
,(concat "* TODO %?\n"
"/Entered on/ %U"))
("e" "email" entry (file "inbox.org")
,(concat "* TODO %:from %:subject \n"
"/Entered on/ %U\n"
"/Received on/ %:date-timestamp-inactive\n"
"%a\n"))
("f" "elfeed" entry (file "inbox.org")
,(concat "* TODO %:elfeed-entry-title\n"
"/Entered on/ %U\n"
"%a\n"))
("n" "note" plain (file my/generate-inbox-note-name)
,(concat "#+TODO: PROCESSED(p)\n"
"\n"
"* %?\n"
"/Entered on/ %U"))))
org-clock & org-clock-agg
org-clock allows for tracking time spent in Org entries. org-clock-agg is my package for creating reports from org-clock records.
It’s been somewhat complicated to integrate into my workflow, but I think it’s been worth it because I can now create reports for:
- how much time i spent on which category of tasks (education / job / …);
- time spent per activity, particularly time spent on meetings per category;
- time spent per project
- …
(use-package org-clock-agg
:straight (:host github :repo "SqrtMinusOne/org-clock-agg")
:commands (org-clock-agg)
:init
(with-eval-after-load 'org
(my-leader-def "ol" #'org-clock-agg))
:config
(push
(cons "Agenda+Archive"
(append
(org-agenda-files)
(thread-last "/projects/archive"
(concat org-directory)
(directory-files)
(mapcar (lambda (f)
(concat
org-directory "/projects/archive/" f)))
(seq-filter (lambda (f)
(not (file-directory-p f)))))))
org-clock-agg-files-preset))
The following enables org-clock persistence between Emacs sessions.
(with-eval-after-load 'org
(setq org-clock-persist 'clock)
(org-clock-persistence-insinuate))
Effort estimation. Not using this as of now.
(with-eval-after-load 'org
(add-to-list
'org-global-properties
'("Effort_ALL" . "0 0:05 0:10 0:15 0:30 0:45 1:00 1:30 2:00 4:00 8:00")))
Log DONE time
(setq org-log-done 'time)
Custom modeline positioning
I wanted org-mode-line-string
to be prepended to global-mode-string
rather than appended, but somehow the modeline stops working if org-mode-line-string
is the first element… So I’ll at least put it before my exwm-modeline-segment
.
(defun my/org-clock-in--fix-mode-line ()
(when (memq 'org-mode-line-string global-mode-string)
(let (new-global-mode-string
appended
(is-first t))
(dolist (item global-mode-string)
(cond
((or (equal item '(:eval (exwm-modeline-segment)))
(equal item '(:eval (persp-mode-line))))
(unless appended
(when is-first
(push "" new-global-mode-string))
(push 'org-mode-line-string new-global-mode-string)
(setq appended t))
(push item new-global-mode-string))
((equal item 'org-mode-line-string))
(t
(push item new-global-mode-string)))
(setq is-first nil))
(unless appended
(push 'org-mode-line-string new-global-mode-string))
(setq global-mode-string (nreverse new-global-mode-string)))))
(add-hook 'org-clock-in-hook #'my/org-clock-in--fix-mode-line)
Prompt start time for org-clock-in
Support prompting for start time for org-clock-in
:
(defun my/org-clock-in-prompt-time (&optional select)
(interactive "P")
(org-clock-in
select
(encode-time
(org-parse-time-string
(org-read-date t)))))
(with-eval-after-load 'org
(my-leader-def
:keymaps 'org-mode-map
:infix "SPC"
"I" #'my/org-clock-in-prompt-time))
Put total clocked time in properties
By default, org-clock
stores its results only in the :LOGBOOK:
drawer, which doesn’t get parsed by org-element-at-point
. As such, clock resutls are inaccessible from org-ql
.
This ensures that the total clocked time is also saved in the :PROPERTIES:
drawer.
We can get the clocked value in minutes with org-clock-sum
. This weird function stores what I need in buffer-local variables and text-properties.
(defun my/org-clock-get-total-minutes-at-point ()
"Get total clocked time for heading at point."
(let* ((element (org-element-at-point-no-context))
(s (buffer-substring-no-properties
(org-element-property :begin element)
(org-element-property :end element))))
(with-temp-buffer
(insert s)
(org-clock-sum)
org-clock-file-total-minutes)))
And use the function to set the total clocked time.
(defconst my/org-clock-total-prop :CLOCK_TOTAL)
(defun my/org-clock-set-total-clocked ()
"Set total clocked time for heading at point."
(interactive)
(save-excursion
(org-back-to-heading t)
(org-set-property
(substring
(symbol-name my/org-clock-total-prop)
1)
(org-duration-from-minutes
(my/org-clock-get-total-minutes-at-point)))))
(add-hook 'org-clock-in-hook #'my/org-clock-set-total-clocked)
(add-hook 'org-clock-out-hook #'my/org-clock-set-total-clocked)
(add-hook 'org-clock-cancel-hook #'my/org-clock-set-total-clocked)
Switch between recently clocked items
(defun my/org-clock-recent ()
(interactive)
(let* ((entries (org-ql-query
:select #'element-with-markers
:from (org-agenda-files)
:where '(clocked :from -1)))
(entries-data (mapcar (lambda (e)
(cons (org-element-property :raw-value e) e))
entries)))
(unless entries
(user-error "No recently clocked entries!"))
entries-data
(let* ((entry (alist-get (completing-read "Entry: " entries-data)
entries-data nil nil #'equal))
(marker (org-element-property :org-marker entry)))
(pop-to-buffer-same-window (marker-buffer marker))
(goto-char marker))))
(with-eval-after-load 'org
(my-leader-def
:keymaps 'org-mode-map
:infix "SPC"
"C" #'my/org-clock-recent))
org-super-agenda
org-super-agenda is alphapapa’s extension to group items in org-agenda. I don’t use it instead of the standard agenda, but org-ql
uses it for some of its views.
(use-package org-super-agenda
:straight t
:after (org)
:config
;; Alphapapa doesn't like evil
(general-define-key
:keymaps '(org-super-agenda-header-map)
"h" nil
"j" nil
"k" nil
"l" nil)
(org-super-agenda--def-auto-group outline-path-file "their outline paths & files"
:key-form
(org-super-agenda--when-with-marker-buffer (org-super-agenda--get-marker item)
;; org-ql depends on f and s anyway
(s-join "/" (cons
(f-filename (buffer-file-name))
(org-get-outline-path))))))
It doesn’t look great with org-bars mode, so…
(defun my/org-super-agenda--make-agenda-header-around (fun name)
(remove-text-properties 0 (length name) '(line-prefix nil) name)
(remove-text-properties 0 (length name) '(wrap-prefix nil) name)
(funcall fun (substring-no-properties name)))
(with-eval-after-load 'org-super-agenda
(advice-add 'org-super-agenda--make-agenda-header :around #'my/org-super-agenda--make-agenda-header-around))
org-ql
org-ql is a package to query org files.
(use-package org-ql
:after (org)
:if (not my/remote-server)
:straight t
:config
(setq org-ql-ask-unsafe-queries nil)
:init
;; See https://github.com/alphapapa/org-ql/pull/237
(setq org-ql-regexp-part-ts-time
(rx " " (repeat 1 2 digit) ":" (repeat 2 digit)
(optional "-" (repeat 1 2 digit) ":" (repeat 2 digit))))
(my-leader-def
:infix "o"
"v" #'org-ql-view
"q" #'org-ql-search))
Recent items
I just want to change the default grouping in org-ql-view-recent-items
…
(cl-defun my/org-ql-view-recent-items
(&key num-days (type 'ts)
(files (org-agenda-files))
(groups '((:auto-outline-path-file t)
(:auto-todo t))))
"Show items in FILES from last NUM-DAYS days with timestamps of TYPE.
TYPE may be `ts', `ts-active', `ts-inactive', `clocked', or
`closed'."
(interactive (list :num-days (read-number "Days: ")
:type (->> '(ts ts-active ts-inactive clocked closed)
(completing-read "Timestamp type: ")
intern)))
;; It doesn't make much sense to use other date-based selectors to
;; look into the past, so to prevent confusion, we won't allow them.
(-let* ((query (pcase-exhaustive type
((or 'ts 'ts-active 'ts-inactive)
`(,type :from ,(- num-days) :to 0))
((or 'clocked 'closed)
`(,type :from ,(- num-days) :to 0)))))
(org-ql-search files query
:title "Recent items"
:sort '(todo priority date)
:super-groups groups)))
Return all TODOs
A view to return all TODOs in a category.
(defun my/org-ql-all-todo ()
(interactive)
;; The hack I borrowed from notmuch to make " " a separator
(let* ((crm-separator " ")
(crm-local-completion-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map crm-local-completion-map)
(define-key map " " 'self-insert-command)
map))
(vertico-sort-function nil)
(categories (completing-read-multiple
"Categories: "
'("TEACH" "EDU" "JOB" "LIFE" "COMP"))))
(org-ql-search (org-agenda-files)
`(and (todo)
,@(unless (seq-empty-p categories)
`((category ,@categories))))
:sort '(priority todo deadline)
:super-groups '((:auto-outline-path-file t)))))
Configuring views
Putting all the above in org-ql-views
.
(setq org-ql-views
(list
(cons "Overview: All TODO" #'my/org-ql-all-todo)
(cons "Review: Stale tasks"
(list :buffers-files #'org-agenda-files
:query '(and (todo)
(not (tags "nots"))
(not (ts :from -14)))
:title "Review: Stale tasks"
:sort '(todo priority date)
:super-groups '((:auto-outline-path-file t))))
(cons "Review: Unclocked tasks"
(list :buffers-files #'org-agenda-files
:query '(and (done)
(ts :from -14)
(not (clocked))
(not (tags "nots")))
:title "Review: Unclocked tasks"
:sort '(todo priority date)
:super-groups '((:auto-outline-path-file t))))
(cons "Review: Recently timestamped" #'my/org-ql-view-recent-items)
(cons "Review: Unlinked to meetings"
(list :buffers-files #'org-agenda-files
:query '(and (todo "DONE" "NO")
(not (property "MEETING"))
(ts :from -7))
:super-groups '((:auto-outline-path-file t))))
(cons "Review: Meeting" #'my/org-ql-meeting-tasks)
(cons "Fix: tasks without TASK_KIND"
(lambda ()
(interactive)
(org-ql-search (current-buffer)
'(and (olp "Tasks")
(not (property "TASK_KIND"))
(clocked))
:super-groups '((:auto-outline-path-file t)))))))
Custom format element
Changing the default org-ql-view--format-element
to include effort estimation and the clocked time. I wish it were more configurable out-of-the-box.
(defun my/org-ql-view--format-element-override (element)
"Format ELEMENT for `org-ql-view'.
Check `org-ql-view--format-element' for the original implementation
and lots of comments which are too long for my Emacs config."
(if (not element)
""
(setf element (org-ql-view--resolve-element-properties element))
(let* ((properties (cadr element))
(properties (cl-loop for (key val) on properties by #'cddr
for symbol = (intern (cl-subseq (symbol-name key) 1))
unless (member symbol '(parent))
append (list symbol val)))
(title (--> (org-ql-view--add-faces element)
(org-element-property :raw-value it)
(org-link-display-format it)))
(todo-keyword (-some--> (org-element-property :todo-keyword element)
(org-ql-view--add-todo-face it)))
(tag-list (if org-use-tag-inheritance
(if-let ((marker (or (org-element-property :org-hd-marker element)
(org-element-property :org-marker element))))
(with-current-buffer (marker-buffer marker)
(org-with-wide-buffer
(goto-char marker)
(cl-loop for type in (org-ql--tags-at marker)
unless (or (eq 'org-ql-nil type)
(not type))
append type)))
(display-warning 'org-ql (format "No marker found for item: %s" title))
(org-element-property :tags element))
(org-element-property :tags element)))
(tag-string (when tag-list
(--> tag-list
(s-join ":" it)
(s-wrap it ":")
(org-add-props it nil 'face 'org-tag))))
;; (category (org-element-property :category element))
(priority-string (-some->> (org-element-property :priority element)
(char-to-string)
(format "[#%s]")
(org-ql-view--add-priority-face)))
(clock-string (let ((effort (org-element-property :EFFORT element))
(clocked (org-element-property my/org-clock-total-prop element)))
(cond
((and clocked effort) (format "[%s/%s]" clocked effort))
((and clocked (not effort) (format "[%s]" clocked)))
((and (not clocked) effort) (format "[EST: %s]" effort)))))
(habit-property (org-with-point-at (or (org-element-property :org-hd-marker element)
(org-element-property :org-marker element))
(when (org-is-habit-p)
(org-habit-parse-todo))))
(due-string (pcase (org-element-property :relative-due-date element)
('nil "")
(string (format " %s " (org-add-props string nil 'face 'org-ql-view-due-date)))))
(string (s-join " " (-non-nil (list todo-keyword priority-string title due-string clock-string tag-string)))))
(remove-list-of-text-properties 0 (length string) '(line-prefix) string)
(--> string
(concat " " it)
(org-add-props it properties
'org-agenda-type 'search
'todo-state todo-keyword
'tags tag-list
'org-habit-p habit-property)))))
(with-eval-after-load 'org-ql
(advice-add #'org-ql-view--format-element :override #'my/org-ql-view--format-element-override))
Tracking habits
Let’s see how this goes.
References:
org-habit-stats is a pretty nice package. Using my fork until my PR is merged.
(use-package org-habit-stats
:straight (:host github :repo "ml729/org-habit-stats")
:after (org)
:config
(general-define-key
:keymaps '(org-habit-stats-mode-map)
:states '(normal emacs)
"q" #'org-habit-stats-exit
"<" #'org-habit-stats-calendar-scroll-left
">" #'org-habit-stats-calendar-scroll-right
"[" #'org-habit-stats-scroll-graph-left
"]" #'org-habit-stats-scroll-graph-right
"{" #'org-habit-stats-scroll-graph-left-big
"}" #'org-habit-stats-scroll-graph-right-big
"." #'org-habit-stats-view-next-habit
"," #'org-habit-stats-view-previous-habit)
(add-hook 'org-after-todo-state-change-hook 'org-habit-stats-update-properties))
Custom agendas
Some custom agendas to fit my workflow.
See this answer at Emacs StackExchange for filtering the agenda
block by tag:
(defun my/org-match-at-point-p (match)
"Return non-nil if headline at point matches MATCH.
Here MATCH is a match string of the same format used by
`org-tags-view'."
(funcall (cdr (org-make-tags-matcher match))
(org-get-todo-state)
(org-get-tags-at)
(org-reduced-level (org-current-level))))
(defun my/org-agenda-skip-without-match (match)
"Skip current headline unless it matches MATCH.
Return nil if headline containing point matches MATCH (which
should be a match string of the same format used by
`org-tags-view'). If headline does not match, return the
position of the next headline in current buffer.
Intended for use with `org-agenda-skip-function', where this will
skip exactly those headlines that do not match."
(save-excursion
(unless (org-at-heading-p) (org-back-to-heading))
(let ((next-headline (save-excursion
(or (outline-next-heading) (point-max)))))
(if (my/org-match-at-point-p match) nil next-headline))))
And the agendas themselves:
(defun my/org-scheduled-get-time ()
(let ((scheduled (org-get-scheduled-time (point))))
(if scheduled
(format-time-string "%Y-%m-%d" scheduled)
"")))
(setq org-agenda-hide-tags-regexp (rx (or "org" "refile" "proj" "habit")))
(setq org-agenda-custom-commands
`(("p" "My outline"
((agenda "" ((org-agenda-skip-function '(my/org-agenda-skip-without-match "-habit"))))
(tags-todo "inbox"
((org-agenda-overriding-header "Inbox")
(org-agenda-prefix-format " %i %-12:c")
(org-agenda-hide-tags-regexp ".")))
(tags-todo "+waitlist+SCHEDULED<=\"<+14d>\""
((org-agenda-overriding-header "Waitlist")
(org-agenda-hide-tags-regexp "waitlist")
(org-agenda-prefix-format " %i %-12:c %-12(my/org-scheduled-get-time)")))
(tags-todo "habit+SCHEDULED<=\"<+0d>\""
((org-agenda-overriding-header "Habits")
(org-agenda-prefix-format " %i %-12:c")
(org-agenda-hide-tags-regexp ".")))))))
Alerts
- Me at 10:00: Open Org Agenga oh, there’s a meeting at 15:00
- Me at 14:00: Open Org Agenda oh, there’s a meeting at 15:00
- Me at 14:45: Gotta remember to join in 15 minutes
- Me at 14:55: Gotta remember to join in 5 minutes
- Me at 15:05: Sh*t
Okay, I will set up org-alert some custom alert system.
There’s also org-yaap by Amy Grinn, but I opt for my system for now.
(use-package org-yaap
:straight (org-yaap :type git :host gitlab :repo "SqrtMinusOne/org-yaap")
:after (org)
:if (not my/nested-emacs)
:disabled t
:config
(org-yaap-mode 1)
(setq org-yaap-alert-before '(10 1))
(setq org-yaap-alert-title "PROXIMITY ALERT")
(setq org-yaap-todo-keywords-only '("FUTURE")))
I want to have multiple warnings, let it be 10 minutes in advance and 1 minute in advance for now.
(setq my/org-alert-notify-times '(600 60))
And IDK if that makes much sense, but I’ll try to avoid re-creating timers. So, here are functions to schedule showing some label at some time and to check whether the label is scheduled:
(setq my/org-alert--alerts (make-hash-table :test #'equal))
(defun my/org-alert--is-scheduled (label time)
"Check if LABEL is scheduled to be shown an TIME."
(gethash (cons label time)
my/org-alert--alerts nil))
(defun my/org-alert--schedule (label time)
"Schedule LABEL to be shown at TIME, unless it's already scheduled."
(unless (my/org-alert--is-scheduled label time)
(puthash (cons label time)
(run-at-time time
nil
(lambda ()
(alert label
:title "PROXIMITY ALERT")))
my/org-alert--alerts)))
And unschedule items that need to be unscheduled:
(defun my/org-alert-cleanup (&optional keys)
"Unschedule items that do not appear in KEYS.
KEYS is a list of cons cells like (<label> . <time>)."
(let ((existing-hash (make-hash-table :test #'equal)))
(cl-loop for key in keys
do (puthash key t existing-hash))
(cl-loop for key being the hash-keys of my/org-alert--alerts
unless (gethash key existing-hash)
do (progn
(cancel-timer (gethash key my/org-alert--alerts))
(remhash key my/org-alert--alerts)))))
And a function to extract the required items with org-ql-query
and schedule them:
(defun my/org-alert--update-today-alerts ()
(when-let* ((files (org-agenda-files))
(items
(org-ql-query
:select 'element
:from files
:where `(and
(todo "FUTURE")
(ts-active :from ,(format-time-string "%Y-%m-%d %H:%M")
:to ,(format-time-string
"%Y-%m-%d"
(time-add
(current-time)
(* 60 60 24)))
:with-time t))
:order-by 'date)))
(let (scheduled-keys)
(cl-loop
for item in items
for scheduled = (org-timestamp-to-time (org-element-property :scheduled item))
do (cl-loop
for before-time in my/org-alert-notify-times
for label = (format "%s at %s [%s min. remaining]"
(org-element-property :raw-value item)
(format-time-string "%H:%M" scheduled)
(number-to-string (/ before-time 60)))
for time = (time-convert
(+ (time-convert scheduled 'integer) (- before-time)))
do (progn
(my/org-alert--schedule label time)
(push (cons label time) scheduled-keys))))
(my/org-alert-cleanup scheduled-keys))))
Let’s wrap it into a minor mode:
(setq my/org-alert--timer nil)
(define-minor-mode my/org-alert-mode ()
:global t
:after-hook
(if my/org-alert-mode
(progn
(my/org-alert--update-today-alerts)
(when (timerp my/org-alert--timer)
(cancel-timer my/org-alert--timer))
(setq my/org-alert--timer
(run-at-time 600 t #'my/org-alert--update-today-alerts)))
(when (timerp my/org-alert--timer)
(cancel-timer my/org-alert--timer))
(my/org-alert-cleanup)))
I don’t have any idea why, but evaluating (my/org-alert-mode)
just after org
breaks font-lock after I try to open inbox.org
. emacs-startup-hook
, however, works fine.
(with-eval-after-load 'org
(if my/emacs-started
(my/org-alert-mode)
(add-hook 'emacs-startup-hook #'my/org-alert-mode)))
Seqeuential headers
I like to add numbers to repeating events, like meetings. E.g.
* Job meeting 62
SCHEDULED: <2022-11-13 16:00>
* Job meeting 63
SCHEDULED: <2022-11-14 16:00>
...
Copying records
Naturally, I want a way to copy such records. Org Mode already has a function called org-clone-subtree-with-time-shift
, that does everything I want except for updating the numbers.
Unfortunately, I see no way to advise the original function, so here’s my version that makes use of evil-numbers
:
(defun my/org-clone-subtree-with-time-shift (n &optional shift)
(interactive "nNumber of clones to produce: ")
(unless (wholenump n) (user-error "Invalid number of replications %s" n))
(when (org-before-first-heading-p) (user-error "No subtree to clone"))
(let* ((beg (save-excursion (org-back-to-heading t) (point)))
(end-of-tree (save-excursion (org-end-of-subtree t t) (point)))
(shift
(or shift
(if (and (not (equal current-prefix-arg '(4)))
(save-excursion
(goto-char beg)
(re-search-forward org-ts-regexp-both end-of-tree t)))
(read-from-minibuffer
"Date shift per clone (e.g. +1w, empty to copy unchanged): ")
""))) ;No time shift
(doshift
(and (org-string-nw-p shift)
(or (string-match "\\`[ \t]*\\([+-]?[0-9]+\\)\\([hdwmy]\\)[ \t]*\\'"
shift)
(user-error "Invalid shift specification %s" shift)))))
(goto-char end-of-tree)
(unless (bolp) (insert "\n"))
(let* ((end (point))
(template (buffer-substring beg end))
(shift-n (and doshift (string-to-number (match-string 1 shift))))
(shift-what (pcase (and doshift (match-string 2 shift))
(`nil nil)
("h" 'hour)
("d" 'day)
("w" (setq shift-n (* 7 shift-n)) 'day)
("m" 'month)
("y" 'year)
(_ (error "Unsupported time unit"))))
(nmin 1)
(nmax n)
(n-no-remove -1)
(org-id-overriding-file-name (buffer-file-name (buffer-base-buffer)))
(idprop (org-entry-get beg "ID")))
(when (and doshift
(string-match-p "<[^<>\n]+ [.+]?\\+[0-9]+[hdwmy][^<>\n]*>"
template))
(delete-region beg end)
(setq end beg)
(setq nmin 0)
(setq nmax (1+ nmax))
(setq n-no-remove nmax))
(goto-char end)
(cl-loop for n from nmin to nmax do
(insert
;; Prepare clone.
(with-temp-buffer
(insert template)
(org-mode)
(goto-char (point-min))
(org-show-subtree)
(and idprop (if org-clone-delete-id
(org-entry-delete nil "ID")
(org-id-get-create t)))
(unless (= n 0)
(while (re-search-forward org-clock-line-re nil t)
(delete-region (line-beginning-position)
(line-beginning-position 2)))
(goto-char (point-min))
(while (re-search-forward org-drawer-regexp nil t)
(org-remove-empty-drawer-at (point))))
(goto-char (point-min))
(when doshift
(while (re-search-forward org-ts-regexp-both nil t)
(org-timestamp-change (* n shift-n) shift-what))
(save-excursion
(goto-char (point-min))
(evil-numbers/inc-at-pt n (point-min)))
(unless (= n n-no-remove)
(goto-char (point-min))
(while (re-search-forward org-ts-regexp nil t)
(save-excursion
(goto-char (match-beginning 0))
(when (looking-at "<[^<>\n]+\\( +[.+]?\\+[0-9]+[hdwmy]\\)")
(delete-region (match-beginning 1) (match-end 1)))))))
(buffer-string)))))
(goto-char beg)))
My addition to that is the form with evil-numbers/inc-at-pt
.
Keeping consistency among sequential records
I also like to keep such headers consistent. Here are a few tools to help with that.
First, I need to find and group and such headers. org-ql
can help with that:
(defun my/org--headings-in-outline ()
(org-ql-query
:select (lambda () (propertize
(substring-no-properties (org-get-heading t t t))
'marker (copy-marker (point))))
:from (append
(list (buffer-file-name))
(let ((archive
(concat (file-name-directory (buffer-file-name))
"archive/"
(file-name-nondirectory (buffer-file-name)))))
(when (file-exists-p archive)
(list archive))))
:where `(and (outline-path ,@(org-get-outline-path))
(level ,(org-current-level)))))
(defun my/org--heading-strip (heading)
(thread-last
heading
(substring-no-properties)
(replace-regexp-in-string (rx (| "(" "[") (+ alnum) (| "]" ")")) "")
(replace-regexp-in-string (rx " " (+ (or digit "."))) " ")
(replace-regexp-in-string (rx (+ " ")) " ")
(string-trim)))
(defun my/org--headings-group-seq (headings)
(thread-last
headings
(seq-group-by #'my/org--heading-strip)
(seq-sort-by #'car #'string-lessp)
(mapcar (lambda (group)
(cons (car group)
(seq-sort-by
(lambda (heading)
(save-match-data
(or
(and (string-match (rx (group (+ digit)))
heading)
(string-to-number (match-string 1 heading)))
-1)))
#'<
(cdr group)))))))
Then, display all such headings a buffer:
(defun my/org-headings-seq ()
(interactive)
(let* ((headings (my/org--headings-in-outline))
(headings-seq (my/org--headings-group-seq headings))
(buffer (generate-new-buffer "*Sequential Headings in Outline*")))
(with-current-buffer buffer
(outline-mode)
(setq-local widget-push-button-prefix "")
(setq-local widget-push-button-suffix "")
(dolist (group headings-seq)
(insert (format "* %s\n" (car group)))
(dolist (heading (cdr group))
(widget-create 'push-button
:marker (get-text-property 0 'marker heading)
:notify (lambda (widget &rest ignore)
(let ((marker (widget-get widget :marker)))
(pop-to-buffer (marker-buffer marker))
(goto-char marker)))
(concat "** " (substring-no-properties heading)))
(insert "\n")))
(widget-setup)
(setq buffer-read-only t)
(goto-char (point-min)))
(pop-to-buffer buffer)))
And insert a similar heading:
(defun my/org-heading-seq-insert ()
(interactive)
(let* ((headings (my/org--headings-in-outline))
(headings-seq (my/org--headings-group-seq headings))
(heading (completing-read "Headings: " headings-seq))
(last-number
(thread-last headings-seq
(assoc heading)
(cdr)
(mapcar (lambda (x)
(save-match-data
(or
(when (string-match (rx (group (+ digit)))
x)
(string-to-number (match-string 1 x)))
1))))
(seq-max)
(1+))))
(org-insert-heading '(4))
(insert (format "FUTURE %s %s" heading last-number))))
Archiving records
- CREDIT: thanks Amy for pointing me to the right functionality of
org-refile
.
I have several org files for long-running projects. They are getting hard to manage because there are lots of different tasks, events, etc.
So I want to create “archive versions” of these files which would have the same structure but store items, say, with a timestamp older than 2 months.
Archive versions are to be stored in the archive
subdirectory relative to the current file, e.g., foo.org
-> archive/foo.org
:
(defun my/org-archive--get-file ()
"Get an archive version of the file."
(let ((archive-file
(concat
(file-name-directory (buffer-file-name))
"archive/" (file-name-nondirectory (buffer-file-name)))))
(unless (file-exists-p archive-file)
(make-empty-file archive-file))
archive-file))
In order to maintain structure, we need to make sure that the archive version has all the necessary headers.
org-refile
(or, to be precise, org-refile-get-location
) by itself can create the last level of headers with org-refile-allow-creating-parent-nodes
. So I can just invoke the same logic for all missing headers:
(defun my/org-refile--assert-path-exists (refile-path)
(cl-assert (equal org-refile-use-outline-path 'file))
(let* ((parts (string-split refile-path "/"))
(tbl (mapcar
(lambda (x)
(cons (concat (car x) "/") (cdr x)))
org-refile-target-table)))
(cl-loop for i from 1
for part in (cdr parts)
for target = (org-refile--get-location
(string-join (seq-take parts (1+ i)) "/")
tbl)
unless target
do (let ((parent-target
(org-refile--get-location
(string-join (seq-take parts i) "/")
tbl)))
(push (org-refile-new-child parent-target part) tbl)))))
Now we can make a function to archive one record interactively.
(defun my/org-archive-refile ()
(interactive)
(let* ((org-refile-targets `((,(my/org-archive--get-file) . (:maxlevel . 6))))
(org-refile-target-table (org-refile-get-targets))
(org-refile-history nil)
(org-refile-use-outline-path 'file)
(org-refile-allow-creating-parent-nodes t)
(org-outline-path-complete-in-steps nil)
(refile-path (string-join
(append
(list (file-name-nondirectory
(buffer-file-name)))
(org-get-outline-path nil t))
"/")))
;; The path is already known
(cl-letf (((symbol-function 'completing-read)
(lambda (&rest _) refile-path)))
(my/org-refile--assert-path-exists refile-path)
(org-refile))))
And a function to archive all records older than the given number of days. I’ll use org-ql
to find these records.
(defun my/org-archive-refile-all (days)
(interactive (list (read-number "Days: " 60)))
(let ((records (org-ql-query
:select #'element-with-markers
:from (current-buffer)
:where `(and (ts :to ,(- days)) (done)))))
(when (y-or-n-p (format "Archive %d records? " (length records)))
(dolist (record records)
(let ((marker (org-element-property :org-marker record)))
(org-with-point-at marker
(my/org-archive-refile)))))))
Keybindings
Global keybindings:
(my-leader-def
:infix "o"
"" '(:which-key "org-mode")
"c" 'org-capture
"a" 'org-agenda
"o" #'my/org-file-open
"v" #'org-ql-view
"q" #'org-ql-search)
Local keybindings
(with-eval-after-load 'org
(my-leader-def
:infix "SPC"
:keymaps '(org-mode-map)
"i" #'org-clock-in
"o" #'org-clock-out
"O" #'org-clock-cancel
"c" #'org-clock-goto
"p" #'org-set-property
"e" #'org-set-effort
"r" #'org-priority
"m" #'my/org-meeting-link))
Org Journal
org-journal is a package for maintaining a journal in org mode.
This part turned out to be great. I even consulted the journal a few times to check if something actually happened, which makes me uneasy now that I think about it…
One issue I found is that it’s kinda hard to find anything in the journal, and I’m not eager to open the journal for a random date anyway. So I’ve made a package called org-journal-tags.
My initial desire was to be able to query the journal for my thoughts on a particular subject or theme, for progress on some project, or for records related to some person… Which is kinda useful, although not quite as much as I expected it to be. Relatively fast querying of the journal is also nice.
The section I named “on this day” turned out to be particularly interesting, as it kinda allowed me to connect with past versions of myself.
And it was interesting to find the reinforcement effect of checked dates on the calendar.
(use-package org-journal
:straight t
:if (not my/remote-server)
:init
(my-leader-def
:infix "oj"
"" '(:which-key "org-journal")
"j" 'org-journal-new-entry
"o" 'org-journal-open-current-journal-file
"s" 'org-journal-tags-status)
:after org
:config
(setq org-journal-dir (concat org-directory "/journal"))
(setq org-journal-file-type 'weekly)
(setq org-journal-file-format "%Y-%m-%d.org")
(setq org-journal-date-format "%A, %Y-%m-%d")
(setq org-journal-enable-encryption t)
(setq org-journal-time-format-post-midnight "PM: %R "))
So, org-journal-tags is my package that implements a tagging system for org-journal.
(use-package org-journal-tags
:straight (:host github :repo "SqrtMinusOne/org-journal-tags")
:after (org-journal)
:if (not my/remote-server)
:config
(org-journal-tags-autosync-mode)
(general-define-key
:keymaps 'org-journal-mode-map
"C-c t" #'org-journal-tags-insert-tag))
Also, I want to add some extra information to the journal. Here’s a functionality to get the current weather from wttr.in:
(use-package request
:straight t
:defer t)
(defvar my/weather-last-time 0)
(defvar my/weather-value nil)
(defun my/weather-get ()
(when (> (- (time-convert nil 'integer) my/weather-last-time)
(* 60 5))
(request (format "https://wttr.in/%s" my/location)
:params '(("format" . "%l:%20%C%20%t%20%w%20%p"))
:sync t
:parser (lambda () (url-unhex-string (buffer-string)))
:timeout 10
:success (cl-function
(lambda (&key data &allow-other-keys)
(setq my/weather-value data)
(setq my/weather-last-time (time-convert nil 'integer))))
:error
(cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
(message "Got error: %S" error-thrown)))))
my/weather-value)
Let’s also try to log the current mood:
(defun my/get-mood ()
(let* ((crm-separator " ")
(crm-local-completion-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map crm-local-completion-map)
(define-key map " " 'self-insert-command)
map))
(vertico-sort-function nil))
(mapconcat
#'identity
(completing-read-multiple
"How do you feel: "
my/mood-list)
" ")))
And here’s the function that creates a drawer with such information. At the moment, it’s:
- Emacs version
- Hostname
- Location
- Weather
- Current EMMS track
- Current mood
(defun my/set-journal-header ()
(org-set-property "Emacs" emacs-version)
(org-set-property "Hostname" (my/system-name))
(org-journal-tags-prop-apply-delta :add (list (format "host.%s" (my/system-name))))
(when (boundp 'my/location)
(org-set-property "Location" my/location)
(when-let ((weather (my/weather-get)))
(org-set-property "Weather" weather)))
(when (boundp 'my/loc-tag)
(org-journal-tags-prop-apply-delta :add (list my/loc-tag)))
(when (fboundp 'emms-playlist-current-selected-track)
(let ((track (emms-playlist-current-selected-track)))
(when track
(let ((album (cdr (assoc 'info-album track)))
(artist (or (cdr (assoc 'info-albumartist track))
(cdr (assoc 'info-album track))))
(title (cdr (assoc 'info-title track)))
(string ""))
(when artist
(setq string (concat string "[" artist "] ")))
(when album
(setq string (concat string album " - ")))
(when title
(setq string (concat string title)))
(when (> (length string) 0)
(org-set-property "EMMS_Track" string))))))
(when-let (mood (my/get-mood))
(org-set-property "Mood" mood)))
(add-hook 'org-journal-after-entry-create-hook
#'my/set-journal-header)
Bibliography
I use Zotero to manage my bibliograhy.
There is a Zotero extension called better bibtex, which allows for having one bibtex file that is always syncronized with the library. That comes quite handy for Emacs integration.
citar
citar is a package that works with citations.
(use-package citar
:straight t
:init
(my-leader-def "fB" #'citar-open)
:commands (citar-open citar-insert-citation)
:config
(setq
org-cite-global-bibliography '("~/30-39 Life/32 org-mode/library.bib")
org-cite-insert-processor 'citar
org-cite-follow-processor 'citar
org-cite-activate-processor 'citar
citar-bibliography org-cite-global-bibliography)
(add-hook 'latex-mode #'citar-capf-setup)
(add-hook 'org-mode #'citar-capf-setup))
(use-package citar-embark
:after (citar embark)
:straight t
:config
(citar-embark-mode))
org-ref
org-ref is a package by John Kitchin that provides support for citations and cross-references in Org Mode.
I’ve switched to citar for citations because org-ref
only works with Ivy and Helm. Fortunately, org-ref
is designed to co-exist with citar
and org-cite
.
Also, at some point the package loaded Helm on start, so I exclude these files from the recipe.
(use-package org-ref
:straight (:files (:defaults "citeproc" (:exclude "*helm*")))
:if (not my/remote-server)
:init
(setq bibtex-dialect 'biblatex)
(add-hook 'bibtex-mode 'smartparens-mode)
:after (org)
:config
(general-define-key
:keymaps 'org-mode-map
"C-c l" #'org-ref-insert-link-hydra/body)
(general-define-key
:keymaps 'bibtex-mode-map
"M-RET" 'org-ref-bibtex-hydra/body)
(setq org-ref-insert-cite-function
(lambda ()
(call-interactively #'citar-insert-citation))))
Org Roam
org-roam is a plain-text knowledge database.
Things I tried with Org Roam:
- Managing projects. Ended up preferring plain Org.
- Writing a journal with
org-roam-dailies
. Didn’t work out as I expected, so I’ve madeorg-journal-tags
after I understood better what I want.
Regardless, it turned out to be great for managing Zettelkasten, which is the original purpose of the package anyway. I didn’t expect to ever get into something like this, but I guess I was wrong.
Some resources that helped me along the way (and still help):
- Sönke Ahrens’ book “How to take smart notes”
- https://zettelkasten.de/ - a lot of useful stuff here, especially in the “Getting Started” section.
- System Crafters Live! - Can You Apply Zettelkasten in Emacs?
Basic package configuration
Guix dependency | Disabled |
---|---|
emacs-emacsql-sqlite3 | t |
graphviz |
About installing the package on Guix (CREDIT: thanks @Ashraz on the SystemCrafters discord)
So, for all those interested: unfortunately, org-roam (or rather emacsql-sqlite) cannot compile the sqlite.c and emacsql.c due to missing headers (linux/falloc.h) on Guix. You would have to properly set all the include paths on Guix, and also adjust the PATH to have gcc actually find as later on in the compilation process.
Instead, you should remove all Org-Roam related packages from your Emacs installation (via M-x package-delete org-roam RET and M-x package-autoremove RET y RET) and then use the Guix package called emacs-org-roam.
References:
(use-package emacsql-sqlite
:defer t
:if (not my/remote-server)
:straight (:type built-in))
(use-package org-roam
:straight (:host github :repo "org-roam/org-roam"
:files (:defaults "extensions/*.el"))
:if (and
(not my/remote-server)
(file-directory-p org-roam-directory))
:after org
:init
(setq org-roam-file-extensions '("org"))
(setq org-roam-v2-ack t)
(setq org-roam-node-display-template (concat "${title:*} " (propertize "${tags:10}" 'face 'org-tag)))
:config
(org-roam-setup)
(require 'org-roam-protocol))
Capture templates
Capture templates for org-roam-capture
. As for now, nothing too complicated here.
(setq org-roam-capture-templates
`(("d" "default" plain "%?"
:if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n")
:unnarrowed t)
("e" "encrypted" plain "%?"
:if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org.gpg" "#+title: ${title}\n")
:unnarrowed t)))
Keybindings
A set of keybindings to quickly access things in Org Roam.
(with-eval-after-load 'org-roam
(my-leader-def
:infix "or"
"" '(:which-key "org-roam")
"i" #'org-roam-node-insert
"r" #'org-roam-node-find
"g" #'org-roam-graph
"c" #'org-roam-capture
"b" #'org-roam-buffer-toggle)
(general-define-key
:keymaps 'org-roam-mode-map
:states '(normal)
"TAB" #'magit-section-toggle
"q" #'quit-window
"k" #'magit-section-backward
"j" #'magit-section-forward
"gr" #'revert-buffer
"RET" #'org-roam-buffer-visit-thing))
(with-eval-after-load 'org
(my-leader-def
:keymap 'org-mode-map
:infix "or"
"t" #'org-roam-tag-add
"T" #'org-roam-tag-remove
"s" #'org-roam-db-autosync-mode
"a" #'org-roam-alias-add)
(general-define-key
:keymap 'org-mode-map
"C-c i" 'org-roam-node-insert))
Backlinks count display
Occasionally I want to see how many backlinks a particular page has.
This idea came to my mind because I often write a note in the following form:
According to <This Person>, <some opinion>
And I have a note called #Personalities
that looks like that:
Philosophers:
- <This Person>
- <That Person>
- <Another Person>
...
So I’m curious to see how many notes I have linked to each:
Philosophers:
- <This Person> [30]
- <That Person> [40]
- <Another Person> [20]
...
The obvious way to implement that is via overlays:
(defface my/org-roam-count-overlay-face
'((t :inherit tooltip))
"Face for Org Roam count overlay.")
(defun my/org-roam--count-overlay-make (pos count)
(let* ((overlay-value (concat
" "
(propertize
(format "%d" count)
'face 'my/org-roam-count-overlay-face)
" "))
(ov (make-overlay pos pos (current-buffer) nil t)))
(overlay-put ov 'roam-backlinks-count count)
(overlay-put ov 'priority 1)
(overlay-put ov 'after-string overlay-value)))
Also a function to remove them:
(defun my/org-roam--count-overlay-remove-all ()
(dolist (ov (overlays-in (point-min) (point-max)))
(when (overlay-get ov 'roam-backlinks-count)
(delete-overlay ov))))
Now we can iterate over all roam links in the buffer, count the number of backlinks via org-roam-db-query
and invoke my/org-roam--count-overlay-make
if that number is greater than zero:
(defun my/org-roam--count-overlay-make-all ()
(my/org-roam--count-overlay-remove-all)
(org-element-map (org-element-parse-buffer) 'link
(lambda (elem)
(when (string-equal (org-element-property :type elem) "id")
(let* ((id (org-element-property :path elem))
(count (caar
(org-roam-db-query
[:select (funcall count source)
:from links
:where (= dest $s1)
:and (= type "id")]
id))))
(when (< 0 count)
(my/org-roam--count-overlay-make
(org-element-property :end elem)
count)))))))
And a minor mode to toggle the display in a particular org-roam
buffer.
(define-minor-mode my/org-roam-count-overlay-mode
"Display backlink count for org-roam links."
:after-hook
(if my/org-roam-count-overlay-mode
(progn
(my/org-roam--count-overlay-make-all)
(add-hook 'after-save-hook #'my/org-roam--count-overlay-make-all nil t))
(my/org-roam--count-overlay-remove-all)
(remove-hook 'after-save-hook #'my/org-roam--count-overlay-remove-all t)))
Extract all links from page
(defun my/org-roam-extract-links ()
(interactive)
(let ((buffer (generate-new-buffer "*roam-links*"))
elems)
(org-element-map (org-element-parse-buffer) 'link
(lambda (elem)
(when (string-equal (org-element-property :type elem) "id")
(push elem elems))))
(with-current-buffer buffer
(cl-loop for elem in elems
for file-name =
(file-name-nondirectory
(caar
(org-roam-db-query
[:select [file]
:from nodes
:where (= id $s1)]
(org-element-property :path elem))))
do (insert file-name "\n")))
(switch-to-buffer buffer)))
Org Roam UI
A browser frontend to visualize the Roam database as a graph.
Actually, I don’t find this quite as useful as structure nodes, because over time my graph grew somewhat convoluted. But it looks impressive.
(use-package org-roam-ui
:straight (:host github :repo "org-roam/org-roam-ui" :branch "main" :files ("*.el" "out"))
:if (not my/remote-server)
:after org-roam
;; :hook (org-roam . org-roam-ui-mode)
:init
(my-leader-def "oru" #'org-roam-ui-mode))
Deft
Deft is an Emacs package to quickly find notes. I use it as a full-text search engine for org-roam
.
(use-package deft
:straight t
:if (not my/remote-server)
:commands (deft)
:after (org)
:init
(my-leader-def "ord" #'deft)
:config
(setq deft-directory org-roam-directory)
(setq deft-recursive t)
(setq deft-use-filter-string-for-filename t)
(add-hook 'deft-mode-hook
(lambda () (display-line-numbers-mode -1)))
(general-define-key
:keymaps 'deft-mode-map
:states '(normal motion)
"q" #'quit-window
"r" #'deft-refresh
"s" #'deft-filter
"d" #'deft-filter-clear
"y" #'deft-filter-yank
"t" #'deft-toggle-incremental-search
"o" #'deft-toggle-sort-method))
The default deft view does not look that great because of various Roam metadata. To improve that, we can tweak deft-strip-summary-regexp
:
(setq deft-strip-summary-regexp
(rx (or
(: ":PROPERTIES:" (* anything) ":END:")
(: "#+" (+ alnum) ":" (* nonl))
(regexp "[\n\t]"))))
And advise deft-parse-summary
to filter out Org links:
(defun my/deft-parse-summary-around (fun contents title)
(funcall fun (org-link-display-format contents) title))
(with-eval-after-load 'deft
(advice-add #'deft-parse-summary :around #'my/deft-parse-summary-around))
Advise deft-parse-title
to be able to extract title from the Org property:
(defun my/deft-parse-title (file contents)
(with-temp-buffer
(insert contents)
(goto-char (point-min))
(if (search-forward-regexp (rx (| "#+title:" "#+TITLE:")) nil t)
(string-trim (buffer-substring-no-properties (point) (line-end-position)))
file)))
(defun my/deft-parse-title-around (fun file contents)
(or (my/deft-parse-title file contents)
(funcall fun file contents)))
(with-eval-after-load 'deft
(advice-add #'deft-parse-title :around #'my/deft-parse-title-around))
Notes display
I decided to borrow a few UX things from Obsidian, namely hiding syntax when cursor leaves the line.
org-appear and org-fragtog do pretty much that.
(defun my/org-roam-node-setup ()
(setq-local org-hide-emphasis-markers t)
(org-appear-mode 1)
(when (display-graphic-p)
(org-fragtog-mode 1)
(org-latex-preview '(16))))
(with-eval-after-load 'org
(add-hook 'org-roam-find-file-hook 'my/org-roam-node-setup))
Review workflow
Tiago Forte has several few interesting blog posts:
This is probably my third time to implement a weekly review.
I want two general things from the workflow:
- to perform maintainance operations, such as clearing various inboxes;
- to reflect on what I’ve done over the past week.
For the second point I’ll try to collect data from various sources and add the data to my review template.
Data from git
First, as I have autocommit set up in my org directory, here is a function to get an alist of changed files in a form (status . path)
. The rev
parameter can be a commit, tag, etc. but here I’m interested in the date form (e.g. @{2021-08-30}
).
(setq my/git-diff-status
'(("A" . added)
("C" . copied)
("D" . deleted)
("M" . modified)
("R" . renamed)
("R100" . moved)
("T" . type-changed)
("U" . unmerged)))
(defun my/get-files-status (rev)
(let ((files (shell-command-to-string (concat "git diff --name-status " rev))))
(mapcar
(lambda (file)
(let ((elems (split-string file "\t")))
(cons
(cdr (assoc (car elems) my/git-diff-status))
(car (last elems)))))
(split-string files "\n" t))))
I’ll use it to get a list of added and changed files in the Org directory since the last review. The date should be in a format YYYY-MM-DD
.
(defun my/org-changed-files-since-date (date)
(let ((default-directory org-directory))
(my/get-files-status (format "@{%s}" date))))
Data from org-roam
I’ll use data from git to get the list of what I’ve been working on. The directories include org-roam
itself and inbox-notes
, where my in-process notes live.
(defun my/org-review-format-org-roam (date)
(let ((changes (my/org-changed-files-since-date date))
(nodes (org-roam-node-list))
(nodes-by-file (make-hash-table :test #'equal)))
(cl-loop for node in nodes
for file = (org-roam-node-file node)
do (puthash file node nodes-by-file))
(concat
"*** Zettelkasten Updates\n"
"TODO Sort the updates by topics\n\n"
"Changes in inbox:\n"
(thread-last
changes
(seq-filter
(lambda (file) (string-match-p (rx bos "inbox-notes") (cdr file))))
(seq-sort-by (lambda (s) (symbol-name (car s)))
#'string-lessp)
(mapcar (lambda (change)
(format "- %s :: %s\n"
(cond
((or (member (car change) '(deleted moved))
(string-match-p "figured-out" (cdr change)))
"Processed")
(t (capitalize (symbol-name (car change)))))
(cdr change))))
(apply #'concat))
"\nChanges in notes:\n"
(thread-last
changes
(mapcar (lambda (c)
(cons (car c)
(gethash
(concat org-directory "/" (cdr c))
nodes-by-file))))
(seq-filter #'cdr)
(seq-sort-by (lambda (c) (concat (symbol-name (car c))
(org-roam-node-title (cdr c))))
#'string-lessp)
(mapcar (lambda (c)
(format "- %s :: [[id:%s][%s]]\n"
(capitalize (symbol-name (car c)))
(org-roam-node-id (cdr c))
(org-roam-node-title (cdr c)))))
(apply #'concat)))))
Org Journal integration
(defun my/org-review-get-last-review-date (kind)
(let* ((start-of-day (- (time-convert nil #'integer)
(% (time-convert nil #'integer)
(* 24 60 60))))
(query-res (org-journal-tags-query
:tag-names (list (format "review.%s" kind))
:start-date (pcase kind
('weekly
(- start-of-day
(* 21 24 60 60)))
(_ (error "Unsupported kind: %s" kind)))
:location 'section
:order 'descending)))
(if query-res
(org-journal-tag-reference-date (car query-res))
(pcase kind
('weekly (- start-of-day (* 7 24 60 60)))))))
(defun my/org-review-set-weekly-record ()
(save-excursion
(let ((last-review-date (my/org-review-get-last-review-date 'weekly)))
(org-journal-tags-prop-apply-delta :add '("review.weekly"))
(insert "Weekly Review")
(goto-char (point-max))
(insert "Last review date: "
(format-time-string
"[%Y-%m-%d]"
(seconds-to-time last-review-date)))
(insert "
Review checklist:
- [ ] Clear email inbox
- [ ] Reconcile ledger
- [ ] Clear [[file:~/Downloads][downloads]] and [[file:~/00-Scratch][scratch]] folders
- [ ] Process [[file:~/30-39 Life/35 Photos/35.00 Inbox/][photo inbox]]
- [ ] Process [[file:../inbox.org][inbox]]
- [ ] Create [[file:../recurring.org][recurring tasks]] for next week
- [ ] Check agenda (-1 / +2 weeks): priorities, deadlines
- [ ] Check TODOs: priorities, deadlines
- [[org-ql-search:todo%3A?buffers-files=%22org-agenda-files%22&super-groups=%28%28%3Aauto-outline-path-file%20t%29%29&sort=%28priority%20todo%20deadline%29][org-ql-search: All TODOs]]
- [[org-ql-search:(and (todo) (not (tags \"nots\")) (not (ts :from -14)))?buffers-files=%22org-agenda-files%22&super-groups=%28%28%3Aauto-outline-path-file%20t%29%29&sort=%28priority%20todo%20deadline%29][org-ql-search: Stale tasks]]
- [[org-ql-search:todo%3AWAIT?buffers-files=%22org-agenda-files%22&super-groups=%28%28%3Aauto-outline-path-file%20t%29%29&sort=%28priority%20todo%20deadline%29][org-ql-search: WAIT]]
- [ ] Run auto-archiving
- [ ] Review journal records
")
(insert (my/org-review-format-org-roam
(format-time-string "%Y-%m-%d" (seconds-to-time last-review-date))))
(insert "
*** Summary
TODO Write something, maybe? "))))
(defun my/org-review-weekly ()
(interactive)
(let ((org-journal-after-entry-create-hook
`(,@org-journal-after-entry-create-hook
my/org-review-set-weekly-record)))
(org-journal-new-entry nil)
(org-fold-show-subtree)))
(with-eval-after-load 'org-journal
(my-leader-def "ojw" #'my/org-review-weekly))
Contacts
org-contacts
is a package to store contacts in an org file.
It seems the package has been somewhat revived in the recent months. It used things like lexical-let
when I first found it.
(use-package org-contacts
:straight (:type git :repo "https://repo.or.cz/org-contacts.git")
:if (not my/remote-server)
:after (org)
:config
(setq org-contacts-files (list
(concat org-directory "/contacts.org"))))
An example contact entry can look like this:
* Pavel Korytov
:PROPERTIES:
:TYPE: person
:EMAIL: thexcloud@gmail.com
:EMAIL+: pvkorytov@etu.ru
:BIRTHDAY: [1998-08-14]
:END:
Calendar view
calfw is a nice package that displays calendars in Emacs.
(defun my/calfw-setup-buffer ()
(display-line-numbers-mode -1))
(use-package calfw
:straight t
:defer t
:config
(add-hook 'cfw:calendar-mode-hook #'my/calfw-setup-buffer))
(use-package calfw-org
:after (calfw org)
:straight t)
org-timeblock
(defun my/org-timeblock-conf ()
(display-line-numbers-mode -1))
(use-package org-timeblock
:straight (:host github :repo "ichernyshovvv/org-timeblock")
:commands (org-timeblock-mode)
:init
(my-leader-def "ot" #'org-timeblock)
:config
(add-hook 'org-timeblock-mode-hook #'my/org-timeblock-conf)
(general-define-key
:keymaps '(org-timeblock-mode-map)
:states '(normal visual)
"j" #'org-timeblock-forward-block
"h" #'org-timeblock-backward-column
"l" #'org-timeblock-forward-column
"k" #'org-timeblock-backward-block
"M-[" #'org-timeblock-day-earlier
"M-]" #'org-timeblock-day-later
"H" #'org-timeblock-day-earlier
"L" #'org-timeblock-day-later
"RET" #'org-timeblock-goto
"t" #'org-timeblock-todo-set
"q" #'quit-window))
org-drill
Trying to learn stuff with this.
(use-package org-drill
:straight t
:commands (org-drill)
:after (org))
Export
Hugo
A package for exporting Org to Hugo. That’s how I manage my sqrtminusone.xyz.
References:
(use-package ox-hugo
:straight t
:if (not my/remote-server)
:after ox)
Jupyter Notebook
(use-package ox-ipynb
:straight (:host github :repo "jkitchin/ox-ipynb")
:if (not my/remote-server)
:disabled t
:after ox)
Html export
(use-package htmlize
:straight t
:after ox
:if (not my/remote-server)
:config
(setq org-html-htmlize-output-type 'css))
org-ref
(with-eval-after-load 'org-ref
(setq org-ref-csl-default-locale "ru-RU")
(setq org-ref-csl-default-style (expand-file-name
(concat user-emacs-directory
"gost-r-7-0-5-2008-numeric.csl"))))
LaTeX
Add a custom LaTeX template without default packages. Packages are indented to be imported with function from Import *.sty.
(defun my/setup-org-latex ()
(setq org-latex-prefer-user-labels t)
(setq org-latex-compiler "xelatex") ;; Probably not necessary
(setq org-latex-pdf-process '("latexmk -outdir=%o %f")) ;; Use latexmk
(setq org-latex-listings 'minted) ;; Use minted to highlight source code
(setq org-latex-minted-options ;; Some minted options I like
'(("breaklines" "true")
("tabsize" "4")
("autogobble")
("linenos")
("numbersep" "0.5cm")
("xleftmargin" "1cm")
("frame" "single")))
;; Use extarticle without the default packages
(add-to-list 'org-latex-classes
'("org-plain-extarticle"
"\\documentclass{extarticle}
[NO-DEFAULT-PACKAGES]
[PACKAGES]
[EXTRA]"
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")
("\\subparagraph{%s}" . "\\subparagraph*{%s}")))
(add-to-list 'org-latex-classes
'("org-plain-extreport"
"\\documentclass{extreport}
[NO-DEFAULT-PACKAGES]
[PACKAGES]
[EXTRA]"
("\\chapter{%s}" . "\\chapter*{%s}")
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")))
;; Use beamer without the default packages
(add-to-list 'org-latex-classes
'("org-latex-beamer"
"\\documentclass{beamer}
[NO-DEFAULT-PACKAGES]
[PACKAGES]
[EXTRA]"
("beamer" "\\documentclass[presentation]{beamer}"
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")))))
;; Make sure to eval the function when org-latex-classes list already exists
(with-eval-after-load 'ox-latex
(my/setup-org-latex))
Fix Russian dictionary
No idea why, but somehow the exported file uses english words if there isn’t :default
key in the dictionary.
(with-eval-after-load 'ox
(setq org-export-dictionary
(cl-loop for item in org-export-dictionary collect
(cons
(car item)
(cl-loop for entry in (cdr item)
if (and (equal (car entry) "ru")
(plist-get (cdr entry) :utf-8))
collect (list "ru" :default (plist-get (cdr entry) :utf-8))
else collect entry)))))
System configuration
Functions related to literate configuration.
Tables for Guix Dependencies
This section deals with using using profiles in GNU Guix.
A “profile” in Guix is a way to group package installations. For instance, I have a “music” profile that has software like MPD, ncmpcpp that I’m still occasionally using because of its tag editor, etc. Corresponding to that profile, there’s a manifest named music.scm
that looks like this:
(specifications->manifest
'(
"flac"
"cuetools"
"shntool"
"mpd-mpc"
"mpd-watcher"
"picard"
"ncmpcpp"
"mpd"))
I could generate this file with org-babel
as any other, but that is often not so convenient. For example, I have a polybar module that uses sunwait to show sunset and sunrise times, and ideally, I want to declare sunwait
to be in the “desktop-polybar” profile in the same section that has the polybar module definition and the bash script.
So here’s an approach I came up with. The relevant section of the config looks like this:
*** sun
| Category | Guix dependency |
|-----------------+-----------------|
| desktop-polybar | sunwait |
Prints out the time of sunrise/sunset. Uses [[https://github.com/risacher/sunwait][sunwait]]
#+begin_src bash :tangle ./bin/polybar/sun.sh :noweb no-export
...script...
#+end_src
#+begin_src ini :noweb no-export
...polybar module definition...
#+end_src
So sunwait
is declared in an Org table with Guix dependency
in the header. Such tables are spread through my configuration files.
Thus I made a function that extracts packages from all such tables from the current Org buffer. The rules are as follows:
- If a column name matches
[G|g]uix.*dep
, its contents are added to the result. - If
CATEGORY
is passed, a column with name[C|c]ategory
is used to filter results. That way, one Org file can be used to produce multiple manifests. - If
CATEGORY
is not passed, entries with the non-empty category are filtered out - If there is a
[D|d]isabled
column, entries that have a non-empty value in this column are filtered out.
And here is the implementation:
(defun my/extract-guix-dependencies (&optional category)
(let ((dependencies '()))
(org-table-map-tables
(lambda ()
(let* ((table
(seq-filter
(lambda (q) (not (eq q 'hline)))
(org-table-to-lisp)))
(dep-name-index
(cl-position
nil
(mapcar #'substring-no-properties (nth 0 table))
:test (lambda (_ elem)
(string-match-p "[G|g]uix.*dep" elem))))
(category-name-index
(cl-position
nil
(mapcar #'substring-no-properties (nth 0 table))
:test (lambda (_ elem)
(string-match-p ".*[C|c]ategory.*" elem))))
(disabled-name-index
(cl-position
nil
(mapcar #'substring-no-properties (nth 0 table))
:test (lambda (_ elem)
(string-match-p ".*[D|d]isabled.*" elem)))))
(when dep-name-index
(dolist (elem (cdr table))
(when
(and
;; Category
(or
;; Category not set and not present in the table
(and
(or (not category) (string-empty-p category))
(not category-name-index))
;; Category is set and present in the table
(and
category-name-index
(not (string-empty-p category))
(string-match-p category (nth category-name-index elem))))
;; Not disabled
(or
(not disabled-name-index)
(string-empty-p (nth disabled-name-index elem))))
(add-to-list
'dependencies
(substring-no-properties (nth dep-name-index elem)))))))))
dependencies))
To make it work in the configuration, it is necessary to format the list so that Scheme could read it:
(defun my/format-guix-dependencies (&optional category)
(mapconcat
(lambda (e) (concat "\"" e "\""))
(my/extract-guix-dependencies category)
"\n"))
And we need an Org snippet such as this:
#+NAME: packages
#+begin_src emacs-lisp :tangle no :var category=""
(my/format-guix-dependencies category)
#+end_src
Now, creating a manifest, for example, for the desktop-polybar
profile is as simple as:
#+begin_src scheme :tangle ~/.config/guix/manifests/desktop-polybar.scm :noweb no-export
(specifications->manifest
'(
<<packages("desktop-polybar")>>))
#+end_src
There’s a newline symbol between “(” and <<packages("desktop-polybar")>>
because whenever a noweb expression expands into multiple lines, for each new line noweb duplicates contents between the start of the line and the start of the expression.
One reason this is so is to support languages where indentation is a part of the syntax, for instance, Python:
class TestClass:
<<class-contents>>
So every line of <<class-contents>>
will be indented appropriately. In our case though, it is a minor inconvenience to be aware of.
Noweb evaluations
One note is that by default running these commands will require the user to confirm evaluation of each code block. To avoid that, I set org-confirm-babel-evaluate
to nil
:
(setq my/org-config-files
(mapcar
#'expand-file-name
'("~/Emacs.org"
"~/Desktop.org"
"~/Console.org"
"~/Guix.org"
"~/Mail.org")))
(add-hook 'org-mode-hook
(lambda ()
(when (member (buffer-file-name) my/org-config-files)
(setq-local org-confirm-babel-evaluate nil))))
yadm hook
A script to run tangle from CLI.
(require 'org)
(org-babel-do-load-languages
'org-babel-load-languages
'((emacs-lisp . t)
(shell . t)))
;; Do not ask to confirm evaluations
(setq org-confirm-babel-evaluate nil)
<<guix-tables>>
;; A few dummy modes to avoid being prompted for comment systax
(define-derived-mode fish-mode prog-mode "Fish"
(setq-local comment-start "# ")
(setq-local comment-start-skip "#+[\t ]*"))
(define-derived-mode yaml-mode text-mode "YAML"
(setq-local comment-start "# ")
(setq-local comment-start-skip "#+ *"))
(mapcar #'org-babel-tangle-file
'("/home/pavel/Emacs.org"
"/home/pavel/Desktop.org"
"/home/pavel/Console.org"
"/home/pavel/Guix.org"
"/home/pavel/Mail.org"))
To launch from CLI, run:
emacs -Q --batch -l run-tangle.el
I have added this line to yadm’s post_alt
hook, so to run tangle after yadm alt
Regenerate desktop config
Somewhat similar to the previous one… Occasinally I want to re-tangle all desktop configuration files, for instance to apply a new theme.
(defun my/regenerate-desktop ()
(interactive)
(org-babel-tangle-file "/home/pavel/Desktop.org")
(org-babel-tangle-file "/home/pavel/Console.org")
(call-process "xrdb" nil nil nil "-load" "/home/pavel/.Xresources")
(call-process "~/bin/polybar.sh")
(call-process "pkill" nil nil nil "dunst")
(call-process "herd" nil nil nil "restart" "xsettingsd")
(when (fboundp #'my/exwm-set-alpha)
(if (my/light-p)
(my/exwm-set-alpha 100)
(my/exwm-set-alpha 90))))
Applications
Dired
Dired is the built-in Emacs file manager. It’s so good that it’s strange that, to my knowledge, no one tried to replicate it outside of Emacs.
I currently use it as my primary file manager.
Basic config & keybindings
My config mostly follows ranger’s and vifm’s keybindings which I’m used to.
(use-package dired
:ensure nil
:custom ((dired-listing-switches "-alh --group-directories-first"))
:commands (dired)
:config
(setq dired-dwim-target t)
(setq wdired-allow-to-change-permissions t)
(setq wdired-create-parent-directories t)
(setq dired-recursive-copies 'always)
(setq dired-recursive-deletes 'always)
(setq dired-kill-when-opening-new-dired-buffer t)
(add-hook 'dired-mode-hook
(lambda ()
(setq truncate-lines t)
(visual-line-mode nil)))
(when my/is-termux
(add-hook 'dired-mode-hook #'dired-hide-details-mode))
(general-define-key
:states '(normal)
:keymaps 'dired-mode-map
"h" #'dired-up-directory
"l" #'dired-find-file
"=" #'dired-narrow
"-" #'my/dired-create-empty-file-subtree
"~" #'eshell
"M-r" #'wdired-change-to-wdired-mode
"<left>" #'dired-up-directory
"<right>" #'dired-find-file
"M-<return>" #'dired-open-xdg))
(defun my/dired-home ()
"Open dired at $HOME"
(interactive)
(dired (expand-file-name "~")))
(my-leader-def
"ad" #'dired
"aD" #'my/dired-bookmark-open)
Addons
I used to use dired+, which provides a lot of extensions for dired functionality, but it also creates some new problems, so I opt out of it. Fortunately, the one feature I want from this package - adding more colors to dired buffers - is available as a separate package.
(use-package diredfl
:straight t
:after (dired)
:config
(diredfl-global-mode 1))
dired-subtree is a package that enables managing Dired buffers in a tree-like manner. By default evil-collection
maps dired-subtree-toggle
to TAB
.
(use-package dired-subtree
:after (dired)
:straight t)
(defun my/dired-create-empty-file-subtree ()
(interactive)
(let ((default-directory (dired-current-directory)))
(dired-create-empty-file
(read-file-name "Create empty file: "))))
dired-sidebar enables opening Dired in sidebar. For me, with dired-subtree this makes dired a better option than Treemacs.
(defun my/dired-sidebar-toggle ()
(interactive)
(if (not current-prefix-arg)
(dired-sidebar-toggle-sidebar)
(let ((dired-sidebar-follow-file-at-point-on-toggle-open
current-prefix-arg)
(current-prefix-arg nil))
(dired-sidebar-toggle-sidebar))))
(use-package dired-sidebar
:straight t
:after (dired)
:commands (dired-sidebar-toggle-sidebar)
:init
(setq dired-sidebar-follow-file-at-point-on-toggle-open nil)
(general-define-key
:keymaps '(normal override global)
"C-n" `(my/dired-sidebar-toggle
:wk "dired-sidebar"))
:config
(setq dired-sidebar-width 45)
(defun my/dired-sidebar-setup ()
(toggle-truncate-lines 1)
(display-line-numbers-mode -1)
(setq-local dired-subtree-use-backgrounds nil)
(setq-local window-size-fixed nil))
(general-define-key
:keymaps 'dired-sidebar-mode-map
:states '(normal emacs)
"l" #'dired-sidebar-find-file
"h" #'dired-sidebar-up-directory
"=" #'dired-narrow)
(add-hook 'dired-sidebar-mode-hook #'my/dired-sidebar-setup)
(advice-add #'dired-create-empty-file :after 'dired-sidebar-refresh-buffer))
dired-recent.el adds history to dired.
(use-package dired-recent
:straight t
:after dired
:config
(dired-recent-mode)
(general-define-key
:keymaps 'dired-recent-mode-map
"C-x C-d" nil))
Display icons for files.
Note | Type |
---|---|
ACHTUNG | This plugin is slow as hell with TRAMP or in gnu/store |
(use-package all-the-icons-dired
:straight t
:disabled t
:after (dired)
:if (display-graphic-p)
:hook (dired-mode . (lambda ()
(unless (string-match-p "/gnu/store" default-directory)
(all-the-icons-dired-mode)))))
(use-package nerd-icons-dired
:straight t
:after (dired)
:hook (dired-mode . (lambda ()
(unless (or (file-remote-p default-directory)
(string-match-p "/gnu/store" default-directory))
(nerd-icons-dired-mode)))))
Provides stuff like dired-open-xdg
(use-package dired-open
:straight t
:after (dired)
:commands (dired-open-xdg))
dired-du is a package that shows directory sizes
(use-package dired-du
:straight t
:commands (dired-du-mode)
:config
(setq dired-du-size-format t))
vifm-like filter
(use-package dired-narrow
:straight t
:commands (dired-narrow)
:config
(general-define-key
:keymaps 'dired-narrow-map
[escape] 'keyboard-quit))
Display git info, such as the last commit for file and stuff. It’s pretty useful but also slows down Dired a bit, hence I don’t turn it out by default.
(use-package dired-git-info
:straight t
:after dired
:config
(general-define-key
:keymap 'dired-mode-map
:states '(normal emacs)
")" 'dired-git-info-mode))
avy-dired is my experimentation with Avy & Dired. It’s somewhat unstable.
(use-package avy-dired
:straight (:host github :repo "SqrtMinusOne/avy-dired")
:after (dired)
:init
(my-leader-def "aa" #'avy-dired-goto-line))
dired-rsync allows using rsync
instead of the default synchronous copy operation.
(use-package dired-rsync
:straight t
:after (dired)
:config
(add-to-list 'global-mode-string '(:eval dired-rsync-modeline-status))
(general-define-key
:states '(normal)
:keymaps '(dired-mode-map)
"C" #'dired-rsync
"gC" #'dired-rsync-transient
"gd" #'dired-do-copy))
(use-package dired-rsync-transient
:straight t
:after (dired))
Subdirectories
Subdirectories are one of the interesting features of Dired. It allows displaying multiple folders on the same window.
I add my own keybindings and some extra functionality.
(defun my/dired-open-this-subdir ()
(interactive)
(dired (dired-current-directory)))
(defun my/dired-kill-all-subdirs ()
(interactive)
(let ((dir dired-directory))
(kill-buffer (current-buffer))
(dired dir)))
(with-eval-after-load 'dired
(general-define-key
:states '(normal)
:keymaps 'dired-mode-map
"s" nil
"ss" 'dired-maybe-insert-subdir
"sl" 'dired-maybe-insert-subdir
"sq" 'dired-kill-subdir
"sk" 'dired-prev-subdir
"sj" 'dired-next-subdir
"sS" 'my/dired-open-this-subdir
"sQ" 'my/dired-kill-all-subdirs
(kbd "TAB") 'dired-hide-subdir))
Other functions
Goto project root.
(defun my/dired-goto-project-root ()
(interactive)
(dired--find-possibly-alternative-file (projectile-project-root)))
(with-eval-after-load 'dired
(general-define-key
:states '(normal)
:keymaps 'dired-mode-map
"H" #'my/dired-goto-project-root))
Bookmarks
A simple bookmark list for Dired, mainly to use with TRAMP. I may look into a proper bookmarking system later.
Bookmarks are listed in the private.el file, which has an expression like this:
(setq my/dired-bookmarks
'(("HOME" . "~")
("sudo" . "/sudo::/")))
The file itself is encrypted with yadm.
(defun my/dired-bookmark-open ()
(interactive)
(let ((bookmarks
(mapcar
(lambda (el) (cons (format "%-30s %s" (car el) (cdr el)) (cdr el)))
my/dired-bookmarks)))
(dired
(cdr
(assoc
(completing-read "Dired: " bookmarks nil nil "^")
bookmarks)))))
Integrations
A few functions to send files from Dired to various places.
First, a function to get the target buffer.
(defun my/get-good-buffer (buffer-major-mode prompt)
(or
(cl-loop
for buf being the buffers
if (eq (buffer-local-value 'major-mode buf) buffer-major-mode)
collect buf into all-buffers
if (and (eq (buffer-local-value 'major-mode buf) buffer-major-mode)
(get-buffer-window buf t))
collect buf into visible-buffers
finally return (if (= (length visible-buffers) 1)
(car visible-buffers)
(if (= (length all-buffers) 1)
(car all-buffers)
(when-let ((buffers-by-name (mapcar (lambda (b)
(cons (buffer-name b) b))
all-buffers)))
(cdr
(assoc
(completing-read prompt buffers-by-name nil t)
buffers-by-name))))))
(user-error "No buffer found!")))
Attach file to telega.
(defun my/dired-attach-to-telega (files telega-buffer)
(interactive
(list (dired-get-marked-files nil nil #'dired-nondirectory-p)
(my/get-good-buffer 'telega-chat-mode "Telega buffer: ")))
(unless files
(user-error "No (non-directory) files selected"))
(with-current-buffer telega-buffer
(dolist (file