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.

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

Table of Contents

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 last commits which had these feature presented.

Feature Last commit
tab-bar.el 19ff54db9fe21fd5bdf404a8d2612176baa8a6f5
spaceline 19ff54db9fe21fd5bdf404a8d2612176baa8a6f5

Bootstrap

Setting up the environment, performance tuning and a few basic settings.

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:

(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 individual package loading time.

References:

(straight-use-package 'use-package)
(eval-when-compile (require 'use-package))

Variables & environment

This section is about optionating the Emacs config.

The following variable is true when my machine is not powerful enough for some resource-heavy packages.

(setq my/lowpower (string= (system-name) "azure"))

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")
       (string= (system-name) "dev-digital")
       (string= (system-name) "violet")))

The following is true is Emacs is ran 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")))

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

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.

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

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

Native compilation

Set the number of native compilation jobs to 1 on low-power machines.

(when my/lowpower
  (setq comp-async-jobs-number 1))

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 auxililary environment variable. This variable is used in the shell config.

References:

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

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

    (load-file private-file))

No littering

By default Mmacs 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:

(use-package general
  :straight t
  :config
  (general-evil-setup))

which-key

A package that displays the available keybindings in a popup. The package is pretty useful, as Emacs seems to have more keybindings than I can remember at any given point.

References:

(use-package which-key
  :config
  (setq which-key-idle-delay (if my/lowpower 1 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 tree-like form.

(defun my/dump-bindings-recursive (prefix &optional level)
  (dolist (key (which-key--get-bindings (kbd prefix)))
    (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))))))

(defun my/dump-bindings (prefix)
  "Dump keybindings starting with PREFIX in tree-like form."
  (interactive "sPrefix: ")
  (with-current-buffer (get-buffer-create "bindings")
    (point-max)
    (erase-buffer)
    (save-excursion
      (my/dump-bindings-recursive prefix)))
  (switch-to-buffer-other-window "bindings"))

Evil

An entire ecosystem of packages that emulates the main features of Vim. Probably the best vim emulator out there.

The only problem is that the package name makes it hard to google anything by just typing “evil”.

References:

Evil-mode

Basic evil configuration.

(use-package evil
  :straight t
  :init
  (setq evil-want-integration t)
  (setq evil-want-C-u-scroll t)
  (setq evil-want-keybinding nil)
  (setq evil-search-module 'evil-search)
  (setq evil-split-window-below t)
  (setq evil-vsplit-window-right t)
  :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 gives 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 the important 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 the 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
  :config
  (global-evil-matchit-mode 1))
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)))

My keybindings

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

(general-define-key
 :keymaps '(normal visual global)
 [escape] 'keyboard-quit)

(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.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)
CHECK 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)
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, but I don’t use it at the moment.

Evil does a pretty good job of abstracting the first two 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)
CHECK 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:

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

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 ans settings that help with the central task of Emacs - editing text.

Indentation & whitespace

Aggressive Indent

A package to keep the code intended.

Doesn’t work too well with many ecosystems because the LSP-based indentation is rather slow, but nice for Lisps.

References:

(use-package aggressive-indent
  :commands (aggressive-indent-mode)
  :straight t)
Delete trailing whitespace

Delete trailing whitespace on save, unless in particular modes where trailing whitespace is important, like Markdown.

