# Emacs config
Freedom is a state of mind
Author: SqrtMinusOne

> 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.

## Introduction

My configuration of GNU Emacs, an awesome text editor program that can do almost anything.

At the moment of this writing, this “almost anything” includes:

- Writing code. With LSP & Co Emacs is as good as many IDEs, and is certainly on par with editors like VS Code.
  Emacs is also particularly good at writing Lisp code, e.g. Clojure, Common Lisp, and, of course, Emacs Lisp.
- Literate programming with Org Mode. That includes:
  - Configuring the entirety of my software (that can be configured with text files).
  - Interactive programming like one provided by Jupyter Notebook.
- File management. Dired is my primary file manager.
- Email, with notmuch.
- Multimedia management, with EMMS.
- RSS feed reader, with elfeed.
- Task management, with Org Mode.
- Managing passwords, with pass.
- IRC, with ERC.
- Formatting documents, also with Org Mode. When the document is too complex, I prefer to write plain LaTeX, but I’ve come to the conclusion that in most cases Org Mode covers my needs there.
- X Window management, with EXWM. So I could say I literally live in Emacs.
- …

As I have hinted above, this file is a piece of literate configuration, where the actual code is interweaved with English-language commentary. One could argue that the commentary, and not the code, is the primary entity of the file.

But at the same time, the configuration is personal, so the primary benefactor of the literate structure is me. The commentary is primarily meant to capture my state of mind at the moment of writing the code, which is immensely helpful for maintaining the code in the future. So the quality and quantity of the commentary are… varying.

Occasionally I save some promising experimentations from scratch buffers without much comment. Or I may not have enough time to describe things in substantial detail. Or, as it is at the moment when I’m writing this, I have the time to write down whatever I consider necessary. Plus I usually incorporate my blog posts back into the config.

Of course, human minds share many similarities, so if you are an avid Emacs user, you have a chance to extract something of value from this document.

If however, by some twist of fate, this document is one of the first things you see about Emacs, it won’t be a good resource for you. And you definitely shouldn’t try to launch this config as it is. If I could suggest only one resource, I’d advise 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

## Bootstrap

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 a straight.el bootstrap script.

References:

- straight.el repo

    (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:

- use-package repo

    (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 if Emacs is meant to be used with TRAMP over slow ssh. Take a look at the TRAMP section for more details.

    (setq my/slow-ssh
          (or
           (string= (getenv "IS_TRAMP") "true")))

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) "violet")
    	  (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")))

Also, I sometimes need to know if a program is running inside Emacs (say, inside a terminal emulator). To do that, I set the following environment variable:

    (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

A convinience macro:

    (defmacro with-eval-after-load-norem (file &rest body)
      (declare (indent 1) (debug (form def-body)))
      `(unless my/remote-server
         (with-eval-after-load ,file
           ,@body)))

### 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 the order in which the packages are loaded and the loading time of individual packages.

    ;; (setq use-package-verbose 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.

Some time has passed, and 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)))))

### Anaconda

Anaconda is a free package and environment manager. I currently use it to manage multiple versions of Python and Node.js. Take a look at the corresponding entry in the Guix config for details about using it on Guix.

The following code uses the `conda` package to activate the base environment on startup if Emacs is launched outside the environment.

Also, some strange things are happening if vterm is launched with conda activated from Emacs, so I advise `conda-env-activate` to set an auxiliary environment variable. This variable is used in the shell config.

References:

- Anaconda docs
- conda.el repo

    (use-package conda
      :straight t
      :if (executable-find "conda")
      :config
      (setq conda-anaconda-home (string-replace "/bin/conda" "" (executable-find "conda")))
      (setq conda-env-home-directory (expand-file-name "~/.conda/"))
      (setq conda-env-subdirectory "envs")

      (advice-add 'conda-env-activate :after
    	      (lambda (&rest _)
    		(setenv "EMACS_CONDA_ENV" conda-env-current-name)
    		(setenv "INIT_CONDA" "true")))
      (advice-add 'conda-env-deactivate :after
    	      (lambda (&rest _)
    		(setenv "EMACS_CONDA_ENV" nil)
    		(setenv "INIT_CONDA" nil)))
      (unless (getenv "CONDA_DEFAULT_ENV")
        (conda-env-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

    (setq auth-source-debug nil)

#### 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)

### Prevent Emacs from closing

This adds a confirmation to avoid accidental Emacs closing.

    (setq confirm-kill-emacs 'y-or-n-p)

## General settings

### Keybindings

#### general.el

general.el provides a convenient interface to manage Emacs keybindings.

References:

- general.el repo

    (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:

- which-key repo

    (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)))
        (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 repo
- (YouTube) Evil Mode: Or, How I Learned to Stop Worrying and Love Emacs

##### 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)
      (evil-set-undo-system 'undo-tree))

##### 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
         ivy
         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)))

#### Avy

Avy is a package that helps navigate Emacs in a tree-like manner.

References:

- Avy can do anything

    (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))

#### My keybindings

Various keybinding settings that I can’t put anywhere else.

##### Escape key

Use the escape key instead of `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)

    (my-leader-def
      "SPC" '(:wk "second level")
      "SPC x" '(:wk "ctl-x")
      "SPC x" ctl-x-map)

`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

    (my-leader-def
      :infix "b"
      "" '(:which-key "buffers")
      "s" '((lambda () (interactive) (switch-to-buffer (persp-scratch-buffer)))
    	:which-key "*scratch*")
      "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
      "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.

    (general-nmap :keymaps '(hs-minor-mode-map outline-minor-mode-map)
      "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)

### i3 integration

UPD <2021-11-27 Sat>. 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 the post for a somewhat better presentation.

One 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 my/remote-server
      (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:

- aggressive-indent-mode repo

    (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:

- UndoTree on EmacsWiki

    (use-package undo-tree
      :straight t
      :config
      (global-undo-tree-mode)
      (setq undo-tree-visualizer-diff t)
      (setq undo-tree-visualizer-timestamps t)

      (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 <2022-04-11 Mon> I don’t really use `yasnippet-snippets`, so I’d rather write stuff manually.

References:

- yasnippet documentation

    (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)

#### 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:

- smartparens repo

    (use-package smartparens
      :straight t)

##### Expand region

A package to select an ever-increasing (or ever-decreasing) region of text.

    (use-package expand-region
      :straight t
      :commands (er/expand-region)
      :init
      (general-nmap "+" 'er/expand-region))

##### Visual fill column mode

    (use-package visual-fill-column
      :straight t
      :commands (visual-fill-column-mode)
      :config
      (add-hook 'visual-fill-column-mode-hook
    	    (lambda () (setq visual-fill-column-center-text t))))

### Working with projects

Packages related to managing projects.

Another important package that also touches this category is dired, but it has its separate section in “Applications”. I used to have Treemacs here, but in the end, decided that dired with dired-sidebar does a better job.

#### Projectile

Projectile gives a bunch of useful functions for managing projects, like finding files within a project, fuzzy-find, replace, etc.

`defadvice` is meant to speed projectile up with TRAMP a bit.

    (use-package projectile
      :straight t
      :config
      (projectile-mode +1)
      (setq projectile-project-search-path '("~/Code" "~/Documents"))
      (defadvice projectile-project-root (around ignore-remote first activate)
        (unless (file-remote-p default-directory) ad-do-it)))

    (use-package counsel-projectile
      :after (counsel projectile)
      :straight t)

    (my-leader-def
      "p" '(:keymap projectile-command-map :which-key "projectile"))

    (general-nmap "C-p" 'counsel-projectile-find-file)

#### Git & Magit

Magit is a git interface for Emacs. The closest non-Emacs alternative (sans actual clones) I know is lazygit, which I used before Emacs.

    (use-package magit
      :straight t
      :commands (magit-status magit-file-dispatch)
      :init
      (my-leader-def
        "m" 'magit
        "M" 'magit-file-dispatch)
      :config
      (setq magit-blame-styles
    	'((margin
    	   (margin-format    . ("%a %A %s"))
    	   (margin-width     . 42)
    	   (margin-face      . magit-blame-margin)
    	   (margin-body-face . (magit-blame-dimmed)))
    	  (headings
    	   (heading-format   . "%-20a %C %s\n"))
    	  (highlight
    	   (highlight-face   . magit-blame-highlight))
    	  (lines
    	   (show-lines       . t)
    	   (show-message     . t)))))

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)))

git-gutter is a package which shows git changes for each line (added/changed/deleted lines).

    (use-package git-gutter
      :straight t
      :if (not my/slow-ssh)
      :config
      (global-git-gutter-mode +1))

git-timemachine allows visiting previous versions of a file.

    (use-package git-timemachine
      :straight t
      :commands (git-timemachine))

#### Editorconfig

Editorconfig support for Emacs.

References:

- Editorconfig reference

    (use-package editorconfig
      :straight t
      :config
      (unless my/slow-ssh (editorconfig-mode 1))
      (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 1)

#### Deadgrep

deadgrep is a nice Emacs for ripgrep. Running `ivy-occur` in `counsel-rg` does something a bit similar, but the deadgrep is much more full-featured.

Somehow I couldn’t hook `toogle-truncate-lines` into the existing hooks, so I add 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))

### Completion

#### Ivy, counsel, swiper

Minibuffer completion tools for Emacs.

References:

- repo
- User Manual

    (use-package ivy
      :straight t
      :config
      (setq ivy-use-virtual-buffers t)
      (ivy-mode))

    (use-package counsel
      :straight t
      :after ivy
      :config
      (counsel-mode))

    (use-package swiper
      :defer t
      :straight t)

#### ivy-rich

ivy-rich provides a more informative interface for ivy.

    (use-package ivy-rich
      :straight t
      :after ivy
      :config
      (ivy-rich-mode 1)
      (setcdr (assq t ivy-format-functions-alist) #'ivy-format-function-line))

#### prescient

A package that enhances sorting & filtering of candidates. `ivy-prescient` adds integration with Ivy.

References:

- prescient.el repo

    (use-package ivy-prescient
      :straight t
      :after counsel
      :config
      (ivy-prescient-mode +1)
      (setq ivy-prescient-retain-classic-highlighting t)
      (prescient-persist-mode 1)
      (setq ivy-prescient-sort-commands
    	'(:not swiper
    	       swiper-isearch
    	       ivy-switch-buffer
    	       ;; ivy-resume
    	       ;; ivy--restore-session
    	       lsp-ivy-workspace-symbol
    	       dap-switch-stack-frame
    	       my/dap-switch-stack-frame
    	       dap-switch-session
    	       dap-switch-thread
    	       counsel-grep
    	       ;; counsel-find-file
    	       counsel-git-grep
    	       counsel-rg
    	       counsel-ag
    	       counsel-ack
    	       counsel-fzf
    	       counsel-pt
    	       counsel-imenu
    	       counsel-yank-pop
    	       counsel-recentf
    	       counsel-buffer-or-recentf
    	       proced-filter-interactive
    	       proced-sort-interactive
    	       perspective-exwm-switch-perspective
    	       my/persp-ivy-switch-buffer-other-window
    	       lsp-execute-code-action
    	       dired-recent-open))
      ;; Do not use prescient in find-file
      (ivy--alist-set 'ivy-sort-functions-alist #'read-file-name-internal #'ivy-sort-file-function-default))

#### keybindings

Setting up quick access to various completions.

    (my-leader-def
      :infix "f"
      "" '(:which-key "various completions")'
      ;; "b" 'counsel-switch-buffer
      "b" 'persp-ivy-switch-buffer
      "e" 'conda-env-activate
      "f" 'project-find-file
      "c" 'counsel-yank-pop
      "a" 'counsel-rg
      "d" 'deadgrep
      "A" 'counsel-ag)

    (general-define-key
     :states '(insert normal)
     "C-y" 'counsel-yank-pop)

    (my-leader-def "SPC SPC" 'ivy-resume)
    (my-leader-def "s" 'swiper-isearch
      "S" 'swiper-all)

    (general-define-key
     :keymaps '(ivy-minibuffer-map swiper-map)
     "M-j" 'ivy-next-line
     "M-k" 'ivy-previous-line
     "<C-return>" 'ivy-call
     "M-RET" 'ivy-immediate-done
     [escape] 'minibuffer-keyboard-quit)

#### company

A completion framework for Emacs.

References:

- company homepage
- company-box homepage

    (use-package company
      :straight t
      :config
      (global-company-mode)
      (setq company-idle-delay 0.125)
      (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)

    (general-define-key
     :keymaps 'help-map
     "f" 'helpful-function
     "k" 'helpful-key
     "v" 'helpful-variable
     "o" 'helpful-symbol)

### Time trackers

A bunch of time trackers I use.

References:

- WakaTime
- ActivityWatch

#### 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/is-termux my/remote-server))
      :config
      (setq wakatime-ignore-exit-codes '(0 1 102))
      (advice-add 'wakatime-init :after (lambda () (setq wakatime-cli-path "/home/pavel/bin/wakatime-cli")))
      ;; (setq wakatime-cli-path (executable-find "wakatime"))
      (global-wakatime-mode))

#### ActivityWatch

    (use-package request
      :straight 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))

Transparency. Not setting it now, as I’m using picom.

    ;; (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. Run `M-x toggle-word-wrap` to toggle that.
- `visual-line-mode` seems to be a superset of `word-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 as `word-wrap`, except it actually edits the buffer to make lines break in the appropriate places.
- `truncate-lines` truncates long lines instead of continuing them. Run `M-x toggle-truncate-lines` to toggle that. I find that `truncate-lines` behaves strangely when `visual-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 looks something like `emacs:project@hostname`.

    (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
      :config
      (setq-default olivetti-body-width 86))

#### Keycast

Showing the last pressed key. Occasionally useful.

    (use-package keycast
      :straight t
      :config
      (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)))))

### Themes and colors

#### Doom themes

My colorscheme of choice.

    (use-package doom-themes
      :straight t
      :if (not my/is-termux)
      :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))

#### Custom theme

Here I define a custom theme, dependent on colors from `doom-themes`.

A custom theme is necessary 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.

To make defining colors a bit easier, here is a function to blend two colors, taken from this post by abo-abo.

    (defun my/color-join (r g b)
      "Build a color from R G B.
    Inverse of `color-values'."
      (format "#%02x%02x%02x"
    	  (ash r -8)
    	  (ash g -8)
    	  (ash b -8)))

    (defun my/color-blend (c1 c2 &optional alpha)
      "Blend the two colors C1 and C2 with ALPHA.
    C1 and C2 are in the format of `color-values'.
    ALPHA is a number between 0.0 and 1.0 which corresponds to the
    influence of C1 on the result."
      (setq alpha (or alpha 0.5))
      (apply #'my/color-join
    	 (cl-mapcar
    	  (lambda (x y)
    	    (round (+ (* x alpha) (* y (- 1 alpha)))))
    	  c1 c2)))

Defining the theme itself.

    (deftheme my-theme-1)

A macro to simplify defining custom colors.

    (defvar my/doom-theme-update-colors-hook nil)

    (defmacro my/use-doom-colors (&rest data)
      `(progn
         (add-hook 'my/doom-theme-update-colors-hook
    	       (lambda ()
    		 (custom-theme-set-faces
    		  'my-theme-1
    		  ,@(cl-loop for i in data collect
    			     `(,'\`
    			       (,(car i)
    				((t (,@(cl-loop for (key value) on (cdr i) by #'cddr
    						append `(,key (,'\, ,value))))))))))))
         (when (and (fboundp 'doom-color) my/emacs-started)
           (my/update-my-theme))))

This macro puts lambdas to `my/doom-theme-update-colors-hook` that updates faces in `my-theme-1`. Now I have to call this hook:

    (defun my/update-my-theme (&rest _)
      (run-hooks 'my/doom-theme-update-colors-hook)
      (enable-theme 'my-theme-1))

    (unless my/is-termux
      (advice-add 'load-theme :after #'my/update-my-theme)
      (when (fboundp 'doom-color)
        (my/update-my-theme))
      (add-hook 'emacs-startup-hook #'my/update-my-theme))

Defining colors for `tab-bar.el`:

    (my/use-doom-colors
     (tab-bar-tab :background (doom-color 'bg)
    	      :foreground (doom-color 'yellow)
    	      :underline (doom-color 'yellow))
     (tab-bar :background nil :foreground nil))

#### 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-doom-colors
       (auto-dim-other-buffers-face
        :background (color-darken-name (doom-color 'bg) 3))))

#### Toggle light/dark

    (defun my/toggle-dark-light-theme ()
      (interactive)
      (let ((is-dark (member 'doom-palenight custom-enabled-themes)))
        (if is-dark
    	(progn
    	  (load-theme 'doom-one-light t)
    	  (disable-theme 'doom-palenight))
          (load-theme 'doom-palenight t)
          (disable-theme 'doom-one-light))))

#### 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-doom-colors
       (ansi-color-black
        :foreground (doom-color 'base2) :background (doom-color 'base0))
       (ansi-color-red
        :foreground (doom-color 'red) :background (doom-color 'red))
       (ansi-color-green
        :foreground (doom-color 'green) :background (doom-color 'green))
       (ansi-color-yellow
        :foreground (doom-color 'yellow) :background (doom-color 'yellow))
       (ansi-color-blue
        :foreground (doom-color 'dark-blue) :background (doom-color 'dark-blue))
       (ansi-color-magenta
        :foreground (doom-color 'violet) :background (doom-color 'violet))
       (ansi-color-cyan
        :foreground (doom-color 'dark-cyan) :background (doom-color 'dark-cyan))
       (ansi-color-white
        :foreground (doom-color 'base8) :background (doom-color 'base8))
       (ansi-color-bright-black
        :foreground (doom-color 'base5) :background (doom-color 'base5))
       (ansi-color-bright-red
        :foreground (doom-color 'orange) :background (doom-color 'orange))
       (ansi-color-bright-green
        :foreground (doom-color 'teal) :background (doom-color 'teal))
       (ansi-color-bright-yellow
        :foreground (doom-color 'yellow) :background (doom-color 'yellow))
       (ansi-color-bright-blue
        :foreground (doom-color 'blue) :background (doom-color 'blue))
       (ansi-color-bright-magenta
        :foreground (doom-color 'magenta) :background (doom-color 'magenta))
       (ansi-color-bright-cyan
        :foreground (doom-color 'cyan) :background (doom-color 'cyan))
       (ansi-color-bright-white
        :foreground (doom-color 'fg) :background (doom-color '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:

- nerd fonts homepage

    (when (display-graphic-p)
      (if (x-list-fonts "JetBrainsMono Nerd Font")
          (set-frame-font "JetBrainsMono Nerd Font 10" nil t)
        (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.

#### 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
         js2-mode
         vue-mode
         svelte-mode
         scss-mode
         php-mode
         python-mode
         js-mode
         markdown-mode
         clojure-mode
         go-mode
         sh-mode
         haskell-mode
         web-mode)
       '("--" "---" "==" "===" "!=" "!==" "=!=" "=:=" "=/=" "<="
         ">=" "&&" "&&&" "&=" "++" "+++" "***" ";;" "!!" "??"
         "?:" "?." "?=" "<:" ":<" ":>" ">:" "<>" "<<<" ">>>"
         "<<" ">>" "||" "-|" "_|_" "|-" "||-" "|=" "||=" "##"
         "###" "####" "#{" "#[" "]#" "#(" "#?" "#_" "#_(" "#:"
         "#!" "#=" "^=" "<$>" "<$" "$>" "<+>" "<+" "+>" "<*>"
         "<*" "*>" "</" "</>" "/>" "<!--" "<#--" "-->" "->" "->>"
         "<<-" "<-" "<=<" "=<<" "<<=" "<==" "<=>" "<==>" "==>" "=>"
         "=>>" ">=>" ">>=" ">>-" ">-" ">--" "-<" "-<<" ">->" "<-<"
         "<-|" "<=|" "|=>" "|->" "<->" "<~~" "<~" "<~>" "~~" "~~>"
         "~>" "~-" "-~" "~@" "[||]" "|]" "[|" "|}" "{|" "[<"
         ">]" "|>" "<|" "||>" "<||" "|||>" "<|||" "<|>" "..." ".."
         ".=" ".-" "..<" ".?" "::" ":::" ":=" "::=" ":?" ":?>"
         "//" "///" "/*" "*/" "/=" "//=" "/==" "@_" "__"))
      (global-ligature-mode t))

#### Icons

Run `M-x all-the-icons-install-fonts` at first setup.

    (use-package all-the-icons
      :if (display-graphic-p)
      :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:

- Doom Modeline

    (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)
      :config
      (setq doom-modeline-minor-modes nil)
      (setq doom-modeline-irc nil)
      (setq doom-modeline-buffer-state-icon nil)
      (doom-modeline-mode 1))

### perspective.el

perspective.el is a package that provides gives Emacs capacities to group buffers into “perspectives”, which are like workspaces in tiling WMs.

An advantage over `tab-bar.el` is that `perspective.el` has better capacities for managing buffers, e.g. gives an ibuffer-like interface inside a perspective.

However, I don’t like that list of workspaces is displayed inside the modeline rather than in an actual bar on the top of the frame. I may look into that later.

    (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-ivy-switch-buffer
       "x" 'persp-ivy-switch-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)))

Switch to a perspective buffer in other window.

    (defun my/persp-ivy-switch-buffer-other-window (arg)
      (interactive "P")
      (declare-function ivy-switch-buffer-other-window "ivy.el")
      (persp--switch-buffer-ivy-counsel-helper
       arg
       (lambda ()
         (ivy-read "Switch to buffer in other window: " #'internal-complete-buffer
    	       :keymap ivy-switch-buffer-map
    	       :preselect (buffer-name (other-buffer (current-buffer)))
    	       :action #'ivy--switch-buffer-other-window-action
    	       :matcher #'ivy--switch-buffer-matcher
    	       :caller 'ivy-switch-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

One thing I don’t like about `perspective.el` is that it doesn’t feature much (or any) capacity for automation. So out-of-the-box we’re supposed to manually assign buffers to perspectives we want.

But we can cook some automation ourselves. 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.

    (defun my/perspective-assign ()
      (when-let* ((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)))))

If EXWM is available, then so is mine `perspective-exwm` package that features a convenient procedure called `perspective-exwm-assign-window`. If not, 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, the packages themselves run `switch-to-buffer`, which screws both EXWM workspaces and perspectives; to work around that, I define a macro that runs a command in 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 quite 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:

- lsp-mode homepage

##### Setup

    (use-package lsp-mode
      :straight t
      :if (not (or my/slow-ssh 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))

##### 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)

#### Flycheck

A syntax checking extension for Emacs. Integrates with LSP-mode, but can also use various standalone checkers.

References:

- Flycheck homepage

    (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))))

#### Tree Sitter

An incremental code parsing system, constructing a syntax tree at runtime.

Right now it doesn’t do much except provide a better syntax highlighting than regexes, but this integration is a rather recent development. There are already some major modes built on top of this thing.

Also, it seems to break if run from mmm-mode, so there is a small workaround.

References:

- Tree-sitter library
- Emacs Tree-sitter

    (defun my/tree-sitter-if-not-mmm ()
      (when (not (and (boundp 'mmm-temp-buffer-name)
    		  (string-equal mmm-temp-buffer-name (buffer-name))))
        (tree-sitter-mode)
        (tree-sitter-hl-mode)))

    (use-package tree-sitter
      :straight t
      :if (not my/remote-server)
      :hook ((typescript-mode . my/tree-sitter-if-not-mmm)
    	 (js-mode . my/tree-sitter-if-not-mmm)
    	 (python-mode . tree-sitter-mode)
    	 (python-mode . tree-sitter-hl-mode)
    	 (csharp-mode . tree-sitter-mode)))

    (use-package tree-sitter-langs
      :straight t
      :after tree-sitter)

#### DAP

An Emacs client for Debugger Adapter Protocol.

As of the time of this writing, I mostly debug TypeScript, so the main competitor is Chrome Inspector for node.js.

References:

- dap-mode homepage

    (use-package dap-mode
      :straight t
      :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 using it 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 "SqrtMinusOne/copilot.el" :files ("dist" "*.el"))
      :commands (copilot-mode)
      :if (not my/remote-server)
      :disabled
      :init
      (add-hook 'prog-mode-hook #'copilot-mode)
      :config
      (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)
      (setq copilot-lispy-integration t))

#### General additional config

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))

### 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 vue-mode-map svelte-mode-map)
        "rr" #'prettier-prettify))

#### TypeScript

    (use-package typescript-mode
      :straight t
      :mode "\\.ts\\'"
      :config
      (add-hook 'typescript-mode-hook #'smartparens-mode)
      (add-hook 'typescript-mode-hook #'rainbow-delimiters-mode)
      (add-hook 'typescript-mode-hook #'hs-minor-mode)
      (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.

    (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))

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)))

    (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:

- AUCTeX homepage

    (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)

### Other 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"))

#### 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))
      :config
      (general-define-key
       :keymaps '(subed-mode-map subed-vtt-mode-map)
       :states '(normal)
       "gp" #'subed-mpv-toggle-pause))

#### 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:

- LanguageTool homepage
- LanguageTool http server

    (use-package langtool
      :straight t
      :commands (langtool-check)
      :config
      (setq langtool-language-tool-server-jar "/home/pavel/bin/LanguageTool-5.7/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)

### 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
      :after flycheck
      :config
      (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)

#### 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\\'"
      :config
      (add-hook 'clips-mode 'lispy-mode))

### Python

#### ein

ein is a package that allows for running Jupyter notebooks in Emacs.

    (use-package ein
      :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
      :if (not my/slow-ssh)
      :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)
      :if (not my/slow-ssh)
      :init
      (setq
       pipenv-projectile-after-switch-function
       #'pipenv-projectile-after-switch-extended))

#### yapf

yapf is a formatter for Python files.

Guix dependency
python-yapf

References:

- yapf repo
- yapfify.el repo

    (use-package yapfify
      :straight (:repo "JorisE/yapfify" :host github)
      :commands (yapfify-region
    	     yapfify-buffer
    	     yapfify-region-or-buffer
    	     yapf-mode))

Global config:

    [style]
    based_on_style = facebook
    column_limit = 80

#### isort

isort is a Python package to sort Python imports.

Guix dependency
python-isort

References:

- isort docs
- py-isort.el repo

    (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
      "rr" (lambda ()
    	 (interactive)
    	 (unless (and (fboundp #'org-src-edit-buffer-p) (org-src-edit-buffer-p))
    	   (py-isort-buffer))
    	 (yapfify-buffer)))

#### 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))

#### pytest

pytest is a unit testing framework for Python.

Once again a function to set pytest executable from pipenv.

References:

- pytest docs
- emacs-python-pytest

    (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.

    (use-package code-cells
      :straight t
      :commands (code-cells-mode))

#### 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 #'smartparens-mode)
      (add-hook 'json-mode #'hs-minor-mode)
      (my/set-smartparens-indent 'json-mode))

#### CSV

    (use-package csv-mode
      :straight t
      :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
      :config
      (add-hook 'jenkinsfile-mode-hook #'smartparens-mode)
      (my/set-smartparens-indent 'jenkinsfile-mode))

#### crontab

    (use-package crontab-mode
      :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))

### x509

    (use-package x509-mode
      :straight t)

### 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

    (use-package csharp-mode
      :straight t
      :mode "\\.cs\\'"
      :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)

### 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, "-u"))

    (my-leader-def
      :keymaps '(sql-mode-map)
      "rr" #'sqlformat-buffer)

### SPARQL

    (use-package sparql-mode
      :straight t)

## Org Mode

Org mode is a tool that leverages plain-text files for various tasks like making notes, literate programming, task management, etc.

References:

- Org Mode homepage
- Manual

### Installation & basic settings

Use the built-in org mode (`:type built-in`).

    (use-package org
      :straight (:type built-in)
      :if (not my/remote-server)
      :defer t
      :init
      (setq org-directory (expand-file-name "~/Documents/org-mode"))
      (unless (file-exists-p org-directory)
        (mkdir org-directory t))
      :config
      (setq org-startup-indented t)
      (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 a part of a file.

    (with-eval-after-load-norem 'org
      (require 'org-crypt)
      (org-crypt-use-before-save-magic)
      (setq org-tags-exclude-from-inheritance (quote ("crypt")))
      (setq org-crypt-key "C1EC867E478472439CC82410DE004F32AFA00205"))

This enables encryption for Org segments which are tagged `:crypt:`.

Another way to encrypt org files is to save them with extension `.org.gpg`. That way by default epa always prompts for a key, which is not what I want when there is in fact only one key to select. So I make 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-norem '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’ve been migrated to separate repos since.

    (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.

    (use-package ol-notmuch
      :straight t
      :after (org))

#### 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))

### Literate programing

#### Python & Jupyter

Use jupyter kernels for Org Mode.

References:

- emacs-jupyter repo
- SCIMAX manual

    (use-package jupyter
      :straight t
      :after (org)
      :if (not my/remote-server))

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))

#### 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:

- WOW! Writing Literate API Documentation in Emacs Org Mode

    (use-package restclient
      :if (not my/remote-server)
      :straight t)

    (use-package ob-restclient
      :after (org restclient)
      :if (not my/remote-server)
      :straight t)

#### Setup

Enable languages

    (with-eval-after-load-norem '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)
         (jupyter . t)
         (sparql . 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 prefer to put the org files to a separate 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
      :after (org-present))

    (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:

- alphapapa/org-make-toc

    (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))

### Productivity & Knowledge management

My ongoing effort to get a productivity setup in Org.

Some inspiration:

- Nicolas P. Rougier. Get Things Done with Emacs
- Jetro Kuan. Org-mode Workflow
- Alexey Shmalko: How I note
- Rohit Goswami: An Orgmode Note Workflow

Used files:

    (defun my/update-org-agenda ()
      (interactive)
      (let ((project-files
    	 (mapcar
    	  (lambda (f) (concat
    		       org-directory "/projects/"
    		       f))
    	  (seq-filter
    	   (lambda (f) (not (member f '("." ".."))))
    	   (directory-files
    	    (concat org-directory "/projects"))))))
        (setq org-agenda-files
    	  `("inbox.org"
    	    ,@project-files))
        (setq org-refile-targets
    	  `(,@(mapcar
    	       (lambda (f) `(,f . (:maxlevel . 2)))
    	       project-files)
    	    ,@(mapcar
    	       (lambda (f) `(,f . (:tag . "refile")))
    	       project-files)))))

    (with-eval-after-load-norem 'org
      (setq org-roam-directory (concat org-directory "/roam"))
      (my/update-org-agenda)
      ;; (setq org-default-notes-file (concat org-directory "/notes.org"))
      )

Hotkeys

    (my-leader-def
      :infix "o"
      "" '(:which-key "org-mode")
      "c" 'org-capture
      "a" 'org-agenda)

Refile targets

    (setq org-refile-use-outline-path 'file)
    (setq org-outline-path-complete-in-steps nil)

#### Capture templates & various settings

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.org"
       org-directory
       (format-time-string "%Y%m%d%H%M%S")))

    (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" entry (file my/generate-inbox-note-name)
    	 ,(concat "* %?\n"
    		  "/Entered on/ %U"))))

Effort estimation

    (with-eval-after-load-norem 'org
      (add-to-list 'org-global-properties
    	       '("Effort_ALL" . "0 0:05 0:10 0:15 0:30 0:45 1:00 2:00 4:00")))

Log DONE time

    (setq org-log-done 'time)

#### OFF (OFF) Trello sync

UPD <2022-03-27 Sun>: disabling this for now.

Some of the projects I’m participating in are managed via Trello, so I use org-trello to keep track of them. The package has a remarkably awkward keybindings setup, so my effort to call `my-leader-def` to set keybindings I like is no less awkward.

Also, trello files are huge and have a lot of information and tasks which do not concern me, so I don’t add them to `org-agenda-files`.

    (unless (file-exists-p (concat org-directory "/trello"))
      (mkdir (concat org-directory "/trello") t))

    (setq org-trello-files
          (thread-last (concat org-directory "/trello")
    	(directory-files)
    	(seq-filter
    	 (lambda (f) (string-match-p (rx ".org" eos) f)))
    	(mapcar
    	 (lambda (f) (concat org-directory "/trello/" f)))))

    (use-package org-trello
      :straight (:build (:not native-compile))
      :commands (org-trello-mode)
      :disabled
      :if (not my/remote-server)
      :init
      (setq org-trello-current-prefix-keybinding "C-c o")
      (setq org-trello-add-tags nil)

      (add-hook 'org-mode-hook
    	    (lambda ()
    	      (when (string-match-p (rx "trello") (or (buffer-file-name) ""))
    		(org-trello-mode))))
      :config
      (eval
       `(my-leader-def
          :infix "o t"
          :keymaps '(org-trello-mode-map)
          "" '(:which-key "trello")
          ,@(mapcan
    	 (lambda (b) (list (nth 1 b) (macroexp-quote (nth 0 b))))
    	 org-trello-interactive-command-binding-couples))))

#### org-ql

org-ql is a package to query the org files. I’m using it in my review workflow and for custom agenda views.

    (use-package org-ql
      :after (org)
      :if (not my/remote-server)
      :straight (:fetcher github
    		      :repo "alphapapa/org-ql"
    		      :files (:defaults (:exclude "helm-org-ql.el"))))

#### Custom agendas

Some custom agendas to fit my workflow.

Despite the fact that I don’t add `org-trello-files` to `org-agenda-files` I still want to see them in agenda, so I use `org-ql-block` from `org-ql`.

    (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" "log" "log_here" "refile")))

    (setq org-agenda-custom-commands
          `(("p" "My outline"
    	 ((agenda "")
    	  (todo "NEXT"
    		((org-agenda-prefix-format "  %i %-12:c [%e] ")
    		 (org-agenda-overriding-header "Next tasks")))
    	  (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)")))))))

#### Bibliography

I’m currently trying to 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.

##### org-ref

org-ref is an excellent package by John Kitchin that provides support for managing citations and references in Org Mode.

It may have become less relevant since `org-cite` was merged into plain Org, but `org-ref` is still just as usable.

As of now, this package loads Helm on start. To avoid this, I have to exclude Helm from the `Package-requires` in the org-ref.el file. I haven’t found a way to do this without modifying the package source yet.

There’s a package called org-roam-bibtex that allows to keep literature notes in org-roam and access them from `org-ref`, but as for now I store literature notes separately, as advised by Ahrens Sönke in his excellent book “How to take smart notes”.

    (use-package org-ref
      :straight (:files (:defaults (:exclude "*helm*")))
      :if (not my/remote-server)
      :init
      (setq bibtex-dialect 'biblatex)
      (setq bibtex-completion-bibliography '("~/Documents/org-mode/library.bib"))
      (setq bibtex-completion-library-path '("~/Documents/library"))
      (setq bibtex-completion-notes-path "~/Documents/org-mode/literature-notes")
      (setq bibtex-completion-display-formats
    	'((t . "${author:36} ${title:*} ${note:10} ${year:4} ${=has-pdf=:1}${=type=:7}")))
      (setq bibtex-completion-pdf-open-function
    	(lambda (file)
    	  (start-process "dired-open" nil
    			 "xdg-open" (file-truename file))))
      :after (org)
      :config
      (require 'org-ref-ivy)
      (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))

##### ivy-bibtex

ivy-bibtex is an Ivy interface to bibtex. It uses the same configuration variables as `org-ref`, or rather, both packages use variables from the built-in `bibtex.el`

    (use-package ivy-bibtex
      :commands (ivy-bibtex)
      :straight t
      :init
      (my-leader-def "fB" 'ivy-bibtex))

    (add-hook 'bibtex-mode 'smartparens-mode)

#### Org Journal

org-journal is a plugin for maintaining a journal in org mode. I want(ed) to have its entries separate from my knowledge base.

I’ve tried switching to Org Roam Dailies, but in the end decided that org-journal fits my workflow better.

    (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))

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)))
          :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)

And here’s a function that creates a drawer with such information. At the moment, it’s:

- Emacs version
- Hostname
- Location
- Weather
- Current EMMS track

    (defun my/set-journal-header ()
      (org-set-property "Emacs" emacs-version)
      (org-set-property "Hostname" system-name)
      (org-journal-tags-prop-apply-delta :add (list (format "host.%s" (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)))))))

    (add-hook 'org-journal-after-entry-create-hook
    	  #'my/set-journal-header)

#### Org Roam

org-roam is a plain-text knowledge database.

Things I tried with Org Roam:

- Managing projects. Ended up preferring plain Org.
- Writing journal. `org-journal` and `org-journal-tags` seem to be a better fit for my workflow.

##### Basic package configuration

Guix dependency
emacs-emacsql-sqlite3
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:

- Hitchhiker’s Rough Guide to Org roam V2

    (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 (not my/remote-server)
      :after org
      :init
      (setq org-roam-file-extensions '("org"))
      (setq org-roam-v2-ack t)
      (setq orb-insert-interface 'ivy-bibtex)
      (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.

I used to have multiple categories of nodes in Org Roam (projects, dailies, etc), but as of now, only Zettelkasten remains.

    (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)
      (general-define-key
       :keymap 'org-mode-map
       "C-c i" 'org-roam-node-insert))

##### Org Roam UI

A browser frontend to visualize a Roam directory in a form of a graph.

    (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))

##### Org Roam Protocol

Open links such as `org-protocol://` from browser. Run `M-x server-start` for org-protocol to work.

    [Desktop Entry]
    Name=Org-Protocol
    Exec=emacsclient %u
    Icon=emacs-icon
    Type=Application
    Terminal=false
    MimeType=x-scheme-handler/org-protocol

Don’t forget to run the following after setup:

    xdg-mime default org-protocol.desktop x-scheme-handler/org-protocol

##### Deft

Deft is an Emacs package to quickly find notes. It seems quite useful to look for `org-roam` notes based on their contents.

    (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))

#### Review workflow

UPD <2022-03-27 Sun>. Out of action for now

My take on a review workflow. As a baseline, I want to have a template that lists the important changes since the last review and other basic information. I’m doing reviews regularly, but the time intervals still may vary, hence this flexibility.

This section has seen some updates over time.

##### Data from git

First, as I have autocommit set up in my org directory, here is a handy function to get an alist of changed files of a form `(status . path)`. In principle, the `rev` parameter can be a commit, tag, etc but here I’m interested in a form like `@{2021-08-30}`.

Also in principle, Org Roam DB also stores stuff like creation time and modification time, but I started this section before I started using Org Roam extensively, so git works fine for me.

    (setq my/git-diff-status
          '(("A" . added)
    	("C" . copied)
    	("D" . deleted)
    	("M" . modified)
    	("R" . renamed)
    	("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))
    	  (nth 1 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

Now that we have the list of new & changed files, I want to sort into a bunch of categories: projects, log entries, etc. The categories are defined by tags.

So here is a list of plists that sets these categories. The properties are as follows:

- `:status` is a git status for the file
- `:tags` is a plist that sets up the following conditions for the Roam node
  - `:include` - should be empty or one of these should be present
  - `:exclude` - should be empty or none of these should be present
- `:title` is the name of category as I want it to be seen in the review template

    (setq my/org-review-roam-queries
          '((:status added
    		 :tags (:include ("org"))
    		 :title "New Project Entries")
    	(:status changed
    		 :tags (:include ("org"))
    		 :title "Changed Project Entries")
    	(:status added
    		 :tags (:exclude ("org"))
    		 :title "New Zettelkasten Entries")
    	(:status changed
    		 :tags (:exclude ("org"))
    		 :title "Changed Zettelkasten Entries")))

This list is used to extract & format the relevant section of the review template.

`cl-loop` seems pretty good as a control flow structure, but I’ll see if it is also pretty good at producing poorly maintainable code. At least at the moment of this writing, the function below looks rather concise.

    (defun my/org-review-format-roam (changes)
      (cl-loop for query in my/org-review-roam-queries
    	   with nodes = (org-roam-node-list)
    	   with node-tags = (mapcar #'org-roam-node-tags nodes)
    	   for include-tags = (plist-get (plist-get query :tags) :include)
    	   for exclude-tags = (plist-get (plist-get query :tags) :exclude)
    	   ;; List of nodes filtered by :tags in query
    	   for filtered-nodes =
    	   (cl-loop for node in nodes
    		    for tags in node-tags
    		    if (and
    			(or (seq-empty-p include-tags)
    			    (seq-intersection include-tags tags))
    			(or (seq-empty-p exclude-tags)
    			    (not (seq-intersection exclude-tags tags))))
    		    collect node)
    	   ;; List of changes filtered by :status in query
    	   for filtered-changes =
    	   (cl-loop for change in changes
    		    if (and (eq (car change) (plist-get query :status))
    			    (string-match-p (rx bos "roam") (cdr change)))
    		    collect (cdr change))
    	   ;; Intersection of the two filtered lists
    	   for final-nodes =
    	   (cl-loop for node in filtered-nodes
    		    for path = (file-relative-name (org-roam-node-file node)
    						   org-directory)
    		    if (member path filtered-changes)
    		    collect node)
    	   ;; If the intersction list is not empty, format it to the result
    	   if final-nodes
    	   concat (format "** %s\n" (plist-get query :title))
    	   ;; FInal list of links, sorted by title
    	   and concat (cl-loop for node in (seq-sort
    					    (lambda (node1 node2)
    					      (string-lessp
    					       (org-roam-node-title node1)
    					       (org-roam-node-title node2)))
    					    final-nodes)
    			       concat (format "- [[id:%s][%s]]\n"
    					      (org-roam-node-id node)
    					      (org-roam-node-title node)))))

##### Data from org-agenda via org-ql

Third second, I want to list some changes in my agenda. This section will change depending on what I’m currently working on.

So, here is a list of queries results of which I want to see in the review template. The format is `(name date-field order-by-field query)`.

    (setq my/org-ql-review-queries
          `(("Waitlist" scheduled scheduled
    	 (and
    	  (done)
    	  (tags-inherited "waitlist")))
    	("Personal tasks done" closed ,nil
    	 (and
    	  (tags-inherited "personal")
    	  (todo "DONE")))
    	("Attended meetings" closed scheduled
    	 (and
    	  (tags-inherited "meeting")
    	  (todo "PASSED")))
    	("Done project tasks" closed deadline
    	 (and
    	  (todo "DONE")
    	  (ancestors
    	   (heading "Tasks"))))))

The query will be executed like this: `(and (date-field :from rev-date) query)`

    (defun my/org-review-exec-ql (saved rev-date)
      (let ((query `(and
    		 (,(nth 1 saved) :from ,rev-date)
    		 ,(nth 3 saved))))
        (org-ql-query
          :select #'element
          :from (org-agenda-files)
          :where query
          :order-by (nth 2 saved))))

Format one element of the query result.

    (defun my/org-review-format-element (elem)
      (concat
       (string-pad
        (plist-get (cadr elem) :raw-value)
        40)
       (when-let (scheduled (plist-get (cadr elem) :scheduled))
         (concat " [SCHEDULED: " (plist-get (cadr scheduled) :raw-value) "]"))
       (when-let (deadline (plist-get (cadr elem) :deadline))
         (concat " [DEADLINE: " (plist-get (cadr deadline) :raw-value) "]"))))

Execute all the saved queries and format an Org list for the capture template.

    (defun my/org-review-format-queries (rev-date)
      (mapconcat
       (lambda (results)
         (concat "** " (car results) "\n"
    	     (string-join
    	      (mapcar (lambda (r) (concat "- " r)) (cdr results))
    	      "\n")
    	     "\n"))
       (seq-filter
        (lambda (result)
          (not (seq-empty-p (cdr result))))
        (mapcar
         (lambda (saved)
           (cons
    	(car saved)
    	(mapcar
    	 #'my/org-review-format-element
    	 (my/org-review-exec-ql saved rev-date))))
         my/org-ql-review-queries))
       "\n"))

##### Capture template

Now, we have to put all this together and define a capture template for the review.

I’ll use a separate directory for the review files, just like for org-journal and org-roam. I’ll store the review files in org-roam. Time will tell if that’s a good idea. The filename will have a format `YYYY-MM-DD.org`, which will also free me from the effort of storing the last review date somewhere.

If somehow there are no files in the folder, fallback to the current date minus one two week. Also featuring the most awkward date transformation I’ve ever done just to add one date.

    (setq my/org-review-directory "review")

    (defun my/get-last-review-date ()
      (->
       (substring
        (or
         (-max-by
          'string-greaterp
          (-filter
           (lambda (f) (not (or (string-equal f ".") (string-equal f ".."))))
           (directory-files (f-join org-roam-directory my/org-review-directory))))
         (format-time-string
          "%Y-%m-%d"
          (time-subtract
           (current-time)
           (seconds-to-time (* 60 60 24 14)))))
        0 10)
       (concat "T00:00:00-00:00")
       parse-time-string
       encode-time
       (time-add (seconds-to-time (* 60 60 24)))
       ((lambda (time)
          (format-time-string "%Y-%m-%d" time)))))

A template looks like this:

    (setq my/org-review-capture-template
          `("r" "Review" plain
    	,(string-join
    	  '("#+title: %<%Y-%m-%d>: REVIEW"
    	    "#+category: REVIEW"
    	    "#+filetags: log review"
    	    "#+STARTUP: overview"
    	    ""
    	    "Last review date: %(org-timestamp-translate (org-timestamp-from-string (format \"<%s>\" (my/get-last-review-date))))"
    	    ""
    	    "* Roam"
    	    "%(my/org-review-format-roam (my/org-changed-files-since-date (my/get-last-review-date)))"
    	    "* Agenda"
    	    "%(my/org-review-format-queries (my/get-last-review-date))"
    	    "* Thoughts"
    	    "%?")
    	  "\n")
    	:if-new (file "review/%<%Y-%m-%d>.org.gpg")))

    (defun my/org-roam-capture-review ()
      (interactive)
      (org-roam-capture- :node (org-roam-node-create)
    		     :templates `(,my/org-review-capture-template)))

#### org-contacts

`org-contacts` is a package to store contacts in an org file.

It seems like 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)
      :commands (org-contacts org-contacts-db)
      :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:

#### 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")))))

#### Copying records

### UI

#### OFF (OFF) Instant equations preview

Instant math previews for org mode.

References:

- org-latex-impatient repo

    (use-package org-latex-impatient
      :straight (:repo "yangsheng6810/org-latex-impatient"
    		   :branch "master"
    		   :host github)
      :hook (org-mode . org-latex-impatient-mode)
      :disabled
      :init
      (setq org-latex-impatient-tex2svg-bin
    	"/home/pavel/Programs/miniconda3/lib/node_modules/mathjax-node-cli/bin/tex2svg")
      (setq org-latex-impatient-scale 1.75)
      (setq org-latex-impatient-delay 1)
      (setq org-latex-impatient-border-color "#ffffff"))

#### 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-norem '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-norem '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))

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-doom-colors
     (org-block :background (color-darken-name (doom-color 'bg) 3))
     (org-block-begin-line :background (color-darken-name (doom-color 'bg) 3)
    		       :foreground (doom-color 'grey)))

### Export

#### Hugo

A package for exporting Org to Hugo. That’s how I manage my sqrtminusone.xyz.

References:

- ox-hugo homepage
- Source code for sqrtminusone.xyz

    (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)
      :after ox)

#### Html export

    (use-package htmlize
      :straight t
      :after ox
      :if (not my/remote-server)
      :config
      (setq org-html-htmlize-output-type 'css))

#### 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-norem 'ox-latex
      (my/setup-org-latex))

### Keybindings & stuff

#### General keybindings

    (with-eval-after-load-norem '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
       "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-norem '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
    	  (append
    	   '("inbox.org" "contacts.org")
    	   (mapcar (lambda (f)
    		     (concat "projects/" f))
    		   (seq-filter
    		    (lambda (f) (not (member f '("." ".."))))
    		    (directory-files
    		     (concat org-directory "/projects")))))))
        (find-file
         (concat org-directory "/"
    	     (completing-read "Org file: " files)))))

    (my-leader-def
      "o o" 'my/org-file-open)

### 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
          '("/home/pavel/Emacs.org"
    	"/home/pavel/Desktop.org"
    	"/home/pavel/Console.org"
    	"/home/pavel/Guix.org"
    	"/home/pavel/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 tangle is run after `yadm alt`

## Applications

### Dired

Dired is a built-in file manager. 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)))
      (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
       "~" #'vterm
       "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)

#### 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
      :commands (dired-recent-open)
      :config
      (dired-recent-mode)
      (general-define-key
       :keymaps 'dired-recent-mode-map
       "C-x C-d" nil)
      (my-leader-def
        "aD" '(dired-recent-open :wk "dired history")))

Reuse the current dired buffer instead of spamming new ones. Looks like not necessary with Emacs 28.1

    (use-package dired-single
      :after dired
      :disabled
      :straight t)

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
      :if (not (or my/slow-ssh (not (display-graphic-p))))
      :hook (dired-mode . (lambda ()
    			(unless (string-match-p "/gnu/store" default-directory)
    			  (all-the-icons-dired-mode))))
      :config
      ;; (advice-add 'dired-add-entry :around #'all-the-icons-dired--propertize)
      ;; (advice-add 'dired-remove-entry :around #'all-the-icons-dired--propertize)
      ;; (advice-add 'dired-kill-subdir :around #'all-the-icons-dired--propertize)
      )

Provides stuff like `dired-open-xdg`

    (use-package dired-open
      :straight t
      :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
      :if (not my/slow-ssh)
      :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))

#### 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))

#### TRAMP

TRAMP is a package that provides remote editing capacities. It is particularly useful for remote server management.

One of the reasons why TRAMP may be slow is that some plugins do too many requests to the filesystem. To debug these issues, set the following variable to 6:

    (setq tramp-verbose 1)

To check if a file is remote, you can use `file-remote-p`. E.g. `(file-remote-p default-directory)` for a current buffer. The problem with this approach is that it’s rather awkward to add these checks in every hook, especially for global modes, so for now, I just set an environment variable for Emacs which disables these modes.

So far I have found the following problematic plugins:

Plugin | Note | Solution
--- | --- | ---
editorconfig | looks for .editorconfig in the file tree | do not enable globally
all-the-icons-dired | runs test on every file in the directory | disable
projectile | looks for .git, .svn, etc | advice `projectile-file-name`
lsp | does a whole lot of stuff | disable
git-gutter | runs git | disable
vterm | no proper TRAMP integration | use eshell or shell

At any rate, it’s usable, although not perfect.

Some other optimization settings:

    (setq remote-file-name-inhibit-cache nil)
    (setq vc-ignore-dir-regexp
          (format "\\(%s\\)\\|\\(%s\\)"
    	      vc-ignore-dir-regexp
    	      tramp-file-name-regexp))

Set the default shell to `bin/bash` for TRAMP or on a remote server.

    (when (or my/remote-server my/slow-ssh)
      (setq explicit-shell-file-name "/bin/bash"))

Also, here is a hack to make TRAMP find `ls` on Guix:

    (with-eval-after-load 'tramp
      (setq tramp-remote-path
    	(append tramp-remote-path
    		'(tramp-own-remote-path))))

#### 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
          '(("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)))))

### Shells

#### vterm

My terminal emulator of choice.

References:

- emacs-libvterm repo

##### Configuration

I use the package from the Guix repository to avoid building libvterm.

    (use-package vterm
      ;; :straight t
      :commands (vterm vterm-other-window)
      :config
      (setq vterm-kill-buffer-on-exit t)

      (add-hook 'vterm-mode-hook
    	    (lambda ()
    	      (setq-local global-display-line-numbers-mode nil)
    	      (display-line-numbers-mode 0)))

      (advice-add 'evil-collection-vterm-insert
    	      :before (lambda (&rest args)
    			(ignore-errors
    			  (apply #'vterm-reset-cursor-point args))))

      (general-define-key
       :keymaps 'vterm-mode-map
       "M-q" 'vterm-send-escape

       "C-h" 'evil-window-left
       "C-l" 'evil-window-right
       "C-k" 'evil-window-up
       "C-j" 'evil-window-down

       "C-<right>" 'evil-window-right
       "C-<left>" 'evil-window-left
       "C-<up>" 'evil-window-up
       "C-<down>" 'evil-window-down

       "M-<left>" 'vterm-send-left
       "M-<right>" 'vterm-send-right
       "M-<up>" 'vterm-send-up
       "M-<down>" 'vterm-send-down)

      (general-define-key
       :keymaps 'vterm-mode-map
       :states '(normal insert)
       "<home>" 'vterm-beginning-of-line
       "<end>" 'vterm-end-of-line)

      (general-define-key
       :keymaps 'vterm-mode-map
       :states '(insert)
       "C-r" 'vterm-send-C-r
       "C-k" 'vterm-send-C-k
       "C-j" 'vterm-send-C-j
       "M-l" 'vterm-send-right
       "M-h" 'vterm-send-left
       "M-k" 'vterm-send-up
       "M-j" 'vterm-send-down))

##### Subterminal

Open a terminal in the lower third of the frame with the ``` key.

    (add-to-list 'display-buffer-alist
    	     `(,"vterm-subterminal.*"
    	       (display-buffer-reuse-window
    		display-buffer-in-side-window)
    	       (side . bottom)
    	       (reusable-frames . visible)
    	       (window-height . 0.33)))

    (defun my/toggle-vterm-subteminal ()
      "Toogle subteminal."
      (interactive)
      (let
          ((vterm-window
    	(seq-find
    	 (lambda (window)
    	   (string-match
    	    "vterm-subterminal.*"
    	    (buffer-name (window-buffer window))))
    	 (window-list))))
        (if vterm-window
    	(if (eq (get-buffer-window (current-buffer)) vterm-window)
    	    (kill-buffer (current-buffer))
    	  (select-window vterm-window))
          (vterm-other-window "vterm-subterminal"))))

    (unless my/slow-ssh
      (general-nmap "`" 'my/toggle-vterm-subteminal)
      (general-nmap "~" 'vterm))

##### Dired integration

A function to get pwd for vterm. Couldn’t find a built-in function for some reason, but this seems to be working fine:

    (defun my/vterm-get-pwd ()
      (if vterm--process
          (file-truename (format "/proc/%d/cwd" (process-id vterm--process)))
        default-directory))

Now we can open dired for vterm pwd:

    (defun my/vterm-dired-other-window ()
      "Open dired in vterm pwd in other window"
      (interactive)
      (dired-other-window (my/vterm-get-pwd)))

    (defun my/vterm-dired-replace ()
      "Replace vterm with dired"
      (interactive)
      (let ((pwd (my/vterm-get-pwd)))
        (kill-process vterm--process)
        (dired pwd)))

The second function is particularly handy because that way I can alternate between vterm and dired.

Keybindings:

    (with-eval-after-load 'vterm
      (general-define-key
       :keymaps 'vterm-mode-map
       :states '(normal)
       "gd" #'my/vterm-dired-other-window
       "gD" #'my/vterm-dired-replace))

##### With-editor integration

A package used by Magit to use the current Emacs instance as the `$EDITOR`.

That is, with the help of this function, I can just write `e <filename>`, edit the file, and then return to the same vterm buffer. No more running vim inside Emacs.

    (use-package with-editor
      :straight t
      :after (vterm)
      :config
      (add-hook 'vterm-mode-hook 'with-editor-export-editor))

#### Eshell

A shell written in Emacs lisp. I don’t use it as of now, but keep the config just in case.

    (defun my/configure-eshell ()
      (add-hook 'eshell-pre-command-hook 'eshell-save-some-history)
      (add-to-list 'eshell-output-filter-functions 'eshell-truncate-buffer)
      (setq eshell-history-size 10000)
      (setq eshell-hist-ingnoredups t)
      (setq eshell-buffer-maximum-lines 10000)

      (evil-define-key '(normal insert visual) eshell-mode-map (kbd "<home>") 'eshell-bol)
      (evil-define-key '(normal insert visual) eshell-mode-map (kbd "C-r") 'counsel-esh-history)
      (general-define-key
       :states '(normal)
       :keymaps 'eshell-mode-map
       (kbd "C-h") 'evil-window-left
       (kbd "C-l") 'evil-window-right
       (kbd "C-k") 'evil-window-up
       (kbd "C-j") 'evil-window-down))

    (use-package eshell
      :ensure nil
      :after evil-collection
      :commands (eshell)
      :init
      (my/use-doom-colors
       (epe-pipeline-delimiter-face :foreground (doom-color 'green))
       (epe-pipeline-host-face      :foreground (doom-color 'blue))
       (epe-pipeline-time-face      :foreground (doom-color 'yellow))
       (epe-pipeline-user-face      :foreground (doom-color 'red)))
      :config
      (add-hook 'eshell-first-time-mode-hook 'my/configure-eshell 90)
      (when my/slow-ssh
        (add-hook 'eshell-mode-hook
    	      (lambda ()
    		(setq-local company-idle-delay 1000))))
      (setq eshell-banner-message ""))

    (use-package aweshell
      :straight (:repo "manateelazycat/aweshell" :host github)
      :after eshell
      :init
      (my/use-doom-colors
       (aweshell-alert-buffer-face  :background (color-darken-name (doom-color 'bg) 3))
       (aweshell-alert-command-face :foreground (doom-color 'red) :weight 'bold))
      :config
      (setq eshell-prompt-regexp "^[^#\nλ]* λ[#]* ")
      (setq eshell-highlight-prompt t)
      (setq eshell-prompt-function 'epe-theme-pipeline))

    (use-package eshell-info-banner
      :defer t
      :if (not my/slow-ssh)
      :straight (eshell-info-banner :type git
    				:host github
    				:repo "phundrak/eshell-info-banner.el")
      :hook (eshell-banner-load . eshell-info-banner-update-banner)
      :config
      (setq eshell-info-banner-filter-duplicate-partitions t)
      (setq eshell-info-banner-exclude-partitions '("b/efi")))

    (when (or my/slow-ssh my/remote-server)
      (general-nmap "`" 'aweshell-dedicated-toggle)
      (general-nmap "~" 'eshell))

#### Shell

Interactive subshell (`M-x shell`) is a way to run commands with input and output through an Emacs buffer.

    (defun my/setup-shell ()
      (setq-local comint-use-prompt-regexp t)
      (setq-local comint-prompt-read-only t))

    (add-hook 'shell-mode-hook #'my/setup-shell)

### Managing dotfiles

A bunch of functions for managing dotfiles with yadm.

#### Open Emacs config

    (general-define-key
     ;; "C-c c" (my/command-in-persp "Emacs.org" "conf" 1 (find-file "~/Emacs.org"))
     "C-c c" `(,(lambda () (interactive) (find-file "~/Emacs.org")) :wk "Emacs.org"))

    (my-leader-def
      :infix "c"
      "" '(:which-key "configuration")
      ;; "c" (my/command-in-persp "Emacs.org" "conf" 1 (find-file "~/Emacs.org"))
      "c" `(,(lambda () (interactive) (find-file "~/Emacs.org")) :wk "Emacs.org"))

#### Open Magit for yadm

Idea:

- https://www.reddit.com/r/emacs/comments/gjukb3/yadm_magit/

    (with-eval-after-load 'tramp
      (add-to-list 'tramp-methods
    	       `("yadm"
    		 (tramp-login-program "yadm")
    		 (tramp-login-args (("enter")))
    		 (tramp-login-env (("SHELL") "/bin/sh"))
    		 (tramp-remote-shell "/bin/sh")
    		 (tramp-remote-shell-args ("-c")))))

    (defun my/yadm-magit ()
      (interactive)
      (magit-status "/yadm::"))

    (my-leader-def "cm" 'my/yadm-magit)

#### Open a dotfile

Open a file managed by yadm.

    (defun my/open-yadm-file ()
      "Open a file managed by yadm"
      (interactive)
      (find-file
       (concat
        (file-name-as-directory (getenv "HOME"))
        (completing-read
         "yadm files: "
         (split-string
          (shell-command-to-string "yadm ls-files $HOME --full-name") "\n")))))

    (general-define-key
     "C-c f" '(my/open-yadm-file :wk "yadm file"))

    (my-leader-def
      "cf" '(my/open-yadm-file :wk "yadm file"))

### Elfeed

elfeed is one of the most popular Emacs packages, and it’s also one in which I ended up investing a lot of effort.

There’s a lot of stuff in this section, so it’s here and not in “Internet and Multimedia”.

#### General settings

The advice there sets `shr-use-fonts` to nil while rendering HTML, so the `elfeed-show` buffer will use monospace font.

Using my own fork until the modifications are merged into master.

    (use-package elfeed
      :straight (:repo "SqrtMinusOne/elfeed" :host github)
      :if (not my/remote-server)
      :commands (elfeed)
      :init
      (my-leader-def "ae" #'elfeed-summary)
      (my/persp-add-rule
        elfeed-summary-mode 0 "elfeed"
        elfeed-search-mode 0 "elfeed"
        elfeed-show-mode 0 "elfeed")
      (setq shr-max-image-proportion 0.5)
      :config
      (setq elfeed-db-directory "~/.elfeed")
      (setq elfeed-enclosure-default-dir (expand-file-name "~/Downloads"))
      (advice-add #'elfeed-insert-html
    	      :around
    	      (lambda (fun &rest r)
    		(let ((shr-use-fonts nil))
    		  (apply fun r))))
      (general-define-key
       :states '(normal)
       :keymaps 'elfeed-search-mode-map
       "o" #'my/elfeed-search-filter-source
       "c" #'elfeed-search-clear-filter
       "gl" (lambda () (interactive) (elfeed-search-set-filter "+later")))
      (general-define-key
       :states '(normal)
       :keymaps 'elfeed-show-mode-map
       "ge" #'my/elfeed-show-visit-eww))

elfeed-org allows configuring Elfeed feeds with an Org file.

    (use-package elfeed-org
      :straight t
      :after (elfeed)
      :config
      (setq rmh-elfeed-org-files '("~/.emacs.d/private.org"))
      (elfeed-org))

#### Some additions

Filter elfeed search buffer by the feed under the cursor.

    (defun my/elfeed-search-filter-source (entry)
      "Filter elfeed search buffer by the feed under cursor."
      (interactive (list (elfeed-search-selected :ignore-region)))
      (when (elfeed-entry-p entry)
        (elfeed-search-set-filter
         (concat
          "@6-months-ago "
          "+unread "
          "="
          (replace-regexp-in-string
           (rx "?" (* not-newline) eos)
           ""
           (elfeed-feed-url (elfeed-entry-feed entry)))))))

Open a URL with eww.

    (defun my/elfeed-show-visit-eww ()
      "Visit the current entry in eww"
      (interactive)
      (let ((link (elfeed-entry-link elfeed-show-entry)))
        (when link
          (eww link))))

#### Custom faces

Setting up custom faces for certain tags to make the feed look a bit nicer.

    (defface elfeed-videos-entry nil
      "Face for the elfeed entries with tag \"videos\"")

    (defface elfeed-twitter-entry nil
      "Face for the elfeed entries with tah \"twitter\"")

    (defface elfeed-emacs-entry nil
      "Face for the elfeed entries with tah \"emacs\"")

    (defface elfeed-music-entry nil
      "Face for the elfeed entries with tah \"music\"")

    (defface elfeed-podcasts-entry nil
      "Face for the elfeed entries with tag \"podcasts\"")

    (defface elfeed-blogs-entry nil
      "Face for the elfeed entries with tag \"blogs\"")

    (defface elfeed-govt-entry nil
      "Face for the elfeed entries with tag \"blogs\"")

    (my/use-doom-colors
     (elfeed-search-tag-face :foreground (doom-color 'yellow))
     (elfeed-videos-entry :foreground (doom-color 'red))
     (elfeed-twitter-entry :foreground (doom-color 'blue))
     (elfeed-emacs-entry :foreground (doom-color 'magenta))
     (elfeed-music-entry :foreground (doom-color 'green))
     (elfeed-podcasts-entry :foreground (doom-color 'yellow))
     (elfeed-blogs-entry :foreground (doom-color 'orange))
     (elfeed-govt-entry :foreground (doom-color 'dark-cyan)))

    (with-eval-after-load 'elfeed
      (setq elfeed-search-face-alist
    	'((podcasts elfeed-podcasts-entry)
    	  (music elfeed-music-entry)
    	  (gov elfeed-govt-entry)
    	  (twitter elfeed-twitter-entry)
    	  (videos elfeed-videos-entry)
    	  (emacs elfeed-emacs-entry)
    	  (blogs elfeed-blogs-entry)
    	  (unread elfeed-search-unread-title-face))))

Also, a function to automatically adjust these colors with the Doom theme.

    (defun my/update-my-theme-elfeed (&rest _)
      (custom-theme-set-faces
       'my-theme-1
       `(elfeed-videos-entry ((t :foreground ,(doom-color 'red))))
       `(elfeed-twitter-entry ((t :foreground ,(doom-color 'blue))))
       `(elfeed-emacs-entry ((t :foreground ,(doom-color 'magenta))))
       `(elfeed-music-entry ((t :foreground ,(doom-color 'green))))
       `(elfeed-podcasts-entry ((t :foreground ,(doom-color 'yellow))))
       `(elfeed-blogs-entry ((t :foreground ,(doom-color 'orange)))))
      (enable-theme 'my-theme-1))

    (advice-add 'load-theme :after #'my/update-my-theme-elfeed)
    (when (fboundp 'doom-color)
      (my/update-my-theme-elfeed))

#### elfeed-summary

elfeed-summary is my package that provides a feed summary interface for elfeed.

The default interface of elfeed is just a list of all entries, so it gets hard to navigate when there are a lot of sources with varying frequencies of posts. This is my attempt to address this issue.

    (use-package elfeed-summary
      :commands (elfeed-summary)
      :straight t
      :config
      (setq elfeed-summary-filter-by-title t))

#### elfeed-sync

elfeed-sync is my package to sync elfeed with tt-rss.

    (use-package elfeed-sync
      :straight (:host github :repo "SqrtMinusOne/elfeed-sync")
      :after elfeed
      :config
      (elfeed-sync-mode)
      (setq elfeed-sync-tt-rss-instance "https://sqrtminusone.xyz/tt-rss")
      (setq elfeed-sync-tt-rss-login "sqrtminusone")
      (setq elfeed-sync-tt-rss-password (my/password-store-get "Selfhosted/tt-rss")))

#### YouTube, podcasts & EMMS

Previously this block was opening MPV with `start-process`, but now I’ve managed to hook up MPV with EMMS. So there is the EMMS+elfeed “integration”.

There are multiple kinds of entries that I want to be opened by EMMS. First, a function that returns a YouTube URL:

    (defun my/get-youtube-url (entry)
      (let ((watch-id (cadr
    		   (assoc "watch?v"
    			  (url-parse-query-string
    			   (substring
    			    (url-filename
    			     (url-generic-parse-url (elfeed-entry-link entry)))
    			    1))))))
        (when watch-id
          (concat "https://www.youtube.com/watch?v=" watch-id))))

Second, a function that returns a URL to an enclosure. This is generally how podcasts are distributed.

    (defun my/get-enclosures-url (entry)
      (caar (elfeed-entry-enclosures entry)))

Now, a function to add a YouTube link with metadata from elfeed to EMMS.

    (with-eval-after-load 'emms
      (define-emms-source elfeed (entry)
        (let ((url (or (my/get-enclosures-url entry)
    		   (my/get-youtube-url entry))))
          (unless url
    	(error "URL not found"))
          (let ((track (emms-track 'url url)))
    	(emms-track-set track 'info-title (elfeed-entry-title entry))
    	(emms-playlist-insert-track track)))))

    (defun my/elfeed-add-emms ()
      (interactive)
      (emms-add-elfeed elfeed-show-entry)
      (elfeed-tag elfeed-show-entry 'watched)
      (elfeed-show-refresh))

    (with-eval-after-load 'elfeed
      (general-define-key
       :states '(normal)
       :keymaps 'elfeed-show-mode-map
       "gm" #'my/elfeed-add-emms))

#### rdrview

rdrview is a command-line tool to strip webpages from clutter, extracting only parts related to the actual content. It’s a standalone port of the corresponding feature of Firefox, called Reader View.

Guix dependency
rdrview

It seems like the tool isn’t available in a whole lot of package repositories, but it’s pretty easy to compile. I’ve put together a Guix definition, which one day I’ll submit to upstream.

##### Integrating rdrview with Emacs

Let’s start by integrating `rdrview` with Emacs. In the general case, we want to fetch both metadata and the actual content from the page.

However, the interface of `rdrview` is a bit awkward in this part, so we have the following options:

- call `rdrview` two times: with `-M` flag to fetch the metadata, and without the flag to fetch the HTML;
- call `rdrview` with `-T` flag to append the metadata to the resulting HTML.

I’ve decided to go with the second option. Here is a function that calls rdrview with the required flags:

    (defun my/rdrview-get (url callback)
      "Get the rdrview representation of URL.

    Call CALLBACK with the output."
      (let* ((buffer (generate-new-buffer "rdrview"))
    	 (proc (start-process "rdrview" buffer "rdrview"
    			      url "-T" "title,sitename,body"
    			      "-H")))
        (set-process-sentinel
         proc
         (lambda (process _msg)
           (let ((status (process-status process))
    	     (code (process-exit-status process)))
    	 (cond ((and (eq status 'exit) (= code 0))
    		(progn
    		  (funcall callback
    			   (with-current-buffer (process-buffer process)
    			     (buffer-string)))
    		  (kill-buffer (process-buffer process))) )
    	       ((or (and (eq status 'exit) (> code 0))
    		    (eq status 'signal))
    		(let ((err (with-current-buffer (process-buffer process)
    			     (buffer-string))))
    		  (kill-buffer (process-buffer process))
    		  (user-error "Error in rdrview: %s" err)))))))
        proc))

The function calls `callback` with the output of `rdrview`. This usually doesn’t take long, but it’s still nice to avoid freezing Emacs that way.

Now we have to parse the output. The `-T` flag puts the title in the `<h1>` tag, the site name site in the `<h2>` tag, and the content in a `<div>`. What’s more, headers of the content are often shifted, e.g. the top-level header may well end up being and `<h2>` or `<h3>`, which does not look great in LaTeX.

With that said, here’s a function that does the required changes:

    (defun my/rdrview-parse (dom-string)
      (let ((dom (with-temp-buffer
    	       (insert dom-string)
    	       (libxml-parse-html-region (point-min) (point-max)))))
        (let (title sitename content (i 0))
          (dolist (child (dom-children (car (dom-by-id dom "readability-page-1"))))
    	(when (listp child)
    	  (cond
    	   ((eq (car child) 'h1)
    	    (setq title (dom-text child)))
    	   ((eq (car child) 'h2)
    	    (setq sitename (dom-text child)))
    	   ((eq (car child) 'div)
    	    (setq content child)))))
          (while (and
    	      (not (dom-by-tag content 'h1))
    	      (dom-search
    	       content
    	       (lambda (el)
    		 (when (listp el)
    		   (pcase (car el)
    		     ('h2 (setf (car el) 'h1))
    		     ('h3 (setf (car el) 'h2))
    		     ('h4 (setf (car el) 'h3))
    		     ('h5 (setf (car el) 'h4))
    		     ('h6 (setf (car el) 'h5))))))))
          `((title . ,title)
    	(sitename . ,sitename)
    	(content . ,(with-temp-buffer
    		      (dom-print content)
    		      (buffer-string)))))))

##### Using rdrview from elfeed

Because I didn’t find a smart way to advise the desired behavior into elfeed, here’s a modification of the `elfeed-show-refresh--mail-style` function with two changes:

- it uses `rdrview` to fetch the HTML;
- it saves the resulting HTML into a buffer-local variable (we’ll need that later).

    (defvar-local my/elfeed-show-rdrview-html nil)

    (defun my/rdrview-elfeed-show ()
      (interactive)
      (unless elfeed-show-entry
        (user-error "No elfeed entry in this buffer!"))
      (my/rdrview-get
       (elfeed-entry-link elfeed-show-entry)
       (lambda (result)
         (let* ((data (my/rdrview-parse result))
    	    (inhibit-read-only t)
    	    (title (elfeed-entry-title elfeed-show-entry))
    	    (date (seconds-to-time (elfeed-entry-date elfeed-show-entry)))
    	    (authors (elfeed-meta elfeed-show-entry :authors))
    	    (link (elfeed-entry-link elfeed-show-entry))
    	    (tags (elfeed-entry-tags elfeed-show-entry))
    	    (tagsstr (mapconcat #'symbol-name tags ", "))
    	    (nicedate (format-time-string "%a, %e %b %Y %T %Z" date))
    	    (content (alist-get 'content data))
    	    (feed (elfeed-entry-feed elfeed-show-entry))
    	    (feed-title (elfeed-feed-title feed))
    	    (base (and feed (elfeed-compute-base (elfeed-feed-url feed)))))
           (erase-buffer)
           (insert (format (propertize "Title: %s\n" 'face 'message-header-name)
    		       (propertize title 'face 'message-header-subject)))
           (when elfeed-show-entry-author
    	 (dolist (author authors)
    	   (let ((formatted (elfeed--show-format-author author)))
    	     (insert
    	      (format (propertize "Author: %s\n" 'face 'message-header-name)
    		      (propertize formatted 'face 'message-header-to))))))
           (insert (format (propertize "Date: %s\n" 'face 'message-header-name)
    		       (propertize nicedate 'face 'message-header-other)))
           (insert (format (propertize "Feed: %s\n" 'face 'message-header-name)
    		       (propertize feed-title 'face 'message-header-other)))
           (when tags
    	 (insert (format (propertize "Tags: %s\n" 'face 'message-header-name)
    			 (propertize tagsstr 'face 'message-header-other))))
           (insert (propertize "Link: " 'face 'message-header-name))
           (elfeed-insert-link link link)
           (insert "\n")
           (cl-loop for enclosure in (elfeed-entry-enclosures elfeed-show-entry)
    		do (insert (propertize "Enclosure: " 'face 'message-header-name))
    		do (elfeed-insert-link (car enclosure))
    		do (insert "\n"))
           (insert "\n")
           (if content
    	   (elfeed-insert-html content base)
    	 (insert (propertize "(empty)\n" 'face 'italic)))
           (setq-local my/elfeed-show-rdrview-html content)
           (goto-char (point-min))))))

That way, calling `M-x my/rdrview-elfeed-show` replaces the original content with one from `rdrview`.

    (with-eval-after-load 'elfeed
      (general-define-key
       :states '(normal)
       :keymaps 'elfeed-show-mode-map
       "gp" #'my/rdrview-elfeed-show))

##### How well does it work?

Rather ironically, it works well with sites that already ship with proper RSS, like Protesilaos Stavrou’s or Karthik Chikmagalur’s blogs or The Atlantic magazine.

Of my other subscriptions, it does a pretty good job with The Verge, which by default sends entries truncated by the words “Read the full article”. For Ars Technica, it works only if the story is not large enough, otherwise the site returns its HTML-based pagination interface.

For paywalled sites such as New York Times or The Economist, this usually doesn’t work (by the way, what’s the problem with providing individual RSS feeds for subscribers?). If you need this kind of thing, I’d suggest using the RSS-Bridge project. And if something is not available, contributing business logic there definitely makes more sense than implementing workarounds in Emacs Lisp.

#### LaTeX and pandoc

However, I also find that I’m not really a fan of reading articles from Emacs. Somehow what works for program code doesn’t work that well for natural text. When I have to, I usually switch the Emacs theme to a light one.

But the best solution I’ve found so far is to render the required articles as PDFs. I may even print out some large articles I want to read.

##### Template

So first, we need a LaTeX template. Pandoc already ships with one, but I don’t like it too much, so I’ve put up a template from my LaTeX styles, targeting my preferred XeLaTeX engine.

The code for the template is available dotfiles repo. If you use LaTeX, you’ll probably be better off using your own setup. Be sure to define the following variables:

- `main-lang` and `other-lang` for polyglossia (or remove them if you have only one language)
- `title`
- `subtitle`
- `author`
- `date`

##### Invoking pandoc

Now that we have the template, let’s save it somewhere and store the path to a variable:

    (setq my/rdrview-template (expand-file-name
    			   (concat user-emacs-directory "rdrview.tex")))

And let’s invoke pandoc. We need to pass the following flags:

- `--pdf-engine=xelatex`, of course
- `--template <path-to-template>`;
- `-o <path-to-pdf>`;
- `--variable key=value`.

In fact, pandoc is a pretty awesome tool in the sense that it allows for feeding custom variables to rich-language templates.

So, the rendering function is as follows:

    (cl-defun my/rdrview-render (content type variables callback
    				     &key file-name overwrite)
      "Render CONTENT with pandoc.

    TYPE is a file extension as supported by pandoc, for instance,
    html or txt.  VARIABLES is an alist that is fed into the
    template.  After the rendering is complete successfully, CALLBACK
    is called with the resulting PDF.

    FILE-NAME is a path to the resulting PDF. If nil it's generated
    randomly.

    If a file with the given FILE-NAME already exists, the function will
    invoke CALLBACK straight away without doing the rendering, unless
    OVERWRITE is non-nil."
      (unless file-name
        (setq file-name (format "/tmp/%d.pdf" (random 100000000))))
      (let (params
    	(temp-file-name (format "/tmp/%d.%s" (random 100000000) type)))
        (cl-loop for (key . value) in variables
    	     when value
    	     do (progn
    		  (push "--variable" params)
    		  (push (format "%s=%s" key value) params)))
        (setq params (nreverse params))
        (if (and (file-exists-p file-name) (not overwrite))
    	(funcall callback file-name)
          (with-temp-file temp-file-name
    	(insert content))
          (let ((proc (apply #'start-process
    			 "pandoc" (get-buffer-create "*Pandoc*") "pandoc"
    			 temp-file-name "-o" file-name
    			 "--pdf-engine=xelatex" "--template" my/rdrview-template
    			 params)))
    	(set-process-sentinel
    	 proc
    	 (lambda (process _msg)
    	   (let ((status (process-status process))
    		 (code (process-exit-status process)))
    	     (cond ((and (eq status 'exit) (= code 0))
    		    (progn
    		      (message "Done!")
    		      (funcall callback file-name)))
    		   ((or (and (eq status 'exit) (> code 0))
    			(eq status 'signal))
    		    (user-error "Error in pandoc. Check the *Pandoc* buffer"))))))))))

##### Opening elfeed entries

Now we have everything required to open elfeed entries.

Also, in my case elfeed entries come in two languages, so I have to set `main-lang` and `other-lang` variables accordingly. Here’s the main function:

    (setq my/elfeed-pdf-dir (expand-file-name "~/.elfeed/pdf/"))

    (defun my/elfeed-open-pdf (entry overwrite)
      "Open the current elfeed ENTRY with a pdf viewer.

    If OVERWRITE is non-nil, do the rendering even if the resulting
    PDF already exists."
      (interactive (list elfeed-show-entry current-prefix-arg))
      (let ((authors (mapcar (lambda (m) (plist-get m :name)) (elfeed-meta entry :authors)))
    	(feed-title (elfeed-feed-title (elfeed-entry-feed entry)))
    	(tags (mapconcat #'symbol-name (elfeed-entry-tags entry) ", "))
    	(date (format-time-string "%a, %e %b %Y"
    				  (seconds-to-time (elfeed-entry-date entry))))
    	(content (elfeed-deref (elfeed-entry-content entry)))
    	(file-name (concat my/elfeed-pdf-dir
    			   (elfeed-ref-id (elfeed-entry-content entry))
    			   ".pdf"))
    	(main-language "english")
    	(other-language "russian"))
        (unless content
          (user-error "No content!"))
        (setq subtitle
    	  (cond
    	   ((seq-empty-p authors) feed-title)
    	   ((and (not (seq-empty-p (car authors)))
    		 (string-match-p (regexp-quote (car authors)) feed-title)) feed-title)
    	   (t (concat (string-join authors ", ") "\\\\" feed-title))))
        (when (member 'ru (elfeed-entry-tags entry))
          (setq main-language "russian")
          (setq other-language "english"))
        (my/rdrview-render
         (if (bound-and-true-p my/elfeed-show-rdrview-html)
    	 my/elfeed-show-rdrview-html
           content)
         (elfeed-entry-content-type entry)
         `((title . ,(elfeed-entry-title entry))
           (subtitle . ,subtitle)
           (date . ,date)
           (tags . ,tags)
           (main-lang . ,main-language)
           (other-lang . ,other-language))
         (lambda (file-name)
           (start-process "xdg-open" nil "xdg-open" file-name))
         :file-name file-name
         :overwrite current-prefix-arg)))

If the `my/elfeed-show-rdrview-html` variable is bound and true, then the content in this buffer was retrieved via `rdrview`, so we’ll use that instead of the output of `elfeed-deref`.

    (with-eval-after-load 'elfeed
      (general-define-key
       :keymaps '(elfeed-show-mode-map)
       :states '(normal)
       "gv" #'my/elfeed-open-pdf))

Now we can open elfeed entries in a PDF viewer, which I find much nicer to read. Given that RSS feeds generally ship with simpler HTML than the regular websites, results usually look awesome.

##### Opening arbitrary sites

As you may have noticed, we also can display arbitrary web pages with this setup, so let’s go ahead and implement that:

    (defun my/get-languages (url)
      (let ((main-lang "english")
    	(other-lang "russian"))
        (when (string-match-p (rx ".ru") url)
          (setq main-lang "russian"
    	    other-lang "english"))
        (list main-lang other-lang)))

    (defun my/rdrview-open (url overwrite)
      (interactive
       (let ((url (read-from-minibuffer
    	       "URL: "
    	       (if (bound-and-true-p elfeed-show-entry)
    		   (elfeed-entry-link elfeed-show-entry)))))
         (when (string-empty-p url)
           (user-error "URL is empty"))
         (list url current-prefix-arg)))
      (my/rdrview-get
       url
       (lambda (res)
         (let ((data (my/rdrview-parse res))
    	   (langs (my/get-languages url)))
           (my/rdrview-render
    	(alist-get 'content data)
    	'html
    	`((title . ,(alist-get 'title data))
    	  (subtitle . ,(alist-get 'sitename data))
    	  (main-lang . ,(nth 0 langs))
    	  (other-lang . ,(nth 1 langs)))
    	(lambda (file-name)
    	  (start-process "xdg-open" nil "xdg-open" file-name)))))))

Unfortunately, this part doesn’t work that well, so we can’t just uninstall Firefox or Chromium and browse the web from a PDF viewer.

The most common problem I’ve encountered is incorrectly formed pictures, such as `.png` files without the boundary info. I’m sure you’ve also come across this if you ever tried to insert a lot of Internet pictures into a LaTeX document.

However, sans the pictures issue, for certain sites like Wikipedia this is usable.

#### YouTube transcripts

##### Getting subtitles

Finally, let’s get to transcripts.

In principle, the YouTube API allows for downloading subtitles, but I’ve found this awesome Python script which does the same. You can install it from `pip`, or here’s mine Guix definition once again.

Much like the previous cases, we need to invoke the program and save the output. The WebVTT format will work well enough for our purposes. Here comes the function:

    (cl-defun my/youtube-subtitles-get (video-id callback &key file-name overwrite)
      "Get subtitles for VIDEO-ID in WebVTT format.

    Call CALLBACK when done.

    FILE-NAME is a path to the resulting WebVTT file. If nil it's
    generated randomly.

    If a file with the given FILE-NAME already exists, the function will
    invoke CALLBACK straight away without doing the rendering, unless
    OVERWRITE is non-nil."
      (unless file-name
        (setq file-name (format "/tmp/%d.vtt" (random 100000000))))
      (if (and (file-exists-p file-name) (not overwrite))
          (funcall callback file-name)
        (let* ((buffer (generate-new-buffer "youtube-transcripts"))
    	   (proc (start-process "youtube_transcript_api" buffer
    				"youtube_transcript_api" video-id
    				"--languages" "en" "ru" "de"
    				"--format" "webvtt")))
          (set-process-sentinel
           proc
           (lambda (process _msg)
    	 (let ((status (process-status process))
    	       (code (process-exit-status process)))
    	   (cond ((and (eq status 'exit) (= code 0))
    		  (progn
    		    (with-current-buffer (process-buffer process)
    		      (setq buffer-file-name file-name)
    		      (save-buffer))
    		    (kill-buffer (process-buffer process))
    		    (funcall callback file-name)))
    		 ((or (and (eq status 'exit) (> code 0))
    		      (eq status 'signal))
    		  (let ((err (with-current-buffer (process-buffer process)
    			       (buffer-string))))
    		    (kill-buffer (process-buffer process))
    		    (user-error "Error in youtube_transcript_api: %s" err)))))))
          proc)))

##### elfeed and subed

Now that we have a standalone function, let’s invoke it with the current `elfeed-show-entry`:

    (setq my/elfeed-srt-dir (expand-file-name "~/.elfeed/srt/"))

    (defun my/elfeed-youtube-subtitles (entry &optional arg)
      "Get subtitles for the current elfeed ENTRY.

    Works only in the entry is a YouTube video.

    If ARG is non-nil, re-fetch the subtitles regardless of whether
    they were fetched before."
      (interactive (list elfeed-show-entry current-prefix-arg))
      (let ((video-id (cadr
    		   (assoc "watch?v"
    			  (url-parse-query-string
    			   (substring
    			    (url-filename
    			     (url-generic-parse-url (elfeed-entry-link entry)))
    			    1))))))
        (unless video-id
          (user-error "Can't get video ID from the entry"))
        (my/youtube-subtitles-get
         video-id
         (lambda (file-name)
           (with-current-buffer (find-file-other-window file-name)
    	 (setq-local elfeed-show-entry entry)
    	 (goto-char (point-min))))
         :file-name (concat my/elfeed-srt-dir
    			(elfeed-ref-id (elfeed-entry-content entry))
    			".vtt")
         :overwrite arg)))

That opens up a `.vtt` buffer with the subtitles for the current video, which means now we can use the functionality of Sacha Chua’s awesome package called subed.

This package, besides syntax highlighting, allows for controlling the MPV playback, for instance by moving the cursor in the subtitles buffer. Using that requires having the URL of the video in this buffer, which necessitates the line with `setq-local` in the previous function.

Also, the package launches its own instance of MPV to control it via JSON-IPC, so there seems to be no easy way to integrate it with EMMS. But at least I can reuse the `emms-player-mpv-parameters` variable, the method of setting which I’ve discussed above. The function is as follows:

    (defun my/subed-elfeed (entry)
      "Open the video file from elfeed ENTRY in MPV.

    This has to be launched from inside the subtitles buffer, opened
    by the `my/elfeed-youtube-subtitles' function."
      (interactive (list elfeed-show-entry))
      (unless entry
        (user-error "No entry!"))
      (unless (derived-mode-p 'subed-mode)
        (user-error "Not subed mode!"))
      (setq-local subed-mpv-arguments
    	      (seq-uniq
    	       (append subed-mpv-arguments emms-player-mpv-parameters)))
      (setq-local subed-mpv-video-file (elfeed-entry-link entry))
      (subed-mpv--play subed-mpv-video-file))

Keep in mind that this function has to be launched inside the buffer opened by the `my/elfeed-youtube-subtitles` function.

#### Podcast transcripts

In my experience, finding something in a podcast is particularly troublesome. For example, occasionally I want to refer to some line in the podcast to make an org-roam node, e.g. I want to check that I got that part right.

And I have no reasonable way to get there because audio files in themselves don’t allow for random access, i.e. there are no “landmarks” that point to this or that portion of the file. At least if nothing like a transcript is available.

For obvious reasons, podcasts rarely ship with transcripts. So in this post, I’ll be using a speech recognition engine to make up for that. A generated transcript is not quite as good as a manually written one, but for the purpose of finding a fragment of a known podcast, it works well enough.

The general idea is to get the podcast info from elfeed, process it with OpenAI Whisper and feed it to subed to control the playback in MPV.

Edit <2022-10-08 Sat>: Changed vosk-api to OpenAI Whisper.

##### Whisper

OpenAI Whisper is an amazing speech recognition toolkit. It’s pretty slow on my PC (compared to vosk-api which I’ve been using before), but the quality is so much better so I think it’s completely worth it.

All we need to do is install Whisper in a virtual environment:

    python -m virtualenv venv
    source venv/bin/activate
    pip install whisper

After which the program can be used as follows:

    whisper <path-to-file> --output-dir <path-to-output-dir>

##### Running it from Emacs

Running the program from Emacs is rather straightforward with asyncronous processes.

I’m using an English-language-only model because that’s the only language I need at the moment.

    (defvar my/whisper-env-path
      "/home/pavel/Code/system-crafting/whisper-test/"
      "Path to the folder with `whisper' environment.")

    (defun my/invoke-whisper (input output-dir)
      "Extract subtitles from the audio file.

    INPUT is the audio file, OUTPUT-DIR is the path to the directory with
    resulting files."
      (interactive
       (list
        (read-file-name "Input file: " nil nil t)
        (read-directory-name "Output directory: ")))
      (let* ((buffer (generate-new-buffer "whisper"))
    	 (default-directory my/whisper-env-path)
    	 (proc (start-process
    		"whisper" buffer
    		(concat my/whisper-env-path "venv/bin/whisper")
    		"--output_dir" output-dir "--model" "tiny.en" input)))
        (set-process-sentinel
         proc
         (lambda (process _msg)
           (let ((status (process-status process))
    	     (code (process-exit-status process)))
    	 (cond ((and (eq status 'exit) (= code 0))
    		(notifications-notify :body "Audio conversion completed"
    				      :title "Whisper")
    		(kill-buffer (process-buffer process)))
    	       ((or (and (eq status 'exit) (> code 0))
    		    (eq status 'signal))
    		(let ((err (with-current-buffer (process-buffer process)
    			     (buffer-string))))
    		  (user-error "Error in Whisper: %s" err)))))))))

If run interactively, the defined function prompts for paths to both files.

The process sentinel sends a desktop notification because it’s a bit more noticeable than `message`, and the process is expected to take some time.

##### Integrating with elfeed

To actually run the function from the section above, we need to download the file in question.

The `whisper` executable, given the file `<file>.<extension>`, creates files named `<file>.vtt`, `<file>.srt`, `<file>.txt`. So first we need to save the file under the correct name.

I use a library called request.el to download files elsewhere, so I’ll re-use it here. You can just as well invoke `curl` or `wget` via a asynchronous process.

This function downloads the file to a non-temporary folder, which is `~/.elfeed/podcast-files/` if you didn’t move the elfeed database. That is so because a permanently downloaded file works better for the next section.

    (with-eval-after-load 'elfeed
      (defvar my/elfeed-whisper-podcast-files-directory
        (concat elfeed-db-directory "/podcast-files/")))

    (defun my/elfeed-whisper-get-transcript-new (entry)
      (interactive (list elfeed-show-entry))
      (let* ((url (caar (elfeed-entry-enclosures entry)))
    	 (file-name (concat
    		     (elfeed-ref-id (elfeed-entry-content entry))
    		     "."
    		     (file-name-extension url)))
    	 (file-path (expand-file-name
    		     (concat
    		      my/elfeed-whisper-podcast-files-directory
    		      file-name))))
        (message "Download started")
        (unless (file-exists-p my/elfeed-whisper-podcast-files-directory)
          (mkdir my/elfeed-whisper-podcast-files-directory))
        (request url
          :type "GET"
          :encoding 'binary
          :complete
          (cl-function
           (lambda (&key data &allow-other-keys)
    	 (let ((coding-system-for-write 'binary)
    	       (write-region-annotate-functions nil)
    	       (write-region-post-annotation-function nil))
    	   (write-region data nil file-path nil :silent))
    	 (message "Conversion started")
    	 (my/invoke-whisper file-path my/elfeed-srt-dir)))
          :error
          (cl-function
           (lambda (&key error-thrown &allow-other-keys)
    	 (message "Error!: %S" error-thrown))))))

I also experimented with a bunch of options to write binary data in Emacs, of which the way with `write-region` (as implemented in f.el) seems to be the fastest. This thread on StackExchange suggests that it may screw some bytes towards the end, but whether or not this is the case, mp3 files survive the procedure. The proposed solution with `seq-doseq` takes at least a few seconds.

As `my/invoke-whisper` creates multiple files, here’s a function to select related files:

    (defun my/elfeed-show-related-files (entry)
      (interactive (list elfeed-show-entry))
      (let* ((files
    	  (mapcar
    	   (lambda (file) (cons (file-name-extension file) file))
    	   (seq-filter
    	    (lambda (file)
    	      (string-match-p
    	       (rx bos (literal (elfeed-ref-id (elfeed-entry-content entry))) ".")
    	       file))
    	    (directory-files my/elfeed-srt-dir))))
    	 (buffer
    	  (find-file-other-window
    	   (concat
    	    my/elfeed-srt-dir
    	    (alist-get
    	     (completing-read "File: " files)
    	     files nil nil #'equal)))))
        (with-current-buffer buffer
          (setq-local elfeed-show-entry entry))))

Finally, we need a function to show the transcript if it exists or invoke `my/elfeed-whisper-get-transcript-new` if it doesn’t. And this is the function that we’ll call from an `elfeed-entry` buffer.

    (defun my/elfeed-whisper-get-transcript (entry)
      "Retrieve transcript for the enclosure of the current elfeed ENTRY."
      (interactive (list elfeed-show-entry))
      (let ((enclosure (caar (elfeed-entry-enclosures entry))))
        (unless enclosure
          (user-error "No enclosure found!"))
        (let ((srt-path (concat my/elfeed-srt-dir
    			    (elfeed-ref-id (elfeed-entry-content entry))
    			    ".srt")))
          (if (file-exists-p srt-path)
    	  (let ((buffer (find-file-other-window srt-path)))
    	    (with-current-buffer buffer
    	      (setq-local elfeed-show-entry entry)))
    	(my/elfeed-whisper-get-transcript-new entry)))))

##### Integrating with subed

Now that we’ve produced a `.srt` file, we can use a package called subed to control the playback, as I had done in the previous post.

By the way, this wasn’t the most straightforward thing to figure out, because the MPV window doesn’t show up for an audio file, and the player itself starts in the paused state. So I thought nothing was happening until I enabled the debug log.

With that in mind, here’s a function to launch MPV from the buffer generated by `my/elfeed-whisper-get-transcript`:

    (defun my/elfeed-whisper-subed (entry)
      "Run MPV for the current Whisper-generated subtitles file.

    ENTRY is an instance of `elfeed-entry'."
      (interactive (list elfeed-show-entry))
      (unless entry
        (user-error "No entry!"))
      (unless (derived-mode-p 'subed-mode)
        (user-error "Not subed mode!"))
      (setq-local subed-mpv-video-file
    	      (expand-file-name
    	       (concat my/elfeed-whisper-podcast-files-directory
    		       (my/get-file-name-from-url
    			(caar (elfeed-entry-enclosures entry))))))
      (subed-mpv--play subed-mpv-video-file))

After running `M-x my/elfeed-whisper-subed`, run `M-x subed-toggle-loop-over-current-subtitle` (`C-c C-l`), because somehow it’s turned on by default, and `M-x subed-toggle-pause-while-typing` (`C-c C-p`), because sometimes this made my instance of MPV lag.

After that, `M-x subed-mpv-toggle-pause` should start the playback, which you can control by moving the cursor in the buffer.

You can also run `M-x subed-toggle-sync-point-to-player` (`C-c .`) to toggle syncing the point in the buffer to the currently played subtitle (this automatically gets disabled when you switch buffers).

Running `M-x subed-toggle-sync-player-to-point` (`C-c ,`) does the opposite, i.e. sets the player position to the subtitle under point. These two functions are useful since the MPV window controls aren’t available.

##### Some observations

So, the functions above work for my purposes.

Vosk API works much faster than Whisper. The smallest Vosk model requires ~10 times less than the playback time, and even the `tiny.en` Whisper model on my PC requires maybe 1.2x playback time.

However, the quality of the output for Whisper is just so much better so I consider it to be worth the wait. Even with the `tiny` model, the transcript is almost perfect, provided that the audio is of reasonable quality.

### Internet & Multimedia

#### Notmuch

My notmuch config now resides in Mail.org.

    (unless (or my/is-termux my/remote-server)
      (let ((mail-file (expand-file-name "mail.el" user-emacs-directory)))
        (if (file-exists-p mail-file)
    	(load-file mail-file)
          (message "Can't load mail.el"))))

#### EMMS

EMMS is the Emacs Multi-Media System. I use it to control MPD & MPV.

References:

- EMMS Manual
- Uncle Dave’s video

    (use-package emms
      :straight t
      :if (not my/remote-server)
      :commands (emms-smart-browse
    	     emms-browser
    	     emms-add-url
    	     emms-add-file
    	     emms-add-find)
      :if (not my/is-termux)
      :init
      (my-leader-def
        :infix "as"
        "" '(:which-key "emms")
        "s" #'emms-smart-browse
        "b" #'emms-browser
        "p" #'emms-pause
        "q" #'emms-stop
        "h" #'emms-previous
        "l" #'emms-next
        "u" #'emms-player-mpd-connect
        "ww" #'emms-lyrics
        "wb" #'emms-lyrics-toggle-display-on-minibuffer
        "wm" #'emms-lyrics-toggle-display-on-modeline)
      (my/persp-add-rule
        emms-browser-mode 0 "EMMS"
        emms-playlist-mode 0 "EMMS")
      (setq emms-mode-line-icon-enabled-p nil)
      :config
      (require 'emms-setup)
      (require 'emms-player-mpd)
      (require 'emms-player-mpv)
      (emms-all)
      ;; MPD setup
      <<emms-mpd-setup>>
      ;; MPV setup
      <<emms-mpv-setup>>
      ;; evil-lion and evil-commentary shadow some gX bindings
      ;; (add-hook 'emms-browser-mode-hook
      ;; (lambda ()
      ;; (evil-lion-mode -1)
      ;; (evil-commentary-mode -1)
      ;; ))
      ;; I have everything I need in polybar
      (emms-mode-line-mode -1)
      (emms-playing-time-display-mode -1)
      <<emms-fixes>>)

##### MPD

mpd is a server for playing music. It has a couple of first-class clients, including curses-based ncmpcpp, but of course, I want to use Emacs.

    (setq emms-source-file-default-directory (expand-file-name "~/Music/"))
    (add-to-list 'emms-info-functions 'emms-info-mpd)
    (add-to-list 'emms-player-list 'emms-player-mpd)
    (setq emms-player-mpd-server-name "localhost")
    (setq emms-player-mpd-server-port "6600")
    (setq emms-player-mpd-music-directory "~/Music")

Connect on setup. For some reason, it stops the mpd playback whenever it connects, but it is not a big issue.

    (emms-player-mpd-connect)

Clear MPD playlist on clearing EMMS playlist. IDK if this is fine for MPD library playlist, I don’t use them anyhow.

    (add-hook 'emms-playlist-cleared-hook 'emms-player-mpd-clear)

Set a custom regex for MPD. EMMS sets up the default one from MPD’s diagnostic output so that regex opens basically everything, including videos, https links, etc. That is fine if MPD is the only player in EMMS, but as I want to use MPV as well, I override the regex.

    (emms-player-set emms-player-mpd
    		 'regex
    		 (rx (or (: "https://" (* nonl) (or "acast.com") (* nonl))
    			 (+ (? (or "https://" "http://"))
    			    (* nonl)
    			    (regexp (eval (emms-player-simple-regexp
    					   "m3u" "ogg" "flac" "mp3" "wav" "mod" "au" "aiff" "m4a")))))))

After all this is done, run `M-x emms-cache-set-from-mpd-all` to set cache from MPD. If everything is correct, EMMS browser will be populated with MPD database.

##### MPV

Guix dependency
mpv
yt-dlp

mpv is a decent media player, which has found a place in this configuration because it integrates with youtube-dl yt-dlp.

It looks like YouTube has started to throttle youtube-dl, and yt-dlp has a workaround for that particular case. Just don’t forget to add the following like to the mpv config:

    script-opts=ytdl_hook-ytdl_path=yt-dlp

It seems a bit strange to keep the MPV config in this file, but I don’t use the program outside Emacs.

    (add-to-list 'emms-player-list 'emms-player-mpv)

Also a custom regex. My demands for MPV include running `yt-dlp`, so there is a regex that matches youtube.com or some of the video formats.

    (emms-player-set emms-player-mpv
    		 'regex
    		 (rx (or (: "https://" (* nonl) "youtube.com" (* nonl))
    			 (+ (? (or "https://" "http://"))
    			    (* nonl)
    			    (regexp (eval (emms-player-simple-regexp
    			    "mp4" "mov" "wmv" "webm" "flv" "avi" "mkv")))))))

By default, MPV plays the video in the best possible quality, which may be pretty high, even too high with limited bandwidth. So here is the logic to choose the quality.

    (setq my/youtube-dl-quality-list
          '("bestvideo[height<=720]+bestaudio/best[height<=720]"
    	"bestvideo[height<=480]+bestaudio/best[height<=480]"
    	"bestvideo[height<=1080]+bestaudio/best[height<=1080]"))

    (setq my/default-emms-player-mpv-parameters
          '("--quiet" "--really-quiet" "--no-audio-display"))

    (defun my/set-emms-mpd-youtube-quality (quality)
      (interactive "P")
      (unless quality
        (setq quality (completing-read "Quality: " my/youtube-dl-quality-list nil t)))
      (setq emms-player-mpv-parameters
    	`(,@my/default-emms-player-mpv-parameters ,(format "--ytdl-format=%s" quality))))

    (my/set-emms-mpd-youtube-quality (car my/youtube-dl-quality-list))

Now `emms-add-url` should work on YouTube URLs just fine. Just keep in mind that it will only add the URL to the playlist, not play it right away.

##### Cache cleanup

All the added URLs reside in the EMMS cache after being played. I don’t want them to stay there for a long time, so here is a handy function to clean it.

    (defun my/emms-cleanup-urls ()
      (interactive)
      (let ((keys-to-delete '()))
        (maphash (lambda (key value)
    	       (when (eq (cdr (assoc 'type value)) 'url)
    		 (add-to-list 'keys-to-delete key)))
    	     emms-cache-db)
        (dolist (key keys-to-delete)
          (remhash key emms-cache-db)))
      (setq emms-cache-dirty t))

    (my-leader-def "asc" #'my/emms-cleanup-urls)

##### Fetching lyrics

My package for fetching EMMS lyrics and album covers.

    (use-package lyrics-fetcher
      :straight t
      :after (emms)
      :init
      (my-leader-def
        "ast" #'lyrics-fetcher-show-lyrics
        "asT" #'lyrics-fetcher-show-lyrics-query)
      :config
      (setq lyrics-fetcher-genius-access-token
    	(my/password-store-get "My_Online/APIs/genius.com"))
      (general-define-key
       :states '(emacs normal)
       :keymaps 'emms-browser-mode-map
       "gl" 'lyrics-fetcher-emms-browser-show-at-point
       "gC" 'lyrics-fetcher-emms-browser-fetch-covers-at-point
       "go" 'lyrics-fetcher-emms-browser-open-large-cover-at-point)

      (advice-add #'emms-lyrics-mode-line
    	      :override #'my/emms-lyrics-mode-line-override))

Also advice to change the location of the lyrics in the mode line.

    (defun my/emms-lyrics-mode-line-override ()
      (add-to-list 'global-mode-string
    	       '(:eval emms-lyrics-mode-line-string)))

    (defun my/emms-lyrics-restore-mode-line-override ()
      "Restore the mode line."
      (setq global-mode-string
    	    (remove '(:eval emms-lyrics-mode-line-string) global-mode-string))
      (force-mode-line-update))

    (with-eval-after-load 'emms-lyrics
      (advice-add #'emms-lyrics-mode-line
    	      :override #'my/emms-lyrics-mode-line-override)
      (advice-add #'emms-lyrics-restore-mode-line
    	      :override #'my/emms-lyrics-restore-mode-line-override))

##### Some keybindings

    (with-eval-after-load 'emms-browser
      (general-define-key
       :states '(normal)
       :keymaps 'emms-browser-mode-map
       "q" 'quit-window))

    (with-eval-after-load 'emms
      (general-define-key
       :states '(normal)
       :keymaps 'emms-playlist-mode-map
       "q" 'quit-window))

##### EMMS & mpd Fixes

Some fixes until I submit a patch. I’ve submitted a patch for with these fixes, so I’ll remove this section eventually.

For some reason EMMS doesn’t fetch `albumartist` from MPD. Overriding this function fixes that.

    (defun emms-info-mpd-process (track info)
      (dolist (data info)
        (let ((name (car data))
    	  (value (cdr data)))
          (setq name (cond ((string= name "artist") 'info-artist)
    		       ((string= name "albumartist") 'info-albumartist)
    		       ((string= name "composer") 'info-composer)
    		       ((string= name "performer") 'info-performer)
    		       ((string= name "title") 'info-title)
    		       ((string= name "album") 'info-album)
    		       ((string= name "track") 'info-tracknumber)
    		       ((string= name "disc") 'info-discnumber)
    		       ((string= name "date") 'info-year)
    		       ((string= name "genre") 'info-genre)
    		       ((string= name "time")
    			(setq value (string-to-number value))
    			'info-playing-time)
    		       (t nil)))
          (when name
    	(emms-track-set track name value)))))

Also, `emms-player-mpd-get-alists` has an interesting bug. This function parses the response to `listallinfo`, which looks something like this:

    tag1: value1
    tag2: value2
    ...
    tag1: value1'
    tag2: value2'

This structure has to be converted to list of alists, which looks like:

    (("tag1" . "value1"
      "tag2" . "value2")
      ("tag1" . "value1'"
      ("tag2" . "value2'")))

The original implementation creates a new alist whenever it encounters a tag it has already put in the current alist. Which doesn’t work too well if some tags don’t repeat, if the order is messed up, etc.

Fortunately, according to the protocol specification, each new record has to start with `file`, `directory` or `playlist`. I’ve overridden the function with that in mind and it fixed the import, at least for my case.

    (defun emms-player-mpd-get-alists (info)
      "Turn the given parsed INFO from MusicPD into an list of alists.

    The list will be in reverse order."
      (when (and info
    	     (null (car info))          ; no error has occurred
    	     (cdr info))                ; data exists
        (let ((alists nil)
    	  (alist nil)
    	  cell)
          (dolist (line (cdr info))
    	(when (setq cell (emms-player-mpd-parse-line line))
    	  (if (member (car cell) '("file" "directory" "playlist"))
    	      (setq alists (cons alist alists)
    		    alist (list cell))
    	    (setq alist (cons cell alist)))))
          (when alist
    	(setq alists (cons alist alists)))
          alists)))

#### ytel

ytel is a YouTube (actually Invidious) frontend, which lets one search YouTube (whereas the setup with elfeed just lets one view the pre-defined subscriptions).

The package doesn’t provide evil bindings, so I define my own.

    (use-package ytel
      :straight t
      :commands (ytel)
      :config
      (setq ytel-invidious-api-url "https://invidio.xamh.de/")
      (general-define-key
       :states '(normal)
       :keymaps 'ytel-mode-map
       "q" #'ytel-quit
       "s" #'ytel-search
       "L" #'ytel-search-next-page
       "H" #'ytel-search-previous-page
       "RET" #'my/ytel-add-emms))

And here is the same kind of integration with EMMS as in the elfeed setup:

    (with-eval-after-load 'emms
      (define-emms-source ytel (video)
        (let ((track (emms-track
    		  'url (concat "https://www.youtube.com/watch?v="
    			       (ytel-video-id video)))))
          (emms-track-set track 'info-title (ytel-video-title video))
          (emms-track-set track 'info-artist (ytel-video-author video))
          (emms-playlist-insert-track track))))

    (defun my/ytel-add-emms ()
      (interactive)
      (emms-add-ytel (ytel-get-current-video)))

Also, a function to copy a URL to the video under cursor.

    (defun my/ytel-kill-url ()
      (interactive)
      (kill-new
       (concat
        "https://www.youtube.com/watch?v="
        (ytel-video-id (ytel-get-current-video)))))

#### wallabag

Wallabag is a self-hosted read-it-later project. I’m not yet sold on integrating it in my workflow, but let’s keep it here for now.

    (use-package wallabag
      :straight (:host github :repo "chenyanming/wallabag.el" :files (:defaults "default.css" "emojis.alist"))
      :commands (wallabag wallabag-add-entry)
      :config
      (setq wallabag-host "https://wallabag.sqrtminusone.xyz")
      (setq wallabag-username "sqrtminusone")
      (setq wallabag-password (my/password-store-get "Selfhosted/wallabag"))
      (setq wallabag-clientid (password-store-get-field "Selfhosted/wallabag" "client_id"))
      (setq wallabag-secret (password-store-get-field "Selfhosted/wallabag" "client_secret")))

    (defun my/elfeed-wallabag (entry)
      (interactive (list elfeed-show-entry))
      (wallabag-add-entry (elfeed-entry-link entry)
    		      (mapconcat #'symbol-name (elfeed-entry-tags entry) ","))
      (elfeed-recommender--rate-current 2))

    (with-eval-after-load 'elfeed
      (general-define-key
       :states '(normal)
       :keymaps '(elfeed-show-mode-map)
       "gw" #'my/elfeed-wallabag))

#### EWW

Emacs built-in web browser. I wonder if anyone actually uses it.

I use it occasionally to open links in elfeed.

    (defun my/toggle-shr-use-fonts ()
      "Toggle the shr-use-fonts variable in buffer"
      (interactive)
      (setq-local shr-use-fonts (not shr-use-fonts)))

    (my-leader-def "aw" 'eww)

    (general-define-key
     :keymaps 'eww-mode-map
     "+" 'text-scale-increase
     "-" 'text-scale-decrease)

#### ERC

ERC is a built-it Emacs IRC client.

    (use-package erc
      :commands (erc erc-tls)
      :straight (:type built-in)
      :config
      (setq erc-log-channels-directory "~/.erc/logs")
      (setq erc-save-buffer-on-part t)
      (add-to-list 'erc-modules 'autojoin)
      (add-to-list 'erc-modules 'notifications)
      (add-to-list 'erc-modules 'log)
      (erc-update-modules)
      (setq erc-autojoin-channels-alist
    	`((,(rx "libera.chat")
    	   "#systemcrafters"
    	   "#emacs"
    	   "#guix")))
      (setq erc-kill-buffer-on-part t)
      (setq erc-track-shorten-start 8))

Exclude everything but actual messages from notifications.

    (setq erc-track-exclude-types '("NICK" "JOIN" "LEAVE" "QUIT" "PART"
    				"301"   ; away notice
    				"305"   ; return from awayness
    				"306"   ; set awayness
    				"324"   ; modes
    				"329"   ; channel creation date
    				"332"   ; topic notice
    				"333"   ; who set the topic
    				"353"   ; Names notice
    				))

A plugin to highlight IRC nicknames:

    (use-package erc-hl-nicks
      :hook (erc-mode . erc-hl-nicks-mode)
      :after (erc)
      :straight t)

ZNC support. Seems to provide a few nice features for ZNC.

    (use-package znc
      :straight t
      :commands (znc-erc)
      :init
      (my-leader-def "ai" #'znc-erc)
      (my/persp-add-rule
        erc-mode 0 "ERC")
      :config
      (setq znc-servers
    	`(("sqrtminusone.xyz" 6697 t
    	   ((libera "sqrtminusone"
    		    ,(my/password-store-get "Selfhosted/ZNC")))))))

Send `/detach` to all servers. Kinda strange that there’s no such function already

    (defun my/erc-detach-all ()
      (interactive)
      (cl-loop for buf being the buffers
    	   if (eq (buffer-local-value 'major-mode buf) 'erc-mode)
    	   do (with-current-buffer buf
    		(when (erc-server-process-alive)
    		  (let ((tgt (erc-default-target)))
    		    (erc-server-send (format "DETACH %s" tgt) nil tgt))))))

#### ement.el

    (use-package plz
      :straight (:host github :repo "alphapapa/plz.el")
      :defer t)

    (use-package ement
      :straight (:host github :repo "alphapapa/ement.el"))

#### OFF (OFF) jabber

    (use-package srv
      :straight t
      :defer t)

    (use-package fsm
      :straight t
      :defer t)

    (use-package emacs-jabber
      :straight (jabber :host nil :repo
    		    "https://tildegit.org/wgreenhouse/emacs-jabber"))

#### Google Translate

Emacs interface to Google Translate.

Can’t make it load lazily for some strange reason.

References:

- google-translate repo
- issue with ttk error fix

    (use-package google-translate
      :straight t
      :if (not my/remote-server)
      :functions (my-google-translate-at-point google-translate--search-tkk)
      :custom
      (google-translate-backend-method 'curl)
      :config
      (require 'facemenu)
      (defun google-translate--search-tkk ()
        "Search TKK."
        (list 430675 2721866130))
      (defun my-google-translate-at-point()
        "reverse translate if prefix"
        (interactive)
        (if current-prefix-arg
    	(google-translate-at-point)
          (google-translate-at-point-reverse)))
      (setq google-translate-translation-directions-alist
    	'(("en" . "ru")
    	  ("ru" . "en"))))

    (my-leader-def
      :infix "at"
      "" '(:which-key "google translate")
      "p" 'google-translate-at-point
      "P" 'google-translate-at-point-reverse
      "q" 'google-translate-query-translate
      "Q" 'google-translate-query-translate-reverse
      "t" 'google-translate-smooth-translate)

#### Reverso

    (use-package reverso
      :straight (:host github :repo "SqrtMinusOne/reverso.el")
      :init
      (my-leader-def "ar" #'reverso)
      :config
      (setq reverso-languages '(russian english german)))

### Reading documentation

#### tldr

tldr is a collaborative project providing cheatsheets for various console commands. For some reason, the built-in download in the package is broken, so I use my own function.

    (use-package tldr
      :straight t
      :commands (tldr)
      :config
      (setq tldr-source-zip-url "https://github.com/tldr-pages/tldr/archive/refs/heads/main.zip")

      (defun tldr-update-docs ()
        (interactive)
        (shell-command-to-string (format "curl -L %s --output %s" tldr-source-zip-url tldr-saved-zip-path))
        (when (file-exists-p "/tmp/tldr")
          (delete-directory "/tmp/tldr" t))
        (shell-command-to-string (format "unzip -d /tmp/tldr/ %s" tldr-saved-zip-path))
        (when (file-exists-p tldr-directory-path)
          (delete-directory tldr-directory-path 'recursive 'no-trash))
        (shell-command-to-string (format "mv %s %s" "/tmp/tldr/tldr-main" tldr-directory-path))))

    (my-leader-def "hT" 'tldr)

#### man & info

Of course, Emacs can also display man and info pages.

    (setq Man-width-max 180)
    (my-leader-def "hM" 'man)

    (general-define-key
     :states '(normal)
     :keymaps 'Info-mode-map
     (kbd "RET") 'Info-follow-nearest-node)

    (defun my/man-fix-width (&rest _)
      (setq-local Man-width (- (window-width) 4)))

    (advice-add #'Man-update-manpage :before #'my/man-fix-width)

#### devdocs.io

Finally, there is also an Emacs plugin for devdocs.io.

    (use-package devdocs
      :straight t
      :commands (devdocs-install devdocs-lookup)
      :config
      (general-define-key
       :keymaps 'devdocs-mode-map
       :states '(normal)
       "H" #'devdocs-go-back
       "L" #'devdocs-go-forward
       "o" #'devdocs-lookup
       "[" #'devdocs-previous-page
       "]" #'devdocs-next-page)
      :init
      (my-leader-def
        "he" #'devdocs-lookup
        "hE" #'devdocs-install))

#### StackExchange

sx.el is a StackExchange client for Emacs.

    (use-package sx
      :straight t
      :config
      (general-define-key
       :states '(normal)
       :keymaps '(sx-question-mode-map sx-question-list-mode-map)
       "go" #'sx-visit-externally
       "q" #'quit-window
       "s*" #'sx-tab-starred
       "sU" #'sx-tab-unanswered-my-tags
       "sa" #'sx-ask
       "sf" #'sx-tab-featured
       "sh" #'sx-tab-frontpage
       "si" #'sx-inbox
       "sm" #'sx-tab-meta-or-main
       "sn" #'sx-tab-newest
       "su" #'sx-tab-unanswered
       "sv" #'sx-tab-topvoted
       "sw" #'sx-tab-week
       "u" #'sx-upvote
       "d" #'sx-downvote
       "j" nil
       "k" nil)
      (general-define-key
       :states '(normal)
       :keymaps '(sx-question-mode-map)
       "gr" #'sx-question-mode-refresh
       "J" #'sx-question-mode-next-section
       "K" #'sx-question-mode-previous-section
       "a" #'sx-answer
       "e" #'sx-edit
       "D" #'sx-delete
       "c" #'sx-comment)
      (general-define-key
       :states '(normal)
       :keymaps '(sx-question-list-mode-map)
       "RET" 'sx-display
       "j" #'sx-question-list-next
       "k" #'sx-question-list-previous
       "S" #'sx-search
       "m" #'sx-question-list-mark-read
       "O" #'sx-question-list-order-by
       "t" #'sx-tab-switch)
      (my-leader-def
       "hs" #'sx-search
       "hS" #'sx-tab-frontpage)
      (my/use-doom-colors
       (sx-question-mode-accepted :foreground (doom-color 'green)
    			      :weight 'bold)
       (sx-question-mode-content :background nil))
      (add-hook 'sx-question-mode-hook #'doom-modeline-mode)
      (add-hook 'sx-question-list-mode-hook #'doom-modeline-mode))

### Utilities

#### pass

I use pass as my password manager. Expectedly, there is Emacs frontend for it.

This package is pretty good to manage the password database. I use password-store-ivy (another package of mine) to actually type passwords. rofi-pass is another good option.

    (use-package pass
      :straight t
      :commands (pass)
      :init
      (my-leader-def "ak" #'pass)
      :config
      (setq pass-show-keybindings nil))

Also I use `password-store-get` in a few places in my config, and by default it returns `nil` if I make an error in the password, which is not too convinient. So:

    (defun my/password-store-get (entry)
      (if-let ((res (password-store-get entry)))
          res
        (my/password-store-get entry)))

#### Docker

A package to manage docker containers from Emacs.

    (use-package docker
      :straight t
      :commands (docker)
      :init
      (my-leader-def "ao" 'docker))

#### Progidy

prodigy.el is a package to manage services. I’ve been using tmux + tmuxp for quite a long time, but want to try this package as well.

The actual service definitions are in my `~/.emacs.d/private.org`, which is encrypted by `yadm`.

    (use-package prodigy
      :straight t
      :commands (prodigy)
      :init
      (my-leader-def "aP" #'prodigy)
      :config
      (general-define-key
       :states '(normal)
       :keymaps 'prodigy-view-mode-map
       "C-h" 'evil-window-left
       "C-l" 'evil-window-right
       "C-k" 'evil-window-up
       "C-j" 'evil-window-down))

A few functions to work with apps on ports.

    (defun my/get-apps-on-ports ()
      (mapcar
       (lambda (line)
         (let* ((split (split-string line (rx (| (+ " ") (+ "\t")))))
    	    (process (elt split 6)))
           `((netid . ,(elt split 0))
    	 (state . ,(elt split 1))
    	 (recv-q . ,(elt split 2))
    	 (send-q . ,(elt split 3))
    	 ,@(let ((data (elt split 4)))
    	     (save-match-data
    	       (string-match (rx (group-n 1 (* nonl)) ":" (group-n 2 (or (+ num) "*"))) data)
    	       `((local-address . ,(match-string 1 data))
    		 (local-port . ,(match-string 2 data)))))
    	 ,@(unless (string-empty-p process)
    	     `((pid . ,(save-match-data
    			 (string-match (rx "pid=" (+ num)) process)
    			 (string-to-number (substring (match-string 0 process) 4)))))))))
       (seq-filter
        (lambda (s) (not (string-empty-p s)))
        (split-string
         (shell-command-to-string "ss -tulpnH | grep LISTEN") "\n"))))

    (defun my/kill-app-on-port (port &optional signal)
      (let ((apps (my/get-apps-on-ports)))
        (dolist (app apps)
          (when (string-equal (cdr (assoc 'local-port app)) port)
    	(signal-process (cdr (assoc 'pid app)) (or signal 15))
    	(message "Sent %d to %d" (or signal 15) (cdr (assoc 'pid app)))))))

#### screenshot.el

Tecosaur’s plugin to make beautiful code screenshots.

Guix dependency
imagemagick

    (use-package screenshot
      :straight (:repo "tecosaur/screenshot"
    		   :host github
    		   :build (:not compile))
      :if (display-graphic-p)
      :commands (screenshot)
      :init
      (my-leader-def "S" 'screenshot))

#### proced

proced is an Emacs built-it process viewer, like top.

    (my-leader-def "ah" 'proced)
    (setq proced-auto-update-interval 1)
    (add-hook 'proced-mode-hook (lambda ()
    			      (visual-line-mode -1)
    			      (setq-local truncate-lines t)
    			      (proced-toggle-auto-update 1)))

#### Guix

An Emacs package to help managing GNU Guix.

    (use-package guix
      :straight t
      :commands (guix)
      :init
      (my-leader-def "ag" 'guix)
      (defun geiser-company--setup (&rest args)
        "A dummy function.")
      (defvar geiser-repl-company-p nil
        "A dummy variable."))

### Productivity

#### pomm

My package for doing Pomodoro timer.

    (use-package pomm
      :straight t
      ;; :straight (:local-repo "~/Code/Emacs/pomm" :files (:defaults "resources"))
      :commands (pomm pomm-third-time)
      :init
      (my-leader-def "ap" #'pomm-third-time)
      (setq alert-default-style 'libnotify)
      :config
      (pomm-mode-line-mode))

#### hledger

is a plain-text double-entry accounting software. I use it for managing my personal finances, and thus far it’s great.

Guix dependency
hledger

    (use-package hledger-mode
      :straight t
      :mode (rx ".journal" eos)
      :config
      (setq hledger-jfile (concat org-directory "/ledger/ledger.journal"))
      (add-hook 'hledger-mode-hook
    	    (lambda ()
    	      (make-local-variable 'company-backends)
    	      (add-to-list 'company-backends 'hledger-company))))

    (use-package flycheck-hledger
      :straight t
      :after (hledger-mode))

Here are some usage notes.

The fastest way to enter new entiries to the journal is by running `hledger add`

Then, run `hledger bs` to check whether the balance sheet matches the ground truth (e.g. the bank UI).

If it doesn’t the simplest way to check for the differences is by running `hledger register <item>`.

Here are some interesting commands to run:

- `hledger incomestatement <query>`, where `<query>` is the account prefix. e.g. `expenses` or `revenues`.
  - add `--pivot=payee` to get grouping by transaction descriptions
  - add `-B` to cast currencies

#### Calendar

Emacs’ built-in calendar. Can even calculate sunrise and sunset times.

    (setq calendar-date-style 'iso) ;; YYYY/mm/dd
    (setq calendar-week-start-day 1)
    (setq calendar-time-display-form '(24-hours ":" minutes))

    (setq calendar-latitude 59.9375)
    (setq calendar-longitude 30.308611)

### Fun

#### Discord integration

Integration with Discord. Shows which file is being edited in Emacs.

In order for this to work in Guix, a service is necessary - Discord rich presence.

Some functions to override the displayed message:

    (defun my/elcord-mask-buffer-name (name)
      (cond
       ((string-match-p (rx bos (? "CAPTURE-") (= 14 num) "-" (* not-newline) ".org" eos) name)
        "<ORG-ROAM>")
       ((string-match-p (rx bos (+ num) "-" (+ num) "-" (+ num) ".org" eos) name)
        "<ORG-JOURNAL>")
       ((string-match-p (rx bos "EXWM") name)
        "<EXWM>")
       (t name)))

    (defun my/elcord-buffer-details-format-functions ()
      (format "Editing %s" (my/elcord-mask-buffer-name (buffer-name))))

    (defun my/elcord-update-presence-mask-advice (r)
      (list (my/elcord-mask-buffer-name (nth 0 r)) (nth 1 r)))

And the package configuration:

    (use-package elcord
      :straight t
      :if (and (or
    	    (string= (system-name) "indigo")
    	    (string= (system-name) "eminence"))
    	   (not my/slow-ssh)
    	   (not my/remote-server))
      :config
      (setq elcord-buffer-details-format-function #'my/elcord-buffer-details-format-functions)
      (advice-add 'elcord--try-update-presence :filter-args #'my/elcord-update-presence-mask-advice)
      (elcord-mode))

#### Snow

    (use-package snow
      :straight (:repo "alphapapa/snow.el" :host github)
      :commands (snow))

#### Power mode

When Emacs doesn’t feel powerful enough.

Watch out if you are using EXWM.

    (use-package power-mode
      :straight (:host github :repo "elizagamedev/power-mode.el")
      :disabled
      :commands (power-mode))

#### Redacted

    (use-package redacted
      :commands (redacted-mode)
      :straight (:host github :repo "bkaestner/redacted.el"))

#### Zone

    (use-package zone
      :ensure nil
      :config
      (setq original-zone-programs (copy-sequence zone-programs)))

    (defun my/zone-with-select ()
      (interactive)
      (ivy-read "Zone programs"
    	    (cl-pairlis
    	     (cl-mapcar 'symbol-name original-zone-programs)
    	     original-zone-programs)
    	    :action (lambda (elem)
    		      (setq zone-programs (vector (cdr elem)))
    		      (zone))))

## Guix settings

Guix dependency | Description
--- | ---
emacs-vterm | The vterm package
ripgrep | A recursive search tool
the-silver-searcher | Another recursive search tool

    (when (fboundp #'my/format-guix-dependencies)
      (my/format-guix-dependencies))

    (specifications->manifest
     '("emacs"
       <<packages()>>))

Table of Contents
<Expand> <Collapse>