(setq my/trailing-whitespace-modes '(markdown-mode))

(require 'cl-extra)

(add-hook 'before-save-hook
	  (lambda ()
	    (unless (cl-some #'derived-mode-p my/trailing-whitespace-modes)
	      (delete-trailing-whitespace))))
Tabs

Some default settings to manage tabs.

(setq tab-always-indent nil)

(setq-default default-tab-width 4)
(setq-default tab-width 4)
(setq-default evil-indent-convert-tabs nil)
(setq-default indent-tabs-mode nil)
(setq-default tab-width 4)
(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 build-in sequential undo system with a tree-based one. Probably one of the greatest options of Emacs as a text editor.

References:

(use-package undo-tree
  :straight t
  :config
  (global-undo-tree-mode)
  (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.

References:

(use-package yasnippet-snippets
  :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))

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

(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
  :config
  (add-hook 'visual-fill-column-mode-hook
	    (lambda () (setq visual-fill-column-center-text t))))

Working with projects

Treemacs

Type Note
TODO Enable modeline only for particular treemacs instances

Treemacs calls itself a tree layout file explorer, but looks more like a project and workspace management system.

Integrates with evil, magit, projectile and perspective. The latter is particularly great - each perspective can have its own treemacs workspace!

(use-package treemacs
  :straight t
  :commands (treemacs treemacs-switch-workspace treemacs-edit-workspace)
  :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))

(use-package treemacs-evil
  :after (treemacs evil)
  :straight t)

(use-package treemacs-magit
  :after (treemacs magit)
  :straight t)

(use-package treemacs-perspective
  :after (treemacs perspective)
  :straight t
  :config
  (treemacs-set-scope-type 'Perspectives))

(general-define-key
 :keymaps '(normal override global)
 "C-n" 'treemacs)

(general-define-key
 :keymaps '(treemacs-mode-map) [mouse-1] #'treemacs-single-click-expand-action)

(my-leader-def
  :infix "t"
  "" '(:which-key "treemacs")
  "w" 'treemacs-switch-workspace
  "e" 'treemacs-edit-workspaces)
Helper functions

Function to open dired and vterm at given nodes.

(defun my/treemacs-open-dired ()
  "Open dired at given treemacs node"
  (interactive)
  (let (path (treemacs--prop-at-point :path))
    (dired path)))

(defun my/treemacs-open-vterm ()
  "Open vterm at given treemacs node"
  (interactive)
  (let ((default-directory (file-name-directory (treemacs--prop-at-point :path))))
    (vterm)))

(with-eval-after-load 'treemacs
  (general-define-key
   :keymaps 'treemacs-mode-map
   :states '(treemacs)
   "gd" 'my/treemacs-open-dired
   "gt" 'my/treemacs-open-vterm
   "`" 'my/treemacs-open-vterm))
Custom icons
;; (treemacs-define-custom-icon (concat " " (all-the-icons-fileicon "typescript")) "spec.ts")
;; (setq treemacs-file-extension-regex (rx "." (or "spec.ts" (+ (not "."))) eos))

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)

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

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

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

Git & Magit

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

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

git-timemachine allows to visit previous versions of a file.

(use-package magit
  :straight t
  :commands (magit-status 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)))))

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

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

(my-leader-def
  "m" 'magit
  "M" 'magit-file-dispatch)

Editorconfig

Editorconfig support for Emacs.

References:

(use-package editorconfig
  :straight t
  :config
  (unless my/slow-ssh (editorconfig-mode 1))
  (add-to-list 'editorconfig-indentation-alist
	       '(emmet-mode emmet-indentation)))

Completion

Ivy, counsel, swiper

Minibuffer completion tools for Emacs.

References:

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

(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))
  ;; 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
  "A" 'counsel-ag)

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

(my-leader-def "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:

(use-package company
  :straight t
  :config
  (global-company-mode)
  (setq company-idle-delay (if my/lowpower 0.5 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.

(use-package company-box
  :straight t
  :disabled
  :if (and (display-graphic-p) (not my/lowpower))
  :after (company)
  :hook (company-mode . company-box-mode))

Help

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. Of course, I didn’t type it all by hand.

(my-leader-def
  :infix "h"
  "" '(:which-key "help")
  "RET" 'view-order-manuals
  "." 'display-local-help
  "?" 'help-for-help
  "C" 'describe-coding-system
  "F" 'Info-goto-emacs-command-node
  "I" 'describe-input-method
  "K" 'Info-goto-emacs-key-command-node
  "L" 'describe-language-environment
  "P" 'describe-package
  "S" 'info-lookup-symbol
  "a" 'helm-apropos
  "b" 'describe-bindings
  "c" 'describe-key-briefly
  "d" 'apropos-documentation
  "e" 'view-echo-area-messages
  "f" 'helpful-function
  "g" 'describe-gnu-project
  "h" 'view-hello-file
  "i" 'info
  "k" 'helpful-key
  "l" 'view-lossage
  "m" 'describe-mode
  "n" 'view-emacs-news
  "o" 'describe-symbol
  "p" 'finder-by-keyword
  "q" 'help-quit
  "r" 'info-emacs-manual
  "s" 'describe-syntax
  "t" 'help-with-tutorial
  "v" 'helpful-variable
  "w" 'where-is
  "<f1>" 'help-for-help)

Time trackers

A bunch of time trackers I use.

References:

WakaTime

Before I figure out how to package this for Guix:

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

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

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

Themes and colors

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

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

A custom theme, dependent on Doom. I set all my custom variables there.

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.

Also, a hook allows me to change doom-theme more or less at will, although I do that only to switch to a light theme once in a blue moon.

(unless my/is-termux
  (deftheme my-theme-1)

  (defun my/update-my-theme (&rest _)
    (custom-theme-set-faces
     'my-theme-1
     `(tab-bar-tab ((t (
			:background ,(doom-color 'bg)
			:foreground ,(doom-color 'yellow)
			:underline ,(doom-color 'yellow)))))
     `(org-block ((t (:background ,(color-darken-name (doom-color 'bg) 3)))))
     `(org-block-begin-line ((t (
				 :background ,(color-darken-name (doom-color 'bg) 3)
				 :foreground ,(doom-color 'grey)))))
     `(auto-dim-other-buffers-face ((t (:background ,(color-darken-name (doom-color 'bg) 3)))))
     `(aweshell-alert-buffer-face ((t (:foreground ,(doom-color 'red) :weight bold))))
     `(aweshell-alert-command-face ((t (:foreground ,(doom-color 'yellow) :weight bold))))
     `(epe-pipeline-delimiter-face ((t (:foreground ,(doom-color 'green)))))
     `(epe-pipeline-host-face ((t (:foreground ,(doom-color 'blue)))))
     `(epe-pipeline-time-face ((t (:foreground ,(doom-color 'yellow)))))
     `(epe-pipeline-user-face ((t (:foreground ,(doom-color 'red)))))
     `(elfeed-search-tag-face ((t (:foreground ,(doom-color 'yellow)))))
     `(notmuch-wash-cited-text ((t (:foreground ,(doom-color 'yellow)))))
     `(spaceline-evil-emacs ((t :background ,(doom-color 'bg)
				:foreground ,(doom-color 'fg))))
     `(spaceline-evil-insert ((t :background ,(doom-color 'green)
				 :foreground ,(doom-color 'base0))))
     `(spaceline-evil-motion ((t :background ,(doom-color 'magenta)
				 :foreground ,(doom-color 'base0))))
     `(spaceline-evil-normal ((t :background ,(doom-color 'blue)
				 :foreground ,(doom-color 'base0))))
     `(spaceline-evil-replace ((t :background ,(doom-color 'yellow)
				  :foreground ,(doom-color 'base0))))
     `(spaceline-evil-visual ((t :background ,(doom-color 'grey)
				 :foreground ,(doom-color 'base0)))))
    (custom-theme-set-variables
     'my-theme-1
     `(aweshell-invalid-command-color ,(doom-color 'red))
     `(aweshell-valid-command-color ,(doom-color 'green)))
    (enable-theme 'my-theme-1))

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

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:

(set-frame-font "JetBrainsMono Nerd Font 10" nil t)

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

(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/lowpower my/remote-server))
  :hook (
	 (prog-mode . highlight-indent-guides-mode)
	 (vue-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
  :if (not my/lowpower)
  :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. For now I opted out of it in favour of spaceline because I want to have a more powerline-ish look.

References:

(use-package doom-modeline
  :straight t
  ;; :if (not (display-graphic-p))
  :init
  (setq doom-modeline-env-enable-python nil)
  (setq doom-modeline-env-enable-go nil)
  (setq doom-modeline-buffer-encoding 'nondefault)
  (setq doom-modeline-hud t)
  :config
  (doom-modeline-mode 1)
  (setq doom-modeline-minor-modes nil)
  (setq doom-modeline-buffer-state-icon nil))

perspective.el

perspective.el is a package which 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)
  :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))

Some functions

Move the current buffer to a perspective and switch to it.

(defun my/persp-move-window-and-switch ()
  (interactive)
  (let* ((buffer (current-buffer)))
    (call-interactively #'persp-switch)
    (persp-set-buffer (buffer-name buffer))
    (switch-to-buffer buffer)))

Copy the current buffer to a perspective and switch to it.

(defun my/persp-copy-window-and-switch ()
  (interactive)
  (let* ((buffer (current-buffer)))
    (call-interactively #'persp-switch)
    (persp-add-buffer (buffer-name buffer))
    (switch-to-buffer buffer)))

Add keybindings to the default map.

(with-eval-after-load 'perspective
  (general-define-key
   :keymaps 'perspective-map
   "m" #'my/persp-move-window-and-switch
   "f" #'my/persp-copy-window-and-switch))

Programming

General setup

LSP

LSP-mode provides an IDE-like experience for Emacs - real-time diagnostic, code actions, intelligent autocompletion, etc.

References:

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

(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 ran from mmm-mode, so there is a small workaround.

References:

(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 time of this writing, I mostly debug TypeScript, so the main competitor is Chrome Inspector for node.js.

References:

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

  (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 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-ui-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 an advice.

(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))))))
Improved stack frame switching

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

OFF (OFF) TabNine

A ML-based autocompletion system.

More often than not gives really good results, but is slow as hell & consumes a lot of RAM. Also, LSP-provided completions were more useful in my experience.

References:

(use-package company-tabnine
  :straight t
  :if (not my/lowpower)
  :after company
  :config
  (add-to-list 'company-backends #'company-tabnine))

OFF (OFF) Code Compass

A set of code analyzing tools.

References:

Dependencies
(use-package async
  :straight t)
(use-package dash
  :straight t)
(use-package f
  :straight t)
(use-package s
  :straight t)
(use-package simple-httpd
  :straight t)
Plugin
(use-package code-compass
  :straight (
  :repo "ag91/code-compass"
  :files ("code-compass.el")
  :branch "main"
  ))

Reformatter

A general-purpose package to run formattters 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)

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

My bit of config here:

(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
  ;; (setq emmet-indent-after-insert nil)
  (setq my/emmet-mmm-submodes '(vue-html-mode css-mode))
  (defun my/emmet-or-tab (&optional arg)
    (interactive)
    (if (and
	 (boundp 'mmm-current-submode)
	 mmm-current-submode
	 (not (member mmm-current-submode my/emmet-mmm-submodes)))
	(indent-for-tab-command arg)
      (or (emmet-expand-line arg)
	  (emmet-go-to-edit-point 1)
	  (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
    "r" 'jest-test-run
    "a" 'jest-test-run-all-tests))
(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-example-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 ()
  (when (string-match-p (rx ".vue" eos) (buffer-name))
    (setq-local web-mode-script-padding 0)))

(add-hook 'web-mode-hook 'my/web-mode-vue-setup)

OFF (OFF) Vue.js

(use-package vue-mode
  :straight t
  :disabled
  :mode "\\.vue\\'"
  :config
  (add-hook 'vue-mode-hook #'hs-minor-mode)
  (add-hook 'vue-mode-hook #'smartparens-mode)
  (my/set-smartparens-indent 'vue-mode)
  (add-hook 'vue-mode-hook (lambda () (set-face-background 'mmm-default-submode-face nil)))
  <<override-mmm-mode-func>>)

(with-eval-after-load 'editorconfig
  (add-to-list 'editorconfig-indentation-alist
	       '(vue-mode css-indent-offset
			  js-indent-level
			  sgml-basic-offset
			  ssass-tab-width
			  typescript-indent-level
			  emmet-indentation
			  vue-html-extra-indent)))
mmm-mode fix

References:

(defun mmm-syntax-propertize-function (start stop)
  (let ((saved-mode mmm-current-submode)
	(saved-ovl  mmm-current-overlay))
    (mmm-save-changed-local-variables
     mmm-current-submode mmm-current-overlay)
    (unwind-protect
	(mapc (lambda (elt)
		(let* ((mode (car elt))
		       (func (get mode 'mmm-syntax-propertize-function))
		       (beg (cadr elt)) (end (nth 2 elt))
		       (ovl (nth 3 elt))
		       syntax-ppss-cache
		       syntax-ppss-last)
		  (goto-char beg)
		  (mmm-set-current-pair mode ovl)
		  (mmm-set-local-variables mode mmm-current-overlay)
		  (save-restriction
		    (if mmm-current-overlay
			(narrow-to-region (overlay-start mmm-current-overlay)
					  (overlay-end mmm-current-overlay))
		      (narrow-to-region beg end))
		    (cond
		     (func
		      (funcall func beg end))
		     (font-lock-syntactic-keywords
		      (let ((syntax-propertize-function nil))
			(font-lock-fontify-syntactic-keywords-region beg end))))
		    (run-hook-with-args 'mmm-after-syntax-propertize-functions
					mmm-current-overlay mode beg end))))
	      (mmm-regions-in start stop))
      (mmm-set-current-pair saved-mode saved-ovl)
      (mmm-set-local-variables (or saved-mode mmm-primary-mode) saved-ovl))))

OFF (OFF) Svelte

Had some problems with this and tree-sitter. Web-mode seems to be doing better.

(use-package svelte-mode
  :straight t
  :mode "\\.svelte\\'"
  :disabled
  :config
  (add-hook 'svelte-mode-hook 'my/set-flycheck-eslint)
  (add-hook 'svelte-mode-hook #'smartparens-mode)
  (my/set-smartparens-indent 'svelte-mode)
  ;; I have my own Emmet
  (setq lsp-svelte-plugin-css-completions-emmet nil)
  (setq lsp-svelte-plugin-html-completions-emmet nil))

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\\'")

LaTeX

AUCTeX

The best LaTeX editing environment I’ve found so far.

References:

(use-package tex
  :straight auctex
  :defer t
  :config
  (setq-default TeX-auto-save t)
  (setq-default TeX-parse-self t)
  (TeX-PDF-mode)
  ;; Use XeLaTeX & stuff
  (setq-default TeX-engine 'xetex)
  (setq-default TeX-command-extra-options "-shell-escape")
  (setq-default TeX-source-correlate-method 'synctex)
  (TeX-source-correlate-mode)
  (setq-default TeX-source-correlate-start-server t)
  (setq-default LaTeX-math-menu-unicode t)

  (setq-default font-latex-fontify-sectioning 1.3)

  ;; Scale preview for my DPI
  (setq-default preview-scale-function 1.4)
  (when (boundp 'tex--prettify-symbols-alist)
    (assoc-delete-all "--" tex--prettify-symbols-alist)
    (assoc-delete-all "---" tex--prettify-symbols-alist))

  (add-hook 'LaTeX-mode-hook
	    (lambda ()
	      (TeX-fold-mode 1)
	      (outline-minor-mode)))

  (add-to-list 'TeX-view-program-selection
	       '(output-pdf "Zathura"))

  ;; Do not run lsp within templated TeX files
  (add-hook 'LaTeX-mode-hook
	    (lambda ()
	      (unless (string-match "\.hogan\.tex$" (buffer-name))
		(lsp))
	      (setq-local lsp-diagnostic-package :none)
	      (setq-local flycheck-checker 'tex-chktex)))

  (add-hook 'LaTeX-mode-hook #'rainbow-delimiters-mode)
  (add-hook 'LaTeX-mode-hook #'smartparens-mode)
  (add-hook 'LaTeX-mode-hook #'prettify-symbols-mode)

  (my/set-smartparens-indent 'LaTeX-mode)
  (require 'smartparens-latex)

  (general-nmap
    :keymaps '(LaTeX-mode-map latex-mode-map)
    "RET" 'TeX-command-run-all
    "C-c t" 'orgtbl-mode)

  <<init-greek-latex-snippets>>
  <<init-english-latex-snippets>>
  <<init-math-latex-snippets>>
  <<init-section-latex-snippets>>)

BibTeX

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

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

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

LanguageTool

LanguageTool is a great offline spell checker. For some reason, the download link is nowhere to be found on the home page, so it is listed in the references as well.

References:

(use-package langtool
  :straight t
  :commands (langtool-check)
  :config
  (setq langtool-language-tool-server-jar "/home/pavel/bin/LanguageTool-5.4/languagetool-server.jar")
  (setq langtool-mother-tongue "ru")
  (setq langtool-default-language "en-US"))

(my-leader-def
  :infix "L"
  "" '(:which-key "languagetool")
  "c" 'langtool-check
  "s" 'langtool-server-stop
  "d" 'langtool-check-done
  "n" 'langtool-goto-next-error
  "p" 'langtool-goto-previous-error
  "l" 'langtool-correct-buffer)

Lisp

These are your father’s parentheses. Elegant weapons for a more… civilized age.

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
  :mode "\\.clj[sc]?\\'"
  :straight t)

Hy

Python requirements:

(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
  :if (not my/lowpower)
  :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

Use Microsoft Language Server for Python.

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:

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

(use-package py-isort
  :straight t
  :commands (py-isort-buffer py-isort-region))

The following bindings 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 an unit testing framework for Python.

Once again a function to set pytest executable from pipenv.

References:

(defun my/set-pipenv-pytest ()
  (setq-local
   python-pytest-executable
   (concat (my/get-pipenv-python) " -m pytest")))

(use-package python-pytest
  :straight t
  :commands (python-pytest-dispatch)
  :init
  (my-leader-def
    :keymaps 'python-mode-map
    :infix "t"
    "t" 'python-pytest-dispatch)
  :config
  <<override-pytest-run>>
  (add-hook 'python-mode-hook #'my/set-pipenv-pytest)
  (when (derived-mode-p 'python-mode)
    (my/set-pipenv-pytest)))
Fix comint buffer width

For some reason 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))

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

fish

(use-package fish-mode
  :straight t
  :mode "\\.fish\\'"
  :config
 (add-hook 'fish-mode-hook #'smartparens-mode))

sh

(add-hook 'sh-mode-hook #'smartparens-mode)

Haskell

(use-package haskell-mode
  :straight t
  :mode "\\.hs\\'")

(use-package lsp-haskell
  :straight t
  :after (lsp haskell-mode))

Lua

(use-package lua-mode
  :straight t
  :mode "\\.lua\\'"
  :hook (lua-mode . smartparens-mode))

(my/set-smartparens-indent 'lua-mode)

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

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

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

.env

(use-package dotenv-mode
  :straight t
  :mode "\\.env\\..*\\'")

CSV

(use-package csv-mode
  :straight t
  :mode "\\.csv\\'")

Docker

(use-package dockerfile-mode
  :mode "Dockerfile\\'"
  :straight t
  :config
  (add-hook 'dockerfile-mode 'smartparens-mode))

crontab

(use-package crontab-mode
  :straight t)

Org Mode

The best feature of Emacs. Just after every other best feature of Emacs, probably.

References:

Installation & basic settings

Use the built-in org mode.

(use-package org
  :straight t
  :if (not my/remote-server)
  :defer t
  :init
  (setq org-directory (expand-file-name "~/Documents/org-mode"))
  :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)))
  (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"))
  <<org-crypt-setup>>
  (unless my/is-termux
    <<org-lang-setup>>)
  <<org-ui-setup>>
  <<org-keys-setup>>
  <<org-productivity-setup>>)

Encryption

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

org-contrib

org-contrib is a package with various additions to Org. I use the following:

(use-package org-contrib
  :straight (org-contrib
	     :type git
	     :host nil
	     :repo "https://git.sr.ht/~bzg/org-contrib"
	     :build t)
  :after (org)
  :config
  (require 'ox-extra)
  (require 'ol-notmuch)
  (ox-extras-activate '(latex-header-blocks ignore-headlines)))

Integration with evil

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

(use-package jupyter
  :straight t
  :if (not my/is-termux)
  :init
  (my-leader-def "ar" 'jupyter-run-repl))

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

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

Setup

Enable languages

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

(add-hook 'org-babel-after-execute-hook 'org-redisplay-inline-images)

Use Jupyter block instead of built-in Python.

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

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

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

(advice-add 'jupyter-org-scalar :around #'my/jupyter-org-scalar-around)

Wrap source code output

A function to remove :RESULTS: drawer from the results. Once again, 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:

As for now, 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*)

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

Productivity & Knowledge management

My ongoing effort to get a productivity setup in Org.

Some inspiration:

Used files

(setq org-agenda-files '("inbox.org" "projects.org" "work.org" "sem-11.org" "life.org"))
;; (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-targets
      '(("projects.org" :maxlevel . 2)
	("work.org" :maxlevel . 2)
	("sem-11.org" :maxlevel . 3)
	("life.org" :maxlevel . 2)))
(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

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

Trello sync

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.

(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)
  :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
  :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-custom-commands
      `(("p" "My outline"
	 ((agenda "")
	  (todo "NEXT"
		((org-agenda-prefix-format "  %i %-12:c [%e] ")
		 (org-agenda-overriding-header "Next tasks")))
	  (org-ql-block
	   `(and
	     (regexp ,(rx ":orgtrello_users:" (* nonl) "sqrtminusone"))
	     (todo)
	     (deadline))
	   ((org-agenda-files ',org-trello-files)
	    (org-ql-block-header "Trello assigned")))
	  (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)")))))
	("tp" "Personal tasks"
	 ((tags-todo "personal"
		     ((org-agenda-prefix-format "  %i %-12:c [%e] ")))))))

Review workflow

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.

Data from git & org-roam

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

(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 Roam files since the last review. Date should have 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))))

Now we are ready to format this list to insert it into the capture template.

(defun my/org-review-format-roam (rev)
  (let* ((changes (my/org-changed-files-since-date rev))
	 (new-roam
	  (seq-filter
	   (lambda (elem)
	     (and (eq (car elem) 'added)
		  (string-match-p (rx bos "roam") (cdr elem))))
	   changes))
	 (changed-roam
	  (seq-filter
	   (lambda (elem)
	     (and (eq (car elem) 'modified)
		  (string-match-p (rx bos "roam") (cdr elem))))
	   changes)))
    (concat
     (unless (seq-empty-p new-roam)
       (concat "** New Roam entries \n"
	       (mapconcat
		(lambda (entry)
		  (format "- [[file:%s][%s]]" (cdr entry) (cdr entry)))
		new-roam
		"\n")
	       "\n"))
     (unless (seq-empty-p changed-roam)
       (concat "** Changed Roam entries \n"
	       (mapconcat
		(lambda (entry)
		  (format "- [[file:%s][%s]]" (cdr entry) (cdr entry)))
		changed-roam
		"\n"))))))
Data from org-journal

Second, I want to have a list of new jounal entries since the last review.

(defun my/org-journal-entries-since-date (rev-date)
  (mapcar
   (lambda (date)
     (let ((time (encode-time (parse-time-string date))))
       `((file . ,(org-journal--get-entry-path time))
	 (header . ,(format-time-string org-journal-date-format time)))))
   (seq-filter
    (lambda (date) (string-lessp rev-date date))
    (mapcar
     (lambda (date)
       (format "%04d-%02d-%02dT00:00:00+0300" (nth 2 date) (nth 0 date) (nth 1 date)))
     (org-journal--list-dates)))))

Format the results:

(defun my/org-review-format-journal (rev-date)
  (mapconcat
   (lambda (item)
     (format "- [[file:%s::*%s][%s]]"
	     (cdr (assoc 'file item))
	     (cdr (assoc 'header item))
	     (cdr (assoc 'header item))))
   (my/org-journal-entries-since-date rev-date)
   "\n"))
Data from org-agenda via org-ql

Third, 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 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. 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 week.

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

(defun my/org-review-get-filename ()
  (concat my/org-review-directory "/" (format-time-string "%Y-%m-%d.org" (current-time))))

(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-directory my/org-review-directory))))
    (format-time-string
     "%Y-%m-%d"
     (time-subtract
      (current-time)
      (seconds-to-time (* 60 60 24 7)))))
   0 10))

A template looks like this:

(setq my/org-review-capture-template
      `("r" "Review" plain (file ,(my/org-review-get-filename))
	,(string-join
	  '("#+TITLE: Review %t"
	    ""
	    "Last review date: %(org-timestamp-translate (org-timestamp-from-string (format \"<%s>\" (my/get-last-review-date))))"
	    ""
	    "* Roam"
	    "%(my/org-review-format-roam (my/get-last-review-date))"
	    "* Journal"
	    "New journal entries:"
	    "%(my/org-review-format-journal (my/get-last-review-date))"
	    "* Agenda"
	    "%(my/org-review-format-queries (my/get-last-review-date))"
	    "* Thoughts                                                            :crypt:"
	    "%?")
	  "\n")))

(add-to-list 'org-capture-templates my/org-review-capture-template t)

Org Journal

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

(use-package org-journal
  :straight t
  :if (not my/remote-server)
  :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))

(my-leader-def
  :infix "oj"
  "" '(:which-key "org-journal")
  "j" 'org-journal-new-entry
  "o" 'org-journal-open-current-journal-file
  "s" 'org-journal-search)

Also, I want to store some information in the journal as properties of the record. So below is a function which does just that.

As of now, it stores Emacs version, hostname, location and current EMMS track if there is one.

(defun my/set-journal-header ()
  (org-set-property "Emacs" emacs-version)
  (org-set-property "Hostname" system-name)
  (when (boundp 'my/location)
    (org-set-property "Location" my/location))
  (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.

Guix dependency
emacs-emacsql-sqlite3
graphviz

References:

(use-package emacsql-sqlite
  :defer t
  :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-directory (concat org-directory "/roam"))
  (setq org-roam-file-extensions '("org"))
  (setq org-roam-v2-ack t)
  (setq orb-insert-interface 'ivy-bibtex)
  :config
  (org-roam-setup)
  (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)))
  (require 'org-roam-protocol)
  (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))

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

(with-eval-after-load 'org
  (my-leader-def
    :keymap 'org-mode-map
    :infix "or"
    "t" 'org-roam-tag-add
    "T" 'org-toam-tag-remove)
  (general-define-key
   :keymap 'org-mode-map
   "C-c i" 'org-id-get-create
   "C-c l o" '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"))
  :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

org-ref

Type Description
TODO Figure out how not to load Helm

org-ref is a package that provides support for various citations & references in Org mode.

Useful to use BibTeX citations in LaTeX export.

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.

(use-package org-ref
  :straight (:files (:defaults (:exclude "*helm*")))
  :if (not my/remote-server)
  :init
  (setq org-ref-completion-library 'org-ref-ivy-cite)
  (setq bibtex-dialect 'biblatex)
  (setq org-ref-default-bibliography '("~/Documents/org-mode/bibliography.bib"))
  (setq reftex-default-bibliography org-ref-default-bibliography)
  (setq bibtex-completion-bibliography org-ref-default-bibliography)
  :after (org)
  :config
  (general-define-key
   :keymaps 'org-mode-map
   :infix "C-c l"
   "" '(:which-key "org-ref")
   "l" 'org-ref-ivy-insert-cite-link
   "r" 'org-ref-ivy-insert-ref-link
   "h" 'org-ref-cite-hydra/body)
  (general-define-key
   :keymaps 'bibtex-mode-map
   "M-RET" 'org-ref-bibtex-hydra/body)
  ;; (add-to-list 'orhc-candidate-formats
  ;;              '("online" . "  |${=key=}| ${title} ${url}"))
  )

org-roam-bibtex

Integration with bibtex and org-ref.

There are some problems with org roam v2, so I disabled it as of now. I will probably use another way of managing bibliography notes anyway.

(use-package org-roam-bibtex
  :straight (:host github :repo "org-roam/org-roam-bibtex")
  :after (org-roam org-ref)
  :disabled
  :config
  (org-roam-bibtex-mode))

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

UI

OFF (OFF) Instant equations preview

Instant math previews for org mode.

References:

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

(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

(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 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 elipsis at the end of folded headlines. The elipsis seems unnecesary with org-bars.

(defun my/org-no-ellipsis-in-headlines ()
  (remove-from-invisibility-spec '(outline . t))
  (add-to-invisibility-spec 'outline))

(add-hook 'org-mode-hook #'my/org-no-ellipsis-in-headlines)

Org Agenda Icons

Categories are broad labels to group agenda items.

(if (not my/lowpower)
    (setq org-agenda-category-icon-alist
	  `(("inbox" ,(list (all-the-icons-faicon "inbox")) nil nil :ascent center)
	    ("work" ,(list (all-the-icons-faicon "cog")) nil nil :ascent center)
	    ("education" ,(list (all-the-icons-material "build")) nil nil :ascent center)
	    ("personal" ,(list (all-the-icons-faicon "music")) nil nil :ascent center)
	    ("misc" ,(list (all-the-icons-material "archive")) nil nil :ascent center)
	    ;; ("lesson" ,(list (all-the-icons-faicon "book")) nil nil :ascent center)
	    ;; ("meeting" ,(list (all-the-icons-material "chat")) nil nil :ascent center)
	    ;; ("event" ,(list (all-the-icons-octicon "clock")) nil nil :ascent center)
	    ("." ,(list (all-the-icons-faicon "circle-o")) nil nil :ascent center))))

Export

General settings

;; (setq org-export-backends '(md html latex beamer org))

Hugo

(use-package ox-hugo
  :straight t
  :after ox)

Jupyter Notebook

(use-package ox-ipynb
  :straight (:host github :repo "jkitchin/ox-ipynb")
  :after ox)

Html export

(use-package htmlize
  :straight t
  :after ox
  :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}")))
  ;; Use beamer without the default packages
  (add-to-list 'org-latex-classes
	       '("org-latex-beamer"
		 "\\documentclass{beamer}
[NO-DEFAULT-PACKAGES]
[PACKAGES]
[EXTRA]"
		 ("beamer" "\\documentclass[presentation]{beamer}"
		  ("\\section{%s}" . "\\section*{%s}")
		  ("\\subsection{%s}" . "\\subsection*{%s}")
		  ("\\subsubsection{%s}" . "\\subsubsection*{%s}")))))

;; Make sure to eval the function when org-latex-classes list already exists
(with-eval-after-load 'ox-latex
  (my/setup-org-latex))

Keybindings & stuff

(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-imap :keymaps 'org-mode-map "RET" 'evil-org-return)
(general-nmap :keymaps 'org-mode-map "RET" 'org-ctrl-c-ctrl-c)

;; (my-leader-def "aa" 'org-agenda)
(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))))

(general-nmap :keymaps 'org-mode-map
    "C-x C-l" 'my/org-link-copy)

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* ((default-directory org-directory)
	 (project-files
	  (seq-filter
	   (lambda (f)
	     (and
	      (string-match-p (rx (* nonl) ".org" eos) f)
	      (not (string-match-p (rx (| "journal" "roam" "review" "archive")) f))))
	   (projectile-current-project-files))))
    (find-file
     (concat org-directory "/" (completing-read "Org file: " project-files)))))

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

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)
  (add-hook 'org-present-mode-hook
	    (lambda ()
	      (blink-cursor-mode 0)
	      (org-present-big)
	      ;; (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))))
  (add-hook 'org-present-mode-quit-hook
	    (lambda ()
	      (blink-cursor-mode 1)
	      (org-present-small)
	      ;; (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)))))

Tools

Various small packages.

TOC

Make a TOC inside the org file.

References:

(use-package org-make-toc
  :after (org)
  :if (not my/remote-server)
  :commands
  (org-make-toc
   org-make-toc-insert
   org-make-toc-set
   org-make-toc-at-point)
  :straight t)

Screenshots

A nice package to make screenshots and insert them to the Org document.

(use-package org-attach-screenshot
  :commands (org-attach-screenshot)
  :straight t)

System configuration

Functions used across my literate config files.

Tables for Guix Dependencies

A function to extract Guix dependencies from the org file.

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

Now, join the dependencies list to make it compatible with Scheme:

(defun my/format-guix-dependencies (&optional category)
  (mapconcat
   (lambda (e) (concat "\"" e "\""))
   (my/extract-guix-dependencies category)
   "\n"))

Noweb evaluations

Turn off eval confirmations for configuration files.

(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
   "-" 'dired-create-empty-file
   "~" 'vterm
   "<left>" 'dired-up-directory
   "<right>" 'dired-find-file
   "M-<return>" 'dired-open-xdg))

(defun my/dired-home ()
  "Open dired at $HOME"
  (interactive)
  (dired (expand-file-name "~")))

(my-leader-def
  "ad" #'dired
  "aD" #'my/dired-home)

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

Reuse the current dired buffer instead of spamming new ones. Looks like not neccesary 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/lowpower 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--refresh-advice)
  (advice-add 'dired-remove-entry :around #'all-the-icons-dired--refresh-advice)
  (advice-add 'dired-kill-subdir :around #'all-the-icons-dired--refresh-advice))

Provides stuff like dired-open-xdg

(use-package dired-open
  :straight t
  :commands (dired-open-xdg))

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

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:

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)
  :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
  :config
  (setq eshell-highlight-prompt nil)
  (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))

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

Managing dotfiles

A bunch of functions for managing dotfiles with yadm.

Open Emacs config

(defun my/edit-configuration ()
  "Open the init file."
  (interactive)
  (find-file "~/Emacs.org"))

;; (defun my/edit-exwm-configuration ()
;;   "Open the exwm config file."
;;   (interactive)
;;   (find-file "~/.emacs.d/exwm.org"))

(general-define-key "C-c c" 'my/edit-configuration)
;; (general-define-key "C-c C" 'my/edit-exwm-configuration)
(my-leader-def
  :infix "c"
  "" '(:which-key "configuration")
  "c" 'my/edit-configuration)

Open Magit for yadm

Idea:

(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)
(my-leader-def "cf" 'my/open-yadm-file)

Internet & Multimedia

Notmuch

My notmuch config now resides in Mail.org.

(unless (or my/is-termux my/remote-server)
  (load-file (expand-file-name "mail.el" user-emacs-directory)))

Elfeed

elfeed is an Emacs RSS client.

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)
  :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
  `((t :foreground ,(doom-color 'red)))
  "Face for the elfeed entries with tag \"videos\"")

(defface elfeed-twitter-entry
  `((t :foreground ,(doom-color 'blue)))
  "Face for the elfeed entries with tah \"twitter\"")

(defface elfeed-emacs-entry
  `((t :foreground ,(doom-color 'magenta)))
  "Face for the elfeed entries with tah \"emacs\"")

(defface elfeed-music-entry
  `((t :foreground ,(doom-color 'green)))
  "Face for the elfeed entries with tah \"music\"")

(defface elfeed-podcasts-entry
  `((t :foreground ,(doom-color 'yellow)))
  "Face for the elfeed entries with tag \"podcasts\"")

(defface elfeed-blogs-entry
  `((t :foreground ,(doom-color 'orange)))
  "Face for the elfeed entries with tag \"blogs\"")

(with-eval-after-load 'elfeed
  (setq elfeed-search-face-alist
	'((twitter elfeed-twitter-entry)
	  (podcasts elfeed-podcasts-entry)
	  (music elfeed-music-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-score

elfeed-score is a package that implements scoring for the elfeed entries. Entries are scored by a set of rules for tags/title/content/etc and sorted by that score.

(defun my/elfeed-toggle-score-sort ()
  (interactive)
  (setq elfeed-search-sort-function
	(if elfeed-search-sort-function
	    nil
	  #'elfeed-score-sort))
  (message "Sorting by score: %S" (if elfeed-search-sort-function "ON" "OFF"))
  (elfeed-search-update--force))

(use-package elfeed-score
  :straight t
  :after (elfeed)
  :init
  (setq elfeed-score-serde-score-file "~/.emacs.d/elfeed.score")
  :config
  (elfeed-score-enable)
  (setq elfeed-search-print-entry-function #'elfeed-score-print-entry)
  (general-define-key
   :states '(normal)
   :keymaps '(elfeed-search-mode-map)
   "=" elfeed-score-map)
  (general-define-key
   :keymaps '(elfeed-score-map)
   "=" #'my/elfeed-toggle-score-sort))
YouTube & 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”.

The following function converts URLs from Invidious and the like to YouTube.

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

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

(with-eval-after-load 'emms
  (define-emms-source elfeed (entry)
    (let ((track (emms-track
		  'url (my/get-youtube-url (elfeed-entry-link entry)))))
      (emms-track-set track 'info-title (elfeed-entry-title entry))
      (emms-playlist-insert-track track))))

(defun my/elfeed-add-emms-youtube ()
  (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-youtube))

EMMS

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

References:

(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)
  (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
		 (emms-player-simple-regexp
		  "m3u" "ogg" "flac" "mp3" "wav" "mod" "au" "aiff"))

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

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)
  :init
  (my-leader-def "ai" #'erc-tls)
  :config
  ;; Logging
  (setq erc-log-channels-directory "~/.erc/logs")
  (setq erc-save-buffer-on-part t)
  ;; Config of my ZNC instance.
  (setq erc-server "sqrtminusone.xyz")
  (setq erc-port 1984)
  (setq erc-nick "sqrtminusone")
  (setq erc-user-full-name "Pavel Korytov")
  (setq erc-password (password-store-get "Selfhosted/ZNC"))
  (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
  :after (erc))

Google Translate

Emacs interface to Google Translate.

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

References:

(use-package google-translate
  :straight t
  :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)

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)
  :init
  (my-leader-def
    "he" #'devdocs-lookup
    "hE" #'devdocs-install))

Utilities

pass

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

Although I use this rofi frontend for actually inserting passwords.

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

Docker

A package to manage docker containers from Emacs.

The file progidy-config.el sets variable my/docker-directories, which allows to

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

By default, docker commands are run in default-directory. Even worse, transient doesn’t allow to set default-directory temporarily, via let. But often I don’t want to change default-directory of a buffer (e.g. via Dired) to run a command from there.

So I decided to implement the following advice:

(setq my/selected-docker-directory nil)

(defun my/docker-override-dir (fun &rest args)
  (let ((default-directory (or my/selected-docker-directory default-directory)))
    (setq my/selected-docker-directory nil)
    (apply fun args)))

It overrides default-directory for the first launch of a function. Now, add the advice to the required functions from docker.el:

(with-eval-after-load 'docker
  (advice-add #'docker-compose-run-docker-compose-async :around #'my/docker-override-dir)
  (advice-add #'docker-compose-run-docker-compose :around #'my/docker-override-dir)
  (advice-add #'docker-run-docker-async :around #'my/docker-override-dir)
  (advice-add #'docker-run-docker :around #'my/docker-override-dir))

And here is a function which prompts the user for the directory. File progidy-config.el sets an alist of possible directories, look the section about progidy.

(defun my/docker-from-dir ()
  (interactive)
  (when (not (boundp 'my/docker-directories))
    (load (concat user-emacs-directory "prodigy-config")))
  (let* ((directories
	  (mapcar
	   (lambda (el) (cons (format "%-30s %s" (car el) (cdr el)) (cdr el)))
	   my/docker-directories))
	 (selected-directory
	  (cdr (assoc (completing-read "Docker: " directories nil nil "^")
		      directories))))
    (setq my/selected-docker-directory selected-directory)
    (docker)))

(my-leader-def "aO" 'my/docker-from-dir)

Progidy

prodigy.el is a package to run various services. I’ve previously used tmuxp + tmux, but want to try this as well.

The actual service definitions are in the ~/.emacs.d/prodigy.org, which tangles to prodigy-config.el. Both files are encrypted in yadm, as they contain personal data.

(use-package prodigy
  :straight t
  :commands (prodigy)
  :init
  (my-leader-def "aP" 'prodigy)
  :config
  (when (not (boundp 'my/docker-directories))
    (load (concat user-emacs-directory "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 :files ("screenshot.el") :commit "f8204e82dc0c1158c401735d36a143e6f6d24cf5")
  :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))

Productivity

pomm

My package for doing Pomodoro timer.

(use-package pomm
  ;; :straight (:host github :repo "SqrtMinusOne/pomm.el" :files (:defaults "resources"))
  :straight (:local-repo "~/Code/Emacs/pomm" :files (:defaults "resources"))
  :commands (pomm)
  :init
  (my-leader-def "ap" #'pomm)
  :config
  (setq alert-default-style 'libnotify)
  (add-hook 'pomm-on-tick-hook 'pomm-update-mode-line-string)
  (add-hook 'pomm-on-status-changed-hook 'pomm-update-mode-line-string))

OFF (OFF) Pomidor

A simple pomodoro technique timer.

Disabled it in favour of my own package.

(use-package pomidor
  :straight t
  :disabled
  :commands (pomidor)
  :init
  (my-leader-def "aP" #'pomidor)
  :config
  (setq pomidor-sound-tick nil)
  (setq pomidor-sound-tack nil)
  (general-define-key
   :states '(normal)
   :keymaps 'pomidor-mode-map
   (kbd "q") #'quit-window
   (kbd "Q") #'pomidor-quit
   (kbd "R") #'pomidor-reset
   (kbd "h") #'pomidor-hold
   (kbd "H") #'pomidor-unhold
   (kbd "RET") #'pomidor-stop
   (kbd "M-RET") #'pomidor-break))

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

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

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

Guix settings

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

(my/format-guix-dependencies)
(specifications->manifest
 '("emacs-native-comp"
   <<packages()>>))